Set up the publisher SDK
Use this guide to add your first Specify placement. You will install the SDK, initialize one client, connect your consent flow, and call serve().
Use the setup wizard: The wizard runs through the coding-agent CLI you already use, including Claude Code, Codex, Cursor, Gemini CLI, opencode, and others. It inspects your codebase, proposes an integration plan for your approval, implements the plan as a reviewable diff, and verifies the result. If it cannot find an agent CLI, it prints the complete manual instructions.
npx @specify-sh/wizard --publisherThe manual steps below remain the canonical setup path.
Manual setup
1. Install the SDK
npm install @specify-sh/sdk2. Initialize the client
Import Specify, then create one client at module scope. Use the publisher key from your environment rather than creating a client during each render.
const specify = new Specify({
publisherKey: 'spk_1234567890abcdef1234567890abcdef'
});3. Connect consent
If your site has a consent management platform, connect its targeted-advertising state to the SDK on every page load. The SDK does not persist consent. Ads can still serve from wallet addresses without this signal, but Enhanced Tracking requires it.
cmp.onConsentChange((consent) => {
if (consent.targetedAdvertising) {
specify.consentForEnhancedTracking();
} else {
specify.revokeEnhancedTrackingConsent();
}
});4. Make the first serve() call
This complete example includes the import and client initialization so you can run it in a new file. Pass an EVM address that the user controls and choose the image format that matches the placement.
import Specify, { ImageFormat } from '@specify-sh/sdk';
// Initialize the SDK
const specify = new Specify({
publisherKey: 'spk_your_publisher_key_here'
});
// Serve an ad to a wallet address
try {
const ad = await specify.serve('0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', {
imageFormat: ImageFormat.LANDSCAPE
});
if (ad) {
console.log('Headline:', ad.headline);
console.log('Content:', ad.content);
console.log('Image URL:', ad.imageUrl);
console.log('CTA:', ad.ctaLabel, '->', ad.ctaUrl);
} else {
console.log('No ad found for this wallet');
}
} catch (error) {
console.error('Error serving ad:', error);
}serve() resolves to null when no matching ad is available. Treat that as a normal result and render nothing or your own fallback. Label every ad as sponsored content and use the returned CTA URL without changing it.
Integration examples
A wallet-connected dapp
import Specify, { ImageFormat, type Address } from '@specify-sh/sdk';
const specify = new Specify({
publisherKey: process.env.NEXT_PUBLIC_SPECIFY_PUBLISHER_KEY!
});
cmp.onConsentChange((consent) => {
consent.targetedAdvertising
? specify.consentForEnhancedTracking()
: specify.revokeEnhancedTrackingConsent();
});
onWalletConnect((addresses) => {
specify.identify(addresses);
renderWalletPlacement(addresses);
});
async function renderWalletPlacement(addresses: Address[]) {
const ad = await specify.serve(addresses, {
imageFormat: ImageFormat.LANDSCAPE,
adUnitId: 'swap-card'
});
render(
ad ? (
<aside>
<span>Sponsored</span>
<a href={ad.ctaUrl}>{ad.headline}</a>
</aside>
) : null
);
}Create the client once at module scope. Connect your CMP on every page load, then call identify(addresses) as soon as the wallet connects. Pass those addresses to the placement’s serve() call. If serve() returns null, render nothing.
A site without wallet connection
A blog, explorer, or docs site does not need wallet UI. Connect the consent banner and use the options-only form of serve().
import Specify, { ImageFormat } from '@specify-sh/sdk';
const specify = new Specify({
publisherKey: process.env.NEXT_PUBLIC_SPECIFY_PUBLISHER_KEY!
});
consentBanner.onChange((consent) => {
consent.targetedAdvertising
? specify.consentForEnhancedTracking()
: specify.revokeEnhancedTrackingConsent();
});
const ad = await specify.serve({
imageFormat: ImageFormat.LONG_BANNER,
adUnitId: 'article-footer'
});
render(
ad ? (
<aside>
<span>Sponsored</span>
<a href={ad.ctaUrl}>{ad.headline}</a>
</aside>
) : null
);With consent, Specify uses its consent-gated cookie to recognise a returning visitor and serve a wallet-targeted ad. With no consent and no known wallets, serve() resolves to null without making a network request.
Network requirements
The SDK sends ad requests to https://spfsrv.com/api/ads. After consent, the browser sends these requests with credentials. If your site has a Content Security Policy, add https://spfsrv.com to connect-src.
Send SDK traffic straight to Specify. Do not proxy it through your backend because a proxy strips the identity cookie.
For every option, method, return field, validation rule, and error type, see the SDK reference.