SDK reference (v0.4.x)
Archived reference for the v0.4.x JavaScript/TypeScript SDK. For new integrations, use the current SDK reference.
Archived reference
This documents @specify-sh/sdk v0.4.x, which is deprecated. Existing integrations keep serving, but the package will not receive new features. For a new integration use the Browser SDK; to upgrade, see Migrating from v0.4.x.
The Specify JavaScript/TypeScript SDK lets publishers serve personalized ads to users based on their Ethereum or EVM-compatible wallet addresses.
Documented version: v0.4.2
Installation
npm install @specify-sh/sdk@0.4.2Quick 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('0x1111111111111111111111111111111111111111', {
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);
}For a new integration, follow the JavaScript guide; for an existing v0.4.x integration, follow Migrating from v0.4.x.
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_1234567890abcdef1234567890abcd',
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 promise resolves to 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(
'0x1111111111111111111111111111111111111111',
{
imageFormat: ImageFormat.LANDSCAPE
}
);With an adUnitId
const ad = await specify.serve(
'0x1111111111111111111111111111111111111111',
{
imageFormat: ImageFormat.LANDSCAPE,
adUnitId: 'homepage-hero-banner'
}
);Multiple wallets for one user
Pass every address you know about for the same user to improve targeting and conversion attribution. See why this matters.
const ad = await specify.serve(
[
'0x1111111111111111111111111111111111111111',
'0x8ba1f109551bD432803012645Hac136c0532925a',
'0x1111111111111111111111111111111111111111'
],
{
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 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 |
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 | 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 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_1234567890abcdef1234567890abcd
Wallet addresses
- Must be valid Ethereum / EVM-compatible addresses
- Must start with
0xfollowed by 40 hexadecimal characters - Case-insensitive
- Example:
0x1111111111111111111111111111111111111111
Address limits per serve() call
- Pass at least one address, or pass
null/[]with caching enabled. - Each request accepts at most 50 addresses.
- The SDK removes duplicate addresses.
Error handling
Wrap SDK calls in try/catch and branch on the specific 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 {
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 error. Retry or show a fallback |
No ad for this user and format is not an error. serve() returns null and your fallback path handles it. The package also exports a NotFoundError class, but nothing in the SDK ever throws it.
Integration examples
React component
import React, { useState, useEffect } from 'react';
import Specify, {
SpecifyAd,
ValidationError,
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 {
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, 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 {
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');
});