npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zeroclickai/offers-sdk

v1.2.0

Published

ZeroClick Offers SDK

Readme

@zeroclickai/offers-sdk

Official SDK for ZeroClick Offers.

Installation

npm install @zeroclickai/offers-sdk

Architecture

  • getOffers() - Server-side only (called from your backend, requires client IP forwarding)
  • renderOffer() - Client-side (renders an offer as a themed iframe with auto-height and CTA handling)
  • trackOfferImpressions() - Client-side (tracks impressions for analytics)
Client/Browser ──────────────────────────────────┐──────────────────────┐
     │                                           │                      │
     │ (1) Request offers                        │ (3) Track            │ (4) Render offer
     ↓                                           ↓     impressions      ↓
Your Backend ──(2)──> ZeroClick API      ZeroClick API          SDK.renderOffer()
(SDK.getOffers)       /api/v2/offers     /api/v2/impressions    (iframe + postMessage)

Quick Start

Server-Side: Fetching Offers

// backend/api/offers.ts (Express, Next.js API route, etc.)
import { ZeroClick } from '@zeroclickai/offers-sdk';

// Initialize once on server startup
ZeroClick.initialize({ apiKey: 'your-api-key' });

// In your API route handler
app.post('/api/offers', async (req, res) => {
  const offers = await ZeroClick.getOffers({
    ipAddress: req.ip || req.headers['x-forwarded-for'], // Required
    userAgent: req.headers['user-agent'], // Optional
    query: req.body.query,
    limit: 3,
    identity: {
      userId: req.user?.id,
      userLocale: req.user?.locale,
    },
  });

  res.json(offers);
});

Client-Side: Rendering Offers

// frontend/client.ts
import { ZeroClick } from '@zeroclickai/offers-sdk';

// Initialize (no API key needed for rendering or tracking)
ZeroClick.initialize();

// Render an offer into a container with theming and auto-height
const handle = await ZeroClick.renderOffer(offer, '#ad-container', {
  width: '100%',
  mode: 'dark',
  style: {
    background: '#1a1a2e',
    textColor: '#ffffff',
    buttonBackground: '#4a9eff',
  },
  autoHeight: true,
  maxHeight: 300,
});

// Track when offers are displayed to the user
await ZeroClick.trackOfferImpressions([offer.id]);

API

ZeroClick.initialize(config?)

Initialize the SDK. Must be called before other methods.

Server-side initialization:

ZeroClick.initialize({
  apiKey: 'your-api-key', // Required for getOffers
});

Client-side initialization (rendering and tracking):

ZeroClick.initialize(); // No API key needed

ZeroClick.getOffers(options) 🔒 Server-side only

Fetch ranked offers based on intent signals. Must be called from your backend server (no CORS access from browsers). Requires apiKey to be set during initialization.

const offers = await ZeroClick.getOffers({
  ipAddress: string;       // Required - Client IP from incoming request
  query?: string;          // Search query for offers
  limit?: number;          // Max offers to return (default: 1)
  userAgent?: string;      // Optional - Client's user agent
  origin?: string;         // Optional - Client's origin/referer
  identity?: {             // Optional - User identity (per-request)
    userId?: string;              // Your user's ID
    userEmailSha256?: string;     // SHA-256 hash of email (lowercase, trimmed)
    userPhoneNumberSha256?: string; // SHA-256 hash of phone (E.164 format)
    userLocale?: string;          // e.g., 'en-US'
    userSessionId?: string;       // Session identifier
    groupingId?: string;          // Grouping ID for analytics segmentation
  };
});

Example:

app.post('/api/offers', async (req, res) => {
  const offers = await ZeroClick.getOffers({
    ipAddress: req.ip || req.headers['x-forwarded-for'],
    userAgent: req.headers['user-agent'],
    query: req.body.query,
    limit: 3,
    identity: {
      userId: req.user?.id,
      userLocale: req.user?.locale,
    },
  });
  res.json(offers);
});

Returns Promise<Offer[]> — an array of offers ranked by relevance.

ZeroClick.trackOfferImpressions(ids) ✅ Client-side

Track offer impressions for analytics. Does not require an API key. Should be called from the client/browser when offers are displayed to the user.

await ZeroClick.trackOfferImpressions(['offer-123', 'offer-456']);

ZeroClick.renderOffer(offer, container, options?) 🖥️ Browser-only

The primary way to display offers in browser environments. Creates a sandboxed iframe, sends offer data via postMessage over a dedicated MessageChannel, and returns a RenderHandle for ongoing control (style updates, cleanup).

const handle = await ZeroClick.renderOffer(offer, '#ad-container', {
  width: '100%',
  mode: 'dark',
  style: {
    background: '#1a1a2e',
    textColor: '#ffffff',
    buttonBackground: '#4a9eff',
  },
  autoHeight: true,
  maxHeight: 300,
  onCtaClick: ({ url }) => window.open(url, '_blank'),
  onResize: ({ height }) => console.log('New height:', height),
});

Key behaviors:

  • Returns null if the offer has no ui field (not all offers support iframe rendering)
  • Re-rendering into the same container automatically replaces any existing ZeroClick iframe
  • On failure (network error, ad blocker), cleans up and returns null silently
  • Supports cancellation via AbortSignal for React Strict Mode and component unmounting

RenderHandle

The returned handle provides ongoing control over the rendered iframe:

if (handle) {
  // Update styles dynamically (e.g., when user toggles theme)
  handle.updateStyle({ background: '#fff', textColor: '#000' });

  // Remove the iframe and clean up the message channel
  handle.destroy();
}

Options

| Option | Type | Default | Description | | ------------ | -------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | mode | 'light' \| 'dark' | — | Theme mode passed to the iframe | | style | IframeStyleConfig | — | Custom colors to match your surface theme (see below) | | width | string | '300px' | CSS width | | height | string | '250px' | CSS height (overridden when autoHeight is enabled) | | autoHeight | boolean | false | Dynamically resize iframe to match content height | | minHeight | number | 50 | Minimum height in px when autoHeight is enabled | | maxHeight | number | — | Maximum height in px when autoHeight is enabled | | onResize | (event: ResizeEvent) => void | — | Callback when iframe height changes (requires autoHeight: true) | | onCtaClick | (event: CtaClickEvent) => void | — | Callback when user clicks the CTA (use in environments where popups are blocked) | | signal | AbortSignal | — | Cancel an in-flight render (useful for React Strict Mode, unmounting) | | sandbox | string | 'allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox' | Iframe sandbox attributes |

Auto-Height

When autoHeight is enabled, the iframe reports its content height and the SDK automatically resizes the iframe element. Use minHeight and maxHeight to constrain the range.

const handle = await ZeroClick.renderOffer(offer, '#container', {
  autoHeight: true,
  minHeight: 80,
  maxHeight: 400,
  onResize: ({ height }) => {
    // Adjust surrounding layout if needed
    console.log('Offer height changed to', height);
  },
});

Style Configuration (IframeStyleConfig)

Pass custom colors to match your host surface theme. If not provided, the iframe renders with a default theme based on mode.

{
  background?: string;            // e.g., '#1a1a2e'
  backgroundHover?: string;       // e.g., '#252540'
  borderColor?: string;           // e.g., '#333333'
  borderRadius?: string;          // e.g., '12px'
  buttonBackground?: string;      // e.g., '#4a9eff'
  buttonBackgroundHover?: string;  // e.g., '#3a8eef'
  buttonTextColor?: string;       // e.g., '#ffffff'
  textColor?: string;             // e.g., '#ffffff'
}

Styles can be updated at any time after render via handle.updateStyle() — useful for responding to theme changes without re-rendering the entire offer.

Cancellation with AbortSignal

Use signal to cancel renders that are no longer needed — particularly useful with React Strict Mode (which mounts/unmounts in development) or when a component unmounts before the iframe finishes loading.

const controller = new AbortController();

ZeroClick.renderOffer(offer, '#container', {
  signal: controller.signal,
});

// Cancel the render (e.g., on unmount)
controller.abort();

ZeroClick.getIframeUrl(offer)

Returns the iframe source URL from the offer's UI configuration, or null if the offer doesn't have iframe rendering configured.

const url = ZeroClick.getIframeUrl(offer);

ZeroClick.getIframeTag(offer, options?)

Generate a static HTML <iframe> tag string for server-side rendering or contexts where JavaScript is not available (e.g., CMS output, static sites). For interactive rendering with theme support, auto-height, and CTA click handling, use renderOffer() instead. Returns null if the offer has no iframe URL.

const html = ZeroClick.getIframeTag(offer, { width: '100%', height: '120px' });
if (html) {
  container.innerHTML = html;
}

Offer Schema

interface Offer {
  id: string;
  title: string | null;
  subtitle: string | null;
  content: string | null;
  cta: string | null;
  clickUrl: string;
  rawUrlEncoded: string | null;
  imageUrl: string | null;
  metadata: Record<string, unknown> | null;
  context: string | null;
  brand: { name; description; url; iconUrl } | null;
  product: { productId; sku; title; description; category; subcategory; image; availability; metadata } | null;
  price: { amount; currency; originalPrice; discount; interval } | null;
  location: { text; address; city; state; zip; distance; distanceUnit; coordinates; hours } | null;
  media: { title; url; description; contentType } | null;
  rating: { value; scale; count } | null;
  ui?: { type: string; url: string } | null;
}

TypeScript

All types are exported for TypeScript users:

import type {
  Offer,
  ZeroClickConfig,
  GetOffersOptions,
  Identity,
  IframeRenderOptions,
  IframeStyleConfig,
  CtaClickEvent,
  ResizeEvent,
  RenderHandle,
} from '@zeroclickai/offers-sdk';

Common Integration Patterns

React

Use AbortSignal to handle Strict Mode double-mounts and component unmounting:

import { useEffect, useRef } from 'react';
import { ZeroClick, type Offer, type RenderHandle } from '@zeroclickai/offers-sdk';

function OfferBanner({ offer }: { offer: Offer }) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const controller = new AbortController();
    let handle: RenderHandle | null = null;

    ZeroClick.renderOffer(offer, containerRef.current!, {
      width: '100%',
      autoHeight: true,
      maxHeight: 300,
      mode: 'light',
      signal: controller.signal,
    }).then((h) => {
      handle = h;
    });

    return () => {
      controller.abort();
      handle?.destroy();
    };
  }, [offer]);

  return <div ref={containerRef} />;
}

Next.js App Router

// app/api/offers/route.ts
import { ZeroClick } from '@zeroclickai/offers-sdk';
import { NextRequest, NextResponse } from 'next/server';

ZeroClick.initialize({ apiKey: process.env.ZEROCLICK_API_KEY });

export async function POST(req: NextRequest) {
  const { query, userId } = await req.json();
  const ip = req.headers.get('x-forwarded-for') || req.ip || '127.0.0.1';

  const offers = await ZeroClick.getOffers({
    ipAddress: ip,
    userAgent: req.headers.get('user-agent') || undefined,
    query,
    limit: 3,
    identity: userId ? { userId } : undefined,
  });

  return NextResponse.json(offers);
}

VS Code Extension (Webview)

Use onCtaClick to handle link opening (VS Code webviews block window.open) and CSS variables to match the editor theme:

const handle = await ZeroClick.renderOffer(offer, '#ad-slot', {
  width: '100%',
  mode: isDarkTheme ? 'dark' : 'light',
  style: {
    background: 'var(--vscode-sideBar-background)',
    textColor: 'var(--vscode-foreground)',
    buttonBackground: 'var(--vscode-button-background)',
    buttonTextColor: 'var(--vscode-button-foreground)',
  },
  autoHeight: true,
  onCtaClick: ({ url }) => vscode.env.openExternal(vscode.Uri.parse(url)),
});

// Update theme when VS Code theme changes
if (handle) {
  window.addEventListener('message', (event) => {
    if (event.data.type === 'theme-change') {
      handle.updateStyle({
        background: event.data.isDark ? '#1e1e1e' : '#ffffff',
        textColor: event.data.isDark ? '#cccccc' : '#333333',
      });
    }
  });
}

Express

import { ZeroClick } from '@zeroclickai/offers-sdk';
import express from 'express';

const app = express();
app.use(express.json());

ZeroClick.initialize({ apiKey: process.env.ZEROCLICK_API_KEY });

app.post('/api/offers', async (req, res) => {
  const offers = await ZeroClick.getOffers({
    ipAddress: req.ip || (req.headers['x-forwarded-for'] as string),
    userAgent: req.headers['user-agent'],
    query: req.body.query,
    limit: 3,
    identity: {
      userId: req.user?.id,
      userLocale: req.user?.locale,
    },
  });

  res.json(offers);
});

License

MIT