aere-quantum/agents/x402
Aere Network 2293c1da86 agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet
The agent does not hold the payment key: the wallet holds it for the owner and signs an EIP-3009 authorization only for a payment the
agent wrote into its signed ledger, verified without trusting the agent under the policy the owner pinned and against the ledger heads
the wallet itself saw (a branch is refused with a proof of equivocation, a backdated entry is refused), naming exactly this purchase,
written now, and within the limit judged also against what the wallet itself has signed. The authorization nonce is sha256(entry hash),
so the on-chain payment names the ledger entry. The policy gains an optional `wallet` field. Also: an x402 v2 client, a minimal resource
server, a local facilitator for tests, and verifica-plati.mjs, which proves from outside that a wallet's on-chain payments were allowed
by the agent's policy (with --all-transfers, that no payment left the wallet without a ledger entry).

Tests: wallet 25/25 and payment verifier 10/10 without a network (the verifier on chain responses recorded on testnet 28001), negative
control 21/21; policy 27/27, agents control 26/26. On the public testnet 28001 through its x402 facilitator: 9/9, with the evidence in
agents/x402/dovezi-28001/. Needs ethers (npm install in agents/x402).
2026-09-30 00:01:34 +03:00
..
dovezi-28001 agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
client.mjs agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
control-negativ-wallet.mjs agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
facilitator-local.mjs agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
inregistreaza-rpc.mjs agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
package.json agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
proba-verifica-plati.mjs agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
proba-wallet.mjs agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
proba-x402-testnet.mjs agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
README.md agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
resource-server.mjs agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
verifica-plati.mjs agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00
wallet.mjs agents/x402: an agent wallet that pays over x402 only what the agent's ledger records and its policy allows, run on the public testnet 2026-09-30 00:01:34 +03:00

Agent payments over x402: a wallet that pays only what the agent's policy allows

An AI agent that holds a payment key can spend without limit, whatever its policy says: the policy is only a promise. Here the agent does not hold the payment key. A small service, the agent wallet, holds it for the owner, and signs an x402 payment only when the agent has written that payment into its signed ledger (../agent-ledger.mjs) and the owner's policy allows it. The agent authenticates to the wallet with the ledger entry itself, which only its ML-DSA-65 key can sign.

Node.js 24 and ethers (for EIP-712 and secp256k1: npm install here). Payments follow x402 version 2 (exact scheme, EIP-3009 transferWithAuthorization), with the HTTP headers PAYMENT-REQUIRED, PAYMENT-SIGNATURE and PAYMENT-RESPONSE.

The flow

  1. The agent asks for a resource and gets 402 with the payment requirements.
  2. The agent writes the payment into its ledger: { kind: "payment", from: <wallet>, to: payTo, amount, asset: "eip155:<chain>/erc20:<token>", ref: <hash of the resource and the requirement> }. If its policy refuses (over the limit, a recipient not allowed, approval missing, revoked), it stops here.
  3. The agent sends the requirement and its ledger to the wallet (POST /authorize). The wallet:
    • checks the requirement (its network and asset, a positive integer amount, a timeout it caps at 300 seconds);
    • verifies the whole ledger without trusting the agent, under the policy the owner pinned when starting the wallet: identity, every signature, the chain of hashes, every decision re-run, the revocations the owner gave the wallet, and the heads the wallet itself saw before, with its own clock. A ledger that does not continue what the wallet saw is a branch: it is refused and, when the wallet kept the entry, answered with a proof of equivocation anyone can check;
    • requires the last entry to be a new, allowed payment from this wallet, naming exactly this purchase, written now by the wallet's clock (within 120 seconds);
    • checks the limit a second time against what the wallet itself has signed, so a rewritten ledger, a branch, or a new ledger under a changed policy cannot take out more than the limit allows;
    • signs an EIP-3009 authorization whose nonce is sha256(entry hash): the on-chain payment names the ledger entry, and one entry can pay once, at the wallet and on chain.
  4. The agent retries the resource with PAYMENT-SIGNATURE; the resource server settles through its facilitator and serves.

verifica-plati.mjs then proves from the outside that a wallet's on-chain payments were allowed by the agent's policy: the ledger verifies, each payment names an allowed entry, its transaction emits AuthorizationUsed(wallet, sha256(entry hash)) and the Transfer of that amount, and with --all-transfers every transfer out of the wallet since a block is one of those payments.

Use

node wallet.mjs serve --config wallet.json [--port 8793]      # POST /authorize, POST /revocations, GET /status on 127.0.0.1

wallet.json: { policy, policyHash, network: "eip155:28001", token: { address, name, version }, evmKeyFile, stateFile }. The policy must name the wallet (wallet: <its address>, see definePolicy) and a spending limit in its asset, or the wallet does not start. The EVM key is read from its file and never printed; the state file keeps the heads seen and what was signed, and is written before a signature is returned. The agent side is payWithAgent({ url, ledger, wallet }) in client.mjs; walletOverHttp(url) talks to the service. resource-server.mjs is a minimal x402 seller for tests; facilitator-local.mjs makes the same checks as an x402 facilitator, in memory, for tests without a chain.

What it does not do

  • It is not trustless custody: whoever holds the wallet key (the owner) can pay without the agent. A 2-of-2 contract wallet (the agent and the policy service, ERC-1271, which the testnet token accepts) would remove that trust; it is not built.
  • An authorization that is signed but never settled still counts against the limit until the window passes (conservative).
  • The wallet API must be reached over a trusted channel: an entry intercepted on the way could be submitted by someone else, who would then receive the payment payload (the money still goes to the seller the entry names).
  • It does not check that the seller delivered what was paid for.
  • The wallet keeps every signed payment in its state; nothing prunes it yet.

Tests

Measured on 2026-09-29 (Node.js 24.14.1, ethers 6.16.0):

test result
node proba-wallet.mjs 25/25 without a network, real EIP-712 signatures and ML-DSA-65 keys: five paid purchases through a local facilitator, the sixth refused by the policy; attacks by the holder of the agent key (a forged "allowed" entry over the limit, an entry for another amount, the same entry twice, a branch with its equivocation proof, a new ledger under a changed policy, a backdated entry, two branches sent at the same time), the owner's revocation, human approval, a payment for another requirement and a replayed payment at the seller
node proba-verifica-plati.mjs 10/10 without a network, on the chain responses recorded for the testnet run below
node control-negativ-wallet.mjs 21/21: each guard of the wallet, the client, the seller and the verifier removed in a copy, and the named check must fail
AERE_TESTNET_KEY_FILE=<funded testnet key> node proba-x402-testnet.mjs 9/9 on the public testnet 28001 through its x402 facilitator: a fresh wallet funded with 0.05 tUSD, three purchases of 0.01 paid and settled on chain, the fourth refused by the policy, a replayed payment refused, the balances and the used authorization nonces read on chain, and the evidence file verified VALID (a changed amount, or a payment removed from the file: INVALID)

The testnet run of 2026-09-29 is in dovezi-28001/: the evidence file and the recorded chain responses. Anyone can check it:

node verifica-plati.mjs dovezi-28001/plati-agent-2026-09-29-20-23-16.json --rpc https://testnet-rpc.aere.network --all-transfers

tUSD is the testnet's EIP-3009 token, minted by a faucet and worth nothing. Nothing here has been run on the Aere Network mainnet. Code comments and test names are in Romanian; messages, data and the documentation are in English. No third party has reviewed it.