I run a small support-triage agent that reads incoming tickets, classifies them, and drafts a first reply for me to approve. It's been quietly doing its job since spring, costing me roughly $1.80 a day in API calls. I'd stopped thinking about it entirely.
Then, on a Monday morning, I opened my provider's usage dashboard out of habit and saw a number that didn't parse at first: $214.37 charged between Friday evening and Sunday night. For context, my entire monthly budget for every agent I run is $60.
This is the honest story of what happened, why the "smart" fixes I tried first didn't work, and the embarrassingly simple thing that actually stopped it.
The setup, before it broke
The agent is nothing exotic. Every five minutes, a cron job fetches new tickets, and the agent does three things per ticket:
- Summarize the ticket plus the customer's last five messages (context assembly).
- Classify urgency and category.
- Draft a reply.
One API call per step, cheap model for classification, better model for drafting. Predictable. Boring. Exactly what you want from automation.
The trigger for the disaster was a change I made Friday at 5 PM — classic deploy-before-weekend energy. A customer had complained that the agent missed context from long email threads, so I added "thoroughness" to the context assembly step: instead of the last five messages, the agent would now fetch the entire ticket history, and I bumped the context window handling to "summarize recursively if it's too long."
I tested it on one ticket. It worked. I went home.
What actually happened
Two things compounded, and neither was obvious in isolation.
First: one of our enterprise customers had opened a ticket with a 340-message thread — an integration issue that had been going back and forth for weeks. The new "fetch everything" logic pulled all of it.
Second: the recursive summarization. When the thread exceeded the context window, the agent chunked it, summarized each chunk, then summarized the summaries. On a 340-message thread, that's dozens of API calls per run. And the cron ran every five minutes — including on tickets that were already triaged, because my "only process new tickets" filter checked for new tickets, not new activity on known tickets. The agent re-summarized the same monster thread 288 times per day.
Do the math: ~40 calls per run × 288 runs × a moderately priced model with large input tokens. That's your $214.
The worst part isn't the money. It's that every single run succeeded. No errors in the logs. No retries. No anomalies in any metric I was watching. The system was doing exactly what I told it to do, enthusiastically, at a cost I never told it to respect.
The fixes that didn't work
I'll be honest about the order I tried things, because it's embarrassing and probably common.
Fix attempt 1: a smarter summarization prompt. My first instinct was to make the agent "realize" when a thread was too long and skip the deep summarization. I added instructions like "if the thread is unusually long, summarize only the last 10 messages and note that history was truncated."
It worked in testing. It failed in production within hours — the model followed the instruction about 80% of the time, and the other 20% was still expensive. LLM judgment is not a billing control. If your cost protection is a sentence in a prompt, you don't have cost protection.
Fix attempt 2: switching to a cheaper model for summarization. This cut the projected burn by about 60%, which sounds good until you realize 60% of $214 is still $128 a weekend. Cheaper models don't fix unbounded loops; they just make the loop cheaper to run forever.
The fix that worked (and what I built after)
The actual solution was three lines of code and a config value. No model involved.
1. Hard input cap, enforced in code, not in the prompt:
MAX_CONTEXT_MESSAGES = 15
thread = fetch_thread(ticket_id)
if len(thread) > MAX_CONTEXT_MESSAGES:
thread = thread[-MAX_CONTEXT_MESSAGES:]
context_note = "(earlier history truncated)"
The truncation happens before anything reaches the model. The agent can't be thorough with messages it never sees.
2. A per-run budget guard with a kill switch:
if get_run_cost_estimate(ticket) > 0.50:
alert_me(ticket_id)
skip_processing(ticket_id)
Any ticket that would cost more than 50 cents to process gets flagged for me personally instead of being processed. Expensive tickets are rare; a human looking at them for two minutes is cheaper than an agent chewing through them.
3. A daily circuit breaker at the account level:
if today_spend() > DAILY_BUDGET * 1.5:
disable_all_agents()
page_me()
This is the one I should have built on day one. Whatever any single agent does wrong, total spend physically cannot exceed 1.5× my daily budget. The breaker sits in the wrapper around every API call, so no agent can route around it.
Since adding these, the agents have tripped the per-ticket guard exactly twice — both times legitimately weird tickets that I was glad to see flagged instead of billed.
What I'd tell anyone running agents today
-
Cost controls belong in code, not prompts. A model can be talked out of anything by a weird input. A
if cost > x: stopcannot. - "Runs every N minutes" is a loop. Loops need bounds. Anything scheduled should have a ceiling on per-run work: max items, max tokens, max dollars.
- Test your automation against your worst data, not your average data. My test ticket had four messages. The ticket that broke the bank had 340. The average case is never the one that costs you money.
- Watch spend rate, not just errors. My logs were clean the whole time. A simple daily spend check — one cron job that reads the usage API and messages me if yesterday exceeded a threshold — would have caught this Saturday morning instead of Monday.
- Never deploy a "thoroughness" change at 5 PM on Friday. Some lessons are just folklore that turns out to be true.
The $214 stung, but it bought me a setup where the worst case is now a few dollars instead of a weekend of compounding. Honestly, that's the cheapest lesson my agent fleet has ever taught me.
All 100 prompts are in The Agent Prompt Vault — $3, lifetime updates. Steal the ones that fit your workflow.
Top comments (0)