Measured, not asserted
Published
A price floor is three things wearing one name: a table of prices by placement and market, a default for everything the table does not name, and a flag that decides whether any of it binds. This test renamed each of them against a real obfuscation engine, plus the competitive block list and the deal priority beside them. The auction still ran, still picked the highest eligible bid, still cleared and still reconciled. In the worst arm the blocked competitor won three hundred thousand impressions, revenue went UP, and the counter that exists to report exactly that read zero.
Three things wearing one name
The fixture is a session of three hundred thousand ad requests across six placements in three markets, four bidders on every request. Guaranteed and preferred demand sits close to the floor because it was negotiated there; open-auction demand is spikier and occasionally beats it on price. That overlap is the point -- if the guaranteed bid always won on price there would be nothing for a priority order to do.
Correct, the session fills 225,000 of 300,000 requests for 1,086.75, guaranteed deals win 200,000 of them, nothing clears below its floor, and the blocked advertiser wins nothing.
Floor enforcement ships OFF by default in real products, and the reason is commercial rather than technical: a floor that silently refuses demand is a support ticket and a floor that is merely reported is not. That single default is what makes the two halves of a floor fail in opposite directions.
The guard and its own alarm, lost in one edit
The block list is one entry: a competitor that holds a guaranteed deal of its own and bids the most money on every page. The only thing keeping it off this inventory is the exclusivity clause the sales team negotiated.
^(blockedAdvertisers)$ lets it win everything. Fill goes from 225,000 to 300,000. Revenue goes from 1,086.75 to 2,336.25, more than double. Guaranteed wins go from 200,000 to 300,000. Every commercial metric on the page improves, which is precisely the problem: a yield report shows a spectacular session.
And BLOCKED-SERVED, the field whose entire job is to report a blocked advertiser being served, reads 0. It is computed by asking the block list whether the winner is on it, and the block list is what was lost. The guard and the alarm that watches the guard went in one edit.
This is the same shape as a denylist of forbidden keys that stops matching, or a cache serializer that strips secret fields by name and copies all of them. A rule expressed as data does not move when the name it refers to moves -- and if the same name is also how you detect the failure, you lose both at once.
Two halves, opposite directions
^(floorRules)$ loses the table and leaves enforcement switched on, so every placement falls back to a five-CPM default floor that most of the book cannot clear. Fill collapses from 225,000 to 75,000 and revenue from 1,086.75 to 378.00. That is a two-thirds drop in fill rate, which is on the front page of every publisher dashboard, and somebody is on the phone about it the same afternoon.
^(enforceFloors)$ loses the flag and leaves the table intact. The table is still correct, still printed on the configuration screen, still returned by the API that asks what the floors are -- and consulted for nothing. On its own the publisher's rule catches it, refusing the session with floor-check(floors are not being enforced).
^(requireFloorCheck|enforceFloors)$ is the silent version: 300,000 filled, revenue 1,433.25 -- higher than correct -- and 100,000 impressions cleared below their floor, giving away 47.25 of floor revenue. The publisher's own rule was still present and still correct. It was simply never called.
^(defaultFloorCpm)$ is the mild one: the unlisted placement loses its fallback floor, fill rises to 262,500 and revenue to 1,262.25. Higher revenue, cheaper inventory sold cheaper than the publisher intended.
A promise that is a sort order, not a price
A guaranteed deal is sold on the promise that it clears BEFORE the open auction at the same money. That promise is not a price. It is a sort order, and it lives in one option key.
^(dealPriority)$ ranks every bid on price alone. The auction still runs, still picks the highest bid, still clears, still reconciles, and foots=true. Revenue even goes slightly UP, from 1,086.75 to 1,099.25, because price-ranking maximises price. Guaranteed wins fall from 200,000 to 150,000.
Those fifty thousand impressions are a contractual delivery shortfall that the publisher will make good on later, at its own cost, and nothing in the session reports it as anything other than a marginally better day. ^(auctionType)$ is the honest counterpart: swapping first-price for the library's second-price default moves revenue from 1,086.75 to 1,055.00 and changes nothing else.
Excluding the config is not enough while the records are in scope
The bid records reach the same three answers by a different route. ^(advertiser)$ is byte-identical to renaming the block list: 300,000 filled, 2,336.25 of revenue, the competitor winning everything. The engine cannot match a block list against a name it can no longer read.
^(placement|geo)$ is byte-identical to renaming the floor table -- 75,000 filled for 378.00 -- because a floor looked up by placement and market is a table miss when neither is readable.
^(dealType)$ goes one step past the priority arm. Revenue is 1,099.25, exactly as when the priority order itself was renamed, but guaranteed wins fall to zero rather than 150,000: with no deal type on any bid there is no guaranteed demand to rank first, and the publisher delivered none of its contracted inventory.
So excluding the options object from a rename pattern is not sufficient while the records it is evaluated against are still in scope. Only two arms in this area are loud: renaming bids throws outright, and renaming cpm produces REVENUE=NaN -- and note that even that one fails OPEN on the floor, because a comparison against a value that is not a number is false and every bid becomes eligible.
What to change, whether or not you obfuscate
Express competitive exclusions as an ALLOWLIST of advertisers permitted on the inventory rather than a denylist of those excluded. An allowlist that loses its name stops matching and fails closed; a denylist that loses its name stops matching and fails open. The shape decides the direction, not the diligence.
Compute the alarm from a different source than the guard. Reconcile served creatives against the contract rather than against the block list, and reconcile clearing prices against a floor table loaded independently of the one the auction used.
Treat guaranteed delivery as a target to be measured, not an ordering to be trusted. A daily count of guaranteed impressions against the contract would have shown fifty thousand missing on day one; the session report showed a good day.
If you do obfuscate, keep member renaming narrow and exclude the yield configuration and the bid records. Renaming identifiers, renaming globals and control-flow transformation are unaffected -- the base column measured all five areas on five presets and every one was byte-identical. Publishers running their own stack should apply the same exclusion in the self-hosted build as in anything they hand to a partner.
How this was measured
A grounding harness drives the protection engine directly. Every sample is deterministic and the unprotected original is run twice and required to reproduce itself before anything is compared to it. The simulated floor-enforcement library is copied in unprotected, and the supply platform's settlement arrives as text that is parsed, so only this bundle's READ of those field names can move.
Before any renaming, every option key and record field was deleted one at a time to establish that it moves the output. The enabling flag and the caller's rule behind it were declared inert in advance in all five areas of this pass, because an inert arm that was predicted is a held prediction and an inert arm that was not is a blind one.
The base column ran all five areas on five presets and all twenty-five verdicts were byte-identical to the unprotected run. For the wider question of what obfuscation is worth on code delivered through an ad slot, see obfuscating ad tech JavaScript.
Frequently asked questions
Did obfuscation on its own break the auction?
No. Five presets -- the ES5 default, the modern target, the emit gate's option set on both targets, and the string transforms -- all produced output byte-identical to the unprotected run across all five areas measured this pass.
What actually broke it, then?
Member renaming with a pattern that reached the names the yield configuration is made of: the floor table, the default floor, the enforcement flag, the deal priority, the auction type, the block list, and the bid record fields.
How can revenue go UP and the session still be a failure?
Because the constraints that were lost were the ones costing money. Renaming the block list let a blocked competitor bidding the most on every page win everything: fill went to 300,000, revenue more than doubled to 2,336.25, and an exclusivity contract was breached on every impression.
Why did the blocked-served counter read zero?
Because it is computed by asking the block list whether the winner is on it, and the block list is what was lost. The guard and the alarm that watches it went in one edit -- the same shape as a denylist of forbidden keys that stops matching.
Why do the floor table and the enforcement flag fail in opposite directions?
Losing the table leaves enforcement on, so a default floor refuses most of the book and fill collapses from 225,000 to 75,000 -- loud, and noticed the same day. Losing the flag leaves the table intact, correct and never consulted, so 100,000 impressions cleared below their floor silently.
What does renaming the deal priority actually change?
Only the sort order. Every bid is still eligible, the highest bid still wins, the session still reconciles and revenue even rises slightly. Guaranteed wins fall from 200,000 to 150,000, which is a contractual shortfall reported as a marginally better day.
What is the safest way to use member renaming here?
Keep the pattern narrow and explicit and exclude any options object carrying floor tables, enforcement flags, priority orders or block lists, along with the bid records they are evaluated against. Renaming identifiers and control flow is unaffected by all of this.
Related reading