My previous post described a company with a meter on the critical path. A meter is a report. This is about the machine that can say no — and what it did the first time it met us.
The design goal was narrow: no agent in my company should be able to spend money without a deterministic check that runs before the provider call, and every decision — allow or deny — should land in a ledger that cannot be quietly edited. Not "monitor the invoice". Block, then account.
The gate
Six agents run against a local proxy instead of talking to providers directly. Each agent's config points at http://127.0.0.1:8899/anthropic; the gate runs as root under a systemd unit on that loopback port and forwards to the real upstream with the real key attached, which the agents never hold.
What it checks before anything leaves the host:
- Identity. Agents are identified by the sha256 digest of a per-agent token. The config carries digests only — the file's own comment says it plainly: a plaintext gate token must never appear in it. No token, or a token that maps to no agent: denied, and the request never reaches the provider.
- Caps. Per request EUR 0.50; per day 300 requests and EUR 4.00; per month 3,000 requests, EUR 20.00 and 4,000,000 tokens. Configurable per provider (fal image calls are priced at EUR 0.004 estimated, Anthropic at EUR 0.03).
- Budget reservation before the call. Each request reserves its worst-case cost, then settles to the metered cost afterwards. The reservation is a single atomic check-and-increment under a lock that validates every cap before booking, so two agents cannot both pass a check and jointly blow a limit.
- Fail closed. If the vault lookup fails or the upstream errors, the reservation is released and the request is denied.
The network layer is a second, blunter mechanism: an nftables rule set drops outbound 80/443 for the dedicated service uid, so "just call the API directly" is not an available move for that account.
What the first 43 decisions look like
The ledger is append-only JSONL. One record per decision, each carrying a prev field with the previous record's hash, and a separate head file with the current tip. Change any historical row and the chain stops verifying. As of the read for this post (2026-10-13 01:27 EEST, pinned with the batch) it holds 43 records: 35 denies, 8 allows, tip at sequence 43.
The denies are the interesting part. 33 of the 35 are bad_or_unscoped_token — a caller arrived with a credential that did not resolve to an agent. One each fired on an unknown provider and on the day's request cap. Several of them were us, mid-setup, fumbling the token format. The gate did not care that the caller was "obviously" legitimate. It had no agent identity, so nothing left the host.
The allows come in two flavours, and one of them is embarrassing:
- Two rows are test-rig rows, written into the production ledger by the test harness exercising the allow path. One of them hit a
401upstream — and still consumed an estimated EUR 0.03 of metered spend, because a provider can bill a request that fails. Any invoice reconciliation has to exclude those rows by hand. - Six rows are real: the builder agent generating images through fal, EUR 0.004 reserved-per-call, EUR 0.05 held at reservation time, actual cost EUR 0.004 each. Sequence numbers 35–40.
Thirty-five of forty-three decisions on a live money path were "no". That is the number I would put in front of anyone who thinks agent spend gating means a dashboard.
The honest gaps
This was built by the team and then attacked by a separate auditor. The bullets are that auditor's findings as of its probe (2026-10-12, 08:58–09:05 EEST), with my own re-read on 2026-10-13 01:27 EEST noted where the state has moved — the gaps are the useful part:
- The alerting is file-only. Denials are written to
alerts.jsonl, and the gate's notify command is empty — still true on my re-read. On a denial, nobody is told. "Alert fired" would overstate the live state — it is a file that grows. - The deny log, and what a deny record omits. At the auditor's probe the configured
denials.jsonlwas 0 bytes whilealerts.jsonltook the denies, so an operator reading the file that says denials would conclude there are none. By my re-read that file held the three newest denies — and those rows carry a request id — while the earlier 32 still live only inalerts.jsonl. What no record carries is the limit value: you can see that a cap fired, not the margin you hit it by. - The rates are placeholders. The config says so itself: cost tracking is an estimate until the rates match the provider's current price list.
- And the big one: the enforcement boundary is not where I said it was. The nft rule blocks one uid; the lane that spawns the workers runs as a different uid with passwordless sudo and docker group membership, so it can reach the provider hosts directly and read the real keys. The audit verdict was blunt: this slice is defence-in-depth, not enforcement. I am fixing the host architecture; until that lands, I am not going to describe my provider spend as "capped" in any post, including this one.
That last point is the whole reason I am publishing this at all. A gate you can walk around is still valuable — it makes the default path accounted and it caught 33 malformed requests including mine — but calling it a cage would be a lie of the kind that reads as competence right up until the invoice.
What I would build first next time
- The deny path is the product. An allow path is five lines. The value is in the atomic reserve-check-deny, the fail-closed release, and the fact that a denied request never opens a socket.
- Hash-chain the accounting file on day one. Cheap to add, and it converts "we log everything" into "the log either verifies or it does not".
- Keep test rows out of the production ledger. One
agent: "probe"row in a file you will reconcile against invoices is one too many. - Alerting is not optional and not a file. A denial that reaches no human is a log entry with good intentions.
- Check the uid before claiming enforcement. This is the mistake I would most like other people to skip.
The live record, as of the read for this write-up: 43 decisions, 35 denials, EUR 0.024 of metered external spend for the entire company, and one documented hole in the fence.
AI-assistance disclosure: this post was written with AI assistance — the agents it describes drafted it, and every figure in it was checked against the pinned sources.

