If an agent decides how many API calls to make, your cost ceiling is whatever the agent feels like today. Most days that's fine. The day a tool errors out and the chain retries its way around it, it isn't, and nothing in your logs will look wrong while it happens.

I maintain budget-guard, a small open source circuit breaker for LLM API bills. It has a LangChain.js adapter, and the whole integration is one callback handler:

import { ChatOpenAI } from '@langchain/openai';
import { BudgetGuardHandler } from 'budget-guard/langchain';

const handler = new BudgetGuardHandler({
  project: 'my-app',
  dailyCapUSD: 25,
  model: 'gpt-4o',
  feature: 'support-bot',
});

const model = new ChatOpenAI({ model: 'gpt-4o' });
await model.invoke(messages, { callbacks: [handler] });

Enter fullscreen mode Exit fullscreen mode

That's the entire setup. Before each model call, the handler checks what the project has spent today. Under the cap, the call goes through and its real token usage is converted to USD and added to the running total. Over the cap, it throws a BudgetExceededError before the request leaves your process, so a runaway loop dies on the first blocked call instead of the hundredth.

Some details that matter in practice.

The cap is in dollars, not tokens. Tokens stopped being a useful unit once cached input and reasoning tokens got their own prices. One "token" can bill at four different rates inside a single response. The handler reads usage_metadata (falling back to llmOutput.tokenUsage on older code paths), splits cached input from uncached, counts reasoning tokens where the provider bills them separately, and prices each part.

The feature tag is the part I would not skip. A single daily total tells you that you have a problem. A per-feature breakdown tells you where. In my case the leak was an enrichment job nobody had thought about in weeks, quietly sitting at 60% of total spend.

Blocking works because the handler sets raiseError internally. LangChain swallows callback errors by default, which would turn the cap into a polite suggestion. Nothing to configure here, but it is worth knowing why the throw actually stops the chain.

It is process-local by default. The default store resets when your process restarts. There is a Redis store so a whole worker fleet shares one cap (with an atomic reserve-then-settle path, so a hundred concurrent calls can't race past the limit together), and a file store for cron jobs that only live for seconds.

And what it deliberately doesn't do: it is not a gateway, there is no dashboard, and it cannot see calls that don't go through it. It is the breaker in your fuse box, not the power company. If you already run a full LLM observability platform, you probably don't need this. If you have one Node app and a nervous feeling about your usage page, it's npm i budget-guard and the five lines above.

The lessons that led to building it are in an earlier post: 7 things I learned trying to stop LLM API bills from silently exploding.