NEWVectors or files. Pick a path.Start →
    Search & Discovery
    12 min read
    Updated 2026-10-19

    Using Mixpeek in the IAB Tech Lab buyer agent: classification, brand safety, contextual search

    IAB Tech Lab's reference buyer agent ships a Mixpeek client with three MCP tools: classify_content, check_brand_safety and contextual_search. Build an IAB Content Taxonomy 3.1 namespace with a tested script, configure the agent, and see what each tool returned from a clean checkout, plus two behaviors to know: limit does not cap results, and half the brand-safety list does not match 3.1 names.

    AdTech
    AAMP
    IAB Tech Lab
    Buyer Agent
    MCP
    IAB Content Taxonomy
    Brand Safety

    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 toolWhat it doesArguments
    classify_contentClassifies text into IAB Content Taxonomy categories, with tier, path and scoretext, optional retriever_id, limit
    check_brand_safetyClassifies the text, then flags matches against a list of sensitive categories and returns safe, risk_level and the flagged categoriestext, optional retriever_id, threshold (default 0.80)
    contextual_searchRuns any Mixpeek retriever over indexed inventoryquery, 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:

  1. A new namespace has a daily budget of $0.00. A batch that runs a model fails on it, while the trigger call still answers "Accepted". The script sets a small cap before triggering (budgets). Embedding 704 short strings costs cents.
  2. Do not declare a 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.
  3. # 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}))
    Indexing took about six minutes in our run. Check it with one call before going further:
    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."}}'
    The first result should be 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())
    What came back on 2026-10-19:

    CallResult
    classify_content on the football sentenceAmerican 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.80safe: false, risk_level: medium, flagged Poker and Professional Gambling (0.823) and Casinos & Gambling (0.816)
    check_brand_safety with threshold=0.75Also flags Lotteries and Scratchcards (0.795); 20 categories clear the threshold
    Retriever discoveryFound 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]
    Every name in that set is in IAB Tech Lab's 3.1 file. A brand-safety list is a policy decision, so adjust it to your advertisers; the point is that each entry has to match a real category name.

    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



  4. 2026-10-19: first version, tested against buyer-agent commit 47ac319.


  5. 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.

    Sources



  6. IAB Tech Lab buyer agent
  7. buyer-agent PR #78: Mixpeek contextual enrichment
  8. IAB Tech Lab taxonomy files
  9. IAB Tech Lab, AAMP 2.3
  10. Mixpeek docs: retrievers
  11. Managed Mixpeek

    Put multimodal search to work

    Connect a bucket and Mixpeek runs the whole multimodal search pipeline for you: extraction, indexing, and search over your own objects. No models to wire up, nothing to host.

    Start with Managed
    MVS · bring your own

    Already have vectors?

    Keep your embeddings on your own cloud and run dense, sparse, and BM25 search directly on object storage. From $25/mo.

    Start with MVS

    Run this on your own data

    Point Mixpeek at the storage you already have and search your video, images, audio, and documents the way this guide describes. Build starts at $25/mo for up to 1M vectors.

    Search your own archiveRead Docs

    Related guides