Ads and sponsored items
Paid items reach you in two ways:
- Ads, from a pool you ask for:
POST /v1/ads/poolwith 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.
// 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');}"""Ads and sponsored items: ask for a pool, show what you choose with its label, confirm each render and click."""
import jsonimport osimport uuid
import httpxfrom masterdb_signing import SANDBOX_API, MasterDBAuth, load_keyfrom masterdb_verifier import SANDBOX_ANCHORS, KeySet, parse_strict, verify_receipt
base_url = os.environ.get("MASTERDB_API_URL", SANDBOX_API)client = httpx.Client(base_url=base_url, auth=MasterDBAuth(load_key(os.environ["MASTERDB_KEY_FILE"])))anchors = json.load(open(os.environ["MASTERDB_ANCHORS_FILE"])) if "MASTERDB_ANCHORS_FILE" in os.environ else SANDBOX_ANCHORSkeys = KeySet.fetch(base_url, anchors=anchors, sandbox=True)
def post(path: str, body: dict, idempotency_key: str | None = None) -> httpx.Response: headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {} return client.post(path, json=body, headers=headers)
def ok(res: httpx.Response) -> dict: if res.status_code != 200: problem = res.json() raise SystemExit(f"{problem['code']}: {problem.get('detail', '')}") return parse_strict(res.content)
# Your own opaque reference for the conversation: never a user id, never the question.session_ref = f"conv-{uuid.uuid4()}"
# The pool: one country, a few subject keywords (an exact filter), the formats you can show, and your order.pool_body = { "country": "US", "keywords": ["wireless headphones"], "formats": ["card", "compact"], "sort_by": "net_price:desc", "max_per_campaign": 1, "size": 5, "session_ref": session_ref,}pool = ok(post("/v1/ads/pool", pool_body))verify_receipt(pool["receipt"], keys)assert pool["ads"], "the sandbox has a live ad for these keywords"ad = pool["ads"][0]price = ad["net_price"]print(f"[{ad['label']}] {ad['creative']['headline']} ({ad['creative']['display_domain']}) — you earn {price['amount']} {price['currency']} {price['basis']}")assert 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.fmt = "card" if "card" in ad.get("formats", []) else "compact"render_key = str(uuid.uuid4())
def confirm_render(idempotency_key: str) -> dict: return ok(post("/v1/ads/render", {"render_token": ad["render_token"], "format": fmt, "session_ref": session_ref}, idempotency_key))
render = confirm_render(render_key)assert render["accepted"] is True and "click_token" in render
# A retry with the same Idempotency-Key gets the first answer; any other second confirmation is token_reused.assert confirm_render(render_key) == renderassert confirm_render(str(uuid.uuid4())) == {"accepted": False, "reason": "token_reused"}
# Rendered once in this conversation, the ad is left out of its pools for the next 60 minutes.following = ok(post("/v1/ads/pool", pool_body))assert all(a["ad_id"] != ad["ad_id"] for a in following["ads"])
# The person clicked: confirm with the click token (valid 24 hours), then send them to the destination yourself.click = ok(post("/v1/ads/click", {"click_token": render["click_token"], "session_ref": session_ref}, str(uuid.uuid4())))assert click["accepted"] is Trueprint("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.search = ok( post( "/v1/search", {"collection": "products", "filter": {"all": [{"country": "US"}, {"vertical": "electronics"}]}, "sort_by": "published_at:desc", "limit": 50}, ))row = next((r for r in search["rows"] if r.get("sponsored") is True), None)assert row is not None, "the sandbox sponsors one electronics product in the US"sponsored_body = {"retrieval_id": search["retrieval_id"], "record_id": row["record_id"], "receipt": search["receipt"], "format": "card", "session_ref": session_ref}sponsored = ok(post("/v1/ads/render", sponsored_body, str(uuid.uuid4())))print(f"[{sponsored['label']}] {row['product_name']}: render accepted {sponsored['accepted']}")assert sponsored["accepted"] is True and sponsored["label"] == "Sponsored"
# A row the search served unmarked is never paid for: its confirmation is refused.plain = next((r for r in search["rows"] if r.get("sponsored") is not True), None)if plain is not None: refused = post("/v1/ads/render", {**sponsored_body, "record_id": plain["record_id"]}, str(uuid.uuid4())) assert refused.status_code == 400 and refused.json()["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.
The flow
Section titled “The flow”- Ask for a pool for one country and a few subject keywords, in your own order.
- Choose what to show — none, one or several — and fetch the images on your servers.
- Show each one with its label (
Ad), in a format it offers. - Confirm each render within ten minutes of the pool, with its render token. The answer carries a click token.
- 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).
The pool
Section titled “The pool”{ "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. |
Checking an ad before you show it
Section titled “Checking an ad before you show it”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.
Formats
Section titled “Formats”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.
Showing a paid item
Section titled “Showing a paid item”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.
Confirming a render
Section titled “Confirming a render”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.
Repeats in one conversation
Section titled “Repeats in one conversation”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.
Confirming a click
Section titled “Confirming a click”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.
Sponsored and promoted rows in search
Section titled “Sponsored and promoted rows in search”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 — andtoken_reusedfor 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 istoken_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. sponsoredis set as a row is served and is not part of the row’s signature;verifyServedRowremoves it before checking (verify).
Confirm a click on a sponsored or promoted row with its click token, as for an ad.
What you must never do
Section titled “What you must never do”- 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_refis 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.
Errors
Section titled “Errors”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.