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/sdkQuick 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
| Parameter | Type | Required | Description |
|---|---|---|---|
config.publisherKey | string | Yes | Your publisher API key. Must start with spk_ and be exactly 34 characters |
config.cacheMostRecentAddress | boolean | No | Enables 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
| Parameter | Type | Required | Description |
|---|---|---|---|
addressOrAddresses | Address | Address[] | undefined | Yes | A single wallet address or array of addresses for the same user. undefined or [] only works when cacheMostRecentAddress is enabled |
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 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:
| 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 |
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:
| 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 | URL to the ad image |
communityName | string | Name of the advertising community (plain text, no markdown) |
communityLogo | string | URL to the community logo |
imageFormat | ImageFormat | The 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:
| 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
- 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
| Error | When it’s thrown |
|---|---|
ValidationError | Invalid input — malformed address, bad publisher key format, wrong options |
AuthenticationError | Publisher key is rejected by the API |
NotFoundError | No ad matched this user and format — expected behaviour, handle with fallback content |
APIError | Unexpected 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');
});