Specify docs

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.2

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('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

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_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

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 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:

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

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:

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 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:

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_1234567890abcdef1234567890abcd

Wallet addresses

  • Must be valid Ethereum / EVM-compatible addresses
  • Must start with 0x followed 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

ErrorWhen it's thrown
ValidationErrorInvalid input, such as a malformed address, bad publisher key format, or wrong options
AuthenticationErrorPublisher key is rejected by the API
APIErrorUnexpected 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');
});

On this page