Manual · AdCP

Buying from a
live seller agent.

sell.nofluffadvisory.com is a publisher-direct AdCP sales agent with real display inventory on the essays here. This is the whole loop: discovery, audience targeting, a media buy, creatives, approval, serving, and the delivery report that closes it.

Every command on this page was run against production before it went up.

  1. 01 What you’re talking to
  2. 02 Three things that will trip you up
  3. 03 Discovery
  4. 04 Targeting: synaptic audiences
  5. 05 Composing a product from a brief
  6. 06 Creating the media buy
  7. 07 Creatives (and banner specs)
  8. 08 The human gate
  9. 09 Serving
  10. 10 Delivery reporting
  11. 11 The whole loop in one script
  12. 12 Tool reference
  13. 13 Errors you may hit
The end-to-end run across the seller agent, the signals agent and the human gate A buying agent moves left to right through six steps: discover, compose, buy, gates, serve, report. Above the main track, on a separate dashed rail, sits the independently operated signals agent, reached by an optional concept bridge that branches off the compose step and rejoins it. At the gates step the run halts and waits for a human approval, even at zero dollars. A return path carries the delivery report back. SIGNALS AGENT · SEPARATE UPTIME adcp.signal-stack.io search_concepts → UCP concept ids concept_bridge: true — optional 3s timeout · degrades to the normal path YOUYour agentMCP client 01Discoverget_products 02Composebrief → product 03Buycreate_media_buy 04Gatespolicy, then a human 05Servecreative renders 06Reportwhat rendered get_media_buy_delivery — impressions, clicks, spend
One request, two independently operated agents, one human. The bridge to the signals agent is opt-in and rejoins where it left — if that agent is down, the buy still completes. The run halts once, at the gate, and no amount of budget skips it.
Prefer to just try it? Open the live console → Type a brief, watch it resolve to real segments — no terminal required.

01 What you’re talking to

One Cloudflare Worker that does four jobs: it publishes a catalog, accepts media buys over MCP, decides which creative fills a slot on a page, and reports what actually rendered. There is no ad server behind it and no exchange in front of it. A buying agent talks straight to the publisher.

The transport is MCP over JSON-RPC 2.0 at https://sell.nofluffadvisory.com/mcp. Every request is a POST. Discovery tools are public; anything that writes needs a bearer token. There are ten tools, listed in full in section 12.

1Discover2Target3Buy4Creatives5Human gate6Serve7Report

02 Three things that will trip you up

Read these before you write any code. Each one costs an hour if you find it the hard way.

1. A .example domain sends you to sandbox

RFC 2606 reserved TLDs — .example, .test, .invalid, .localhost — can never belong to a real advertiser, so a buy branded with one is classified as conformance traffic automatically. It is persisted and echoed back to you exactly like a real buy, so the response looks identical. But it never reaches the human approval queue and it never serves.

That is deliberate: it keeps fixture traffic out of the queue. If you want to exercise the real path, use a real domain in brand.domain. If you want sandbox on purpose, either use a reserved TLD or pass account.sandbox: true.

2. Auth is self-serve, but only for writes

Discovery needs no credentials at all — auth-gated discovery is where buying agents go to die. Anything that writes needs Authorization: Bearer demo-<anything>. Any token matching demo-* is accepted, so you never wait on a key handshake. Pick something identifiable like demo-scope3; it becomes your account key.

3. You get pending, never a confirmation

Every buy clears a machine policy gate, then stops and waits for a human at a $0 floor. There is no auto-accept threshold to clear. Your agent receives media_buy_status: pending_creatives and human_review: pending, and that is the terminal state until a person clicks approve.

If your agent expects a synchronous confirmation, this is where it will break. Pass a webhook_url and you will get a signed status callback instead of polling.

03 Discovery

Three public calls. No auth. Start here.

What can this agent do?

Returns supported AdCP versions, the protocol block, governance posture (mode, review thresholds), creative policy, and the tool list. This is the call that tells you the human gate exists before you design around its absence.

curl -s -X POST https://sell.nofluffadvisory.com/mcp \
  -H 'content-type: application/json' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_adcp_capabilities",
    "arguments": {}
  }
}'
Response
{
  "adcp_version": "3.1",
  "supported_versions": ["3.0", "3.1"],
  "adcp": {
    "major_versions": [3],
    "supported_versions": ["3.0", "3.1"],
    "idempotency": { "supported": true, "replay_ttl_seconds": 604800 }
  },
  "supported_protocols": ["media_buy"],
  "governance": {
    "mode": "enforce",
    "human_review": { "creatives": "always", "media_buys_above_usd": 0 }
  }
  … request_signing, webhook_signing, identity, media_buy, account and ext trimmed
}

What tools are there?

curl -s -X POST https://sell.nofluffadvisory.com/mcp \
  -H 'content-type: application/json' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}'

What is for sale?

Four static products. Each carries pricing options, format refs, estimated monthly exposures, and the basis those estimates came from — the avails are GA4-derived and say so, rather than being a number someone liked.

curl -s -X POST https://sell.nofluffadvisory.com/mcp \
  -H 'content-type: application/json' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_products",
    "arguments": {}
  }
}'
Response
{
  "adcp_version": "3.1",
  "status": "completed",
  "cache_scope": "public",
  "products": [
    {
      "product_id": "prod_ros_essays",
      "name": "Run of Essays — display",
      "kind": "static",
      "delivery_type": "non_guaranteed",
      "pricing_options": [
        { "pricing_option_id": "cpm_usd_fixed",
          "pricing_model": "cpm", "rate": 10, "currency": "USD" }
      ],
      "estimated_exposures": {
        "period": "month", "impressions": 2000,
        "basis": "GA4 2026-07 snapshot: 2,709 pageviews/mo site-wide; …"
      }
    }
    … prod_agentic_standards, prod_data_collab, prod_interop_test trimmed
  ],
  "curation": "static_catalog_v0"
}
product_idCPMEst. / moWhat it is
prod_interop_test$1200Token-priced SKU for exactly this exercise. Start here.
prod_ros_essays$102,000Run of essays. The actual rate card.
prod_agentic_standards$18400AdCP / AAMP / protocol governance cluster.
prod_data_collab$18300Clean rooms, CDP, identity cluster.

prod_interop_test exists so you can run the loop without a budget conversation. It is capped so test buys cannot eat sellable inventory, and it sorts last on any commercial brief. It is not a rate card.

That call returns the shelf. get_products also takes a brief and a concept_bridge flag, which compose a product instead of picking one — §04 lays out the five routes and which to use when.

What creative can I run?

curl -s -X POST https://sell.nofluffadvisory.com/mcp \
  -H 'content-type: application/json' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_creative_formats",
    "arguments": {}
  }
}'
Response
{
  "adcp_version": "3.1",
  "status": "completed",
  "formats": [
    {
      "format_id": {
        "agent_url": "https://sell.nofluffadvisory.com",
        "id": "display_300x250"
      },
      "name": "Medium Rectangle",
      "type": "display",
      "assets": [
        {
          "asset_id": "primary", "asset_role": "banner_image",
          "asset_type": "image", "required": true,
          "requirements": {
            "min_width": 300, "max_width": 300,
            "min_height": 250, "max_height": 250
          }
        }
        … click_url and alt_text assets trimmed
      ]
    }
    … display_728x90 trimmed
  ]
}

04 Targeting: synaptic audiences

This is the part that isn’t a cookie. A synaptic audience is a session-scoped, cookieless audience derived from the connections a reader traverses — the meaning of the current page plus the trajectory of pages before it — rather than from any persistent identifier.

There is no profile, no ID graph, and nothing to sync. The segment is computed from where the reader is and where they just came from, and it evaporates when the session does. Generation is deterministic keyword rules over essay frontmatter, not runtime inference, so the same reader path always produces the same segment — and the rules are readable in the repo.

Pick one route — they escalate

There are five ways to say who you want, and you only need one. They are ordered by how much you have to know up front. Start at A. Go down a rung only when the rung above can’t express what you mean — every step down buys precision with a bit more machinery, and D and E are the only ones that touch a second system.

  1. A

    Buy the product as-is

    You want reach across the whole essay set and the product’s own definition is already the audience.

    Omit targeting from the package. Nothing else to learn.

  2. B

    Name the pages — essay_slugs

    You have read the site and know exactly which essays you want to sit beside.

    Put slugs on the package. Deterministic, no matching involved.

  3. C

    Name the audience — segment_ids

    You want the reader regardless of which essay they landed on, and one of the 20 segments below already says it.

    Put segment ids on the package. Browse them in /synaptic/catalog.

  4. D

    Describe it — brief

    You can say it in a sentence but don’t want to read a segment list first. This is the common case.

    Send prose to get_products; a product is composed for you (section 05).

  5. E

    Use your own vocabulary — concept_bridge

    You already work in UCP concept ids across several publishers and want to address this inventory in the nouns you hold, not ours.

    Add concept_bridge: true to the same brief call. Detailed below.

A, B and C are the seller agent alone. D adds in-house brief matching. E is the only route that calls a second agent — and if that agent is down, E degrades to D rather than failing.

Where B and C go in the request

Targeting lives on the package, not the buy. Two dimensions, and they are ORed — a slot matches if it satisfies either one. Omit targeting entirely and you are on route A.

"targeting": {
  "segment_ids": ["syn_agentic_standards", "syn_agent_governance"],
  "essay_slugs": ["adcp-vs-aamp", "the-answerer-of-record"]
}

The catalog (route C)

Twenty segments, each listing every member essay so you can audit exactly what you are buying. Measured serve beacons replace the estimates as they accumulate.

curl -s https://sell.nofluffadvisory.com/synaptic/catalog
Response
{
  "catalog_version": "…",
  "definition": "A synaptic audience is a session-scoped, cookieless audience derived from the connections a reader traverses — the meaning of the current page plus the trajectory of pages before it — rather than from any persistent identifier.",
  "generation": "Deterministic keyword rules over essay frontmatter (scripts/generate-synaptic-facets.mjs, reviewable in the repo). No runtime inference.",
  "segments": [
    {
      "segment_id": "syn_agentic_standards",
      "name": "Agentic standards evaluators",
      "audience": "Practitioners tracking AdCP/AAMP/UCP and the agentic protocol layer.",
      "member_slugs": ["adcp-the-open-standard-for-agentic-advertising",
                       "adcp-vs-aamp", "…"]
    }
    … 19 more segments
  ]
}
syn_agentic_standards Agentic standards evaluators 10 essays
syn_agentic_buying Agentic buying & automation 27 essays
syn_agent_governance Agent governance & accountability 13 essays
syn_measurement Measurement & attribution modernizers 17 essays
syn_cdp_architecture CDP & data architecture 8 essays
syn_clean_rooms Clean room & data collaboration 4 essays
syn_identity Identity & addressability 11 essays
syn_first_party_data First/zero-party data 7 essays
syn_contextual Contextual intelligence 6 essays
syn_storytelling Narrative & creative strategy 6 essays
syn_embeddings_ai Embeddings & AI infrastructure 5 essays
syn_ai_search AI search & answer engines 14 essays
syn_agi_futures AGI & intelligence economy 9 essays
syn_attention Attention & relevance science 7 essays
syn_commerce Commerce & retail media 4 essays
syn_walled_gardens Walled gardens & platforms 10 essays
syn_fraud_quality Ad fraud & quality 2 essays
syn_gtm_us US market entry & GTM 2 essays
syn_operators Operating leaders & talent 5 essays
syn_howto_builders Hands-on builders 5 essays

Route E in full: where the signals agent fits

There is a second agent at adcp.signal-stack.io speaking AdCP’s signals protocol, and it is independently operated — separate codebase, separate deploy, separate uptime. By default this seller agent does not call it, and discovery never depends on it.

What the two now share is a vocabulary, not a retrieval path. Synaptic ids like syn_clean_rooms are publisher-local — useful here, meaningless to anyone else. UCP concept ids like CLEAN_ROOM_PRACTITIONER live in a shared registry, so two publishers can be compared on the same noun. Each segment now publishes its concept ids in /synaptic/catalog, computed at build time.

Be clear what that buys you: comparability, not accuracy. Brief matching here is already good without it. What it adds is the ability to address this inventory in nouns you already hold.

The concept ids also resolve the other way. On the signals agent, a concept can carry member_nodes pointing at its own catalog — so CDP_ARCHITECT hands back sig_bi_data_platform_eval alongside our syn_cdp_architecture. One noun, two independently operated inventories. That crosswalk covers 4 of the 16 B2B concepts today; the rest carry no nodes, for the same reason the segments above carry no concepts.

10 of 20 segments carry a concept — and the other 10 are the finding

The UCP registry was consumer/CTV shaped. Mapping 20 B2B ad-tech audiences onto it returned nothing usable, and lexical matching cheerfully paired “engineers working with embeddings and AI infrastructure” with PARENTS_WITH_YOUNG_CHILDREN_US. The registry has since been extended with 16 B2B role and domain concepts; 10 segments now map cleanly.

The remaining 10 publish no concept at all. A match has to clear a floor and dominate the runner-up — syn_commerce cleared the floor but its top two candidates were too close, so it was dropped rather than guessed. A published mapping is a claim you would act on, so the absence is deliberate. Unmapped means unmapped, not unpopular.

Making the route E call

Pass concept_bridge: true to get_products and your brief is resolved against the UCP registry as well as in-house, then mapped back to inventory. It only ever widens the result, so opting in cannot make the answer worse.

curl -s -X POST https://sell.nofluffadvisory.com/mcp \
  -H 'content-type: application/json' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_products",
    "arguments": {
      "brief": "identity resolution and addressability after third party cookies",
      "concept_bridge": true
    }
  }
}'
Response
{
  "adcp_version": "3.1",
  "status": "completed",
  "products": [
    {
      "product_id": "prod_dyn_identity_84fllj",
      "segment_ids": ["syn_identity", "syn_clean_rooms", "syn_first_party_data"],
      "curation": {
        "resolution": "concept_v1",
        "concept_bridge": {
          "fired": true,
          "agent": "adcp.signal-stack.io",
          "concepts": ["IDENTITY_RESOLUTION_STRATEGIST"],
          "note": "UCP concept ids resolved to inventory via the build-time segment mapping."
        }
      }
    }
    … the four static products follow, unchanged
  ],
  "curation": "dynamic_hybrid_v1"
}

The response always states whether the bridge actually contributed. resolution only reads concept_v1 when a bridged segment made the final cut; otherwise you get hybrid_v1 and a concept_bridge.reasonnot_requested, no_dominant_concept, concepts_unmapped_to_inventory, signals_unreachable. Same rule as everything else here: a signal that didn’t contribute is never dressed up as one that did. The call has a 3s timeout and degrades to the normal path, so the second agent can be down without breaking your buy.

And syn_embeddings_ai is a topic, not a method

The trap in that name: syn_embeddings_ai means readers of the five essays about embeddings and AI infrastructure. It does not mean “the segment built using embeddings.” Every segment id names its subject matter, the same way syn_clean_rooms does.

Embeddings do appear in the stack, but at a different step — and only one:

  • Segment definition (which essays belong to which segment) is pure deterministic keyword rules over essay frontmatter. No embeddings, no inference, reviewable in the repo.
  • Brief → segment matching (section 5) is hybrid: lexical keyword scoring fused with semantic similarity against the site’s existing corpus embeddings, via reciprocal-rank fusion.

The response tells you which one actually fired — dynamic_hybrid_v1 when the semantic pass contributed, dynamic_lexical_v1 when it degraded to keywords alone. It is never dressed up as a signal that didn’t contribute. Either way the result is deterministic: the embedding model is pinned, so the same brief returns the same product id.

05 Composing a product from a brief

You don’t have to pick a product off the shelf. Pass a brief to get_products and the agent composes one from whichever synaptic segments the brief resolves to, returning it ahead of the static catalog. The composed product is purchasable — its product_id goes straight into create_media_buy, and packages inherit its targeting unless you override them.

curl -s -X POST https://sell.nofluffadvisory.com/mcp \
  -H 'content-type: application/json' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_products",
    "arguments": {
      "brief": "reach people evaluating agentic advertising standards"
    }
  }
}'
Response
{
  "adcp_version": "3.1",
  "status": "completed",
  "products": [
    {
      "product_id": "prod_dyn_2f0f2a1c8d3b",
      "name": "Agentic standards evaluators — composed",
      "kind": "dynamic",
      "segment_ids": ["syn_agentic_standards", "syn_agent_governance"],
      "pricing_options": [
        { "pricing_option_id": "cpm_usd_fixed",
          "pricing_model": "cpm", "rate": 18, "currency": "USD" }
      ]
    }
    … the four static products follow, unchanged
  ],
  "curation": "dynamic_hybrid_v1",
  "curation_note": "Composed prod_dyn_… from syn_agentic_standards, syn_agent_governance — purchasable via create_media_buy; packages inherit the product's essay/segment targeting unless overridden."
}

Composition is deterministic: the same brief produces the same product_id, word order included. If a brief resolves to nothing you get the static catalog back with a curation_note saying so, rather than an invented match.

06 Creating the media buy

The first call that needs a bearer. Required fields are brand, packages and budget; everything else is optional but most of it is worth sending.

curl -s -X POST https://sell.nofluffadvisory.com/mcp \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer demo-yourname' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "create_media_buy",
    "arguments": {
      "buyer_ref": "my-first-adcp-buy",
      "buyer_agent": "https://your-agent.com",
      "brand": {
        "name": "Your Brand",
        "domain": "yourbrand.com"
      },
      "packages": [
        {
          "product_id": "prod_interop_test",
          "buyer_ref": "pkg-1",
          "targeting": {
            "segment_ids": [
              "syn_agentic_standards"
            ]
          }
        }
      ],
      "budget": 1,
      "currency": "USD",
      "idempotency_key": "my-first-buy-001"
    }
  }
}'

What comes back

{
  "adcp_version": "3.1",
  "status": "completed",
  "media_buy_id": "mb_dc593a7ea963",
  "buyer_ref": "my-first-adcp-buy",
  "media_buy_status": "pending_creatives",
  "budget": 1, "currency": "USD", "cpm": 1,
  "packages": [
    { "package_id": "pk_9f2c1a44b8e0", "product_id": "prod_interop_test" }
  ],
  "governance": {
    "machine_gate": "pass",
    "policy_flags": [],
    "human_review": { "status": "pending", "floor_usd": 0 }
  }
}

Note the two different status fields. The envelope status is the task — your call succeeded. The media_buy_status is the buy — it is waiting on creatives and a human. Domain state never rides the envelope slot.

Fields worth knowing

FieldWhy it matters
brandRequired, { name, domain } minimum. The domain decides sandbox vs real (see section 2).
packages[]Required. Each needs a product_id; add targeting and budget_share as needed.
budgetRequired. Total USD, must be greater than 0 and at most 100,000.
idempotency_keyReplays the canonical response for the life of the buy. Retry safely.
webhook_urlHTTPS endpoint for signed status callbacks. Strongly recommended given the human gate.
buyer_agentIdentifier or URL of your agent. Shows up in the approval queue — it is how a human knows who is asking.
buyer_refYour own correlation id. get_media_buys filters on it.

Checking on it later

curl -s -X POST https://sell.nofluffadvisory.com/mcp \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer demo-yourname' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_media_buys",
    "arguments": {
      "buyer_ref": "my-first-adcp-buy"
    }
  }
}'
Response
{
  "adcp_version": "3.1",
  "status": "completed",
  "media_buys": [
    {
      "media_buy_id": "mb_15cf038726e0",
      "buyer_ref": "my-first-adcp-buy",
      "media_buy_status": "pending_creatives",
      "budget": 1, "currency": "USD", "cpm": 1,
      "sandbox": true,
      "packages": [
        {
          "package_id": "pk_f3159f795e41",
          "product_id": "prod_interop_test",
          "buyer_ref": "pkg-1",
          "pricing_option_id": "cpm_usd_fixed",
          "targeting": { "segment_ids": ["syn_agentic_standards"] }
        }
      ],
      "governance": {
        "machine_gate": "pass",
        "policy_flags": [],
        "human_review": { "status": "pending", "floor_usd": 0 }
      },
      "valid_actions": ["pause", "cancel"]
    }
  ]
}

07 Creatives (and banner specs)

Creative policy is narrow on purpose: an image URL, a click URL, and alt text. No HTML, no JavaScript, no tags, no third-party trackers. A publisher that accepts arbitrary markup has accepted arbitrary code, and this one doesn’t.

The hard rules

  • PNG, JPEG or WebP only. SVG is rejected. So are data: URIs — the image is verified with a real HTTP HEAD probe, so it has to be hosted and reachable.
  • 300KB maximum. Checked against content-length.
  • Exact dimensions. Each format pins min and max to the same number — 728×90 means 728×90.
  • Alt text is required, 140 characters max. It is an accessibility requirement, not a formality.
  • The HEAD probe times out at 4 seconds. A slow CDN flags the creative for review rather than failing it.

Formats

format_idSizeWhere it lands
display_728x90728 × 90Leaderboard. Reads well mid-essay on desktop.
display_300x250300 × 250Medium rectangle. The related-essays slot.

What a good one looks like

The house ads set the register: dark field, restrained type, one line of argument and one link. The slot sits inside an essay someone is actually reading, so a banner that behaves like a banner gets ignored. Below is the house creative that fills any unsold slot — copy the restraint, not the copy.

AD · SERVED BY THIS ADCP STACK
This slot was bought by an agent Machine-gated. Human-approved. $1 well spent. See the whole loop → nofluffadvisory.com/building/adcp-publisher-stack

Rendered at 728×90. Note what it doesn’t do: no exclamation marks, no countdown, no fake urgency. It states what happened and offers one link.

Submitting

sync_creatives upserts on your own creative_id, so re-sending an edited creative updates it rather than duplicating. assignments bind a creative to a package — without an assignment the creative sits in your account library and never serves.

curl -s -X POST https://sell.nofluffadvisory.com/mcp \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer demo-yourname' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "sync_creatives",
    "arguments": {
      "media_buy_id": "mb_dc593a7ea963",
      "creatives": [
        {
          "creative_id": "cr-hello-001",
          "name": "Hello from my agent",
          "format_id": "display_728x90",
          "image_url": "https://cdn.yourbrand.com/banner-728x90.png",
          "click_url": "https://yourbrand.com/landing",
          "alt_text": "Your Brand — agentic media buying, explained."
        }
      ],
      "assignments": [
        {
          "creative_id": "cr-hello-001",
          "package_id": "pk_9f2c1a44b8e0"
        }
      ]
    }
  }
}'
Response
{
  "adcp_version": "3.1",
  "status": "completed",
  "media_buy_id": "mb_15cf038726e0",
  "creatives": [
    {
      "creative_id": "cr-hello-001",
      "platform_id": "cr_42cc86d69b3e",
      "action": "created",
      "status": "pending_review",
      "policy_flags": [],
      "assigned_to": ["pk_f3159f795e41"]
    }
  ],
  "human_review": "always (v1 policy)"
}

Every creative goes to human review independently of the buy. Assignments release if the buy is cancelled; the creative and its review state persist, so a later buy can reuse an already-approved creative.

08 The human gate

There are two gates, and they do different jobs.

Gate 1 · machine

Runs on every commit. Schema validation with machine-readable error codes, plus eleven policy pattern groups that flag rather than block. Returns machine_gate: "pass" and any policy_flags in the response. Fast, deterministic, no human involved.

Gate 2 · human

Every surviving buy and every creative waits for a person. The auto-accept floor is $0, which means there is nothing that clears it — a one-dollar buy and a ten-thousand-dollar buy both wait. Decisions arrive by signed webhook if you supplied a webhook_url.

This is the part of the stack that is an argument, not just an implementation. An agent can be delegated the authority to spend, but that authority is conferred by a party who remains answerable for it — it cannot be computed by the agent itself. The $0 floor is what that claim looks like when you have to write it down in code.

Practical consequence for you: build for asynchronous settlement. Treat pending as a normal terminal state for a session, not an error, and let the webhook or a later poll close the loop.

09 Serving

Once approved, the buy competes for two fixed slots on essay pages. The page calls the agent’s decision endpoint with the slot, the essay slug, the format and the session’s synaptic segments; the agent picks a creative or falls back to a house ad.

SlotPosition
essay-midMid-essay, inside the reading flow.
essay-relatedBeside the related-essays module at the end.

The three serving endpoints

  • /serve — the decision. Takes slot, page, fmt, and optional syn segments. Returns the winning creative, or a house ad when nothing matches.
  • /px — the impression beacon. Fires on real render, not on ad call.
  • /go — the click redirect.

Beacons are first-party and aggregate-only: one row per day, slot, page, segment and buy. No per-user records exist to leak, which is the same reason the audiences are session-scoped in the first place. An unsold slot fills with a house ad, and the house-fill rate is published rather than hidden.

10 Delivery reporting

Impressions, clicks, spend at CPM, pacing against budget, broken out by day. This is the call that closes the loop back at your agent.

curl -s -X POST https://sell.nofluffadvisory.com/mcp \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer demo-yourname' \
  -d '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_media_buy_delivery",
    "arguments": {
      "media_buy_id": "mb_dc593a7ea963"
    }
  }
}'
Response
{
  "adcp_version": "3.1",
  "status": "completed",
  "media_buy_id": "mb_15cf038726e0",
  "media_buy_status": "pending_creatives",
  "totals": { "impressions": 0, "clicks": 0, "spend": 0, "currency": "USD" },
  "pacing": {
    "budget": 1, "spend": 0,
    "budget_utilization": 0, "expected_utilization": null,
    "pacing_status": "on_track", "projected_exhaustion_date": null
  },
  "by_day": [],
  "measurement_basis": "First-party impression beacons fired on real creative render; aggregate counts only."
}

The publisher’s own numbers

Public, no auth: trailing 28-day totals including the house-fill percentage. If the inventory is mostly unsold, this endpoint says so.

curl -s https://sell.nofluffadvisory.com/stats/serve
Response
{
  "window": "trailing 28 days",
  "totals": {
    "impressions": 24, "clicks": 1,
    "house_impressions": 3, "paid_impressions": 21,
    "house_fill_pct": 12.5
  },
  "by_day": [
    { "date": "2026-08-04", "impressions": 11, "clicks": 1, "house_impressions": 3 },
    { "date": "2026-08-05", "impressions": 4,  "clicks": 0, "house_impressions": 0 }
    … remaining days trimmed
  ],
  "basis": "First-party impression beacons fired on real creative render…"
}

11 The whole loop in one script

Save it, change the key, the brand domain and the image URL, and run it. It stops where it should: at the human gate.

#!/usr/bin/env bash
set -euo pipefail
AGENT="https://sell.nofluffadvisory.com/mcp"
KEY="demo-yourname"

call () {  # call <tool> <json-args> [auth]
  local auth=()
  [ "${3:-}" = "auth" ] && auth=(-H "authorization: Bearer $KEY")
  curl -s -X POST "$AGENT" -H 'content-type: application/json' "${auth[@]}" \
    -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"$1\",\"arguments\":$2}}" \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["result"]["content"][0]["text"])'
}

echo "1. What is for sale?"
call get_products '{}'

echo "2. Buy \$1 of the interop SKU, targeted at one synaptic segment."
BUY=$(call create_media_buy '{
  "buyer_ref":"loop-demo",
  "buyer_agent":"https://your-agent.com",
  "brand":{"name":"Your Brand","domain":"yourbrand.com"},
  "packages":[{"product_id":"prod_interop_test",
               "targeting":{"segment_ids":["syn_agentic_standards"]}}],
  "budget":1,"currency":"USD"
}' auth)
echo "$BUY"

MB=$(echo "$BUY" | python3 -c 'import sys,json; print(json.load(sys.stdin)["media_buy_id"])')
PK=$(echo "$BUY" | python3 -c 'import sys,json; print(json.load(sys.stdin)["packages"][0]["package_id"])')

echo "3. Attach a creative (real hosted PNG/JPEG/WebP, under 300KB)."
call sync_creatives "{
  \"media_buy_id\":\"$MB\",
  \"creatives\":[{\"creative_id\":\"cr-1\",\"format_id\":\"display_728x90\",
    \"image_url\":\"https://cdn.yourbrand.com/banner-728x90.png\",
    \"click_url\":\"https://yourbrand.com/landing\",
    \"alt_text\":\"Your Brand\"}],
  \"assignments\":[{\"creative_id\":\"cr-1\",\"package_id\":\"$PK\"}]
}" auth

echo "4. Now it waits for a human. Poll delivery once approved."
call get_media_buy_delivery "{\"media_buy_id\":\"$MB\"}"  auth

Or point an MCP client at it

Claude Desktop, Cursor, or anything else that speaks MCP. Drop this in your client config and the ten tools show up natively.

{
  "mcpServers": {
    "nofluff-sales-agent": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://sell.nofluffadvisory.com/mcp",
               "--header", "Authorization: Bearer demo-yourname"]
    }
  }
}

12 Tool reference

ToolAuthWhat it does
get_adcp_capabilities public Version support, governance posture, tool list, creative policy.
list_creative_formats public The two display formats and their asset requirements.
get_products public The catalog. Pass a brief to compose a curated product.
create_media_buy bearer Commit a buy. Machine gate, then human review. Idempotent.
update_media_buy bearer Pause, resume, cancel, or change budget / end_date.
sync_creatives bearer Upsert creatives and bind them to packages.
list_creatives bearer Review status, policy flags, assignments.
get_media_buys bearer Your buys, with status and governance state.
list_accounts bearer Your auto-provisioned buyer account.
get_media_buy_delivery bearer Impressions, clicks, spend, pacing, by day.

13 Errors you may hit

ErrorWhat happenedFix
401 unauthorizedA write tool with no bearer, or a token that is not demo-*.Add the Authorization header.
IMAGE_TYPE_INVALIDimage_url is not PNG, JPEG or WebP.Serve a real raster image. SVG and data: URIs are rejected.
IMAGE_TOO_LARGEThe image exceeds 300KB.Compress it.
image_unverifiedThe HEAD probe failed or timed out (4s).Not fatal — it flags for human review. Check the CDN allows HEAD.
VERSION_UNSUPPORTEDYou pinned an adcp_version outside 3.0 / 3.1.Pin 3.1 or omit the pin.
Buy never appearsYour brand domain ends in .example / .test / .invalid / .localhost.That is sandbox traffic by design. Use a real domain.

Tell me where it caught

What I want out of this is the first end-to-end AdCP transaction between two independently operated agents, with the snags written down. If you run a buying agent and something here broke, misled you, or needed a workaround, that is the useful part — more useful than a clean run.