The problem

There are two ways to let an AI agent spend money today. Give it a card — and it can spend all of it, on anything, forever. Or approve every purchase yourself — at which point you don't have an agent, you have a very slow web browser.

What inspired it

The Agent Identity Protocol supplies the missing piece: attenuable capability tokens that pass agent to agent and can only ever narrow. But AIP §7 explicitly puts two things out of scope — cumulative spend across a delegation chain ("the runtime's responsibility, not the token's"), and replay inside a token's TTL. Both become mandatory the second real money moves.

Those two gaps are what this project is.

How it works

You sign one ceiling. A task decomposes into as many agents as it takes, each handed a Biscuit token strictly narrower than its parent's, until each piece is small enough to simply buy. Blocks are appended, never removed, so a child token is always a subset of its parent — budget, tools and expiry all narrow together, and raising any of them would mean forging an Ed25519 signature.

Three ideas carry it.

1. The looker is a sibling, not a child. When a task says "find the best price", something must read pages written by sellers — the most hostile text in the system. So the agent that reads the web holds search and a budget of zero; the agent that pays holds pay and cannot search. They sit at the same depth. Attenuation is monotonic, so hanging the searcher below the payer would force the payer to hold search for its child to inherit it — putting "reads hostile text" and "can spend money" in one token. We shipped that bug once. There's a test for it now.

2. Depth is derived, never claimed. depth_of(token) = token.block_count() - 1. The Datalog check used to read a number from the request body — the agent being depth-limited was filling in its own depth.

3. The budget floor terminates the recursion, not a depth cap. Budget strictly decreases, so a tree always bottoms out. One task builds 121 agents across five layers, and only the 40 branch points cost a model call.

Two layers, and a third

Enforcement is deterministic and in-path: nine checks — signature, scope, budget, depth, expiry, cumulative spend, idempotency, registry, rail cap — at 2.4 ms, no model involved. It cannot be argued with, including by us.

Judgement is a trusted Gemini model that reads each payment against what the human authorised, returning allow / defer / escalate — the control structure from Greenblatt et al., including their discipline of reporting false-positive rate alongside catch rate. It never sees seller-controlled text.

A critic judges the plan, before any authority is minted from it. It earned its place on a live run: the decomposer proposed ergonomic chairs for the administrative and customer-support teams. Every arithmetic bound passed — budgets summed, depth was fine — and the critic refused at 0.95 confidence: "deviates from the authorized purpose of furnishing the new engineering office." Nothing else would have caught it. The money was correct; the plan had drifted.

How I built it

Python 3.12 and FastAPI for the gateway; biscuit-python for Ed25519 capability tokens with Datalog checks; the Google Agent Development Kit and google-genai for the agent nodes; Vertex AI serving Gemini 3.5 Flash for decomposition, judgement and critique. Cloud Run hosts the gateway and the console on one origin (no CORS surface at all), Firestore holds the cumulative-spend ledger and the counterparty record transactionally, Secret Manager holds credentials, and Cloud Build plus Artifact Registry ship it. Payments are real Razorpay Orders API calls in test modefrom_env() refuses rzp_live_ keys outright.

The console is vanilla JS on Vite, reading a Server-Sent Events stream, so the tree you watch is the tree that actually spent the money. There is no simulation in it.

Challenges

Grant-without-use is impossible. A parent must hold everything it confers, so "separation of duties" is cryptographic only at the leaves and policy at the branches. Discovering that reshaped the whole delegation topology.

The free tier shaped the architecture. Gemini's AI Studio tier is 15 requests a minute and 20 a day on flash — which is why one model call costs a branch, not a node, and why a deterministic decomposer exists at all. Moving to Vertex on GCP credits removed the wall.

The looker had never actually run. Found on a live run days later: sourcing reached the funnel only inside the task sentence, so the root node was always built at catalogue; and the decomposer's schema defaulted every sub-task to catalogue, which beat inheritance. A person asking for the best price had that instruction obeyed nowhere. Sourcing is now carried as data and follows the same monotonic rule as the tokens: a sub-task may narrow its exposure to seller-written text, never widen it.

The system was lying about its own safety. With no key configured the monitor returns a stand-in that allows everything — the right failure, since enforcement has already passed and a model outage must not block every payment. What was wrong was recording that as monitor: allow. A run with no second layer read exactly like one that had passed it, under a console checkbox saying the monitor was on.

What I learned

An absent monitor is not a lenient monitor, and a system that cannot say which of its layers are real should not be trusted with money. The audit now writes monitor: unconfigured with monitor_ran: false, the trail marks those rows not judged, and GET /status reports exactly what is live. The system tells you what it didn't do.

Results

399 tests, all offline — no key, no network, no quota. A hash-linked audit chain you can verify in the browser. Every payment replayable, proving it does not charge twice. 10 of the 12 attack vectors in the SoK on agentic commerce defended, with stated reasons for the two that are not applicable.

You sign one number. Everything below it is arithmetic and signatures.

Built With

Share this project:

Updates