Google Tag Manager
If your site is instrumented through Google Tag Manager (or any tag manager that can fire a Custom HTML tag), you can run Specify without touching your codebase. The loader is the same SDK as the npm package, wrapped in a gtag-style command queue and served from Specify’s edge.
Current version: v1.0.0 — loader URL https://spfsrv.com/sdk/v1.js
GTM or npm?
| Google Tag Manager | npm (@specify-sh/sdk) | |
|---|---|---|
| Best for | Content sites, docs, blogs, marketing pages — anywhere a marketer owns the tags | Dapps and apps with a build step and a wallet layer |
| Install | Paste one Custom HTML tag | npm install, import, deploy |
| Wallet addresses | Auto-detected from the page; pass known ones with identify | Same, plus first-class serve(addresses, …) |
| Rendering | You write the DOM in the tag | Your framework’s components |
| Types | — | Full TypeScript types, error classes |
If you have a build step and a wallet integration, use the SDK — it’s easier to reason about and gives you the error classes. If you’re adding Specify to surfaces that don’t have wallet UX at all, GTM is usually the faster path. Both talk to the same edge, and both support Enhanced Tracking.
The install snippet
Paste this into a Custom HTML tag firing on All Pages (or directly into your page <head> if you’d rather not use a tag manager at all):
<script>window.specify=window.specify||function(){(window.specify.q=window.specify.q||[]).push(arguments)};</script>
<script async src="https://spfsrv.com/sdk/v1.js"></script>The first line is a stub that buffers commands into window.specify.q; the second loads the SDK asynchronously. When it arrives it swaps in the real dispatcher and drains the buffer in order. That means you can call specify(...) immediately, from the very next line or from another tag, without checking whether the SDK has finished downloading.
Loading the script twice is safe — the second copy detects the live dispatcher and does nothing.
Commands
Every interaction goes through the specify() function: a command name, then its arguments.
| Command | Arguments | Notes |
|---|---|---|
specify('init', config) | The same config object as new Specify(config) | Creates the singleton. A repeat init logs a warning and is ignored |
specify('consent') | — | Grants Enhanced Tracking consent |
specify('revokeConsent') | — | Withdraws it |
specify('identify', addresses) | An address, or an array of addresses | Merged into every later serve |
specify('serve', options, callback) | options is { imageFormat, adUnitId } | callback(ad, error) — see below |
init
specify('init', { publisherKey: 'spk_your_publisher_key_here' });config accepts everything the constructor does — including privacy: { disableWalletDetection: true } to turn off passive wallet detection. Keys are publishable and safe in browser code; see the SDK Reference for the full option list.
serve
specify('serve', { imageFormat: 'LANDSCAPE', adUnitId: 'header-banner-1' }, function (ad, error) {
if (error || !ad) return; // No fill, or a request error — render nothing
// render ad
});The callback receives (ad, error): on success ad is the ad object (or null when there’s no fill) and error is undefined; on failure ad is null and error is an Error. Treat both no-ad cases identically — render nothing. imageFormat is passed as a plain string ('LANDSCAPE', 'LONG_BANNER', 'SHORT_BANNER'), since there’s no enum to import here.
The loader’s serve uses the SDK’s options-only form: it serves against wallets detected on the page, anything passed to identify, and the Enhanced Tracking cookie once consent is granted. If a callback throws, the error is logged and the queue keeps running.
Ordering and buffering
GTM does not guarantee the order your tags fire in, so the loader buffers: consent, revokeConsent, identify and serve calls made before init are queued and replayed as soon as init runs. A serve tag that beats the init tag still works.
window.Specify
The class itself is exposed as window.Specify, so you can construct and hold your own instance if the command queue is too coarse for what you’re building. Most integrations never need this.
A complete Custom HTML tag
This tag initialises Specify, wires consent from a CMP’s dataLayer event, serves one ad, and renders it with a “Sponsored” label — and renders nothing at all when there’s no ad.
<div id="specify-ad"></div>
<script>window.specify=window.specify||function(){(window.specify.q=window.specify.q||[]).push(arguments)};</script>
<script async src="https://spfsrv.com/sdk/v1.js"></script>
<script>
specify('init', { publisherKey: 'spk_your_publisher_key_here' });
// Consent — from your CMP, on every page load. Most CMPs expose a callback
// like this one; adapt it to whatever yours provides.
window.myCmp.onConsentReady(function (consent) {
specify(consent.targetedAdvertising ? 'consent' : 'revokeConsent');
});
// If your site already knows the connected wallet, hand it over:
// specify('identify', '0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510');
specify('serve', { imageFormat: 'LONG_BANNER', adUnitId: 'gtm-header-banner' }, function (ad, error) {
var slot = document.getElementById('specify-ad');
if (!slot || error || !ad) {
return; // No ad: leave the slot empty — no box, no spinner, no error state
}
var label = document.createElement('span');
label.textContent = 'Sponsored';
label.style.fontSize = '11px';
label.style.textTransform = 'uppercase';
label.style.opacity = '0.6';
var link = document.createElement('a');
link.href = ad.ctaUrl; // Use exactly as returned — never modify or add params
link.target = '_blank';
link.rel = 'noopener noreferrer sponsored';
if (ad.imageUrl) {
var image = document.createElement('img');
image.src = ad.imageUrl; // May be an animated GIF
image.alt = ad.headline;
image.style.maxWidth = '100%';
link.appendChild(image);
}
var headline = document.createElement('h4');
headline.textContent = ad.headline;
var cta = document.createElement('span');
cta.textContent = ad.ctaLabel;
link.appendChild(headline);
link.appendChild(cta);
slot.appendChild(label);
slot.appendChild(link);
});
</script>Two rules carry over from every other integration: the “Sponsored” label is your responsibility on every placement, and the no-ad case is normal — Specify serves nothing rather than filler, so the slot must disappear cleanly. See Placements for how to size and position the slot.
Consent
Fire specify('consent') from your CMP, in the advertising / targeting-cookies category, on every page load where the user has consented. If your CMP doesn’t expose a callback but does push to the dataLayer, the equivalent is a second Custom HTML tag containing just specify('consent');, triggered on that CMP event — the command queue handles the ordering for you. The loader does not persist consent between pageviews — that’s deliberate, so your CMP stays the single source of truth and a withdrawal takes effect on the very next pageview. Use specify('revokeConsent') for the other direction.
Without consent, ads still serve. Consent only enables Enhanced Tracking: the server-side cookie that lets Specify recognise a returning visitor on pages where no wallet is connected. A visitor who declines still gets wallet-targeted ads wherever a wallet is present, exactly as before. There is no dark-pattern incentive to push the prompt, and no degraded experience for saying no.
Content-Security-Policy
If you enforce a CSP, the loader needs two directives — one to fetch the script, one for the ad requests it makes:
Content-Security-Policy: script-src 'self' https://spfsrv.com; connect-src 'self' https://spfsrv.com;Ad images and community logos are served from https://content.specify.sh and https://assets.specify.sh, so add those to img-src if your policy restricts images. CORS is handled on Specify’s side — your origin is reflected and Allow-Credentials is set — so no proxy is needed. Don’t add one: proxying strips the identity cookie and disables Enhanced Tracking.