Guides
From an AdCP media buy to an OpenRTB bid request: the mapping
Every agentic buying stack ends at the same place. An agent discovers inventory, negotiates terms, and creates a media buy, and then something has to serve an impression, which means something has to emit a bid request. AdCP is explicit about this: several of its own fields are defined by reference to OpenRTB 2.6, and its real-time execution layer speaks in OpenRTB-shaped requests. This guide is the handoff, field by field, and the places where a clean negotiation still produces a payload a bidder will drop. For the standards landscape, see the AdCP explainer and IAB Tech Lab's agentic stack.
The layer split, stated precisely
AdCP is a task protocol between agents, organised into protocol families: media-buy, creative, signals, governance, brand, sponsored-intelligence, and an experimental measurement family in 3.1. A buyer agent calls tasks such as get_products, create_media_buy, and sync_creatives on a seller agent, over MCP or A2A. None of those messages are a bid request, and none of them clear an auction.
OpenRTB is the wire protocol for the auction itself. When an AdCP media buy transacts against programmatic inventory, the agreement it produced has to be expressed as something an exchange and a bidder already understand: a deal id in imp.pmp.deals, a placement signalled in imp.video or imp.audio, a supply path in source.schain. The agents agree on terms. The bid stream carries them.
That split is why validation moves rather than disappears. Nothing in an AdCP task response tells you whether the bid request that eventually results is well formed against the OpenRTB version the exchange is running.
Placement types: AdCP names, OpenRTB integers
The cleanest example of the handoff is video placement. AdCP's video_placement_types, used both as a get_products filter and as a declaration on a placement, is defined in the specification as using OpenRTB 2.6 video.plcmt semantics with AdCP-native names. The mapping is one to one:
| AdCP name | OpenRTB video.plcmt | Meaning |
|---|---|---|
instream | 1 | Instream: sound on by default, video is the primary content |
accompanying_content | 2 | Player beside editorial content, plays on entering the viewport |
interstitial | 3 | No video content; takes over the viewport |
standalone | 4 | No content or standalone: feeds, slideshows, sticky and floating players |
Two failure modes follow directly. First, a product negotiated as instream that executes as a request carrying only the deprecated video.placement has lost the signal the buyer selected on, because placement and plcmt do not share an enum. Second, an aggregate placement may legitimately declare several types, but the bid request describes one impression, so exactly one plcmt value has to be chosen at emit time. The enum tables and the migration are in placement versus plcmt.
Audio: the same pattern, a longer list
AdCP's audio_distribution_types is defined against OpenRTB 2.6 audio.feed, the AdCOM Feed Types list, again with readable names: music_streaming_service, fm_am_broadcast, podcast, catch_up_radio, web_radio, video_game, and text_to_speech. A buyer that filtered products to podcast is entitled to a request whose imp.audio.feed says podcast. If the emitted request omits feed entirely, which is common in audio integrations built before 2.6, the negotiated constraint exists only in the paperwork. See how audio ads work in OpenRTB.
The media buy becomes a deal
AdCP models inventory as products, packages, and placements, with per-placement format support and pricing. Programmatic execution of that agreement lands in the fields OpenRTB already has for private supply:
| What was agreed | Where it executes | What can go wrong |
|---|---|---|
| The agreement itself | imp.pmp.deals[].id | A bid naming a deal that was never offered in the request is dropped (loss reason 4); RTBlint reports openrtb.bid.dealid_unknown on the pair |
| Negotiated price | deals[].bidfloor, bidfloorcur, at | Deal-backed bids clear the deal floor, not the impression floor; a mismatch produces loss reason 101 |
| Guaranteed versus auction | deals[].guar, pmp.private_auction, at 3 | A guaranteed buy executed as an open-auction impression is a different commercial product than the one that was agreed |
| Who may buy it | deals[].wseat, request wseat / bseat | Seat restrictions that do not match the buyer agent's seat filter the bid (loss reason 104); openrtb.seatbid.seat_not_allowed |
| Creative constraints | imp.video / imp.audio / imp.banner duration, mimes, protocols, sizes | A format accepted at sync_creatives time that violates the emitted impression's constraints is filtered at auction (loss reasons 203, 204) |
| Duration-priced video and audio | durfloors, mincpmpersec | Pod fields declared without pod context are a builder bug RTBlint flags directly |
Deal semantics field by field are in OpenRTB deals and PMP. The sell-side packaging question that sits next to this one is curation: a curated deal and an agent-negotiated deal are structurally identical in the payload, which is exactly why the payload has to be checked rather than trusted.
Trust: adagents.json meets the supply chain
AdCP's authorization primitive is adagents.json, published by a publisher to declare which sales agents may sell which properties, with attested signing keys per agent, and paired on the brand side with brand.json. It answers a question ads.txt never did: is this agent allowed to represent this inventory?
It does not answer the question the bid stream asks, which is whether the chain of intermediaries that actually handled this impression is complete and authorised. That is still source.schain reconciled against ads.txt and sellers.json. The two layers are complements: agent authorization at negotiation time, seller authorization at execution time. A stack that verifies the first and skips the second has an auditable agreement attached to an unauditable impression. See the supply chain trust stack and supply path optimization.
AXE: where AdCP touches an actual request
AdCP's Agentic eXecution Engine is the part that runs at impression time rather than planning time. The documented flow has the ad server or a Prebid real-time data module send an OpenRTB-style request to an AXE endpoint, which returns segment decisions the ad server turns into targeting: axei to include a segment, axex to exclude for suitability, and axem to carry creative macro data.
Two practical notes for anyone wiring this. The request going to AXE is OpenRTB-shaped, so it is validatable with the same tooling as the auction request, and it is worth validating, because a malformed context object degrades segment lookup silently rather than loudly. And axem data ends up substituted into creative URLs, which puts it in the same class of problem as OpenRTB substitution macros: AdCP's own macro specification carries a normative percent-encoding contract and published test vectors precisely because unencoded values break URLs and open injection paths.
Where the mapping silently breaks
- Version drift. An agent stack built against 2.6 language emitting into an exchange pinned to an older snapshot produces fields that do not exist yet in that version. RTBlint reports
openrtb.field.not_yet_availablefor exactly this, and it is invisible in any AdCP-level test. - Names that look mapped but are not.
video_placement_typesmaps toplcmt, not to the deprecatedplacement. Sending the old field with the new intent loses the constraint. - Constraint loss. Everything narrowed during negotiation (format, duration, seat, geo, suitability) has to be re-expressed in the impression. Anything not expressed is not enforced, no matter how precisely it was agreed.
- Privacy signals dropped at the handoff. A media buy carries no consent string.
regs.gdpr,user.consent,regs.gppandgpp_sidare emitted by the seller's ad stack, and a half-populated pair is treated as unusable by buyers. See privacy signals. - Type drift from generated code. Agent stacks assemble JSON programmatically, and the classic failures are a string where the spec says integer, a single object where an array is required, and a boolean coerced from a flag.
Why a generated payload needs a verifier, not a reviewer
One more reason the emitted request deserves a hard check: increasingly it is not written by a template, it is written by a model. A system assembling a bid request by pattern completion is very good at shape and indifferent to constraint, so what comes out looks right the way a forged signature looks right. The recognisable defects follow from that: "at": "1" as a string because most values in most JSON are strings, a field placed under ext because that is where it lived in an older integration, a 2.6 field inside a request an exchange will read as 2.5, an enum value that does not exist but sounds like it should.
Asking a second model to review the first does not resolve it. Both have the same relationship to the specification, which is that they have read a lot of payloads and hold no ground truth. Two plausibility engines agreeing is not verification.
A validator is different in kind, because the questions it answers are decidable. The JSON parses or it does not. The field exists in this version or it does not. The value is in the AdCOM list or it is not. That makes the output usable as a feedback signal rather than an opinion: an agent emits a request, calls the validator, gets back rule ids with JSON paths, repairs those exact defects, and calls again. Because the errors are structural, the loop converges quickly. It is the same reason coding agents run tests instead of asking a model whether the code compiles, and it is why RTBlint ships as an MCP server: the cheap correct check belongs in front of the expensive confident generator.
What to check, and where
The useful discipline is to treat the emitted bid request as the contract's only observable output, and to check it at three points:
- In CI, against fixtures. One fixture per product type your agent can transact, validated against the exact OpenRTB snapshot each exchange runs. The pattern is in validation in CI.
- At the emit boundary. Validate the bytes you send, after serialization, not the object you think you built.
- On the pair. Cross-validate the bid response against the request that produced it, so deal id, seat, currency, and media type are checked against what was actually offered rather than what was negotiated.
RTBlint does all three from one core: in the browser in the tester, in CI from the CLI, in Node through rtblint-core, and as an MCP server so a buying or selling agent can validate its own output as a tool call rather than waiting for a partner to complain.
Sources
- Ad Context Protocol specification (media-buy and creative task references, agentic execution engine, universal macros)
- adcontextprotocol/adcp: schemas, including the OpenRTB-referenced placement and audio distribution enums
- OpenRTB 2.6: Imp (3.2.4), Pmp (3.2.11), Deal (3.2.12), Video (3.2.7), Audio (3.2.8), Source (3.2.2)
- IAB Tech Lab: AAMP, for the parallel stack and its OpenRTB-extension strategy