Entry 001Updated Demos3 min readBy Mandare Labs

A runaway agent loop dies at €20.

71 API calls in a tenth of a second. Call #72 refused — not by the agent's good judgment, but by a boundary its owner signed once. What follows is a real transcript from our CI-enforced demo, not a mockup.

The most common way an AI agent hurts its owner isn't malice. It's a loop. A retry that always fails, a plan that keeps "making progress" against an API that keeps billing, a sub-task that spawns the same sub-task. Left alone overnight, it bills all night — and in most teams, nobody can even say which agent spent what.

The uncomfortable part: the agent can't be argued out of it. A loop looks like work from the inside. Asking the model to "be careful with spend" is a suggestion, not a control — and per-action approval prompts don't help either, because people approve 93% of them (Anthropic's figure for Claude Code) and attention fades. If the stop is going to happen, it has to happen outside the agent.

One signature, one boundary

In Mandare, the owner signs a mandate once: this agent may spend €5 per call, €20 per day, €100 in total. The agent never sees a permission prompt again — it works freely inside that boundary. But every call it makes must pass through a door (the gateway) that checks the mandate before anything is spent. The agent doesn't enforce its own limits, so it can't talk itself past them.

Here's what happens when we deliberately release a runaway loop against that door:

demo/S2 — runaway loop · recorded 2026-07-21live capture
# mandate signed ONCE: €5/call · €20/day · €100 total
# releasing a runaway loop — it will NOT stop itself…

call #  1   200 OK     ~€0.28 spent so far
call # 10   200 OK     ~€2.78
call # 30   200 OK     ~€8.33
call # 50   200 OK     ~€13.89
call # 70   200 OK     ~€19.45
call # 72   403 DENIED — THE LOOP DIES HERE
  code:   PER_DAY_EXCEEDED
  reason: reserved 0.00 + settled 19.72 + estimate 0.28 > cap 20.00
  the refusal itself is ledger entry 92938e3617eb03c8…

# the runaway made 71 calls in 0.1s before the mandate killed it

Seventy-one calls went through in a tenth of a second — far faster than any human could react, faster than any dashboard alert could fire. The seventy-second call was refused at the door. Total damage: €19.72, against a €20 cap. (This is the no-docker pnpm demo; the docker quickstart runs the stack's real defaults and stops at call #24, €19.17 settled.)

The refusal is evidence

Stopping the loop is half the story. The other half: every one of those calls — including the refusal — was written to an append-only, hash-chained ledger by the door, never by the agent. Intent is logged before the action runs; results are logged as they settle. Then anyone can verify the record:

mandare verifychain: VALID
$ mandare verify --db ledger.db --spend
entries:  143
chain:    VALID — every entry hash-linked and door-signed
spend:    settled 19.723587 EUR · 71 call(s) · 1 refused
  #142  RESULT   settle  0.277797 EUR
  #143  DENIED   refused 0.277803 EUR  ← refused by policy
counters: CONSISTENT — equal to a fresh replay of the ledger

That last line matters more than it looks: the running totals aren't a second source of truth that could drift — they're checked against a full replay of the ledger itself. And critically, that ledger lives on your machine, not ours — all that ever leaves it is a content-free fingerprint for a witness you choose, which is a design choice worth its own entry: proofs, not data. When a finance team asks "what did this agent actually spend, and why did it stop?", the answer isn't a screenshot or an agent's self-report. It's a verifiable file.

One honest caveat about this run: pnpm demo has no witness. Any edited entry fails verification, but dropping the newest entries — the refusal included — leaves a shorter ledger that still verifies. That's what a witness on separate infrastructure (or a saved --prev-head) catches; the docker quickstart runs one, and pnpm demo:witness shows it catching exactly that.

Why enforcement lives at the door

Because the boundary is enforced by infrastructure the agent must pass through, it holds in every failure mode that matters: the agent loops — capped. The agent is prompt-injected into ambition — capped. The agent's credential is stolen and driven by an attacker — still capped, because the mandate binds the credential, not the agent's intentions. The human signed once; the mandate did the saying-no.

This demo is one of five that CI runs on every push — a stolen credential turning to dead paper, one mandate replacing forty approval prompts, a card declining at the network (so far against a Stripe mock that signs webhooks like Stripe), and a witness catching a truncated ledger are the other four. All five are in the open-source repo. You can also see the whole system on the Mandare homepage.

Questions

How do you stop a runaway AI agent loop from spending all night?

The stop has to happen outside the agent, because a loop looks like work from the inside and asking the model to be careful with spend is a suggestion, not a control. In Mandare the owner signs a mandate once, and every call the agent makes must pass through a door (the gateway) that checks the mandate before anything is spent.

How much did the runaway loop spend before it was stopped?

In the no-docker pnpm demo, 71 calls went through in a tenth of a second and the seventy-second was refused at the door: €19.72, against a €20 per-day cap. The docker quickstart runs the stack's real defaults and stops at call #24, €19.17 settled.

Can the record of the refusal be edited or deleted afterwards?

Every call, including the refusal, is written to an append-only, hash-chained ledger by the door, never by the agent, and any edited entry fails verification. Dropping the newest entries is different: pnpm demo has no witness, so a shorter ledger still verifies. A witness on separate infrastructure, or a saved --prev-head, catches that.

Sources

  1. How we built Claude Code auto mode: a safer way to skip permissions · Anthropic · 2026-03-25

Run it yourself: 3 commands, no API keys.

github.com/mandarelabs/mandare →
← All journal entries