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.

FileByteslast_updatedAuthorized agentssigning_keys
cafemedia.com3,353,1402026-08-051, interchange.io, publisher_properties over 6,800 publisher domains; 6,804 properties[] entries under 1 tagnone
www.mediavine.com3,272,0112026-07-062, both interchange.io, property_tags; 8,865 properties[] entries under 24 tagsnone
kijk.nl and 7 more Talpa Network domains8,9492026-06-193: swivel.ai, Triton Digital, interchange.io, all directnone
equalize.cloud2,051absent2, both self-hosted MCP endpointsnone
www.vox.com1,963absent1, salesagent.voxmedia.comnone
www.straitstimes.com2102026-07-31pointer to sales-agent.adzymic.ainone

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.json entry for an agent contains signing_keys, the verifier MUST reject any signature whose keyid is not in that pinned set, regardless of jwks_uri contents. 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.

LayerAdCPAAMP
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 topagenticadvertising.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 statusHow it is reachedMaximum access tierPricing visibility
unknowndefault on first contactpublicpublished rate card only
registeredfound in the AAMP agent registry, verified automaticallyseatseat-level pricing
approvedseller operator actionadvertiserfull advertiser pricing
preferredseller operator actionadvertiserfull pricing plus priority access
blockedseller operator actionnoneno 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 inBase URLResolved 16 August 2026
buyer-agent/docs/api/seller-discovery.mdhttps://registry.aamp.iab.com/agent-registryno DNS record; iab.com resolves
seller-agent/src/ad_seller/config/settings.pyhttps://tools.iabtechlab.com/agent-registry200
iab-agentic-primitives/SANDBOX_REGISTRY.mdhttps://registry.iabtechlab.com302 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.