Skip to content

Ads and sponsored items

Paid items reach you in two ways:

  • Ads, from a pool you ask for: POST /v1/ads/pool with a country and a few subject keywords.
  • Sponsored and promoted rows inside an ordinary search: a product, event, job or update a business pays to have marked, served in the order you asked for and carrying sponsored: true.

Nothing is shown for you. You choose what to show, show it with its label, and tell MasterDB what you showed: POST /v1/ads/render for each render, POST /v1/ads/click for each click. MasterDB pays you the net price of each billed confirmation. Paid items never change the order of a search.

samples/typescript/ads.ts
// Ads and sponsored items: ask for a pool, show what you choose with its label, confirm each render and click.
import assert from 'node:assert/strict';
import { createPrivateKey, randomUUID } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { SANDBOX, createEd25519Signer, createRetrievalClient } from '@masterdb/client';
import { KeySet, SANDBOX_ANCHORS, verifyReceipt } from '@masterdb/verifier';
const baseUrl = process.env.MASTERDB_API_URL ?? SANDBOX.retrieval;
const signer = createEd25519Signer(createPrivateKey(readFileSync(process.env.MASTERDB_KEY_FILE as string)));
const client = createRetrievalClient({ baseUrl, signer });
const anchors = process.env.MASTERDB_ANCHORS_FILE ? JSON.parse(readFileSync(process.env.MASTERDB_ANCHORS_FILE, 'utf8')) : SANDBOX_ANCHORS;
const keys = await KeySet.fetch(baseUrl, { anchors, sandbox: true });
// Your own opaque reference for the conversation: never a user id, never the question.
const sessionRef = `conv-${randomUUID()}`;
// The pool: one country, a few subject keywords (an exact filter), the formats you can show, and your order.
const poolBody = { country: 'US', keywords: ['wireless headphones'], formats: ['card', 'compact'] as ('card' | 'compact')[], sort_by: 'net_price:desc', max_per_campaign: 1, size: 5, session_ref: sessionRef };
const pool = await client.POST('/v1/ads/pool', { body: poolBody });
if (pool.error) throw new Error(`${pool.error.code}: ${pool.error.detail ?? ''}`);
verifyReceipt(pool.data.receipt, keys);
assert.ok(pool.data.ads.length > 0, 'the sandbox has a live ad for these keywords');
const ad = pool.data.ads[0] as (typeof pool.data.ads)[number];
console.log(`[${ad.label}] ${ad.creative.headline} (${ad.creative.display_domain}) — you earn ${ad.net_price.amount} ${ad.net_price.currency} ${ad.net_price.basis}`);
assert.equal(ad.label, 'Ad');
// Show it with its label. Fetch the images from your servers with the signed links (fifteen minutes), never
// from the person's browser. Then confirm the render within ten minutes, in a format the ad offers.
const format = ad.formats?.includes('card') ? 'card' : 'compact';
const renderKey = randomUUID();
const confirmRender = (idempotencyKey: string) =>
client.POST('/v1/ads/render', { params: { header: { 'Idempotency-Key': idempotencyKey } }, body: { render_token: ad.render_token, format, session_ref: sessionRef } });
const render = await confirmRender(renderKey);
if (render.error) throw new Error(`${render.error.code}: ${render.error.detail ?? ''}`);
assert.equal(render.data.accepted, true);
assert.ok(render.data.click_token);
// A retry with the same Idempotency-Key gets the first answer; any other second confirmation is token_reused.
const retried = await confirmRender(renderKey);
assert.deepEqual(retried.data, render.data);
const replayed = await confirmRender(randomUUID());
assert.deepEqual(replayed.data, { accepted: false, reason: 'token_reused' });
// Rendered once in this conversation, the ad is left out of its pools for the next 60 minutes.
const next = await client.POST('/v1/ads/pool', { body: poolBody });
if (next.error) throw new Error(next.error.code);
assert.ok(!next.data.ads.some((a) => a.ad_id === ad.ad_id));
// The person clicked: confirm with the click token (valid 24 hours), then send them to the destination yourself.
const click = await client.POST('/v1/ads/click', {
params: { header: { 'Idempotency-Key': randomUUID() } },
body: { click_token: render.data.click_token, session_ref: sessionRef },
});
if (click.error) throw new Error(`${click.error.code}: ${click.error.detail ?? ''}`);
assert.equal(click.data.accepted, true);
console.log('click confirmed; open', ad.creative.destination_url);
// A sponsored row in an ordinary search carries `sponsored: true`. Confirm its render against that search's receipt.
const search = await client.POST('/v1/search', {
body: { collection: 'products', filter: { all: [{ country: 'US' }, { vertical: 'electronics' }] }, sort_by: 'published_at:desc', limit: 50 },
});
if (search.error) throw new Error(search.error.code);
const row = search.data.rows.find((r) => r['sponsored'] === true);
assert.ok(row, 'the sandbox sponsors one electronics product in the US');
const sponsored = await client.POST('/v1/ads/render', {
params: { header: { 'Idempotency-Key': randomUUID() } },
body: { retrieval_id: search.data.retrieval_id, record_id: row.record_id, receipt: search.data.receipt, format: 'card', session_ref: sessionRef },
});
if (sponsored.error) throw new Error(`${sponsored.error.code}: ${sponsored.error.detail ?? ''}`);
console.log(`[${sponsored.data.label}] ${String(row['product_name'])}: render accepted ${sponsored.data.accepted}`);
assert.equal(sponsored.data.accepted, true);
assert.equal(sponsored.data.label, 'Sponsored');
// A row the search served unmarked is never paid for: its confirmation is refused.
const plain = search.data.rows.find((r) => r['sponsored'] !== true);
if (plain) {
const refused = await client.POST('/v1/ads/render', {
params: { header: { 'Idempotency-Key': randomUUID() } },
body: { retrieval_id: search.data.retrieval_id, record_id: plain.record_id, receipt: search.data.receipt, format: 'card', session_ref: sessionRef },
});
assert.equal(refused.error?.code, 'request_invalid');
}

In the sandbox, ads are served from fictional budgets, with real tokens and no money: the corpus’s US electronics business advertises headphones under wireless headphones, and one of its products is sponsored.

  1. Ask for a pool for one country and a few subject keywords, in your own order.
  2. Choose what to show — none, one or several — and fetch the images on your servers.
  3. Show each one with its label (Ad), in a format it offers.
  4. Confirm each render within ten minutes of the pool, with its render token. The answer carries a click token.
  5. If the person clicks, confirm the click with the click token within 24 hours, and send them to the destination yourself.

Every call is a signed request with your retrieval key and tag="mdb-retrieval", exactly as a search is (signing requests). The MCP server, masterdb-mcp, offers the same calls as the ads_pool and report tools (MCP).

{
"country": "US",
"keywords": ["wireless headphones"],
"formats": ["card", "compact"],
"sort_by": "net_price:desc",
"max_per_campaign": 1,
"size": 5,
"session_ref": "conv-5b0c9a4e"
}
Member
country The person’s country (ISO 3166-1 alpha-2), as you know it. subdivision (ISO 3166-2) too, if you know it.
keywords 1 to 10 subject terms, matched exactly against the ads’ keywords — a filter, never a text query. Never the person’s question, never a user or conversation id.
formats The render formats you can show: compact, card or both.
sort_by Mandatory, as in search: up to three of net_price, published_at, keyword_matches and random (seeded by the pool’s retrieval_id), each :asc or :desc. MasterDB never supplies an order.
max_per_campaign Optional: at most this many ads from one campaign.
size Up to 20; default 20.
session_ref Required. Your opaque reference for the conversation, for the repeat rules below. HMACed on arrival, kept 24 hours, never shown to a business.

The answer is up to size ads — possibly none, when nothing payable matches — with a retrieval_id and a receipt like every other retrieval (receipts). Each ad carries:

Member
ad_id, campaign_id, business_uuid Which ad, which campaign, whose.
label Ad. Show it beside the ad.
formats The formats it offers among those you asked for: compact needs its square image, card its landscape one.
creative headline, description, cta, destination_url, display_domain, and signed links to its images, image_square_url (1:1, for compact) and image_landscape_url (1.91:1, for card), valid fifteen minutes.
net_price What you earn: {amount, currency: "USD", basis}, per thousand renders (cpm) or per click (cpc).
adl_origin_seal The business’s seal over the ad as it was published.
record_base64 The exact bytes the business sealed (masterdb/ads/1), base64.
row The ad’s signed index row: the seal inline (adl_origin_seal), the destination (adl_dest), a hash per image (adl_creative), and the business’s ai_policy_bits, as on every search row.
ai_policy The business’s sealed AI policy, {version, record_base64, seal} — as every fetch carries it — which proves the bits on row. Version 0 with nulls: it has sealed none.
render_token A signed, single-use token: what you confirm the render with.

An ad is rendered alone, so each item carries everything needed to check it, with no further request: that MasterDB signed row, that row.adl_origin is the SHA-256 of record_base64, that the business’s seal covers those bytes, and that every text and link in creative is the one the business sealed (cta is the sealed cta_text). Hash each image you fetch against row.adl_creative. The libraries do all of it from public data — MasterDB’s key set and the business’s keys as published beside its certificate:

import { verifyAdItem, verifyAdImage } from '@masterdb/verifier';
const published = await (await fetch(`https://verify.masterdb.ai/v1/certificates/${ad.business_uuid}/keys`)).json();
verifyAdItem(ad, keySet, { seal: { publishedKeys: published, keySet } }); // throws VerificationError: ad_mismatch, hash_mismatch, …
verifyAdImage(ad.row, squareImageBytes);

POST /v1/verify with {"ad_item": <the item>} answers the same question, signed (verify). An ad that fails is not one to show.

Net only. The price in the pool is what you receive after MasterDB’s take. The gross price and the take rate never leave MasterDB, not in the pool, a token or a confirmation. How you weigh relevance against revenue is your decision; there is no auction.

A pool holds budget. Each campaign in a pool reserves a little of its budget — the cost of one render, or the expected cost of one click — for ten minutes. At most three reserves per key per campaign are outstanding at once, and past 600 pools an hour a key may ask for at most 20 pools per render it confirms (rate_limited otherwise). Ask for a pool when you are about to answer, not speculatively.

compact and card are two ways to render the same ad, not two kinds of ad:

Format
compact A small square image beside the headline, the description and a text link, in the flow of your answer.
card A self-contained card: the wide image above the headline, the description and a button with the call to action. Beside or below your answer.

You choose the format and state it when you confirm the render; it must be one the ad offers.

Every paid item is shown with its label, beside it, every time:

Item Label Where it comes from
An ad Ad the pool
A sponsored product Sponsored a products search row with sponsored: true
A promoted event Promoted event an events search row with sponsored: true
A promoted job Promoted vacancy a jobs search row with sponsored: true
A promoted update Promoted update an updates search row with sponsored: true

Show the creative as the business published it: its headline, description, call to action and display_domain, which is the destination’s own host — so the domain the person sees is the domain the click lands on. The destination_url is delivered exactly as the business entered it, query string included; the business measures its results with it. Link to it directly.

Within ten minutes of the pool, on MasterDB’s clock:

{ "render_token": { "payloadType": "application/vnd.masterdb.render.v1+json", "payload": "…", "signatures": [ … ] },
"format": "card",
"session_ref": "conv-5b0c9a4e" }

Send an Idempotency-Key header with every confirmation. A confirmation MasterDB can read is answered 200, saying whether the render was accepted:

Answer Meaning
{"accepted": true, "click_token": {…}} Recorded. Keep the click token for this item.
{"accepted": false, "reason": "window_expired"} More than ten minutes after the pool: recorded, never billed, never paid.
{"accepted": false, "reason": "budget_exhausted"} The campaign’s money ran out: recorded, never billed, never paid.
{"accepted": false, "reason": "token_reused"} This token was already confirmed.

Retries. A token is confirmed once. A retry with the same Idempotency-Key gets the first answer again, click token included, so a confirmation lost to a timeout is safe to repeat. A second confirmation with any other key is token_reused: recorded, never billed.

A token that MasterDB did not sign, or that was issued to another AI company, is refused 400 token_invalid; a format the ad does not offer is request_invalid. Confirm wherever your requests land: MasterDB routes the confirmation to the region that served the pool. If it cannot, the answer is 503 unavailable — retry with the same Idempotency-Key.

session_ref is how MasterDB applies the per-conversation rules, and it is only as good as you make it: one value per conversation, opaque, never a user id.

  • An item rendered in a conversation is left out of that conversation’s pools for 60 minutes.
  • Rendered again in the same conversation, it is accepted and recorded, but neither charged nor paid — unless its advertiser allows repeats, in which case each render is charged, up to the advertiser’s own maximum per conversation.
  • A repeat click on the same item from the same conversation within 15 minutes is recorded and not billed.

When the person clicks, send them to destination_url yourself — MasterDB is never in the redirect path — and confirm the click with the click token from the render confirmation and the same session_ref:

{ "click_token": { "payloadType": "application/vnd.masterdb.click.v1+json", "payload": "…", "signatures": [ … ] },
"session_ref": "conv-5b0c9a4e" }

The click token is valid for 24 hours from the render confirmation. A later click is answered {"accepted": false, "reason": "window_expired"}: recorded, never billed. Clicks are billed only on campaigns that pay per click (net_price.basis cpc); on a campaign that pays per thousand renders, and on every promotion, a click is accepted and recorded but not billed — you were paid for the render.

A business may sponsor its products, or promote an event, a job or an update. Such a row is served in an ordinary search, in the order you asked for, with sponsored: true — and its entry in the search’s receipt says sponsored: true too. It is paid for exactly like an ad: show it with its label, and confirm the render with the search’s retrieval_id, the row’s id and the search’s receipt, exactly as you received it:

{ "retrieval_id": "r_…",
"record_id": "mdb_…",
"receipt": { "payloadType": "application/vnd.masterdb.receipt.v3+json", "payload": "…", "signatures": [ … ] },
"format": "card",
"session_ref": "conv-5b0c9a4e" }

MasterDB checks the receipt is its own, reserves and confirms the render in one step, and answers with the click token and the label to show:

{ "accepted": true, "label": "Sponsored", "click_token": { … } }
  • The same ten-minute window runs from the search’s served_at; the same answers apply (window_expired, budget_exhausted — the campaign has no money left in the region, and nothing is billed — and token_reused for a second confirmation of the same row of the same search, with the same retry rule).
  • The receipt must be format 2 and issued to the key making the confirmation (caller_key_id): a receipt from another key or another company is token_invalid. So is a receipt MasterDB did not sign.
  • A row the receipt shows served without the marker is never paid for: its confirmation is refused request_invalid.
  • sponsored is set as a row is served and is not part of the row’s signature; verifyServedRow removes it before checking (verify).

Confirm a click on a sponsored or promoted row with its click token, as for an ad.

  • Never fetch a creative from the person’s browser or device. The signed image links are for your servers: fetch them there, within their fifteen minutes, and serve the images yourself. A link passed to a browser would hand the person’s IP address to MasterDB’s CDN.
  • Never send the conversation. Keywords are subject terms you extract; never the question, never a user or conversation id. session_ref is opaque.
  • Never confirm what did not happen. A render is confirmed because the item was shown, a click because the person clicked. Confirmation and click rates are monitored, per AI company, against the platform’s; an outlier’s click revenue is held for review before any payout.
  • Never show a paid item without its label, or alter its creative, its display domain or its destination, or put your own redirect in front of it.
  • Never ask for pools you will not use. A pool holds the advertiser’s budget for ten minutes.
  • Never present another key’s tokens or receipts. They are bound to the company, and a search receipt to the key, they were issued to.

Refusals are RFC 9457 problems with a stable code (errors). The ones particular to paid items:

Code Status
token_invalid 400 A render token, click token or search receipt MasterDB did not sign, or one issued to another company or key.
request_invalid 400 A pool request outside its limits, a format the item does not offer, a row the receipt does not show as sponsored, or a render sent with both a render token and a search receipt.
rate_limited 429 Over the key’s request rate, or far more pools than renders. Wait as the answer says (Retry-After, retry_after_ms).
unavailable 503 The confirmation cannot be recorded right now. Retry with the same Idempotency-Key.

A late, unfunded or repeated confirmation is not an error: it is a 200 with accepted: false and its reason, so you can tell it from a fault.