Skip to main content
Sam stands at a wide marble counter facing a confident agent in a sharp blazer holding a clipboard of glossy ad placements — but the counter behind the agent is empty, with no logo, no signage, and no one else in sight A get_products response just hit Sam’s orchestrator. The seller — Northwind Media — quoted CTV inventory across StreamHaus, a sports network Acme Outdoor wants to be on. The CPMs look fair, the avails fit the flight, and the response arrived in under a second. Sam has never transacted with Northwind. He has no idea whether the agent that sent this response is actually authorized to sell StreamHaus inventory, whether StreamHaus is a real publisher under a parent house he recognizes, or whether a clever attacker registered northw1nd.example last week and is about to walk away with $25,000. He doesn’t need Northwind to convince him with a sales deck. He needs the protocol to make the chain verifiable in code — every link from the agent URL his orchestrator called, through Northwind’s brand identity, to StreamHaus’s authorization, back to a parent house he can recognize. His buyer agent walks the chain automatically; Sam reads the verdict. This walkthrough follows that chain through Sam’s eyes.
What this verifies and what it doesn’t. The chain answers who is authorized to sell. It does not answer who the legal entity behind the agent is (KYC, real operator), whether the avails reflect reality at delivery time (catalog accuracy, CPM, delivery), or whether the hosting infrastructure can be trusted (DNS, CDN, registrar). The bounded-honesty step names every limit explicitly. This is the C2PA “claim-not-certification” posture applied to inventory — the protocol carries the authorization claim and makes it verifiable, and stops there.

The chain at a glance

The connection itself plus three discoverable surfaces. Sam’s agent runs them in whatever order is convenient: The chain is bilateral by design: each fact is asserted by exactly the party with authority over it. The publisher decides who can sell its inventory. The brand owner decides what it owns. The seller decides which keys sign the requests and webhooks it sends. No third-party registry adjudicates between them — a misbehaving authorized seller is remediated by the publisher revoking the adagents.json entry, not by an in-protocol claim check. AdCP puts no transport signature on synchronous responses: TLS to the agent URL carries their integrity, and transport signatures apply to the requests and webhooks an agent sends (see Signed requests). The only signed response payloads are on verify_brand_claim and verify_brand_claims; get_products is not one of them. So the steps below are integrity-checked discoveries — canonical URL matches against authoritative files at well-known locations — plus the keys Sam will use to verify Northwind’s signed webhooks. The chain is only as strong as the publisher’s and seller’s control of their own DNS, hosting, and well-known endpoints. See Step 5 for what that implies.

Step 1: Anchor on the agent URL

Sam’s orchestrator called https://northwind.example/mcp over HTTPS. That URL is the agent’s identity: the TLS session authenticates the host, and the get_products response carries no signature. Sellers MUST NOT apply RFC 9421 response signing to synchronous responses, and buyers MUST NOT rely on it (Signed requests). So every later step asks one precise question: is the agent at this exact URL authorized to sell StreamHaus inventory? Signatures still matter once the buy is live. When Northwind sends Sam signed webhooks (delivery reports, status changes), Sam verifies each one before acting on it, using keys discovered from the files in steps 2 and 3:
If Sam can’t say which agent he is talking to, nothing downstream can be checked. Every later match (Northwind’s agents[] entry, StreamHaus’s authorized_agents[] entry) is against this URL. Compare URLs with the AdCP URL canonicalization rules, not byte equality.For webhooks, maxAge and requireCreated are not optional: without a freshness window the same signed webhook can be replayed indefinitely. Tune maxAge to your clock-skew budget; 300s is a common starting point. Webhook key discovery is specified in Webhook callbacks.

Step 2: Read Northwind’s brand.json

Sam opens a leather-bound folder on Northwind's reception desk — inside is a single embossed page declaring the agency's portfolio, signing keys, and authorized operators, with a glowing seal at the top Sam’s agent fetches https://northwind.example/.well-known/brand.json. This is Northwind’s self-declaration: who it is, which agents it runs, which publishers it says it sells for, and where its signing keys live.
Two things matter here:
  1. The agent URL from step 1 matches exactly one agents[] entry (canonically). That entry’s jwks_uri is where Northwind publishes the keys its webhooks are signed with. Sam now has a binding from the agent to a self-declared brand identity, and Northwind claims StreamHaus’s app as a delegated property.
  2. Northwind is a standalone agency — no house_domain field. There is no parent house claim to verify on Northwind’s side. The authorization claim that matters lives on the publisher’s side, in step 3.
brand.json is published over HTTPS by the entity it describes. A buyer agent that pipes raw fields into an LLM prompt without schema-validating them first is taking adversarial input from a counterparty. Validate against the brand.json schema before parsing, and never pass attacker-controlled string fields (names, description, custom keys) into an LLM context without sanitization.

Step 3: Confirm against StreamHaus’s adagents.json

Sam holds two documents side by side — Northwind's brand.json on the left listing StreamHaus, and StreamHaus's adagents.json on the right listing Northwind — and the matching delegation_type field on both glows green as the chain locks in Sam’s agent fetches https://streamhaus.example/.well-known/adagents.json — the publisher’s own declaration of who is authorized to sell its inventory:
This is the bilateral lock:
  • url matches the agent URL from step 1 and the endpoint Northwind’s brand.json named in agents[].url. Same agent on both sides.
  • delegation_type: "delegated" declares the commercial relationship — Northwind is authorized to sell on StreamHaus’s behalf — and matches the relationship Northwind claimed for the property in its brand.json. The enum is direct | delegated | ad_network; the publisher chooses which fits.
  • signing_keys[] pins the keys Northwind may use for signed webhook deliveries about StreamHaus inventory. When Sam verifies a delivery webhook from step 1, its keyid must be in this pinned set. StreamHaus, the publisher, attests in its own file which keys may sign for this path.
Now Sam has the answer to “who is authorized to sell”: the agent he is talking to is named in the publisher’s own authorization file, with a matching commercial relationship, and its delivery webhooks must be signed with a key the publisher pinned. The chain is closed.
The two files are published independently, so Sam’s agent can find either side without the other:If Northwind and StreamHaus were the same company (first-party inventory), Northwind’s brand.json would list the property with relationship: "owned" and there would be no delegation to confirm.

Step 4: Walk the parent house

Sam stands in front of a wall display showing a brand-portfolio hierarchy — a large parent-house emblem at top, with the publisher emblem below it connected by a glowing teal line, and other sibling sub-brand emblems branching off the parent Acme Outdoor’s inclusion list resolves at the parent-house level — they trust Sportshaus Holdings’ family of brands. StreamHaus is a sub-brand, so Sam’s agent walks one hop further. StreamHaus’s own brand.json declares its parent:
Then the parent’s brand.json reciprocates:
The reciprocity rule: StreamHaus’s house_domain ↔ Sportshaus Holdings’ brand_refs[].domain. Both sides agree. Two distinct concepts ride along this step, and the doc is careful not to conflate them:
  • The keller_type on the child (endorsed) describes the brand-architecture relationship — how the sub-brand is positioned beside the parent. It is Keller-architecture metadata, not a commercial authorization.
  • The commercial relationship that lets Northwind sell is delegation_type: "delegated" from step 3, which lives on the publisher (StreamHaus), not on the parent house.
Sportshaus Holdings is on Acme Outdoor’s inclusion list. The chain Sam’s agent just walked is: agent URL → Northwind’s brand.json → StreamHaus’s adagents.json → StreamHaus’s and Sportshaus Holdings’ mutual brand.json declarations. In registry terms this house relationship is relationship_trust: "mutual". One authenticated connection, three discoverable surfaces, zero phone calls for the authorization question.

Step 5: Know what the chain does not prove

Sam sits at his desk with the sealed document beside him, holding his phone to his ear — the technical chain is complete, and now he's calling a person at the publisher to confirm the human-layer details the protocol cannot attest The authorization chain is closed. Northwind is for the first time a counterparty Acme Outdoor will actually transact with at meaningful spend, so Sam picks up the phone. Not for the protocol’s sake — the chain told him what it can tell him. For everything the chain can’t. Three named gaps live on the right side of that table. The human-layer gap. AdCP does not carry an operator/human KYC primitive. The chain says “this agent is authorized to sell, and these keys sign its webhooks” — it does not say “a verified human at a verified company is on the other side of this agent.” KYC is the membership and account layer. For a new counterparty above Acme’s threshold, Sam still escalates to a human check, exactly as he would for any meaningful new vendor. The hosting-layer gap. The chain trusts whoever controls northwind.example, its DNS, TLS, and CDN. A registrar takeover, a CDN compromise, or a mis-issued TLS certificate substitutes the entire chain — the attacker serves attacker-controlled brand.json, adagents.json, and JWKS over a valid-looking pipeline. There is no public key-transparency log in 3.x; first-encounter trust is trust-on-first-use, and revocation is detectable only by re-fetching. A buyer client that has previously transacted with Northwind pins the seen kids and warns on rotation; a buyer client on first encounter does not have that signal. See #3925. The delivery-time gap. Catalog accuracy is not protocol-attested. Publishers do not sign individual product entries, and per-product attestation does not match how inventory operates in production — forecasts drift, prices move, supply is dynamic. A misbehaving authorized seller is remediated by the publisher revoking the adagents.json entry, not by an in-protocol claim check. Delivery-time truth lives in measurement and billing reconciliation. This is the C2PA “claim-not-certification” posture. The protocol carries the authorization claim and makes it cryptographically verifiable. It does not — and at the protocol layer should not — replace the human-layer, hosting-layer, or delivery-layer checks a buyer would do for any meaningful new counterparty.

What Sam does next

Sam’s client logs the verification result with the get_products response — including the captured brand.json, adagents.json, and JWKS bytes at decision time, not just pointers to live files. Months from now, an auditor can re-check the authorization, and re-verify any signed webhooks, against those captured artifacts and reproduce Sam’s decision exactly. Re-fetching the live .well-known/ files is not sufficient — they are mutable, and a single key rotation or domain transfer would invalidate a naive replay. The verification result travels with the candidate plan into Sam’s governance flow, where Jordan’s governance agent applies Acme’s policy checks before any spend is committed.

Where to go from here

  • brand.json reference — full schema for self-declarations, parent-house portfolios, and Keller architecture metadata
  • Seller setup — how publishers, networks, and SSPs publish seller identity and authorization files
  • adagents.json reference — publisher-side authorization, the authorization_type discriminator, and the signing_keys[] JWK shape
  • verify_brand_claim — Tier-2 implementer guide for delegating verification to a brand agent
  • Security model — three-party governance and trust posture
  • Request signing — RFC 9421 details, key rotation, transparency-log roadmap
  • Trust & Security — CISO-facing surface map; this walkthrough is the buyer-facing companion
  • AAO Verified — continuous behavioral conformance attestation, layered on top of the identity chain above