Your spend ceiling has to be a 403, not a dashboard
The agent that bankrupted its operator while scanning DN42 didn't malfunction. That's the part that bothers me. It ran a loop, got results, adjusted, ran the loop again. Nothing crashed. Nothing hallucinated. It just kep
The agent that bankrupted its operator while scanning DN42 didn't malfunction. That's the part that bothers me. It ran a loop, got results, adjusted, ran the loop again. Nothing crashed. Nothing hallucinated. It just kept going, because nothing in its world told it that going costs money.
Cost lived in a dashboard. Dashboards are for humans. Agents don't look at dashboards, and even if you paste a number into the system prompt, that's a suggestion, not a constraint.
So here's the tip, and it's the only one I care about this week: put the spend ceiling inside the tool wrapper your agent calls, as a hard precondition that fails the call the way a 403 fails. Not a metric. Not an alert. A refusal.
Why the wrapper and not the orchestrator
The obvious place to put a budget check is the agent loop — before each step, check remaining budget, stop if it's gone. That works until you have subagents, parallel tool calls, or a retry policy, and then it stops working in ways that are hard to see.
The wrapper is better because it's the one chokepoint every side effect passes through. Every HTTP request, every shell command, every model call. If it can spend money, it goes through the wrapper. If it doesn't go through the wrapper, it shouldn't exist in your tool list.
def call_tool(name, args, ctx):
est = estimate_cost(name, args) # from YOUR price table
if not ctx.budget.try_reserve(est):
return ToolError(
code="BUDGET_EXCEEDED",
retryable=False,
message=(
f"Budget exhausted for task {ctx.task_id}. "
f"Remaining: {ctx.budget.remaining():.4f} USD. "
"This is terminal. Do not retry. "
"Summarize findings so far and stop."
),
)
try:
result = dispatch(name, args)
finally:
ctx.budget.settle(est, actual_cost(result))
return result
Three things in that snippet matter more than the rest.
The check runs before dispatch. Post-hoc accounting tells you how much you already lost. It's an autopsy, not a guardrail.
It reserves, then settles. If ten parallel subagents each read remaining > 0 and all pass, you've blown the ceiling by 10x. Reserve the estimate up front, settle to actual after. The reservation is what makes the ceiling real under concurrency.
The error is shaped like a 403, not a 500. This is the bit people skip. Your agent's retry logic almost certainly treats tool errors as transient — that's the sane default, networks flake. A budget error is not transient. If it looks retryable, you get a retry storm that burns tokens arguing with a wall. Mark it terminal, say so in the message, and give the model a legal exit: summarize and stop. An agent with no graceful stop will flail, and flailing costs money too.
Estimate from your table, not from the model
The model will underestimate. It doesn't know your egress pricing, it doesn't know that this particular endpoint bills per request rather than per token, and it has no idea that a scan is cheap per call and catastrophic per thousand calls.
So the estimate comes from a table you wrote, keyed by tool name and argument shape. Rough is fine. requests × unit_price, tokens × price_per_1k, seconds × gpu_hourly / 3600. The point isn't precision, it's that the number exists before the call instead of after.
This is also why a single global "tokens spent" meter is the wrong abstraction. The DN42 case was a scanning task — the cost driver there was almost certainly requests and bandwidth, not completion tokens. Different tools spend different units. One meter can't see all of them.
Put the number in the context too
The hard check is the guardrail. The number in the prompt is the steering wheel. Both, not either.
I put remaining budget in the task context on every turn: "You have $3.40 left for this task." An agent that can see the number will sometimes choose the cheaper path on its own — fewer pages, a smaller model, a cached result. An agent that can't see it will always choose the thorough path, because thorough is free from where it's standing.
But never let the model's own arithmetic be the check. It's an input to its planning, not a fact about your account.
Set the ceiling low enough that hitting it is boring
If hitting your budget ceiling is an incident, the ceiling is too high. Mine are set at the level where I'd shrug. A per-task ceiling, not just a per-day one — a day budget lets one runaway task eat everything by 9am and then you're debugging a "mystery outage" that's actually your own guardrail working correctly.
And log every tool call with args and cost, before you need it. The bug report you'll get is "it did something weird yesterday," and the only thing that saves you is a row per call with the price attached.
The uncomfortable part
The DN42 agent didn't need better reasoning. It needed a number it couldn't argue with. Every reliability problem I've fixed this year came down to removing freedom, not adding intelligence — and cost control is just another one of those. You're not making the agent smarter about money. You're making it impossible to spend money it doesn't have, and telling it so in a language it already understands: a failed call.
Give it a cost input. Make it fail like a 403. Then go look at your dashboard, which is now decoration.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.