What the IAB buyer agent does with Mixpeek
IAB Tech Lab's buyer agent is the reference agent for DSPs and agencies in AAMP: it reads seller media kits, negotiates and books deals over MCP, A2A and REST. Since July 2026 it ships a Mixpeek client (PR #78, merged 2026-07-15) that gives the agent three content tools:
| MCP tool | What it does | Arguments |
|---|---|---|
classify_content | Classifies text into IAB Content Taxonomy categories, with tier, path and score | text, optional retriever_id, limit |
check_brand_safety | Classifies the text, then flags matches against a list of sensitive categories and returns safe, risk_level and the flagged categories | text, optional retriever_id, threshold (default 0.80) |
contextual_search | Runs any Mixpeek retriever over indexed inventory | query, retriever_id, limit |
The agent reads
MIXPEEK_API_KEY, MIXPEEK_BASE_URL and MIXPEEK_NAMESPACE from its environment. The first two tools need a retriever over IAB categories; if you do not pass retriever_id, the agent picks a retriever in the namespace whose name contains "iab" and "text".This guide builds that namespace, connects the buyer agent and runs all three tools. Every step ran on 2026-10-19 against a clean checkout of
IABTechLab/buyer-agent at commit 47ac319, in a Linux container.Step 1: build an IAB Content Taxonomy 3.1 namespace
The classifier is semantic search over the taxonomy itself: each of the 704 categories in IAB Tech Lab's published 3.1 file becomes a document whose text is its name and path ("American Football. Category path: Sports > American Football."), embedded with a text model. A piece of content is classified by finding the categories closest to it. The script below creates the namespace, a bucket of the 704 categories, a text collection that keeps
iab_category_name, iab_tier and iab_path on every document, and a retriever named iab_text_search.Two things the script handles that are easy to miss:
unique_key on the bucket if you load it with the batch endpoint: POST /v1/buckets/{id}/objects/batch refuses buckets that declare one, because it cannot enforce it.# setup_iab_namespace.py (Python 3.10+, stdlib only)
import csv, io, json, os, sys, time, urllib.request
API = "https://api.mixpeek.com"
TSV = ("https://raw.githubusercontent.com/InteractiveAdvertisingBureau/Taxonomies/main/"
"Content%20Taxonomies/Content%20Taxonomy%203.1.tsv")
def call(method, path, body=None, ns=None):
headers = {"Authorization": f"Bearer {os.environ['MIXPEEK_API_KEY']}", "Content-Type": "application/json"}
if ns:
headers["X-Namespace"] = ns
req = urllib.request.Request(API + path, method=method, headers=headers,
data=json.dumps(body).encode() if body is not None else None)
return json.load(urllib.request.urlopen(req, timeout=300))
rows = list(csv.reader(io.StringIO(urllib.request.urlopen(TSV).read().decode()), delimiter="\t"))
start = next(i + 1 for i, r in enumerate(rows) if r and r[0].strip() == "Unique ID")
cats = []
for r in rows[start:]:
if r and r[0].strip():
path = [t.strip() for t in (r + [""] * 7)[3:7] if t.strip()]
cats.append({"id": r[0].strip(), "name": r[2].strip(), "tier": len(path), "path": path})
ns = call("POST", "/v1/namespaces", {"namespace_name": "iab-content-taxonomy-3-1",
"feature_extractors": [{"feature_extractor_name": "text_extractor", "version": "v1"}]})["namespace_id"]
call("PATCH", f"/v1/namespaces/{ns}", {"spend_budget": {"daily_usd": 1, "monthly_usd": 2}})
bucket = call("POST", "/v1/buckets", {"bucket_name": "iab_categories", "bucket_schema": {"properties": {
"category_text": {"type": "text", "required": True},
"iab_category_id": {"type": "string", "required": True},
"iab_category_name": {"type": "string", "required": True},
"iab_tier": {"type": "integer", "required": True},
"iab_path": {"type": "array", "items": {"type": "string"}, "required": False}}}}, ns)["bucket_id"]
objs = [{"blobs": [{"property": "category_text", "type": "text",
"data": f"{c['name']}. Category path: {' > '.join(c['path'])}."}],
"iab_category_id": c["id"], "iab_category_name": c["name"],
"iab_tier": c["tier"], "iab_path": c["path"]} for c in cats]
for i in range(0, len(objs), 100):
call("POST", f"/v1/buckets/{bucket}/objects/batch", {"objects": objs[i:i + 100]}, ns)
col = call("POST", "/v1/collections", {"collection_name": "iab_categories_text",
"source": {"type": "bucket", "bucket_ids": [bucket]},
"feature_extractor": {"feature_extractor_name": "text_extractor", "version": "v1",
"input_mappings": {"text": "category_text"},
"field_passthrough": [{"source_path": k} for k in
("iab_category_id", "iab_category_name", "iab_tier", "iab_path")]}}, ns)["collection_id"]
task = call("POST", f"/v1/collections/{col}/trigger", {}, ns)["task_id"]
ret = call("POST", "/v1/retrievers", {"retriever_name": "iab_text_search", "collection_identifiers": [col],
"input_schema": {"query": {"type": "text", "required": True}},
"stages": [{"stage_name": "semantic_search", "stage_type": "filter", "config": {
"stage_id": "feature_search", "parameters": {
"searches": [{"feature_uri": "mixpeek://text_extractor@v1/multilingual_e5_large_instruct_v1",
"query": {"input_mode": "text", "text": "{{INPUT.query}}"}, "top_k": 50}],
"final_top_k": 20}}}]}, ns)
ret = ret.get("retriever", ret)["retriever_id"]
while True: # wait until all 704 categories are searchable
if call("GET", f"/v1/tasks/{task}", ns=ns).get("status") == "FAILED":
sys.exit(call("GET", f"/v1/tasks/{task}", ns=ns)["additional_data"]["error"])
n = call("GET", f"/v1/collections/{col}/documents?page_size=1&include_total=true", ns=ns)["pagination"]["total"]
print(f"{n}/{len(cats)} indexed")
if n >= len(cats):
break
time.sleep(15)
print(json.dumps({"MIXPEEK_NAMESPACE": ns, "MIXPEEK_IAB_RETRIEVER_ID": ret}))curl -s -X POST "https://api.mixpeek.com/v1/retrievers/$RETRIEVER_ID/execute" \
-H "Authorization: Bearer $MIXPEEK_API_KEY" -H "X-Namespace: $NAMESPACE_ID" \
-H "Content-Type: application/json" \
-d '{"inputs": {"query": "The Chiefs clinched the AFC West with a late field goal in Sunday night football."}}'American Football (tier 2, path Sports > American Football).Step 2: point the buyer agent at the namespace
git clone https://github.com/IABTechLab/buyer-agent && cd buyer-agent uv sync --all-extras cat >> .env <<'EOF' MIXPEEK_API_KEY=your_mixpeek_api_key MIXPEEK_BASE_URL=https://api.mixpeek.com MIXPEEK_NAMESPACE=your_namespace_id EOF
uv sync needs Linux or Apple Silicon: one dependency (lancedb) publishes no wheel for Intel Macs, so on one of those, run the agent in a Linux container. The repository's Mixpeek tests run offline; uv run pytest tests/unit/test_mixpeek_client.py tests/unit/test_contextual_enrichment.py gave 25 passed.Step 3: call the three tools
The tools are MCP tools, so in production an LLM agent calls them. To see exactly what they return, call them from Python inside the repository:
import asyncio, json
from ad_buyer.interfaces import mcp_server as m
async def main():
print(await m.classify_content(
text="The Chiefs clinched the AFC West with a late field goal in Sunday night football."))
print(await m.check_brand_safety(
text="Claim a 200 percent deposit bonus at our online casino and play poker and slots tonight."))
rid = await m._discover_iab_retriever(m._get_mixpeek_client())
print(rid, await m.contextual_search(query="home renovation", retriever_id=rid, limit=3))
asyncio.run(main())| Call | Result |
|---|---|
classify_content on the football sentence | American Football (tier 2, 0.790), Sports (tier 1, 0.758), College Football (tier 3, 0.757) |
check_brand_safety on the casino text, default threshold 0.80 | safe: false, risk_level: medium, flagged Poker and Professional Gambling (0.823) and Casinos & Gambling (0.816) |
check_brand_safety with threshold=0.75 | Also flags Lotteries and Scratchcards (0.795); 20 categories clear the threshold |
| Retriever discovery | Found iab_text_search with no retriever_id passed |
contextual_search for "home renovation" | status: completed, top score 0.897 |
risk_level is high only when a flagged score reaches 0.85; with this embedding model, clear gambling text scored 0.82, so expect medium for most real flags.Two things to know before you rely on it
limit does not cap the results. classify_content(limit=5) returned 20 categories, because the retriever's final_top_k (20 above) decides how many come back. Set final_top_k on the retriever to the number you want, or slice the list yourself.The brand-safety list misses half its categories.
check_brand_safety flags a category by exact name, and at commit 47ac319 seven of its fourteen names are not names in Content Taxonomy 3.1: "Casino Games", "Adult Content", "Illegal Content", "Debated Sensitive Social Topics", "Drugs", "Tobacco" and "Death & Grieving". Content in those areas is classified correctly and then not flagged. Until the list changes, check the categories yourself with the 3.1 names:UNSAFE_3_1 = {
"Poker and Professional Gambling", "Casinos & Gambling", "Casino and Gambling Video Games",
"Lotteries and Scratchcards", "Sensitive Topics", "Adult & Explicit Sexual Content",
"Adult Video Games", "Debated Sensitive Social Issues", "Terrorism", "Crime",
"Crime & Harmful Acts to Individuals, Society & Human Right Violations",
"Illegal Drugs, Tobacco, eCigarettes, Vaping, Alcohol", "Arms & Ammunition",
"Death, Injury, or Military Conflict",
}
async def flagged_categories(page_text):
result = json.loads(await m.check_brand_safety(text=page_text))
return [c for c in result["categories"] if c["category"] in UNSAFE_3_1]Where Mixpeek fits
The buyer agent's tools classify text. Mixpeek classifies video, images and audio into the same 3.1 categories at ingest, per scene for long video (IAB Content Taxonomy classification), and
contextual_search can run any retriever you build over that inventory. IAB Tech Lab says AAMP 2.3 integrates contextual enrichment technology that Mixpeek donated (IAB Tech Lab, AAMP 2.3), and the Mixpeek MCP server is listed in the IAB Tech Lab Agent Registry. For the standards around the buyer agent, see AdCP, AAMP, ARTF and Agentic Audiences: what each one does.Changelog
47ac319.Frequently Asked Questions
Does the IAB Tech Lab buyer agent support contextual classification?
Yes. It ships a Mixpeek client with three MCP tools:
classify_content, check_brand_safety and contextual_search (PR #78). They are enabled when MIXPEEK_API_KEY and MIXPEEK_NAMESPACE are set.Which IAB taxonomy does the buyer agent classify into?
Whatever taxonomy the namespace holds. With the setup above it is IAB Content Taxonomy 3.1, built from IAB Tech Lab's published file, and every result carries the category name, tier and full path.
How does the buyer agent find the right Mixpeek retriever?
If you do not pass
retriever_id, it lists the retrievers in the namespace and uses one whose name contains both "iab" and "text", such as iab_text_search.What threshold should I use for check_brand_safety?
The default is 0.80. In our run, clear gambling text scored 0.823 and 0.816 for the two gambling categories and 0.795 for lotteries, so 0.75 to 0.80 is a reasonable range for this embedding model. Test it on your own inventory, since scores depend on the model and the text.
Why did my Mixpeek batch fail right after I created the namespace?
New namespaces start with a daily budget of $0.00, and a batch that runs a model fails on it. Set
spend_budget on the namespace with PATCH /v1/namespaces/{id}, then trigger the collection again. The batch and task show the reason; the trigger response does not.