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

promptbid

v0.2.5

Published

Official PromptBid JavaScript SDK - Monetize your AI applications with contextual advertising

Readme

PromptBid Publisher SDK

Lightweight JavaScript SDK for monetizing AI apps with the PromptBid ad exchange. Drop it in, fire the ad request in parallel with your AI call, and render the ad however you want — the SDK handles context, frequency, and tracking.

Installation

npm install @promptbid/sdk

Or load directly in a browser:

<script type="module">
  import PromptBid from './node_modules/@promptbid/sdk/src/index.js';
</script>

Quickstart

import PromptBid from '@promptbid/sdk';

const pb = new PromptBid({ apiKey: 'pk_live_xxx' });

// When a new conversation starts
pb.newConversation();

let previousAIResponse = null;

async function onUserMessage(userMessage) {
  // Fire the AI call and the ad request in parallel.
  // The ad request uses the previous AI response as context so it can run
  // immediately without waiting for the current response to finish.
  const [aiResponse, ad] = await Promise.all([
    askAI(userMessage),
    pb.requestAd('after-response', {
      userMessage,
      assistantMessage: previousAIResponse,
    }),
  ]);

  renderResponse(aiResponse);
  previousAIResponse = aiResponse;

  if (ad) {
    // One-line render: IAB-compliant markup, tracking wired automatically.
    pb.renderAd(ad, document.getElementById('ad-slot'));
  }
}

Configuration

All options are passed to the constructor.

| Option | Type | Default | Description | |---|---|---|---| | apiKey | string | — | Required. Your PromptBid API key. | | baseUrl | string | https://llm-exchange.fly.dev | Exchange base URL. | | contextWindow | number | 3 | How many past turns to include with each ad request for relevance matching. | | adFrequency | number | 1 | Only make a real ad request every N calls to requestAd. E.g. 8 means one request per 8 turns. | | debug | boolean | false | Log all requests and responses to the console. |

const pb = new PromptBid({
  apiKey: 'pk_live_xxx',
  baseUrl: 'https://exchange.promptbid.ai',
  contextWindow: 5,  // include last 5 turns in each request
  adFrequency: 8,    // request an ad every 8th turn
  debug: true,       // print request/response logs
});

API

new PromptBid(options)

Creates the client and immediately validates the API key against the exchange. Throws synchronously if apiKey is missing or contextWindow/adFrequency are invalid.


pb.newConversation()

Starts a new conversation. Resets the turn counter, clears the message buffer, and generates a fresh conversation ID. Call this whenever the user begins a new chat session.

If you never call newConversation(), a conversation is started automatically on the first requestAd() call.


pb.requestAd(placementId, options)Promise<Ad | null>

Requests an ad for a placement slot. Returns null on no-fill, network failure, or when the call is skipped due to adFrequency.

Parameters:

| Parameter | Type | Description | |---|---|---| | placementId | string | Identifies the slot. Use sidebar for a sidebar placement, inline for inline, anything else defaults to a follow-up placement. | | options.userMessage | string | The current user's message for this turn. | | options.assistantMessage | string | The previous AI response. Pass this so the request can run in parallel with the current AI call — it's already available while the new response is still generating. | | options.topics | string[] | Optional topic hints (e.g. ["wellness", "finance"]). | | options.locale | string | BCP47 locale string. Defaults to en-US. |

Returns an Ad object on fill:

| Field | Type | Description | |---|---|---| | impressionId | string | Pass this to all tracking calls. | | adId | string | ID of the ad creative. | | headline | string | Ad title text. | | description | string | Ad body text. | | ctaText | string | Call-to-action label (e.g. "Learn More"). | | ctaUrl | string | Landing page URL. | | displayUrl | string \| null | Shortened domain for disclosure (e.g. "notion.so"). IAB Native: displayurl (data type 11) | | advertiser | string | Advertiser name shown in "Sponsored by" label. IAB Native: sponsored (data type 1) | | privacyUrl | string \| null | AdChoices / privacy info URL. IAB Native: privacy | | format | string | Slot type: FOLLOWUP, SIDEBAR, or INLINE_SNIPPET. |


pb.reportImpression(impressionId)

Call this after you render the ad on screen. Impressions are only counted when confirmed — not when the server responds.


pb.reportClick(impressionId)

Call this when the user taps or clicks the call-to-action.


pb.reportDismiss(impressionId)

Call this when the user closes or dismisses the ad.


pb.renderAd(ad, containerEl)HTMLElement

Renders an IAB Native-compliant ad into a container element. Handles all disclosure requirements, tracking, and interaction wiring automatically. Returns the rendered element.

const el = pb.renderAd(ad, document.getElementById('ad-slot'));

What it does:

  • Replaces the container's content with the rendered ad
  • Shows a "Sponsored · Advertiser" disclosure label (IAB required)
  • Includes an AdChoices link if ad.privacyUrl is set
  • Sets rel="noopener sponsored" on the CTA link (IAB + Google guidance)
  • Calls reportImpression once the element is in the DOM
  • Calls reportClick when the user clicks the CTA
  • Calls reportDismiss and removes the element when the user dismisses

All text is set via textContent — no innerHTML with ad data, so XSS-safe.

Styling with CSS — override any of these classes:

| Class | Element | |---|---| | .pb-ad | Outer wrapper | | .pb-ad-label | Disclosure row | | .pb-ad-sponsored | "Sponsored" text | | .pb-ad-advertiser | Advertiser name | | .pb-ad-privacy | AdChoices link (present only when privacyUrl is set) | | .pb-ad-headline | Ad title | | .pb-ad-body | Ad description | | .pb-ad-cta | Call-to-action link | | .pb-ad-dismiss | Dismiss button |

If you need full control over markup, use the raw ad fields and call reportImpression, reportClick, and reportDismiss yourself — see the custom rendering example below.


Rendering

Easy — use renderAd

if (ad) {
  pb.renderAd(ad, document.getElementById('ad-slot'));
}

Custom — render your own markup

If renderAd doesn't fit your UI, use the ad fields directly. The only requirement is calling the three tracking methods at the right times.

if (ad) {
  const el = document.createElement('div');

  const label = document.createElement('p');
  label.textContent = `Sponsored · ${ad.displayUrl ?? ad.advertiser}`;
  el.appendChild(label);

  const headline = document.createElement('p');
  headline.textContent = ad.headline;
  el.appendChild(headline);

  const cta = document.createElement('a');
  cta.href = ad.ctaUrl;
  cta.rel = 'noopener sponsored';
  cta.textContent = ad.ctaText;
  cta.addEventListener('click', () => pb.reportClick(ad.impressionId));
  el.appendChild(cta);

  document.getElementById('ad-slot').replaceChildren(el);
  pb.reportImpression(ad.impressionId);   // call after the element is in the DOM

  onAdDismiss(() => pb.reportDismiss(ad.impressionId));
}

How context works

Each call to requestAd takes both sides of the current turn: userMessage (available immediately) and assistantMessage (the previous AI response, already available while the new one generates). This lets the ad request fire in parallel with the AI call.

First turn — user initiates

When the user sends the first message there is no prior AI response. Omit assistantMessage (or pass null):

// Turn 1 — no previous AI response exists yet
const [aiResponse, ad] = await Promise.all([
  askAI(userMessage),
  pb.requestAd('after-response', { userMessage }),
]);
previousAIResponse = aiResponse;

First turn — AI initiates

When the AI sends an opening message before the user responds, you already have both sides on the user's first reply. Pass both:

// AI sent an opener before the user responded — pass it as assistantMessage
const [aiResponse, ad] = await Promise.all([
  askAI(userMessage),
  pb.requestAd('after-response', {
    userMessage,
    assistantMessage: aiOpener,
  }),
]);
previousAIResponse = aiResponse;

Turn 2 and beyond

const [aiResponse, ad] = await Promise.all([
  askAI(userMessage),
  pb.requestAd('after-response', {
    userMessage,
    assistantMessage: previousAIResponse,
  }),
]);
previousAIResponse = aiResponse;

How the buffer fills over time

The SDK sends the conversation buffer to the exchange as a structured messages array — each entry has a role ("user" or "assistant") and content. The exchange and its bidders can use this for relevance matching and policy checks.

[
  { "role": "user",      "content": "Hello" },
  { "role": "assistant", "content": "Hi! How can I help?" },
  { "role": "user",      "content": "Follow up question" }
]
Call 1: { user: "Hello",            assistant: null }    → buffer: [t1]
Call 2: { user: "Follow up",        assistant: <r1> }    → buffer: [t1, t2]
Call 3: { user: "Another question", assistant: <r2> }    → buffer: [t1, t2, t3]
Call 4: { user: "...",              assistant: <r3> }    → buffer: [t2, t3, t4]  ← t1 dropped

The buffer resets when newConversation() is called.

How ad frequency works

With adFrequency: 8, requestAd() returns null for turns 1–7 and makes a real network call on turn 8, then again on turn 16, and so on. The message buffer still updates on every turn so context stays current when the request fires.

// Only one real network call per 8 turns — no extra logic needed on your side
for (const userMessage of conversationTurns) {
  const [aiResponse, ad] = await Promise.all([
    askAI(userMessage),
    pb.requestAd('after-response', {
      userMessage,
      assistantMessage: previousAIResponse,
    }),
  ]);
  previousAIResponse = aiResponse;
  if (ad) renderAd(ad); // only non-null on every 8th turn
}

Error handling

  • Failed requests get one automatic retry, then return null.
  • Tracking calls (reportImpression, reportClick, reportDismiss) are fire-and-forget — failures are silent.
  • If the exchange is unreachable, requestAd() returns null and your app keeps working normally.