AI & Agents

Get an Alert When an AI Agent Burns Through Its Token Budget

A looping agent spends money quietly until the invoice arrives. Track tokens against a budget and push an alert to your phone when it crosses the line.

Red and black halftone illustration: a coin-fed electricity meter on a basement wall, dial spinning behind glass, slot below (TheNotificationApp)

The expensive agent failure is not the one that crashes. It is the one that works.

A tool returns something slightly malformed, the model tries again, the retry produces the same result, and the loop keeps its composure for four hours. Every iteration is a legitimate API call. Nothing errors. Nothing alerts. You find out from the usage dashboard, some days later, when the number has already been spent.

Every other kind of monitoring you have is watching for failure. This one has to watch for success that costs too much.

The thing to actually measure

Token usage comes back on every response, so you already have the raw number. What you need is a running total, a price, and a line you would not want to cross without knowing.

Two lines, in fact. A warning threshold, where you want to be told, and a hard ceiling, where the agent should stop on its own. The warning is the useful one day to day. The ceiling is the one that saves you the weekend a loop gets going on Friday night.

A budget you can wrap around anything

import os, requests

TNA_URL = "https://thenotification.app/api/sendNotification"

def notify(title: str, body: str) -> None:
    try:
        requests.post(
            TNA_URL,
            headers={"app_key": os.environ["TNA_APP_KEY"], "Content-Type": "application/json"},
            json={"title": title, "body": body},
            timeout=10,
        )
    except Exception as exc:
        print(f"[notify] {exc}")

class BudgetExceeded(RuntimeError):
    pass

class Budget:
    """Track spend, warn once, then stop."""

    def __init__(self, label, ceiling_usd, warn_at=0.8, per_1k_in=0.003, per_1k_out=0.015):
        self.label = label
        self.ceiling = ceiling_usd
        self.warn_at = warn_at
        self.per_1k_in = per_1k_in
        self.per_1k_out = per_1k_out
        self.spent = 0.0
        self.calls = 0
        self.warned = False

    def record(self, usage) -> None:
        self.calls += 1
        self.spent += (
            usage.input_tokens / 1000 * self.per_1k_in
            + usage.output_tokens / 1000 * self.per_1k_out
        )

        if self.spent >= self.ceiling:
            notify(
                f"{self.label} stopped at ${self.spent:.2f}",
                f"Hit the ${self.ceiling:.2f} ceiling after {self.calls} calls.",
            )
            raise BudgetExceeded(f"{self.label}: ${self.spent:.2f} of ${self.ceiling:.2f}")

        if not self.warned and self.spent >= self.ceiling * self.warn_at:
            self.warned = True
            notify(
                f"{self.label} at ${self.spent:.2f}",
                f"{int(self.warn_at * 100)}% of the ${self.ceiling:.2f} budget, {self.calls} calls in.",
            )

The warned flag is the part people leave out and regret. Without it, every call past 80% sends a notification, so a loop that is already burning money also spends your notification quota telling you about it, forty times a minute.

Using it

budget = Budget("Nightly triage", ceiling_usd=5.00)

try:
    while not done:
        response = client.messages.create(model=MODEL, messages=history, tools=TOOLS)
        budget.record(response.usage)
        history = advance(history, response)
except BudgetExceeded as exc:
    print(exc)   # the notification already went out

Put record immediately after the model call, before any tool execution. If a tool is what is hanging, you still want the spend counted, and you want the exception raised before the next expensive call rather than after it.

Per-day, not just per-run

A five dollar ceiling per run does nothing if a scheduler starts forty runs. Persist the total somewhere shared and the same class covers the day:

import json, datetime, pathlib

STATE = pathlib.Path.home() / ".agent-budget.json"

def load_today() -> float:
    today = datetime.date.today().isoformat()
    if STATE.exists():
        data = json.loads(STATE.read_text())
        if data.get("date") == today:
            return data.get("spent", 0.0)
    return 0.0

def save_today(spent: float) -> None:
    STATE.write_text(json.dumps({"date": datetime.date.today().isoformat(), "spent": spent}))

A file is enough for one machine. If the agent runs in more than one place, use whatever you already have: a Redis key, a row in Postgres. The point is that the counter outlives the process, not that the storage is clever.

What to put in the notification

"Budget warning" makes you go and look. Include the numbers and you can decide from the lock screen:

  • Where it is now. The dollar figure, not a percentage on its own.
  • How fast it got there. The call count is a rough proxy for whether this is normal work or a loop.
  • Which agent. Obvious with one. Essential with four.

"Nightly triage at $4.10, 80% of the $5.00 budget, 612 calls in" tells you it is looping, because a triage run does not make six hundred calls. That is the notification doing real work.

The honest part

Two notifications per agent per run, at most, by design. That fits comfortably in the free tier, which is 100 notifications for the lifetime of the account rather than per month. Pro is $2.99 a month for 1,000 if you are running enough agents to need it.

The bigger caveat is that a hardcoded price table drifts. Model prices change, and a budget calculated on last quarter's numbers is a budget that is quietly wrong. Read them from config, and if the exact figure matters, treat this as an early warning rather than an accounting system. It is there to tell you something is looping, not to reconcile your invoice.

Where this fits

This is the same one-line POST as every other alert here, pointed at a number instead of an exit code. If you want the completion side too, that pattern is in notifying your phone when an agent run finishes, and the request shape is in the API reference.

Grab a key at thenotification.app and put a ceiling on the next agent you leave running overnight.

New to this? Start with what an MCP server is.