Skip to Content
PublishingSDK Reference - v0.4.2

SDK Reference

The Specify JavaScript/TypeScript SDK lets publishers serve personalized ads to users based on their Ethereum or EVM-compatible wallet addresses.

Current version: v0.4.2

Installation

npm install @specify-sh/sdk

Quick start

import Specify, { ImageFormat } from '@specify-sh/sdk'; // Initialize the SDK const specify = new Specify({ publisherKey: 'spk_your_publisher_key_here', // Enable for more efficient client-side serves cacheMostRecentAddress: true }); // Serve an ad to a wallet address try { const ad = await specify.serve('0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', { imageFormat: ImageFormat.LANDSCAPE }); if (ad) { console.log('Headline:', ad.headline); console.log('Content:', ad.content); console.log('Image URL:', ad.imageUrl); console.log('CTA:', ad.ctaLabel, '->', ad.ctaUrl); } else { console.log('No ad found for this wallet'); } } catch (error) { console.error('Error serving ad:', error); }

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.cacheMostRecentAddressbooleanNoEnables ad serving after a user disconnects their wallet. Defaults to false. Browser-only — has no effect on server-side calls

See Improving Results → Browser caching for when to enable cacheMostRecentAddress.

Example

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

serve()

Serves targeted advertising content for one or more wallet addresses.

serve( addressOrAddresses: Address | Address[] | undefined, options: ServeOptions ): Promise<SpecifyAd | null>

Parameters

ParameterTypeRequiredDescription
addressOrAddressesAddress | Address[] | undefinedYesA single wallet address or array of addresses for the same user. undefined or [] only works when cacheMostRecentAddress is enabled
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 ad content object, or null if no matching ad is available.

Examples

Single wallet address

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

With an adUnitId

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

Multiple wallets for one user

Pass every address you know about for the same user. This significantly improves targeting accuracy and conversion attribution — see why this matters.

const ad = await specify.serve( [ '0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510', '0x8ba1f109551bD432803012645Hac136c0532925a', '0x1234567890123456789012345678901234567890' ], { imageFormat: ImageFormat.LONG_BANNER, adUnitId: 'multi-wallet-banner' } );

No address (browser cache fallback)

When cacheMostRecentAddress is enabled, the SDK can serve an ad using the last known wallet — useful after a user disconnects or their session times out.

const ad = await specify.serve(null, { imageFormat: ImageFormat.LONG_BANNER, adUnitId: 'header-banner' }); // Or with an empty array const ad = await specify.serve([], { imageFormat: ImageFormat.LONG_BANNER, adUnitId: 'header-banner' });

Image format options

The SDK supports three ad image 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

For guidance on choosing between them — and on building native placements beyond simple banners — see the Placements page.


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)
imageUrlstringURL to the ad image
communityNamestringName of the advertising community (plain text, no markdown)
communityLogostringURL to the community logo
imageFormatImageFormatThe image format returned (LANDSCAPE, LONG_BANNER, or SHORT_BANNER)

The response has more than just an image — headline, content, CTA, and community branding are all available separately, so you can build rich native placements rather than just dropping in a single 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

  • Minimum: 1 address (or null / [] with caching enabled)
  • Maximum: 50 addresses per request
  • Duplicates: automatically deduplicated

Error handling

Wrap SDK calls in try/catch and branch on the specific error classes exported by the SDK.

import { AuthenticationError, ValidationError, NotFoundError, 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 { showFallbackContent(); } } catch (error) { if (error instanceof ValidationError) { console.error('Invalid input:', error.message); } else if (error instanceof NotFoundError) { showFallbackContent(); } 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 — malformed address, bad publisher key format, wrong options
AuthenticationErrorPublisher key is rejected by the API
NotFoundErrorNo ad matched this user and format — expected behaviour, handle with fallback content
APIErrorUnexpected API-side error. Retry or show a fallback

NotFoundError is a normal outcome, not a bug — it just means Specify had nothing relevant to serve to this user right now. Plan your UI around this case rather than treating it as failure.


Integration examples

React component

import React, { useState, useEffect } from 'react'; import Specify, { SpecifyAd, ValidationError, NotFoundError, ImageFormat } from '@specify-sh/sdk'; const specify = new Specify({ publisherKey: process.env.REACT_APP_SPECIFY_PUBLISHER_KEY!, cacheMostRecentAddress: true }); interface AdComponentProps { walletAddress: string; imageFormat?: ImageFormat; adUnitId?: string; } const AdComponent: React.FC<AdComponentProps> = ({ walletAddress, imageFormat = ImageFormat.LANDSCAPE, adUnitId }) => { const [ad, setAd] = useState<SpecifyAd | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const fetchAd = async () => { try { setLoading(true); setError(null); const adData = await specify.serve(walletAddress, { imageFormat, adUnitId }); setAd(adData); } catch (err) { if (err instanceof ValidationError) { setError('Invalid wallet address'); } else if (err instanceof NotFoundError) { setAd(null); } else { setError('Failed to load ad'); } } finally { setLoading(false); } }; if (walletAddress) fetchAd(); }, [walletAddress, imageFormat, adUnitId]); if (loading) return <div>Loading ad...</div>; if (error) return <div>Error: {error}</div>; if (!ad) return null; // No ad — render nothing (or your own fallback) return ( <div className="ad-container"> <div className="ad-header"> <img src={ad.communityLogo} alt={ad.communityName} className="community-logo" /> <span className="community-name">{ad.communityName}</span> </div> <h3 className="ad-headline">{ad.headline}</h3> <img src={ad.imageUrl} alt={ad.headline} className="ad-image" /> <p className="ad-content">{ad.content}</p> <a href={ad.ctaUrl} className="ad-cta" target="_blank" rel="noopener noreferrer"> {ad.ctaLabel} </a> </div> ); }; export default AdComponent;

Node.js server

import express from 'express'; import Specify, { ValidationError, NotFoundError, ImageFormat } from '@specify-sh/sdk'; const app = express(); const specify = new Specify({ publisherKey: process.env.SPECIFY_PUBLISHER_KEY! // Note: cacheMostRecentAddress has no effect server-side }); app.get('/api/ads/:walletAddress', async (req, res) => { try { const { walletAddress } = req.params; const { format = 'LANDSCAPE', adUnitId } = req.query; const ad = await specify.serve(walletAddress, { imageFormat: format as ImageFormat, adUnitId: adUnitId as string | undefined }); if (ad) { res.json({ success: true, ad: { ...ad, timestamp: new Date().toISOString() } }); } else { res.status(404).json({ success: false, message: 'No ad found' }); } } catch (error) { if (error instanceof ValidationError) { res.status(400).json({ success: false, message: error.message, details: (error as ValidationError & { details?: unknown }).details }); } else if (error instanceof NotFoundError) { res.status(404).json({ success: false, message: 'No ad found' }); } else { console.error('Server error:', error); res.status(500).json({ success: false, message: 'Internal server error' }); } } }); app.listen(3000, () => { console.log('Server running on port 3000'); });
Last updated on