DEV Community

Cover image for Giving an AI agent a wallet without giving it your keys
Ahmad Shawar
Ahmad Shawar

Posted on Edited on

Giving an AI agent a wallet without giving it your keys

Agents are starting to pay for things. An agent buying API credits, renting a GPU for an hour or paying for a dataset is no longer a thought experiment, and x402 (HTTP 402 Payment Required with on-chain payment) makes it a normal web request.

That leaves the question of how the agent holds money.

The two bad options

Give the agent a private key. It works until a prompt injection, a bug or a leaked environment file. Then the wallet is empty, and there is nothing between the key and the funds.

Approve every payment by hand. Safe, and it removes the reason to use an agent.

What people actually want is what they already do with a company card: a limit per purchase, a monthly budget, a list of places it works, and a phone call for anything unusual.

Put the limits where the agent can't reach them

The limits have to live somewhere the agent, and the machine it runs on, can't change. A smart contract is a good fit: the rules are checked by every node, and changing them needs the owner's key.

Purser is an ERC-4337 smart account built for this. The owner grants each agent a session key with a policy:

  • an allow-list of contracts and functions it may call
  • a cap per payment and a budget per period, per token
  • a validity window

Inside the policy, the agent pays on its own. Over the limit, the payment becomes an approval request, and it runs only if the owner co-signs. One transaction revokes a key or freezes the whole account.

If the agent's key leaks, the damage is bounded: the rest of the current budget, at allowed places, until you revoke it.

The part that was harder than it looks

ERC-4337 splits an operation into validation and execution, and validation is not allowed to read the clock (ERC-7562), because bundlers have to be able to simulate it reliably. A daily budget is all about the clock.

So the checks are split:

  • Validation: signature, validity window, allow-list and per-payment cap. A disallowed call fails here, before it costs anything.
  • Execution: the rolling budget, in executeUserOp, where reading the block time is fine.

Two smaller rules that matter: an owner approval raises the amount but never the scope, so an approved payment still has to be to an allowed place; and a revoke earlier in a bundle still stops a later operation in the same bundle.

Testing money code

Unit tests aren't enough for this. Purser has 41 unit tests through a real EntryPoint, fuzz tests that compare random payment and time sequences against a simple model of the budget, and invariant tests that throw thousands of random calls at the account and check three properties after each one: no spending over budget without approval, no funds moved by a revoked key, no policy change without the owner. Slither runs in CI.

It's still early and not audited, which is why everything below runs on a local chain.

Agents use it through MCP

An MCP server exposes six tools: wallet_status, send_eth, send_token, call_contract, get_request and list_requests. Each payment returns one of three outcomes, written for the model: done with the budget left, waiting for the owner with a request id, or refused with a reason it can act on.

Try it

docker run --rm -it -p 127.0.0.1:8787:8787 ghcr.io/ahmad-codex/purser:demo
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:8787. The container starts a local chain, deploys everything, and plays the agent story while the console updates. Then connect your own agent:

claude mcp add --transport http purser http://localhost:8787/mcp
Enter fullscreen mode Exit fullscreen mode

and ask it to pay the merchant 0.3 ETH.

The roadmap starts with paying x402 endpoints within the budget, a gas budget per key and EIP-7702 support. It's MIT licensed, and there are good first issues open. If you let agents spend money today, I'd like to hear how you limit it.

Top comments (3)

Collapse
 
bloqarl profile image
Carlos (Bloqarl) •

Nice split between validation and execution, a lot of people miss that clock rule.

On the gas line in your security doc: with a leaked key it's not only ETH burned on ops that fail. The session key also signs the gas fields (maxFeePerGas, maxPriorityFeePerGas, preVerificationGas), and the fee goes to whoever bundles the op. So someone holding the key can call handleOps from their own address with one allowed op, a huge priority fee and a big preVerificationGas, and collect most of the account's ETH as the fee. Every cap still passes. A per-op gas cap doesn't need the clock, so it could sit in validation next to the per-payment cap while the per-period budget from #3 waits.

Collapse
 
codearchitect_j profile image
CodeArchitect_J •

The company-card analogy is exactly right: per-purchase cap, period budget, allow-listed places, and a phone call for anything unusual. Putting those limits in a contract the agent's machine can't touch is the key design choice. This week's exchange hack was the same lesson at a bigger scale: valid signatures with unbounded authority are the real risk.

Collapse
 
alexshev profile image
Alex Shev •

The wallet should express intent limits, not just hold a smaller secret. Bind each spend authority to a merchant or API allow-list, amount ceiling, expiry, and receipt requirement; then a compromised agent can at most consume a narrow, auditable budget. Revocation and read-back matter as much as creation.