Next.js
Add Specify to a Next.js app.
Set up with the wizard
npx @specify-sh/wizard --publisherThe wizard runs through the coding agent CLI you already use (Claude Code, Codex etc.). It inspects your codebase, proposes an integration plan for your approval, implements it as a reviewable diff, and verifies the result.
Prefer to do it by hand? The manual steps below are the same setup.
Manual setup
Next.js can serve ads two ways:
| Where you serve | Import | Identity comes from |
|---|---|---|
| Client component | @specify-sh/publisher-sdk | The visitor's cookie, plus wallets you pass |
| Server component, route handler, server action (Not recommended) | @specify-sh/publisher-sdk/server | Wallets you pass |
Only the browser can hold the Specify cookie, so only client components can fill a placement for a visitor who has no wallet connected. We recommend that you serve from the client when possible as this will give you higher fill rates.
1. Install
npm i @specify-sh/publisher-sdk2. Set your keys
You will need a publisher key. See Publisher keys.
The environment variable needs a NEXT_PUBLIC_ variable which is fine: publisher keys are public by design.
NEXT_PUBLIC_SPECIFY_PUBLISHER_KEY=spk_your_key_here3. Serve an ad
Create the client in its own module and import it where you need it.
import Specify from '@specify-sh/publisher-sdk';
export const specify = new Specify({
publisherKey: process.env.NEXT_PUBLIC_SPECIFY_PUBLISHER_KEY!
});serve() is async, so it belongs in an useEffect.
'use client';
import { useEffect, useState } from 'react';
import { ImageFormat, type Address, type SpecifyAd } from '@specify-sh/publisher-sdk';
import { specify } from '@/lib/specify';
export function AdSlot({ wallets = [] }: { wallets?: Address[] }) {
const [ad, setAd] = useState<SpecifyAd | null>(null);
const key = wallets.join(',');
useEffect(() => {
let active = true;
specify
.serve(wallets, { imageFormat: ImageFormat.LANDSCAPE, adUnitId: 'sidebar' })
.then((result) => {
if (active) setAd(result);
});
return () => {
active = false;
};
}, [key]);
if (!ad) return null;
return (
<a href={ad.ctaUrl} target="_blank" rel="noopener noreferrer">
{ad.imageUrl && <img src={ad.imageUrl} alt="" />}
<h3>{ad.headline}</h3>
<p>{ad.content}</p>
<span>{ad.ctaLabel}</span>
</a>
);
}Pass a wallet address that the user controls and choose the image format that matches your ad placement.
Available image formats and the fields returned in the ad can be found in the Browser SDK reference.
4. Connect consent
Wire your consent platform to the SDK, high in the component tree. Consent starts false and the SDK never stores it.
'use client';
import { useEffect } from 'react';
import { specify } from '@/lib/specify';
export function ConsentBridge() {
useEffect(() => cmp.onChange((consent) => {
specify.setCookieConsent(consent.targetedAdvertising);
}), []);
return null;
}With consent granted, Specify recognises returning visitors across every site in our network, so a placement can fill even when nobody has connected a wallet to your app.
5. Register connected wallets
If your app has a wallet connection flow, notify the SDK when a wallet is connected. Every later serve() includes it, so components deeper in the tree do not need the address passed down.
'use client';
import { useEffect } from 'react';
import { useAccount } from 'wagmi';
import { specify } from '@/lib/specify';
export function WalletBridge() {
const { address } = useAccount();
useEffect(() => {
if (address) specify.identify(address);
}, [address]);
return null;
}With this in place, an ad slot needs no wallet address at all.
specify.serve({ imageFormat: ImageFormat.LANDSCAPE, adUnitId: 'sidebar' })Serving from a server component
import { serve, ImageFormat } from '@specify-sh/publisher-sdk/server';
export default async function Page() {
const ad = await serve({
publisherKey: process.env.SPECIFY_PUBLISHER_KEY!,
walletAddresses: ['0x1111111111111111111111111111111111111111'],
imageFormat: ImageFormat.LANDSCAPE,
adUnitId: 'sidebar'
});
if (!ad) return null;
return <a href={ad.ctaUrl}>{ad.headline}</a>;
}walletAddresses is required here.
Importing the wrong entrypoint
Importing @specify-sh/publisher-sdk from a server component fails immediately, with a message telling you to use @specify-sh/publisher-sdk/server.