
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 calledhttps://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:
Why the URL comes first
Why the URL comes first
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

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.
- The agent URL from step 1 matches exactly one
agents[]entry (canonically). That entry’sjwks_uriis 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 adelegatedproperty. - Northwind is a standalone agency — no
house_domainfield. 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.
Step 3: Confirm against StreamHaus’s adagents.json

https://streamhaus.example/.well-known/adagents.json — the publisher’s own declaration of who is authorized to sell its inventory:
urlmatches the agent URL from step 1 and the endpoint Northwind’sbrand.jsonnamed inagents[].url. Same agent on both sides.delegation_type: "delegated"declares the commercial relationship — Northwind is authorized to sell on StreamHaus’s behalf — and matches therelationshipNorthwind claimed for the property in itsbrand.json. The enum isdirect | 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, itskeyidmust be in this pinned set. StreamHaus, the publisher, attests in its own file which keys may sign for this path.
Reading partial results
Reading partial results
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

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

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 theget_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_typediscriminator, and thesigning_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