Skip to Content
PublishingSDK reference - v1.0.0

SDK reference

The Specify JavaScript/TypeScript SDK serves personalized ads for Ethereum and EVM-compatible wallet addresses.

Current version: v1.0.0

npm install @specify-sh/sdk

Follow the setup guide for a first integration. v1 adds automatic wallet detection, identify(), a consent gate, and Enhanced Tracking . See Migrating from v0.4.x or the v0.4.x reference for existing integrations. You can also install the SDK through Google Tag Manager.

Constructor

Creates a new Specify client instance.

const specify = new Specify(config: SpecifyInitConfig);

Parameters

ParameterTypeRequiredDescription
config.publisherKeystringYesYour publisher API key. Must start with spk_ and be exactly 34 characters
config.privacy.disableWalletDetectionbooleanNoTurns off automatic wallet detection. Defaults to false

Create one client at module scope and reuse it. Do not create one per render.

Example

const specify = new Specify({ publisherKey: 'spk_1234567890abcdef1234567890abcdef' });

Deprecated options

ParameterTypeDescription
config.cacheMostRecentAddressbooleanDeprecated. Cached the most recent wallet in browser storage so serve() could still fill after a disconnect. The option remains accepted and works as described here, but Enhanced Tracking  supersedes it with server-side recognition that survives cache clears. Browser-only. Has no effect on server-side calls

serve()

Serves targeted advertising content for one or more wallet addresses. There are two forms:

// Wallets first. Works everywhere, including server-side serve( addressOrAddresses: Address | Address[] | undefined | null, options: ServeOptions ): Promise<SpecifyAd | null> // Options only. The recommended browser form serve(options: ServeOptions): Promise<SpecifyAd | null>

The options-only form uses all visitor data the client already knows. This includes addresses registered with identify(), addresses found by automatic wallet detection, and the Enhanced Tracking  cookie after consent. Use this form when the placement has no address to pass.

You can mix both forms across placements. The SDK combines explicit addresses with the ones it already knows, deduplicates them case-insensitively, and caps the request at 50.

Browser recognition works only with client-side serve() because the identity cookie lives in the visitor’s browser. Server-side serving can target passed wallet addresses, but it cannot recognise the browser and makes geo compliance harder, so talk to Specify before using it.

Parameters

ParameterTypeRequiredDescription
addressOrAddressesAddress | Address[] | undefined | nullNoA single wallet address or array of addresses for the same user. Omit the argument entirely for the options-only form
options.imageFormatImageFormatYesRequired image format for the placement. Specify matches the best ad available in that format. See formats
options.adUnitIdstringNoIdentifier for this specific placement. Used for per-placement performance analytics and A/B testing. Learn more

Returns

Promise<SpecifyAd | null>. The promise resolves to the ad content object, or null if no matching ad is available.

Examples

Wallet address and placement ID

import { ImageFormat } from '@specify-sh/sdk'; const ad = await specify.serve( '0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', { imageFormat: ImageFormat.LANDSCAPE, adUnitId: 'homepage-hero-banner' } );

When one user has multiple wallets, pass every address you know in the first argument to improve targeting and conversion attribution. See passing multiple wallets.

No address (browser)

Drop the address argument on pages where you don’t have one. The SDK serves against identified wallets, detected wallets, and the Enhanced Tracking cookie.

const ad = await specify.serve({ imageFormat: ImageFormat.LONG_BANNER, adUnitId: 'header-banner' });

On a page where the SDK has no wallets and Enhanced Tracking consent hasn’t been granted, serve() resolves to null without making a network request.


identify()

Registers wallet address(es) your app already knows about, so they’re merged into every later serve() call.

identify(addressOrAddresses: Address | Address[]): void

Call it when your app learns the user’s address outside a render, such as in a wallet connect callback, restored session, or WalletConnect pairing that detection can’t see. This saves you from passing the address through every ad slot.

onWalletConnect((addresses) => specify.identify(addresses));

Calling identify() again adds addresses to the existing set. The SDK stores them lowercased and removes duplicates. It throws ValidationError for a malformed address.


You must send an explicit consent signal before Enhanced Tracking  can run. Until then, the SDK sends ad requests without credentials. It neither sends nor sets a cookie.

consentForEnhancedTracking(): void revokeEnhancedTrackingConsent(): void hasEnhancedTrackingConsent(): boolean
MethodWhat it does
consentForEnhancedTracking()Grants consent on this client. Ad requests are sent credentialed, so the Specify identity cookie is attached. No-op outside the browser
revokeEnhancedTrackingConsent()Withdraws it. Later requests omit credentials; ads continue to serve from wallet addresses alone
hasEnhancedTrackingConsent()true when requests will be sent credentialed

The SDK never persists consent. Consent lives on the client instance for the lifetime of the page, leaving your CMP as the source of truth. Grant consent on every page load where the user has agreed. A withdrawal in the CMP then takes effect on the next pageview.

cmp.onConsentChange((consent) => { if (consent.targetedAdvertising) { specify.consentForEnhancedTracking(); } else { specify.revokeEnhancedTrackingConsent(); } });

The cookie is server-only. The SDK never reads, writes, or sees its value. Ads serve with or without consent. Consent only enables recognition on pages where no wallet is connected.


Automatic wallet detection

In the browser, the SDK passively discovers wallets already connected to the page and includes them in every request. It’s on by default and requires no code.

  • It never prompts. Detection calls the silent eth_accounts only, never eth_requestAccounts. It cannot open a wallet, request permissions, or connect anything the user hasn’t already connected.
  • It uses EIP-6963 provider discovery, with a window.ethereum fallback, so multi-wallet browsers work correctly.
  • The SDK cannot see WalletConnect v2 sessions because they don’t inject a provider into the page. Pass those addresses to identify() or serve().
getDetectedWallets(): Address[]

Returns the addresses detection currently sees, lowercased. Empty when detection is disabled or when running outside a browser.

To turn detection off, use this option. Addresses passed to serve() and identify() still work:

const specify = new Specify({ publisherKey: 'spk_1234567890abcdef1234567890abcdef', privacy: { disableWalletDetection: true } });

destroy()

Releases the client’s resources: stops wallet detection, detaches its listeners, clears identified addresses, and resets consent.

destroy(): void

Most integrations don’t need this because a module-scope client lives as long as the page. If you create a client per view in a single-page app, call it when that view unmounts. Do not reuse the instance afterward.

useEffect(() => () => specify.destroy(), []);

Image format options

The SDK supports four ad formats to match common layout patterns:

FormatAspect ratioResolutionUse case
LANDSCAPE16:9640×360Hero banners, featured placements
LONG_BANNER8.09:11456×180Header/footer placements, leaderboards
SHORT_BANNER16:5640×200Inline content, sidebars, mobile banners
NO_IMAGENot applicableNot applicableText-only ads: headline, content, and CTA with no creative image

See the Placements page for format guidance and native placement examples beyond simple banners.


Return type: SpecifyAd

A successful serve() call returns a SpecifyAd object:

PropertyTypeDescription
walletAddressstringThe wallet address that matched for this ad
campaignIdstringUnique identifier for the ad campaign
adIdstringUnique identifier for this specific ad
headlinestringAd headline text (plain text, no markdown)
contentstringAd body content. Supports simplified markdown. Max 400 characters
ctaUrlstringCall-to-action URL
ctaLabelstringCall-to-action button text (plain text, no markdown)
imageUrlstring | nullURL to the ad image (null for NO_IMAGE placements)
communityNamestringName of the advertising community (plain text, no markdown)
communityLogostringURL to the community logo
imageFormatImageFormatThe format returned (LANDSCAPE, LONG_BANNER, SHORT_BANNER, or NO_IMAGE)
adUnitIdstring?Echoes the adUnitId you passed to serve(), when you set one

The response provides the headline, content, CTA, and community branding as separate fields. Use the fields you need to build a native placement instead of displaying only the image.

Content formatting

The content field supports a simplified markdown subset:

FormatSyntaxExample
Bold**text****Join today**
Italic*text**Italic*
Underline__text____Out now__
Bullet points* item* Feature 1\n* Feature 2
Line breaks\nFirst line\nSecond line

Validation rules

Publisher key

  • Must start with spk_
  • Must be exactly 34 characters long
  • Example: spk_1234567890abcdef1234567890abcdef

Wallet addresses

  • Must be valid Ethereum / EVM-compatible addresses
  • Must start with 0x followed by 40 hexadecimal characters
  • Case-insensitive
  • Example: 0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510

Address limits per serve() call

  • A browser call can omit addresses. The SDK fills in addresses it already knows.
  • Each request accepts at most 50 addresses. Passing more than 50 explicitly throws ValidationError. The SDK drops inferred addresses at the cap instead of throwing.
  • The SDK removes duplicates case-insensitively.

Error handling

When Specify has no relevant ad, serve() resolves to null. A thrown error means the request failed. Wrap SDK calls in try/catch and branch on the error classes exported by the SDK.

import { AuthenticationError, ValidationError, APIError, ImageFormat } from '@specify-sh/sdk'; try { const ad = await specify.serve(walletAddress, { imageFormat: ImageFormat.LANDSCAPE, adUnitId: 'main-content-ad' }); if (ad) { displayAd(ad); } else { // No-fill is normal and frequent by design. Render nothing (or your own fallback). showFallbackContent(); } } catch (error) { if (error instanceof ValidationError) { console.error('Invalid input:', error.message); } else if (error instanceof AuthenticationError) { console.error('Authentication failed. Check your publisher key.'); } else if (error instanceof APIError) { console.error('API error:', error.message); showErrorMessage(); } else { console.error('Unexpected error:', error); } }

Error reference

ErrorWhen it’s thrown
ValidationErrorInvalid input, such as a malformed address, bad publisher key format, or wrong options
AuthenticationErrorPublisher key is rejected by the API
APIErrorUnexpected API-side or network error. Retry or show a fallback

The no-ad case never throws. serve() resolves to null, which your if (ad) branch can handle. The SDK still exports NotFoundError for backwards compatibility but no longer throws it. Treat null as a normal, frequent outcome.

Last updated on