Most spend-gate implementations for AI agents follow the same shape: read the current spend total, compare it to the limit, decide, write the decision. That's a classic check-then-act pattern — and if two calls for the same wallet land close enough together, both can read the total before either one writes, and both get approved even though combined they blow the limit. It's a textbook TOCTOU race condition, and it's easy to miss because it only shows up under real concurrent load, not in a quick manual test.
The other half of the problem: most audit logs are just timestamped rows. Anyone with database write access can edit history after the fact. For a system making autonomous approve/deny decisions, that's not good enough — the log itself needs to prove it wasn't tampered with, independent of who has DB access.
Open-sourced the reference pattern for both pieces: valta-audit-chain.
The gate: a synchronous checkSpend() that runs before the call fires — balance → per-tx limit → daily limit → monthly limit, cheapest/most-decisive check first, denial short-circuits before touching the ledger.
The log: every decision (approved or denied) gets SHA256(prevHash | scopeId | agentId | eventType | timestamp), chained to the entry before it. Edit, reorder, or delete any entry after the fact, and verification fails.
Clone it, npm run demo — simulates a rogue agent spending $2 four times against a $6 daily limit. Loops 1–3 approved, loop 4 denied, chain verified at the end.
The concurrency bug above? Named explicitly in the README, not hidden. This reference has no row locking, no atomic check-and-reserve — it demonstrates the pattern, not a production-hardened implementation. If you're wiring this into something with real concurrent load, the README spells out three ways to close the gap (row locks, atomic conditional updates, optimistic concurrency with version columns) — pick one before you ship it.
Repo: https://github.com/Billionaire664/valta-audit-chain
This came out of building Valta — happy to go deeper on any part of it in the comments.
Good breakdown. One thing I'd add from running a portfolio of AI-powered apps: the race condition is real, but the bigger dollar leak often happens upstream — before you even hit the spend gate.
Most teams default every agent call to their most expensive model, so 60-70% of tasks burn premium tokens on work a cheaper model handles equally well. We saw $10K/mo → $3K/mo just by classifying task complexity and routing to the right model tier before execution.
The spend gate then becomes a safety net for edge cases rather than the primary cost control. Pairs well with the atomic reserve pattern — if you reserve against per-model-tier budgets rather than a single pool, the concurrency window matters less because each tier has its own cap. A planning task can't eat the budget meant for linting, and vice versa.
Strong framing. The part I would make explicit is that the approval step and the side-effect trigger need one atomic boundary. If checkSpend() approves but the external payment/API call is fired before the reserve/write is durable, you still have a small gap under retries or worker crashes.
For money-moving agent workflows I like this shape: reserve budget atomically -> emit a signed intent/event -> execute the external call -> settle or release the reservation. The audit chain then proves the decision history, while the reservation prevents concurrent agents from spending the same allowance twice.
This is the same shape of bug I see in agent tool permissions generally. The check happens, gets a green light, and then the actual action runs moments later against a world that already moved on, so the gate ends up validating a snapshot that's already stale by the time it matters. The fix that has actually held up for me is making the act itself atomic with the check, in the same call, rather than trusting a check that happened a moment earlier. Anything short of that is just narrowing the window, not closing it.