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/sdkFollow 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
| Parameter | Type | Required | Description |
|---|---|---|---|
config.publisherKey | string | Yes | Your publisher API key. Must start with spk_ and be exactly 34 characters |
config.privacy.disableWalletDetection | boolean | No | Turns 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
| Parameter | Type | Description |
|---|---|---|
config.cacheMostRecentAddress | boolean | Deprecated. 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
| Parameter | Type | Required | Description |
|---|---|---|---|
addressOrAddresses | Address | Address[] | undefined | null | No | A single wallet address or array of addresses for the same user. Omit the argument entirely for the options-only form |
options.imageFormat | ImageFormat | Yes | Required image format for the placement. Specify matches the best ad available in that format. See formats |
options.adUnitId | string | No | Identifier 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[]): voidCall 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.
Consent
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| Method | What 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_accountsonly, nevereth_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.ethereumfallback, 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()orserve().
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(): voidMost 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:
| Format | Aspect ratio | Resolution | Use case |
|---|---|---|---|
LANDSCAPE | 16:9 | 640×360 | Hero banners, featured placements |
LONG_BANNER | 8.09:1 | 1456×180 | Header/footer placements, leaderboards |
SHORT_BANNER | 16:5 | 640×200 | Inline content, sidebars, mobile banners |
NO_IMAGE | Not applicable | Not applicable | Text-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:
| Property | Type | Description |
|---|---|---|
walletAddress | string | The wallet address that matched for this ad |
campaignId | string | Unique identifier for the ad campaign |
adId | string | Unique identifier for this specific ad |
headline | string | Ad headline text (plain text, no markdown) |
content | string | Ad body content. Supports simplified markdown. Max 400 characters |
ctaUrl | string | Call-to-action URL |
ctaLabel | string | Call-to-action button text (plain text, no markdown) |
imageUrl | string | null | URL to the ad image (null for NO_IMAGE placements) |
communityName | string | Name of the advertising community (plain text, no markdown) |
communityLogo | string | URL to the community logo |
imageFormat | ImageFormat | The format returned (LANDSCAPE, LONG_BANNER, SHORT_BANNER, or NO_IMAGE) |
adUnitId | string? | 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:
| Format | Syntax | Example |
|---|---|---|
| Bold | **text** | **Join today** |
| Italic | *text* | *Italic* |
| Underline | __text__ | __Out now__ |
| Bullet points | * item | * Feature 1\n* Feature 2 |
| Line breaks | \n | First 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
0xfollowed 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
| Error | When it’s thrown |
|---|---|
ValidationError | Invalid input, such as a malformed address, bad publisher key format, or wrong options |
AuthenticationError | Publisher key is rejected by the API |
APIError | Unexpected 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.