Gate a risky AI agent tool call with a permit, and keep a receipt A developer working on Agent Middleware (AMW) outlined a permit-and-receipt pattern for gating risky AI agent tool calls, in which a signed, scoped permit authorizes a call and a signed receipt records its outcome. The design places an idempotency-key replay check before the permit check, charge, and dispatch so that a retried call returns the original result instead of executing a second time, addressing duplicate payouts, database deletions, and emails caused by timeouts. In AMW, a governed call is an MCP tools/call carrying the wallet, permit, and idempotency key in mcpContext. Your agent calls a payout tool. The payout goes through on the other side, but the response never makes it back. The client times out, and the agent does the reasonable thing: it retries. Now the vendor has been paid twice. Swap "payout" for "delete the staging database" or "send the customer email" and the story is the same. The problem is that nothing between the agent and the tool knows the second call is the same action as the first. This post walks through a pattern for that gap, first in general terms you can build yourself, then how Agent Middleware AMW , the project I work on, applies it. The Python below is a simplified sketch I wrote for this post, not AMW's source. The one AMW snippet is the request shape from its README. A permit is a signed, scoped grant that says what an agent may do: which tools, which scopes, how much it may spend, and until when. The gateway checks it before anything costs money or touches the outside world. A receipt is a signed record of what happened to one governed call: which permit it ran under, which tool, what was charged, and the outcome. A refusal under a valid permit gets one too, so "denied" leaves evidence just like "succeeded". You can build this in front of any tool that has side effects. The flow: The ordering matters. The replay check comes before the permit check, the charge, and the dispatch, so a retry never gets a chance to spend anything. This is illustrative code to show the shape of the idea. It is not AMW's implementation, and the store , permits , tools and sign receipt pieces are stand ins for whatever your stack uses. python import hashlib import json class KeyReused Exception : pass class InProgress Exception : pass def request hash tool, args, caller, permit id : canonical = json.dumps {"tool": tool, "args": args, "caller": caller, "permit": permit id}, sort keys=True, separators= ",", ":" , return hashlib.sha256 canonical.encode .hexdigest def governed call store, permits, tools, , key, tool, args, caller, permit id : h = request hash tool, args, caller, permit id Look up the key before anything can spend money or touch the tool. record = store.get caller, key if record is not None: if record.request hash = h: raise KeyReused key same key, different request if record.response is None: raise InProgress key first call still running return record.response original result and receipt New key only. The store must enforce uniqueness on caller, key , so two racing first attempts cannot both get past this line. record = store.insert caller, key, h permit = permits.check permit id, caller=caller, tool=tool raises on deny permit.reserve tools tool .price result = tools tool .call args, idempotency key=key response = {"result": result, "receipt": sign receipt permit, tool, h, result } store.finish record, response return response A real version also needs a signed receipt for denials, a budget release when the call fails before it is sent, and an answer for the case where the tool may have acted but you never heard back. That last one is where most of the difficulty lives. In AMW, agents act under a permit; a same-key retry returns the original receipt, no second call or charge. A governed call is an MCP tools/call with the wallet, permit and idempotency key carried in mcpContext . This is the request shape from the AMW README: curl -sS -X POST "$API URL/mcp/messages" \ -H "X-API-Key: $AGENT KEY" \ -H "Content-Type: application/json" \ -d "{ \"jsonrpc\": \"2.0\", \"id\": \"request-1\", \"method\": \"tools/call\", \"params\": { \"name\": \"$TOOL ID\", \"arguments\": {\"input\": \"hello\"}, \"mcpContext\": { \"wallet id\": \"$WALLET ID\", \"permit id\": \"$PERMIT ID\", \"idempotency key\": \"invoke-1\" } } }" The behavior follows the same steps as the sketch: idempotency key reused , and no new receipt. A retry while the first call is still running gets idempotency in progress . On the permit side, each governed call is checked against a signed permit: which wallet and key may use it, which tools and scopes it covers, a credit budget, an expiry, and optional limits such as per tool call counts, an aggregate value cap, forbidden argument fields, and a required recipient domain. Calls outside the permit are refused before any charge. Every governed call, whether it succeeds, is denied, or fails, produces an Ed25519 signed receipt. The receipt binds the permit, wallet, tool, credits authorized and charged, outcome, ledger entry and audit event, plus SHA-256 hashes of the request and the tool's response. A receipt can be exported as a portable bundle and checked by anyone, with no account and no call back to AMW, using the published public key. delivery uncertain the gateway sent it, but cannot say what the tool did with a signed receipt. AMW never re-sends the call automatically. Someone still has to reconcile it with the tool. I'm Chris, the founder and sole owner of AMW. It is pre-revenue. This post was drafted with help from an AI assistant; the Python sketches are illustrative and are not AMW's source, the curl call is from AMW's README, and every product claim was checked against the AMW code. A sample signed receipt and the public API docs are linked from thisisatest.tech https://www.thisisatest.tech . The pattern above works without AMW. The hard part in my experience is step 1, so here is my question: when your agent retries a tool call, where does its idempotency key come from, and does it survive a process restart?