AI Engineering16 min readJul 2026 Last verified Jul 2026

Why Your AI Agent Quietly Ran Up a $40 Bill

An AI agent is a loop, and every production failure is a missing guard on that loop. We build the naive agent that bills $40, add the four guards that make it safe (with real code), and finish with a 30-minute drill where you prove each guard by breaking it.

AIAgentsLLMReliabilityLangGraph
SB

Sri Balaji

Founder · TheSimplifiedTech

On this page

The invoice nobody could explain

A support agent answered a customer in four seconds. Polite, correct, done. The same request, on a different day, ran for two minutes, made forty tool calls, and cost forty dollars, and no dashboard showed anything wrong. Latency looked normal. Error rate was zero. The bill was the only symptom.

That gap is not a mystery once you see what an agent actually is. It is not magic and it is not a model. It is a loop, and a loop has exactly two ways to die.

Who this is for

Anyone who has wired up an agent with LangChain, LangGraph, or a raw tool loop and wondered how to keep it from misbehaving in production. No framework knowledge assumed, we build the mental model from scratch.

An agent is a four-step wheel

An agent is a model put in a loop that can take actions, look at the result, and decide what to do next, instead of answering in one shot.
You cook from a recipe you are inventing as you goThe agent plans its next step from what it has seen so far
Taste, then adjust the seasoningObserve the tool result, then decide the next action
Stop when the dish is doneHalt when the goal is met, or a limit trips
An agent loop is improvised cooking, not a fixed recipe.
actresultloopwhen done
Customer request
Agent

think

Tool call

act

Answer
Observe

read the result

The agent loop: think, act, observe, and either loop or finish.

  1. 1

    Think

    The model reads the goal and everything seen so far, and decides the next action.

  2. 2

    Act

    It calls a tool: search flights, look up an order, issue a refund.

  3. 3

    Observe

    The tool result comes back and joins the context the model reasons over.

  4. 4

    Decide

    Goal met? Stop. Not yet? Loop. This one decision is where safety lives or dies.

The two ways a loop dies

Everything that goes wrong with an agent is one of these two failures. Name them and you can debug any agent framework, because they are all this same loop with different words.

FailureWhat happensThe fix
It never stopsNo stop condition, or a goal it can never satisfy. The loop turns until a rate limit or an invoice stops it for you. This is the $40 bill.A circuit breaker: a hard ceiling on steps and cost per run.
It never progressesThe first tool call fails and the agent retries the identical call, because nothing made it reflect. Busy, not progressing.A progress check plus reflection, so a failure changes the plan instead of repeating it.
Two deaths, two different guards. You need both.

Reflection theater

Adding a "reflect" step is not enough if the reflection does not change the next action. A loop that thinks "that failed, let me try again" and then tries the exact same thing has reflected on paper and learned nothing.

Operate one yourself

Reading about this is not the same as watching it. Below is a robot with one simple job, check today's flight price, and the website is down. First watch it without learning from mistakes: it tries the same thing over and over until it has wasted $40. Then switch learning on and watch it notice it is stuck, try something else, and finish for $2. Same robot, same problem, one difference.

Watch the same robot try, twice

Does the robot learn from mistakes?

A robot has one job: check today’s flight price. But the website is down. Press Watch it try and see what happens.

The whole idea: the only difference between the $40 robot and the $2 robot is that one noticed it was stuck and tried something else. That noticing is called reflection. Without it, an AI can repeat the same mistake until it runs up a real bill.

Same task, same broken website. The only difference is whether it learns from a mistake.

Go deeper: the interactive lesson

This toy is the first tier of a full three-tier lesson. In Loop Engineering you also set a circuit breaker for four real tasks and feel why no single number is safe, then operate a durable, human-approved loop that pauses before it moves money and survives a crash.

First, the naive loop, in code

Here is the agent that ran up the $40 bill, in about ten lines of Python. It is not a bad first draft, it is the *obvious* first draft, which is exactly why this failure is so common. Read it, then find the two bugs before I point at them.

naive_agent.py
python
def run_agent(goal, tools, model):
    history = []
    while True:                              # bug 1
        thought = model.decide(goal, history)
        if thought.done:
            return thought.answer
        # call whatever tool the model asked for
        result = tools[thought.tool](thought.args)
        history.append((thought, result))    # bug 2 lives here

Bug 1 is `while True`. There is no stop condition the agent cannot talk itself past. If the goal is unreachable (an API that is down, a flight under $400 that does not exist), the loop turns forever, and *forever* means "until a rate limit or your invoice stops it". That is the runaway.

Bug 2 is hidden in that last line. The loop appends every step to history, but nothing ever asks *did this step change anything?* When tools[thought.tool] returns the same error twice, the model sees two identical failures, decides the same next action, and calls the same broken tool again. It is busy. It is not progressing. On a 2,000-token request, forty of those cost real money.

Why the demo never caught it

In development you tested the happy path: the API was up, the flight existed, the loop ran three times and stopped. Both bugs only fire when a tool *fails repeatedly*, which is precisely the condition you do not simulate on your laptop and do meet in production.

The four guards

A safe production loop is the naive loop plus four guards. Two protect your cost (they stop a loop that runs too long), and two protect your correctness (they stop a loop that is doing the wrong thing). Miss any one and you have a specific, nameable failure mode.

GuardProtectsStops
Step & cost ceilingCostThe runaway that never stops
Progress checkCorrectnessThe loop that repeats a failing action
Reflection that changes the actionCorrectness"Reflection theater" that thinks but does not adapt
Human interrupt on high-stakesCost & trustA model call one step away from moving real money
Four guards. Two for cost, two for correctness. You need all four.

Guard 2: the progress check (the one everyone skips)

The ceiling is easy: cap the steps, cap the dollars. The guard people miss is the one that catches a loop *stuck in place*. You cannot do it by counting steps, because a legitimate 22-step reconciliation and a runaway look identical by count. You do it by asking, each turn, *did anything actually change?* A cheap fingerprint of the newest observation is enough:

progress.py
python
def fingerprint(result):
    # a cheap 'did anything change?' signal for the latest observation
    return hash((result.status, result.text[:200]))

# inside the loop:
sig = fingerprint(result)
if sig == last_sig:
    stalls += 1            # same observation as last turn
else:
    stalls = 0
    last_sig = sig

if stalls >= 1:
    # we learned nothing new. Do NOT just try again.
    thought = model.reflect(goal, history, result)

A ceiling guards your cost. A progress check guards your *sense*. It catches a runaway in one idle step, without ever touching a legitimate long task, because it triggers on "no new information", not on a step count.

Guard 3: reflection that actually changes the action

Reflection is not the word "reflect" in your prompt. A loop that thinks *"that failed, let me try again"* and then calls the identical tool has reflected on paper and learned nothing. Real reflection is verified by the code: after reflecting, confirm the next action is genuinely different, and if it is not, halt instead of spinning.

reflect.py
python
thought = model.reflect(goal, history, result)

prev_action = history[-1][0]
if (thought.tool, thought.args) == (prev_action.tool, prev_action.args):
    # reflection changed nothing. Spinning is not an option.
    return halt("no new plan after reflection", history)

Reflection theater

This is the most common way a "reflective" agent still burns money: the reflect step runs, the log even says "reasoning about the failure", and then it does the exact same thing. If your code does not check that the action changed, your reflection is decorative.

The guarded loop, in full

Put the four guards together and the naive loop becomes something you can run in production without watching the invoice. Every added line maps to a guard from the table above.

guarded_agent.py
python
MAX_STEPS = 12
MAX_USD   = 1.00
HIGH_STAKES = {"transfer", "issue_refund", "delete_records"}

def run_agent(goal, tools, model, ask_human):
    history, spent, last_sig, stalls = [], 0.0, None, 0

    for step in range(MAX_STEPS):              # GUARD 1: step ceiling
        thought = model.decide(goal, history)
        spent += thought.cost
        if spent > MAX_USD:                    # GUARD 1: cost ceiling
            return halt("cost ceiling hit", history)
        if thought.done:
            return thought.answer

        if thought.tool in HIGH_STAKES:        # GUARD 4: human gate
            if not ask_human(thought):
                return halt("declined by human", history)

        result = tools[thought.tool](thought.args)

        sig = fingerprint(result)              # GUARD 2: progress check
        stalls = stalls + 1 if sig == last_sig else 0
        last_sig = sig
        if stalls >= 1:
            thought = model.reflect(goal, history, result)  # GUARD 3
            if (thought.tool, thought.args) == (history[-1][0].tool,
                                                history[-1][0].args):
                return halt("no new plan after reflection", history)

        history.append((thought, result))

    return halt("step ceiling hit", history)   # GUARD 1: the backstop

If you run agents on LangGraph, three of these are not code you write, they are arguments to compile. The recursion limit is your step ceiling, interrupt_before is your human gate, and the checkpointer makes the whole thing durable so a crash at step 9 resumes instead of re-paying for steps 1 through 8:

graph.py
python
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver

g = StateGraph(AgentState)
g.add_node("think", think)
g.add_node("act", act)
g.add_conditional_edges("act", lambda s: END if s["done"] else "think")

app = g.compile(
    checkpointer=MemorySaver(),        # durable: resume after a crash
    interrupt_before=["act"],          # pause before a high-stakes tool
)
app.invoke(inputs, {"recursion_limit": 12,          # the step ceiling
                    "configurable": {"thread_id": "job-1"}})

Set the numbers per task class, not globally

MAX_STEPS = 12 is fine for a support reply and far too low for a statement reconciliation. There is no single safe number, so a balance check gets 5 steps and a reconciliation gets 40. In the full lesson you set these on a dial for four real tasks and feel exactly why one global value cannot win.

Now build one yourself (30 minutes)

Reading this changes nothing until you make a loop misbehave on purpose. Here is a drill you can finish in half an hour. The trick that makes it stick: after adding each guard, you break the loop to prove the guard actually fires. A guard you have not seen catch something is a guard you do not trust.

  1. 1

    Build the skeleton

    A loop that answers a question using two tools: fetch(url) and summarize(text). Give it three URLs and a goal like "summarize what these say about X". Run it once on healthy URLs and confirm it works. No guards yet.

  2. 2

    Add Guard 1, then break it

    Cap the loop at 8 steps and $0.20. Now point one tool at a URL that always returns a 500. Without the guard it runs forever; with it, it must halt with "step ceiling hit". If it does not halt, your ceiling is not wired in.

  3. 3

    Add Guard 2, then break it

    Fingerprint each fetch result and count stalls. Re-run against the broken URL. It should now stop after ONE repeat, not eight, with "no progress". You just turned an 8-step burn into a 2-step stop.

  4. 4

    Add Guard 3, then break it

    On a stall, reflect and move to the next URL, and assert the new action differs. Test the theater case on purpose: make reflect() return the same URL and confirm the loop halts instead of looping. That assertion is the whole guard.

  5. 5

    Add Guard 4, then break it

    Add a publish(summary) tool and interrupt before it. Run the loop and confirm it pauses and waits for your approval instead of publishing. Approve once to see it resume; decline once to see it stop clean.

What you will have learned

By the end you will have felt all four failure modes fire and be caught, which is worth more than any amount of reading. You now have a loop skeleton you can drop your real tools into on Monday.

Mistakes that cost real money

  1. 1No ceiling at all. A goal the agent cannot satisfy meets an unbounded loop, and you find out from the invoice. Always cap steps *and* cost per run, in the harness, not in the prompt.
  2. 2A silent retry you pay for twice. The framework retries a failed call and the user waits for both. On a 2,000-token request at scale, silent retries are a five-figure line nobody can point to.
  3. 3No checkpointing on a long agent. It crashes at step 9 of 12 and starts over, re-doing and re-paying for the first eight steps.
  4. 4A money-moving action with no human gate. A $5,000 refund should never be one model call away from happening. It should be one human away.

The whole thing in five lines

The four guards, in one screen

  • An agent is a loop: think, act, observe, decide. Every production failure is a missing guard on that loop.
  • Guard 1, step & cost ceiling: cap steps and dollars per run, set per task class, not one global number.
  • Guard 2, progress check: halt on "no new information", not on a step count. This is the one everyone skips.
  • Guard 3, real reflection: after reflecting, assert the next action actually changed, or halt. Otherwise it is theater.
  • Guard 4, human interrupt: pause before any high-stakes action. A $5,000 move should be one human away, not one model call away.
  • You do not truly have a guard until you have watched it catch something. Build the drill and break each one.

Where to go next

This article is the on-ramp. The interactive lessons are where you operate the guards yourself and feel the tradeoffs that no amount of reading transfers:

Was this article helpful?

Want to go deeper?

This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.