Measured Agentic Ad Lab
Agent Discovery Files: Six Live adagents.json Files, Zero Signing Keys
Every adagents.json a publisher serves today, swept on 16 August 2026: six distinct files across thirteen domains, two of them schema-invalid, none of them pinning a key.
18 min read 10 chapters
Original data. Collected on the date below, by the method stated in each section.
If you own inventory and anyone has asked you for a file, publish adagents.json on your own origin
this week and populate its signing_keys. If neither is true, read the schema and wait. The two
halves move on different clocks: the file is yours to publish on Monday, while signing_keys is a
per-entry field inside each authorized_agents object, not a top-level one, and it can only be
filled once that agent hands you the public half of a keypair it mints itself, so publish with the
entries you have and add each key as its agent produces one. If you own inventory and nobody has
asked yet, you are between the two branches: the file is an afternoon’s work and the registry will
validate it on request, so waiting buys nothing. One static file at
https://{yourdomain}/.well-known/adagents.json, a released JSON Schema, a validator that
answers an unauthenticated GET, and the answer to “who is allowed to sell my inventory” stays on your
domain instead of in somebody else’s database. It is the cheapest useful thing a publisher can do in
this whole category and the thing most often wrong on the first attempt.
Swept on 16 August 2026: six distinct files in production, two of them rejected by the released schema, none of them carrying a key. The only reader anyone can point to is the registry that validates those files on request; no buying agent has been observed fetching one at decision time, which is the measured fact to weigh against the instruction above before you fund anything larger than the file itself.
Publish the file, then pin the keys
The keys are the part publishers skip, including the largest publisher currently doing this at all. Skipping them is the mistake. The pin is the only mechanism in either stack that still holds when an agent’s own domain is compromised, AdCP already makes it a MUST for the four mutating operations, and the rotation rules cost you one coordination step.
On the buy side, publish brand.json for the same reason. Then read every registry listing in either
stack as a catalog entry, because neither registry has a field that means “a publisher agreed to
this”.
If you are building against AAMP, do not hard-code a registry hostname: one of the three named
across its own repositories has no DNS record at all. Configure it, cache the verdicts, decide your
own access tiers and treat the registry’s answer as one input to them, and build nothing that depends
on authorized-agents.txt until that file has a schema.
What publishing buys you today is a record the registry will validate on request, plus the option on the day a buyer starts reading them. That is a model worth being ready for, not a deadline.
Six files, thirteen domains, no keys anywhere
The instruction is that blunt because the live population is small enough to list in full.
Method, 16 August 2026. GET https://agenticadvertising.org/api/registry/publishers?limit=200 needs
no token and returns the eleven domains AgenticAdvertising.org (AAO) members have declared as
publishers: eight Talpa Network domains, plus equalize.cloud, www.straitstimes.com and
sofascore.com. Add cafemedia.com and mediavine.com; the four domains the
vendor diligence page spot-checked (straitstimes.com,
vox.com, cnn.com, theguardian.com, the first of them already on the AAO list); and seven
controls, the two protocol organisations’ own sites adcontextprotocol.org and
agenticadvertising.org, three SSPs sovrn.com, pubmatic.com and magnite.com, and two large
publishers nytimes.com and reddit.com. Twenty-three distinct domains. Fetch
/.well-known/adagents.json on each, following redirects. Thirteen answer 200 with
application/json, carrying six distinct documents.
| File | Bytes | last_updated | Authorized agents | signing_keys |
|---|---|---|---|---|
cafemedia.com | 3,353,140 | 2026-08-05 | 1, interchange.io, publisher_properties over 6,800 publisher domains; 6,804 properties[] entries under 1 tag | none |
www.mediavine.com | 3,272,011 | 2026-07-06 | 2, both interchange.io, property_tags; 8,865 properties[] entries under 24 tags | none |
kijk.nl and 7 more Talpa Network domains | 8,949 | 2026-06-19 | 3: swivel.ai, Triton Digital, interchange.io, all direct | none |
equalize.cloud | 2,051 | absent | 2, both self-hosted MCP endpoints | none |
www.vox.com | 1,963 | absent | 1, salesagent.voxmedia.com | none |
www.straitstimes.com | 210 | 2026-07-31 | pointer to sales-agent.adzymic.ai | none |
CafeMedia/Raptive serves the largest file, at the apex, with no redirect. Mediavine has the longer
properties[] array instead, 8,865 entries against CafeMedia’s 6,804, and sorts them under 24
property tags where CafeMedia uses one. One publisher can carry that much in this format without
the model falling over. One file of that size against 404s everywhere else says the model ships.
Now the uncomfortable half. Mediavine’s two entries both point at https://interchange.io/, both
authorization_type: property_tags with property_tags: ["scope3_aee"], both
delegation_type: ad_network, differing only in the free-text authorized_for string: display
inventory on one, video on the other.
Machine-readably they are identical, and the distinction the publisher meant sits in prose no
resolver can act on.
The string signing_keys appears in none of the six. Zero, in the largest live file in production,
the one the spec’s MUST is aimed at.
The other ten domains served nothing: eight 404s, adcontextprotocol.org, agenticadvertising.org,
sovrn.com, cnn.com, theguardian.com, nytimes.com, pubmatic.com and magnite.com, plus 403s
from reddit.com and sofascore.com. sofascore.com is on the registry’s own publisher list, and
its registry record carries last_http_status: 403, last_bytes: 48 and a null last_validated, so
the registry hit the same refusal this sweep did. A publisher-list entry records a member’s own
declaration, not a file anyone has read.
adagents.mdx says a publisher-origin not-found result is not proof that no community mirror
exists, and the AAO mirror at creative.adcontextprotocol.org/translated/meta/adagents.json
answers 200 with 26,485 bytes. The sweep establishes who self-hosts, not how many documents exist.
Note the shape the instruction enters: interchange.io is an authorized agent in three of the six files and the only agent in two, so the live population is three managed networks pointing at one agent plus three single-publisher files.
Four fixes, and the record the registry already keeps on you
Serve it at the apex and at www. Mediavine’s apex 301s to www, and so do vox.com and
straitstimes.com. adagents.mdx tells publishers to deploy the file at the final resolved URL too,
and warns that “a redirect chain that ends in 404 means the file is missing at the canonical host”.
The AAO registry follows the hop and records Mediavine as hosting.mode: self_redirected with a 200
and 3,272,011 bytes. CafeMedia, kijk.nl and equalize.cloud answer 200 at both names.
Set authorization_type on every entry, with the field that variant requires. It is the
most-asked question about this file, filed as
adcp#4776, and where two of the six
production files fail. Vox and Equalize both omit it, so
GET /api/public/validate-publisher?domain=vox.com comes back valid: false with the discriminator
named. A live file can stay schema-invalid for months and nobody notices, which the
vendor diligence page turns into a check you run on a
counterparty.
Serve Cache-Control: max-age=300. CafeMedia sends max-age=600 today, kijk.nl 3,600,
Equalize max-age=0, must-revalidate, Straits Times only stale-while-revalidate, and Mediavine and
Vox no directive at all, which drops verifiers onto the one-hour default. Five minutes is the number
the revocation rules assume, and the publisher is the only party who can settle it.
Populate signing_keys. Nothing else on this list matters if an agent’s domain gets taken over.
How the pin works, and why no validator will fail you for skipping it, is a section of its own below.
Then read your own record. GET https://agenticadvertising.org/api/registry/publisher?domain= needs
no token and returns hosting.mode, discovery_method, last_http_status, last_bytes and
last_validated. Today kijk.nl came back with a last_validated seconds old, generated by the
lookup itself, while CafeMedia’s verdict is frozen at 2026-05-20 and 2,604,617 bytes against a live
file of 3,353,140 updated on 5 August. Vox is recorded aao_hosted, which the OpenAPI defines as a
publisher stub pointing at AAO’s copy, while vox.com serves its own full document at 200. Nothing
in the response flags which of those you are holding except the timestamp.
That GET is the free check. POST /api/adagents/validate, the endpoint the documentation points at,
answers an anonymous request with 403 {"error":"CSRF validation failed"}, although the OpenAPI
document carries security: [] and the operation carries no security of its own, while writes still
need a bearer token. The revalidate
endpoint is not yours either: its description opens “Admin-only endpoint for support/operator tooling
to synchronously fetch a publisher’s live /.well-known/adagents.json”, rate-limited to five minutes
per domain.
The agenticadvertising.org registry reads it, demonstrably. What is missing is a buying agent
fetching the file at decision time. On the write side there is a released schema with a validator in
front of it; on the read side, the validator is the whole of the evidence. The nearest thing to a
second reader is a pair of SDKs that resolve one document differently, a TypeScript consumer and a
Python consumer disagreeing in adcp-client’s own issue tracker, neither observed inside a media
buy. That registry listed 23 agents when
adcpexplorer probed it on 12 August 2026,
three of which answered an anonymous product call.
What goes in adagents.json
The fixes above all name fields; this is the surface they come from.
One file at one path. Its schema ships in the released registry as
dist/schemas/3.1.13/adagents.json, generated 2026-08-04, a oneOf over two variants.
The pointer variant has three properties and requires one, authoritative_location, an HTTPS URL
where the real file lives. Three rules in docs/governance/property/adagents.mdx stop that redirect
turning into a maze: “Same Schema”, “No Nested References”, and “Single Hop”, which reads “Only one
level of URL indirection is allowed.” Straits Times ships one: 210 bytes pointing at its sales
agent’s host.
The inline variant carries sixteen top-level fields and requires exactly one of them,
authorized_agents. A complete one from docs/brand-protocol/seller-setup.mdx, with the contact
block dropped for length:
{
"$schema": "https://adcontextprotocol.org/schemas/v3/adagents.json",
"properties": [
{
"property_id": "streamhaus_ctv",
"property_type": "ctv_app",
"name": "StreamHaus CTV App",
"publisher_domain": "streamhaus.example",
"identifiers": [{ "type": "bundle_id", "value": "com.streamhaus.ctv" }]
}
],
"authorized_agents": [
{
"authorization_type": "property_ids",
"url": "https://ads.streamhaus.example/mcp",
"authorized_for": "StreamHaus direct CTV inventory",
"property_ids": ["streamhaus_ctv"],
"signing_keys": [
{
"kid": "streamhaus-sales-prod-2026",
"kty": "OKP",
"alg": "EdDSA",
"crv": "Ed25519",
"x": "w8zcY1LZqV4n1oKbfyq3n2q3sL2uV3z7kEw1m9Qjv4A",
"use": "sig"
}
]
}
]
}
That is the whole thing. Every entry inherits core/authorized-agent-base.json, which requires url
and authorized_for and offers signing_keys, encryption_keys and last_updated. The rest is
scope, and it lives in the six discriminator variants inside adagents.json itself, each naming its
own required companion field: property_ids, property_tags, inline_properties (which requires
properties), publisher_properties, signal_ids, signal_tags. The first four also take
collections, countries, delegation_type (direct, delegated, ad_network), placement_ids,
placement_tags, exclusive and an effective_from / effective_until window. The two signal
variants take none of that, worth knowing before you write a validator expecting a country on every
entry.
An empty authorized_agents array is legal. The schema names the case it is for, a catalog-only
mirror published for a platform that has not adopted AdCP, where there is no sales agent to
authorize, and then spends four negative clauses telling validators not to read the empty array as
deny-all, authorize-all, a revocation, or an error. Nobody writes that for a hypothetical; somebody
hit all four readings in the wild and the schema carries the scar.
If your inventory sits inside a manager network, one clause of the ads.txt fallback is yours rather
than your manager’s. A validator MAY read MANAGERDOMAIN= entries from https://{publisher}/ads.txt,
but only when the direct fetch returned 404 or an S3/CloudFront-style 403 AccessDenied body; a
generic 403, a 500, a timeout, malformed JSON or a schema failure does not trigger it. It takes the
last eligible entry, one hop, with cycle detection. A trailing comment carrying the token noagents
opts you out entirely: MANAGERDOMAIN=example.com #NOAGENTS, which clients MUST honour. Both stacks
descend from ads.txt, traced on the standards map.
On the buy side, brand.json sits at
/.well-known/brand.json: five variants in 3.1.13, split the way adagents.json splits, two
redirects and three actual documents. Somebody asked AdCP for exactly this paragraph in
adcp#5094, which records that the Brand
Protocol documentation explains brand.json from the buyer’s side and leaves the sell-side use
implicit. The split is one sentence: adagents.json asserts who may sell you, brand.json asserts
who the buyer is, and each side verifies the other’s file at its own origin.
Field-by-field reference lives on AdCP Explorer.
The signing key pin inverts key discovery
Key objects come from core/agent-signing-key.json, which requires a kid and a kty, and the
governance documentation makes the pin normative:
if the publisher’s
adagents.jsonentry for an agent containssigning_keys, the verifier MUST reject any signature whosekeyidis not in that pinned set, regardless ofjwks_uricontents. The pin is authoritative; the agent-hosted JWKS is advisory and MUST NOT override it.
Normally the agent tells you its keys and you believe it. Here the publisher pins them and the
agent’s own JWKS loses the argument. The obligation covers “any authorized agent whose delegated
scopes include mutating operations”, which in the 3.x catalog means create_media_buy,
update_media_buy, sync_creatives and update_performance_index, and the same sentence extends it
to any future task flagged as mutating.
Nothing enforces it. signing_keys is optional in core/authorized-agent-base.json, and
adagents.mdx says so out loud, a few lines under its own MUST: “A follow-up is tracked to promote
signing_keys from optional to required at the schema level for mutating-scope authorizations; the
prose requirement above is the normative floor until that schema change lands.” That is the
mechanical reason six production files carry no keys and still pass a validator. A MUST no validator
can fail is a MUST at zero adoption.
Rotation is the obvious operational objection, and the same document answers it. The agent mints its
own keypair and publishes it under its own jwks_uri; the publisher pins the public half, so a
rotation needs both parties to move. It does not turn into a lockout, though. Verifiers SHOULD cache
the pinned set no longer than the Cache-Control max-age on adagents.json, defaulting to one
hour when the publisher sends no directive. On an unknown keyid a verifier MUST force-refresh
before rejecting for good. And publishers MAY carry both keys through the window, since the pinned
set is unordered and presence in it is sufficient. Keys also take an optional revoked_at that
verifiers MUST honour.
Two things are left loose. adagents.mdx defaults the pin cache to an hour while
core/agent-signing-key.json reasons about revocation on a “recommended: 5 minutes” TTL for the same
document; use the shorter one. And the pin says nothing about publisher-domain compromise. Rewrite
adagents.json and you rewrite the pin, so the first retrieval is trusted on TLS alone. AdCP agrees
in the same section and names the work item that would close it, a root-of-trust and key-transparency
track that has not landed.
Both stacks copied ads.txt. One of them shipped the file.
A buying agent arrives at a publisher’s domain with one question: who is allowed to sell this
inventory? Both protocol stacks landed on the same two-layer answer. A file the publisher hosts on
its own origin, then a registry that fetches that file, validates it, and keeps the agents’ own
identity records. Both say where they got it: AdCP writes the managerdomain fallback above, and IAB
Tech Lab’s registry announcement lists “Creating the ability to verify that seller agent actually
represents a property (like ads.txt for agents)” as a roadmap item. They shipped opposite halves of
it.
| Layer | AdCP | AAMP |
|---|---|---|
| Publisher-hosted authorization file | /.well-known/adagents.json, released JSON Schema in 3.1.13, six documents live | /.well-known/authorized-agents.txt, named three times in eight repositories, all three in Python |
| Registry over the top | agenticadvertising.org, 105 API paths, reads unauthenticated (security: [] on the whole OpenAPI document) | registry.iabtechlab.ai, 40 agents listed, reads gated by an HS256 JWT with issuer IAB |
Files versus registry is the wrong axis to argue about. AdCP’s registry has something authoritative to index and AAMP’s does not, which is why the instruction above comes out the same whichever stack your counterparty is on.
Neither stack has read the other. At the commits this site pins, zero files across the eight IAB
agentic repositories mention adagents and zero lines mention AdCP; the silence runs both ways and
is priced in AdCP vs AAMP. Both stacks do serve an A2A card at
.well-known/agent.json, and that one shared artifact is a format coincidence. The one detailed
comparison anybody has published, adgentek.ai/blog/adcp-vs-aamp/, describes a hybrid flow in which
a buyer resolves brand.json and adagents.json and then cross-checks the IAB
agent registry, and no file in either corpus
implements it.
If you are a publisher, you are done. What follows is for anyone being asked to integrate against AAMP.
Registry membership is worth money
Of the two layers in the table above, the AAMP registry is the one with a price attached.
seller-agent/docs/api/agent-discovery.md publishes a five-value
trust status and maps each value to a maximum
access tier. Read the table with the sentence
that opens its section: “The seller agent maintains a local registry of known buyer agents.” Local.
| Trust status | How it is reached | Maximum access tier | Pricing visibility |
|---|---|---|---|
unknown | default on first contact | public | published rate card only |
registered | found in the AAMP agent registry, verified automatically | seat | seat-level pricing |
approved | seller operator action | advertiser | full advertiser pricing |
preferred | seller operator action | advertiser | full pricing plus priority access |
blocked | seller operator action | none | no access |
The five values are ordered, and the effective tier is the minimum of the trust ceiling and the tier
the buyer claims. Claim advertiser on registered trust and you get seat.
Read the second row commercially. registered is granted by registry membership alone, and it moves
a buyer from published rate card to seat-level pricing. Being in the registry is worth money, and
that is the best argument for the registry-first direction in either corpus.
Then the wire contract contradicts itself. AgentTrustVerification.json hard-codes the five values
and states that trust “is verified against the AAMP registry (the IAB Tech Lab agent discovery and
trust registry), never self-asserted”. SANDBOX_REGISTRY.md, in the same repository, says the real
registry “has no self-asserted access-tier / trust-tier enum — trust is the verification_status +
domain_verified + iab_member triplet”, and files the whole trust-tier surface under “Legacy AAMP
trust-tier surface”, “a compatibility layer”. That ordering is the seller agent’s local policy. The
registry does not issue it.
AAMP discovery is three calls, and the default points at localhost
Nobody has run this against a production registry. The buyer agent’s default registry URL is
http://localhost:8080/agent-registry, so an unconfigured deployment discovers nobody.
The flow is three calls, per buyer-agent/docs/api/seller-discovery.md.
GET /agents?agent_type=seller&capabilities=ctv against the registry, then
GET /.well-known/agent.json against whichever seller came back, then GET /verify?agent_url=...
back to the registry for a trust verdict. The client caches verdicts for 300 seconds by default.
The agent card it fetches is a real document:
authentication.schemes (api_key, bearer), skills[] (discovery, pricing, proposals,
negotiation, deals), inventory_types,
supported_deal_types (pg, pmp, preferred_deal, private_auction), and
capabilities.protocols, which is ["opendirect21"]. That is OpenDirect 2.1, the pre-agentic
direct-buy API. The doc notes that “a2a will be listed once an inbound A2A server ships”. That
path, .well-known/agent.json, is the Agent2Agent card convention: the address
itself advertises that an agent speaks A2A, and the card parked there lists one protocol, the legacy
one.
Which registry it calls is unsettled. Three files name three different production bases, resolved on 16 August 2026.
| Named in | Base URL | Resolved 16 August 2026 |
|---|---|---|
buyer-agent/docs/api/seller-discovery.md | https://registry.aamp.iab.com/agent-registry | no DNS record; iab.com resolves |
seller-agent/src/ad_seller/config/settings.py | https://tools.iabtechlab.com/agent-registry | 200 |
iab-agentic-primitives/SANDBOX_REGISTRY.md | https://registry.iabtechlab.com | 302 to https://registry.iabtechlab.ai/ |
The first row is the one to stop on. A production hostname in a shipped reference implementation’s
own documentation was never created, and the third row is the base the primitives library pins as
IAB_PROD.
The live service is real. GET https://registry.iabtechlab.ai/api/agents returns 401 with
"error":"Authentication required", an envelope matching what SANDBOX_REGISTRY.md documents, while
the browse page renders 40 agent cards with Domain, GPP and Endpoint verification indicators. GPP is
IAB Tech Lab’s own Global Privacy Platform consent framework, so a third of the trust signal is
“carries an ID from another IAB standard”. That page now offers visitors a short-lived credential for
REST or MCP; registering an agent still needs a Tools Portal login. Whether the reference client’s
iab_member field exists on the live service is unconfirmed.
authorized-agents.txt: the file IAB never specified
Then the file the whole model would rest on. AAMP’s answer to adagents.json has a name and nothing
else. iab_agentic_primitives/registry_client.py defines a class called AgentAuthorization with
this docstring: “Result of POST /api/agents/validate/agent (public endpoint). Whether a publisher
domain authorizes a given agent URL, per the publisher’s /.well-known/authorized-agents.txt file.”
That filename occurs three times across eight repositories, and the other two occurrences are an
admission and a test. IAB’s own sandbox test double does not fetch the publisher’s
authorized-agents.txt, and IAB’s own reference client ships a test asserting that a publisher
authorization check returns false, because there is no file to check. No schema for the file exists
in the repository, and nothing anywhere in the corpus describes its syntax, so two implementations
would have nothing to agree on.
The corroboration comes from IAB Tech Lab’s own wire contract. STANDARDS_GAP_REPORT.md is
machine-generated and guarded by a drift test, so it is not an offhand remark in a README. It lists
“IAB Tech Lab AAMP agent discovery and trust registry” as UNVERIFIED and gives the action to close
it: “Obtain the IAB Tech Lab AAMP (agent discovery and trust registry) specification”. That is IAB
Tech Lab’s own reference implementation recording that it could not obtain IAB Tech Lab’s discovery
specification, and two-thirds of the standards that report tracks are blocked the same way, on a
specification IAB has not published.
Discovery has a request shape and no answer shape:
AgentDiscoveryRequest.json exists with a single required field, agent_url, and there is no
AgentDiscoveryResponse.json anywhere in the repository. The Node.js service that does run the
registry, IABTechLab/agent-registry, is not in the AAMP hub README at all, while the 22-line
registry-agent-example is. The rest of that maturity picture is on the
AAMP verdict.
The AAMP registry is real, live, and better engineered than the documentation around it suggests. It
has nothing authoritative to index. It can tell you an agent exists and that someone DNS-verified its
domain; it cannot tell you a publisher authorized it, and there is no cryptographic fallback either,
because no file across the eight repositories mentions signing keys. One corner of the corpus does
specify key material: agentic-audiences/specs/v1.0/embedding-exchange.md tells implementers to
“sign the envelope; include key_id resolvable via JWKS or equivalent” for audience-embedding
envelopes, and both agent repositories vendor that text. Nothing specifies key material for agent
identity, which is the path discovery runs on. That missing file is the largest single item in
what these protocols don’t standardize.
A registry listing is a catalog entry, not a grant of authority
Both stacks take self-registration: POST /api/me/agents in AdCP, bearer-authenticated, and
POST /api/agents in AAMP, where the JWT’s company domain must match the submitted primary_domain.
What the listing buys you is the difference. An AdCP entry is a catalog record and the publisher’s
file still decides; an AAMP entry is the trust record the table above already priced.
The failure modes are not the same size either. Get an adagents.json wrong and one publisher’s
inventory stops being sellable by one agent until somebody edits one static file, and nobody else
notices, because there is no shared object to corrupt. Two of the six files in production are wrong
in exactly that way today. Break the registry and the blast radius is every participant at once: a
seller agent that cannot reach it has no path to registered, which drops its counterparties back to
public tier and rate card. In AAMP the registry going down is a pricing event, and one bad
verification_status record travels just as far. Price that difference before you decide which layer
your revenue depends on.