Skip to Content
PublishingEnhanced Tracking

Enhanced Tracking

Specify has always matched ads to people using their onchain activity — no pixels, no profiles, no third-party data brokers. Until now, that came with one hard limitation: if a visitor didn’t have a wallet connected on your page, Specify couldn’t serve them anything.

Enhanced Tracking removes that limitation. Specify can now recognise a returning visitor across the network and serve them a relevant, wallet-targeted ad even on pages where no wallet is connected — and it does so with a strict, publisher-controlled consent gate.

What this means for you

More of your inventory earns. Blog posts, docs, explorers, landing pages, logged-out states — surfaces where there’s no wallet UX at all can now serve targeted ads, as long as that visitor has connected a wallet somewhere on the Specify network before. Pageviews that previously returned “no ad” become fillable.

The relevance doesn’t get watered down. This is not contextual filler or lookalike guesswork. Every ad served through Enhanced Tracking is still targeted on real onchain behaviour — the same targeting quality your wallet-connected placements get today. Specify still serves at most one ad, still serves nothing rather than something irrelevant, and moderation still works exactly as it does now.

Recognition survives the messy reality of browsing. The connection between a browser and its wallets is durable. If a user clears their cookies or shows up on a new device, the link re-establishes itself the next time they connect a wallet anywhere on the network. You don’t lose the audience every time a cookie dies.

Better measurement means better campaigns on your inventory. Enhanced Tracking gives advertisers a complete view from impression to onchain conversion — including impressions that happened on wallet-less pages. Advertisers who can see what’s working spend more confidently, which flows through to publisher earnings.

What stays exactly the same

  • Your responsibilities. Render the ad, label it “Sponsored.” That’s still the whole job. No targeting code, no tracking pixels, no moderation logic on your side.
  • The no-ad case. Specify still never serves filler. When there’s nothing relevant, you render nothing.
  • Payments and reporting. No changes to how you get paid.
  • Users who don’t consent. For visitors who decline the consent prompt, everything behaves exactly as it does today: wallet connected → targeted ad; no wallet → no ad. Nothing breaks, nothing degrades below current behaviour.

Enhanced Tracking was designed consent-first, not consent-bolted-on:

  • One cookie, set server-side, on Specify’s own dedicated domain — not on yours. There is no fingerprinting, no probabilistic device matching, and no reading of anything else in the visitor’s browser.
  • The SDK ships a consent gate. The cookie is only sent or set after the user consents. You wire the gate to your existing consent banner / CMP — Specify doesn’t decide consent for you.
  • Consent governs the cookie and only the cookie. Declining consent doesn’t punish the user or the publisher: wallet-based serving continues as today. There is no dark-pattern incentive to force the prompt.
  • Wallet detection is passive and silent. The SDK reads wallets that are already connected to the page and never prompts, never opens a wallet, and never requests permissions. You can turn it off entirely — see Automatic wallet detection.
  • The wallet remains the identity. Enhanced Tracking never invents new data about a user — it only connects a browser to wallets that user has already, voluntarily connected on the network.

This design maps cleanly onto GDPR/ePrivacy consent requirements: the only stored identifier requiring consent is the cookie, and its purpose (cross-site ad targeting) is exactly what your consent prompt discloses.


For developers

Enhanced Tracking arrives with SDK v1.0.0, a new major version of @specify-sh/sdk. If you’ve integrated v0.4.x, the mental model is unchanged — one client, serve(), render or render nothing — with a handful of additions. Upgrading is covered step by step in Migrating from v0.4.x; the full method-by-method detail lives in the SDK Reference.

const specify = new Specify({ publisherKey: process.env.SPECIFY_PUBLISHER_KEY }); // Call when your CMP / consent banner reports consent for advertising cookies. specify.consentForEnhancedTracking();

Until consent is given, the SDK makes uncredentialed requests: no cookie is sent, no cookie is set, and serving behaves exactly like v0.4.x. After consent, requests are credentialed and the Specify cookie rides along automatically. You never touch the cookie yourself.

Consent is not persisted by the SDK. It lives on the client instance for the lifetime of the page, which keeps your CMP the single source of truth: call the gate on every page load where the user has consented, and a withdrawal in your CMP takes effect on the very next pageview. Wire both directions:

cmp.onConsentChange((consent) => { if (consent.targetedAdvertising) { specify.consentForEnhancedTracking(); } else { specify.revokeEnhancedTrackingConsent(); } }); specify.hasEnhancedTrackingConsent(); // -> boolean

2. serve() without a wallet

The big unlock. On v1.0.0, serve() works on any page, whether or not the visitor has a wallet connected — no configuration flag needed. In the browser, the cleanest form is options-only: the SDK serves against everything it already knows about this visitor (identified wallets, detected wallets, and the Enhanced Tracking cookie once consent is granted).

// A page with no wallet UX at all: const ad = await specify.serve({ imageFormat: ImageFormat.LONG_BANNER, adUnitId: 'blog-footer', }); // Returns an ad if Specify recognises this browser, null otherwise.

The wallets-first form is unchanged and still works everywhere, including with an empty array:

const ad = await specify.serve([], { imageFormat: ImageFormat.LONG_BANNER, adUnitId: 'blog-footer', });

When the visitor does have connected wallets, keep passing them all, exactly as today — wallets are still the strongest signal and they’re what heals recognition across devices and cleared cookies:

const ad = await specify.serve(addresses, { imageFormat: ImageFormat.LANDSCAPE, adUnitId: 'swap-card', });

One useful detail for planning your slots: on a page with no wallets and no consent, serve() doesn’t even make a network request — it resolves to null immediately. Nothing to optimise around, nothing to wait on.

3. identify()

An explicit way to tell Specify about a wallet connection at the moment it happens, outside of an ad serve — for example on pages where a user connects but no placement renders:

// On wallet connect: specify.identify(connectedAddresses);

Identified addresses accumulate on the client and are merged into every later serve() call, so you don’t have to thread the user’s wallet down to each ad slot. This strengthens recognition for every future pageview, on your site and across the network.

4. Automatic wallet detection

In the browser, the SDK passively discovers wallets that are already connected to the page and folds them into every request. It’s on by default, and it is deliberately unobtrusive:

  • It never prompts. Detection only ever calls the silent eth_accounts, never eth_requestAccounts. It cannot open a wallet popup, cannot request permissions, and cannot connect anything the user hasn’t already connected.
  • It uses EIP-6963 provider discovery, with a window.ethereum fallback, so multi-wallet browsers are handled correctly.
  • It sees nothing the page can’t already see. Addresses are normalised to lowercase and used for targeting only.
specify.getDetectedWallets(); // -> addresses found on this page, lowercased

Turn it off with one flag if your privacy posture calls for it — addresses you pass to serve() and identify() still work with detection disabled:

const specify = new Specify({ publisherKey: process.env.NEXT_PUBLIC_SPECIFY_PUBLISHER_KEY, privacy: { disableWalletDetection: true }, });

One known gap: WalletConnect v2 sessions don’t inject a provider into the page, so remote wallets are invisible to detection. Pass those to identify() or straight to serve().

If you create a client per view in a single-page app, call specify.destroy() when that view unmounts. It stops detection and detaches its listeners; the instance shouldn’t be reused afterwards.

What changed under the hood

  • New serving domain. The SDK now talks to spfsrv.comhttps://spfsrv.com/api/ads — with credentials: 'include' once consent is granted. If you enforce a Content-Security-Policy, add https://spfsrv.com to connect-src. CORS is handled by Specify; your origin is reflected automatically.
  • Click redirects also move to https://spfsrv.com/r/…. Nothing to do — links returned in ctaUrl are already correct, and old-format links continue to 302.
  • Client-side calls matter more than ever. The cookie lives in the browser, so Enhanced Tracking only works for client-side serve() calls. Server-side serving still works for wallet-targeted ads but can’t participate in browser recognition (and, as today, makes geo compliance harder — talk to us first).
  • Don’t proxy SDK traffic through your own backend. A proxy strips the cookie and disables Enhanced Tracking.

Everything else is unchanged

ImageFormats, the SpecifyAd shape, simplified-markdown content, error classes, the no-ad case (serve() resolves to null — still a normal outcome), the 50-address limit, the “Sponsored” label requirement, test vs production keys — all identical to the current reference.

Quick integration sketch

import Specify, { ImageFormat } from '@specify-sh/sdk'; const specify = new Specify({ publisherKey: process.env.NEXT_PUBLIC_SPECIFY_PUBLISHER_KEY, }); // 1. Wire consent on every page load, from your CMP callback: onConsentChange((granted) => granted ? specify.consentForEnhancedTracking() : specify.revokeEnhancedTrackingConsent(), ); // 2. Tell Specify about wallet connects as they happen: onWalletConnect((addresses) => specify.identify(addresses)); // 3. Serve everywhere — the SDK uses whatever it knows about this visitor: const ad = await specify.serve({ imageFormat: ImageFormat.SHORT_BANNER, adUnitId: 'docs-sidebar', });

Render the result with a “Sponsored” label; render nothing on null. Same as it ever was — just on far more of your pages.

Installing through a tag manager instead of npm? The same feature set is available through the Google Tag Manager loader.

Last updated on