The IAB Tech Lab Agentic Real Time Framework puts an agent beside an SSP, a DSP, or an exchange, as a container in the host's own infrastructure. The host sends an OpenRTB payload in an RTBRequest envelope. The agent returns mutations. The orchestrator decides, change by change, what to apply. Buyers then bid on whatever the host actually forwarded.
Nothing in v1.0 checks that the result is still valid OpenRTB. The RPC can succeed, the auction can be nonsense, and the spec is fine with both of those being true at once.
I implemented the other side of that: three validation passes in RTBlint, against the document and against the .proto in the same repository. They do not say the same thing.
Two vocabularies, one mutation
The v1.0 document's worked examples write this:
{
"intent": "adjustDeals",
"op": "replace",
"path": "/imp/imp-1/deals/deal-standard",
"value": { "AdjustDealPayload": { "bidfloor": 7.25 } }
}
The .proto defines ACTIVATE_DEALS / ADJUST_DEAL_FLOOR, OPERATION_REPLACE, and top-level oneof members (ids, adjust_deal, adjust_bid, metrics, content_data). No value wrapper. No camelCase intents. The deal path in the document sometimes omits pmp; the examples in the repo put it back.
Anyone generating code from the document produces something the reference server cannot read. Anyone generating from the proto produces something that does not look like the spec they were handed.
RTBlint maps the document encoding rather than discarding it, and reports artf.mutation.legacy_spec_encoding. The floor still lands, if the path resolves. The warning is the point: you are speaking the PDF, not the wire.
The same split shows up in field numbers. The document's illustrative message block numbers tmax as field 6 and ext as field 3. The proto uses 3 for tmax and 6 for originator. That one you will not see in JSON. You will see it the first time someone codes against the table in the PDF.
The id that is not the bid request id
An RTBResponse has an id. So does the OpenRTB BidRequest inside the envelope. They are not the same field.
The envelope id is the extension-point request id: how the orchestrator ties mutations back to the call it issued. The bid request id is the auction. The reference implementation mixed these. A host that keys on the envelope id then cannot attach the mutations to the call.
$ rtblint validate --type artf-response \
--request rtb-request.json rtb-response.json
FAILED (OpenRTB 2.6-202606 ARTF mutation set): 8 error(s), 4 warning(s).
- [error] id: RTBResponse.id "extension-point-999" does not echo the
RTBRequest id "extension-point-001". The envelope id is the
extension point request id, not the bid request id.
(artf.response.id_mismatch)
If you take one thing from this article, take that. Fluent gRPC, wrong id, auction never asked.
Paths are semantic, and they have to exist
ARTF paths are not JSON pointers. They name business entities by id:
/imp/imp-1
/imp/imp-1/pmp/deals/deal-premium
/user/data/segment
/seatbid/dsp-1/bid/bid-abc
An agent that writes /imp/imp-404/pmp/deals/deal-premium is not "close." That impression is not in this auction. A host that forwards the patch writes into nothing, or into the next object its JSON library happens to create.
- [error] mutations[1].path: path "/imp/imp-404/pmp/deals/deal-premium"
names impression "imp-404", which is not among the impressions the
bid request carries ("imp-1"). (artf.mutation.imp_unknown)
- [error] mutations[2].path: path "/imp/imp-1/pmp/deals/deal-unlisted"
names deal "deal-unlisted", which impression "imp-1" does not offer
("deal-premium", "deal-standard"). (artf.mutation.deal_unknown)
The document and the example docs also disagree about whether a deal sits at /imp/{id}/deals/{id} or /imp/{id}/pmp/deals/{id}. Both resolve in RTBlint, because punishing one reading of a published ambiguity is worse than accepting both.
BID_SHADE is a bid-response intent. Send it at LIFECYCLE_PUBLISHER_BID_REQUEST and there is no bid_response to mutate. That is artf.mutation.intent_not_applicable plus artf.mutation.target_payload_missing. Shading a bid that has not been made yet is not a floor adjustment.
One intent has nowhere to write
ADJUST_DEAL_MARGIN is a real ARTF intent. Margin is not a field on the OpenRTB Deal object. The orchestrator has to apply it out of band. No validator can check the result, because the result is not in the payload.
That is artf.mutation.no_openrtb_target, a warning. Treating it as an error would be lying: the intent is legal. Pretending the Deal grew a margin field would be lying the other way.
ADD_CIDS is the mirror image on the version axis. The intent is legal ARTF. cids arrived in OpenRTB 2.6-202505. Adding it to a request pinned to an earlier snapshot produces a field that does not exist in that version. The agent does not necessarily know which snapshot the host is running. The envelope does not carry one.
Three passes, because one is not the question
A mutation that looks right can produce a document that is not. A document that was already broken should not be blamed on the agent. So:
-
Envelope. Required members,
lifecycleagainst the payloads actually carried,tmaxthat could plausibly fit inside an auction (the samples use 150 ms; a 30-secondtmaxis not an in-auction call),originatorandapplicable_intentsenums, and the carried bid request / bid response validated as protobuf JSON. - Mutations. Response id, declared intent, operation and payload oneof matching the intent, every semantic path resolving to something the auction carries.
- Applied. Write the patches in, revalidate, report only the OpenRTB findings the mutations introduced. What the agent broke, not what arrived broken.
Pass 3 is the one that surprises people. This mutation is structurally fine: ADD_METRICS on /imp/imp-1, payload present, path exists. The value is the string "high" instead of a number.
$ rtblint validate --type artf-response --apply \
--request rtb-request.json rtb-response.json
FAILED (OpenRTB 2.6-202606 ARTF mutation set): 1 error(s), 0 warning(s).
- [error] bid_request.imp[0].metric[0].value: After applying the
mutations: imp[0].metric[0].value expects number but received
string. (openrtb.type.mismatch) ยท spec 3.2.5
Applied 1 of 1 mutation(s).
The framework accepted the RPC. The metric object is no longer valid OpenRTB. Independent acceptability in ARTF means the orchestrator may drop the whole set; it does not mean "forward the ones that parsed."
Where to put the check
If you host agents, validate after every stage that can rewrite the document, and keep the pre-mutation payload long enough to answer "why did this request claim that segment." If you build agents, validate what your mutation produces against the snapshot the host actually runs, not against the newest spec you read.
$ cargo install rtblint
$ rtblint validate --type artf-request rtb-request.json
$ rtblint validate --type artf-response --request rtb-request.json rtb-response.json
$ rtblint validate --type artf-response --apply \
--request rtb-request.json rtb-response.json
The same three passes are MCP tools (validate_artf_request, validate_artf_response with apply), a browser simulator, and gRPC ValidateArtfEnvelope / ValidateArtfMutations on openadtech.rtblint.v1. ARTF already mandates gRPC for the extension point, so the guardrail belongs on that channel rather than in a shell-out.
The longer primer is ARTF explained. The spec and proto are in IABTechLab/agentic-real-time-framework. RTBlint is independent of IAB Tech Lab. The document-versus-proto findings exist because both files are public and they do not match.
Top comments (0)