AI Recommendations: sponsored product cards in chat assistants
Your AI chat app, bot or agent sends the user's question to Adfacten, and we return up to three related products from advertisers' product catalogs as cards labelled “Sponsored”. When the user clicks a card and buys (or signs up), you earn the campaign's normal commission.
- Paid on outcomes only. Cards come only from campaigns that pay per sale, lead or install (CPA, CPL, CPS, revenue share, CPI, CPE). Views and clicks are not paid.
- Advertiser opt-in. A campaign appears in cards only if the advertiser turned on the “AI assistant recommendations” traffic source (off by default).
- Real data. Titles, prices and images come only from the advertiser's product catalog. Products out of
stock or not updated for more than 7 days are left out; every price carries its
as_oftime. - Fast. Call this in parallel with your model call; answers are usually under 150 ms. If nothing is ready within 400 ms you get an empty answer (204).
Rules
These rules are part of the publisher terms; assistants that break them are suspended.
- The label is mandatory. Always show
disclosure.label(“Sponsorlu” / “Sponsored”) anddisclosure.textnext to the cards. Do not hide, shrink or fade them. The JavaScript package draws a label that cannot be removed. - Separate from the answer. Do not blend cards into the model's answer or present them as the model's
own advice; show them as a separate block below it. Each card's
whyline explains why it is shown. - No children. Child-directed assistants cannot be added, and an assistant later marked as child-directed gets no cards.
- No sensitive topics. For questions about health, mental health, sexuality, gambling, debt distress, politics, religion, weapons/drugs, self-harm or minors the API returns no cards (204). You are expected not to ask on these topics either.
- Links as they are. Open
urlunchanged; do not shorten it, open it without a user click or fetch it with bots. This link records the click and the sale.
Quick start
- In the panel go to AI assistants → Add assistant: name, type (web chat, mobile app, messaging bot, AI agent), your website and whether it is directed at children.
- On the assistant page, Add placement for each place cards appear (
plc_…), with the label language and the maximum number of cards (1–3). - Copy the public key (
rpk_…) and the placement ID. Type a question in the Live preview box to see cards right away. - Add the code below. New assistants are in test mode: you get sample product cards, links go to adfacten.com and nothing is recorded.
- Once the network team approves your assistant, turn test mode off and real cards start.
In the browser (JavaScript)
<div id="adfacten-cards"></div>
<script src="https://cdn.adfacten.com/js/chat-1.0.0.js"></script>
<script>
// Call it while you send the user's message to your model.
AdfactenChat.recommend({ key: "rpk_…", placement: "plc_…", query: userMessage })
.then((result) => AdfactenChat.render(document.getElementById("adfacten-cards"), result));
</script>
For browser calls, add your site to Allowed origins on the assistant page (e.g.
https://www.yoursite.com); the key then stops working on other sites.
From your server
curl -X POST https://api.adfacten.com/v1/recommendations \
-H "X-Adfacten-Key: rpk_…" \
-H "Content-Type: application/json" \
-d '{"placement": "plc_…", "query": "best coffee machine under 3000 TL", "lang": "en", "limit": 3}'
import httpx
r = httpx.post(
"https://api.adfacten.com/v1/recommendations",
headers={"X-Adfacten-Key": "rpk_…"},
json={"placement": "plc_…", "query": user_message, "lang": "en", "limit": 3},
timeout=0.4,
)
cards = r.json() if r.status_code == 200 else None # 204: no cards, carry on
On the server you can also use an API key with the recommendations:read scope instead of rpk_:
Authorization: Bearer af_live_….
API reference
POST https://api.adfacten.com/v1/recommendations
| Header | |
|---|---|
X-Adfacten-Key: rpk_… |
The assistant's public key (safe in browsers) |
Authorization: Bearer af_live_… |
Or an API key with recommendations:read (server only) |
Request body:
| Field | Type | |
|---|---|---|
placement |
string | Placement ID (plc_…), required |
query |
string | The user's latest message or the shopping intent you extracted, up to 500 characters, required |
lang |
string | tr (default) or en; language of the label and why |
country |
string | ISO country code, e.g. TR |
currency |
string | E.g. TRY |
price_max |
number | Upper price limit in currency |
limit |
number | 1–3; never more than the placement's maximum |
session |
string | Optional opaque conversation ID; we keep only a one-way hash |
agent_checkout |
boolean | true when your agent completes the purchase itself (see below) |
test |
boolean | Ask for test cards even for an approved assistant |
Response 200:
{
"request_id": "rec_…",
"test": false,
"disclosure": { "label": "Sponsored", "text": "These are ads; if you buy, Adfacten and the publisher earn a commission." },
"items": [
{
"id": "prd_…",
"title": "Filter coffee machine 1.2 L",
"brand": "Brand",
"merchant": "Shop name",
"price": { "amount": "1299.90", "currency": "TRY", "as_of": "2026-11-09T08:00:00Z" },
"image_url": "https://…",
"url": "https://trk.adfacten.com/c/…",
"shown_url": "https://trk.adfacten.com/i?…&e=rec_shown",
"why": "Related to your question: coffee machine; the advertiser pays a commission"
}
]
}
204: no matching product, a sensitive topic, or time ran out. The body is empty; carry on without cards.url: the tracking link to open when the user clicks. Do not change it.shown_url: call it once withGETwhen the card has really been seen (at least half on screen for 1 second). The JavaScript package does this for you. In messaging bots, call it when the card is sent.why: why the card is shown; display it under the card.
Errors use the RFC 9457 format:
| HTTP | code |
|
|---|---|---|
| 401 | unknown_key |
Invalid or renewed key |
| 403 | placement_mismatch |
The placement does not belong to this assistant or was deleted (a paused placement returns 204) |
| 403 | assistant_suspended |
The assistant is suspended or rejected |
| 403 | origin_not_allowed |
The browser origin is not in the allowed origins |
| 422 | invalid_request |
Missing or invalid field |
| 429 | rate_limited |
Per-key or per-IP rate limit; wait for Retry-After |
JavaScript package: @adfacten/chat
No dependencies, under 8 KB gzipped. Use it from the CDN (chat-1.0.0.js, global AdfactenChat) or npm.
| Function | |
|---|---|
recommend(options) |
Makes the request and resolves to the cards or null. Never throws. Default timeout 400 ms (timeout). Use onError to see failures. |
render(element, result, { theme, lang, onClick }) |
Draws the cards. The label is on top and is put back if removed. Links open in a new tab with rel="sponsored noopener". The view beacon is sent for you. theme: light, dark, auto. |
toPlainText(result) |
Plain text for messaging bots: one Sponsored · Product – price link line per card, then the disclosure sentence. |
For React:
import { AdfactenRecommendations } from "@adfacten/chat/react";
<AdfactenRecommendations request={{ key: "rpk_…", placement: "plc_…", query: userMessage }} theme="auto" />
Colors can be adjusted on the container with --adf-bg, --adf-fg, --adf-muted, --adf-border,
--adf-accent, --adf-radius and --adf-font; the label keeps fixed colors.
MCP tool
Through the MCP server, agents can call recommend_products:
| Parameter | |
|---|---|
query |
Required |
placement |
Optional; defaults to the first placement of your first assistant |
lang, country, price_max, limit |
Same as the API |
The tool returns the same cards as the API. When the agent shows them to the user, it must show the label
and the disclosure sentence too. The key needs the recommendations:read scope.
The search_products tool now returns tracking links instead of raw shop URLs.
Agent checkout
If your agent completes the purchase for the user (without opening a browser), send
"agent_checkout": true. Each card then has an attribution_token and expires_at (30 minutes). Pass the
token to the shop's checkout (a referral/coupon field or a parameter the shop supports); the advertiser
sends it back when reporting the conversion and the sale is credited to you. Details:
Attribution tokens.
Privacy
- The query text is processed in memory only; the raw text is never stored or logged.
- Before processing we strip e-mail addresses, phone numbers, Turkish ID numbers, IBANs, card numbers and URLs with parameters. Still, do not send personal data.
- Per request we keep only: request ID, placement, a one-way hash of the query (with a key that changes
daily), a coarse category, the number of cards, latency and the test flag. No user IDs;
sessionis hashed too. - Card links are normal tracking links: the usual campaign rules apply to the visitor who clicks.
FAQ
Why do I get no cards? The question may not be about shopping, the topic may be sensitive, no product may match, or time may have run out; all return 204. Try the live preview in the panel.
Do test cards earn money? No. Test cards are sample products; no clicks or sales are recorded.
Do I earn from views or clicks? No. You earn only when a sale or sign-up from a card is approved. Views and clicks appear in reports (CTR, EPC) only.
Can I show the cards with my own design? Yes, call the API directly. You must show the label, the
disclosure sentence, the why line and the price time, and call shown_url once when the card is seen.
Can I put cards inside the model's answer? No. Cards must be a separate, labelled block.
My key is being abused. Renew the key on the assistant page and set the allowed origins.