The spend alert that was wrong, and the rule it produced

Note · Operations~5 min read · TypeScript · telephony billing · incident · October 2026

A glass 3D gauge reading far past its true mark, with a smaller correct gauge beside it

On the evening of 15 October 2026, three days before an AI phone receptionist went public for a client's business, the system sent its first spending alert. October usage, it said, had passed $60 of the $100 monthly target. A push notification reached the owner's phone and a text message reached mine. The telephony provider's own total for the month, checked the next day, was $29.97. The alert was wrong by a factor of about 2.4, and it was my code that was wrong.

What the number was for

The client set one firm expectation about money: the phone line should cost about $100 a month, all in, and nobody should be surprised by the bill. So the system meters everything the provider charges for (call minutes, speech recognition, generated speech, text segments and carrier fees, the number's rental, the texting campaign fee, one-time registration fees) and compares a running figure against the target. At $60 and $90 it alerts the owner and me, once each per month. The alert does not switch anything off. It exists so that two humans know where the month stands.

That makes the figure's correctness the whole product. An alert that fires early is not harmless caution; it is the system telling its owner something false about the business’s money, and training them to ignore the next one.

How $29.97 became $71.78

The first version read the provider's month-to-date usage records and summed every category it returned. That is the obvious implementation and it is the wrong one. The categories are nested: a parent such as phone numbers repeats its child, local phone numbers; recordings repeats call recordings; the registration-fee rollup repeats its one-time and campaign children; and some children are not part of the total at all. Summing the list counted most of the month two or three times. The result, $71.78, cleared the $60 threshold and the alert went out at 6:14 PM.

The code had tests. They passed. They had been written against a usage response I made up, and a made-up response carries the author's assumption about its shape. The test that would have caught this was a real snapshot of the provider's answer, and that test did not exist until the incident produced it.

Closing it properly

  • Fix the figure. The same evening the calculation was changed to read only the provider's own total record. A regression test was built from the real October snapshot, the one that had produced $71.78, so this exact failure cannot come back quietly.
  • Do not delete the evidence. The false alert row was not removed. It was retracted with an audit event that records the threshold, the wrong figure and the reason, and both outgoing messages (the push and the text) stay in the outbox as sent, because they were. The record says what happened, not what I wish had happened.
  • Prove a real alert still fires. After the fix there was no alert row for October, and a test raises the $60 alert exactly once at $60.06 on the same code path. Fixing an over-count must not quietly disable the alert.
  • Tell the owner in plain words. A short note in plain words: the notification was wrong, it was a counting mistake in the new system, it was fixed the same evening, the month's cost so far was about $33 of the $100 target, and nothing had been switched off. The two figures differ in scope, not by error: $29.97 was the provider's own running total, which had not yet priced that month's texts, while about $33 was the reconciled figure before tax (the larger of the provider's number and our own records, per family of charges, never summed across nested categories). The project's spending record shows both.
  • Account for the incident's own cost. One text segment, roughly a cent and a half, already inside the month's figure. The push was free.

The rule it produced

The lasting output of a small incident should be a rule that stops a class of mistakes, not a patch for one. The rule here is written at the top of the spending module and in the project's standing constraints: no computed money figure may alert anyone until it has been checked against the provider's own total. Two things follow from it.

First, the system now keeps three money figures that are never added together: what the provider has billed (its own total, which lags by hours and is the only source for one-time charges), what our records estimate (every call and text at verified unit prices, current to the second), and what is reserved (calls in progress at their worst case, texts queued but unsent). Each is labelled with its source wherever it appears. A reservation is not a charge, and an estimate is never called "actual".

Second, when the billed and estimated figures are reconciled, the provider's total is split into families (calls, texts, fixed monthly charges, and "other" for anything new or one-time), and the month's spend is other plus, for each family, the larger of the provider's figure and ours. Nothing is counted twice, nothing billed is omitted, and a lag on either side can only make the figure higher. An unknown category falls into "other" and is over-counted rather than lost. If the number is going to be wrong, it is going to be wrong in the direction that gets a human to look.

Why it was a near miss and not a disaster

The line was still restricted to approved testers, so no customer was affected, and the only people alerted were the two who were supposed to be. But the flaw was structural, and it would have gone public three days later. In a busy month it would have fired a false $90 alert; in a quiet one it would have reported "over the target" for a bill that was nowhere near it. The incident cost a cent and a half and an evening. What it bought was a rule I now apply everywhere: a computed figure about money is a claim, and a claim about money gets checked against the party that is actually sending the invoice.

Read nextA local model on the call path: grammar-constrained, resident, signed