An AAMP buyer agent finishes a programmatic video buy. The AdCP manifest lists duration_ms: 30000. The creative agent's format capability promised at most sixty seconds. The task status is success. The deal is in the library.
The player loads the tag and reads <Duration>00:01:05</Duration>. The slot was sold as thirty seconds. Quartile beacons fire on the wrong clock. Some CTV stacks hard-reject the break. Nothing in the agent transcript failed, because nothing in the agent layer opened the XML.
IAB Tech Lab's Agentic Advertising Management Protocols (AAMP) keeps VAST as the video envelope. Ad Context Protocol (AdCP) negotiates the creative contract above it. AAMP 2.3 added diligence gates on price-moving paths. AdCP 3.1 added canonical format capabilities with stable capability_id values and explicit duration bounds. Those are real improvements on what two agents believe they agreed to build.
They are still three assertions at three altitudes. Only one of them is the instruction the player executes.
What the format layer actually declares
AdCP 3.1 replaced loose format string matching with a capability catalog. A creative agent publishes entries like video_hosted_16x9 with a format_kind and a duration model:
{
"capability_id": "video_hosted_16x9",
"format_kind": "hosted_video",
"duration_ms_range": [null, 60000]
}
A fixed slot uses duration_ms_exact instead of a range. Open-ended bounds are first-class: "up to sixty seconds" does not force you to invent a lower bound nobody meant. Under-specified selectors now fail closed rather than matching by loose overlap, which stops a buyer from accidentally getting a format nobody asked for.
When the buy closes, the manifest carries a VAST asset:
{
"asset_type": "vast",
"delivery_type": "url",
"url": "https://adserver.example/vast?pl=12345",
"vast_version": "4.2",
"duration_ms": 30000
}
Note the qualifier on duration_ms in the schema: if known. The field is optional. The URL delivery type means the bytes have not been fetched yet.
The player never reads any of this JSON. It reads VAST:
<Linear>
<Duration>00:01:05</Duration>
<MediaFiles>...</MediaFiles>
</Linear>
Thirty thousand milliseconds satisfies the sixty second capability bound. Thirty thousand in the manifest validates against the JSON Schema. Sixty-five seconds in <Duration> is what the SDK schedules against. All three can pass their respective checks in one flight.
Or the inline can omit <Duration> entirely. VAST 2.0 through 4.x require it inside every <Linear>. A trafficking export that lists <MediaFiles> and tracking URLs but skips duration validates in the manifest layer and breaks on strict players. That failure mode is not hypothetical; it is what rule VAST-2.0-linear-duration catches.
The AdCP 3.1 format capabilities write-up walks the three-number problem in full. The asset-level version of the same argument is that every field on a VAST asset is a claim about a document nobody parsed.
Why AAMP makes the gap visible
AAMP buyer agents transact over MCP. AdCP-style creative negotiation produces manifests, capability targets, and delivery URLs. The agent loop has a natural terminal state: manifest accepted, tag URI stored, task marked complete.
That terminal state is negotiation proof, not playback proof. It is the same class of blind spot as the wrapper problem: the agent holds a URL, not the creative. Here the URL may resolve to an inline with the wrong length, or no length at all, while the JSON layer says thirty seconds.
AdCP conformance storyboards prove the protocol handshake. They assert response schemas on tool calls. They do not fetch the tag behind delivery_type: url and compare <Duration> to duration_ms. The AdCP conformance guide grades whether your agent speaks AdCP correctly. Passing it says nothing about whether the VAST document honors the duration the manifest claimed.
Generative creative agents make this worse, not better. They produce XML shaped like training data. A plausible <Duration>, a plausible tracking block, and a MediaFile URL that 404s can all coexist in one document. The manifest references the tag. The format capability bounds the manifest. Nobody closed the loop on the wire format.
How I catch it before the flight
Split the job the way players split it.
Step one: the live tag URL in the VAST tag tester. Paste the URI from the manifest, not a snippet from an earlier debug session. The tester fetches what the endpoint serves now, previews the creative when it can, and shows the resolved XML at the bottom. Compare <Duration> to the duration_ms you think you bought. If hop one is a wrapper, you still see wrapper structure immediately; duration lives on the inline.
Step two: the same URL in the VAST inspector. The inspector resolves hop by hop. Duration is never on the wrapper document. If your QA stops at hop one, you approved a redirect graph without reading the terminal <Linear>.
Step three: wire a deterministic gate into the agent loop. The agentic integration docs describe where to call validation at build_creative, preview_creative, and delivery checks. MCP or gRPC after the tester and inspector pass. Compare declared duration_ms against parsed <Duration> in milliseconds. Flag missing <Duration> before the buy writes to your system of record.
Treat "manifest accepted" as an intermediate state, not COMPLETE. The AdCP and VAST handoff guide is the reference for where the protocol stops and the creative envelope begins.
Where the check belongs
If you host an AAMP buyer agent, add a delivery gate that opens every VAST URL before the deal closes. Capability bounds and manifest metadata are the contract. <Duration> is the delivery receipt.
If you operate a creative agent, return duration_ms only after you parsed the tag you built, or omit it and force the buyer to verify. Declaring thirty thousand milliseconds because the brief said thirty seconds is how three numbers diverge.
If you are comparing stacks, AAMP and AdCP disagree about where agents sit in the stack. They agree that VAST stays the envelope. Format capabilities fixed the vocabulary for what you negotiate. They did not give the player a JSON file to render.
Top comments (0)