```mermaid
flowchart TD
  A["Agent: tools/call wallet<br/>wallet.paymentrequired.com/mcp"] --> W["wallet service :8798<br/>handleMcp, mcp-wallet-tool.mjs call()"]
  W --> SW{"action"}
  SW -->|balance| BAL["resolve or create Safe<br/>finishTap, recoverKey<br/>reserve via banking helper"]
  SW -->|fund| FUND["topUp: own wallet, then reserve,<br/>then bank send to Safe"]
  SW -->|pay| PAY["x402 dry run, limits,<br/>top-up, pay, receipt"]
  SW -->|send| SEND["USDC to 0x;<br/>owned wallet = owner tap"]
  SW -->|history| HIS["read past operations"]
  SW -->|bridge| BR["Gateway deposit, burn, mint<br/>to own Safe on chain"]
  SW -->|discover| DIS["own list + Circle x402 directory<br/>pure read"]
  FUND -->|"action send, to=Safe"| BK["banking bank tool"]
  SEND -->|"approval_required"| SG["approval page / signer"]
  PAY --> NET(("Base 8453<br/>USDC"))
  SEND --> NET
  BR --> NET
```

# `wallet` (Wallet)

[← Index](../index.md) · [Overview](../overview.md) · [Signer](../signer.md) · [Issued links](../links.md)

> Evidence labels: **CODE** = read in the cited file at the recorded revision/hash; **FIXTURE** = parent-accepted combined cloud fixture run 99/99 (`directory-wave/parent-directory-wave-acceptance.json`), not live; **LIVE-RO** = observed read-only on the live host 2026-10-14 (raw files in `evidence/live-probes/`); **UNVERIFIED** = not executed here. Topology = accepted directory-wave source, **not deployed**. Nothing in this handbook moved money, signed, or activated a card.

## What it does

The agent's own USDC wallet (a Safe on Base). It holds money for the agent, pays x402 paid links, sends USDC, moves USDC to another chain, and finds paid services. Before the person connects, the agent holds it through `wallet_key`. After the person taps the connect link, the wallet becomes theirs and outgoing sends need their tap.

**Catalog:** wallet.paymentrequired.com/mcp (wallet service tools/list: `wallet` only). **Schema & dispatch:** Input `wallet:src/mcp-wallet-tool.mjs:116-139`; action enum `wallet:src/mcp-wallet-tool.mjs:123`; output `wallet:src/mcp-wallet-tool.mjs:139-181`; dispatch `wallet:src/mcp-wallet-tool.mjs:827-834`; allowed fields `wallet:src/mcp-wallet-tool.mjs:819`. `chain` enum is filled at runtime from payable+Gateway chains `wallet:src/mcp-wallet-tool.mjs:224` (pinned contract `facade/contracts/tools.json` enum: `base, ethereum, arbitrum, optimism, polygon`; FIXTURE schema probe equal).

## Actions / operations

| action | what happens | effect | source |
|---|---|---|---|
| `balance` | Shows `available` USDC and `address`. The first call with no key and no session asks whether to make a new wallet (`new: true` makes one, `wallet:src/mcp-wallet-merge.mjs:36`). Returns `wallet_key` once when created or when the key changes. Finishes a pending hand-over or top-up. Adds `reserve` (USDC on the bank account, only after the identity check), `gateway` per chain, and a connect `approval_url` while nobody owns the wallet. | read (+ may create a wallet, may finish an already-approved hand-over) | `wallet:src/mcp-wallet-tool.mjs:465`–`wallet:src/mcp-wallet-tool.mjs:515` |
| `fund` | Adds money. Order: the person's connected wallet, then the reserve path (sandbox chain 84532 only; mainnet answers 'not available yet'), then a bank `send` of `amount` from the person's bank account to the Safe. With nothing usable it answers `funded:false` and the address to send USDC to. Without `amount` (with a session) it asks how much. | MONEY MOVE (bank→Safe via banking, approval link) | `wallet:src/mcp-wallet-tool.mjs:427`, `wallet:src/mcp-wallet-tool.mjs:587`; reserve gate `wallet:src/mcp-wallet-fund-reserve.mjs:29` |
| `pay` | Pays an https x402 link (`url`, optional `method`, `body`, `max_amount`, `chain`). It first dry-runs to read the price, checks the owner limits (over a limit the owner gets a one-time lift link), tops up any shortfall from the own-wallet/reserve lanes, then pays and returns `receipt {tx, amount, to}` plus the seller's `resource`. If the merchant answers ≥400 after settling, the answer is `ok:false` *with* the receipt. | MONEY MOVE (USDC to merchant) | `wallet:src/mcp-wallet-tool.mjs:622`–`wallet:src/mcp-wallet-tool.mjs:696` |
| `send` | Sends USDC `amount` to `to` (0x). Needs `wallet_key` (guest 44 / connected 66 chars) or the owner's `session`. A wallet owned by a person returns `approval_required` plus a link (D-OWNER-TAP). Wallet limits are enforced before signing. | MONEY MOVE (USDC on Base) | `wallet:src/mcp-wallet-tool.mjs:697`–`wallet:src/mcp-wallet-tool.mjs:736` |
| `history` | Lists past operations, oldest first: `request_id, kind, status (done/pending/failed), amount, to, tx, reason`. Needs `wallet_key` or `session` and never creates a wallet. | read | `wallet:src/mcp-wallet-tool.mjs:794` |
| `bridge` | Moves `amount` USDC through Circle Gateway to the same Safe on `chain`. It deposits the gap first (Circle credits it in about 20 min), then burns and mints. `ethereum` is refused. A second bridge under a new `request_id` waits while one is in flight. | MONEY MOVE (cross-chain, self-to-self) | `wallet:src/mcp-wallet-tool.mjs:527` |
| `discover` | Finds paid services: the operator's checked list first, then Circle's public x402 directory (`query`, `limit` 1–25). It is a pure read and resolves no wallet. Shape from code: `ok, services[], total, sources{paymentrequired,circle}, next, status`. | read | `wallet:src/mcp-wallet-tool.mjs:744` |

**Not public:** Private RPC only, **not public**: `action: "pay_fee"` (tool-price payer) is routed in `wallet-server.mjs` `wallet:src/wallet-server.mjs:122`; helper ops `safe_address`, `charge`, `remember`, `on_tap`, `identity` `wallet:src/wallet-server.mjs:125`.


## Prerequisites

None for `discover`. `balance` with `new:true` creates a guest wallet. Money actions need the `wallet_key` from the first answer, or the owner's `session` once connected. `fund` from the bank needs a verified bank account (see `account`). Funds on Base chain 8453.

**Session / connect (shared by `wallet`, `account`, `bank`).** A call without `session` (or with a dead one) does not fail: the connection layer answers `status: "connect_required"` with an `approval_url` the person opens once, then the same call is repeated (`banking:src/mcp-connection.mjs:233`, `banking:src/mcp-connection.mjs:109`). `session` must match `SESSION_PATTERN` `banking:src/mcp-session.mjs:3`. A connect reference passed as `request_id` (43 chars) without `session` is resolved to the session first (`banking:src/mcp-connection.mjs:215`). For `account`/`bank` on a handed-off identity, a one-time *lazy bank grant* link may appear first (`banking:src/mcp-connection.mjs:153`).

**Per-action price.** Off by default. If the operator sets `TOOL_PRICES_JSON`, a priced action is paid from the caller's agent wallet *before* it runs, and an unpaid charge means the action does not run (`banking:src/tool-prices.mjs:1`, `banking:src/mcp-connection.mjs:293`). On banking the charge goes to the wallet service over RPC; if that peer is down the answer is `service_unavailable … Nothing was charged` (`banking:src/server.mjs:170`). Live price table: UNVERIFIED (env, not in repo).

## How to use it (exact `tools/call` JSON)

Endpoint: `POST https://wallet.paymentrequired.com/mcp` (JSON-RPC 2.0). Placeholders in `<…>` are never real values; secrets (`session`, `wallet_key`) are never pasted into docs or logs.

**Find a paid weather API (read-only, safe demo)**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "wallet",
    "arguments": {
      "action": "discover",
      "query": "weather",
      "limit": 3
    }
  }
}
```

Expected result (`structuredContent`, shape from code; values illustrative):

```json
{"ok":true,"services":[{"name":"…","url":"https://…","method":"GET","price":{"amount":"0.01","asset":"USDC","chain":"base"},"source":"circle"}],"total":123,"sources":{"paymentrequired":0,"circle":3},"next":"Pick one, then pay it with its method."}
```

**Make / open the agent wallet (creates a guest wallet; no money moves)**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "wallet",
    "arguments": {
      "action": "balance",
      "new": true
    }
  }
}
```

Expected result (`structuredContent`, shape from code; values illustrative):

```json
{"ok":true,"available":"0.00","address":"0x…","wallet_key":"<keep secret>","approval_url":"https://<PUBLIC_ORIGIN>/s#…","next":"Wallet ready: show address and balance. Link optional: makes it theirs."}
```

**Pay a paid link — MONEY, separately authorized only**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "wallet",
    "arguments": {
      "action": "pay",
      "wallet_key": "<wallet_key>",
      "url": "https://example.com/paid",
      "max_amount": "0.10",
      "request_id": "pay-001"
    }
  }
}
```

Expected result (`structuredContent`, shape from code; values illustrative):

```json
{"ok":true,"status":"done","receipt":{"tx":"0x…","amount":"0.01","to":"0x…"},"resource":{…},"next":"Paid."}
```

**Send USDC — MONEY, separately authorized only**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "wallet",
    "arguments": {
      "action": "send",
      "session": "<session>",
      "to": "<authorized_recipient_address>",
      "amount": "1.00",
      "request_id": "send-001"
    }
  }
}
```

Expected result (`structuredContent`, shape from code; values illustrative):

```json
{"ok":true,"status":"approval_required","approval_url":"https://…","request_id":"send-001","next":"Show link. After tap: same call."}
```

## Approval, signature and money

Guest wallet (no owner): the `wallet_key` is the authority, and `send`/`pay` run without a tap. Owned wallet: `send` waits for the owner's tap (`approval_required`). Owner limits (`account limits`) are checked before any signature, and over a limit the owner gets a one-time lift link (`wallet:src/mcp-wallet-tool.mjs:329`). Missing or old key → `connect_required` with a fresh link that re-derives the key (`wallet:src/mcp-wallet-tool.mjs:397`).

## Failure, peer-down and retry

Every money action takes `request_id`, and repeating the same label continues the same operation; it never pays twice. Unconfirmed states (`pending, unknown, submitted, submitting, prepared`) answer `status:"pending"` with 'Same call in 10 s' (`wallet:src/mcp-wallet-tool.mjs:43`). `bridge` with a new label is blocked while another bridge is in flight (`wallet:src/mcp-wallet-tool.mjs:552`). If banking is down, `fund` from the bank and `reserve` return an error answer or are left out; the wallet itself keeps working (code-read, UNVERIFIED live).

**Peer-down / retry contract (cross-service RPC).** Calls between wallet and banking (and the apex directory's `helper`/`bridge` calls for issued links) go over loopback `POST /rpc/tool` with a bearer secret (`banking:src/service-rpc.mjs:50`). A refused connection answers `service_unavailable … Nothing was sent` (`banking:src/service-rpc.mjs:65`); any later failure answers `status: "unknown"`, `service_unconfirmed … repeat the same call with the same request_id` (`banking:src/service-rpc.mjs:56`). Rule for the agent: on `unknown`, repeat with the **same** `request_id`; never start a new one.

## What was actually observed

FIXTURE: wallet `tools/list` schema equals the directory pin (probe-schema 3/3); fixture continuation and history calls passed in the accepted 99/99 run (no real Privy/Wirex/mainnet). LIVE-RO 2026-10-14T18:09Z: `wallet.paymentrequired.com/mcp` answers 503 'Service not deployed yet' (evidence/live-probes). Every money action live: UNVERIFIED (no money, no session used).
