Traced Agentic Ad Lab

Five Gaps in the Agentic Ad Stacks, and the Two That Have Not Moved

AdCP 3.1.13 put a named counting authority, final records, invoice status and a remedy menu on the wire. Its own non-goals page still says none of that exists.

18 min read 10 chapters

Every claim below opens a file at a named commit. The shas are in the citations.

Measurement, invalid traffic, billing settlement, cross-stack identity, dispute resolution. Neither AdCP nor AAMP standardises any of the five, so each of them ends up in a contract instead of a schema, and that is what makes them expensive. A schema is a one-time integration cost. A contract is one you pay again with every counterparty you sign.

That held when AdCP 3.0 shipped. At 3.1.13 it still describes the clauses you write and no longer describes the fields they attach to: a named counting authority, a finalisation deadline and a closed menu of remedies are all on the wire, and the page AdCP tells buyers to plan against says none of it exists. Two gaps have not moved by a single field, cross-stack identity and the agent catalogue every other control hangs off.

The single finding I would carry into a procurement review is smaller than any of the five and sits underneath all of them. AdCP has one human-review rule that a validator enforces rather than a policy document requests, covering fair housing, fair lending, fair employment and pharmaceutical advertising, plus the eu_ai_act_annex_iii policy id, and it fires only for buyers who have opted into plan registration. Nothing in the protocol notices when a counterparty doesn’t.

AdCP, the Ad Context Protocol, is one versioned registry, currently 3.1.13, and it publishes its own non-goals, which more specs should. docs/reference/known-limitations.mdx opens with a line worth stealing: “Knowing what a protocol doesn’t do is part of evaluating it.” The live URL is stamped “AdCP 3.0” as of 16 August 2026, and held against the schemas the same repository ships it produces four disagreements, none in the protocol’s favour. The shipped half is not frozen either: trusted-match and sponsored-intelligence are wholly experimental by x-status, and so are 8 of the 13 schemas under governance.

AAMP, Agentic Advertising Management Protocols, is IAB Tech Lab’s programme of eight independently versioned repositories, whose own files disagree about what the acronym stands for, and there is nothing at the umbrella level to hold anyone to. The nearest artifact is STANDARDS_GAP_REPORT.md inside iab-agentic-primitives, which is at v0.5.0 and unreleased by its own README. It audits that library against 12 external standards, marks 8 unverified, and writes down a rule I have not seen another spec put in writing: “‘We model a field named after a standard’ is never treated as ‘we verified conformance against the published standard’.” All four entries it marks conformant are AAMP’s own artifacts, so by its own count external conformance is zero.

Disclosure, because it colours everything below: drafting those clauses against the schemas is work we sell.

GapAdCP 3.1.13AAMPRisk lands on
Measurement and attributionNo measurement task among 64 operations, over a binding committed_metrics contractNo measurement, report or delivery object in the wire contractBuyer
Invalid traffic and verificationDetection out of scope; ivt a contractible ceiling, post_sivt a billing windowViewability priced and goal-tracked, never attestedBuyer
Billing settlementCounting authority, final records, invoices and remedies on the wire; payment is notNo invoice, usage or payment primitive; makegood is a typed change requestFinance
Cross-stack identityOpaque tokens, no mapping to anything AAMP carriesTwo live identities and one unwired, no mapping between themBuyer
Dispute resolutionData shape shipped, dispute task targeted for 3.2Divergence classified, resolution undefinedBoth
Agent registryWorks; no key transparency until 4.0Trust rooted in a registry with no published spec

The last cell in the registry row is empty because no risk lands there on its own. The registry is the enforcement point for the other five. AAMP trust status caps the access tier, which caps the price a buyer sees; the AdCP catalogue binds a counterparty domain to a verification key. If the registry can be spoofed or mocked, every contractual control resting on it becomes advisory. Six rows, five clauses you write per counterparty, and one call-ordering rule to insist on underneath all of them.

Programmatic never standardised these five either. Invalid traffic sat with MRC accreditation and the vendor layer, settlement sat in the insertion order, and a shortfall was argued out between two account teams. AdCP says so in the flattest sentence on its limitations page: “AdCP is the wire and the contract, not the ledger.” Read that as a boast and it is half right. The wire is real; the contract it means is the deal terms a buy carries, not the counting authority, the verification vendor or the remedy for a shortfall.

What changes is the human. An insertion order gets drafted because a person is about to press buy; an agent presses buy on its own schedule, so the clauses have to exist first.

AdCP has built exactly that. core/insertion-order.json is “a signing wrapper attached to a committed proposal” whose stated design is that “all negotiated terms (performance standards, measurement terms, cancellation policy, pricing) live on the product and package.” Only the signature stays: signing_url means “a human must sign before the buyer agent can proceed with create_media_buy,” which then demands an io_acceptance carrying signatory and signature_id.

Neither corpus names a deployment. The named, dated ones are in the deployments ledger; what the repositories tell you is the state of the parts those deployments are built on.

Where the published statements and the repositories disagree

None of that machinery reaches AdCP’s non-goals page. Five published statements, set against the files shipped at the same commit:

Published statementWhat the repository contains
IAB Tech Lab, 30 July 2026: “Agentic Audiences are now ready for fully programmatic and agentic transactions”Four zero-byte files under specs/ in agentic-audiences: the agent interface schema, both example payloads, the roadmap
known-limitations.mdx: “3.0 rejects authority_level: agent_full at the schema level” on three policy categoriesauthority_level appears in zero files under dist/schemas/3.1.13/, so the rejection survives only in prose
Same file: pharmaceutical advertising “relies on the governance-agent implementation rather than a schema invariant”governance/sync-plans-request.json pins human_review_required to true on four categories including pharmaceutical_advertising, and again on eu_ai_act_annex_iii
Same file: “AdCP does not carry a normative consent tag”trusted-match/identity-match-request.json carries gdpr, tcf_consent (TCF v2.2), gpp and us_privacy, under a MUST NOT for regulated jurisdictions
Same file: “A structured dispute task is a candidate for future work”billing-authority.mdx, same commit: the dispute task is “targeted for AdCP 3.2” with four named states

Four of the five rows are AdCP against itself, and the errors run both ways. Take the authority_level row into a vendor call: the page says AdCP blocks full agent authority on fair-housing campaigns at the schema level, no shipped schema has ever carried the field, and the mechanism that does the work has a different name and a different trigger. The consent row is the mirror image, with its caveat in the same breath: that object lives in one file, in an area where 12 of 12 schemas are experimental.

Documentation lags schemas and announcements lead them, which is true of every standards organisation I have read. The prices differ. A stale non-goals page wastes an afternoon of your implementer’s time; an announcement running a year ahead of an empty schema file can waste a slot on your roadmap. The AAMP row costs a buyer more, and it doesn’t stand alone. What each of those repositories actually holds is the wider version of that one line.

Measurement: an empty task list on a binding reporting contract

measurement is the seventh value of supported_protocols in get-adcp-capabilities-response.json, and none of the 64 operations in manifest.json is a measurement task. The schema is candid: the protocol “is experimental in 3.1 and currently scoped to get_adcp_capabilities catalog discovery,” with tasks landing “in subsequent minors.” A sales agent can advertise a measurement protocol with nothing in it and a buyer’s capability check passes.

Meanwhile the limitations page says AdCP “does not specify an attribution model,” and enums/attribution-methodology.json is a closed four-value vocabulary, deterministic_purchase, probabilistic, panel_based and modeled, which names Media Mix Modeling, Multi-Touch Attribution and incrementality testing inside its modeled value. One of those two files has to give. docs/faq.mdx puts attribution inside AdCP’s advertising scope at line 165, “Sponsored discovery, media buying, creative, brand governance, attribution,” and line 53 of the same file reads “AdCP does not specify attribution or viewability.” Its FAQ claims a scope the same file denies a hundred lines earlier.

The non-goal undersells what AdCP actually carries. What is empty is the task list, not the surface. core/committed-metric.json is “one metric in a package’s binding reporting contract,” snapshotted at create_media_buy and append-only for the life of the buy; core/missing-metric.json is its mirror, a metric “declared but not populated in a delivery report.” That is a declared-against-delivered diff with a name, and docs/measurement/taxonomy.mdx says how to read a silence: absence of committed_metrics means “no audit-grade contract,” not “clean delivery.” enums/available-metric.json carries 36 metrics, roas and incremental_sales_lift among them. So the wire transports attributed numbers and refuses to compute them, and the vocabulary exists to stop anyone summing a panel number with an MMM number, which is the right line to draw.

Which file gives is answered by a third. taxonomy.mdx sorts measurement by rate of change, delivery facts on decade timescales against attribution on every model release, and binding the fast layer into the slow one breaks the schema every cycle. That is a written argument, and it makes this gap a scoping decision rather than an oversight. It is also live work: Yannis Vassiliadis filed adcp#4580 showing that buyers summing daily reach rows double-count, and reach_window now rides on core/delivery-metrics.json. Peter Kras filed adcp#6207 on 4 August 2026, still open.

AAMP has no equivalent to argue about. Nothing in iab-agentic-primitives/spec/jsonschema/ is a measurement, report or delivery object, and its one delivery-reporting path sits in a reference implementation rather than on a wire: a Google Ad Manager adapter donated by Green Mountain Systems, 503-ing unless three environment variables are set. The endpoint that path feeds, /api/v1/deals/{deal_id}/performance on the seller agent, is described in its own OpenAPI document as returning “placeholder/mock stats initially — real ad server integration comes in a future phase.” agentic-audiences lists measurement as an agent_role in embedding_format.schema.json; the file that would carry that agent’s request, agent_interface.schema.json, is zero bytes.

Neither protocol detects invalid traffic

Both standardise the paperwork around detection and leave the detecting to the vendor layer, which AdCP names outright: GIVT/SIVT filtration, viewability and brand-safety verification “execute in the delivery stack or the chosen vendor layer (e.g., DoubleVerify, IAS, HUMAN).”

The asymmetry is in the paperwork. core/performance-standard.json binds a metric to a threshold and a named vendor, requires a standard whenever the metric is viewability because “MRC and GroupM define materially different thresholds,” and adds a rule with teeth: “when specified on a confirmed package, creatives MUST include tracker_script or tracker_pixel assets from this vendor.” ivt is the only metric in it measured as a ceiling, and get_products filters accept required_performance_standards, so the requirement travels with discovery. One more field decides the argument a publisher actually has: billing_measurement.measurement_window takes post_ivt and post_sivt, so AdCP will not measure invalid traffic but will let two parties invoice against the post-SIVT number. Measuring it is somebody else’s job.

AAMP’s wire prices viewability, bills on it and sets goals against it, and cannot say who measured any of it. cpmv sits among ten pricing models, and Proposal.json carries viewable_impressions as a GoalType and viewable_impression as a BillableEvent. That is worse than having only a price, because a goal type invites a commitment nothing in the corpus can adjudicate. ARTF, the agentic-rtb-framework repository and AAMP’s serve-time agent layer, goes further: ADD_METRICS = 7 in its protobuf lets an in-path agent write OpenRTB Metric objects into a bid request. AAMP’s first measurement primitive is a mechanism for injecting unattested viewability numbers into a live auction. State it precisely, because a vendor will correct you: Metric has a vendor slot, the proto declares all three of type, value and vendor optional, and both the metric name and the attesting vendor are exchange-curated open strings.

Settlement reached the wire in 3.1, and the non-goals page has not noticed

docs/media-buy/advanced-topics/billing-authority.mdx documents the flow end to end across three named patterns, seller-attested, buyer-attested 3PAS and vendor-attested, with a worked create_media_buy payload for each. billing_measurement.vendor names the party whose count governs invoicing. finalization_deadline_hours says by when that party MUST publish a final record, and what a miss costs: the counterparty may fall back to its own attestation, and the breach is handled under makegood_policy. report_usage carries the final records, and its own description says “the seller invoices against those final records.” get_account_financials returns an invoices[] array with a five-value status enum, plus payment_status and payment_terms.

core/business-entity.json carries vat_id, tax_id and a write-only bank object with IBAN, BIC and routing number, which is most of what Lukas Ott asked for in adcp#868; enums/adjustment-kind.json took the agency-commission half and added a settlement kind. One rule has not moved: one ISO 4217 currency per buy, so “the buy either uses a matching currency or is rejected.” AdCP’s “Commerce and settlement” section is still three bullets long and still says everything after report_usage happens out-of-band.

A smaller contradiction sits inside the same evidence. get-account-financials-response.json describes itself as guaranteeing “only account, currency, and period” on success; its success branch requires account, currency, period and timezone. AdCP’s prose disagreeing with AdCP’s validator, in one file.

AAMP models money better at the primitive level and not at all at the process level. FD-11, one of the numbered flagged decisions the primitives repository uses to record a binding choice, requires integer micros and rejects float-typed money on the wire, which is a better default than AdCP’s. RateCard is then the only money primitive in the corpus: no Invoice, no Usage, no Payment. FD-11 sits at spec/jsonschema/Package.json and RECONCILIATION.md G7, and its own CHANGELOG.md records the hole: money on the POST /products/avails surface stays float as “a documented FD-11 exception to keep the live wire compatible.” protocol/Avails.json types price as a number, so the better default lapses where a buyer first sees a price.

A publisher reading this should notice which way the absence cuts. AAMP makes the seller’s count authoritative when the two sides diverge, and AdCP has the seller proposing the remedy from an agreed menu, so an unspecified settlement path leaves the party that already holds the numbers holding them. Neither body wants to be in payments and neither should be, but the cost of that decision lands on a finance team rather than on a standards body. The wire trace shows where the automation stops and accounts payable picks it up.

No identity crosses between the stacks, or inside AAMP

AAMP carries an audience identity, {namespace, value_hash, confidence} in the agentic-audiences embedding envelope, and an account identity, BuyerIdentity, eleven optional fields from seat_id to campaign_name whose docstring says what AAMP thinks identity is for: “buyer identity revealed progressively to unlock better pricing.” An organisation is being described there, not a person. A third was meant to sit in the bidstream, ADD_CIDS = 8 in ARTF, and it is an unwired enum: the handler says the intent “is defined in proto but not yet in generated Go code,” and the mapping for it is commented out. No file maps any of them onto any other.

AdCP keeps identity opaque and separates it structurally rather than by policy. identity-match-response.json returns eligible_package_ids and a serve_window_sec and carries no page context, while context_match carries page context and no identity. sync_audiences takes hashed_email and hashed_phone, SHA-256 only. Then known-limitations.mdx narrows its own claim twice, in a lawyer’s language rather than a spec author’s: “AdCP authenticates agents, not the humans they act for,” and “SHA-256 hashes of email and phone remain pseudonymous identifiers under GDPR and CPRA.”

Across the two stacks there is no mapping at all, which follows from the finding underneath the whole protocol comparison. Hold one audience definition for both sides and you translate it by hand, every time, with nothing published to check it against.

The dispute flow one stack has dated and the other has not

You pick up the phone. That is the answer on both sides; only one of them has said when it stops being the answer.

core/measurement-terms.json hands you the vocabulary. max_variance_percent is the divergence between the two parties’ counts “before resolution is triggered,” and makegood_policy reads: “when a breach occurs, the seller proposes a remedy from this menu; the buyer accepts or disputes.” The menu is closed and three values long in enums/makegood-remedy.json: additional delivery, credit, invoice adjustment. What is missing is the message. known-limitations.mdx calls a structured dispute task “a candidate for future work”; billing-authority.mdx at the same commit names its four states, under_review, seller_proposed_adjustment, buyer_accepted, unresolved_arbitration, and targets it at AdCP 3.2. A named minor with named states is something a procurement lead can plan against, and the non-goals page reports the weaker of the two.

AAMP has the state machine AdCP lacks and stops one step earlier. state/Reconciliation.json encodes FD-8 as a state-pair table across change requests, deals and orders, classifying every pair as consistent, buyer_behind, seller_behind or divergent, with the seller authoritative and the policy stated outright: “divergence is reported, not silently resolved.” It carries the remedy too: ChangeRequest.json implements FD-6 with makegood as a typed change type, a payload of shortfall_grps, owed_impressions and a replacement flight, and a makegood_pending deal state. Neither corpus has the escalation. What follows divergent is undefined, and the word “dispute” appears nowhere in iab-agentic-primitives. Whatever either body means by autonomous buying, it stops at the first disagreement.

Can either registry be trusted with autonomous spend?

Agent.json is emphatic about trust. It defines TrustStatus, unknown, registered, approved, preferred and blocked, as “verified against the AAMP registry… never self-asserted.” STANDARDS_GAP_REPORT.md in the same repository files that registry UNVERIFIED because its authors could not obtain the specification: “trust_status semantics are asserted by our own spec, not against AAMP publications.” The canonical wire contract cannot verify the registry its own trust model is rooted in.

Downstream, the reference implementation is blunter than its documentation: the reference seller agent stubs AAMPRegistryClient “pending the public AAMP API specification,” returning mock data unless AAMP_REGISTRY_URL is set. The mock is the default, so five graded trust levels collapse into whatever list the operator keeps locally. The discovery guide has the hostnames and what each answers.

AdCP’s registry works, and its own limitations page tells you how to defeat it. There is no enrollment ceremony binding a domain to a root verification key and no append-only rotation record, so “an attacker who controls a counterparty’s CDN, DNS, or /.well-known path can serve attacker-controlled keys.” Trust-on-first-use with continuity is the mitigation, “detectably raising the bar, but not cryptographically closing the gap,” and the full fix is a 4.0 deliverable.

A nearer problem sits below it, filed by Lukas Ott as adcp-client#2363 and open since 16 July 2026: the SDK’s auto-wired verifier “is enforcement-only: it accepts or 401s, and the verification result never reaches anything downstream,” with verifiedSigner “written in exactly two places… and read nowhere.” The signature passed, the buy went through, and the log cannot say who signed it. So the answer is no, in two different ways: underwrite autonomous spend against either registry and your control is a domain takeover away in one case and an operator’s local list in the other.

AdCP’s strongest control is the one you have to volunteer for

Governance is AdCP’s largest area, 22 of the 64 operations, and it holds the protocol’s only human-review rule a validator enforces rather than a policy document requests. Two if/then blocks in governance/sync-plans-request.json do it. One fires when a plan’s policy_categories contains fair_housing, fair_lending, fair_employment or pharmaceutical_advertising; the other when its policy_ids contains eu_ai_act_annex_iii. Both pin human_review_required to a const true and add it to required, so a plan declaring the category and omitting the flag does not validate. Four validator-enforced categories plus one policy id, rejected by any conformant implementation without being asked.

The defect is one line in a different file. governance/check-governance-request.json has required: ["plan_id", "caller"]. No call shape reaches a governance agent without a registered plan, so an implementer who never calls sync_plans has nothing to validate against, the invariants never fire, and no operation anywhere observes the omission. The rule that could stop an agent buying fair-housing inventory with no human in the loop only fires if you opt in, and anything you opt into is a feature.

Two things sharpen that. All three files carry x-status: experimental, so the strongest control in either stack is also the least frozen. And enforcement sits wholly on the buyer’s side: a seller receiving a create_media_buy cannot tell whether a plan was ever registered behind it.

The five clauses you write yourself

One per gap, and nobody writes them for you. Two of them are shorter than they were a release ago, because AdCP now ships the fields and the argument has moved to who populates them. Sign on AAMP and the same five run longer, not shorter: the objects the first three clauses point at have no AAMP counterpart, so each clause has to define the term before it can set a value.

  1. Name the counting authority and the tolerance. Five fields carry it: billing_measurement.vendor, max_variance_percent, measurement_window, finalization_deadline_hours and the makegood_policy menu. Your clause sets the values; your diligence question is whether the counterparty sends the object at all at create_media_buy.
  2. Name your verification vendor and its thresholds. core/performance-standard.json carries the metric, threshold, standard and vendor once you have agreed them, and agreeing them is contract work.
  3. State the settlement mechanism and the currency. One ISO 4217 currency per buy or the buy is rejected. Net terms, prepay, escrow or a standing credit line: get_account_financials carries payment_terms, payment_status, an invoices[] status enum and a currency required on every success branch, so the record of a payment exists and the mechanism that clears it is yours to name.
  4. Put a name on the audience crosswalk. One person owns the mapping between an AAMP {namespace, value_hash, confidence} identity and the SHA-256 hashes sync_audiences accepts, in writing, because nobody else publishes it.
  5. Attach a phone number to divergent. Reconciliation.json will tell you the two agents disagree and will not tell you who to call.

All five are per-counterparty. Ten partners is ten sets of clauses, and no schema release reduces that number.

Each gap closes as a file changing, not as an announcement

GapWhat has to changeWhere you would see it, and what is dated
MeasurementA task under the measurement protocol AdCP has already declaredmanifest.json grows past 64 tools. adcp#6207, the measurement-capability RFC, open since 4 August 2026
VerificationA wire attestation replacing an agreed vendor, or an AAMP primitive naming a measurercore/performance-standard.json; iab-agentic-primitives/spec/jsonschema/
SettlementMostly done. What is left is payment, and a non-goals page that admits the restAdCP’s “Commerce and settlement” bullets, unchanged at 3.1.13; iab-agentic-primitives/spec/jsonschema/
Cross-stack identityEither corpus admits the other existsThe first mention of AdCP anywhere in the eight IAB repositories
DisputesThe 3.2 dispute task shipping with its four states, or an AAMP policy after divergentbilling-authority.mdx names the target; state/Reconciliation.json is where AAMP’s would land
RegistryKey transparency, a 4.0 deliverable; a published AAMP registry API specificationAdCP release notes; adcp-client#2363 closing; AAMPRegistryClient dropping its mock

One of those you can delegate today. Ask any vendor selling you AAMP measurement to show you a non-empty agent_interface.schema.json on main in agentic-audiences. Until they can, AAMP has no measurement agent that talks to the rest of the corpus, whatever the press release says.

And whichever stack you sign for, require plan registration: sync_plans before check_governance, or the human-review invariants are decoration your counterparty can skip.