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('Ad content:', 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);
}Configuration
Constructor
Creates a new Specify client instance.
const specify = new Specify(config: SpecifyInitConfig);Parameters
| Parameter | Type | Description |
|---|---|---|
| config | SpecifyInitConfig | Configuration object |
| config.publisherKey | string | Your publisher API key (must start with spk_ and be 34 characters long) |
| config.cacheMostRecentAddress | boolean | Allows for more efficient serving even after a user disconnects wallet (false by default & only supported on the browser client) |
Example
const specify = new Specify({
publisherKey: 'spk_1234567890abcdef1234567890abcdef',
cacheMostRecentAddress: true
});SDK Reference
serve()
Serves targeted advertising content to specified wallet address(es).
serve(
addressOrAddresses: Address | Address[] | undefined,
options: ServeOptions
): Promise<SpecifyAd | null >Parameters
| Parameter | Type | Description | Required |
|---|---|---|---|
| addressOrAddresses | Address | Address[] | undefined | Single wallet address or array of wallet addresses. Undefined or empty only works with cacheMostRecentAddress = true | Yes |
| options | ServeOptions | Configuration options for serving the ad | Yes |
| options.imageFormat | ImageFormat | Image format required for placement. Will match best ad with supported image format. See below for all formats. | Yes |
| options.adUnitId | string | Id used to identify individual ad placements / units. Useful for performance analytics. | No |
ServeOptions Interface
interface ServeOptions {
imageFormat: ImageFormat;
adUnitId?: string;
}Returns
Promise<SpecifyAd | null> - Ad content object or null if no ad is found
Examples
Single wallet address:
import { ImageFormat } from '@specify-sh/sdk';
const ad = await specify.serve(
'0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510',
{
imageFormat: ImageFormat.LANDSCAPE
}
);With adUnitId:
const ad = await specify.serve(
'0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510',
{
imageFormat: ImageFormat.LANDSCAPE,
adUnitId: 'homepage-hero-banner'
}
);Multiple wallet addresses - used when you have multiple addresses for one user:
const ad = await specify.serve([
'0x742d35Cc6634C0532925a3b8D57C11E4a3e1A510',
'0x8ba1f109551bD432803012645Hac136c0532925a',
'0x1234567890123456789012345678901234567890'
], {
imageFormat: ImageFormat.LONG_BANNER,
adUnitId: 'multi-wallet-banner'
});Undefined wallet addresses - With cacheMostRecentAddress we can serve ads even after the user disconnects their wallet:
const ad = await specify.serve(null, {
imageFormat: ImageFormat.LONG_BANNER,
adUnitId: 'multi-wallet-banner'
});
// Or
const ad = await specify.serve([], {
imageFormat: ImageFormat.LONG_BANNER,
adUnitId: 'multi-wallet-banner'
});Image Format Options
The Specify SDK supports three different ad image formats to match your layout needs:
| Format | Aspect Ratio | Resolution | Description | Use Case |
|---|---|---|---|---|
| LANDSCAPE | 16:9 | 640x360 | Wide horizontal format | Hero banners, featured placements |
| LONG_BANNER | 8.09:1 | 1456x180 | Extended horizontal banner | Header/footer placements, leaderboards |
| SHORT_BANNER | 16:5 | 640x200 | Compact horizontal banner | Inline content, sidebars, mobile banners |
Return Type
When the serve() method successfully finds an ad, it returns a SpecifyAd object with the following properties:
| 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 (no markdown) | Ad headline text |
| content | string (markdown) | less than 400 characters |
| ctaUrl | string | Call-to-action URL |
| ctaLabel | string (no markdown) | Call-to-action button text |
| imageUrl | string | URL to the ad image |
| communityName | string (no markdown) | Name of the advertising community |
| communityLogo | string | URL to the community logo |
| imageFormat | string | The requested image format (LANDSCAPE, LONG_BANNER, or SHORT_BANNER) |
Content Formatting
The content field in ad responses uses simplified Markdown formatting:
| Format | Syntax | Example |
|---|---|---|
| Bold text | **text** | Join today for exclusive benefits |
| Bullet points | * item | * Feature 1\n* Feature 2\n* Feature 3 |
| Line breaks | \n | First line\nSecond line |
| Underlined Text | __text__ | Out now |
| Italic Text | *text* | Italic |
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
- Minimum: 1 wallet address required
- Maximum: 50 wallet addresses per request
- Duplicate addresses are automatically removed
Best Practices
Error Handling
Always wrap SDK calls in try-catch blocks and handle specific error types:
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) {
// Display the ad
displayAd(ad);
} else {
// Handle no ad case
showFallbackContent();
}
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid input:', error.message);
} else if (error instanceof NotFoundError) {
console.log('No ad available');
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);
}
}Examples
React Integration
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!
});
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); // No ad available
} 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 <div>No ad available</div>;
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 Integration
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!
});
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,
// You can transform or add additional fields here
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.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');
});