reneLast week I published a small JavaScript class that stops AI agents from burning money on tool calls....
Last week I published a small JavaScript class that stops AI agents from burning money on tool calls. A reader left a comment that was better than the article: use the real token counts from the API response, make the price a per-model map, and cap each tool separately. While answering, I reread my own code and found two bugs. This post is the corrected version.
budget.cost was never initialised, so budget.cost += cost produced NaN on the first call.EventEmitter in this.emitter but my example called guard.on(...), which did not exist.If you copied the first version, fix both before trusting any number it prints.
A length / 4 guess is fine for deciding whether to start a call. It can be off by 2x for code or non-English text, so it must not be what you record. Every major chat API returns the real count in the response, for example the usage object described in the OpenAI API reference and the usage field in the Anthropic Messages API.
const PRICE_PER_1M = {
"cheap-model": { in: 0.15, out: 0.60 },
"expensive-model": { in: 3.00, out: 15.00 },
};
function costOf(model, usage) {
const p = PRICE_PER_1M[model];
if (!p) throw new Error(`no price for ${model}`);
return (usage.input_tokens * p.in + usage.output_tokens * p.out) / 1e6;
}
Throwing on an unknown model is deliberate: a silent default price is how budgets drift. The numbers above are placeholders, so copy current prices from your provider's pricing page.
import { EventEmitter } from "node:events";
export class BudgetGuard extends EventEmitter {
constructor({ perToolUsd = 2, perAgentUsd = 10 } = {}) {
super();
this.perToolUsd = perToolUsd;
this.perAgentUsd = perAgentUsd;
this.spent = new Map(); // "agent|tool" -> usd
}
_agentTotal(agent) {
let sum = 0;
for (const [k, v] of this.spent) if (k.startsWith(agent + "|")) sum += v;
return sum;
}
canCall(agent, tool, estimateUsd = 0) {
const key = `${agent}|${tool}`;
const tool$ = (this.spent.get(key) ?? 0) + estimateUsd;
const agent$ = this._agentTotal(agent) + estimateUsd;
if (tool$ > this.perToolUsd) { this.emit("blocked", { agent, tool, why: "tool" }); return false; }
if (agent$ > this.perAgentUsd) { this.emit("blocked", { agent, tool, why: "agent" }); return false; }
return true;
}
record(agent, tool, usd) {
const key = `${agent}|${tool}`;
this.spent.set(key, (this.spent.get(key) ?? 0) + usd); // starts at 0, no NaN
}
}
Usage: call canCall with the cheap estimate, make the request, then call record with costOf(model, response.usage). Because the class now extends EventEmitter, guard.on("blocked", ...) works.
One looping search or retrieval tool is usually where the money goes. An agent-level cap stops the damage eventually, but a tool-level cap stops the one misbehaving tool while the rest of the agent keeps working.
How do you set the per-tool limit: a fixed dollar number, or a percentage of the agent's total budget? I have used a fixed number so far and I am curious whether a share works better in practice.
This article was written with AI assistance and checked by the author.