The manus-watch commit says it missed 152 real credits. The commit before it says a shared alert key ate a real 28-credit charge. Two separate bugs in a guard built to do one job: tell us when the free ride ended.

What the guard was watching for

The Manus free-usage promo was scheduled to end on 2026-08-28. Until then, every task on --profile standard ran at 0 credits, images included. That was true even though the ops repo’s own providers/manus_run.py says in its comments that images cost 70-524 credits per generation. We had measured the discrepancy directly: a 2560x1440 hero image, task S55YRpXRB2rHdhyMpB4W5m, came back with credit_usage: 0 and an unchanged balance. The guard existed for the moment that stopped being true.

The design was straightforward. A spend-guard script reads a balance snapshot through the existing get_balance() call, with no second API client, and persists it under .runs/manus/. It diffs that against the last snapshot. On a real charge, it posts to Discord through notify_owner(venture="dxdev"), which maps to one Discord channel and gates repeats behind a 6-hour key.

The balance shape is the first trap. The response splits credits into pools: freeCredits, periodicCredits, addonCredits, proMonthlyCredits, refreshCredits, maxRefreshCredits. The free-daily pool refreshes to 300 at 04:00 UTC, so totalCredits moving is not evidence of spend. It moves every day at refresh regardless of usage. The guard has to read which specific pool drained to know whether the prepaid plan was touched.

The key two alerts shared

notify_owner takes a key argument so the 6-hour repeat gate can dedupe pings. The plan-pool alert and another alert were sharing one key. When the plan-pool alert fired, it consumed the gate. The next real charge, the 28-credit one, hit the same key inside the 6-hour window and was suppressed with no sign anything was wrong.

The fix was one line: give the plan-pool alert its own key. The threshold logic and the balance math were fine. Two unrelated alerts were fighting over one piece of state that was supposed to be a per-alert identity.

Midnight in the wrong timezone

The free-daily refresh happens at 04:00 UTC. The promo’s end date was tracked as “2026-08-28” with no timezone, and a separate commit corrected it: the promo lapsed on the 27th, not the 28th, “measured, not guessed” from the run records rather than assumed from the calendar.

The guard had the same class of bug at the per-task level. manus-watch checked whether a task’s charge belonged to “today” by comparing against local midnight, while the API’s own day boundary is UTC. On a machine hours behind UTC, a charge that landed after UTC midnight but before local midnight went into the wrong day’s bucket, and the day-over-day comparison threw it out. The commit message states the damage: the credits from the top of this post.

That is the dangerous failure mode for this kind of monitor. It doesn’t crash, and it doesn’t raise an exception anyone would see in a log. It just agrees that nothing happened. A guard that pages too often gets noticed and fixed. A guard that goes silent at exactly the wrong moment looks identical to a quiet night.

One value that was really two

Both bugs share a root cause. Each was state that looked like a single value but was partitioned, and the partition boundary didn’t match the code’s assumption. The alert key was one string standing in for two independent concerns. The date comparison was one clock standing in for two timezones.

Neither bug was visible from reading the function in isolation. Both only showed up when a second, concurrent thing was running against the same state: a second alert type, a second timezone. Reading the code and reasoning about it had already happened once, and it hadn’t caught either one.

So we verified the fix the way the original build spec required. We forced the charge condition with a synthetic previous snapshot, confirmed a Discord message actually landed on the channel, and confirmed the no-change case sent nothing. Firing the guard found what reading it hadn’t.

The guard is a small thing: a balance read, a diff, a Discord POST. But small and safe to skip testing are different properties, especially once something else in the system runs in parallel. A monitor that watches for money moving has to survive money moving at a time its own clock doesn’t expect.

If a monitor exists to catch spend, test it by forcing a real charge and confirming the alert arrives, then confirm a no-change run sends nothing. Check that each alert has its own repeat-suppression key, and that every date comparison uses the same timezone as the system it reads from.