Specify docs

Next.js

Add Specify to a Next.js app.

Set up with the wizard

npx @specify-sh/wizard --publisher

The 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 serveImportIdentity comes from
Client component@specify-sh/publisher-sdkThe visitor's cookie, plus wallets you pass
Server component, route handler, server action (Not recommended)@specify-sh/publisher-sdk/serverWallets 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-sdk

2. 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.

.env.local
NEXT_PUBLIC_SPECIFY_PUBLISHER_KEY=spk_your_key_here

3. Serve an ad

Create the client in its own module and import it where you need it.

lib/specify.ts
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.

components/AdSlot.tsx
'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.

Wire your consent platform to the SDK, high in the component tree. Consent starts false and the SDK never stores it.

components/ConsentBridge.tsx
'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.

components/WalletBridge.tsx
'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.

components/AdSlot.tsx
specify.serve({ imageFormat: ImageFormat.LANDSCAPE, adUnitId: 'sidebar' })

Serving from a server component

app/page.tsx
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.

Next steps

On this page