LabArticleSeat-measured

A lifetime cap that silently became one day

An optional lifetime budget was coalesced to the daily budget on every Beeswax write path. The campaign looked uncapped in our database and stopped after day one.

Published 2026-09-09 · Search job: campaign stopped spending after one day

A campaign that stopped spending after one day was often not paused, not out of prepaid balance, and not out of daily budget. It had hit an exchange lifetime cap that our own database still called optional and unset.

total_budget_cents is documented as optional. When a buyer omits it, the campaign is supposed to spend until it is paused or the prepaid wallet is empty. All three Beeswax write paths coalesced that null with daily_budget_cents. The campaign our database showed as uncapped was created with spend_budget.lifetime equal to its daily budget. Day one exhausted the exchange lifetime. The line item never served again. campaigns.status still read running. Nothing raised an error.

It happened three times

This is seat-measured, not a one-off inference.

  1. June 18, 2026 learning campaigns. The same coalesce wrote a one-day lifetime. Delivery stopped after the first day while the product still showed the campaigns as running.
  2. The June 2026 relaunch. The operator had to raise total_budget_cents to 14000 by hand so the exchange would keep serving past day one.
  3. September 3, 2026. A continuous $20/day portfolio landed with a $2.00 lifetime cap. The same pattern: our row looked open-ended, Beeswax held a lifetime equal to (or smaller than) one day, and the line item went quiet.

Three hand-fixes of the same silent stop is the definition of a load-bearing bug. The next agent who "simplifies" the mapper will recreate it.

The fix, and the two ways to break it

The coalesce now lives in one function, beeswaxLifetimeBudgetCents.

  • An explicit cap is honoured exactly. A buyer who asks for a $20 lifetime test gets $20, not 30 times $20.
  • A null or zero cap gets OPEN_ENDED_ENVELOPE_DAYS (30) times the daily budget. That is an envelope so the exchange has a number, not a product promise that the campaign lasts 30 days.

Do not reintroduce ?? daily_budget_cents at the three call sites that create or resume a campaign, create a line item, or re-sync the serving graph on resume. Do not "simplify" an explicit small cap through the 30x path either. Those are two different mistakes that look like helpful defaults.

What this is not

This is not a pacing story. Pacing spreads a daily budget inside a day. This bug set the lifetime equal to one day, so there was nothing left to spread on day two.

It is also not a prepaid-wallet story. The campaign can still show as running with balance remaining. The exchange has already decided the object is done.

If a campaign dies after one day and status still says running, read the Beeswax spend_budget.lifetime before you change targeting, creatives, or bid. The first question is whether we sent a one-day cap by accident.

How to traffic a campaign you can actually judge is the parent play. Budget fields have to move together is the sibling failure, where daily was written above lifetime and Beeswax rejected the write.