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.
- 01 What you’re talking to
- 02 Three things that will trip you up
- 03 Discovery
- 04 Targeting: synaptic audiences
- 05 Composing a product from a brief
- 06 Creating the media buy
- 07 Creatives (and banner specs)
- 08 The human gate
- 09 Serving
- 10 Delivery reporting
- 11 The whole loop in one script
- 12 Tool reference
- 13 Errors you may hit
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.
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": {}
}
}' $body = @'
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_adcp_capabilities",
"arguments": {}
}
}
'@
$r = Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/mcp' -Method Post `
-ContentType 'application/json' `
-Body $body
$r.result.content[0].text 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"
}' $body = @'
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}
'@
$r = Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/mcp' -Method Post `
-ContentType 'application/json' `
-Body $body
$r.result.content[0].text 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": {}
}
}' $body = @'
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_products",
"arguments": {}
}
}
'@
$r = Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/mcp' -Method Post `
-ContentType 'application/json' `
-Body $body
$r.result.content[0].text 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_id | CPM | Est. / mo | What it is |
|---|---|---|---|
prod_interop_test | $1 | 200 | Token-priced SKU for exactly this exercise. Start here. |
prod_ros_essays | $10 | 2,000 | Run of essays. The actual rate card. |
prod_agentic_standards | $18 | 400 | AdCP / AAMP / protocol governance cluster. |
prod_data_collab | $18 | 300 | Clean 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": {}
}
}' $body = @'
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_creative_formats",
"arguments": {}
}
}
'@
$r = Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/mcp' -Method Post `
-ContentType 'application/json' `
-Body $body
$r.result.content[0].text 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.
- 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
targetingfrom the package. Nothing else to learn. - B
Name the pages —
essay_slugsYou have read the site and know exactly which essays you want to sit beside.
Put slugs on the package. Deterministic, no matching involved.
- C
Name the audience —
segment_idsYou 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. - D
Describe it —
briefYou 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). - E
Use your own vocabulary —
concept_bridgeYou 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: trueto 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 Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/synaptic/catalog' | ConvertTo-Json -Depth 10 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
}
}
}' $body = @'
{
"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
}
}
}
'@
$r = Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/mcp' -Method Post `
-ContentType 'application/json' `
-Body $body
$r.result.content[0].text 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.reason — not_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"
}
}
}' $body = @'
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_products",
"arguments": {
"brief": "reach people evaluating agentic advertising standards"
}
}
}
'@
$r = Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/mcp' -Method Post `
-ContentType 'application/json' `
-Body $body
$r.result.content[0].text 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"
}
}
}' $body = @'
{
"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"
}
}
}
'@
$r = Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/mcp' -Method Post `
-ContentType 'application/json' `
-Headers @{ Authorization = 'Bearer demo-yourname' } `
-Body $body
$r.result.content[0].text 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
| Field | Why it matters |
|---|---|
brand | Required, { 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. |
budget | Required. Total USD, must be greater than 0 and at most 100,000. |
idempotency_key | Replays the canonical response for the life of the buy. Retry safely. |
webhook_url | HTTPS endpoint for signed status callbacks. Strongly recommended given the human gate. |
buyer_agent | Identifier or URL of your agent. Shows up in the approval queue — it is how a human knows who is asking. |
buyer_ref | Your 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"
}
}
}'
$body = @'
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_media_buys",
"arguments": {
"buyer_ref": "my-first-adcp-buy"
}
}
}
'@
$r = Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/mcp' -Method Post `
-ContentType 'application/json' `
-Headers @{ Authorization = 'Bearer demo-yourname' } `
-Body $body
$r.result.content[0].text
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_id Size Where it lands display_728x90728 × 90 Leaderboard. Reads well mid-essay on desktop. display_300x250300 × 250 Medium 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.
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"
}
]
}
}
}'
$body = @'
{
"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"
}
]
}
}
}
'@
$r = Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/mcp' -Method Post `
-ContentType 'application/json' `
-Headers @{ Authorization = 'Bearer demo-yourname' } `
-Body $body
$r.result.content[0].text
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.
Slot Position 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"
}
}
}'
$body = @'
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_media_buy_delivery",
"arguments": {
"media_buy_id": "mb_dc593a7ea963"
}
}
}
'@
$r = Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/mcp' -Method Post `
-ContentType 'application/json' `
-Headers @{ Authorization = 'Bearer demo-yourname' } `
-Body $body
$r.result.content[0].text
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
Invoke-RestMethod -Uri 'https://sell.nofluffadvisory.com/stats/serve' | ConvertTo-Json -Depth 10
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
$Agent = 'https://sell.nofluffadvisory.com/mcp'
$Key = 'demo-yourname'
function Invoke-AdcpTool {
param([string]$Tool, [hashtable]$Arguments = @{}, [switch]$Auth)
$payload = @{
jsonrpc = '2.0'; id = 1; method = 'tools/call'
params = @{ name = $Tool; arguments = $Arguments }
} | ConvertTo-Json -Depth 20 -Compress
$headers = @{}
if ($Auth) { $headers['Authorization'] = "Bearer $Key" }
$r = Invoke-RestMethod -Uri $Agent -Method Post `
-ContentType 'application/json' -Headers $headers -Body $payload
$r.result.content[0].text | ConvertFrom-Json
}
Write-Host '1. What is for sale?'
Invoke-AdcpTool -Tool 'get_products' | ConvertTo-Json -Depth 10
Write-Host '2. Buy $1 of the interop SKU, targeted at one synaptic segment.'
$buy = Invoke-AdcpTool -Tool 'create_media_buy' -Auth -Arguments @{
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'
}
$buy | ConvertTo-Json -Depth 10
$mb = $buy.media_buy_id
$pk = $buy.packages[0].package_id
Write-Host '3. Attach a creative (real hosted PNG/JPEG/WebP, under 300KB).'
Invoke-AdcpTool -Tool 'sync_creatives' -Auth -Arguments @{
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 })
} | ConvertTo-Json -Depth 10
Write-Host '4. Now it waits for a human. Poll delivery once approved.'
Invoke-AdcpTool -Tool 'get_media_buy_delivery' -Auth -Arguments @{ media_buy_id = $mb } |
ConvertTo-Json -Depth 10
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
Tool Auth What 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
Error What happened Fix 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.