Blog · Protocol
An exchange bid-request fixture should fail in CI on the builder, not on a cleaned sample.
An engineer at an SSP or an ad exchange owns the code that builds the bid request. They do not own the publisher page, and they do not own the bidder. The fixture in the pull request is the only request they can make fail before it is offered.
The sample in the repo was edited until it passed
Partner reviews produce a pretty JSON file. Someone moves regs.gdpr out of ext, replaces video.placement with plcmt, deletes the extra site object, and checks that file in as fixtures/bid-request.json. CI validates the file. The builder that runs in production still merges a stored site with a live app, still writes regs.ext.gdpr, and still emits imp.secure as true because the struct tags came from the protobuf schema.
Strict partners no-bid that body and send little back. nbr is the bidder declining to bid. It is not a field-level explanation of the request you sent. A revenue dip weeks later is how the cleaned fixture announces itself. The gate has to run on the bytes the builder function returns, with ids and timestamps frozen in the test, not on a document a solutions engineer repaired by hand.
One happy-path banner is not the suite. Keep a fixture the job must reject: both site and app, a string at, imp.secure as a boolean under Spec JSON, regs.ext.gdpr on a pinned 2.6 snapshot, an empty imp array. A suite that only contains the repaired file will stay green on the bug you already shipped.
Pin the release and the snapshot, and read the exit code
The composite action lives in the rtblint repository. It downloads a release tarball for Linux or macOS, x86_64 or aarch64. A Windows runner is outside that install step. version of auto uses the action ref when you pin a v* tag. A branch ref falls through to latest, so a Monday rule change can fail a Tuesday deploy of a builder you did not edit. Pin the tag. Bump it in a pull request when you want the new findings. cargo install rtblint is the same binary on a laptop.
rtblint validate exits 0 when the payload is valid. Warnings do not fail the process. It exits 1 when there are validation errors. It exits 2 on usage or I/O: a missing file, a bad flag, a snapshot name that does not exist. A red job is a spec error, not a formatting nit. --format json emits stable rule ids, severity, message, and path. The CI guide shows how to allow a known deprecation while still failing structural errors. Do that only for a rule you have a ticket for. An empty allowlist that ignores openrtb.field.moved forever is how source.ext.schain survives a 2.6 migration.
spec-version is the OpenRTB snapshot, passed through as --version. Without it, the CLI uses the latest tracked 2.6 snapshot, which moves when a release lands. Pin 2.6-202505 or whichever snapshot the exchange actually speaks. A field added in 2.6-202211, such as regs.gpp, is not a core field on an older pin. Upgrading the snapshot is a one-line diff, the same way you bump a dependency.
The step that fails the builder output
- uses: aleksUIX/rtblint@v0.13.3
with:
path: fixtures/bid-request.json
spec-version: 2.6-202505
dialect: spec-json
format: json
# same binary, generated from the builder, not from a repaired sample
rtblint validate fixtures/bid-request.json --version 2.6-202505 --dialect spec-jsondialect defaults to Spec JSON, where flag fields are integers. If the exchange speaks protobuf JSON on the wire, set proto-json. "secure": 1 fails that dialect. "secure": true fails Spec JSON. Running the job in the dialect you do not speak produces a green build and a partner that cannot unmarshal the message. The split is the same 28 flags covered in the two JSON encodings, including regs.gdpr, source.fd, and pmp.private_auction.
profile is optional and defaults to the specification only. prebid-server requires each impression to carry imp.ext.prebid.bidder, a legacy bidder object, or a stored request, and it rejects wseat and bseat with openrtb.profile.field_forbidden. google-ab accepts at of 3, fixed price, and requires imp.ext.billing_id. xandr requires ext.appnexus.seller_member_id and, on video, ext.appnexus.context. magnite requires the xAPI identity fields imp.ext.rp.zone_id, ext.rp.site_id on site or app, and publisher.ext.rp.account_id. A spec-clean request can still be refused by the exchange whose profile you forgot to set. A profile-clean request can still be illegal as plain OpenRTB if you only ever run the profile.
Supply-chain checks read a cache you filled, and responses need the request
--resolve --cache checks each SupplyChain hop against sellers.json and the publisher ads.txt or app-ads.txt stored under that directory: sellers/<asi>/sellers.json, ads/<domain>/ads.txt, app-ads/<bundle>/app-ads.txt. Nothing is fetched. A green source.schain object means the object matches the spec. It does not mean the seller id is authorized until the cache is populated and the flag is on. An engineer who treats a spec pass as a sellers.json pass has skipped the check.
Bid responses are a second type. --type response validates the response. --request pairs it with the originating request, including in --batch where one request covers many response lines. A request-only fixture never looks at nurl, burl, or lurl. Notice settlement, the price macro, and loss codes are a different contract, written up in nurl vs burl vs lurl. If the exchange builds those URLs, generate a response fixture from that builder too.
--batch reads one JSON object per line. --summary ranks rule ids across a captured stream. That is how you learn that openrtb.field.moved is the common finding in yesterday traffic, which a single golden file will not show. Keep the stream redacted. The CI job should not download a production log that still has device identifiers into the workspace.
What the red job will not see
CI does not run the publisher page. A Prebid ortb2 merge that only fails for one ad unit will not appear unless that unit output is a fixture. CI does not run the next hop. An exchange that appends a schain node, lowers tmax, or sets source.fd after your builder returns has changed a request your fixture already passed. Capture that outbound body, redact it, and either paste it in the tester or add it as a fixture generated from the same serializer the process uses.
The gRPC sidecar is the same core beside the auction. It does not send payloads off-box. It is not a substitute for the fixture. A sidecar that only logs findings in production is how you discover site plus app after the partner has already stopped bidding. The pull request is the cheaper place.
RTBlint is not affiliated with IAB Tech Lab. Passing the action means the artifact matches the snapshot, dialect, and profile you named. It does not mean the bidder filled, and it does not mean tmax on the wire still matches the fixture after the next hop decreased it.
What an exchange engineer can gate on the next pull request
- Pin
aleksUIX/rtblintat av*tag. Pinspec-versionto the snapshot you speak. - Generate the fixture from the builder. A file repaired during a partner review is a different program.
- Set
dialectto the encoding on the wire. Spec JSON wants integer flags. Protobuf JSON wants bools. - Set
profilewhen the body is Prebid Server, Authorized Buyers, Xandr, or Magnite. Run Spec as well when partners also consume the plain object. - Fail the job on exit 1. Treat warnings as a reading assignment with a rule id, not as a pass you ignore.
- Keep a fixture that must fail: two inventory objects, a boolean secure flag in Spec JSON,
regs.ext.gdpron a 2.6 pin. - Pair a response fixture with
--requestwhen you build notices. Populate a local cache before you treat--resolveas a sellers.json result.