```mermaid
flowchart TD
  A["Agent: tools/call bank<br/>banking.paymentrequired.com/mcp"] --> B["banking :8796 handleMcp<br/>bankGrant, accountReadyFor, call()"]
  B --> G{"KYC verified?<br/>ensureReady"}
  G -->|no| NV["Identity check missing<br/>call account verify"]
  G -->|yes| SW{"action"}
  SW -->|card| CA["masked card, limits,<br/>details link (in_chat option)"]
  SW -->|iban| IB["IBAN, BIC, holder, Base address,<br/>crypto deposit addresses"]
  SW -->|"send iban"| SE["quote, then tap or within limits,<br/>then SEPA transfer"]
  SW -->|"send card_id"| PU["card payout quote, tap, transfer"]
  SW -->|"send to 0x"| ST["withdrawal from account,<br/>two taps"]
  SW -->|swap| SWP["FX estimate, withdrawal to deposit,<br/>execute trade"]
  SW -->|transactions| TX["activity feed or statement range"]
  SW -->|manage| MG["11 ops: freeze, unfreeze, activate,<br/>close, set-limit, order-physical,<br/>3ds_status, 3ds_respond, payout-cards,<br/>add-payout-card, issue-card"]
  SE --> SG["approval page / signer"]
  ST --> SG
  MG --> SG
  SE --> WX(("Wirex BaaS"))
  ST --> WX
  SWP --> WX
  MG --> WX
```

# `bank` (Banking)

[← 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 person's Visa card and euro account at the provider (Wirex). Read the balance, IBAN and deposit addresses; send euros by SEPA, to a saved card, or dollars to a 0x address; swap EUR↔USD; read history; manage cards.

**Catalog:** banking.paymentrequired.com/mcp (banking service tools/list: `account`, `bank`). **Schema & dispatch:** Input `banking:src/mcp-bank-tool.mjs:87-123`; ops `banking:src/mcp-bank-tool.mjs:69`, tap ops `banking:src/mcp-bank-tool.mjs:71`; dispatch `banking:src/mcp-bank-tool.mjs:1021-1058`; output = generic `MCP_RESULT_SCHEMA` `banking:src/mcp-tool-results.mjs:36`. `card_number_last4` and `expiry_date` (#407 activate) are in the accepted banking code and the directory pin (FIXTURE schema probe equal). The pre-cutover live apex ed64a10 `tools/list` (evidence/live-probes) lacks them; that host is the old deployment, not this topology.

## Actions / operations

| action | what happens | effect | source |
|---|---|---|---|
| `card` | Masked card(s) (`card_id`, `masked`, `expiry`, `brand`, `state`, `limits`), the balance, and a details link. Full numbers appear only on the approval page after the person signs. `in_chat: true` + `request_id`: after the tap and the signed chat consent, the same call returns the numbers **once**. | read (+ sensitive reveal behind tap) | `banking:src/mcp-bank-tool.mjs:204` |
| `iban` | `iban, bic, holder, currency EUR`, the account's own Base `address` (USDC/EURC sent there counts as balance), `deposit {addresses, networks, minimums}`, plus balance/holdings. | read | `banking:src/mcp-bank-tool.mjs:242` |
| `send (iban)` | SEPA euros: `iban`, `recipient_name` (first + last), `amount`, optional `reference` ≤140, `request_id`. The BIC is derived from the IBAN. It quotes first: within limits it sends at once; otherwise it returns an owner-pinned tap link, and the same call sends. A fee move of cents after the tap re-quotes and sends without asking again. | MONEY MOVE (SEPA) | `banking:src/mcp-bank-tool.mjs:303` |
| `send (card_id)` | Euros to a saved payout card (`card_id` from `manage payout-cards`). The same quote → tap/limits → transfer pattern. | MONEY MOVE (card payout) | `banking:src/mcp-bank-tool.mjs:572` |
| `send (to 0x)` | Dollars (WUSD→USDC, or WEUR→EURC if dollars are short) from the account's smart wallet to an address on Base, via a two-phase withdrawal (two taps). Needs KYC only, not the euro account. | MONEY MOVE (on-chain, two taps) | `banking:src/mcp-bank-tool.mjs:463` |
| `swap` | EUR↔USD inside the account (`to: USD|EUR`, `amount` ≥10). It shows the rate, `receive` and `deposit`; it withdraws into the provider's deposit address (taps), then executes a fresh estimate once. | MONEY MOVE (FX) | `banking:src/mcp-bank-tool.mjs:396` |
| `transactions` | Card + bank activity, 10 per `page`. With `start_date`/`end_date` (YYYY-MM-DD, at most 6 months) it returns a statement instead. | read | `banking:src/mcp-bank-tool.mjs:651` |
| `manage` | `op` = `freeze`, `unfreeze` (run on first call; target by `card_id` or rule), `activate` (physical card; `card_id` + `card_number_last4` + `expiry_date` MM/YYYY), `set-limit` (`limits{daily_limit,monthly_limit,transaction_limit}`), `3ds_status` (list pending purchases), `3ds_respond` (`transaction_id`, `approve`), `payout-cards` (read), and tap ops `close`, `add-payout-card` (`card_number`, `cardholder_name`, `card_label`), `issue-card` (`limits`, `currency`), `order-physical` (without address → lists `card_types` fees and `delivery` providers; with `country`, `delivery_provider`, `name_on_card`, `delivery_address`, `card_type` → pays the card fee from the account (taps) and orders). | WRITE / MONEY (card fee) / IRREVERSIBLE (close) | `banking:src/mcp-bank-tool.mjs:980`, activate `banking:src/mcp-bank-tool.mjs:1004`, order `banking:src/mcp-bank-tool.mjs:913`, tap `banking:src/mcp-bank-tool.mjs:952` |

**Not public:** `cardApproved` is a bridge callback for the details page, **not** a tool action (`banking:src/mcp-bank-tool.mjs:1064`).


## Prerequisites

`session` (required by schema). KYC verified (`account verify`). Euro account active for `iban`, `send iban`; card active for `card`. `send to`/`swap` need KYC only.

**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://banking.paymentrequired.com/mcp` (JSON-RPC 2.0). Placeholders in `<…>` are never real values; secrets (`session`, `wallet_key`) are never pasted into docs or logs.

**Show IBAN and deposit addresses (read-only)**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "bank",
    "arguments": {
      "action": "iban",
      "session": "<session>"
    }
  }
}
```

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

```json
{"ok":true,"status":"done","iban":"DE…","bic":"…","holder":"…","currency":"EUR","address":"0x…","deposit":{"addresses":[…],"networks":[…],"minimums":[…]},"balance":"12.34","next":"Add money: IBAN or deposit address."}
```

**Show the card, masked (read-only)**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "bank",
    "arguments": {
      "action": "card",
      "session": "<session>"
    }
  }
}
```

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

```json
{"ok":true,"status":"done","card":{"card_id":"…","masked":"**** **** **** 1234","expiry":"…","brand":"…","state":"active"},"approval_url":"https://…","next":"Show link: card details."}
```

**Recent transactions (read-only)**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "bank",
    "arguments": {
      "action": "transactions",
      "session": "<session>",
      "page": 1
    }
  }
}
```

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

```json
{"ok":true,…,"next":"Shown."}
```

**SEPA transfer — MONEY, separately authorized only**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "bank",
    "arguments": {
      "action": "send",
      "session": "<session>",
      "iban": "DE00…",
      "recipient_name": "Erika Mustermann",
      "amount": "12.50",
      "reference": "Rent",
      "request_id": "sepa-001"
    }
  }
}
```

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

```json
{"ok":true,"status":"approval_required","amount":"12.50","fee":"0.44 EUR","approval_url":"https://…","request_id":"sepa-001","next":"Show link. After tap: same call."}
```

**Activate a physical card — card activation, separately authorized only**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "bank",
    "arguments": {
      "action": "manage",
      "op": "activate",
      "session": "<session>",
      "card_id": "<card_id>",
      "card_number_last4": "1234",
      "expiry_date": "09/2029"
    }
  }
}
```

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

```json
{"ok":true,"status":"done","card_id":"…","card":{"masked":"**** **** **** 1234",…},"next":"Activate done."}
```

## Approval, signature and money

Money and irreversible ops create an owner-pinned approval session (`expectedAddress: owner`), and the result is accepted only after `bridge.approved()` verifies that owner's signature (`banking:src/mcp-bank-tool.mjs:362`). SEPA and card payouts within `account limits` run without a tap (`banking:src/mcp-bank-tool.mjs:296`). `send to`, `swap` and the physical-card fee use the account withdrawal's own two taps.

## Failure, peer-down and retry

`request_id` binds a quote/transfer in SQLite `mcp_bank_requests`, so a restart cannot cause a second send (`banking:src/mcp-bank-tool.mjs:14`). Unclear outcomes answer `pending`, 'Call send again with the same request_id … Do not start a second transfer'. An expired approval link → `failed`, 'Start again with a new request_id'. Card reveals are RAM-only (15 min) and lost on restart.

**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: banking `tools/list` `bank` schema equals the directory pin incl. #407 fields (probe-schema 3/3), not real Wirex. LIVE-RO 2026-10-14T18:09Z: banking host 503 'Service not deployed yet'; old apex ed64a10 still lists `bank` without #407 fields (pre-cutover deployment). No call executed; no card activated. Everything else UNVERIFIED live.
