Migrating from v0.4.x
Move an existing publisher integration from @specify-sh/sdk to @specify-sh/publisher-sdk v1.
Migrate with the wizard
npx @specify-sh/wizard --publisherThe wizard runs through the coding agent you already use (Claude Code, Codex etc.). It recognises a v0.4.x integration, proposes a migration plan from this guide and implements it as a reviewable diff.
Prefer to do it by hand? The steps below are the same migration.
Migrate manually
@specify-sh/sdk is deprecated. Its replacement is @specify-sh/publisher-sdk, which adds Enhanced Tracking: targeted ads for a visitor who has connected a wallet somewhere on the network, but not on the page they are looking at now.
This is a breaking change. Plan about an hour.
What changes
| v0.4.x | v1 | |
|---|---|---|
| Package | @specify-sh/sdk | @specify-sh/publisher-sdk |
| Endpoint | app.specify.sh/api/ads | spfsrv.com/v1/ads, called with credentials so the identity cookie can travel |
| Serving with no wallet | Only through cacheMostRecentAddress | Through the identity cookie, gated on consent |
| Consent | n/a | setCookieConsent(granted), hasCookieConsent() |
| Wallet registration | n/a | identify(addresses) |
| Server-side calls | Same client | Separate entry: @specify-sh/publisher-sdk/server |
serve() | serve(addresses, options) | Unchanged, plus serve(options) for the cookie-only case |
| Click redirect host | app.specify.sh/r/… | spfsrv.com/r/…. Old links redirect, and ctaUrl is always correct as returned |
What is removed
cacheMostRecentAddressand the browser cache behind it. The identity cookie replaces it, and resolves across the network rather than only your site.localId. The cookie is the durable handle now, so nothing is stored inlocalStorage.walletAddresson the returned ad. After a cookie-only serve it could name a wallet you never sent, so it is no longer returned.AuthenticationError,APIErrorandNotFoundError.ValidationErroris the only error the SDK throws. Everything from the request onward returnsnull.detailsonValidationError. v0.4.x carried a field-leveldetailsarray alongside the message. v1 does not, so readerror.message.
One field is narrowed rather than removed: imageUrl is typed string | null in v1, where v0.4.x typed it string even though a NO_IMAGE placement has no image. TypeScript will point at anywhere you hand it straight to an <img>.
1. Swap the package
npm uninstall @specify-sh/sdk
npm install @specify-sh/publisher-sdkThen update your imports. Nothing else about the import shape changes.
-import Specify, { ImageFormat } from '@specify-sh/sdk';
+import Specify, { ImageFormat } from '@specify-sh/publisher-sdk';2. Drop cacheMostRecentAddress
const specify = new Specify({
publisherKey: process.env.NEXT_PUBLIC_SPECIFY_PUBLISHER_KEY,
- cacheMostRecentAddress: true,
});The option no longer exists. Consent plus the identity cookie covers the same case, and covers it across every site in the network rather than only yours.
3. Wire the consent gate
The cookie is set on Specify's domain and is never read or set without your signal. Connect it to the advertising or targeting category in your consent platform:
cmp.onConsentChange((consent) => {
specify.setCookieConsent(consent.targetedAdvertising);
});Consent starts false, and the SDK never stores it, so set it on every page load and again whenever the visitor changes their mind. Without consent the SDK behaves as v0.4.x did: it serves against the wallet addresses you pass, and neither reads nor sets the cookie.
Use hasCookieConsent() to assert the state in tests.
4. Serve without addresses
Your existing calls keep working unchanged. What is new is that a placement with no wallet can now fill. serve() accepts null or undefined in the address position, so the call site does not have to branch on whether a wallet is connected:
const ad = await specify.serve(wallet ?? null, {
imageFormat: ImageFormat.LONG_BANNER,
adUnitId: 'header-banner'
});If you have no address at the call site at all, serve({ imageFormat }) is the same call without the first argument.
Keep passing every wallet you know about. Wallets remain the strongest targeting signal, and each serve that carries one strengthens the link used for later wallet-free serving.
5. Register wallets on connect
If people connect wallets on pages where no placement renders, tell Specify at the moment of connection:
onWalletConnect((addresses) => specify.identify(addresses));Registered addresses accumulate and ride along on every later serve(), so deeper placements do not need the address passed down.
6. Move server-side calls
Serving from a backend now uses its own entry point, and there is no client to construct:
import { serve, ImageFormat } from '@specify-sh/publisher-sdk/server';
const ad = await serve({
publisherKey: process.env.SPECIFY_PUBLISHER_KEY!,
walletAddresses: addresses,
imageFormat: ImageFormat.LANDSCAPE
});Cookies, consent and the identity cookie are browser features and do not apply, so walletAddresses is required. See the Node.js SDK.
7. Update CSP and remove any proxy
Requests go to https://spfsrv.com with credentials. If you set a Content-Security-Policy, add it to connect-src:
Content-Security-Policy: connect-src ... https://spfsrv.com;Specify reflects your origin and sets Allow-Credentials, so no proxy is needed. If you currently proxy SDK traffic through your backend, remove it: a proxy strips the cookie and disables Enhanced Tracking.
8. Test and ship
- Use your Development key to check that the integration builds and renders, as on v0.4.x.
- Switch to your production key before you test any cookie integrations as the Development key returns a sample ad and stops there.
- On the production key, check three states: wallet connected, no wallet with consent on a recognised browser, and no wallet without consent.
- Confirm your no-ad path still renders nothing, with no empty boxes or spinners.
Questions
-
Do I have to migrate? Eventually.
@specify-sh/sdkis deprecated and will not receive new features, though existing integrations keep serving. -
Does anything change about payments or reporting? No.
-
Does the ad response change?
walletAddressis gone andimageUrlis nowstring | null. Every other field, the simplified-markdowncontent, and the image formats are unchanged. -
Is the "Sponsored" label still my responsibility? Yes, on every placement, including wallet-free ones.
-
What is in the cookie? An encrypted identifier on Specify's domain. It carries no wallet address, and the SDK cannot read it. Specify does not fingerprint visitors. See On privacy and the Terms of Service.