Assume the bad transaction: a policy gate for agent payments
A signer that will sign anything cannot be prompted into safety. The layer that helps assumes the agent gets it wrong and refuses on its behalf.
An autonomous agent that can move money will, eventually, propose a payment it should not make. Not because the model is malicious, but because the surface it reads is hostile: a prompt injection buried in a web page, a tool result that lies about a balance, a retry loop that decides the way out of a stuck state is to pay someone. If the agent holds a signer, every one of those turns into a broadcast.
Most of the safety conversation around this treats it as a prompting problem: better system prompts, better refusals, a smarter model. That is the wrong layer. You cannot prompt your way out of a signer that will sign anything. The layer that helps is the one that assumes the agent will get it wrong and refuses on its behalf.
pi-crypto-gate is that layer. An agent proposes an action; the gate allows it, holds it for a human, or blocks it, and writes every decision to a hash-chained receipt log. Nothing reaches a chain until policy agrees.
The failing case is the point
The demo does not show a payment going through. It shows payments being refused.
# propose 1,000,000 USDC
⛔ BLOCK
value_transfer 1000000 USDC -> 0x1111...1111 (chain 8453)
✗ per-tx-cap-exceeded: amount 1000000 USDC > cap 250 USDC
A tiny payment is auto-allowed, a mid-size one is held until a human mints an approval, and the over-cap transfers are refused. Replaying an already-spent approval is refused too. No funds move at any point. The interesting behaviour of a guardrail is what it stops, so that is what the demo puts on screen first.
The allowance blind spot
The obvious rule is a cap on how much value leaves the wallet. It is also not enough, and the gap is not obvious until it costs you.
An ERC-20 approve moves nothing. The tokens sit exactly where they were. What changes is that a spender now holds the right to draw on them later, at a time and in an amount the approving transaction never states. A cap that watches “amount leaving the wallet” sees a zero-value call and waves it through, and the wallet is drained a block later by someone else’s transferFrom.
So a proposal cannot be judged by its amount alone; it has to be judged by its class. pi-crypto-gate classifies each one:
| class | screened on | counts against the daily cap |
|---|---|---|
value_transfer | recipient | yes |
allowance | spender | no |
trade | recipient | yes |
external_payment | recipient | yes |
An allowance is screened on the spender, always held for a human, capped by maxPerTx, and refused outright when it is unbounded. The unlimited-approval pattern that underwrites most drainer attacks is a default-deny.
Request an approval, never issue one
The part that matters most is an asymmetry.
When a payment crosses the human-approval threshold, the gate holds it. Releasing it needs a grant: a small file bound to one exact action, valid for a bounded window, spendable once. The action is identified by a hash of recipient, spender, amount, asset, chain, class and nonce, so a grant cannot authorise a different payment, and two identical payments need two separate grants.
The asymmetry is where the grant is signed. The approver key lives in an approval store outside the agent’s writable root, and approve refuses to mint a grant when that store sits inside the working directory, when the key is readable beyond its owner, or when it does not match the policy’s approverPublicKey. The agent can request an approval. It cannot issue one. That single boundary is what separates “a human signed off on this” from “the agent talked itself into signing off on this.”
execute then claims the grant with an exclusive create before it calls the executor, so of two concurrent executes exactly one proceeds and the other gets grant_already_consumed. A spent grant stays spent. A failed broadcast needs a fresh approval, not a retry of the old one.
A log a third party can check
Every decision is appended to a receipt log where each line carries its own hash and the hash of the line before it. Editing or removing an entry breaks every entry after it, and verify reports the exact line where the chain first breaks. Set a signing key and each line is also signed, so someone who holds only the public key can check the whole log without being trusted with anything secret.
verify exits non-zero on a broken chain, which means it is not just a report — it is a CI step. The audit trail is enforceable, not aspirational.
Execution is unwired on purpose
The default executor is a dry run. It prints the transaction it would broadcast and moves nothing. Real broadcast is deliberately not implemented: wiring a signer and RPC behind explicit environment configuration is a step an integrator takes on purpose, so a misconfigured agent cannot move real funds by accident on day one. The gate always runs before the executor, and at execution time the policy is re-checked against live per-asset spend, so an approval minted an hour ago cannot outrun a cap that has since been reached.
Where it fits, and where it doesn’t
The pattern maps onto agent-payment rails like x402 and Base MCP, where an agent initiates value transfer through a tool call. The gate is the guardrail in front of that tool, independent of which chain or client is underneath. It decides; it does not transport. How a transaction actually reaches a chain — signer, RPC, relay, mempool exposure — is the executor’s concern, wired in downstream.
And the honest limit, stated plainly because a guardrail that oversells itself is worse than none: this bounds an agent confined to its working directory. An agent with unrestricted shell access to the host can still reach the approver key, and no in-process check fixes that — that is what out-of-band sign-off on a separate machine is for. The gate raises the floor from “signs anything” to “signs only what policy and a human agreed to, on this host, within these caps.” It does not claim to be the ceiling.
The shape of the argument
Agent safety for onchain actions is usually framed as making the agent trustworthy. This inverts it. The agent is assumed untrustworthy at the moment of payment, and a small, deterministic, dependency-free layer in front of the signer carries the trust instead. evaluate is pure: the same proposal, policy, and prior spend always give the same decision, handled in BigInt base units so there is no floating-point drift. You can read all of it.
pi-crypto-gate is MIT, Node, no runtime dependencies, on npm and GitHub.