Let an agent pay for itself.
aeron-wallet: a non-custodial wallet that answers 402 on its own, as a CLI and an MCP server — plus how to sign the payment yourself if you would rather not run it.
aeron-wallet is the buyer side of the rail: a non-custodial wallet that holds USDG, reads a 402 challenge, checks it against your budget, signs, and retries the request — without a human in the loop. It ships as a CLI and as an MCP server, and it works against any x402 endpoint on eip155:4663, not only Aeron’s.
First payment
npx -y aeron-wallet address # prints an address, creates a key on first run
# send USDG to that address, then:
npx -y aeron-wallet pay https://inference.aeron.sh/v1/chat/completions \
'{"model":"deepseek/deepseek-chat","messages":[{"role":"user","content":"hi"}]}'You do not need ETH. The facilitator relays the transaction and pays the gas.
| Command | What it does |
|---|---|
address | Print the wallet address. Creates the key if none exists. |
balance | ETH and USDG balances, read from chain. |
pay <url> [json] | Call an x402 endpoint, paying if it answers 402. |
history | The last 10 payments, from the local log. |
session create | Mint a scoped session: hosts, budget, per-call cap, expiry. |
session list / revoke | What each session spent, and killing one. |
mcp | Run as an MCP server over stdio. The default with no arguments. |
Give it to an agent
/plugin marketplace add aeronlabs/aeron-wallet /plugin install aeron-wallet@aeronlabs
{
"mcpServers": {
"aeron-wallet": { "command": "npx", "args": ["-y", "aeron-wallet", "mcp"] }
}
}Four tools: get_address, get_balance, pay, history. An unbound server also gets create_session, list_sessions, revoke_session. Gemini CLI installs it as an extension; VS Code takes code --add-mcp.
Sessions
A session is a scope you can hand to an agent without handing over the wallet: the hosts it may pay, a total budget, a per-call cap, an expiry.
aeron-wallet session create --host inference.aeron.sh --budget 0.25 --ttl 2h # prints a token, once
{
"mcpServers": {
"aeron-wallet": {
"command": "npx", "args": ["-y", "aeron-wallet", "mcp"],
"env": { "AERON_WALLET_SESSION": "<token>" }
}
}
}A bound server has no session tools — an agent that could mint itself a wider session would not be contained by one. It also refuses hosts outside the scope before the request goes out, so an agent talked into paying an attacker’s endpoint never contacts it. Revocation takes effect on the next call, including for an already-running server, because the scope is re-read every time.
Sessions narrow the wallet; they never widen it. The caps below still apply underneath, so a $5 session on a $1/day wallet spends $1 a day.
Caps and configuration
| Variable | Default | Meaning |
|---|---|---|
MAX_PER_CALL_USD | 0.05 | Largest single payment. |
DAILY_CAP_USD | 1 | Total for the current UTC day. Only settled calls count. |
AERON_WALLET_DIR | ~/.aeron/wallet | Where the key lives. |
AERON_WALLET_KEY | unset | Supply the key yourself instead of storing one. |
AERON_WALLET_SESSION | unset | Bind the whole process to one session. |
RPC_URL | rpc.mainnet.chain.robinhood.com | Chain reads. |
The key is generated on your machine and written to ~/.aeron/wallet/key with 0600 permissions, unencrypted, so an agent can sign without a prompt. Keep the balance small — fund it like a prepaid card, not like savings.
In an ephemeral container, $HOME is wiped between runs and the wallet regenerates, stranding whatever USDG was on the old address. Mount a volume for ~/.aeron, set AERON_WALLET_DIR, or pass AERON_WALLET_KEY.
Reading a result
A 4xx from a paid call is three different situations, and they differ in the only way that matters: whether the money left the wallet. The signal is the receipt — a service that settled returns X-PAYMENT-RESPONSE with a transaction hash, and one that did not, does not.
| status | Charged | What happened |
|---|---|---|
settled | yes | The service answered. A settled row with a reason is the one bad case: the money moved and nothing came back. |
rejected | no | HTTP 402. The service refused the payment; the authorization is unspent. |
failed | no | The service errored and declined to charge — usually its own upstream failed. |
Signing it yourself
If you would rather not run the wallet, the payer side is small. Read accepts[0] from the 402, sign the authorization under the token’s EIP-712 domain, and retry with the header.
import { privateKeyToAccount } from 'viem/accounts'
import { randomBytes } from 'node:crypto'
const TYPES = {
TransferWithAuthorization: [
{ name: 'from', type: 'address' }, { name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' }, { name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' },
],
}
const DOMAIN = {
name: 'Global Dollar', version: '1', chainId: 4663,
verifyingContract: '0x5fc5360d0400a0fd4f2af552add042d716f1d168',
}
const account = privateKeyToAccount(process.env.KEY)
const first = await fetch(url, { method: 'POST', body })
const offer = (await first.json()).accepts[0]
const now = Math.floor(Date.now() / 1000)
const authorization = {
from: account.address,
to: offer.payTo,
value: BigInt(offer.maxAmountRequired),
validAfter: BigInt(now - 60), // strictly past
validBefore: BigInt(now + (offer.maxTimeoutSeconds ?? 60) + 540),
nonce: `0x${randomBytes(32).toString('hex')}`,
}
const signature = await account.signTypedData({
domain: DOMAIN, types: TYPES, primaryType: 'TransferWithAuthorization', message: authorization,
})
const header = Buffer.from(JSON.stringify({
x402Version: 1, scheme: 'exact', network: 'eip155:4663',
payload: {
signature,
authorization: Object.fromEntries(
Object.entries(authorization).map(([k, v]) => [k, typeof v === 'bigint' ? String(v) : v]),
),
},
})).toString('base64')
const paid = await fetch(url, { method: 'POST', body, headers: { 'x-payment': header } })Check the offer before you sign it: network, asset, and maxAmountRequired come from the seller, and a wallet that signs whatever it is handed is a wallet that can be drained one small payment at a time. Cap the amount, and pin the host.
Source
aeronlabs/aeron-wallet, MIT, published to npm from a tag by GitHub Actions with provenance attestation — verify with npm audit signatures.
Updated 1 September 2026. Everything on this page is read from the running services; report a drift at github.com/aeronlabs.