This is the part that took longest to get right, and the answer is a genuine limitation rather than a clever trick. It is worth stating plainly instead of hiding, because pretending otherwise produces a demo that hangs.
Why project #3's answer doesn't transfer
Project #3's gate pauses, and the mechanism is lovely. The loop stops. The
HTTP request ends. The entire agent — its whole messages array — is written
to Postgres. You close your laptop. A brand-new request, possibly on a
different machine, thaws it out five minutes later and carries on.
That works because the thing being frozen is yours. Your agent does not mind waiting. It isn't waiting; it doesn't exist. It is a row.
A sampling request is not yours.
You cannot write somebody else's server to Postgres. You do not have it. All you have is an open HTTP request that is going to expire whether or not your human has looked at their screen.
Synchronous refusal, asynchronous approval
So the gate is shaped differently on purpose:
| the refusal | immediate, and free |
| the human's answer | a card, minutes later, granting a one-shot allowance |
| when it takes effect | the agent's next attempt |
Note where the grant is checked: after the ceiling, not before. A grant is only ever spent on a call that actually needed it, so it can't be quietly consumed by the next cheap request that happens along.
The refusal costs nothing, and that is the whole argument
🛑 The Kitchen → summarise_week [claude-sonnet-5] 6.0¢ estimated — REFUSED, $0.00 spent tokens on the tab : 0 refused (not spent) : 6.0¢ ← what the ceiling saved you
Your own server asked your own host to spend your money, and your host said no, for free.
The wording is doing real work
This is project #3's deny-branch lesson, one network hop further out — and it cost real money to learn the first time.
Say only "denied" and the server's own model very reasonably retries the identical call. Which gets denied again. And each retry costs the agent another full round trip, because that is how multi-round-trip works. A bad sentence is a bill.
So the refusal text names the price, names the ceiling, and says what would actually help:
REFUSED BY THE HOST'S SPEND GATE. This request is estimated at 6.0¢, which is over the 2.0¢ ceiling configured for the server "kitchen". No model call was made and nothing was charged. Do not retry this request unchanged — either ask for a smaller maxTokens, send less text, or proceed without the model's help and say plainly what you could not do.
Three things in there, each load-bearing: what it cost, what the limit was, and three specific alternatives. A refusal that only refuses turns into a loop.
The demo, in three commands
npm run ledger -- --ceiling kitchen 2 # two cents per request
npm run sampling -- --deep # 🛑 your host tells your own server no
npm run ledger -- --grant kitchen 8 # a human says "yes, this once"
npm run sampling -- --deep # …and now it goes throughCheckpoint
The third command writes a grant. The fourth spends it — and the ledger ends up with a refused row and an allowed row for the same request, which is exactly the history you want when somebody asks later why that 6¢ was spent.