Drop one wrapper around your LLM client and you get three things the raw SDK doesn't: a hard cap that actually holds under concurrency, a per-agent spend breakdown, and an early pause when a loop goes runaway.
node report.js
No network, zero dependencies. It runs a small fleet of named agents under a $50 cap β including one that slips into a runaway loop β and prints the answer you actually want: where the money went, whether the cap held, and that the runaway was stopped.
const { SpendGovernor } = require('./governor');
const gov = new SpendGovernor({
hardCapUsd: 25,
burnSoftUsdPerMin: 30,
onBurnAlert: (info) => notifyHumans(info), // pause-and-ask hook
});
const answer = await gov.run({
provider: 'openai',
source: 'research-agent', // who to bill -> shows in the breakdown
worstCaseUsd: 0.05, // max-output-token price
invoke: async () => {
const resp = await openai.chat.completions.create({ /* ... */ });
return { costUsd: priceOf(resp.usage), result: resp }; // reconcile to actual
},
});
const spend = gov.report(); // { totalSpentUsd, capHeld, bySource, topSpender, ... }
The same gov in front of an anthropic SDK call shares one ceiling and one breakdown β it sits at the call boundary, above every provider.
You set a cap. It looks right. The fleet crosses it anyway, by a wide margin β because a cap that increments after the call is racing itself the moment work runs concurrently.
Cited as motivation. This code is not affiliated with, and makes no claim about, those projects.
The naive cap reads the cumulative total, awaits the call, then adds the cost. Between the read and the add, every in-flight worker reads the same stale total, sees headroom, and proceeds β N workers each pass the cap and collectively overshoot by up to N Γ (largest call). The fix reserves the worst-case cost before dispatching, in one atomic add-and-check with no await in between, then reconciles to the real cost after.
// RESERVE (atomic, no await between read and write) β like Redis INCRBY.
const afterReserve = this.reserved.add(worst);
if (afterReserve > this.hardCap) {
this.reserved.add(-worst); throw new SpendCapExceeded(...);
}
const { costUsd, result } = await invoke(); // the real call
this.reserved.add(usd(costUsd) - worst); // RECONCILE to actual
this._charge(source, usd(costUsd)); // attribute to the source
node demo.js proves it directly: 20 concurrent workers, a $25 cap β the naive version overshoots to ~$78, the governor holds the ceiling.
INCRBY on one key β it returns the post-increment value atomically, so reserve-check and reserve-write stay one round trip with no read-modify-write race. Every .add() β one INCRBY.Every run({ source }) attributes the reconciled cost to that source, so report() tells you who spent what β the difference between βwe spent too muchβ and βthe research-agent's retry loop spent too much.β
A cumulative-$ or per-call check is blind to a loop whose total is nowhere near the cap but whose $/minute is screaming. A sliding-window burn meter fires a βpause and askβ before the hard cap is approached β a loop bug caught in seconds instead of at the ceiling.
add(delta) β INCRBY key delta; value β GET key. One key per budget window, TTL to the window.ZADD / ZREMRANGEBYSCORE) gives the same window across processes.