Skip to main content
Glama
loveaihq

ada-wallet

by loveaihq
README.md
# ada-agent-wallet

Out-of-process spend control for AI agents that pay with x402. The agent gets MCP tools that can
pay — for an HTTP endpoint, or for a paid tool on another MCP server — and never holds the key.
That sits in a separate daemon, which enforces the spend policy, queues what needs a human, and
writes a hash-chained audit log the agent cannot rewrite to hand itself more budget.

Why a separate process: `@x402/core`'s spend controls and `@x402/mcp`'s `onPaymentRequested` hook —
the two places the official stack puts a limit — both run *inside* the agent. An agent that has
been steered can be steered through them. These limits live where the key lives.

Settles on Cardano, through the official `@x402/cardano` and `@x402/mcp` clients — nothing forked.

**Status: 0.2.9, preprod only.** It has never run on mainnet and has had no external security
review. signerd holds a decrypted mnemonic in memory for as long as it runs — that is what a hot
wallet is — so keep in it only what you would accept losing outright, and set
`MAX_HOT_BALANCE_LOVELACE` before pointing it at real funds. Apache-2.0: provided as is, without
warranty; the risk of running it is yours.

```
agent (Claude / any MCP client)
   └── mcp.ts        wallet_status · x402_fetch(url) · x402_mcp_tools/call(server, tool)  ← no keys
         ├── gatedSigner.ts  implements ClientCardanoSigner, forwards to ↓
         └── batchProxy.ts   the batch-settlement scheme, forwards to ↓
signerd.ts   127.0.0.1 only, bearer token, holds the mnemonic
   ├── policy.ts    per-tx max · rolling-24h max · payee, resource, asset and transfer-method
   │                allowlists · per-hour rate · approval threshold · channel deposits and keys
   ├── replay.ts    rebuilds the 24h spend window at startup, and checks what it rebuilds from
   ├── expiry.ts    which signed payments the chain has moved past, and reading that from Koios or
   │                Blockfrost: the only way a spend's budget comes back
   ├── retry.ts     which provider read failures are worth a second try, and the retrying
   ├── serialize.ts one lock per agent for the cap, one per wallet for signing
   ├── keystore.ts  scrypt + AES-256-GCM, so the mnemonic is not plaintext at rest
   ├── verifyTx.ts  reads back the signed transaction: does it pay who was authorised, only them
   ├── channelTx.ts the same for channel transactions: deposits, top-ups, refunds, exits
   ├── tidy.ts      the UTxO layout channel steps need, the plan to reach it, and the same
   │                read-back for the one transaction that does
   ├── ledger.json  the spend state the cap is computed from, rewritten after every signature
   ├── audit.jsonl  every decision, hash-chained, the checkpoint pointing into it
   ├── tokens.json  optional: a token per agent, so `agentId` is not self-reported
   ├── channels/    batch-settlement: what was signed on each channel (channelStore.ts), and
   │                subbit-x402's channel records, one directory per agent
   └── @x402/cardano toClientCardanoSigner (Koios by default, Blockfrost optional)
       subbit-x402   the batch-settlement channel client (preprod, Blockfrost)
walletctl.ts  status | preflight | pending | approve <id> | deny <id> | audit
              channels | refund | close | end | elapse | recover | tidy
```

`ledger.json` and `audit.jsonl` are deliberately two files: the cap is state, the log is a log, and
conflating them meant `rm audit.jsonl` handed a spent agent its budget back.

What `@x402/core` already has: a per-payment USD cap and an asset allowlist, inside the agent
process — and `@x402/mcp` puts its `onPaymentRequested` approval hook in the same place.
What this adds: key isolation, rolling daily/hourly limits, payee allowlist, human approval, audit trail —
enforced in the process that holds the key, so an agent cannot loosen its own limits.
Masumi's Payment Service (the other Cardano agent-payment stack) has none of these on the buying side.

## Install
```
npm i -g ada-agent-wallet
```
Node 22 or newer (Node 20 reached end of life in April 2026), on Linux, macOS or Windows; CI runs
the tests on all three. Five commands: `ada-signerd`, `ada-wallet-mcp`, `ada-walletctl`, `ada-keystore`,
and `ada-agent-wallet` (the MCP server again, so an agent config line is `npx -y ada-agent-wallet`).
To work on the code instead, clone this repo and use the flow below.

## Run (preprod)
```
npm install
cp policy.example.json policy.json            # edit the limits; amounts are in the asset's smallest unit
export SIGNERD_TOKEN=$(openssl rand -hex 16)
export WALLET_MNEMONIC_FILE=~/.ada-agent-wallet/mnemonic   # 24 words, chmod 600
export CARDANO_NETWORK=cardano:preprod                     # or cardano:preview; Koios, no API key needed
npm run signerd                                            # prints the address → fund it from the preprod faucet
```
The policy file declares the network it was written for, and signerd refuses to start on a mismatch.
A plaintext mnemonic is fine for a faucet wallet; for anything else see "The key" below.

Koios is enough for signerd and, since `@evolution-sdk/evolution` 0.5.14, a whole `exact` 402
round-trip: before it, the facilitator's verify step could not read most UTXOs through Koios (see
"Known behaviour worth expecting"). signerd's `batch-settlement` runs on Koios too, since 0.2.4 on
subbit-x402 0.1.4. The `dev/` drivers that reconcile a run against the chain (`batch`, `batchend`,
`batchexit`, `batchsponsored`, `contention` and `mcpbatch`) read it through Blockfrost, and need
`BLOCKFROST_PROJECT_ID`.

**Where the policy, audit and ledger files live is part of the security model.** The daily cap is
computed from `ledger.json`, rebuilt where needed by replaying `audit.jsonl`, and the limits
themselves are re-read from `policy.json` on every decision. An agent that can write any of the
three raises its own budget without going near the key. Put all of them where the agent's user
cannot write, and run signerd as a different user. signerd prints the absolute paths at startup so
this is checkable rather than assumed.

The example policy's `allowedResources` names the two local dev sellers — `http://127.0.0.1:7401/*` for
HTTP, `http://127.0.0.1:7404/*` for MCP — alongside a placeholder, so the round-trips below work from
a fresh copy; drop both dev entries before the file governs anything real. The list applies to MCP
tool calls too: the resource checked is the MCP server's URL.

Two defaults in `policy.example.json` are deliberately conservative and worth a look before you
widen them: `allowedPayees: ["*"]` accepts any payee, which is only reasonable while the agent is
talking to endpoints you chose; and `allowedAssetTransferMethods: ["default"]` refuses masumi
escrow, whose cost is not only `amount` — it also locks buyer collateral that no field in the
policy can see, up to the SDK's 15 ADA ceiling, and not released until the contract's
`submit_result_time`. Enabling masumi means accepting that charge; bound it with
`MASUMI_MAX_COLLATERAL_LOVELACE`.
MCP registration (Claude Desktop / Code / Cowork):
```json
{ "mcpServers": { "ada-wallet": { "command": "npx", "args": ["tsx", "/path/ada-agent-wallet/src/mcp.ts"],
  "env": { "SIGNERD_TOKEN": "<same token>", "AGENT_ID": "default" } } } }
```
Human approval: `npm run walletctl -- pending` → `npm run walletctl -- approve <id>`.

A queued payment holds the agent's `/sign` request open while the human decides, and that request
is the only place the signed transaction can go. Node's `fetch` stops waiting for a response after
five minutes; when the caller's connection drops, signerd withdraws the request from the queue,
gives the budget back and logs `pending_abandoned`, so an approval that comes after that signs
nothing rather than recording a spend for a transaction nobody will broadcast. Approve within five
minutes, or expect the agent to have to ask again.

## Dev stack (a whole 402 round-trip, locally)
`dev/` runs the seller side so the loop can be closed without an external endpoint. Three shells:

```
npm run dev:facilitator   # 127.0.0.1:7403  verifies + broadcasts. No key, no funds: the buyer's
                          #                 transaction is already signed and pays its own fee.
npm run dev:resource      # 127.0.0.1:7401  the paid endpoint, settles via the facilitator
npm run dev:mcpresource   # 127.0.0.1:7404  the same three prices as paid MCP tools, through
                          #                 @x402/mcp's own createPaymentWrapper
npm run signerd           # 127.0.0.1:7402  the key + policy
```
Then drive it with a real MCP client — `dev/roundtrip.ts` spawns `src/mcp.ts` over stdio and calls
`x402_fetch`, so the path is exactly the agent's:

```
npm run roundtrip           # GET /quote    1.5 tADA, under approvalAbove -> signs unattended
npm run roundtrip approve   # GET /report   4 tADA, queues -> npm run walletctl -- approve <id>
npm run roundtrip deny      # GET /premium  6 tADA, over perTxMax -> denied, nothing signed
npm run roundtrip auto mcp  # the same three modes, bought as MCP tools through x402_mcp_call
npm run balance             # BUYER_ADDRESS / SELLER_ADDRESS balances, to see the faucet land
npm run concurrency         # regression check for the spend-cap race (spends nothing)
npm run walletctl -- preflight   # deployment checks; see "Before mainnet"
npm run integrity                # proves no single deletion resets the spend cap
npm run approvals                # every way out of the approval queue gives the budget back
npm run assets                   # per-asset caps and windows, and the rate they share
npm run identity                 # whether an agent can spend a budget that is not its own
npm run queue                    # every exit from the approval queue that does not sign — no chain,
                                 # no wallet, so this one runs wherever the unit tests do
npm run posix                    # mode bits and signals; Linux only, since Windows can check neither
npm run delivery                 # a caller that leaves while a signature is in flight
npm run context                  # two tool calls at once keep their own reasons in the audit
npm run tidy                     # preprod: `walletctl tidy` on a wallet whose ADA is inside token UTxOs,
                                 # read back from the provider; spends one fee, stops on a tidy wallet
npm run keystore -- create ...   # encrypt the mnemonic at rest
```

`npm run concurrency` starts a throwaway signerd with a cap that fits one payment, fires several
requests at once, and fails unless exactly one is signed. It needs a funded wallet because signing
reads its UTXOs, but nothing it signs is ever handed to a facilitator, so no funds move.

Routes pin `confirmationPolicy: { l1Confirmations: 0 }`. Depth above canonical inclusion needs an
evidence hook that only the Blockfrost provider supplies, so a Koios facilitator advertises
`l1Confirmations 0..0` and the default of 1 would make the 402 unserviceable. Zero still means the
transaction is on chain — `submitTransaction` awaits confirmation — just without extra depth. Set
`BLOCKFROST_PROJECT_ID` (or `L1_CONFIRMATIONS`) to ask for more.

The trap that goes with it: without an evidence hook the settle verdict *is* whether
`submitTransaction`'s `awaitTx` returned in time, and the SDK's provider timeout defaults to 10s —
under preprod's ~20s block. A default-configured Koios facilitator therefore reports `mempool` on
payments that confirm seconds later, and the buyer gets a 402 for a transaction that settled: funds
gone, no goods. `dev/facilitator.ts` gives `awaitTx` 100s and `dev/resource.ts` gives the
facilitator client 115s so the wait is not cut off one level up. Anything talking to mainnet needs
the same budget chain, or Blockfrost.

## batch-settlement (preprod)
An `exact` payment on Cardano is a transaction whose output to the seller must hold about 0.97 ADA
($0.23), plus a fee. Most of what x402 sells costs less than that. x402's `batch-settlement`
scheme is the answer to small prices: the buyer locks funds in a payment channel once, pays each
request with a signed cumulative voucher that stays off chain, and the seller redeems many at a
time. This wallet pays it over [Subbit](https://github.com/kompact-io/subbit-xyz) channels, with
the binding in [subbit-x402](https://github.com/loveaihq/subbit-x402); the design is in
[DESIGN-batch-settlement.md](DESIGN-batch-settlement.md).

**A voucher is money** — the seller can redeem up to the highest one signed, and signing one costs
nothing — so the vouchers are signed where the policy is: signerd runs subbit-x402's client, with
the wallet and the channels' IOU keys, and the agent's process gets `batchProxy.ts`, a scheme with
no keys that forwards the 402 to signerd and the payload back. `x402_fetch` prefers
`batch-settlement` when a seller offers it and the agent's policy allows it.

```json
"allowedSchemes": ["exact", "batch-settlement"],
"allowedProviderKeys": ["<the seller's receiverAuthorizer>"],
"channelDepositMax": { "lovelace": "5000000" },
"channelLockedMax": { "lovelace": "10000000" },
"maxWithdrawDelay": 86400,
"maxVouchersPerHour": 600
```

- **A voucher spends what it adds** to the most signerd has signed on that channel, and that
  increment goes through the same rules as an `exact` payment: payee, resource, `perTxMax`,
  `dailyMax`, `approvalAbove`. A retry, or the re-sign after a corrective 402, adds nothing and
  spends nothing. That record is signerd's own (`CHANNELS_DIR/index.json`): what sellers answer
  moves the channel's count, never what the policy counts.
- **A deposit is locked, not spent**: what the vouchers do not give the seller comes back. It has
  its own limits — `channelDepositMax` per opening or top-up, `channelLockedMax` across the agent's
  open channels, read from the chain — and does not touch `dailyMax`. A token channel's ADA
  reserve counts as locked lovelace, so token channels need a lovelace entry too.
- **The payee is a key.** A channel names the seller's `receiverAuthorizer`, which redeems, and the
  validator does not restrict where a redemption pays — so `allowedPayees` alone binds nothing
  here, and `allowedProviderKeys` must list the key.
- **The close period is the seller's**, up to 30 days, and leaving without the seller keeps the
  money that long; a channel asking for more than `maxWithdrawDelay` is not opened.
- **Every channel transaction is read back before it leaves**, as `exact` ones are: the channel
  step it claims to be, every other output to this wallet, a refund paying the seller no more than
  was signed and not yet redeemed, a fee under 2 ADA and collateral under 5 ADA.

The operator's side: `walletctl channels`, `walletctl refund <channelId> <url>` (the seller
co-signs; signerd builds and signs it but talks to no seller, walletctl carries the messages; a
refund pays the seller what it is still owed in an output of its own, so when that is below an
output's minimum, about 1 ADA, the seller has to claim first and the refund then owes it nothing),
`close`, `end` and `elapse` to leave without the seller, `recover <agentId>` to find an agent's
channels on chain again after losing the records, and `tidy` (below). An exit waits for its block
inside the wallet lock, so payments pause while one lands.

Every channel transaction but the opening puts up collateral from an ADA-only UTxO and keeps about
1 ADA of it back as change. So the wallet needs one ADA-only UTxO of at least about 2 ADA, and ADA
sitting in a UTxO with tokens does not count. A sponsored channel is the exception (below).

`exact` payments undo that. Each one merges every token and the leftover ADA into a single change
output, so after a few token payments all of the wallet's ADA can be inside token-bearing UTxOs, and
a channel step fails `insufficient_funds` although the wallet holds plenty. `walletctl tidy
[--dry-run] [--collateral <ada>]` puts it right: it spends every UTxO into one output with every
token at its min-ADA, one ADA-only output of the collateral size (5 ADA unless `--collateral` says
between 2 and 5), and the rest as ADA-only change. It does nothing when the wallet is already in
that shape, that is when at most one UTxO holds tokens, it holds no more than its min-ADA and 1 ADA,
and an ADA-only UTxO holds the collateral. `--dry-run` reads the wallet and says what it would do,
and builds nothing. It refuses, as `utxo_busy`, while any of the wallet's UTxOs is committed to a
payment or a channel step that has not settled, and while an output of the wallet's own transaction
is not listed yet; it refuses more than 60 UTxOs, and a wallet that lacks the ADA for the layout.
The transaction is read back before it is submitted, as every other is: its inputs are the wallet's
UTxOs and no others, every output goes to the wallet, every token comes out as it went in, and the
fee is under 1 ADA. The fee is the wallet's, and no agent's budget is charged for it. Like the
channel endpoints it is the operator's, and preprod's.

signerd also does it by itself. When a channel opening, a top-up or the operator's refund fails
short of ADA-only funds (the client's `no UTxO to open`, `would leave no ADA-only UTxOs`, a failed
coin selection), signerd reads the wallet, and if a tidy would leave more ADA in ADA-only UTxOs than
there is now, with its fee counted at 1 ADA, it starts one in the background and answers `409
wallet_tidying`: retryable, "the wallet is rearranging its funds; try again in about a minute".
That replaces `insufficient_funds`, which says the wallet lacks the funds and not to ask again. The
agent is not held while the tidy confirms. The provider lists its outputs 20 to 60 seconds after
the block, and a step that comes up short until then gets the same answer. The agent's tools pass it
on as `denied: true, rule: "wallet_tidying", retryable: true, retryAfterSeconds: 60`. The audit has
`auto_tidy`, naming the agent, the step and the shortage, and the tidy's `tidied` record says
`auto: true`. It costs one tidy fee, about 0.2 to 0.3 ADA, paid by the wallet; no agent's budget is
charged. There is at most one automatic tidy per `AUTO_TIDY_MIN_INTERVAL_SECONDS` (600; at least
60), whether or not the last one went through. A shortage inside that time, or one a tidy would not
fix, is `insufficient_funds` as before, and its audit record says why no tidy was tried: the wallet
is already tidy, lacks the ADA for the layout, holds its ADA in ADA-only UTxOs that are merely
small, or is short of the token itself. An `exact` payment never starts a tidy. `AUTO_TIDY=0` turns
it off, and then only `walletctl tidy` does it.

**Sponsored channels (subbit-x402 0.2.1).** A seller may pay for a token channel. Its 402 then
carries a fee-sponsor offer, one of the seller's ADA-only UTxOs. The channel client opens, tops up
and refunds on that offer, so a wallet that holds only a stablecoin can pay (subbit-x402's
SPONSORSHIP.md). The seller's ADA pays the fees, the channel's reserve and the collateral. The
wallet's own ADA only passes through. signerd checks a sponsored transaction as it checks the
others, and more:
- signerd reads the offer from the chain itself. A step whose offer is not, on chain, an ADA-only
  UTxO at the offer's address with the offer's lovelace, and not this wallet's, is refused
  (`sponsor_offer`).
- What the channel keeps, what the seller gets and the fee must all come out of the offer and the
  channel. So this wallet's ADA comes back to it in full, and the offer is the only collateral.
- The reserve is the seller's. The policy does not count it, so a sponsored token channel needs no
  lovelace entry in `channelDepositMax` or `channelLockedMax`, and the refund pays the reserve back
  to the seller. signerd records the channel as `reserveFrom: "seller"`, and pays a reserve to the
  seller only on a channel recorded that way. `recover` finds it out again from the chain
  (subbit-x402 0.2.2): the client walks a token channel back to its opening, and one whose opening
  took none of the wallet's ADA was opened on an offer.
- Leaving without the seller (`close`, `end`, `elapse`) still needs ADA of the wallet's own.

A walkthrough, from the seller to the refund, is in
[`docs/usdm-only-agent.md`](docs/usdm-only-agent.md) (a draft).

A top-up is sized for `BATCH_DEPOSIT_REQUESTS` requests at the price that ran short. When the
wallet cannot fund that much, it tops up what it can, keeping back the fee and such a UTxO for the
refund. Failing that, it tops up at least what this request is short of. A top-up that would leave
nothing to put up as the refund's collateral is refused as `insufficient_funds`, or as
`wallet_tidying` when signerd tidies the wallet in answer. Right after a
channel transaction of its own, the client waits, for up to a minute, for Blockfrost to list that
transaction's change before it builds again.

Limits: preprod only, and Subbit's validator is alpha software. The channel client reads the chain
as the `exact` signer does: through Blockfrost with `BLOCKFROST_PROJECT_ID`, and through Koios,
which needs no key, without it. Koios' preprod index has trailed the chain by over two minutes, so
there an opening or top-up can take a minute or more to confirm.

`x402_mcp_call` pays paid MCP tools the same way, through the same proxy and on the same channels.
A seller's HTTP routes and MCP tools share one channel when they name the same provider.

A paid answer lost in transit is given again to its retry, over MCP as over HTTP, when the seller
runs subbit-x402 0.1.2 or later. The retry carries the same voucher, signerd re-signs it without
spending anything (`voucher_resigned`), and the seller hands back the answer it kept. Over MCP it
keeps a tool's result only when `@x402/mcp` can give it back unchanged: one text block, or
structured content with its JSON as that block. A retry after any other result is charged again,
one voucher's worth.

`npm run batchseller` is the preprod seller, with its MCP tools on :7414. `npm run batch`,
`batchexit`, `batchend`, `mcpbatch`, `batchsponsored` and `autotidy` are the round trips against
it; the results are under "Verified".

## Verified
- `npm test`: 207 unit tests over the policy engine, the startup replay and the release of expired
  spends, the locks, the keystore, the network table, the signed-transaction and
  channel-transaction checks (sponsored steps included), the channel index, the batch-settlement
  proxy, the settlement receipt, the build retry, the known assets, the tidy plan and when to tidy
  by itself. None needs a chain. `npm run typecheck` covers `dev/` and `test/` too, which `tsx` runs without checking.
- CI (`.github/workflows/ci.yml`) runs both on Linux, macOS and Windows, each on Node 22 and 24,
  and `npm run posix` (real mode bits, SIGTERM draining the approval queue) on Linux and macOS.
  All six passed on 2026-09-29. That is the only macOS this has run on: nothing here has been
  driven on a Mac by hand, and no preprod round trip has run there.
- `npm run batch` on preprod (2026-09-25), a real MCP client paying `dev/batchseller.ts` through
  `x402_fetch`, 0.1 tADA a request: the first purchase opened a channel in 38 s (deposit 2.732620
  tADA, reserve included), the next nine were vouchers at 33–39 ms each; a response the seller
  dropped was retried with the same voucher and spent nothing twice (`voucher_resigned`); a 0.2
  tADA purchase over `approvalAbove` queued, was approved with the operator's token, and paid;
  the channel was topped up twice as it ran short; the 25th voucher was refused by `dailyMax`; a
  seller naming a provider key outside `allowedProviderKeys` was refused and got no channel; and
  `walletctl refund` closed the channel with the seller co-signing. 24 vouchers for 2.500000 tADA
  in the audit log, in the ledger and in signerd's channel index alike; on chain the seller
  received exactly 2.500000 and the wallet lost exactly that plus the four transactions' fees,
  0.923814 (opening `1ea7adcb…` 0.178173, top-ups `4b373326…` and `38170b84…` 0.253535 and
  0.253533, refund `13808a96…` 0.238573).
- `npm run mcpbatch` on preprod (2026-09-26, subbit-x402 0.1.3): paid MCP tools, bought through
  `x402_mcp_call`, at prices no Cardano output could carry.
  - **The calls.** One channel paid 107 calls. Over MCP there were 100 `quote` at 0.01 tADA,
    5 `report` at 0.05 tADA and one `digest` at 0.02 tADA; over HTTP, one `/data` at 0.1 tADA from
    the same seller.
  - **Speed.** The first call opened the channel in 23.5 s. The other calls were vouchers at
    37–65 ms each, median 46 ms, except the report that topped the channel up (53 s).
  - **A top-up the wallet could not fund in full.** With the opening in, the wallet held 5.758573
    tADA in ADA-only UTxOs. That is more than the 5 tADA the top-up asked for, but not enough for
    its fee and a change output as well. The index did not list the opening's change yet, so the
    client waited for it. It then topped up 3.258573 tADA, all it could, keeping back an ADA-only
    UTxO of 2.244289 for the refund's collateral.
  - **A lost answer.** The seller charged the `digest` and dropped its answer. The retry carried
    the same voucher, which signerd re-signed without spending anything (`voucher_resigned`), and
    got the lost call's answer back: the tool did not run again.
  - **Settlement.** The seller claimed all 1.370000 tADA in one transaction into its own wallet.
    It had to claim before the refund, since a refund cannot pay an amount below an output's
    minimum. `walletctl refund` then returned the rest, 4.388648 tADA, owing it nothing.
  - **Reconciliation.** 107 vouchers for 1.370000 tADA in the audit log, the ledger and signerd's
    channel index alike. On chain, the seller gained exactly 1.370000 less its claim's fee, and the
    wallet lost exactly that plus its own fees. That is four transactions and 0.922743 tADA, or
    0.008623 per call:
    - opening `1d0cb8cc…` 0.176589, top-up `d417da2a…` 0.255711, refund `a1010bac…` 0.232545;
    - claim `b834de67…`, 0.257898.
- `npm run mcpbatch` again, on Koios alone (2026-09-26): signerd and the seller had no Blockfrost
  key. Only the demo's own checks used one, so they do not rest on what they check.
  - **Every step passed**, on the same 107 calls as the run above: the opening, the fallback
    top-up (0.674183 tADA, with the validator evaluated through Koios' Ogmios endpoint), the lost
    answer's retry, the seller's claim, and the refund.
  - **Reconciled to the lovelace.** Fees were 0.915222 tADA over four transactions: opening
    `8c8e4807…`, top-up `f6302be7…`, claim `40248f9e…`, refund `afddfdf7…`.
  - **Slower.** The opening took 52.6 s, against 17–33 s on Blockfrost. Koios' preprod index has
    trailed the chain by over two minutes.
  - **A fault, found by an earlier attempt, and fixed in subbit-x402.** That attempt found that the
    seller's facilitator checked a `settlement_pending` retry again, and refused it when its
    transaction had landed in between.
- `npm run contention` on preprod (2026-09-26): an `exact` payment and a channel transaction never
  go out spending the same UTxO while the first is unsettled.
  - **`exact` first.** A 1.5 tADA purchase was built on the wallet's first-listed UTxO, its
    largest ADA-only one. A channel asked to open meanwhile could not use that UTxO, and the
    wallet could not fund the opening without it, so it was refused as `insufficient_funds`. It
    opened from other UTxOs once the purchase was on chain.
  - **Channel first.** While a top-up was in flight, a purchase was built on the wallet's token
    UTxO, which holds only its min-UTxO of ADA, so the SDK added the largest ADA-only UTxO: the
    top-up's. signerd refused it as `utxo_busy` and audited `input_in_flight`, naming that input.
    Asked again once the top-up was on chain, it went through.
  - **On chain.** All four transactions handed out landed, and no two share an input: purchases
    `708aa478…` and `4b6c31bd…`, opening `60f4363b…`, top-up `a408c7a2…`. Buyer and seller
    reconciled to the lovelace across the run's eight transactions. Those count the layout before
    it, the seller's claim, the refund, and 3 tADA sent to the buyer for the refund's collateral.
  - **What it cost the wallet's ADA.** An `exact` payment's change keeps the wallet's tokens and
    its leftover ADA in one output. ADA held with tokens counts neither for channels nor as
    collateral, so after the two purchases the wallet held no ADA-only UTxO, and its refund had to
    wait for more.
- `npm run batchsponsored` on preprod (2026-09-28, subbit-x402 0.2.1): a token channel the seller
  paid for, through signerd, from a wallet holding only tUSDM and the 1.176630 tADA that came with
  it. The seller ran `dev/batchseller.ts` with `BATCH_SELLER_SPONSOR_ACCOUNT=9`. The run made 25
  purchases at 0.001 tUSDM, with `BATCH_DEPOSIT_REQUESTS=10` and a policy with no lovelace entry
  at all.
  - The opening `8c4f3373…` (fee 0.190097) and the top-ups `440e17e5…` and `951dc64d…` (0.266094
    each) each used another of the seller's offers, named in the audit as `sponsoredBy`. signerd
    recorded the channel as `reserveFrom: "seller"`.
  - The seller claimed (`4dc5c0eb…`, 0.268721). Then `walletctl refund` (`6053f5a2…`, 0.244491) put
    up the seller's offer as its collateral, and did not spend it.
  - Reconciled on chain across the five: the wallet's ADA did not move, to the lovelace, and its
    tUSDM went down exactly the 0.025 the vouchers signed. The seller, across `payTo` and its
    sponsor key, got +0.025 tUSDM and paid −1.235497 tADA, the five fees exactly, so its reserve
    came back.
  - The first attempt stopped at the second top-up. Two seconds after the first top-up's block,
    the SDK's script evaluation through Blockfrost failed ("Blockfrost evaluateTx failed"), and
    the agent got `batch_failed`, with nothing signed or sent. Resumed 22 minutes later on the same
    channel, that top-up passed. Why it failed is not established.
- `npm run batchsponsored` again (2026-09-28, 0.2.6 on subbit-x402 0.2.2), in two phases with
  the wallet's records lost in between (`BATCHSPONSORED_PHASE=pay`, then `recover`).
  - On Blockfrost, signerd paid 25 purchases: the opening `1342b697…`, the top-ups `eb562bb1…` and
    `1341473d…` (in blocks 5228801 and 5228804), and then the seller claimed (`f3d51a24…`).
  - signerd was stopped, its channels directory deleted, and started again with no Blockfrost
    key, so it read the chain through Koios. `walletctl recover` found the channel with its
    reserve the seller's, and `walletctl refund` was sponsored (`470822ba…`).
  - The wallet's ADA did not move, to the lovelace, and its tUSDM went down the 0.025 the vouchers
    signed. The seller paid the five fees, 1.235908 tADA.
- `npm run expiry` on preprod (2026-09-29, Blockfrost, the release margin at its 60 s floor).
  Payment A (`34c64379…`, 1.5 tADA, 60 s TTL) was signed and never submitted; payment B
  (`f1bfe56b…`, 1.6 tADA, 180 s TTL) was submitted straight to the provider and landed. A's budget
  came back 183 s into the run, once the tip was past its TTL plus the margin, with one
  `spend_released` naming its transaction, agent, asset, amount and TTL slot. B stayed counted
  through its own TTL and margin. After a restart the ledger still held B and not A, and the
  checkpoint kept B's transaction and TTL slot. The first attempt had not got that far: the public
  test wallet's only UTxO held its tokens and 4.7 tADA, too little to pay 1.5 tADA and still leave
  its tokens their min-ADA in the change, which is what the next entry is for.
  - Again through Koios alone (no Blockfrost key), on `@evolution-sdk/evolution` 0.5.15, whose
    Koios decoding was rewritten: A (`2518cb91…`) came back 191 s into the run on Koios's
    `/tx_status`, B (`93255b7e…`, landed 59 s after its submit) stayed counted past its TTL and
    margin and across the restart.
- `npm run tidy` on preprod (2026-09-29). The wallet as the provider listed it: 3 UTxOs, one of
  them holding every token and 15.82 of its 27.02 tADA, since B's change in the entry above had
  folded them together. `tidy` (`79b7b679…`, fee 0.200921) left the tokens in one UTxO at their
  min-ADA (3.59023), one ADA-only UTxO of exactly 5 tADA for collateral, and the rest ADA-only;
  every token came through in the same amount, and the wallet's ADA went down by the fee and
  nothing else. A second `tidy` straight after was refused `utxo_busy`, a dry run once the provider
  listed the outputs said the wallet was already tidy, and an agent's token was refused
  `operator_only`.
- `npm run autotidy` on preprod (2026-09-29, Blockfrost), through a real MCP client. The script
  put the wallet out of order itself (`e84e5c63…`): every token and 19.25 of its 21.16 tADA in one
  UTxO, 1.90 tADA ADA-only. The agent's purchase could not fund an opening; signerd answered
  `wallet_tidying` within 2 s, retryable after 60 s, and started one tidy in the background
  (`123d865d…`, `auto: true`); a second ask straight after was told to wait and started none. Once
  the tidy was listed (21 s), the same purchase opened the channel (`1ef92388…`) and paid by
  voucher. Put out of order again two minutes later, the top-up's shortage was
  `insufficient_funds`, with "tidied 2 minutes ago; at most one automatic tidy every 10 minutes"
  in its audit record; a tidy by hand (`b9ad35ba…`) let the same purchase top the channel up
  (`336d32f7…`). After the refund (`ecf439de…`) the seller had +1.3 tADA, what the vouchers
  signed, and the wallet −2.927010: that and the seven transactions' fees, the automatic tidy's
  0.199337 among them, to the lovelace.
- `npm run batch` again on `@evolution-sdk/evolution` 0.5.15 with subbit-x402 0.2.3 (2026-09-29,
  Blockfrost): 22 purchases on one channel (vouchers 27–38 ms after the opening's first), the lost
  response and its re-sign, the approval, the `dailyMax` refusal with a top-up on the way, the
  refused provider key, and `walletctl refund`. Four transactions (`5119e404…`, `8c7c1f74…`,
  `5b65a64b…`, `4b9cb79f…`); the seller +2.5 tADA, what the vouchers signed, and the wallet −3.667211,
  that and the fees (1.167211), to the lovelace.
- `npm run batchexit` on preprod (2026-09-25), the way out without the seller: three purchases,
  `walletctl close` (`95f5f833…`), then signerd restarted with its channel directory deleted;
  `walletctl recover` found the closed channel on chain with its IOU key derived again, and
  `walletctl elapse` (`262b5b15…`) took everything back. The seller never settled, so it received
  nothing, and the wallet was down exactly the three fees, 0.751551 tADA; the three vouchers stay
  spent in the ledger, as any signed payment does.
- the batch-settlement runs again on `@evolution-sdk/evolution` 0.5.14 (2026-09-25), with
  subbit-x402 0.1.1: `npm run batch` passed with the same reconciliation to the lovelace, and
  `npm run batchend` too. Each first found a race that 0.5.13's runs had missed by timing: a retry
  16 s after a top-up's block built a second top-up of the output the first had spent (the client
  now waits for the index, and the retry in the passing run came 13 s after its block); and an
  `elapse` straight after a close was let through by a check that read the chain separately from
  the elapse itself, which then waited out `elapse_at` inside the wallet lock (the client now
  decides on the one read, and refuses). On Koios alone, `roundtrip deny` and `roundtrip auto`
  passed (`3bb30455…`, 31.9 s).
- `npm run batchend` on preprod (2026-09-25), a token channel and the seller's side of an exit:
  five purchases priced at 0.001 tUSDM opened a tUSDM channel through signerd (`6829d63d…`, 45 s,
  then 36–53 ms a voucher); `walletctl close` (`b7dbfbba…`); an `elapse` straight after was
  refused as `not_yet` rather than waited out inside the wallet lock; the seller's watcher settled
  the latest voucher 20 s after it saw the close (`4c76972b…`); `walletctl end` (`71d076bc…`) took
  the rest back. The seller received exactly 0.005 tUSDM and paid only its settle fee (0.275504
  tADA); the wallet gave exactly 0.005 tUSDM and its ADA was down only its three fees, 0.687401 —
  the channel's ADA reserve came back.
- the modules that say "no chain, no keys, no I/O" are checked to import nothing that would make
  that false, and to still say it — the first run of that check found one that had stopped
- signerd asks the socket what address it bound and refuses anything but the loopback, so the one
  line that decides whether the wallet is on the network is not taken on trust
- every signature is read back before it is handed over: an output pays the authorised payee at
  least the authorised amount, the nonce is spent, and nothing else is paid at all
- `npm run integrity`: no single deletion resets the spend cap, and restarting does not re-count
  what the checkpoint already holds
- `npm run concurrency`: the cap holds under simultaneous requests, and two agents sharing one
  wallet contend for its UTXO without either being handed a transaction that cannot settle
- `npm run approvals`: approved, denied and timed out all give the held budget back, two queued
  payments cannot promise the same budget twice, and a limit tightened while one waits is applied
  to it rather than bypassed by the approval
- `npm run assets`: caps and 24h windows are per asset, the hourly rate is shared across them, and
  an asset the wallet does not hold is refused as `insufficient_funds` rather than as a fault.
  Not covered: an actual native-asset settlement, which needs a wallet holding one.
- `npm run identity`: with `AGENT_TOKENS_FILE`, an agent cannot sign as another or read another's
  budget, and the operator token cannot sign at all. Without it, the same check demonstrates that
  it can.
- `npm run queue`: every exit from the approval queue that does not sign — denied, timed out, the
  caller leaving, the policy tightening underneath it, and a SIGKILL'd run's orphan closed at the
  next start — each giving the held budget back. No chain and no wallet, so it runs in CI
- `npm run posix`: on Linux, where the mode-bit checks and the shutdown signal can actually run.
  SIGTERM drains the queue and answers whoever was waiting; a group-writable policy warns on
  preprod and refuses to start on mainnet; malformed, oversize and wrong-chain requests are 4xx
  without writing to the log; a torn append is repaired and an edited record refuses to start
- `npm run delivery`: a caller that leaves while a payment ahead of it is signing takes its request
  with it, and a signature that lands after its one recipient has gone is recorded as
  `signed_undelivered` rather than counted silently
- `npm run context`: two `x402_fetch` calls in flight at once each keep their own reason all the
  way into the audit, which is what the `AsyncLocalStorage` in mcp.ts exists for
- mcp: tool listing and `wallet_status` through a real MCP client
- `npm run mcptools`: the MCP-side tools register, and the payment client `src/mcp.ts` builds is
  accepted by `@x402/mcp`'s `wrapMCPClientWithPayment`. Types agreeing is not the hand-off working,
  so it is run rather than assumed.
- `npm run roundtrip deny mcp`: a paid MCP tool over perTxMax, bought through `x402_mcp_call` from a
  seller built on `@x402/mcp`'s own `createPaymentWrapper`, comes back `per_tx_max` in 0.1s with
  nothing signed.
- **both tools used to report a payment that did not settle as paid.** `x402_fetch` read "paid" off
  whether a `PAYMENT-RESPONSE` header existed, and core sends one with `success: false` on every
  settle failure; `x402_mcp_call` read it off `paymentMade`, which means sent. Both now read the
  settlement itself (`src/receipt.ts`, tested against core's real header encoding), carry the
  transaction as a receipt, and say `unsettled` when signerd signed something that did not settle —
  checked on preprod against a real facilitator rejection on both transports.
- **a paid MCP tool call, settled on preprod, 2026-09-16** — `x402_mcp_call` bought `quote` from
  `dev/mcpresource.ts` and came back `paid: true` with its transaction in 33s. Facilitator
  `verify isValid:true`, then `settle success:true status:"confirmed" confirmations:1`, transaction
  `15f100abe211a43f711371c4854cc43aed18978d0ffed8630c6c17daeae9642b` in block 5183279. Not taken on
  the facilitator's word: read back from Blockfrost, it pays the seller exactly 1.5 tADA, returns
  the change to the buyer, pays nobody else, and spends only the buyer's inputs. `signed` in
  `audit.jsonl` with the reason tagged `[mcp tool quote]`, and the chain replays clean through
  `replayAudit`. The seller is `@x402/mcp`'s own `createPaymentWrapper`, so this is the wallet
  against the official stack on both sides of the call.
- **the approval queue over MCP, settled on preprod, 2026-09-21** — `x402_mcp_call report` (4 tADA,
  over `approvalAbove`) parked as `pending` carrying the agent's reason tagged `[mcp tool report]`,
  `walletctl approve` released it, and the call came back `paid: true` in 25.2s. `pending` →
  `signed` → `approved` in `audit.jsonl`, transaction
  `adb52eb1cdb6878fc72943894ee95477be992ccad49cd8b418bef0b4eb606285` in block 5202188, read back
  from Blockfrost: exactly 4 tADA to the seller and nothing to anyone else. The nonce it spent was
  an output of `01b6275f8f…`, a Plutus transaction — the case Koios could not look up before
  evolution-sdk#544, so this one ran on Blockfrost.
- **the whole matrix on `@x402/*` 2.27.0, preprod, 2026-09-23** — 2.27.0 shipped on 2026-09-22 and
  a `^2.26.0` range floats onto it, so what someone installs today is not the build the round-trips
  had been run against. All six combinations of {`deny`, `auto`, `approve`} x {HTTP, MCP} pass on it.
  `deny` denies on `per_tx_max` and broadcasts nothing on either transport; the payments read back
  from Blockfrost as exactly 1.5, 1.5, 4 and 4 tADA to the seller, no output to a third address and
  every input the buyer's — `b99226d5…` (block 5209209), `38046624…` (5209211), `ff9705f4…`
  (5209214), and for approve over MCP `a92857ee…` (5209223). That last one is a re-run: on the
  first pass the harness approved within a second of the entry appearing, so `roundtrip.ts`'s own
  once-a-second watch on `/pending` never saw the queue non-empty and failed the run — the payment
  itself had settled correctly as `5a59f437…` (5209217), and the audit recorded `pending` →
  `signed` → `approved` for it. Both harnesses polling on the same interval is the whole of it.
  `replayAudit` clean afterwards, and `dailyRemaining` and `paymentsLastHour` match what was spent.
- **off `vendor/` and onto the published packages, 2026-09-21** — `@x402/cardano` reached npm as
  2.26.0 on 2026-09-18, so the vendored tarballs, their checksums and the second copy of
  `@x402/core` are gone, and with them the casts that let one copy's client reach the other's API.
  Re-run on preprod against the published build: `deny` and `auto` on both transports, `auto`
  settling as `3eb923952376ce5299f8cba6a70e3979fca92d3bd6ec82a738c5f4204d8f5344` over MCP and
  `1061f314b2ea23e906e3293a9f0bade743c2dd5bedd43b28dd659072e96d2319` over HTTP, each read back from
  Blockfrost as exactly 1.5 tADA to the seller and nothing to anyone else.
- the same run bought `/quote` over HTTP after the receipt change: `status 200, paid: true`,
  transaction `ba269ae6f25b2ba505d3da251bbed9904f10466db05ed87c163edec66145eb3e` in block 5183281,
  checked the same way. The fix that stopped failures reading as paid did not stop successes.
- one MCP payment before that settled on Koios at `confirmations:0` (`37120eda…`, block 5183270),
  between runs where Koios verify failed on the same wallet. Koios is intermittent for this, not
  unusable; Blockfrost is what gets past it every time, and the only way to ask for depth.
- deny path, end to end: MCP `x402_fetch` → 402 → gated signer → signerd → `per_tx_max` →
  structured verdict back at the tool, `denied` in `audit.jsonl`
- **a whole 402 round-trip on preprod, 2026-09-15** — `x402_fetch GET /quote` returned
  `status 200, paid true` with the real body in 72s. Facilitator `verify isValid:true`, then
  `settle success:true status:"confirmed"`, transaction
  `6d94fdc0617a8e58ca23402826f36767bab9a00a77dfe02de851d5828428e7e0` on preprod; `signed` in
  `audit.jsonl` with the agent's stated reason and the UTXO it spent as nonce; 1.5 tADA landed at
  the seller address. The first real signature and the facilitator settlement are no longer unrun.
- **the same round-trip with a human in it, 2026-09-16** — `x402_fetch GET /report` at 4 tADA is
  over `approvalAbove`, so it parked in the queue and held the agent's request open while
  `walletctl approve 4df1a492` was run from another shell; `status 200, paid true` came back 179s
  later, the audit reading `pending` → `signed` → `approved`. Over the same session the buyer went
  from 169.638625 to 163.780519 tADA and the seller from nothing to 5.5 in two UTXOs, which is the
  1.5 and the 4 arriving. On Blockfrost: this cannot be done on Koios, see below.
- **approve → sign, on chain** — a 4 tADA route parked in the queue as `pending` with the agent's
  reason and the threshold that caught it, `walletctl approve` released it, and the same call
  returned `200 paid true` in 48.7s. Transaction
  `020af86ddcd581e3379305d832b5da957b877fb0edca97dab1ab3457ce93a72b`; `pending` → `signed` →
  `approved` all in `audit.jsonl`.
- ledger accounting against the real chain: three payments (1.5 + 1.5 + 4 tADA) left
  `dailyRemaining 13000000` of a 20 tADA cap and `paymentsLastHour 3`, and 7 tADA arrived at the
  seller address in three UTXOs.
- the spend cap holds under concurrency: four simultaneous requests against a cap that fits one
  produce one `signed` and three `daily_max` denials. Before the agent lock the same probe signed
  every one of them, on one shared nonce.
- on `subbit-x402@0.3.0`, which opens channels at Subbit's fixed validator (`6d877463…`), `batch`
  and `batchexit` passed again (2026-10-07): both channels sat at that validator, and `recover`
  found channels at the build before it as well. Channels opened at that earlier build keep
  working, and a record that names no validator is read as one of them.
- what the registry serves is what was built: `ada-agent-wallet@0.2.9` on npm is byte for byte the
  tarball checked before publishing (24 files, shasum `478a9fca…`, the one `npm publish` printed),
  and says Node 22 or newer. Its `subbit-x402@0.3.0` is byte for byte the tarball the wallet was
  tested against (shasum `3388ca55…`). Both pin the SDK at 0.5.15, so an install holds one copy of
  it, and neither needs `typescript` or `tsx`, because both packages ship prebuilt.
  - `ada-walletctl` prints its usage, `tidy` included.
  - `ada-wallet-mcp` gets as far as its own "signerd is not answering" check.
  - `ada-signerd`, from the same bytes installed before publishing, comes up on preprod on Koios
    with batch-settlement available and the release loop on.

  Earlier releases were checked the same way: 0.2.7 (`94736c62…`), 0.2.6 (`44bfa586…`), 0.2.5
  (`43b7f48d…`), 0.2.4 (`87f7ffb1…`), 0.2.3 (`dde3cc15…`), 0.2.2 (`b27996d7…`), 0.2.1
  (`e11f0510…`), 0.2.0 (`6edfe7ba…`) and 0.1.0 (17 files, `a6c269ca…`).

## Before mainnet
```
npm run walletctl -- preflight     # fails on anything that must be fixed, warns on every decision
```
`ALLOW_UNVERIFIED_AUDIT=1` is the way past a refused startup when the audit log was rotated on
purpose; it waives the check, not the ledger. `LEDGER_FILE` and `AUDIT_FILE` say where both live.

`preflight` refuses to pass until the policy declares `"network": "cardano:mainnet"`, and signerd
refuses to start on mainnet without it: caps here are bare integers with no unit, so a file tuned
against 10,000 faucet tADA says exactly the same thing to a wallet holding real ADA. It also warns
on each choice that is yours rather than a bug — a payee allowlist of `["*"]`, a missing resource
allowlist, no approval threshold, masumi enabled, Koios as the provider.

On mainnet signerd additionally refuses to start when the policy, audit or ledger file is group- or
world-writable, because "the agent cannot raise its own limits" stops being true the moment the
agent's user can write any of them. On Windows the mode bits do not carry that meaning, so the
check reports that it could not run rather than passing: verify the ACL yourself.

`allowedResources` bounds which URLs an agent may buy from. It is checked against a URL the agent
process reports, because the reference `@x402/cardano` client does not pass the resource through to
the signer — so it constrains an agent that is running this code and being steered, which is the
prompt-injection case, and not one whose process has been replaced. `allowedPayees` is the control
that binds the transaction itself; treat the resource list as the layer above it.

### Which asset id
Caps in `policy.json` are keyed by `"lovelace"` or `"<policyId>.<assetNameHex>"`, and the amounts are
in the smallest unit: `"1000000"` of a 6-decimal token is one token, so
`"1f3aec8bfe7ea4fe14c5f121e2a92e301afe414147860d557cac7e34.5553444378": "5000000"` caps USDCx at 5
per payment. The wallet pays only assets that are listed, so a lookalike token cannot be paid
unless you list it. The way that happens is a wrong copy: token names are not unique, and an
explorer search for "USDCx" returns five policies, only one of them the xReserve USDCx that IOG
bridges.

The mainnet stablecoins this wallet knows (`src/assets.ts`), checked on 2026-09-29 against the
chain (Koios), the Cardano token registry and one more source each (Moneta's policy-id page, the
fingerprint in Circle's announcement, Indigo's contract repository, the Open DJED protocol
datum; USDA has only CoinGecko besides the registry):

| Ticker | Policy id | Asset name (hex) | Decimals | Issuer |
| --- | --- | --- | --- | --- |
| USDCx | `1f3aec8bfe7ea4fe14c5f121e2a92e301afe414147860d557cac7e34` | `5553444378` | 6 | Circle xReserve, bridged by IOG |
| USDM | `c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad` | `0014df105553444d` | 6 | Moneta |
| DJED | `8db269c3ec630e06ae29f74bc39edd1f87c819f1056206e879a1cd61` | `446a65644d6963726f555344` | 6 | COTI, with IOG |
| iUSD | `f66d78b4a3cb3d37afa0ec36461e51ecbde00f26c8f0a68f94b69880` | `69555344` | 6 | Indigo Protocol |
| USDA | `fe7c786ab321f41c654ef6c1af7b3250a613c24e4213e0425a7ae456` | `55534441` | 6 | Anzens |

USDM is a CIP-68 token, so its name is the label prefix `0014df10` and `USDM`; DJED's on-chain
name is `DjedMicroUSD`. Decimals are what the token registry says (for USDM and the preprod test
token also the CIP-68 datum); nothing on chain enforces them for a plain native asset, so they are
the issuer's word. On preprod the wallet knows tUSDM,
`e675b46e4d2242c991a8932a99db3044e80515ae14b4c4ccf6b3f4c9.0014df10745553444d`, also 6 decimals.

Copycats seen on mainnet on 2026-09-29, searching Koios by exact asset name. None is a token you
want; all of them are worth nothing next to the real ones:
- USDCx, four: `038f14bd637e6c7b4ecdb2bf5dde2ccfd69b415f10d01bb5bd0f31da`,
  `325fb65426e2a2af4749d9347e28d4c109d0895340dec5547036ab05`,
  `4e74a46ecac6d7cde02e07e4e829659dcec39bc4704b0a40506f2c70`,
  `82db3e78cea2810a39a97a65820d19621848d132db566ae88105813e`. They are tiny next to the real one:
  about 2,010 USDCx between them at 6 decimals, in at most five addresses each, against 46.25 million
  across 1,381 addresses.
- USDM, twenty-two: fifteen with the same CIP-68 name as Moneta's (`3aba99e1…` even copied the
  reference token, and did it before Moneta minted its own), five plain `USDM`, two lowercase `usdm`.
  Most hold a single unit. Moneta's has 6,218 holding addresses.
- USDA, four: `0b17e18e…`, `203b808c…`, `36a2d845…`, `8bb07d0a…`. `36a2d845…` also mints a fake USDM.
- iUSD, one: `648823ffdad1610b4162f4dbc87bd47f6f9cf45d772ddef661eff198`.
- DJED, one, burned to nothing: `4eea0f3c96825d83a42ecffd9f2b4fb6683977ffc557284644c4ee3c`.

`preflight` reads every asset id in every agent's `perTxMax`. A listed one prints its ticker and
decimals; one whose name matches a known ticker but whose id does not fails on mainnet (warns
elsewhere) and names the real id; anything else warns that it is not a known stablecoin, so check
the policy id and its decimals yourself.

### The key
```
npm run keystore -- create --mnemonic-file ~/.ada-agent-wallet/mnemonic --out ~/.ada-agent-wallet/keystore.json
```
scrypt and AES-256-GCM, asked for the passphrase twice, proved to round-trip before anything is
written. Point signerd at it with `WALLET_KEYSTORE_FILE`, then delete the plaintext mnemonic — once
you are certain the passphrase is recoverable, because at that point the keystore is the wallet.

The passphrase comes from `WALLET_PASSPHRASE_FILE`, or a terminal prompt when there is one.
Deliberately not an environment variable: the point of a keystore is that reading one thing is not
enough, and an env var is readable by anything that can read the process. And if the passphrase
file lives next to the keystore, preflight says so — encryption with the key taped to the box is
worth about what it sounds like.

What this buys: reading a file is no longer enough. A stolen backup, a disk image, a stray copy in
a repository, a directory whose permissions were wrong for a week — all of those now yield
ciphertext. What it does not buy: anything at all against a compromised signerd, which holds the
decrypted mnemonic in memory for as long as it runs. That is what a hot wallet is.

Which is why `MAX_HOT_BALANCE_LOVELACE` exists and why preflight fails on mainnet without it. The
daily cap bounds an agent. Nothing bounds someone who has the key, except how much is in the
wallet, so decide that number deliberately and alert on `ada_wallet_balance_over_ceiling`.

### Who an agent is
By default there is one token, and `agentId` arrives in the request body. With a single agent that
is fine. With several it means the split between them is a convention: any process holding that
token can spend any agent's budget by naming it, which is the opposite of what a per-agent policy
is for. Preflight says so when the policy has more than one agent.

`AGENT_TOKENS_FILE` is a JSON map of `{"<token>": "<agentId>"}`. With it, a token *is* an identity:
`/sign` takes the agent from the token and refuses a body that names another, `/status` returns only
that agent's budget, the approval queue and everything else is the operator's alone, and the
operator token stops signing at all. Operating and paying become different authorities, which they
were not.

### Monitoring
`GET /metrics` is Prometheus exposition behind the same bearer token — it reports what this wallet
has spent, which is not public. Counters restart with the process, as is conventional; the gauges
carry the live state counters cannot.

Alert on: any increase in `ada_wallet_audit_events_total{event="denied"}`, `{event="policy_error"}`
or `{event="nonce_collision"}`; `ada_wallet_over_budget` reaching 1; `ada_wallet_pending_approvals`
staying above zero longer than a human should take; `ada_wallet_policy_readable` reaching 0. A
denial is not by itself an incident — an agent hitting its cap is the system working — but a change
in the rate of them is the first sign that something upstream is steering it somewhere new.

`{event="signed_undelivered"}` is the one that means the ledger and the world have come apart: a
transaction was signed and its spend recorded, and the single request it could have been handed to
had closed. Nothing will broadcast it, and the budget stays spent — a transaction is not unsigned
by nobody having read it — until its TTL and the release margin have passed and the chain shows it
was never included. It takes a caller leaving during the seconds a signature is in flight, so
it should be rare; each one is a payment the agent will have to ask for again.

`{event="spend_released"}` counts budget given back for payments that never landed, and
`{event="provider_retry"}` counts builds that had to be tried again. Neither is an incident on its
own; a rate that keeps climbing is the provider, or a facilitator that is not settling what it is
sent, and worth knowing about before the free tier's rate limit finds it for you.

`{event="auto_tidy"}` counts the tidies signerd started itself because a channel step ran short of
ADA-only funds. Each costs a fee, and there is at most one per `AUTO_TIDY_MIN_INTERVAL_SECONDS`, so
a count that keeps climbing means `exact` payments keep folding the wallet's ADA into its tokens
about as fast as the tidies undo it. `auto_tidy_failed` and `auto_tidy_skipped` are the ones that
did not go through.

### What this still does not do
Naming these is the point; none is fixed by more policy code.
- **There is no hardware-wallet or KMS path.** The keystore means a file read is no longer enough,
  but signerd still decrypts the mnemonic into its own memory and keeps it there. A key that never
  leaves a device is a different architecture, not a setting — and the honest mitigation for this
  one is the balance ceiling: keep in the wallet only what you would accept losing outright.
- **Nothing scrapes `/metrics` for you.** The signals are exposed and the alerts worth writing are
  listed above, but wiring them to something that pages a human is deployment work, not code here.
- **Deleting both `ledger.json` and `audit.jsonl` together still resets the cap.** No single
  deletion does — see `npm run integrity` — but two coordinated ones against files the agent is not
  supposed to be able to write is the residual risk, and file permissions are what covers it.
- **On mainnet you want more than zero confirmations**, which needs Blockfrost: Koios exposes no
  transaction-evidence hook, so a facilitator on it can only settle at `l1Confirmations: 0`. As a
  buyer you do not choose this — the seller's 402 does — but it governs any facilitator you run.

## Known behaviour worth expecting
- **A signed payment counts against the budget when it is signed, and comes back only when the
  chain shows it never landed.** signerd records the spend at signing and never hears whether the
  facilitator got the transaction on chain, so a transient submit failure costs budget without
  moving funds — observed once here: `dailySpent` of 16.5 tADA against 12.5 tADA actually delivered.
  What signerd can know is this: the `exact` signer always gives a transaction a TTL, and one the
  chain has passed without including it never will be. So once a minute signerd reads the chain's
  tip from its own provider, and for every payment whose TTL plus `RELEASE_MARGIN_SECONDS` (default
  1800, so half an hour) is behind the tip, asks whether the transaction is in a block. A definite
  no takes the spend off the ledger: `spend_released` in the audit, counted in `/metrics`, and kept
  across a restart. Anything else leaves it counted — yes, an HTTP error, a timeout, a reply that
  is not what was asked for. The budget comes back on evidence signerd read itself and on nothing
  the agent or the seller says about the payment; the agent cannot talk its way to it, and a payment
  it submitted on its own shows up as in a block and stays spent. `RELEASE_EXPIRED_SPENDS=0` turns
  it off. Three limits. It trusts the provider's "not found": a provider that answered a clean 404
  for a transaction that did land would give back budget that was spent, and what bounds that is the
  balance ceiling, `MAX_HOT_BALANCE_LOVELACE`, not this. A payment signed before this existed
  carries no transaction hash in its record and is never given back. And a batch-settlement
  voucher's spend, or budget held by an approval still waiting, is not a signed transaction with a
  TTL, so neither is released. The tools say so when it happens: `paid: false` with an `unsettled`
  note, and the transaction if one was broadcast, since a settlement reported failed can still
  confirm.
- **Before `@evolution-sdk/evolution` 0.5.14, a 402 round-trip could not complete on Koios at
  all.** Fixed upstream in evolution-sdk#544 (reported from here as #540), released in 0.5.14 on
  2026-09-24, which this package now pins; the `exact` round trip passed on Koios alone the next
  day. What it was: the facilitator's `verify` resolves each input the buyer's transaction spends,
  and `@evolution-sdk`'s Koios provider failed that lookup for almost all of them. Over one wallet's 19 UTXOs, asked three ways: the Koios provider resolved 3,
  the Blockfrost provider resolved 19, and plain HTTP to the same Koios endpoint returned all 19.
  So the data is there and Koios is serving it; the provider cannot read it. The failures are
  deterministic, ~1.4s on an idle box, and grouped by the transaction that created the UTXO — not a
  timeout, not rate limiting, not native assets, not a UTXO that is missing or spent. All you are
  told is `Koios getUtxosByOutRef failed`, with the cause discarded. There is no steering around it
  either, because the SDK always takes `utxos[0]` as the payment nonce: if the wallet's first UTXO
  is one it cannot read, nothing that wallet does can pay. The cause was the provider decoding the
  creating transaction's whole `/tx_info` row, three fields of which Koios returns as `null`.
- **Three batch-settlement answers mean "ask again", not "broken".** `channel_busy` (409, retryable):
  the channel is between two states the chain has not finished showing — right after a top-up of
  this wallet's own, Blockfrost's index can still show the channel where the top-up spent it, and
  the client waits up to 30 s for it before saying so. `wallet_tidying` (409, retryable, 60 s):
  a channel step ran short of ADA-only funds and signerd is tidying the wallet, or has and the
  provider does not list the result yet (see the tidy paragraph under batch-settlement). `not_yet`
  (409): `walletctl elapse` before the channel's `elapse_at`, refused rather than waited out inside
  the wallet lock. Only `insufficient_funds` is an answer about the wallet.
- **A wallet that cannot fund an `exact` payment is `insufficient_funds`, in three of the builder's
  wordings.** `Coin selection failed`, `Cannot create valid change`, and `Cannot balance
  transaction: Native assets present in leftover but insufficient lovelace`, the last seen from a
  wallet whose one UTxO held many tokens and 4.7 tADA: it could pay, but not and still give the
  tokens back their min-ADA. That one was a 500 `sign_failed` until it was classed with the
  others. An `exact` payment does not start a tidy, whichever it is; only channel steps do.
- **Koios can refuse a submit moments after confirming the transaction it chains from.** A payment
  spending the change of a just-confirmed transaction was rejected with `Koios submitTx failed`,
  and the identical payment succeeded on retry: its query view had the new UTXO before its submit
  node had the block. Retry rather than treat it as fatal — and note this is why the in-flight UTXO
  hold is short, since an over-long one makes that retry impossible.
- **A payment started moments after the previous one settled can fail at `sign_failed: Blockfrost
  getUtxos failed`.** Seen once, about ten seconds after an MCP payment had settled: signerd's
  provider call for the wallet's UTxOs failed in under a second, while the identical run passed on
  its own a minute later, as did a direct query of the same endpoint. The cause was not captured —
  a free key's burst limit is the obvious suspect and not a proven one — so treat it as retryable,
  the way `Koios submitTx failed` above is. signerd now does that itself for the build: a provider
  read that failed, or the network under one (`fetch failed`, `ECONNRESET`, a timeout), is tried
  again after 1s and then 3s, each try logged as `provider_retry`, and only then reported as
  `sign_failed`. Nothing has been signed or handed out when a build throws, so a second try cannot
  commit anything twice. A submit failure is never retried this way — the facilitator does the
  submitting — and neither is `insufficient_funds`, which is an answer about the wallet.
- The in-flight hold is not what keeps an agent inside its cap; the ledger is. `NONCE_HOLD_SECONDS`
  (120s) only avoids handing back a transaction some other unsettled one has already doomed.

## Not yet
- direct `send` (non-x402 transfer) — needs our own submit path; v1 is x402 only
- Masumi escrow flows (`assetTransferMethod: masumi`) pass through untouched; policy still applies to the amount
- policy is per-agent, not per-resource; add `allowedResources` if needed

## License

Apache-2.0 — see [LICENSE](LICENSE).