Most guides to setting up an RTB exchange are really guides to writing an auction. Parse the bid request, fan out to your demand partners, rank what comes back, return the winner. That work is real, but it is bounded: a competent team finishes it, and once it is finished it mostly stays finished.
Then the exchange goes live and the actual job starts.
An RTB exchange runs on two clocks, and only one of them ever stops:
- The auction clock is synchronous and short. It opens when a bid request arrives and closes when you return a response, a few hundred milliseconds later. Every decision inside it is made with the data in front of you. You can specify this clock completely, test it exhaustively, and declare it done.
- The settlement clock is asynchronous and open-ended. Win notices arrive after the auction that produced them has been forgotten. Discrepancies against a buyer’s numbers surface a day later. A floor that was right in March is quietly costing you money in June. This clock has no terminal state.
Teams budget for the first clock and get ambushed by the second. The exchanges we see stall after launch stall on the settlement side: wins that cannot be matched to auctions, counts that do not agree with the buyer’s, floors nobody has revisited since integration week.
This guide covers both, in that order.
Table of Contents
- What you are actually building
- Which OpenRTB version should you target?
- The fields to validate before anything else runs
- Should you use JSON or Protocol Buffers?
- Where the auction clock actually goes
- Win notices, and why they break first
- What to measure, and what to alert on
- Hardening the endpoint against malformed and hostile input
- The creative types you have to support
- Testing before you connect live demand
- A rollout you can reverse
- How long does setting up an RTB exchange take?
- Key takeaways
- Running the exchange on Floxis instead
- Sources and standards worth bookmarking
What you are actually building
An exchange is not one service. It is a chain of components with a defined hand-off between each pair, and integrations break at the hand-offs far more often than inside the components.
| Component | Responsibility | The guarantee it owes the next stage |
|---|---|---|
| Endpoint | Accept the POST, deserialize, enforce the schema | Nothing malformed reaches the router |
| Eligibility | Seller authorization, geo, format, floor lookup | Every request that proceeds is one you are willing to sell |
| Router | Fan out to seats in parallel, enforce the deadline | No response is accepted after the auction closed |
| Seat adapters | Translate to and from each partner’s dialect | Price, currency and creative fields arrive normalized |
| Auction | Filter below floor, rank, apply margin, pick a winner | Deterministic, reproducible winner selection |
| Win pipeline | Credit inbound notices to the auction; substitute macros and fire the demand side’s | Every win maps back to a recorded auction id |
| Telemetry | Latency percentiles, bid rate, win rate, drop reasons | You find out from a metric, not from a partner |
Two of those deserve more attention than they usually get. Eligibility is where authorized-seller and supply-chain checks belong, because a request refused before the auction opens costs you microseconds and nothing else; we wrote about the economics of that placement in the guide to ad fraud prevention for exchange operators. Telemetry is not an operational nicety — it is the only reason you will ever discover that the settlement clock has stopped turning.
The state that ties it together is a per-auction record keyed on the bid request id, holding which seats were called, which responded, and what cleared. It must outlive the response, because the win notice that references it has not arrived yet. Get its lifetime wrong and every downstream number is wrong with it.
The router’s “fan out to every seat” also has a shelf life. Every demand partner will hand you a QPS cap, and exceeding it gets you throttled at their edge, silently. Per-seat caps — and eventually shaping, sending each seat only the traffic it has shown it will bid on — belong in the router from the start.
Which OpenRTB version should you target?
OpenRTB is the IAB Tech Lab standard that defines the bid request and bid response envelope every programmatic partner speaks. For a new exchange, support 2.5 and 2.6 together and let the partner pick. 2.5 remains the version with the broadest installed base among demand partners; 2.6 adds the structured video and CTV fields that anyone selling those formats will eventually need, and adopting it later means a second integration round with every seat.
The practical rule is that version negotiation should be a per-seat configuration value, not a global build-time choice. Partners upgrade on their own schedules, and an exchange that can only speak one dialect ends up maintaining a fork of its own adapter layer.
Unknown extension objects are the other half of interoperability. Partners ship proprietary fields inside ext, and an exchange that rejects a request because it did not recognize one is an exchange that breaks every time a buyer ships a feature. Pass unknown extensions through untouched, log that you saw them, and never fail validation on them.
The fields to validate before anything else runs
Validation is cheap at the endpoint and expensive everywhere else. These are the checks worth making before a request is allowed to cost you anything:
id— the auction identifier. Absent means you cannot match a win notice later. Reject.imp— at least one impression object. The spec requires it and reference implementations reject its absence outright; an empty array is a malformed request, not a polite no-bid.imp[].id— identifies which impression within the request a bid refers to. Required on every impression, not just multi-impression requests: the spec mandates it, and a bid’simpidhas nothing to match against without it.siteorapp— or, in 2.6,dooh— the context object. None present means you cannot apply seller or category rules. Reject.at— auction type. The spec default when absent is 2, second price plus, but most partners who omit it mean first price. This field decides what the winner pays, so never resolve it with a silent global default: pin it per partner at integration, and set it explicitly on every request you send to demand.tmax— the total milliseconds the sender gives you to answer. Your demand deadline is not a constant; it istmaxminus everything else in the latency table below, and a request that arrives without it gets your configured default, logged.cur— accepted currencies. Default to a single configured currency, and normalize every bid into it before ranking.imp[].bidfloorandbidfloorcur— treat a missing floor as zero, but log it; a supply partner sending unfloored traffic is usually a misconfiguration rather than a decision.
Distinguish the two failure modes in your response. A payload you could not parse — or that violates the schema, like a missing id or an empty imp — is an HTTP 400: the caller has a bug and should see it. A payload that is valid OpenRTB you simply will not transact on, such as a blocked seller or a category you refuse, is an HTTP 204 no-bid. Keep that line clean, because each code trains different machinery on the other side: persistent 400s trip the sender’s endpoint health checks and get your QPS cut, while 400-ing valid requests you merely dislike buries your real integration errors in noise, on their dashboard and yours.
If you want a working answer to most of the request-path decisions in this guide, Prebid Server is an open-source implementation whose validation code you can simply read.
Should you use JSON or Protocol Buffers?
Start with JSON. Every partner speaks it, every proxy logs it, and when an integration misbehaves at 2am you can read the payload without a decoder. Protocol Buffers offer smaller messages and schema evolution through field numbering, which is a genuine advantage at high request volume, but the advantage is a bandwidth-and-CPU one that only pays off once you have measured serialization as a real cost in your own profile.
The migration order that works is: ship JSON, instrument serialization time and egress separately from everything else, and revisit only when one of them shows up in the top few line items. Adopting Protobuf on the strength of a benchmark someone else published is how teams acquire a code-generation step and a debugging handicap in exchange for a rounding error.
Note that the choice is per-partner, not global. Large buyers who support both will negotiate it with you, and the exchange side of that negotiation is far easier if your internal request model is already independent of its wire format.
Where the auction clock actually goes
The whole auction clock is spent waiting for demand. Everything you control is noise beside it, which is exactly why the parts you control have to stay noise.
| Stage | Budget | Notes |
|---|---|---|
| Parse and schema validation | under 5 ms | A compiled validator, not runtime reflection over a schema document |
| Eligibility and floor lookup | under 5 ms | In-memory. A database round trip on this path is a design error, not a slow query |
| Demand fan-out | 200–300 ms | The number you publish to buyers, bounded above by the inbound request’s tmax minus everything else in this table |
| Auction and margin | under 5 ms | Pure computation over the bids that arrived |
| Serialize and respond | under 5 ms | Pre-allocated buffers; do not rebuild the encoder per request |
| Win notice handling | off the critical path | Asynchronous by construction. If it can block a bid response, it will |
Floxis runs a 200–300 ms DSP bid timeout in production, which is a reasonable anchor for what a partner will accept. Treat any budget as provisional until you have measured your own P99 against it — a P99 above your published timeout means you are silently discarding bids you already paid to solicit.
Three implementation details account for most of the difference between a stack that hits these numbers and one that does not:
- Keep connections alive. A fresh TCP and TLS handshake per bid request can consume a meaningful share of the budget before a single byte of the request is sent. Pool connections per seat and size the pool for peak concurrency, not average.
- Terminate TLS at the edge. A load balancer or sidecar does this better than your auction process, and it keeps handshake cost off the same threads that are trying to rank bids.
- Cancel, do not wait. Every seat call needs a deadline derived from the time actually remaining, not a fixed constant. A slow partner must be abandoned mid-flight; if your adapter layer can only wait for a response, one degraded seat sets the latency for everyone.
Win notices, and why they break first
The win notice is the exchange’s only evidence that something it sold actually happened, and it runs through you in both directions. Facing your supply, you are the bidder: the nurl and burl you put in your bid response are called — by the upstream platform or the device — after the auction closes, with macros like ${AUCTION_PRICE} already substituted by the caller. Facing your demand, the roles flip: you substitute the macros into each DSP’s nurl and burl and fire them yourself.
The two URLs are not synonyms. The nurl announces a win, which can happen before — or without — the ad ever rendering; the burl fires at the billable event, and the spec keeps them apart precisely so that money follows burl. Count wins on nurl if you like, but bill and reconcile on burl or your own render tracking, or the gap between the two becomes a revenue overstatement you discover during reconciliation.
Three things make this the most fragile part of a new exchange:
The auction record expires before the notice arrives. Teams size the in-memory auction map for the auction clock — a few seconds — and then find that notices from slow-rendering creatives, or from a buyer batching its callbacks, land well outside that window. The win is real, the money is real, and the exchange has no idea what it belonged to. Size the retention for the slowest legitimate notice you observe, and write through to durable storage for anything that has to survive a restart.
Unsubstituted macros get recorded literally. A win handler that fires a URL still containing ${AUCTION_PRICE} does not fail loudly. It records a string where a price should be, and the corruption surfaces days later in a reconciliation nobody can close. Validate that every macro was replaced before the notice is accepted, and reject the ones that were not.
A broken win loop looks like a demand problem. This is the failure that costs the most time, because it presents in the wrong place. Where the exchange enforces spend caps or pacing for its buyers, those caps are computed from confirmed wins: lose the confirmations and the pacer must either assume the worst and throttle, or assume the best and blow through a cap. Any sensible implementation throttles. So the symptom is falling fill and unhappy supply partners, and the cause is an accounting path that quietly stopped resolving three days ago. Run no caps at all and the same broken loop presents as revenue quietly missing from your own ledger instead; the throttling variant is just the one that gets you a phone call.
The practical defence is a counter, not a framework: compare auctions you believe you won against win notices you actually received, per seat, continuously. It is the cheapest instrument in the entire stack and it is the one that tells you the settlement clock has stopped.
What to measure, and what to alert on
The metrics that matter are the ones that catch a stalled settlement clock, not the ones that look good on a status page.
| Metric | What a change in it means |
|---|---|
| Requests per second | Baseline traffic. A drop is usually upstream of you |
| Bid rate — bids received over requests sent | Falling means seats are timing out, floors are too high, or an adapter is broken |
| Win rate — wins over bids returned | Falling means creative rejections, or your response is losing a downstream auction |
| Bid-to-win discrepancy | The settlement clock’s health indicator. Should sit near zero and stay there |
| Per-seat response latency P95 and P99 | Above your published timeout, you are discarding bids you solicited |
| Drop reasons, counted separately | Turns “fill is down” into “seat 4 has failed schema validation 40,000 times” |
That last row is worth building properly. A single rejection counter tells you something is wrong; a counter per reason tells you what, and usually within minutes rather than a debugging session.
One demand-side lever hides behind the bid-rate row: identity. DSPs bid less often, and lower, on requests they cannot match to a user they already know, so cookie sync and server-side ID bridges are not an optimisation to defer — an exchange with broken sync can run a perfect auction and still watch demand ignore it. Watch match rate next to bid rate, and when bid rate falls, check which of the two moved first.
For alerting, the discrepancy metric is the one to wire first, and it degrades quietly. Pick thresholds against your own baseline once you have a couple of days of production data — a fixed percentage copied from an article, this one included, is a starting point and not a calibration.
Hardening the endpoint against malformed and hostile input
A public bid endpoint is an unauthenticated POST target that parses complex structured input. Treat it accordingly.
On the request side: enforce a maximum payload size and reject above it, since legitimate bid requests are small and an oversized one is either a bug or a probe. Reject negative or non-numeric floors. Rate limit per supply source, so one misconfigured integration cannot exhaust capacity for everyone.
On the response side — the side that gets forgotten — treat every bid response as untrusted too. A seat’s response carries markup that will eventually render on your supply partner’s page, under your brand. Check that the seat identifier on the response matches a seat you actually called. Validate adomain against your allow and block lists. Confirm that creative identifiers are present and that the creative has been approved, or route it for review before it is allowed to clear.
Consent and privacy signals belong in the same pass. The regulatory strings a request carries — the IAB TCF consent string, the US privacy and GPP signals — must reach your demand partners unmodified. An exchange that drops or rewrites them makes every downstream partner non-compliant on its behalf, and the failure is invisible until somebody audits it. Pass them through, log that you did, and treat any code path that mutates them as a defect.
The creative types you have to support
Three formats cover the great majority of demand, and each has its own validation surface.
| Format | Fields to insist on | Response carries | What to check |
|---|---|---|---|
| Banner | imp.banner with a format array of sizes, or the legacy w, h pair |
Markup, or a URL to fetch it | Declared size matches the slot; markup contains nothing that escapes it |
| Video | imp.video with mimes — the only field the spec requires — plus minduration, maxduration and protocols, which it merely recommends and you should insist on |
VAST XML, or a VAST URL | VAST version is one the request asked for; duration inside the declared bounds |
| Native | imp.native with the native request object |
The native response object | Every required asset present; image URLs resolve |
The spec is looser here than a working exchange can afford to be: several of these fields are “recommended” on paper, but demand cannot bid sensibly without them, so require them from your supply and say so in your integration docs.
Video deserves specific attention because it has two independent event streams. The win notice fires when the auction is won; the VAST impression fires when the video actually starts playing. Those are different events with different failure modes, and a discrepancy between them is diagnostic — a large gap usually means creatives are winning and then failing to render. Track both, separately, and never treat one as a proxy for the other.
If your ad server fetches VAST at render time from a slow endpoint, the user sees the latency as a blank pre-roll. For anything at volume, resolve the VAST document at win time rather than at render time.
Testing before you connect live demand
Everything below can be exercised against mock seats, and all of it should be, because the alternative is discovering it with a partner’s money.
- A valid request returns a valid response. Well-formed OpenRTB in, HTTP 200 and a schema-valid bid response out.
- A malformed request returns 400, and an untransactable one returns 204. These are different paths and they get conflated constantly.
- The deadline is enforced. Give a mock seat an artificial delay past the timeout and confirm the auction closes without it, and that its late response is discarded rather than counted.
- The win loop closes. Fire a notice with substituted macros and confirm it lands against the right auction record. Then fire one with an unsubstituted macro and confirm it is rejected.
- A late win notice is handled. Fire one deliberately after the auction record’s retention window and confirm the behaviour is defined rather than accidental.
- Concurrency is safe. Run many simultaneous auctions and confirm the auction state map has no races. This is where a bid arriving exactly at the close boundary will find you.
Wire these into CI rather than a pre-launch checklist. Schema and timing regressions are exactly the kind that reappear.
A rollout you can reverse
Stage the launch, and decide what would make you roll back before you have any reason to.
- Staging under load. Run at your target request rate and confirm P99 sits below your published timeout. Confirm bid-to-win discrepancy is flat, not merely small, across a sustained run. Trigger an alert deliberately and watch it arrive.
- Canary. Route a small share of live traffic for at least a full daily cycle — traffic mix changes by hour, and a two-hour canary sees one shape of it. Compare bid rate, win rate and fill against the baseline rather than against expectations.
- Increments. Step the share up with a verification window between steps, and reconcile accounting against your demand partners at each one. A discrepancy found at 25% is a configuration bug; the same discrepancy found at 100% is a credit note.
- The first week. Watch latency trends and discrepancy daily. Confirm the auction record retention is actually long enough for the slowest notices you now see in production rather than the ones you assumed in staging.
Write the rollback procedure before the canary. One page: the metric that triggers it, who decides, and the exact commands. Decisions made at 11pm under pressure are worse than the same decisions made in the afternoon.
How long does setting up an RTB exchange take?
A working auction — endpoint, validation, fan-out, ranking, response — is a matter of weeks for an experienced team. That estimate is also why so many exchange projects are mis-scoped, because it is the estimate people quote and it only covers the auction clock.
The realistic path to a production exchange is longer, and the extra time goes into the parts that cannot be tested into existence: the win and reconciliation loop agreeing with each demand partner’s numbers, per-seat adapter quirks that only appear under real traffic, latency tuning against actual partner endpoints, and the monitoring that has to exist before you can trust any of it. Then the ongoing operation begins — floors, shaping, and seat health need continuous attention, and unlike the build, that work does not end.
The honest way to size it is to ask how much of the settlement clock you intend to own. Building the auction is a project. Operating the loop is a staffing decision.
Key takeaways
| Point | Detail |
|---|---|
| Two clocks, one deadline | The auction clock can be finished; the settlement clock cannot. Budget for both |
| Validate at the edge | Rejecting at the endpoint costs microseconds; every later stage costs more |
| 400 and 204 are different answers | One says you have a bug, the other says no thanks. Conflating them causes retry storms |
| The win loop is the fragile part | Expired auction records and unsubstituted macros corrupt accounting silently |
| Falling fill is often an accounting bug | An exchange that cannot confirm wins cannot pace, so it throttles |
| Count drop reasons separately | The difference between “fill is down” and a seat name with a specific failure |
| Rehearse the rollback | Decide the trigger and the commands before the canary, not during the incident |
Running the exchange on Floxis instead
Everything above is buildable. The question worth asking is which parts of it you want to own permanently, because the auction is a project with an end date and the settlement loop is not.
Floxis is a white-label RTB exchange you run as your own — your domain, your branding, your invoicing, your margin applied per transaction at a rate you set. Supply and demand partners integrate with your exchange and never see Floxis underneath it. The protocol surface described in this article is what the platform already speaks: OpenRTB 2.5 and 2.6, Prebid, VAST and JS tags, with custom adapters built and maintained for partners who speak none of them. Endpoints are configurable anywhere from 1 to 100k+ QPS, with live clusters in the US, EMEA and APAC.
The settlement clock is where the hand-over is worth the most. Every bid, win and drop lands in your reporting as log-level data about a minute later, so reconciliation reads from a record rather than an argument. The optimization engine then closes the loop automatically: you set an objective per endpoint — net revenue, fill, or win volume — and it blocks, scores and floors each auction toward it. Floors come from each segment’s own observed clearing prices and only ever move above the minimum you set, margins self-tune inside bounds you own, and every lever runs Off, then Shadow, then Enforce against a live holdout, so uplift is measured rather than assumed. A safety breaker reverts Enforce to Shadow if a test arm’s bid rate falls below 80% of the holdout.
The supply-side controls described earlier are built in as well: ads.txt with its own crawler, sellers.json and full schain validation with supply-path analysis, allow and block lists, and IVT and creative scanning you can either bring your own key for or have Floxis run.
For the vendor-evaluation half of that decision — the checklist, the SLA questions, the red flags — see the guide to white label ad exchanges.
If you are weighing a build against this, the useful comparison is not the auction — it is the second clock. Request a technical walkthrough and we will map it against what you already run.
