Two spend sources, one auction
beeswax hourly rows and beeswax_snapshot lifetime rows cover the same auctions. A plain sum doubles spend. Use greatest of max snapshot and sum of non-snapshot.
Published 2026-09-09 · Search job: double counting ad spend
Double counting ad spend on this seat happens when two impression_log_rollups sources are added together. beeswax is hourly Reporting API rows. beeswax_snapshot is one lifetime row per line item. The snapshot dedupe key ends :lifetime. Both cover the same auctions. A plain sum(spend_cents) across all rows roughly doubles spend.
Every product read uses greatest(max(spend) filter (source = 'beeswax_snapshot'), sum(spend) filter (source <> 'beeswax_snapshot')). That is the rule. Do not "simplify" it to a single sum.
What a naive sum already did
A derived hour in the reporting-row dedupe key produced the same class of double. Ten campaigns each gained a duplicate hour and the non-snapshot sum doubled from $24.46 to $48.92, which paused the fleet on a cap it had not reached. That writeup is Do not put a derived hour in a dedupe key. Two ways to double, one auction. Supply Audit observed_spend_cents can double the same way when inventory_agg and domain_agg are added together over the same auctions.
The prepaid ledger was not over-debited in the hour-label incident because the cross-source debit reconciles reporting against the lifetime snapshot. That check is load-bearing.
Clicks are not on the snapshot
Snapshot rows store clicks = 0 on purpose. The performance endpoint excludes snapshot when it reads clicks. Real clicks come from source = beeswax reporting. If you reuse the spend greatest() pattern on clicks, you will pick the snapshot zero whenever it is "greater" in the wrong direction, or you will add zeros. Clicks have their own rule. Why a dashboard can show zero clicks.
What we will not do
We will not drop the snapshot because hourly reporting lags. Lifetime spend tracks the exchange within about a minute because the supervisor writes a snapshot every tick. Today's hourly spend is built from reporting rows that arrive later. The greatest() read exists so a lagging hour cannot hide spend the snapshot already saw, and so a snapshot cannot be added on top of hours that already contain it.
We will not publish a third source as if it were a third set of auctions without proving it. Two sources, one auction, one greatest(). If a later verification query reports a spend figure that is about twice the snapshot, assume this bug before assuming the fleet doubled its spend. Prove the source split first. Then judge the buy.