TL;DR: Every bid response a DSP sends carries URLs that anyone who has seen the response can call: impression, click, win, billing. Most DSPs count whatever comes in, which is where discrepancy reports come from. We treated a notification URL like a cheque: valid only if we issued it, for that bid, and it hasn’t been cashed. The bidder mints hashes and a spend reservation at bid time; every callback is checked against them, duplicates and replays are rejected, and unclaimed reservations expire after 45 minutes. Result: about 6% of callbacks rejected as unverifiable, and reported impressions within 1% of the exchanges’ figures.
Every bid response a DSP sends contains a set of URLs: call this one when the ad is shown, this one when it is clicked, this one when the auction is won, this one when it is billed. The exchange, the publisher’s page, and anyone else who has seen the response can call them. Most DSPs count whatever comes in.
That is the quiet reason discrepancy reports exist. Not fraud, usually, though fraud rides along happily. Mostly it is duplicate calls, replayed calls, calls from cached responses that never rendered, and callbacks for auctions the DSP did not win. Every one of them becomes an impression in a report, a dollar in an invoice, and an argument with an advertiser.
When I built the bidder for Mobinner’s DSP, I treated a notification URL the way a bank treats a cheque: it is only good if we issued it, for that amount, and it has not been cashed already.
Mint at bid time, verify at callback.
The mechanism is simple. When a bidder node decides to bid, it does three things before it responds. It reserves the bid amount against the campaign budget in a shared store, so that parallel nodes cannot spend the same dollar twice. It generates a set of hashes, one for the conversion and one for each tracking event, and stores them alongside the reservation. And it embeds those hashes in the notification URLs it returns.
When a callback arrives, the handler does not count it. It looks up the hashes for that bid and compares. If they match, the event is written to reporting, the hash is cleared so the same event cannot be counted twice, and the spend is committed. If they do not match, or there is no reservation, the call is rejected, and nothing is recorded. If no callback ever arrives, the reservation expires after 45 minutes, and a cleaner service returns the budget to the campaign; conversion hashes live 48 hours, because conversions arrive late.
The result is that a notification URL has no value to anyone except the winner of the bid it was issued for, and that nothing reaches a report unless it was bid for, won, and seen once.
What it costs.
A write to the shared store on every bid, and a read on every callback. That is not free at scale, and it is the reason some DSPs skip it: the bid path is where milliseconds matter, and a store round trip is a visible cost. We judged it worth paying, because the alternative is reconciling discrepancies by hand every month, which costs more and never ends. In a typical month, we rejected roughly 6% of incoming callbacks as unverifiable, and our reported impressions came within 1% of the exchanges’ own figures.
Why this matters more now.
Three things have changed since this design was new. Supply-path optimisation means a DSP sees the same impression through several paths, and duplicate callbacks are a feature of the plumbing, not a bug. Server-side rendering and prefetching mean more responses are generated than are ever shown. And the gap between a bid and its billing notice is where most of the money in programmatic goes missing.
A DSP that counts callbacks on trust is making a promise to advertisers it cannot keep. The fix is not a fraud vendor bolted on afterwards; it is refusing, at the point of counting, to count anything you did not issue.
The Test
Ask your DSP one question: if I call your impression URL twice, what happens? If the answer is anything other than “the second call is rejected,” you are paying for the second call.