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/sdkOr 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.privacyUrlis set - Sets
rel="noopener sponsored"on the CTA link (IAB + Google guidance) - Calls
reportImpressiononce the element is in the DOM - Calls
reportClickwhen the user clicks the CTA - Calls
reportDismissand 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 droppedThe 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()returnsnulland your app keeps working normally.
