# Wallet OS API

Read a Solana multisig live from chain and run its whole governed lifecycle — prepare, propose, vote, execute. Every write hands back unsigned bytes; the keys stay yours.

You are helping a user call this API. This document is the complete
contract: every endpoint, its parameters and request body, the auth, and
the MCP surface. Generate runnable `curl`/TypeScript/Python on demand
using the values below; replace the placeholder key with the user's real
`k256_live_` key.

- Base URL: `https://api.k256.xyz`
- Data plane: `https://api.k256.xyz/v1/wallet-os`
- OpenAPI: `https://api.k256.xyz/wallet-os/api/spec.json`
- MCP (for agents): `https://api.k256.xyz/wallet-os/mcp` (server name `k256-gateway`) — the same operations as 1:1 tools.
- MCP writes need one extra argument: every tool whose `Action` below is `write` requires `confirm_mcp_write: true`. Reads take no such argument.
- Auth: every request needs `Authorization: Bearer <YOUR_API_KEY>`. Never put the key in a URL query string.

Wallet OS operates Squads-compatible Solana multisigs. Two facts drive every correct call. First, the gateway holds no keys: prepare ops (prepare_action, prepare_config_change, prepare_spending_limit_change, prepare_program_upgrade, propose_action, approve_action, reject_action, execute_action, …) return message_base64 with required_signers in signature-slot order, and wallet-os.submit_transaction sends the signed result. A prepare call changes nothing on chain. Second, only get_multisig, import_multisig, create_multisig, and list_multisigs work from a cluster + configuration address; every other multisig-scoped op takes the import id (msig_…) returned by import_multisig or list_multisigs, so import first. The lifecycle is create the transaction, open voting on it (propose_action), collect approvals to the threshold, wait out the time lock, then execute. A transaction with no proposal cannot be voted on or executed by anyone: get_queue lists those with state `created` and can_propose. Config changes fold their actions IN ORDER and are checked once on the result, so changing what a member may do is remove_member + add_member on the same address in one call. Executing a config change voids every proposal beneath it. If a submit fails, it does not tell you whether the bytes reached the chain — read the queue before signing the same thing again.

## Endpoints

### `GET /v1/wallet-os/info` — Wallet OS service info.
- Action: `read` · MCP tool: `wallet-os.get_info`
- Returns the Wallet OS product status and the spec it is built to.
- Response (JSON):
  - `product` — string
  - `status` — string
  - `message` — string
  - `spec_ref` — string

### `GET /v1/wallet-os/multisigs/{cluster}/{address}` — Read a multisig configuration (live chain decode)
- Action: `read` · MCP tool: `wallet-os.get_multisig`
- Fetches the multisig configuration account at finalized commitment, verifies the owner program and account discriminator, and decodes it: members with permission masks, threshold, time lock, transaction and stale indexes, rent collector, and the vault (acting) address at index 0. Errors are exact: wrong owner, wrong account type, and address-not-on-this-cluster each name the corrective step. The observed slot is the slot the account bytes were read at.
- Path param `cluster` (string, required) — mainnet, devnet, or testnet
- Path param `address` (string, required) — Multisig configuration address (base58) — the address shown as the multisig's own address, never the vault/acting address
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `config_address` — string
  - `create_key` — string
  - `config_authority` — string | null
  - `threshold` — integer
  - `time_lock_seconds` — integer
  - `transaction_index` — integer
  - `stale_transaction_index` — integer
  - `rent_collector` — string | null
  - `vault_address` — string
  - `members[]` — object[]
    - `address` — string
    - `mask` — integer
    - `initiate` — boolean
    - `vote` — boolean
    - `execute` — boolean
  - `observed_slot` — integer

### `POST /v1/wallet-os/multisigs/import` — Import a multisig into the workspace
- Action: `write` · MCP tool: `wallet-os.import_multisig`
- Verifies the address is a live multisig configuration account on the given cluster (same verification and decode as get_multisig) and records the import for the org. Idempotent on (org, cluster, config address): re-importing returns the existing row with created=false, refreshing label/project_id when supplied and restoring an archived binding — importing is the statement "I want this active", and it is the undo path for remove_multisig.
- Request body (JSON, required):
  - `cluster` — string (required) — mainnet, devnet, or testnet
  - `address` — string (required) — Multisig configuration address (base58) — the address shown as the multisig's own address, never the vault/acting address
  - `label` — string — Display label; kept on re-import when omitted
  - `project_id` — string — Project to file the multisig under
- Response (JSON):
  - `multisig` — object
    - `id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `config_address` — string
    - `program_id` — string
    - `label` — string | null
    - `project_id` — string | null
    - `imported` — boolean
    - `created_at` — integer
    - `archived_at` — integer | null
  - `created` — boolean
  - `config` — object
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `config_address` — string
    - `create_key` — string
    - `config_authority` — string | null
    - `threshold` — integer
    - `time_lock_seconds` — integer
    - `transaction_index` — integer
    - `stale_transaction_index` — integer
    - `rent_collector` — string | null
    - `vault_address` — string
    - `members[]` — object[]
      - `address` — string
      - `mask` — integer
      - `initiate` — boolean
      - `vote` — boolean
      - `execute` — boolean
    - `observed_slot` — integer

### `GET /v1/wallet-os/multisigs` — List the workspace's imported multisigs
- Action: `read` · MCP tool: `wallet-os.list_multisigs`
- Returns the org's imported multisigs with a live freshness re-read of each config account (same decode as get_multisig). A row whose account no longer verifies — closed, wrong owner, or unreachable RPC — is isolated into `unavailable` with its exact corrective error instead of failing the whole list. Supports name/address search, cluster/project/archived filters, and keyset pagination (deterministic created-order): `total` is the real match count across all pages and `next_cursor` is null only when the list is complete, so no page ever reads as the whole workspace. Omitting `archived` returns active and archived bindings alike.
- Query param `search` (string) — Case-insensitive substring over label and configuration address
- Query param `cluster` ("mainnet" | "devnet" | "testnet") — Only this cluster
- Query param `project_id` (string) — Only multisigs filed under this project
- Query param `archived` ("exclude" | "only") — exclude = active bindings only; only = archived only. Omitted returns both.
- Query param `cursor` (string) — Opaque page cursor from a previous response's next_cursor
- Query param `limit` (integer) — Page size (default 100)
- Response (JSON):
  - `multisigs[]` — object[]
    - `id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `config_address` — string
    - `program_id` — string
    - `label` — string | null
    - `project_id` — string | null
    - `imported` — boolean
    - `created_at` — integer
    - `archived_at` — integer | null
    - `live` — object
      - `cluster` — "mainnet" | "devnet" | "testnet"
      - `config_address` — string
      - `create_key` — string
      - `config_authority` — string | null
      - `threshold` — integer
      - `time_lock_seconds` — integer
      - `transaction_index` — integer
      - `stale_transaction_index` — integer
      - `rent_collector` — string | null
      - `vault_address` — string
      - `members[]` — object[]
      - `observed_slot` — integer
  - `unavailable[]` — object[]
    - `id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `config_address` — string
    - `program_id` — string
    - `label` — string | null
    - `project_id` — string | null
    - `imported` — boolean
    - `created_at` — integer
    - `archived_at` — integer | null
    - `error` — object
      - `code` — string
      - `message` — string
  - `total` — integer
  - `next_cursor` — string | null

### `POST /v1/wallet-os/multisigs/update` — Rename, re-file, or archive a workspace binding
- Action: `write` · MCP tool: `wallet-os.update_multisig`
- Updates the workspace's import record for a multisig: rename its label, file / re-file it under a project (null project_id unfiles it), or archive / restore it (archive:true / archive:false). Every one of these is workspace filing only — the on-chain multisig, its members, assets, proposals and notifications are untouched. Each change writes an org-audit receipt.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `label` — string — Rename the binding (workspace label only)
  - `project_id` — string | null — File / re-file under this project; null unfiles. Omitted keeps the current filing.
  - `archive` — boolean — true archives the binding, false restores it. The on-chain multisig is untouched either way.
- Response (JSON):
  - `multisig` — object
    - `id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `config_address` — string
    - `program_id` — string
    - `label` — string | null
    - `project_id` — string | null
    - `imported` — boolean
    - `created_at` — integer
    - `archived_at` — integer | null
  - `summary` — string

### `POST /v1/wallet-os/multisigs/remove` — Remove a multisig's workspace shortcut
- Action: `write` · MCP tool: `wallet-os.remove_multisig`
- Deletes the workspace binding (the local shortcut) only. The on-chain multisig, its members, vaults, assets and proposals are NOT deleted or altered in any way — re-importing the address brings the binding back, which is also the undo path. Idempotent: removing an already-removed binding returns removed:false instead of erroring. Writes an org-audit receipt recording what was removed.
- Request body (JSON, required):
  - `multisig_id` — string (required)
- Response (JSON):
  - `multisig_id` — string
  - `removed` — boolean
  - `summary` — string

### `GET /v1/wallet-os/queue/{multisig_id}` — Get a multisig's action queue (live proposal scan)
- Action: `read` · MCP tool: `wallet-os.get_queue`
- Scans the multisig's proposals live (a getProgramAccounts call filtered by the proposal discriminator and this multisig — bounded to its own history, capped at 100 items with truncated:true past the cap; the cap is the M0 constraint the indexer removes) and buckets each by what the given signer can do: needs_you (vote, activate, execute, or cancel-vote now), waiting (voted already / time lock running / no permission), ready (approved with the time lock released), done (terminal, most recent first). Each item carries votes with member addresses, vote math (approvals/rejections needed), the time-lock countdown, staleness, vault index, close eligibility, and amount_binding — "unverified" marks a stored confidential transfer whose hidden amount this lane cannot verify, so it is visible before any approval.

A row may also carry state `created`: the transaction exists on chain with NO proposal on it, so nobody can vote until someone opens one (wallet-os.propose_action). Those rows carry can_propose for the given signer and never a vote affordance, and only appear above the stale boundary — the program refuses a proposal on a stale transaction, so anything at or below it can never be opened. The response also reports rent_collector: the only account that can sign a close (wallet-os.close_action_accounts).
- Path param `multisig_id` (string, required) — Imported multisig id (msig_…) from import_multisig
- Query param `signer` (string, required) — Member address (base58) the capability flags and needs_you bucket are computed for
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `multisig_id` — string
  - `config_address` — string
  - `signer` — string
  - `signer_is_member` — boolean
  - `threshold` — integer
  - `time_lock_seconds` — integer
  - `transaction_index` — integer
  - `stale_transaction_index` — integer
  - `rent_collector` — string | null
  - `buckets` — object
    - `needs_you[]` — object[]
      - `transaction_index` — integer
      - `kind` — "vault_transaction" | "batch" | "config_transaction" | "unknown"
      - `state` — "created" | "draft" | "active" | "rejected" | "approved" | "executing" | "executed" | "cancelled"
      - `stale` — boolean
      - `vault_index` — integer | null
      - `approved[]` — string[]
      - `rejected[]` — string[]
      - `cancelled[]` — string[]
      - `approvals_needed` — integer | null
      - `rejections_needed` — integer | null
      - `status_timestamp` — integer | null
      - `execute_after` — integer | null
      - `time_lock_remaining_seconds` — integer | null
      - `effect_summary` — string | null
      - `decode_blocking` — boolean
      - `amount_binding` — "unverified" | "not_applicable"
      - `can_propose` — boolean
      - `can_activate` — boolean
      - `can_approve` — boolean
      - `can_reject` — boolean
      - `can_execute` — boolean
      - `can_cancel_vote` — boolean
      - `close` — object
      - `note` — string | null
    - `waiting[]` — object[]
      - `transaction_index` — integer
      - `kind` — "vault_transaction" | "batch" | "config_transaction" | "unknown"
      - `state` — "created" | "draft" | "active" | "rejected" | "approved" | "executing" | "executed" | "cancelled"
      - `stale` — boolean
      - `vault_index` — integer | null
      - `approved[]` — string[]
      - `rejected[]` — string[]
      - `cancelled[]` — string[]
      - `approvals_needed` — integer | null
      - `rejections_needed` — integer | null
      - `status_timestamp` — integer | null
      - `execute_after` — integer | null
      - `time_lock_remaining_seconds` — integer | null
      - `effect_summary` — string | null
      - `decode_blocking` — boolean
      - `amount_binding` — "unverified" | "not_applicable"
      - `can_propose` — boolean
      - `can_activate` — boolean
      - `can_approve` — boolean
      - `can_reject` — boolean
      - `can_execute` — boolean
      - `can_cancel_vote` — boolean
      - `close` — object
      - `note` — string | null
    - `ready[]` — object[]
      - `transaction_index` — integer
      - `kind` — "vault_transaction" | "batch" | "config_transaction" | "unknown"
      - `state` — "created" | "draft" | "active" | "rejected" | "approved" | "executing" | "executed" | "cancelled"
      - `stale` — boolean
      - `vault_index` — integer | null
      - `approved[]` — string[]
      - `rejected[]` — string[]
      - `cancelled[]` — string[]
      - `approvals_needed` — integer | null
      - `rejections_needed` — integer | null
      - `status_timestamp` — integer | null
      - `execute_after` — integer | null
      - `time_lock_remaining_seconds` — integer | null
      - `effect_summary` — string | null
      - `decode_blocking` — boolean
      - `amount_binding` — "unverified" | "not_applicable"
      - `can_propose` — boolean
      - `can_activate` — boolean
      - `can_approve` — boolean
      - `can_reject` — boolean
      - `can_execute` — boolean
      - `can_cancel_vote` — boolean
      - `close` — object
      - `note` — string | null
    - `done[]` — object[]
      - `transaction_index` — integer
      - `kind` — "vault_transaction" | "batch" | "config_transaction" | "unknown"
      - `state` — "created" | "draft" | "active" | "rejected" | "approved" | "executing" | "executed" | "cancelled"
      - `stale` — boolean
      - `vault_index` — integer | null
      - `approved[]` — string[]
      - `rejected[]` — string[]
      - `cancelled[]` — string[]
      - `approvals_needed` — integer | null
      - `rejections_needed` — integer | null
      - `status_timestamp` — integer | null
      - `execute_after` — integer | null
      - `time_lock_remaining_seconds` — integer | null
      - `effect_summary` — string | null
      - `decode_blocking` — boolean
      - `amount_binding` — "unverified" | "not_applicable"
      - `can_propose` — boolean
      - `can_activate` — boolean
      - `can_approve` — boolean
      - `can_reject` — boolean
      - `can_execute` — boolean
      - `can_cancel_vote` — boolean
      - `close` — object
      - `note` — string | null
  - `scanned` — integer
  - `truncated` — boolean
  - `observed_slot` — integer

### `GET /v1/wallet-os/actions/{multisig_id}/{transaction_index}` — Get one multisig action (full live decode)
- Action: `read` · MCP tool: `wallet-os.get_action`
- Reads the config, transaction, and proposal accounts for one transaction index in a single finalized batch and returns the full detail: proposal state and votes, threshold/time-lock/staleness, the canonical structured effect (named per-instruction decode with a blocking status for known programs that fail their layout) and its digest, the raw stored vault message (program ids, account metas, instruction data as hex), the action digest (SHA-256 over the exact stored message bytes for vault transactions, the serialized action vec for config transactions), the ordered config change rows with live before/after and stale impact while still pending, and close eligibility per the rent close matrix. Batch actions carry the batch fields only at M0. amount_binding marks a stored confidential transfer "unverified" — the hidden amount cannot be verified on this lane, so a direct approval carries no amount-binding record.
- Path param `multisig_id` (string, required) — Imported multisig id (msig_…)
- Path param `transaction_index` (string, required) — Base-10 transaction index within the multisig
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `multisig_id` — string
  - `config_address` — string
  - `transaction_index` — integer
  - `transaction_address` — string
  - `proposal_address` — string
  - `note` — string | null
  - `kind` — "vault_transaction" | "batch" | "config_transaction" | "unknown"
  - `state` — "draft" | "active" | "rejected" | "approved" | "executing" | "executed" | "cancelled" | null
  - `approved[]` — string[]
  - `rejected[]` — string[]
  - `cancelled[]` — string[]
  - `status_timestamp` — integer | null
  - `threshold` — integer
  - `time_lock_seconds` — integer
  - `stale` — boolean
  - `stale_transaction_index` — integer
  - `execute_after` — integer | null
  - `time_lock_remaining_seconds` — integer | null
  - `vault_index` — integer | null
  - `vault_address` — string | null
  - `message` — object | null
    - `num_signers` — integer
    - `num_writable_signers` — integer
    - `num_writable_non_signers` — integer
    - `account_keys[]` — object[]
      - `address` — string
      - `signer` — boolean
      - `writable` — boolean
    - `instructions[]` — object[]
      - `program_id_index` — integer
      - `program_id` — string | null
      - `account_indexes[]` — integer[]
      - `account_addresses[]` — string | null[]
      - `data_hex` — string
    - `address_table_lookups[]` — object[]
      - `account_key` — string
      - `writable_indexes[]` — integer[]
      - `readonly_indexes[]` — integer[]
  - `digest` — string | null
  - `effect` — object | null
    - `version` — number
    - `kind` — "vault_transaction" | "config_transaction"
    - `instructions[]` — object[]
      - `index` — integer
      - `program_id` — string | null
      - `program_name` — string | null
      - `instruction` — string | null
      - `status` — "decoded" | "decode_error" | "unknown_program"
      - `summary` — string
      - `fields[]` — object[]
      - `error` — string | null
    - `config_actions[]` — object[] | null
      - `index` — integer
      - `action` — "add_member" | "remove_member" | "change_threshold" | "set_time_lock" | "add_spending_limit" | "remove_spending_limit" | "set_rent_collector"
      - `field` — "member" | "threshold" | "time_lock" | "rent_collector" | "spending_limit"
      - `subject` — string | null
      - `proposed` — string
      - `mask` — integer | null
      - `summary` — string
    - `summary` — string
    - `decoded` — integer
    - `total` — integer
    - `blocking` — boolean
    - `has_unknown_programs` — boolean
  - `effect_digest` — string | null
  - `amount_binding` — "unverified" | "not_applicable"
  - `config_review` — object | null
    - `base` — string
    - `rows[]` — object[]
      - `index` — integer
      - `field` — string
      - `subject` — string | null
      - `before` — string
      - `after` — string
  - `stale_impact` — object | null
    - `stale_transaction_index_after_execute` — integer
    - `voided[]` — object[]
      - `transaction_index` — integer
      - `state` — string
  - `close` — object
    - `eligible` — boolean
    - `reason` — string | null
  - `observed_slot` — integer

### `POST /v1/wallet-os/multisigs/create` — Prepare a multisig creation
- Action: `write` · MCP tool: `wallet-os.create_multisig`
- Builds the create instruction for a new autonomous multisig (members sorted, role coverage and threshold validated). Devnet creates target the k256 kernel (no fee); mainnet stays on the v4 program (creation fee + treasury read live from the program config). Returns the unsigned transaction plus the derived config and vault addresses. The caller signs with the creator AND the create key, submits via wallet-os.submit_transaction, then records it with wallet-os.import_multisig.
- Request body (JSON, required):
  - `cluster` — string (required) — mainnet, devnet, or testnet
  - `program` — "v4" | "kernel" — Target program: kernel (default on devnet) or v4 (default on mainnet — the kernel is devnet-only until M4)
  - `create_key` — string (required) — Caller-generated keypair pubkey seeding the config PDA — it must sign the transaction. Keep the keypair.
  - `creator` — string (required) — Fee + rent payer; signs the transaction
  - `members` — object[] (required) — Sorted by the op; must cover Initiate, Vote, and Execute roles
  - `threshold` — integer (required) — Approvals required; at most the number of Vote members
  - `time_lock_seconds` — integer (required) — Seconds between approval and execution (0-7776000)
  - `rent_collector` — string | null — Solana address (base58)
  - `memo` — string
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `program_id` — string — The program the multisig is created on (v4 or the k256 kernel)
  - `create_key` — string
  - `config_address` — string
  - `vault_address` — string
  - `creation_fee_lamports` — integer
  - `treasury` — string | null
  - `summary` — string

### `POST /v1/wallet-os/actions/{multisig_id}/{transaction_index}/note` — Append a correction to an action's reason
- Action: `write` · MCP tool: `wallet-os.set_action_note`
- Appends a revision to the landed action's reason, shown to every approver in the queue and on the action itself. Workspace context only: stored off chain, encrypted at rest, and editing it never changes the digest an approval binds to. Append-only — only the reason's author may correct it, prior revisions stay inspectable via wallet-os.get_action_reason_history, and pass expected_version so concurrent corrections are refused instead of silently lost. For a NEW proposal, save the reason before signing with wallet-os.save_action_reason and bind it after landing with wallet-os.bind_action_reason.
- Path param `multisig_id` (string, required)
- Path param `transaction_index` (string, required)
- Request body (JSON, required):
  - `note` — string (required) — Why this action was proposed — shown to every approver
  - `expected_version` — integer — Optimistic lock: the reason version this correction is based on — a mismatch is refused, never silently overwritten
- Response (JSON):
  - `multisig_id` — string
  - `transaction_index` — integer
  - `note` — string
  - `version` — integer
  - `commitment` — string
  - `updated_at` — integer

### `POST /v1/wallet-os/actions/{multisig_id}/reason-drafts` — Save a proposal's reason before anything is signed
- Action: `write` · MCP tool: `wallet-os.save_action_reason`
- Persists the proposer's reason durably BEFORE the wallet opens, keyed by a client draft_id so retries land on one record. The draft claims no action identity: it is invisible to every read surface until wallet-os.bind_action_reason ties it to the LANDED transaction index, so a rejected wallet leaves only this recoverable draft — never a note attached to an index something else may take. Re-saving an unbound draft replaces its working copy.
- Path param `multisig_id` (string, required)
- Request body (JSON, required):
  - `draft_id` — string (required) — Client idempotency key for this proposal attempt — reuse it on every retry
  - `reason` — string (required) — Why this action is being proposed
  - `digest` — string — The prepared payload digest the reason describes (audit linkage)
- Response (JSON):
  - `multisig_id` — string
  - `draft_id` — string
  - `version` — integer
  - `commitment` — string
  - `saved_at` — integer

### `POST /v1/wallet-os/actions/{multisig_id}/reason-drafts/{draft_id}/bind` — Bind a saved reason to the landed action
- Action: `write` · MCP tool: `wallet-os.bind_action_reason`
- Idempotently ties a saved reason draft to the transaction index the action actually landed at — the only moment that identity is safe to claim. Binding the same draft to the same index again reports success (already_bound), so a client that is unsure whether its first attempt arrived simply retries.
- Path param `multisig_id` (string, required)
- Path param `draft_id` (string, required)
- Request body (JSON, required):
  - `transaction_index` — string (required)
- Response (JSON):
  - `multisig_id` — string
  - `draft_id` — string
  - `transaction_index` — integer
  - `version` — integer
  - `commitment` — string | null
  - `already_bound` — boolean

### `GET /v1/wallet-os/actions/{multisig_id}/{transaction_index}/reason-history` — The append-only history of an action's reason
- Action: `read` · MCP tool: `wallet-os.get_action_reason_history`
- Every revision of the reason with author, time, character count, and the commitment chain (SHA-256 over each revision's AEAD ciphertext, linked by prior_commitment). A redacted revision renders `[redacted]` with its receipt — who removed it, when, and why — while its commitment and metadata remain as evidence.
- Path param `multisig_id` (string, required)
- Path param `transaction_index` (string, required)
- Response (JSON):
  - `multisig_id` — string
  - `transaction_index` — integer
  - `revisions[]` — object[]
    - `version` — integer
    - `author` — string
    - `created_at` — integer
    - `note` — string
    - `char_length` — integer
    - `commitment` — string
    - `prior_commitment` — string | null
    - `redacted` — boolean
    - `redacted_at` — integer | null
    - `redacted_by` — string | null
    - `redact_reason` — string | null

### `POST /v1/wallet-os/actions/{multisig_id}/{transaction_index}/reason/redact` — Redact one reason revision (org admin)
- Action: `write` · MCP tool: `wallet-os.redact_action_reason`
- Provable removal, never a rewrite: destroys the revision's wrapped data-encryption key — the only material that can decrypt it — and records who redacted it, when, and why. The revision row, its commitment, and the history chain remain; every read surface serves `[redacted]` from then on. Org admins only.
- Path param `multisig_id` (string, required)
- Path param `transaction_index` (string, required)
- Request body (JSON, required):
  - `version` — integer (required) — The revision to redact
  - `reason` — string (required) — Why this text is being removed — kept in the receipt
- Response (JSON):
  - `multisig_id` — string
  - `transaction_index` — integer
  - `version` — integer
  - `commitment` — string
  - `redacted_at` — integer | null
  - `redacted_by` — string | null
  - `redact_reason` — string | null

### `POST /v1/wallet-os/config-changes/prepare` — Prepare a governed config change
- Action: `write` · MCP tool: `wallet-os.prepare_config_change`
- Builds a config transaction for member add/remove, threshold, time lock, or rent collector changes, with the stale-impact preview: the live proposals the change voids when it executes (the stale boundary advances to the current transaction index). The digest is SHA-256 over the canonical action bytes stored on-chain. Propose, vote, and execute it with the proposal lifecycle ops.

The actions are applied IN ORDER onto the live config and the invariants are checked once, on the result — the same way the program does it. Two consequences worth planning around: (1) changing what a member may do is remove_member + add_member on the SAME address in one call (there is no set-permissions action, and re-adding an address you removed in this same call is expected, not a duplicate); (2) an end state the program would reject is refused here rather than at execute — so removing voters and lowering the threshold belong in one call, and the member set must still cover Initiate, Vote, and Execute when the dust settles.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `creator` — string (required) — Member with the Initiate permission; signs
  - `actions` — object | object | object | object | object[] (required)
  - `memo` — string
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `transaction_address` — string
  - `digest` — string
  - `changes[]` — object[]
    - `field` — "member" | "threshold" | "time_lock_seconds" | "rent_collector"
    - `subject` — string | null
    - `before` — string | null
    - `after` — string | null
    - `effect` — string
  - `stale_preview` — object
    - `stale_transaction_index_after_execute` — integer
    - `voided[]` — object[]
      - `transaction_index` — integer
      - `state` — "draft" | "active" | "rejected" | "approved" | "executing" | "executed" | "cancelled" | string
  - `summary` — string

### `POST /v1/wallet-os/actions/prepare` — Prepare an action from a typed intent
- Action: `write` · MCP tool: `wallet-os.prepare_action`
- The intent engine: move assets (SOL/SPL), change config, set a spending limit, or a custom raw instruction — one mechanism, four templates. Returns the canonical payload, the exact digest (SHA-256 over the stored bytes, identical to the on-chain event digest after execution), plain-language effects with decode coverage, itemized rent/fee cost, an automated-steps report, and an artifact hash for resume. Oversized messages route through the staging buffer automatically, with the full step plan. Confidential transports (browser_handoff / sealed_artifact / hosted_plaintext) route through the fail-closed top-level union (PRD §10.1.2) and default-deny until their package graduates; every existing non-confidential request returns the exact current object.
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `kind` — "vault_transaction" | "config_transaction" | "vault_transaction_staged"
  - `transaction_index` — integer
  - `transaction_address` — string
  - `digest` — string
  - `artifact_hash` — string
  - `effects[]` — object[]
    - `summary` — string
    - `decoded` — boolean
  - `decode_coverage` — object
    - `decoded` — integer
    - `total` — integer
    - `line` — string
  - `cost` — object
    - `items[]` — object[]
      - `label` — string
      - `lamports` — integer
    - `total_lamports_estimate` — integer
  - `automated_steps[]` — string[]
  - `state_binding` — object
    - `transaction_index` — integer
    - `stale_transaction_index` — integer
    - `threshold` — integer
    - `programdata_len` — integer
    - `upgrade_authority` — string
  - `plan` — object
    - `kind` — string
    - `buffer_address` — string
    - `buffer_index` — integer
    - `steps[]` — object[]
      - `op` — string
      - `note` — string
  - `stale_preview` — object
    - `stale_transaction_index_after_execute` — integer
    - `voided[]` — object[]
      - `transaction_index` — integer
      - `state` — string
  - `spending_limit_accounts[]` — string[]
  - `policy_flags[]` — object[]
    - `policy_id` — string
    - `policy_name` — string
    - `kind` — string
    - `plane` — string
    - `flag` — string
    - `message` — string
  - `summary` — string

### `GET /v1/wallet-os/actions/outcome-handoff` — Read a confidential handoff's public binding and one-use continuation token
- Action: `read` · MCP tool: `wallet-os.get_outcome_handoff`
- Returns the handoff's public binding, the deterministically reproduced signed continuation token, and the server-offered request key for the sealed call (PRD §10.4). Token and key are not authority: the sealed transition rechecks authorization and every payload/row field. They bind the current outcome_version, repeat identically until consumption/expiry, then disappear.
- Query param `outcome_id` (string, required)
- Query param `handoff_id` (string, required)
- Response (JSON):
  - `status` — string
  - `outcome_id` — string
  - `attempt_id` — string
  - `outcome_version` — integer
  - `platform_visibility` — "client_sealed" | "hosted_plaintext"
  - `handoff` — object
    - `id` — string
    - `request_id` — string
    - `public_intent_hash` — string
    - `expires_at` — integer
    - `state_binding` — string
    - `required_private_steps[]` — "unlock_privacy_key" | "verify_balance" | "generate_proofs" | "create_review_envelopes"[]
  - `request_key` — string
  - `continuation` — object
    - `outcome_id` — string
    - `attempt_id` — string
    - `handoff_id` — string
    - `request_id` — string
    - `continuation_token` — string

### `POST /v1/wallet-os/actions/continue` — Continue an outcome through one exact transition
- Action: `write` · MCP tool: `wallet-os.continue_action`
- The strict transition union for outcome-bound work (PRD §10.4): refresh_envelope, submit_signed_candidate, continue_exact_suffix, review_rebuilt_suffix, rebuild_handoff, and close_incomplete. Every variant requires request_key, outcome/attempt identity, its transition's server token, and the expected outcome_version; authorization reruns on every call. close_incomplete is fully live: it rechecks the write permission, the expected version, the one-use resolution token, and the original actor's live Initiate seat, then closes no-effect (replacement window or terminal) or partial-effect with an immutable receipt — never touching chain state. The construction-bearing transitions validate identity/token and default-deny until WP4 graduates confidential transfer construction.
- Response (JSON):
  - `result_kind` — string
  - `outcome_id` — string
  - `attempt_id` — string
  - `outcome_version` — integer
  - `platform_visibility` — "client_sealed" | "hosted_plaintext"
  - `confidential_context` — object
    - `public_effects[]` — object[]
      - `kind` — string
      - `summary` — string
      - `amount` — string | null
    - `privacy` — object
      - `platform_visibility` — "client_sealed" | "hosted_plaintext"
      - `disclosures[]` — string[]
      - `private_receipt_available` — boolean
    - `sealed_artifact` — object | null
      - `version` — number
      - `content_hash` — string
      - `consumed` — boolean
    - `proof_plan` — object | null
      - `steps[]` — object[]
    - `resume_id` — string | null
  - `receipt` — object | null
    - `receipt_core_version` — number
    - `terminal_state` — "settled" | "closed_no_effect" | "closed_partial_effect"
    - `action_digest` — string | null
    - `effect_digest` — string | null
    - `source` — string | null — Solana address (base58)
    - `destination` — string | null — Solana address (base58)
    - `mint` — string | null — Solana address (base58)
    - `privacy_level` — "client_sealed" | "hosted_plaintext"
    - `public_facts[]` — string[]
    - `auditor_key` — string | null — Solana address (base58)
    - `proof_kinds[]` — string[]
    - `proof_contexts[]` — string[]
    - `signatures[]` — string[]
    - `slot` — integer | null
    - `execution_state` — string
    - `rent_lamports` — object | null
      - `temporary` — integer
      - `recovered` — integer
      - `destination` — string | null — Solana address (base58)
    - `state_binding_hashes[]` — string[]
    - `observed_slots[]` — integer[]
    - `runtime_versions` — object
      - `program` — string
      - `feature_set` — integer | null
    - `observed_effects[]` — string[]
    - `unmet_effects[]` — string[]
    - `rolled_back` — boolean
  - `next_action` — object | object | object | object | null
    - `kind` — string
    - `operation_id` — string
    - `input` — any
  - `plan_state` — string
  - `status` — "prepared" | "in_progress" | "submission_uncertain" | "partially_effected" | "resolution_required" | "replacement_allowed" | "settled" | "closed_no_effect" | "closed_partial_effect"
  - `plan` — object
    - `plan_id` — string
    - `revision` — integer
    - `plan_digest` — string
    - `actions[]` — object | object[]
      - `sequence` — integer
      - `purpose` — string
      - `binding_state` — string
      - `action_digest` — string
      - `bound_transaction_address` — string | null — Solana address (base58)
      - `state_binding_hash` — string
      - `state` — string
    - `plan_status` — string
    - `current_step_id` — string | null
    - `steps[]` — object[]
      - `id` — string
      - `action_sequence` — integer | null
      - `kind` — string
      - `logical_step_hash` — string
      - `message_template_hash` — string | null
      - `envelope` — any | null
      - `required_signers[]` — string[]
      - `expected_effect` — any | null
      - `retry_class` — string
      - `cleanup_obligation` — any | null
      - `state` — string

### `GET /v1/wallet-os/actions/outcome` — Read the canonical state of one outcome
- Action: `read` · MCP tool: `wallet-os.resolve_outcome`
- The Reconciliation Resolver read (PRD §10.6): one response shape for every timeout, reload, or agent retry. Read-only and always available; org-bound. While the outcome is submission_uncertain this is the ONLY public next action — the reconciler owns progress, and safe_to_retry is true only with positive no-effect proof (replacement_allowed).
- Query param `outcome_id` (string, required)
- Response (JSON):
  - `outcome_id` — string
  - `attempt_id` — string
  - `outcome_version` — integer
  - `state` — "open" | "submission_uncertain" | "partially_effected" | "resolution_required" | "replacement_allowed" | "settled" | "closed_no_effect" | "closed_partial_effect"
  - `platform_visibility` — "client_sealed" | "hosted_plaintext"
  - `safe_to_retry` — boolean
  - `canonical_actions[]` — object[]
    - `sequence` — integer
    - `purpose` — string
    - `address` — string | null — Solana address (base58)
    - `digest` — string | null
    - `state` — string
  - `blocking_action_sequence` — integer | null
  - `observations` — object
    - `candidate_signatures[]` — object[]
      - `signature` — string
      - `status` — string
    - `public_effects[]` — object[]
      - `kind` — string
      - `summary` — string
      - `amount` — string | null
    - `last_checked_slot` — integer | null
  - `next_action` — object | object | object | object | null
    - `kind` — string
    - `operation_id` — string
    - `input` — any

### `POST /v1/wallet-os/actions/replace-attempt` — Replace a proven-no-effect attempt with a fresh one
- Action: `write` · MCP tool: `wallet-os.replace_attempt`
- Only from replacement_allowed (PRD §10.3.1 rule 7): locks the outcome, checks the expected outcome_version, revalidates that the prior attempt is already closed_no_effect with no bound plan and no active attempt, then atomically installs attempt sequence+1 with a fresh handoff, its own request-key idempotency, and the outbox record — on the SAME outcome, preserving monotonic visibility. A replacement of one proven-no-effect attempt, never a second live payment.
- Request body (JSON, required):
  - `outcome_id` — string (required)
  - `attempt_id` — string (required)
  - `expected_outcome_version` — integer (required)
  - `request_key` — string (required)
  - `intent` — object | object | object | object | object | object | object | object | object | object | object | object | object | object | object | object | object | object | object | object | object | object | object | object (required)
- Response (JSON):
  - `result_kind` — string
  - `status` — string
  - `outcome_id` — string
  - `attempt_id` — string
  - `outcome_version` — integer
  - `platform_visibility` — "client_sealed" | "hosted_plaintext"
  - `handoff` — object
    - `id` — string
    - `request_id` — string
    - `public_intent_hash` — string
    - `expires_at` — integer
    - `state_binding` — string
    - `required_private_steps[]` — "unlock_privacy_key" | "verify_balance" | "generate_proofs" | "create_review_envelopes"[]
  - `next_action` — object | object | object | object
    - `kind` — string
    - `operation_id` — string
    - `input` — any

### `POST /v1/wallet-os/privacy/challenges` — Issue a one-use privacy enrollment, rotation, fetch, or attestation challenge
- Action: `write` · MCP tool: `wallet-os.issue_recovery_challenge`
- Mints the one-use nonce a privacy-identity key holder signs over (PRD §9.7): the issued purpose-specific fields are the bound tuple the canonical signature bytes commit to, so the consuming ops take only the challenge and the required proofs and nothing can be substituted after issuance. All four §10.4 purposes are live: enroll_identity, rotate_identity, package_fetch (C2), and recovery_attestation (C3 — the bound identity needs an active grant on the package's account, or the caller must be its uploader).
- Response (JSON):
  - `challenge_id` — string
  - `purpose` — "enroll_identity" | "rotate_identity" | "recovery_attestation" | "package_fetch"
  - `nonce` — string
  - `expires_at` — integer
  - `created_at` — integer
  - `binding` — object | object | object | object
    - `kind` — "invitation" | "disclosure" | "recovery_custodian"
    - `public_key` — string
    - `device_label` — string

### `GET /v1/wallet-os/privacy/identities` — List privacy identity lifecycle state
- Action: `read` · MCP tool: `wallet-os.list_privacy_identities`
- The public enrollment lifecycle — kind, state, public key, rotation counter — filtered by kind/state, org-scoped (PRD §10.4). Public enrollments only; no private key material exists at this layer.
- Query param `kind` ("invitation" | "disclosure" | "recovery_custodian")
- Query param `state` ("active" | "revoked")
- Response (JSON):
  - `identities[]` — object[]
    - `identity_id` — string
    - `kind` — "invitation" | "disclosure" | "recovery_custodian"
    - `state` — "active" | "revoked"
    - `public_key` — string
    - `device_label` — string
    - `rotation_counter` — integer
    - `enrolled_by` — string
    - `enrolled_at` — integer
    - `updated_at` — integer
    - `revoked_at` — integer | null

### `POST /v1/wallet-os/privacy/identities` — Enroll a privacy identity against a verified challenge signature
- Action: `write` · MCP tool: `wallet-os.enroll_privacy_identity`
- Verifies the one-use challenge (purpose, unconsumed, unexpired) and the ed25519 signature over the canonical enrollment bytes against the challenge-bound public key, consumes the challenge, and stores the PUBLIC enrollment (invitation, disclosure, or recovery_custodian) at rotation counter 0 (PRD §§9.7/9.8). One active identity per key; grants no on-chain authority.
- Request body (JSON, required):
  - `challenge_id` — string (required)
  - `signature` — string (required) — The 64-byte ed25519 signature over the canonical challenge bytes, base58-encoded
- Response (JSON):
  - `identity` — object
    - `identity_id` — string
    - `kind` — "invitation" | "disclosure" | "recovery_custodian"
    - `state` — "active" | "revoked"
    - `public_key` — string
    - `device_label` — string
    - `rotation_counter` — integer
    - `enrolled_by` — string
    - `enrolled_at` — integer
    - `updated_at` — integer
    - `revoked_at` — integer | null

### `POST /v1/wallet-os/privacy/identities/rotate` — Rotate a privacy identity to a new key
- Action: `write` · MCP tool: `wallet-os.rotate_privacy_identity`
- Verifies the rotation challenge and TWO proofs (PRD §§9.7/10.4): the NEW key's signature over the canonical bytes at the expected next counter, plus either the CURRENT key's authorization of the same tuple (a role-tagged signature, so the proofs cannot be swapped) or an explicit active recovery-custodian restore_package grant. Then compare-and-set swaps the key of record and advances the monotonic counter in one write — the old key stops being valid for future proofs the moment the rotation commits.
- Request body (JSON, required):
  - `challenge_id` — string (required)
  - `signature` — string (required) — The 64-byte ed25519 signature over the canonical challenge bytes, base58-encoded
  - `current_key_signature` — string — The 64-byte ed25519 signature over the canonical challenge bytes, base58-encoded
- Response (JSON):
  - `identity` — object
    - `identity_id` — string
    - `kind` — "invitation" | "disclosure" | "recovery_custodian"
    - `state` — "active" | "revoked"
    - `public_key` — string
    - `device_label` — string
    - `rotation_counter` — integer
    - `enrolled_by` — string
    - `enrolled_at` — integer
    - `updated_at` — integer
    - `revoked_at` — integer | null

### `POST /v1/wallet-os/privacy/identities/revoke` — Revoke a privacy identity
- Action: `write` · MCP tool: `wallet-os.revoke_privacy_identity`
- Revokes the enrollment under compare-and-set on the rotation counter (a revocation decided before a rotation cannot land after it), and in the SAME transition revokes every active grant the identity principals — new use stops immediately (PRD §§9.8/10.4). Idempotent: revoking an already-revoked identity with a matching counter returns its revoked state. Already-copied keys or plaintext cannot be recalled.
- Request body (JSON, required):
  - `identity_id` — string (required)
  - `expected_rotation_counter` — integer (required)
- Response (JSON):
  - `identity` — object
    - `identity_id` — string
    - `kind` — "invitation" | "disclosure" | "recovery_custodian"
    - `state` — "active" | "revoked"
    - `public_key` — string
    - `device_label` — string
    - `rotation_counter` — integer
    - `enrolled_by` — string
    - `enrolled_at` — integer
    - `updated_at` — integer
    - `revoked_at` — integer | null

### `GET /v1/wallet-os/privacy/grants` — List redacted privacy grant records
- Action: `read` · MCP tool: `wallet-os.list_privacy_grants`
- The org's privacy grants filtered by capability, principal identity, or exact resource (PRD §10.4). These ARE the records — no ciphertext exists at this layer, so the full grant row (principal, resource, capability, expiry, version, policy digest) is the response.
- Query param `capability` ("view_balance" | "generate_proof" | "review_amount" | "restore_package" | "read_private_receipt" | "create_disclosure")
- Query param `principal_identity_id` (string)
- Query param `resource` (object | object | object)
- Response (JSON):
  - `grants[]` — object[]
    - `grant_id` — string
    - `principal` — object | object
      - `kind` — string
      - `custodian_id` — string
    - `resource` — object | object | object
      - `kind` — string
      - `cluster` — "mainnet" | "devnet" | "testnet"
      - `account` — string — Solana address (base58)
    - `capability` — "view_balance" | "generate_proof" | "review_amount" | "restore_package" | "read_private_receipt" | "create_disclosure"
    - `issued_at` — integer
    - `expires_at` — integer | null
    - `revoked_at` — integer | null
    - `version` — integer
    - `policy_digest` — string
    - `request_id` — string
    - `request_digest` — string
    - `created_at` — integer
    - `updated_at` — integer

### `POST /v1/wallet-os/privacy/grants` — Grant privacy access to an enrolled principal
- Action: `write` · MCP tool: `wallet-os.grant_privacy_access`
- Stores the explicit, auditable privacy grant (PRD §9.8): an exact principal (an active same-org enrolled recovery-custodian or disclosure identity), resource, capability, and optional expiry, plus the policy digest binding that tuple. The grant authorizes DELIVERY of a client-encrypted wrap — it never gives the server plaintext or signing power. Idempotent by request_id: an identical replay returns the recorded grant; a different tuple under the same request_id is a 409.
- Request body (JSON, required):
  - `principal` — object | object (required)
  - `resource` — object | object | object (required)
  - `capability` — "view_balance" | "generate_proof" | "review_amount" | "restore_package" | "read_private_receipt" | "create_disclosure" (required)
  - `expires_at` — integer
  - `request_id` — string (required)
- Response (JSON):
  - `grant` — object
    - `grant_id` — string
    - `principal` — object | object
      - `kind` — string
      - `custodian_id` — string
    - `resource` — object | object | object
      - `kind` — string
      - `cluster` — "mainnet" | "devnet" | "testnet"
      - `account` — string — Solana address (base58)
    - `capability` — "view_balance" | "generate_proof" | "review_amount" | "restore_package" | "read_private_receipt" | "create_disclosure"
    - `issued_at` — integer
    - `expires_at` — integer | null
    - `revoked_at` — integer | null
    - `version` — integer
    - `policy_digest` — string
    - `request_id` — string
    - `request_digest` — string
    - `created_at` — integer
    - `updated_at` — integer

### `POST /v1/wallet-os/privacy/grants/update` — Update a privacy grant's expiry
- Action: `write` · MCP tool: `wallet-os.update_privacy_grant`
- Compare-and-set on the grant version (PRD §10.4): sets the new expiry (null clears it), advances the version, and recomputes the policy digest over the new tuple. A revoked grant is terminal for updates.
- Request body (JSON, required):
  - `grant_id` — string (required)
  - `expected_version` — integer (required)
  - `expires_at` — integer | null (required)
- Response (JSON):
  - `grant` — object
    - `grant_id` — string
    - `principal` — object | object
      - `kind` — string
      - `custodian_id` — string
    - `resource` — object | object | object
      - `kind` — string
      - `cluster` — "mainnet" | "devnet" | "testnet"
      - `account` — string — Solana address (base58)
    - `capability` — "view_balance" | "generate_proof" | "review_amount" | "restore_package" | "read_private_receipt" | "create_disclosure"
    - `issued_at` — integer
    - `expires_at` — integer | null
    - `revoked_at` — integer | null
    - `version` — integer
    - `policy_digest` — string
    - `request_id` — string
    - `request_digest` — string
    - `created_at` — integer
    - `updated_at` — integer

### `POST /v1/wallet-os/privacy/grants/revoke` — Revoke a privacy grant
- Action: `write` · MCP tool: `wallet-os.revoke_privacy_grant`
- Revokes the grant under compare-and-set: future fetch/wrap delivery under it stops (PRD §10.4). Idempotent — revoking an already-revoked grant returns its revoked state. Already delivered or copied material cannot be recalled.
- Request body (JSON, required):
  - `grant_id` — string (required)
  - `expected_version` — integer (required)
- Response (JSON):
  - `grant` — object
    - `grant_id` — string
    - `principal` — object | object
      - `kind` — string
      - `custodian_id` — string
    - `resource` — object | object | object
      - `kind` — string
      - `cluster` — "mainnet" | "devnet" | "testnet"
      - `account` — string — Solana address (base58)
    - `capability` — "view_balance" | "generate_proof" | "review_amount" | "restore_package" | "read_private_receipt" | "create_disclosure"
    - `issued_at` — integer
    - `expires_at` — integer | null
    - `revoked_at` — integer | null
    - `version` — integer
    - `policy_digest` — string
    - `request_id` — string
    - `request_digest` — string
    - `created_at` — integer
    - `updated_at` — integer

### `GET /v1/wallet-os/privacy/key-packages` — List hosted key-package versions (public headers and wrap availability)
- Action: `read` · MCP tool: `wallet-os.list_key_package_versions`
- The §10.4 list row: public headers, wrap availability (kind/identity/scheme — never wrap material), lifecycle state, and retention, filtered by cluster/token_account/package_id. Authorized in-service: the named identity must be active and hold an active view_balance or restore_package grant, and packages are visible only for accounts that identity is granted on. No ciphertext exists in this response.
- Query param `identity_id` (string, required)
- Query param `cluster` ("mainnet" | "devnet" | "testnet")
- Query param `token_account` (string) — Solana address (base58)
- Query param `package_id` (string)
- Response (JSON):
  - `packages[]` — object[]
    - `package_id` — string
    - `package_version` — integer
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `mint` — string
    - `token_account` — string
    - `vault_owner` — string
    - `elgamal_pubkey` — string
    - `header` — object
      - `suite_id` — string
      - `package_id` — string
      - `format_version` — integer
      - `cluster` — "mainnet" | "devnet" | "testnet"
      - `mint` — string — Solana address (base58)
      - `token_account` — string — Solana address (base58)
      - `vault_owner` — string — Solana address (base58)
      - `elgamal_pubkey` — string
      - `created_at` — integer
      - `package_version` — integer
      - `recovery_policy_version` — integer
      - `kdf_salt` — string
      - `package_nonce` — string
      - `kdf_params` — object | null
    - `content_hash` — string
    - `state` — "active" | "retired" | "deleted"
    - `retention_until` — integer
    - `created_by` — string
    - `created_at` — integer
    - `updated_at` — integer
    - `deleted_at` — integer | null
    - `wraps[]` — object[]
      - `wrap_index` — integer
      - `kind` — "device" | "custodian" | "passphrase"
      - `identity_id` — string | null
      - `scheme` — string
    - `assurance` — object
      - `preflight` — object | null
      - `full` — object | null

### `POST /v1/wallet-os/privacy/key-packages` — Store a privacy key-package ciphertext against a grant or the setup claim
- Action: `write` · MCP tool: `wallet-os.put_key_package_ciphertext`
- Validates the FROZEN v1 package structure exactly (PRD §9.6.1 — suite/format allowlist, Argon2id floor/ceiling, wrap-slot rules, content-hash parity), verifies the package_fetch challenge signature with the bound identity's current key, and authorizes in-service (§9.8): an active restore_package grant on the account, or the restricted setup claim when no package exists for the account yet. Versions are monotonic per package id; the content hash is the idempotency identity — an identical replay returns the stored row, the same hash under a different header tuple is a 409. The server validates structure only; plaintext key material never transits.
- Request body (JSON, required):
  - `header` — object (required)
  - `ciphertext` — string (required)
  - `wraps` — object[] (required)
  - `content_hash` — string (required)
  - `challenge_id` — string (required)
  - `signature` — string (required) — The 64-byte ed25519 signature over the canonical challenge bytes, base58-encoded
- Response (JSON):
  - `package` — object
    - `package_id` — string
    - `package_version` — integer
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `mint` — string
    - `token_account` — string
    - `vault_owner` — string
    - `elgamal_pubkey` — string
    - `header` — object
      - `suite_id` — string
      - `package_id` — string
      - `format_version` — integer
      - `cluster` — "mainnet" | "devnet" | "testnet"
      - `mint` — string — Solana address (base58)
      - `token_account` — string — Solana address (base58)
      - `vault_owner` — string — Solana address (base58)
      - `elgamal_pubkey` — string
      - `created_at` — integer
      - `package_version` — integer
      - `recovery_policy_version` — integer
      - `kdf_salt` — string
      - `package_nonce` — string
      - `kdf_params` — object | null
    - `content_hash` — string
    - `state` — "active" | "retired" | "deleted"
    - `retention_until` — integer
    - `created_by` — string
    - `created_at` — integer
    - `updated_at` — integer
    - `deleted_at` — integer | null

### `POST /v1/wallet-os/privacy/key-packages/fetch` — Fetch one package version's header, ciphertext, and caller wrap
- Action: `write` · MCP tool: `wallet-os.get_key_package_ciphertext`
- POST because it consumes a one-use package_fetch challenge (PRD §9.6.1 fetch flow). Verifies the challenge, the ed25519 signature over the canonical fetch bytes with the identity's CURRENT key, the identity's active resource-bound view_balance or restore_package grant, the package state and retention — then atomically consumes the nonce and returns the header, ciphertext, and ONLY the wrap addressed to the challenging identity; another principal's wrap is never returned.
- Request body (JSON, required):
  - `challenge_id` — string (required)
  - `signature` — string (required) — The 64-byte ed25519 signature over the canonical challenge bytes, base58-encoded
- Response (JSON):
  - `header` — object
    - `suite_id` — string
    - `package_id` — string
    - `format_version` — integer
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `mint` — string — Solana address (base58)
    - `token_account` — string — Solana address (base58)
    - `vault_owner` — string — Solana address (base58)
    - `elgamal_pubkey` — string
    - `created_at` — integer
    - `package_version` — integer
    - `recovery_policy_version` — integer
    - `kdf_salt` — string
    - `package_nonce` — string
    - `kdf_params` — object | null
      - `m` — integer
      - `t` — integer
      - `p` — integer
  - `ciphertext` — string
  - `content_hash` — string
  - `state` — "active" | "retired" | "deleted"
  - `retention_until` — integer
  - `wrap` — object
    - `kind` — "device" | "custodian" | "passphrase"
    - `identity_id` — string | null
    - `scheme` — string
    - `wrap_nonce` — string
    - `wrapped_dek` — string
    - `wrap_index` — integer

### `POST /v1/wallet-os/privacy/key-packages/delete` — Tombstone a hosted key-package version (ciphertext wiped, row retained)
- Action: `write` · MCP tool: `wallet-os.delete_key_package_ciphertext`
- The §10.4 delete row: the manage permission plus the custodian proof (a package_fetch challenge signed by an identity holding an active restore_package grant on the account), compare-and-set on expected_state. Refuses the sole active/retired-restorable version for the account and any version inside its retention window. The tombstone wipes ciphertext and wrap material and keeps the structural/hash columns for audit; fetched or exported copies cannot be revoked.
- Request body (JSON, required):
  - `package_id` — string (required)
  - `package_version` — integer (required)
  - `expected_state` — "active" | "retired" | "deleted" (required)
  - `challenge_id` — string (required)
  - `signature` — string (required) — The 64-byte ed25519 signature over the canonical challenge bytes, base58-encoded
- Response (JSON):
  - `package` — object
    - `package_id` — string
    - `package_version` — integer
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `mint` — string
    - `token_account` — string
    - `vault_owner` — string
    - `elgamal_pubkey` — string
    - `header` — object
      - `suite_id` — string
      - `package_id` — string
      - `format_version` — integer
      - `cluster` — "mainnet" | "devnet" | "testnet"
      - `mint` — string — Solana address (base58)
      - `token_account` — string — Solana address (base58)
      - `vault_owner` — string — Solana address (base58)
      - `elgamal_pubkey` — string
      - `created_at` — integer
      - `package_version` — integer
      - `recovery_policy_version` — integer
      - `kdf_salt` — string
      - `package_nonce` — string
      - `kdf_params` — object | null
    - `content_hash` — string
    - `state` — "active" | "retired" | "deleted"
    - `retention_until` — integer
    - `created_by` — string
    - `created_at` — integer
    - `updated_at` — integer
    - `deleted_at` — integer | null

### `POST /v1/wallet-os/privacy/recovery-assurance` — Attest a fresh-device package restore (preflight or full)
- Action: `write` · MCP tool: `wallet-os.attest_recovery`
- Verifies the one-use recovery_attestation challenge and every binding it carries, the ed25519 signature over the canonical attestation bytes with the identity's CURRENT key, the stored package header's account tuple and recovery-policy digest, the pinned tested_at+TTL expiry, and the kind's exact check set — a full attestation additionally re-reads the live token account on the attested cluster (mint and ElGamal pubkey must match, observed_slot must be fresh). The nonce is consumed atomically with the stored attestation; any previous valid assurance of the same kind is superseded. The attestation is non-secret by design (PRD §9.6.1): no balance, witness, or key ever transits. capture is skipped as with the C2 package ops — the body carries account bindings that Requests capture does not need.
- Request body (JSON, required):
  - `challenge_id` — string (required)
  - `attestation` — object (required)
  - `signature` — string (required) — The 64-byte ed25519 signature over the canonical challenge bytes, base58-encoded
- Response (JSON):
  - `assurance` — object
    - `assurance_id` — string
    - `package_content_hash` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `mint` — string
    - `token_account` — string
    - `elgamal_pubkey` — string
    - `device_identity_id` — string
    - `kind` — "preflight" | "full"
    - `checks[]` — "package_aead_opened" | "derived_pubkey_matches_account" | "aes_balance_matches_canonical_elgamal"[]
    - `observed_slot` — integer | null
    - `attestation_hash` — string
    - `tested_at` — integer
    - `expires_at` — integer
    - `state` — "valid" | "superseded" | "expired"
    - `created_at` — integer

### `POST /v1/wallet-os/privacy/recovery-diagnoses` — Begin a bound, expiring confidential-recovery diagnosis session
- Action: `write` · MCP tool: `wallet-os.begin_confidential_recovery`
- The gateway half of the PRD §9.6.2 flow: proves the account is a live confidential-transfer account on the named cluster (the mint comes from the same read), enforces the §10.4 authorization (an active view_balance/restore_package grant held by a privacy identity the caller enrolled, or the package-uploader lane — the hosted key is never the privacy authorization), and opens ONE bound, expiring diagnosis session per account carrying the user-level symptom answers and the current restorable-package binding. Re-beginning with the same answers adopts the active session; different answers are a typed conflict. The response carries the authenticated Wallet OS recovery navigation; the browser performs the secret diagnosis locally and submits the signed result. capture is skipped — the body binds an account the same way the C2 package ops do.
- Request body (JSON, required):
  - `cluster` — "mainnet" | "devnet" | "testnet" (required)
  - `account` — string (required) — Solana address (base58)
  - `symptom_answers` — "device_unlocks" | "other_device_restores" | "device_exposed" | "canonical_check_passes"[] (required)
- Response (JSON):
  - `session` — object
    - `diagnosis_id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `account` — string
    - `mint` — string
    - `symptom_answers[]` — "device_unlocks" | "other_device_restores" | "device_exposed" | "canonical_check_passes"[]
    - `status` — "open" | "diagnosed" | "expired"
    - `diagnosis_case` — "restore_access" | "cache_key_rotation" | "urgent_pause_migrate" | "review_device_replacement" | "terminal_unrecoverable" | null
    - `package_id` — string | null
    - `package_version` — integer | null
    - `created_at` — integer
    - `expires_at` — integer
  - `next_action` — object
    - `kind` — string
    - `route_id` — "wallet_os.pay" | "wallet_os.asset" | "wallet_os.outcome" | "wallet_os.settings"
    - `context` — object
      - `id` — string
    - `expires_at` — integer

### `POST /v1/wallet-os/privacy/recovery-diagnoses/record` — Record the signed recovery diagnosis for an open session
- Action: `write` · MCP tool: `wallet-os.submit_recovery_diagnosis`
- Records the browser's signed, expiring §9.6.2 diagnosis: exactly one of the five cases, boolean checks only, bound to the session's org/cluster/account/mint, the LIVE account state hash, and the CURRENT restorable package (a stale binding is diagnosis_state_drift, never a silent reroute). The ed25519 signature verifies over the canonical bytes domain k256.wallet-os.recovery-diagnosis.v1 against the named enrolled identity's CURRENT key; the identity must hold an active view_balance/restore_package grant on the account (or the uploader lane). The expiry is server-derived (exactly the session's), the record is a diagnose-once compare-and-set, and no balance, witness, or key ever transits. capture is skipped as with the C2/C3 privacy ops.
- Request body (JSON, required):
  - `diagnosis_id` — string (required)
  - `diagnosis` — object (required)
  - `signature` — string (required) — The 64-byte ed25519 signature over the canonical challenge bytes, base58-encoded
- Response (JSON):
  - `session` — object
    - `diagnosis_id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `account` — string
    - `mint` — string
    - `symptom_answers[]` — "device_unlocks" | "other_device_restores" | "device_exposed" | "canonical_check_passes"[]
    - `status` — "open" | "diagnosed" | "expired"
    - `diagnosis_case` — "restore_access" | "cache_key_rotation" | "urgent_pause_migrate" | "review_device_replacement" | "terminal_unrecoverable" | null
    - `package_id` — string | null
    - `package_version` — integer | null
    - `created_at` — integer
    - `expires_at` — integer
  - `canonical_hash` — string

### `GET /v1/wallet-os/privacy/receive-invitations` — Read a receive invitation's redacted state and web continuation
- Action: `read` · MCP tool: `wallet-os.get_receive_invitation`
- The claim state machine (PRD §10.4.2) with lazy expiry, plus the authenticated web continuation (the governed multisig's surface when the owner resolves to one). Same-org and a party to the invitation — the sender's or recipient's enrolling principal. Includes the allocated outcome id once configuring.
- Query param `invitation_id` (string, required)
- Response (JSON):
  - `invitation` — object
    - `invitation_id` — string
    - `purpose` — "configure_confidential_account" | "make_ready"
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `mint` — string
    - `intended_owner` — string
    - `recipient_identity_id` — string
    - `sender_identity_id` — string
    - `sender_signature` — string
    - `sender_rotation_counter` — integer
    - `state` — "issued" | "opened" | "owner_verified" | "configuring" | "issuer_approval_waiting" | "ready" | "expired" | "owner_mismatch" | "approval_impossible" | "revoked"
    - `version` — integer
    - `outcome_id` — string | null
    - `issued_at` — integer
    - `expires_at` — integer
    - `created_at` — integer
    - `updated_at` — integer
  - `continuation` — object
    - `multisig_id` — string | null
    - `link` — string | null

### `POST /v1/wallet-os/privacy/receive-invitations` — Create an amount-free receive invitation
- Action: `write` · MCP tool: `wallet-os.create_receive_invitation`
- Verifies both identities (active same-org; the sender is invitation-kind), the sender's ed25519 signature over the canonical amount-free invitation bytes (PRD §10.4.2), the issued_at freshness and expiry bound, and the mint's approval posture on a live chain read — a manual-approval mint with no live authority is refused upfront (§8.1). Amount and reason fields are rejected at the boundary by construction. Idempotent by request_id over the immutable original-tuple digest.
- Request body (JSON, required):
  - `request_id` — string (required)
  - `purpose` — "configure_confidential_account" | "make_ready" (required)
  - `cluster` — "mainnet" | "devnet" | "testnet" (required)
  - `mint` — string (required) — Solana address (base58)
  - `intended_owner` — string (required) — Solana address (base58)
  - `recipient_identity_id` — string (required)
  - `sender_identity_id` — string (required)
  - `issued_at` — integer (required)
  - `expires_at` — integer (required)
  - `signature` — string (required) — The 64-byte ed25519 signature over the canonical challenge bytes, base58-encoded
- Response (JSON):
  - `invitation` — object
    - `invitation_id` — string
    - `purpose` — "configure_confidential_account" | "make_ready"
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `mint` — string
    - `intended_owner` — string
    - `recipient_identity_id` — string
    - `sender_identity_id` — string
    - `sender_signature` — string
    - `sender_rotation_counter` — integer
    - `state` — "issued" | "opened" | "owner_verified" | "configuring" | "issuer_approval_waiting" | "ready" | "expired" | "owner_mismatch" | "approval_impossible" | "revoked"
    - `version` — integer
    - `outcome_id` — string | null
    - `issued_at` — integer
    - `expires_at` — integer
    - `created_at` — integer
    - `updated_at` — integer

### `POST /v1/wallet-os/privacy/receive-invitations/continue` — Drive a receive invitation's step state machine
- Action: `write` · MCP tool: `wallet-os.continue_receive_invitation`
- The resumable step surface (PRD §10.4.2): open (fresh chain read + one-use nonce), verify_owner (the PDA attestation arm — a mismatch is terminally owner_mismatch), configure (allocates the governed setup through the existing outcome spine), finalize (chain readiness + valid full Recovery Assurance → the signed Recipient Receive Capability, or the approval waiting/terminal states), and revoke (either party). Every transition is compare-and-set on the invitation version; the caller is the recipient's enrolling principal except revoke, which either party may drive.
- Response (JSON):
  - `invitation` — object
    - `invitation_id` — string
    - `purpose` — "configure_confidential_account" | "make_ready"
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `mint` — string
    - `intended_owner` — string
    - `recipient_identity_id` — string
    - `sender_identity_id` — string
    - `sender_signature` — string
    - `sender_rotation_counter` — integer
    - `state` — "issued" | "opened" | "owner_verified" | "configuring" | "issuer_approval_waiting" | "ready" | "expired" | "owner_mismatch" | "approval_impossible" | "revoked"
    - `version` — integer
    - `outcome_id` — string | null
    - `issued_at` — integer
    - `expires_at` — integer
    - `created_at` — integer
    - `updated_at` — integer
  - `step_nonce` — string | null
  - `chain` — object
    - `mint_confidential` — boolean
    - `auto_approve` — boolean
    - `approval_authority_live` — boolean
    - `owner_is_governed_vault` — boolean
    - `multisig_id` — string | null
    - `configured_accounts[]` — string[]
  - `multisig_id` — string | null
  - `outcome_id` — string | null
  - `attempt_id` — string | null
  - `handoff_id` — string | null
  - `capability` — object
    - `capability_id` — string
    - `invitation_id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `mint` — string
    - `token_account` — string
    - `elgamal_pubkey` — string
    - `allow_confidential_credits` — boolean
    - `allow_non_confidential_credits` — boolean
    - `pending_credit_counter` — string
    - `maximum_pending_credit_counter` — string
    - `assurance_id` — string
    - `assurance_expires_at` — integer
    - `version` — integer
    - `recipient_identity_id` — string
    - `recipient_rotation_counter` — integer
    - `signature` — string
    - `issued_at` — integer
    - `expires_at` — integer
    - `state` — "active" | "superseded" | "expired"
    - `created_at` — integer
  - `signing` — object
    - `token_account` — string
    - `elgamal_pubkey` — string
    - `allow_confidential_credits` — boolean
    - `allow_non_confidential_credits` — boolean
    - `pending_credit_counter` — string
    - `maximum_pending_credit_counter` — string
    - `assurance_id` — string
    - `assurance_expires_at` — integer
    - `version` — integer
    - `issued_at` — integer
    - `expires_at` — integer
    - `finalize_nonce` — string

### `GET /v1/wallet-os/privacy/receive-capability` — Read a Recipient Receive Capability with a live health verdict
- Action: `read` · MCP tool: `wallet-os.get_receive_capability`
- The signed readiness metadata (PRD §10.4.2: exact counters, assurance binding, expiry) — the capability id is lookup data, never authentication; the same-org relationship check still applies. ALWAYS re-reads: the returned `valid` is true only while the chain account is ready, the bound Recovery Assurance is valid, and the capability is unexpired — never a stale healthy display (§8.3).
- Query param `capability_id` (string, required)
- Response (JSON):
  - `capability` — object
    - `capability_id` — string
    - `invitation_id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `mint` — string
    - `token_account` — string
    - `elgamal_pubkey` — string
    - `allow_confidential_credits` — boolean
    - `allow_non_confidential_credits` — boolean
    - `pending_credit_counter` — string
    - `maximum_pending_credit_counter` — string
    - `assurance_id` — string
    - `assurance_expires_at` — integer
    - `version` — integer
    - `recipient_identity_id` — string
    - `recipient_rotation_counter` — integer
    - `signature` — string
    - `issued_at` — integer
    - `expires_at` — integer
    - `state` — "active" | "superseded" | "expired"
    - `created_at` — integer
  - `valid` — boolean
  - `chain_ready` — boolean
  - `assurance_valid` — boolean

### `POST /v1/wallet-os/actions/simulate` — Simulate a prepared action's governed effect against current state
- Action: `write` · MCP tool: `wallet-os.simulate_action`
- Independent simulation of any prepare output, targeted at what the multisig would EXECUTE: before create, the vault payload embedded in the prepared transaction runs against current chain state (its signatures satisfied only inside the simulation); at execute, the exact prepared execution envelope runs as-is; a prepared transaction that IS the protocol action (votes, closes, …) runs unchanged. Returns success or the verbatim program error (never rendered as success), logs, compute units, a lamport/data effect diff per declared account, and a freshness binding (governed digest, observed slot, substituted blockhash, exact simulated message). A governed effect that cannot be judged — incomplete staged buffer, config change awaiting the lifecycle, RPC overload — is a typed `unavailable` result, never a fabricated verdict. When the prepare's state binding is passed back, reports drift (index, stale boundary, or threshold moved since preparation).
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `transaction_base64` — string (required) — The unsigned transaction from any prepare op (transaction_base64)
  - `state_binding` — object
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `ok` — boolean
  - `err` — any | null
  - `logs[]` — string[]
  - `units_consumed` — integer | null
  - `drift` — object
    - `computed` — boolean
    - `drifted` — boolean
    - `reasons[]` — string[]
  - `effects[]` — object[]
    - `address` — string
    - `lamports_before` — integer | null
    - `lamports_after` — integer | null
    - `data_changed` — boolean | null
  - `status` — "simulated" | "unavailable"
  - `mode` — "governed_payload" | "execution_envelope" | "outer"
  - `unavailable` — object | null
    - `code` — string
    - `message` — string
    - `next_step` — string
  - `binding` — object
    - `digest` — string | null
    - `context_slot` — integer | null
    - `accounts_slot` — integer | null
    - `replacement_blockhash` — string | null
    - `simulated_message_base64` — string | null
    - `obtained_at` — string
  - `hypothetical_signers[]` — string[]
  - `summary` — string

### `POST /v1/wallet-os/actions/propose` — Prepare proposal creation for a transaction
- Action: `write` · MCP tool: `wallet-os.propose_action`
- Creates the proposal for an existing transaction (draft for later activation, or active immediately). Verifies the transaction exists, no proposal exists yet, and the index is not stale.

To find the transactions that need this: wallet-os.get_queue lists them with state `created` and can_propose for the given signer. A transaction with no proposal cannot be voted on or executed by anyone, so opening one is the only way it ever moves.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `transaction_index` — string (required)
  - `creator` — string (required) — Member with Initiate or Vote permission; signs
  - `draft` — boolean — true creates a Draft (activate_action later); false creates Active
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `proposal_address` — string
  - `summary` — string

### `POST /v1/wallet-os/actions/activate` — Prepare a Draft activation
- Action: `write` · MCP tool: `wallet-os.activate_action`
- Activates a Draft proposal (an Initiate-permission member; stale Drafts cannot activate). Activation is a distinct on-chain instruction — Drafts cannot receive votes.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `transaction_index` — string (required)
  - `member` — string (required) — The acting member; signs
  - `memo` — string
  - `acknowledge_unknown_programs` — boolean — approve_action only: required when the stored message carries instructions against programs this workspace cannot decode — the acknowledgement is recorded in the action audit
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `proposal_address` — string
  - `summary` — string

### `POST /v1/wallet-os/actions/approve` — Prepare an approval vote
- Action: `write` · MCP tool: `wallet-os.approve_action`
- Casts an approval on an Active proposal (Vote permission, not stale, no double vote; a previous rejection is removed). Approves at threshold. Refused outright when the stored message fails a known program's expected layout (blocking decode error); an instruction against an unknown program requires acknowledge_unknown_programs, recorded in the action audit. When the stored message carries a confidential transfer the response marks amount_binding "unverified" with a boundary note: this lane cannot verify the hidden amount, so a direct approval here carries no amount-binding record — surfaced, never refused, never relabeled.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `transaction_index` — string (required)
  - `member` — string (required) — The acting member; signs
  - `memo` — string
  - `acknowledge_unknown_programs` — boolean — approve_action only: required when the stored message carries instructions against programs this workspace cannot decode — the acknowledgement is recorded in the action audit
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `proposal_address` — string
  - `summary` — string
  - `amount_binding` — "unverified" | "not_applicable"
  - `amount_binding_note` — string | null

### `POST /v1/wallet-os/actions/reject` — Prepare a rejection vote
- Action: `write` · MCP tool: `wallet-os.reject_action`
- Casts a rejection on an Active proposal (Vote permission, not stale; a previous approval is removed). Rejects at the cutoff: voter count − threshold + 1.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `transaction_index` — string (required)
  - `member` — string (required) — The acting member; signs
  - `memo` — string
  - `acknowledge_unknown_programs` — boolean — approve_action only: required when the stored message carries instructions against programs this workspace cannot decode — the acknowledgement is recorded in the action audit
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `proposal_address` — string
  - `summary` — string

### `POST /v1/wallet-os/actions/cancel` — Prepare a cancellation vote
- Action: `write` · MCP tool: `wallet-os.cancel_action`
- Votes to cancel an Approved proposal (Vote permission; allowed even when stale). Cancels at threshold.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `transaction_index` — string (required)
  - `member` — string (required) — The acting member; signs
  - `memo` — string
  - `acknowledge_unknown_programs` — boolean — approve_action only: required when the stored message carries instructions against programs this workspace cannot decode — the acknowledgement is recorded in the action audit
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `proposal_address` — string
  - `summary` — string

### `POST /v1/wallet-os/actions/execute` — Prepare an execution
- Action: `write` · MCP tool: `wallet-os.execute_action`
- Builds the execution for an Approved proposal after the time lock releases (approved_at + time_lock, exact). Execute permission required. Stale rules are exact: a stale Approved config transaction cannot execute; a stale Approved vault transaction still can. Batch execution lands with the batch ops.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `transaction_index` — string (required)
  - `member` — string (required) — The acting member; signs
  - `memo` — string
  - `acknowledge_unknown_programs` — boolean — approve_action only: required when the stored message carries instructions against programs this workspace cannot decode — the acknowledgement is recorded in the action audit
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `proposal_address` — string
  - `summary` — string

### `POST /v1/wallet-os/transactions/submit` — Land a signed transaction durably
- Action: `write` · MCP tool: `wallet-os.submit_transaction`
- Lands a caller-signed transaction (from any prepare op). Lifecycle transactions route through the durable landing lane: a durable intent owns retries, escalation, and the watch through finalized — the response carries intent_id and landing_status, a reload resumes the same receipt, identical signed bytes dedup by signature, and a blockhash-only re-sign continues through the stored intent_id. A conflict never reveals another intent. An on-chain failure returns the program's error; a slow answer returns unconfirmed with the precise landing state to check later — never a fake success.
- Request body (JSON, required):
  - `cluster` — string (required) — mainnet, devnet, or testnet
  - `signed_transaction` — string (required) — Fully-signed legacy wire transaction, base64
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `signature` — string
  - `confirmation_status` — "confirmed" | "finalized" | "unconfirmed"
  - `slot` — integer | null
  - `receipt_recorded` — boolean
  - `landing` — string
  - `intent_id` — string
  - `landing_status` — string
  - `envelope_revision` — integer

### `POST /v1/wallet-os/plans/prepare` — Prepare one reviewed plan for a whole governed action
- Action: `write` · MCP tool: `wallet-os.prepare_plan`
- Derives every dependent identity BEFORE the first signature — the transaction index, its digest, the proposal address — compiles one unsigned envelope per step, hashes them into one plan digest, and stores the plan durably. Hand `steps` to a single signAllTransactions call and the whole action costs one approval.

The step set is derived from the live config and the actor's seat, never asked for: create and open-voting always; the actor's own approval when their seat can vote; and execution only when that approval alone reaches the live threshold with no time lock — any time lock, or any threshold above 1, leaves execution as a later reviewed action. Dependent envelopes cannot be simulated (their preconditions do not exist yet), so they carry the Solana implicit runtime budget, shown as `implicit_runtime_budget`, never a tighter guess.

Pass `plan_id` instead to recompile an existing plan's UNLANDED steps against current state: finalized work is never rebuilt or repeated, and if live state changed what the remaining steps would do, the plan comes back `review_required` with the exact reason and a new digest — no signature is requested until that digest is acknowledged.
- Request body (JSON, required):
  - `multisig_id` — string
  - `actor` — string — The member who signs every step; needs the Initiate permission
  - `vault_index` — integer
  - `intent` — object | object | object | object | object | object | object
  - `plan_id` — string — Recompile an existing plan's unlanded steps against live state instead of compiling a new one
  - `reviewed_plan_digest` — string — The plan_digest you showed the person approving. Passing it arms the plan (awaiting_signatures) and refuses if the plan changed since — this is what makes 'the review came before the wallet' checkable rather than promised.
  - `max_total_network_cost_lamports` — integer — Optional advanced guardrail: the maximum network cost to authorize per step
- Response (JSON):
  - `plan_id` — string
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `protocol` — "kernel" | "v4"
  - `multisig_id` — string
  - `config_address` — string
  - `actor` — string
  - `transaction_index` — integer
  - `transaction_address` — string
  - `action_digest` — string
  - `plan_digest` — string
  - `revision` — integer
  - `version` — integer
  - `state` — "draft" | "prepared" | "awaiting_signatures" | "signed" | "submitting" | "partially_landed" | "completed" | "review_required" | "unknown" | "failed" | "abandoned"
  - `review_reason` — string | null
  - `failure_reason` — string | null
  - `steps[]` — object[]
    - `ordinal` — integer
    - `step_kind` — string
    - `logical_step_hash` — string
    - `depends_on` — integer | null
    - `state` — "prepared" | "signed" | "submitted" | "unknown" | "confirmed" | "finalized" | "reorged" | "failed" | "invalidated"
    - `needs_resign` — boolean
    - `effect` — string
    - `transaction_base64` — string
    - `message_base64` — string
    - `required_signers[]` — string[]
    - `recent_blockhash` — string
    - `last_valid_block_height` — integer
    - `authorization` — object | null
      - `cu_limit` — integer
      - `cu_price_micro_lamports` — integer
      - `base_fee_lamports` — integer
      - `priority_fee_lamports` — integer
      - `max_total_network_cost_lamports` — integer
      - `limit_basis` — "simulated" | "implicit_runtime_budget"
      - `simulated_units` — integer | null
      - `quote_id` — string | null
      - `quote_confidence` — "high" | "medium" | "low" | null
      - `envelope_revision` — integer
    - `signature` — string | null
    - `intent_id` — string | null
    - `slot` — integer | null
    - `err` — any | null
    - `waiting_on` — "signature" | "predecessor" | "landing" | null
  - `signable_ordinals[]` — integer[]
  - `max_total_network_cost_lamports` — integer
  - `includes_execute` — boolean
  - `state_binding` — object
    - `transaction_index` — integer
    - `stale_transaction_index` — integer
    - `threshold` — integer
    - `programdata_len` — integer
    - `upgrade_authority` — string
  - `effects[]` — object[]
    - `summary` — string
    - `decoded` — boolean
  - `decode_coverage` — object
    - `decoded` — integer
    - `total` — integer
    - `line` — string
  - `cost` — object
    - `items[]` — object[]
      - `label` — string
      - `lamports` — integer
    - `total_lamports_estimate` — integer
  - `stale_preview` — object
    - `stale_transaction_index_after_execute` — integer
    - `voided[]` — object[]
      - `transaction_index` — integer
      - `state` — string
  - `automated_steps[]` — string[]
  - `policy_flags[]` — object[]
    - `policy_id` — string
    - `policy_name` — string
    - `kind` — string
    - `plane` — string
    - `flag` — string
    - `message` — string
  - `next_step` — string
  - `summary` — string

### `GET /v1/wallet-os/plans/{multisig_id}` — Read a plan, reconciled against the chain
- Action: `read` · MCP tool: `wallet-os.get_plan`
- The resume handle: with `plan_id` it reads that plan, without one it finds the live plan for the given signer on this multisig — so a reload that lost everything still lands on the same plan rather than a stranded action. Every non-terminal step is reconciled before the answer: the durable landing lane and the chain decide, a submitted step that finalized is reported finalized, a confirmed one that did not survive is reported reorged, and an unreadable answer leaves the step exactly where it was rather than inventing one.
- Path param `multisig_id` (string, required)
- Query param `signer` (string, required) — The connected signer — resumes that member's live plan when plan_id is unknown
- Query param `plan_id` (string)
- Response (JSON):
  - `plan` — object | null
    - `plan_id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `protocol` — "kernel" | "v4"
    - `multisig_id` — string
    - `config_address` — string
    - `actor` — string
    - `transaction_index` — integer
    - `transaction_address` — string
    - `action_digest` — string
    - `plan_digest` — string
    - `revision` — integer
    - `version` — integer
    - `state` — "draft" | "prepared" | "awaiting_signatures" | "signed" | "submitting" | "partially_landed" | "completed" | "review_required" | "unknown" | "failed" | "abandoned"
    - `review_reason` — string | null
    - `failure_reason` — string | null
    - `steps[]` — object[]
      - `ordinal` — integer
      - `step_kind` — string
      - `logical_step_hash` — string
      - `depends_on` — integer | null
      - `state` — "prepared" | "signed" | "submitted" | "unknown" | "confirmed" | "finalized" | "reorged" | "failed" | "invalidated"
      - `needs_resign` — boolean
      - `effect` — string
      - `transaction_base64` — string
      - `message_base64` — string
      - `required_signers[]` — string[]
      - `recent_blockhash` — string
      - `last_valid_block_height` — integer
      - `authorization` — object | null
      - `signature` — string | null
      - `intent_id` — string | null
      - `slot` — integer | null
      - `err` — any | null
      - `waiting_on` — "signature" | "predecessor" | "landing" | null
    - `signable_ordinals[]` — integer[]
    - `max_total_network_cost_lamports` — integer
    - `includes_execute` — boolean
    - `state_binding` — object
      - `transaction_index` — integer
      - `stale_transaction_index` — integer
      - `threshold` — integer
      - `programdata_len` — integer
      - `upgrade_authority` — string
    - `effects[]` — object[]
      - `summary` — string
      - `decoded` — boolean
    - `decode_coverage` — object
      - `decoded` — integer
      - `total` — integer
      - `line` — string
    - `cost` — object
      - `items[]` — object[]
      - `total_lamports_estimate` — integer
    - `stale_preview` — object
      - `stale_transaction_index_after_execute` — integer
      - `voided[]` — object[]
    - `automated_steps[]` — string[]
    - `policy_flags[]` — object[]
      - `policy_id` — string
      - `policy_name` — string
      - `kind` — string
      - `plane` — string
      - `flag` — string
      - `message` — string
    - `next_step` — string
    - `summary` — string

### `POST /v1/wallet-os/plans/submit` — Land a reviewed plan's steps in dependency order
- Action: `write` · MCP tool: `wallet-os.submit_plan`
- Takes the signed bytes the wallet returned and drives the plan. A signature is accepted only for bytes that are the reviewed envelope modulo its blockhash — nothing else can be submitted under this plan. Steps land in order, and a causally dependent step is not submitted until its predecessor is FINALIZED, because confirmed state can still be reorged and irreversible downstream work must never rely on it.

The call is bounded and idempotent: it returns the state it proved, and calling it again (with no new signatures) continues from there. Each step lands through the same durable landing lane and produces the same receipts as a hand-driven governed write, under the same logical step identity — a retry can never create a second logical action. If a step's blockhash expires before it lands, that ONE step comes back with `needs_resign` and a fresh envelope; re-signing it continues the same intent rather than starting anything over.
- Request body (JSON, required):
  - `plan_id` — string (required)
  - `expected_version` — integer (required) — The version you read — a lost race is reported, never applied
  - `reviewed_plan_digest` — string (required) — The plan_digest you showed the person approving
  - `signed_steps` — object[] — Signed bytes for the steps the wallet just returned; omit to continue an already-signed plan
- Response (JSON):
  - `plan_id` — string
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `protocol` — "kernel" | "v4"
  - `multisig_id` — string
  - `config_address` — string
  - `actor` — string
  - `transaction_index` — integer
  - `transaction_address` — string
  - `action_digest` — string
  - `plan_digest` — string
  - `revision` — integer
  - `version` — integer
  - `state` — "draft" | "prepared" | "awaiting_signatures" | "signed" | "submitting" | "partially_landed" | "completed" | "review_required" | "unknown" | "failed" | "abandoned"
  - `review_reason` — string | null
  - `failure_reason` — string | null
  - `steps[]` — object[]
    - `ordinal` — integer
    - `step_kind` — string
    - `logical_step_hash` — string
    - `depends_on` — integer | null
    - `state` — "prepared" | "signed" | "submitted" | "unknown" | "confirmed" | "finalized" | "reorged" | "failed" | "invalidated"
    - `needs_resign` — boolean
    - `effect` — string
    - `transaction_base64` — string
    - `message_base64` — string
    - `required_signers[]` — string[]
    - `recent_blockhash` — string
    - `last_valid_block_height` — integer
    - `authorization` — object | null
      - `cu_limit` — integer
      - `cu_price_micro_lamports` — integer
      - `base_fee_lamports` — integer
      - `priority_fee_lamports` — integer
      - `max_total_network_cost_lamports` — integer
      - `limit_basis` — "simulated" | "implicit_runtime_budget"
      - `simulated_units` — integer | null
      - `quote_id` — string | null
      - `quote_confidence` — "high" | "medium" | "low" | null
      - `envelope_revision` — integer
    - `signature` — string | null
    - `intent_id` — string | null
    - `slot` — integer | null
    - `err` — any | null
    - `waiting_on` — "signature" | "predecessor" | "landing" | null
  - `signable_ordinals[]` — integer[]
  - `max_total_network_cost_lamports` — integer
  - `includes_execute` — boolean
  - `state_binding` — object
    - `transaction_index` — integer
    - `stale_transaction_index` — integer
    - `threshold` — integer
    - `programdata_len` — integer
    - `upgrade_authority` — string
  - `effects[]` — object[]
    - `summary` — string
    - `decoded` — boolean
  - `decode_coverage` — object
    - `decoded` — integer
    - `total` — integer
    - `line` — string
  - `cost` — object
    - `items[]` — object[]
      - `label` — string
      - `lamports` — integer
    - `total_lamports_estimate` — integer
  - `stale_preview` — object
    - `stale_transaction_index_after_execute` — integer
    - `voided[]` — object[]
      - `transaction_index` — integer
      - `state` — string
  - `automated_steps[]` — string[]
  - `policy_flags[]` — object[]
    - `policy_id` — string
    - `policy_name` — string
    - `kind` — string
    - `plane` — string
    - `flag` — string
    - `message` — string
  - `next_step` — string
  - `summary` — string

### `POST /v1/wallet-os/plans/abandon` — Abandon a plan's unlanded steps
- Action: `write` · MCP tool: `wallet-os.abandon_plan`
- Drops the steps that were never submitted. A finalized prefix and its receipts are immutable and stay exactly as they are — if the action itself already landed, it remains on chain and the response says so. Refused while any step is still in flight (submitted, unknown, or confirmed): abandoning then would claim an outcome nobody can honour, so the refusal names the steps to wait on instead.
- Request body (JSON, required):
  - `plan_id` — string (required)
  - `expected_version` — integer (required)
- Response (JSON):
  - `plan_id` — string
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `protocol` — "kernel" | "v4"
  - `multisig_id` — string
  - `config_address` — string
  - `actor` — string
  - `transaction_index` — integer
  - `transaction_address` — string
  - `action_digest` — string
  - `plan_digest` — string
  - `revision` — integer
  - `version` — integer
  - `state` — "draft" | "prepared" | "awaiting_signatures" | "signed" | "submitting" | "partially_landed" | "completed" | "review_required" | "unknown" | "failed" | "abandoned"
  - `review_reason` — string | null
  - `failure_reason` — string | null
  - `steps[]` — object[]
    - `ordinal` — integer
    - `step_kind` — string
    - `logical_step_hash` — string
    - `depends_on` — integer | null
    - `state` — "prepared" | "signed" | "submitted" | "unknown" | "confirmed" | "finalized" | "reorged" | "failed" | "invalidated"
    - `needs_resign` — boolean
    - `effect` — string
    - `transaction_base64` — string
    - `message_base64` — string
    - `required_signers[]` — string[]
    - `recent_blockhash` — string
    - `last_valid_block_height` — integer
    - `authorization` — object | null
      - `cu_limit` — integer
      - `cu_price_micro_lamports` — integer
      - `base_fee_lamports` — integer
      - `priority_fee_lamports` — integer
      - `max_total_network_cost_lamports` — integer
      - `limit_basis` — "simulated" | "implicit_runtime_budget"
      - `simulated_units` — integer | null
      - `quote_id` — string | null
      - `quote_confidence` — "high" | "medium" | "low" | null
      - `envelope_revision` — integer
    - `signature` — string | null
    - `intent_id` — string | null
    - `slot` — integer | null
    - `err` — any | null
    - `waiting_on` — "signature" | "predecessor" | "landing" | null
  - `signable_ordinals[]` — integer[]
  - `max_total_network_cost_lamports` — integer
  - `includes_execute` — boolean
  - `state_binding` — object
    - `transaction_index` — integer
    - `stale_transaction_index` — integer
    - `threshold` — integer
    - `programdata_len` — integer
    - `upgrade_authority` — string
  - `effects[]` — object[]
    - `summary` — string
    - `decoded` — boolean
  - `decode_coverage` — object
    - `decoded` — integer
    - `total` — integer
    - `line` — string
  - `cost` — object
    - `items[]` — object[]
      - `label` — string
      - `lamports` — integer
    - `total_lamports_estimate` — integer
  - `stale_preview` — object
    - `stale_transaction_index_after_execute` — integer
    - `voided[]` — object[]
      - `transaction_index` — integer
      - `state` — string
  - `automated_steps[]` — string[]
  - `policy_flags[]` — object[]
    - `policy_id` — string
    - `policy_name` — string
    - `kind` — string
    - `plane` — string
    - `flag` — string
    - `message` — string
  - `next_step` — string
  - `summary` — string

### `GET /v1/wallet-os/vaults/{multisig_id}` — The multisig's vaults and what they hold
- Action: `read` · MCP tool: `wallet-os.list_vaults`
- Vaults are PDAs — every index 0-255 exists implicitly and nothing on chain records which ones you use. Returns index 0 (the acting vault, always) plus every scanned index holding lamports, with the derived address and balance. `scanned_indices` states the bound rather than applying it silently: a vault beyond it exists and holds whatever it holds.
- Path param `multisig_id` (string, required) — Imported multisig id (msig_…)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `multisig_id` — string
  - `config_address` — string
  - `vaults[]` — object[]
    - `index` — integer
    - `address` — string
    - `lamports` — integer
    - `in_use` — boolean
    - `tokens[]` — object[]
      - `mint` — string
      - `amount` — string — Base units, as a decimal string
      - `decimals` — integer
      - `token_account` — string
    - `tokens_read` — "ok" | "unavailable"
  - `scanned_indices` — integer
  - `discovery` — object
    - `complete` — boolean
    - `reason` — string
    - `scanned_indices` — integer
    - `next_step` — string
  - `observed_slot` — integer

### `GET /v1/wallet-os/vaults/{multisig_id}/resolve` — Identify a vault from its address
- Action: `read` · MCP tool: `wallet-os.resolve_vault`
- For the vault you have the ADDRESS of — from an explorer, a deposit receipt, a colleague. Its index is DERIVED by walking this multisig's 256 vault PDAs and matching, never asked for: nobody knows a vault index by heart. A match proves the address is this multisig's vault; no match proves it is not, and says so rather than reporting an ambiguous result. This is how a vault outside the bounded scan of list_vaults is reached.
- Path param `multisig_id` (string, required)
- Query param `address` (string, required) — The vault address you have — its index is derived, never asked for
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `multisig_id` — string
  - `config_address` — string
  - `vault` — object
    - `index` — integer
    - `address` — string
    - `lamports` — integer
    - `in_use` — boolean
    - `tokens[]` — object[]
      - `mint` — string
      - `amount` — string — Base units, as a decimal string
      - `decimals` — integer
      - `token_account` — string
    - `tokens_read` — "ok" | "unavailable"
  - `observed_slot` — integer
  - `summary` — string

### `POST /v1/wallet-os/vaults/{multisig_id}/sub-accounts` — Name a new sub-account; its index is chosen for you
- Action: `write` · MCP tool: `wallet-os.create_sub_account`
- Say what the sub-account is FOR. The vault index is chosen — the next one this workspace has not already named AND that holds nothing — because an index is an implementation detail of a PDA, not a decision a person should make. NOTHING is created on chain: every index 0-255 already exists as a PDA, so this records the name and picks the index. The name is hosted context and confers no on-chain authority; the vault exists with or without it.
- Path param `multisig_id` (string, required)
- Request body (JSON, required):
  - `name` — string (required) — What this sub-account is called
  - `purpose` — string — What it is for, in your words
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `multisig_id` — string
  - `config_address` — string
  - `vault_index` — integer
  - `address` — string
  - `name` — string
  - `purpose` — string | null
  - `summary` — string

### `GET /v1/wallet-os/governed-programs/{multisig_id}` — Programs this multisig upgrades
- Action: `read` · MCP tool: `wallet-os.list_governed_programs`
- The programs someone registered against this multisig, so the upgrade flow can offer them instead of asking for the address every time. Chain discovery is not possible — the BPF Upgradeable Loader owns every program on the cluster, so a scan by upgrade authority times out and there is no index for it. Registration grants nothing: the upgrade path re-reads the real authority from chain and refuses if it is not this multisig's.
- Path param `multisig_id` (string, required)
- Response (JSON):
  - `multisig_id` — string
  - `programs[]` — object[]
    - `program_id` — string
    - `label` — string
    - `updated_at` — integer

### `POST /v1/wallet-os/governed-programs/{multisig_id}` — Register a program this multisig upgrades
- Action: `write` · MCP tool: `wallet-os.save_governed_program`
- Name a program so the upgrade flow can offer it. Idempotent on the program id: naming the same one twice is a rename. Confers no authority whatsoever.
- Path param `multisig_id` (string, required)
- Request body (JSON, required):
  - `program_id` — string (required) — The program this multisig upgrades
  - `label` — string (required) — What your team calls it
- Response (JSON):
  - `multisig_id` — string
  - `program_id` — string
  - `label` — string
  - `updated_at` — integer

### `DELETE /v1/wallet-os/governed-programs/{multisig_id}/{program_id}` — Forget a registered program
- Action: `write` · MCP tool: `wallet-os.forget_governed_program`
- Removes the entry. The program keeps existing, its upgrade authority is untouched, and any upgrade already in flight is unaffected — only the shortcut goes away.
- Path param `multisig_id` (string, required)
- Path param `program_id` (string, required) — Solana address (base58)
- Response (JSON):
  - `multisig_id` — string
  - `program_id` — string
  - `removed` — boolean

### `GET /v1/wallet-os/limits/list` — List a multisig's spending limits
- Action: `read` · MCP tool: `wallet-os.list_spending_limits`
- Live scan of the multisig's spending-limit accounts: mint, amount per period, remaining this period, last reset, listed signers (independent of multisig membership), and allowed destinations (empty = unrestricted).
- Query param `multisig_id` (string, required)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `multisig_id` — string
  - `config_address` — string
  - `limits[]` — object[]
    - `address` — string
    - `create_key` — string
    - `vault_index` — integer
    - `vault_address` — string
    - `mint` — string — "sol" for native SOL, else the mint address
    - `amount` — integer
    - `period` — "one_time" | "day" | "week" | "month"
    - `period_seconds` — integer
    - `remaining` — integer
    - `last_reset` — integer
    - `signers[]` — string[] — Listed signers — independent of multisig membership
    - `destinations[]` — string[] — Empty means unrestricted
  - `observed_slot` — integer

### `POST /v1/wallet-os/limits/prepare-change` — Prepare a spending-limit change
- Action: `write` · MCP tool: `wallet-os.prepare_spending_limit_change`
- Builds a config transaction adding or removing a spending limit — a change is remove + recreate in one transaction (there is no in-place edit). The change then rides the normal proposal lifecycle. Spending-limit changes never advance the stale boundary.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `creator` — string (required) — Member with the Initiate permission; signs
  - `change` — object | object | object (required)
  - `memo` — string
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `transaction_address` — string
  - `spending_limit_accounts[]` — string[]
  - `digest` — string
  - `replacement` — object
    - `old_address` — string
    - `new_address` — string
    - `old_remaining` — string
    - `old_spent` — string
    - `new_immediate_allowance` — string
    - `restored_allowance` — string
    - `requires_confirmation` — boolean
    - `period_reset` — string
    - `next_reset_unix` — integer | null
    - `rent_to` — string
    - `rent_lamports` — integer
    - `destinations[]` — string[]
  - `summary` — string

### `POST /v1/wallet-os/limits/use` — Use a spending limit
- Action: `write` · MCP tool: `wallet-os.use_spending_limit`
- Builds the direct transfer a listed signer can make without quorum: validates the signer is listed, the destination is permitted, and the amount fits the remaining allowance (periods reset strictly after the period rolls; the program re-enforces every rule). SOL and SPL.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `spending_limit` — string (required) — Solana address (base58)
  - `signer` — string (required) — A listed signer on the limit (need not be a multisig member); signs
  - `destination` — string (required) — Solana address (base58)
  - `amount` — string (required) — Base units, as a decimal string
  - `destination_token_account` — string — SPL only: token account of the mint whose owner is the destination
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `spending_limit` — string
  - `mint` — string
  - `decimals` — integer
  - `remaining_after` — integer
  - `summary` — string

### `POST /v1/wallet-os/batches/create` — Create a batch
- Action: `write` · MCP tool: `wallet-os.create_batch`
- Creates an empty batch (one proposal governs every child). Next: open its proposal as a Draft, then add children — only the batch creator, only while Draft.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `creator` — string (required) — Member with the Initiate permission; the only one who may add children
  - `vault_index` — integer
  - `memo` — string
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `batch_index` — integer
  - `batch_address` — string
  - `summary` — string

### `POST /v1/wallet-os/batches/add-transaction` — Add a child to a batch
- Action: `write` · MCP tool: `wallet-os.add_batch_transaction`
- Appends one child message to a Draft batch (creator only) and returns the aggregate review: every child in the batch with a one-line effect summary, so the whole batch is reviewed before approval.

A child moves SOL (transfer_sol), an SPL or Token-2022 amount (transfer_token), or raw instructions. A token child takes the mint, an amount in base units, and a destination token account — the mint's decimals, which token program owns it, and the vault's own token account are read live and derived here, never asked for. It compiles through the same resolver as a single token action, so a batch row and a one-off send produce byte-identical instructions.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `member` — string (required) — The batch creator; signs
  - `batch_index` — integer (required)
  - `intent` — object | object | object (required)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `batch_index` — integer
  - `child_index` — integer
  - `child_address` — string
  - `digest` — string
  - `batch_review` — object
    - `size_after` — integer
    - `children[]` — object[]
      - `child_index` — integer
      - `child_address` — string
      - `summary` — string
  - `summary` — string

### `POST /v1/wallet-os/batches/execute-next` — Execute the next batch child
- Action: `write` · MCP tool: `wallet-os.execute_batch_next`
- Builds the execution of exactly the next unexecuted child (strict order — no skipping). The proposal becomes Executed only after the final child.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `member` — string (required) — Member with the Execute permission; signs
  - `batch_index` — integer (required)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `batch_index` — integer
  - `child_index` — integer
  - `is_final` — boolean
  - `summary` — string

### `POST /v1/wallet-os/batches/close` — Close a batch (one step)
- Action: `write` · MCP tool: `wallet-os.close_batch`
- One close step per call per the close matrix: the last open child (children close last-to-first), or the batch + its proposal once every child is closed. Rent goes to the configured collector; call again until the batch is gone.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `batch_index` — integer (required)
  - `payer` — string — Who pays the fee. Defaults to the rent collector, which only works if that account can sign.
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `batch_index` — integer
  - `step` — "close_child" | "close_batch"
  - `child_index` — integer
  - `remaining_children_after` — integer
  - `summary` — string

### `POST /v1/wallet-os/buffers/create` — Create a staging buffer
- Action: `write` · MCP tool: `wallet-os.create_staging_buffer`
- Starts a transaction buffer for an oversized vault message: the full message's size and SHA-256 hash are declared up front (content pinned), preallocated on-chain. Extend with the same message bytes until complete, then finalize.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `creator` — string (required) — Member with the Initiate permission; the only one who may extend/close
  - `buffer_index` — integer (required)
  - `vault_index` — integer
  - `message_base64` — string (required) — The full vault message bytes (the hash pins the content)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `buffer_address` — string
  - `buffer_index` — integer
  - `final_buffer_hash` — string
  - `final_buffer_size` — integer
  - `buffered_bytes` — integer
  - `complete` — boolean
  - `summary` — string

### `POST /v1/wallet-os/buffers/extend` — Extend a staging buffer
- Action: `write` · MCP tool: `wallet-os.extend_staging_buffer`
- Appends the next chunk to a staging buffer (creator only). Pass the same message bytes the buffer was created with — the declared hash verifies the content, so resume is always safe.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `creator` — string (required) — Solana address (base58)
  - `buffer_index` — integer (required)
  - `message_base64` — string (required) — The SAME message bytes the buffer was created with
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `buffer_address` — string
  - `buffered_bytes` — integer
  - `complete` — boolean
  - `summary` — string

### `POST /v1/wallet-os/buffers/finalize` — Finalize a staging buffer into a vault transaction
- Action: `write` · MCP tool: `wallet-os.finalize_staging_buffer`
- Converts a complete buffer into a vault transaction (exact size and hash match verified on-chain; the buffer closes and its rent returns to its creator). The transaction then rides the normal proposal lifecycle.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `creator` — string (required) — Solana address (base58)
  - `buffer_index` — integer (required)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `transaction_address` — string
  - `digest` — string
  - `summary` — string

### `POST /v1/wallet-os/buffers/close` — Close a staging buffer
- Action: `write` · MCP tool: `wallet-os.close_staging_buffer`
- Closes a staging buffer (creator only) and reclaims its rent to the creator.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `creator` — string (required) — Solana address (base58)
  - `buffer_index` — integer (required)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `buffer_address` — string
  - `summary` — string

### `POST /v1/wallet-os/actions/close` — Close a transaction and its proposal
- Action: `write` · MCP tool: `wallet-os.close_action_accounts`
- Reclaims rent for a vault or config transaction per the close matrix: terminal states always close; Draft/Active (and Approved config) only when stale; an Approved vault transaction never closes. Batches close via wallet-os.close_batch. The configured rent collector receives the rent. Closing is PERMISSIONLESS on chain — the program requires no signer, so any account may pay the fee and the rent still reaches only the governed collector. Pass `payer` to choose who pays; it defaults to the collector, which works only when that account can sign (a vault collector is a program-derived address and cannot). Without a rent collector configured, nothing can be reclaimed. wallet-os.get_queue reports both the collector and, per item, whether it is closeable and the exact reason when it is not.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `transaction_index` — string (required) — The transaction index to close (decimal string)
  - `payer` — string — Who pays the fee. Defaults to the rent collector, which only works if that account can sign — a vault collector is a PDA and cannot.
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `closed_accounts[]` — string[]
  - `summary` — string

### `POST /v1/wallet-os/actions/abandon` — Abandon an action that was never proposed
- Action: `write` · MCP tool: `wallet-os.prepare_abandon_action`
- Closes a transaction that provably has NO proposal and returns its rent to the configured rent collector. This is the path for a create that landed but whose voting never opened: without it the only recoveries were opening voting purely to reject the action, or executing an unrelated config change to make the orphan stale. Authorization is the STORED transaction creator's signature alone — not membership and not the fee payer, so a different payer gains no deletion authority. Any proposal, in any status, forecloses abandon (that account closes through wallet-os.close_action_accounts instead). It changes no configuration, never advances the stale boundary, and leaves every other action executable. k256 kernel only — imported Squads v4 has no equivalent instruction and says so rather than claiming unsupported behaviour.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `creator` — string (required) — The member who CREATED this transaction; signs. v1 authorization is the stored creator's signature — not membership, not the fee payer
  - `transaction_index` — string (required)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `transaction_index` — integer
  - `kind` — "vault_transaction" | "config_transaction"
  - `closed_account` — string
  - `rent_collector` — string
  - `reclaim_lamports` — integer
  - `summary` — string

### `POST /v1/wallet-os/upgrades/prepare` — Stage a program build for upgrade
- Action: `write` · MCP tool: `wallet-os.prepare_program_upgrade`
- Stages a compiled .so into a BPF Upgradeable Loader buffer in chunked Write steps (authority = the proposing member's signer, never a hosted key). Verifies the program is upgradeable and governed by this multisig's acting address, shows the exact buffer rent, and returns the upload plan — every step idempotent and resumable by buffer address + artifact hash.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `member` — string (required) — The proposing member (Initiate); becomes the buffer's authority — never a hosted key
  - `program_address` — string (required) — The program to upgrade; its current upgrade authority must be this multisig's acting address
  - `artifact_base64` — string (required) — The compiled .so bytes, base64 (re-sent on resume)
  - `buffer_address` — string (required) — Client-generated buffer keypair address (fresh), or the existing buffer address to resume
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `program` — "v4" | "kernel"
  - `program_address` — string
  - `buffer_address` — string
  - `artifact_hash` — string — SHA-256 hex of the artifact — the resume key
  - `artifact_bytes` — integer
  - `uploaded_bytes` — integer — Bytes already verified in the buffer (0 for a fresh upload)
  - `steps[]` — object[]
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `transaction_base64` — string
    - `message_base64` — string
    - `required_signers[]` — string[]
    - `recent_blockhash` — string
    - `last_valid_block_height` — integer
    - `authorization` — object
      - `cu_limit` — integer
      - `cu_price_micro_lamports` — integer
      - `base_fee_lamports` — integer
      - `priority_fee_lamports` — integer
      - `max_total_network_cost_lamports` — integer
      - `limit_basis` — "simulated" | "implicit_runtime_budget"
      - `simulated_units` — integer | null
      - `quote_id` — string | null
      - `quote_confidence` — "high" | "medium" | "low" | null
      - `envelope_revision` — integer
    - `step` — integer
    - `kind` — "init_buffer" | "write"
    - `offset` — integer
    - `bytes` — integer
    - `summary` — string
  - `cost` — object
    - `items[]` — object[]
      - `label` — string
      - `lamports` — integer
    - `total_lamports_estimate` — integer
  - `governed_preview` — object
    - `instructions[]` — string[]
    - `effect` — string
  - `automated_steps[]` — string[]
  - `summary` — string

### `POST /v1/wallet-os/upgrades/finalize` — Verify the staged build and prepare the governed upgrade
- Action: `write` · MCP tool: `wallet-os.finalize_program_upgrade`
- Re-fetches the buffer and re-hashes it against the artifact (the checked-for-you proof, with the buffer address), then builds the immutable governed payload: SetBufferAuthority to the acting address plus Upgrade signed by it — one vault transaction for the normal proposal lifecycle. The drained buffer's rent spills to the rent collector on execution.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `member` — string (required) — Solana address (base58)
  - `program_address` — string (required) — Solana address (base58)
  - `buffer_address` — string (required) — Solana address (base58)
  - `artifact_base64` — string (required) — The same artifact bytes — the buffer is re-hashed against them
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `kind` — string
  - `transaction_index` — integer
  - `transaction_address` — string
  - `digest` — string
  - `artifact_hash` — string
  - `verification` — object
    - `buffer_address` — string
    - `buffer_hash` — string
    - `artifact_hash` — string
    - `uploaded_bytes` — integer
    - `proof` — string
  - `capacity` — object
    - `required` — boolean
    - `current_programdata_len` — integer
    - `new_programdata_len` — integer
    - `artifact_len` — integer
    - `required_programdata_len` — integer
    - `additional_bytes` — integer
    - `max_rent_lamports` — integer
    - `rent_payer` — string | null
    - `reclaim` — string | null
  - `effects[]` — object[]
    - `summary` — string
    - `decoded` — boolean
  - `decode_coverage` — object
    - `decoded` — integer
    - `total` — integer
    - `line` — string
  - `state_binding` — object
    - `transaction_index` — integer
    - `stale_transaction_index` — integer
    - `threshold` — integer
    - `programdata_len` — integer
    - `upgrade_authority` — string
  - `summary` — string

### `POST /v1/wallet-os/upgrades/close-staging` — Reclaim a staging deposit
- Action: `write` · MCP tool: `wallet-os.close_staging_upgrade`
- The abandonment path: closes an upload buffer and reclaims its rent to the proposer's signer (the only key that can close it — no hosted component). Refused while the buffer is referenced by an active proposal.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `member` — string (required) — The proposing member — the buffer's authority (no hosted component can close it)
  - `buffer_address` — string (required) — Solana address (base58)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `buffer_address` — string
  - `reclaim_lamports` — integer
  - `summary` — string

### `POST /v1/wallet-os/programs/authority/transfer` — Move a program's upgrade authority
- Action: `write` · MCP tool: `wallet-os.prepare_program_authority_transfer`
- One reviewable plan for every direction an upgrade authority can move. Reads the CURRENT authority from ProgramData and derives the direction from it, so the caller never declares state that chain already knows: omit a destination to adopt the program into this multisig, or pass another governed multisig, a wallet, or intentional immutability. Reads the cluster's live checked-instruction feature state and picks the protocol-exact loader instruction; refuses a destination that could never sign — a multisig config address is named as the unrecoverable mistake it would be. Adoption and handoff to a wallet are ONE atomic transaction that the destination co-signs, never a one-sided transfer and never a temporary member seat. A handoff between two governed multisigs is two stages and says so.
- Request body (JSON, required):
  - `multisig_id` — string (required) — The multisig acting in this stage: the destination when adopting, the source when exiting
  - `member` — string (required) — The proposing member (Initiate)
  - `program_address` — string (required) — Solana address (base58)
  - `destination` — object | object | object — Omit to adopt the program into this multisig's acting address
  - `transfer_id` — string — Governed vault → governed vault only: the id from the destination's acceptance
  - `confirm` — string — Immutable only: the typed irreversible confirmation
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `kind` — string
  - `direction` — "external_to_vault" | "vault_to_external" | "vault_to_vault" | "vault_to_immutable"
  - `stage` — "destination_proposal" | "source_proposal"
  - `atomic` — boolean
  - `transfer_id` — string
  - `transaction_index` — integer
  - `transaction_address` — string
  - `digest` — string
  - `plan` — object
    - `program_address` — string
    - `programdata_address` — string
    - `current_authority` — string
    - `destination` — string | null
    - `destination_kind` — "vault" | "external" | "immutable"
    - `multisig_config_address` — string
    - `instruction` — "SetAuthority" | "SetAuthorityChecked"
    - `loader_variant` — 4 | 7
    - `checked_feature` — object
      - `feature_id` — string
      - `active` — boolean
      - `activated_at_slot` — integer | null
      - `observed_at_slot` — integer
    - `execute_signers[]` — string[]
  - `acceptance` — object
    - `digest` — string — SHA-256 of the domain-separated ProgramAuthorityAcceptanceV1 encoding
    - `memo` — string — The exact UTF-8 Memo body the destination signs
    - `destination_config` — string
    - `destination_vault` — string
    - `destination_config_digest` — string
    - `source_config_digest` — string
    - `source_plan_digest` — string
    - `transaction_index` — integer
    - `program_address` — string
    - `programdata_address` — string
    - `source_config` — string
    - `source_vault` — string
    - `source_config_slot` — integer
    - `destination_config_slot` — integer
  - `simulation` — object
    - `programdata_address` — string
    - `err` — string | null
  - `effects[]` — object[]
    - `summary` — string
    - `decoded` — boolean
  - `decode_coverage` — object
    - `decoded` — integer
    - `total` — integer
    - `line` — string
  - `automated_steps[]` — string[]
  - `state_binding` — object
    - `transaction_index` — integer
    - `stale_transaction_index` — integer
    - `threshold` — integer
    - `upgrade_authority` — string
    - `checked_feature_active` — boolean
    - `checked_feature_activated_at_slot` — integer | null
  - `disclosure[]` — string[]
  - `summary` — string

### `POST /v1/wallet-os/programs/authority/accept` — Accept a program authority from another multisig
- Action: `write` · MCP tool: `wallet-os.prepare_program_authority_acceptance`
- The destination's half of a governed-to-governed handoff. One multisig program cannot make another multisig's acting address sign inside its loader CPI, so the two consents cannot be atomic; the destination instead executes a Memo whose digest covers this exact handoff — the program, both configs' exact bytes, both acting addresses, and the source plan it authorizes. The source can only propose the transfer after this has executed, and any configuration change on either side invalidates it. Returns the transfer id the source stage needs.
- Request body (JSON, required):
  - `multisig_id` — string (required) — The DESTINATION multisig — the one taking the authority on
  - `member` — string (required) — The proposing member (Initiate) on the destination
  - `program_address` — string (required) — Solana address (base58)
  - `source_multisig_id` — string (required) — The multisig that currently holds the authority
  - `transfer_id` — string — Omit to mint one; pass it back to rebuild the identical acceptance
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `authorization` — object
    - `cu_limit` — integer
    - `cu_price_micro_lamports` — integer
    - `base_fee_lamports` — integer
    - `priority_fee_lamports` — integer
    - `max_total_network_cost_lamports` — integer
    - `limit_basis` — "simulated" | "implicit_runtime_budget"
    - `simulated_units` — integer | null
    - `quote_id` — string | null
    - `quote_confidence` — "high" | "medium" | "low" | null
    - `envelope_revision` — integer
  - `kind` — string
  - `direction` — string
  - `stage` — string
  - `atomic` — boolean
  - `transfer_id` — string
  - `transaction_index` — integer
  - `transaction_address` — string
  - `digest` — string
  - `acceptance` — object
    - `digest` — string — SHA-256 of the domain-separated ProgramAuthorityAcceptanceV1 encoding
    - `memo` — string — The exact UTF-8 Memo body the destination signs
    - `destination_config` — string
    - `destination_vault` — string
    - `destination_config_digest` — string
    - `source_config_digest` — string
    - `source_plan_digest` — string
    - `transaction_index` — integer
    - `program_address` — string
    - `programdata_address` — string
    - `source_config` — string
    - `source_vault` — string
    - `source_config_slot` — integer
    - `destination_config_slot` — integer
  - `effects[]` — object[]
    - `summary` — string
    - `decoded` — boolean
  - `decode_coverage` — object
    - `decoded` — integer
    - `total` — integer
    - `line` — string
  - `disclosure[]` — string[]
  - `summary` — string

### `GET /v1/wallet-os/programs/authority` — Who can upgrade this program, from chain
- Action: `read` · MCP tool: `wallet-os.verify_program_authority`
- Reads the program's upgrade authority straight from ProgramData and says whether it is this multisig's acting address, another key, or gone (immutable) — the answer after a transfer, and the answer before one. Given a prepared transfer's index it re-runs the same live check that gates execution: a plan whose source authority has since moved, or one built while the loader's checked instruction was inactive and now delayed past its activation, is reported as no longer executable with the exact reason.
- Query param `multisig_id` (string, required)
- Query param `program_address` (string, required) — Solana address (base58)
- Query param `transaction_index` (string) — A prepared transfer's index — re-verifies the stored plan against live cluster state
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `program_address` — string
  - `programdata_address` — string
  - `upgrade_authority` — string | null
  - `last_deployed_slot` — integer
  - `immutable` — boolean
  - `governed_by_this_multisig` — boolean
  - `acting_address` — string
  - `multisig_config_address` — string
  - `checked_feature` — object
    - `feature_id` — string
    - `active` — boolean
    - `activated_at_slot` — integer | null
    - `observed_at_slot` — integer
  - `pending_transfers[]` — object[]
    - `transaction_index` — integer
    - `status` — string
    - `instruction` — "SetAuthority" | "SetAuthorityChecked"
    - `destination` — string | null
  - `plan` — object | null
    - `transaction_index` — integer
    - `transaction_address` — string
    - `digest` — string
    - `instruction` — "SetAuthority" | "SetAuthorityChecked"
    - `loader_variant` — 4 | 7
    - `from` — string
    - `to` — string | null
    - `executable` — boolean
    - `reason` — string | null
  - `summary` — string

### `GET /v1/wallet-os/address-book` — The workspace's saved addresses
- Action: `read` · MCP tool: `wallet-os.list_address_book`
- Names your team has given to destinations. A label only — it confers no authority and cannot approve, sign, or widen what any member may do. Every surface that shows a label also shows the address, so the name can be checked rather than trusted.
- Response (JSON):
  - `addresses[]` — object[]
    - `address` — string
    - `label` — string
    - `note` — string | null
    - `updated_at` — integer

### `POST /v1/wallet-os/address-book` — Name an address
- Action: `write` · MCP tool: `wallet-os.save_address`
- Saves or renames one destination for this workspace. Idempotent on the address: naming the same one twice is a rename, not a duplicate.
- Request body (JSON, required):
  - `address` — string (required) — The destination to name
  - `label` — string (required) — What your team calls it
  - `note` — string — Why it is worth writing down
- Response (JSON):
  - `address` — string
  - `label` — string
  - `note` — string | null
  - `updated_at` — integer

### `DELETE /v1/wallet-os/address-book/{address}` — Forget a saved address
- Action: `write` · MCP tool: `wallet-os.forget_address`
- Removes the name. The address keeps existing on chain and any action already built against it is unaffected — only what your workspace calls it goes away.
- Path param `address` (string, required) — Solana address (base58)
- Response (JSON):
  - `address` — string
  - `removed` — boolean

### `GET /v1/wallet-os/org/projects` — List the org's projects
- Action: `read` · MCP tool: `wallet-os.list_projects`
- Projects are the default ACL boundary for visibility and drafting — named groupings of multisigs by real-world responsibility. Each carries its live multisig count and archive state.
- Response (JSON):
  - `projects[]` — object[]
    - `project_id` — string
    - `name` — string
    - `description` — string | null
    - `multisig_count` — integer
    - `archived` — boolean
    - `created_at` — integer
    - `archived_at` — integer | null

### `POST /v1/wallet-os/org/projects/create` — Create a project
- Action: `write` · MCP tool: `wallet-os.create_project`
- Creates a project (org admin). The creation is written to the org audit log (plane workspace).
- Request body (JSON, required):
  - `name` — string (required) — Project name — the default ACL boundary for visibility and drafting
  - `description` — string
- Response (JSON):
  - `project_id` — string
  - `name` — string
  - `description` — string | null
  - `multisig_count` — integer
  - `archived` — boolean
  - `created_at` — integer
  - `archived_at` — integer | null
  - `summary` — string

### `POST /v1/wallet-os/org/projects/update` — Rename, describe, or archive a project
- Action: `write` · MCP tool: `wallet-os.update_project`
- Updates a project's name/description or archives it (org admin). Archived projects keep their multisigs filed and fully operable. A rename is written to the org audit log.
- Request body (JSON, required):
  - `project_id` — string (required)
  - `name` — string
  - `description` — string
  - `archive` — boolean — true archives (multisigs stay filed and operable); false restores an archived project
- Response (JSON):
  - `project_id` — string
  - `name` — string
  - `description` — string | null
  - `archived` — boolean
  - `created_at` — integer
  - `archived_at` — integer | null
  - `summary` — string

### `POST /v1/wallet-os/org/projects/assign` — File an imported multisig under a project
- Action: `write` · MCP tool: `wallet-os.assign_multisig_project`
- Sets the multisig's project (org admin). Corrective errors when the project does not exist in the org or is archived, and when the multisig is not imported in the workspace.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `project_id` — string | null (required) — The project to file under; null unfiles the multisig from any project
- Response (JSON):
  - `multisig_id` — string
  - `project_id` — string | null
  - `summary` — string

### `GET /v1/wallet-os/org/people` — List people, their signer bindings, and live on-chain seats
- Action: `read` · MCP tool: `wallet-os.list_people`
- The org's WorkOS memberships (email, name, org role, status, joined — status 'inactive' is the deactivated state the UI renders, e.g. after SCIM off-boarding) merged with active signer bindings and each binding's on-chain seats — which imported multisigs list that signer as a member, with role bits, computed live from the config accounts. A signer binding is display, notifications, and audit attribution only — it confers no authority. A directory outage degrades to bindings + seats (reachable:false), never an error.
- Response (JSON):
  - `directory` — object
    - `source` — string
    - `reachable` — boolean
    - `memberships` — integer
  - `people[]` — object[]
    - `workos_user_id` — string
    - `email` — string | null
    - `first_name` — string | null
    - `last_name` — string | null
    - `org_role` — string | null
    - `status` — string | null
    - `joined_at` — string | null
    - `in_directory` — boolean
    - `bindings[]` — object[]
      - `binding_id` — string
      - `workos_user_id` — string
      - `signer_address` — string
      - `bound_at` — integer
      - `seats[]` — object[]
  - `attribution_note` — string

### `POST /v1/wallet-os/org/people/bind-signer` — Link your identity to a signer address by signed message
- Action: `write` · MCP tool: `wallet-os.bind_signer`
- Verifies an ed25519 signature over the canonical message `k256-wallet-os signer binding\norg:<org>\nuser:<user>\nsigner:<address>` against the signer address, then stores the binding (attribution only — no authority is granted or implied). Web-lane only: the binding ties to the signed-in user. One active binding per signer per org; the binding is written to the org audit log.
- Request body (JSON, required):
  - `signer_address` — string (required) — The signer's base58 address — the ed25519 key that signed the message
  - `message` — string (required) — The EXACT canonical binding message (org and user are bound server-side; any other text is rejected)
  - `signature` — string (required) — The 64-byte ed25519 signature over the message, base58-encoded
- Response (JSON):
  - `binding_id` — string
  - `workos_user_id` — string
  - `signer_address` — string
  - `bound_at` — integer
  - `created` — boolean
  - `attribution_note` — string
  - `summary` — string

### `POST /v1/wallet-os/org/people/revoke-binding` — Revoke a signer binding
- Action: `write` · MCP tool: `wallet-os.revoke_binding`
- Soft-revokes a binding (the owner revokes their own; an org admin revokes anyone's). Attribution metadata only — nothing on-chain changes. Written to the org audit log.
- Request body (JSON, required):
  - `binding_id` — string (required)
- Response (JSON):
  - `binding_id` — string
  - `revoked` — boolean
  - `summary` — string

### `POST /v1/wallet-os/org/people/remove` — Deprovisioning report: residual on-chain signers for a person
- Action: `write` · MCP tool: `wallet-os.remove_person`
- Deprovisioning honesty (R-ORG-004): does NOT touch WorkOS (workspace access is removed in the WorkOS dashboard or by SCIM — guidance included) and never presents access removal as signer removal. Returns the residual-signer report: every imported multisig where the person's bound signers still hold on-chain permissions (address, role bits, config address, label), each with the governed rotation path (remove-member via wallet-os.prepare_config_change).
- Request body (JSON, required):
  - `workos_user_id` — string (required)
- Response (JSON):
  - `workos_user_id` — string
  - `removed_from_directory` — boolean
  - `directory_guidance` — string
  - `residual_signers[]` — object[]
    - `binding_id` — string
    - `signer_address` — string
    - `seats[]` — object[]
      - `multisig_id` — string
      - `config_address` — string
      - `label` — string | null
      - `cluster` — "mainnet" | "devnet" | "testnet"
      - `program_id` — string
      - `mask` — integer
      - `initiate` — boolean
      - `vote` — boolean
      - `execute` — boolean
      - `rotation` — object
  - `bindings_without_seats` — integer
  - `honesty_note` — string
  - `summary` — string

### `POST /v1/wallet-os/payouts/prepare` — Build or re-review a multi-recipient payout and get its review
- Action: `write` · MCP tool: `wallet-os.prepare_payout`
- Takes a recipient table or a CSV (UTF-8, header recipient,amount,note) and returns the review a person acts on: the exact rows, the partition into governed batches, the capacity that partition was measured against, and one digest identifying all three. CSV problems are reported BY LINE, all of them, before anything is signed. Duplicate recipients are reported and never silently merged — two rows to one address may be deliberate. Pass draft_id + expected_version to re-review an edited draft; an edit that raced another edit loses rather than overwriting the table someone else is reviewing. Nothing is signed here.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `author` — string (required) — The member building this payout; the draft is theirs until it is governed
  - `label` — string
  - `vault_index` — integer — Defaults to 0
  - `mint` — string — "sol" (default) or an SPL mint address — applies to every row
  - `rows` — object[]
  - `csv` — string — UTF-8 with the versioned header recipient,amount,note. Errors are reported BY LINE, all of them.
  - `distribute_total` — string — One total in base units, split across the rows. Excludes per-row amounts.
  - `duplicate_choice` — "keep_both" | "merge" | "removed" — Required only when a recipient repeats — duplicates are reported, never silently merged.
  - `draft_id` — string — Omit to create; pass to re-review an existing draft
  - `expected_version` — integer — Required with draft_id, so an edit that raced another edit loses rather than overwriting
- Response (JSON):
  - `draft` — object
    - `id` — string
    - `multisig_id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `author` — string
    - `label` — string | null
    - `vault_index` — integer
    - `asset_kind` — string
    - `mint` — string | null
    - `rows[]` — object[]
      - `ordinal` — integer
      - `recipient` — string
      - `amount` — string
      - `note` — string
    - `distribute_total` — string | null
    - `duplicate_choice` — "keep_both" | "merge" | "removed" | null
    - `reviewed_digest` — string | null
    - `reviewed_batch_sizes[]` — integer[] | null
    - `reviewed_cap` — integer | null
    - `reviewed_at` — integer | null
    - `version` — integer
    - `created_at` — integer
    - `updated_at` — integer
    - `discarded` — boolean
  - `review` — object
    - `digest` — string — Identifies the rows, the partition and the capacity together
    - `batches[]` — object[] — One entry per governed batch, in execution order — a partitioned payout is several approvals
      - `batch_ordinal` — integer
      - `recipient_count` — integer
      - `first_ordinal` — integer
      - `last_ordinal` — integer
    - `recipient_cap` — integer
    - `capacity_measured` — false | true — False on a cluster whose settle time has never been measured; the cap is then the conservative bound
    - `window_blocks` — integer
  - `summary` — string

### `GET /v1/wallet-os/payouts/get` — Read one payout draft, and whether an acknowledgement still stands
- Action: `read` · MCP tool: `wallet-os.get_payout`
- Pass acknowledged_digest to learn whether the digest you approved is still current — and when it is not, WHICH of two very different things happened: `network` means you changed nothing and the measured capacity moved (so the partition changed under you), `edited` means the payout itself is different. A person who changed nothing and is told their approval is void needs to be told which one it was.
- Query param `draft_id` (string, required)
- Query param `acknowledged_digest` (string) — Pass a digest you acknowledged to learn whether it still stands, and if not WHY
- Response (JSON):
  - `draft` — object
    - `id` — string
    - `multisig_id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `author` — string
    - `label` — string | null
    - `vault_index` — integer
    - `asset_kind` — string
    - `mint` — string | null
    - `rows[]` — object[]
      - `ordinal` — integer
      - `recipient` — string
      - `amount` — string
      - `note` — string
    - `distribute_total` — string | null
    - `duplicate_choice` — "keep_both" | "merge" | "removed" | null
    - `reviewed_digest` — string | null
    - `reviewed_batch_sizes[]` — integer[] | null
    - `reviewed_cap` — integer | null
    - `reviewed_at` — integer | null
    - `version` — integer
    - `created_at` — integer
    - `updated_at` — integer
    - `discarded` — boolean
  - `acknowledgement` — object
    - `stale` — boolean
    - `cause` — "network" | "edited" — `network` — you changed nothing and the measured capacity moved. `edited` — the payout itself differs.
    - `message` — string

### `GET /v1/wallet-os/payouts` — List an author's live payout drafts for a multisig
- Action: `read` · MCP tool: `wallet-os.list_payouts`
- Scoped to the author: a payout draft is that person's own working table until it becomes a governed batch. Discarded drafts are hidden here and never deleted — wallet-os.get_payout still reads them by id.
- Query param `multisig_id` (string, required)
- Query param `author` (string, required) — Drafts are per-author until they become governed batches
- Response (JSON):
  - `drafts[]` — object[]
    - `id` — string
    - `multisig_id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `author` — string
    - `label` — string | null
    - `vault_index` — integer
    - `asset_kind` — string
    - `mint` — string | null
    - `rows[]` — object[]
      - `ordinal` — integer
      - `recipient` — string
      - `amount` — string
      - `note` — string
    - `distribute_total` — string | null
    - `duplicate_choice` — "keep_both" | "merge" | "removed" | null
    - `reviewed_digest` — string | null
    - `reviewed_batch_sizes[]` — integer[] | null
    - `reviewed_cap` — integer | null
    - `reviewed_at` — integer | null
    - `version` — integer
    - `created_at` — integer
    - `updated_at` — integer
    - `discarded` — boolean
  - `excludes_discarded` — boolean — Discarded drafts are hidden here, never deleted — get_payout still reads them by id

### `POST /v1/wallet-os/payouts/discard` — Discard a payout draft (reversible, never destructive)
- Action: `write` · MCP tool: `wallet-os.discard_payout`
- Soft delete on an expected version. The draft leaves the list and stays readable by id; wallet-os.restore_payout brings it back. A draft dropped by mistake is recoverable — that is the point.
- Request body (JSON, required):
  - `draft_id` — string (required)
  - `expected_version` — integer (required)
- Response (JSON):
  - `draft` — object
    - `id` — string
    - `multisig_id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `author` — string
    - `label` — string | null
    - `vault_index` — integer
    - `asset_kind` — string
    - `mint` — string | null
    - `rows[]` — object[]
      - `ordinal` — integer
      - `recipient` — string
      - `amount` — string
      - `note` — string
    - `distribute_total` — string | null
    - `duplicate_choice` — "keep_both" | "merge" | "removed" | null
    - `reviewed_digest` — string | null
    - `reviewed_batch_sizes[]` — integer[] | null
    - `reviewed_cap` — integer | null
    - `reviewed_at` — integer | null
    - `version` — integer
    - `created_at` — integer
    - `updated_at` — integer
    - `discarded` — boolean
  - `summary` — string

### `POST /v1/wallet-os/payouts/restore` — Restore a discarded payout draft
- Action: `write` · MCP tool: `wallet-os.restore_payout`
- Undo a discard, on an expected version.
- Request body (JSON, required):
  - `draft_id` — string (required)
  - `expected_version` — integer (required)
- Response (JSON):
  - `draft` — object
    - `id` — string
    - `multisig_id` — string
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `author` — string
    - `label` — string | null
    - `vault_index` — integer
    - `asset_kind` — string
    - `mint` — string | null
    - `rows[]` — object[]
      - `ordinal` — integer
      - `recipient` — string
      - `amount` — string
      - `note` — string
    - `distribute_total` — string | null
    - `duplicate_choice` — "keep_both" | "merge" | "removed" | null
    - `reviewed_digest` — string | null
    - `reviewed_batch_sizes[]` — integer[] | null
    - `reviewed_cap` — integer | null
    - `reviewed_at` — integer | null
    - `version` — integer
    - `created_at` — integer
    - `updated_at` — integer
    - `discarded` — boolean
  - `summary` — string

### `GET /v1/wallet-os/imports` — List imported drafts for a multisig, newest first
- Action: `read` · MCP tool: `wallet-os.list_imported_drafts`
- Discarded drafts are hidden by default and included with include_discarded — hidden, never unfindable. The response says which of those two it is rather than leaving a caller to infer it from a count.
- Query param `multisig_id` (string, required)
- Query param `include_discarded` (boolean) — Discarded drafts are hidden by default, never unfindable
- Response (JSON):
  - `drafts[]` — object[]
    - `id` — string
    - `visible_id` — string — The quotable id a person sees, e.g. IMP-01J8ABCD
    - `multisig_id` — string
    - `title` — string | null — What the importer called it. An untitled draft stays untitled.
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `creator` — string
    - `digest` — string — SHA-256 of the governed message this draft becomes — the value a reviewer approves
    - `payload_sha256` — string — Identifies the submitted bytes without retaining them
    - `message_version` — "legacy" | "v0" — What arrived, not what was compiled
    - `instruction_count` — integer
    - `disclosures[]` — string[] — What normalization REMOVED, in the normalizer's own words — the difference between what was submitted and what will execute
    - `status` — "active" | "discarded"
    - `discarded_at` — integer | null
    - `discarded_by` — string | null
    - `created_at` — integer
    - `updated_at` — integer
  - `includes_discarded` — boolean — Said plainly: a list that silently omits discarded drafts looks like a list of all of them

### `POST /v1/wallet-os/imports` — Import a serialized transaction as a reviewable draft
- Action: `write` · MCP tool: `wallet-os.import_transaction`
- Accepts a legacy or v0 serialized message, or a full transaction, and normalizes it: the fee payer's envelope role, the blockhash, every signature and recognized Compute Budget controls are removed, while retained instruction order and data are preserved byte-for-byte. v0 lookup tables are PRESERVED, not flattened. Everything removed is disclosed in `disclosures` — that delta is the whole risk of importing, because it is the one thing you cannot see by reading your own payload. Nothing is signed: governance builds a new outer message, so any signatures on the input cannot carry over and are never approvals here. The payload itself is not retained, only its hash.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `payload_base64` — string (required) — A serialized legacy or v0 message, or a full transaction, base64
  - `creator` — string (required) — The member importing it; becomes the governed action's creator
  - `title` — string — Optional. An untitled draft stays untitled rather than being given a number.
  - `vault_index` — integer — Defaults to 0
- Response (JSON):
  - `draft` — object
    - `id` — string
    - `visible_id` — string — The quotable id a person sees, e.g. IMP-01J8ABCD
    - `multisig_id` — string
    - `title` — string | null — What the importer called it. An untitled draft stays untitled.
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `creator` — string
    - `digest` — string — SHA-256 of the governed message this draft becomes — the value a reviewer approves
    - `payload_sha256` — string — Identifies the submitted bytes without retaining them
    - `message_version` — "legacy" | "v0" — What arrived, not what was compiled
    - `instruction_count` — integer
    - `disclosures[]` — string[] — What normalization REMOVED, in the normalizer's own words — the difference between what was submitted and what will execute
    - `status` — "active" | "discarded"
    - `discarded_at` — integer | null
    - `discarded_by` — string | null
    - `created_at` — integer
    - `updated_at` — integer
  - `programs[]` — string[] — Every distinct program a retained instruction targets — an unfamiliar entry is the thing to stop on
  - `discarded_signature_count` — integer — Signatures on the input. They signed the transaction submitted, not the governed one, so they are never approvals here.
  - `summary` — string

### `GET /v1/wallet-os/imports/get` — Read one imported draft by id, discarded or not
- Action: `read` · MCP tool: `wallet-os.get_imported_draft`
- A discarded draft is still readable here. Discard hides it from the list; it never deletes it, and a restore surface has to be able to show what it is restoring.
- Query param `draft_id` (string, required)
- Response (JSON):
  - `draft` — object
    - `id` — string
    - `visible_id` — string — The quotable id a person sees, e.g. IMP-01J8ABCD
    - `multisig_id` — string
    - `title` — string | null — What the importer called it. An untitled draft stays untitled.
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `creator` — string
    - `digest` — string — SHA-256 of the governed message this draft becomes — the value a reviewer approves
    - `payload_sha256` — string — Identifies the submitted bytes without retaining them
    - `message_version` — "legacy" | "v0" — What arrived, not what was compiled
    - `instruction_count` — integer
    - `disclosures[]` — string[] — What normalization REMOVED, in the normalizer's own words — the difference between what was submitted and what will execute
    - `status` — "active" | "discarded"
    - `discarded_at` — integer | null
    - `discarded_by` — string | null
    - `created_at` — integer
    - `updated_at` — integer

### `POST /v1/wallet-os/imports/discard` — Discard an imported draft (reversible, never destructive)
- Action: `write` · MCP tool: `wallet-os.discard_imported_draft`
- Records who discarded it and when, and the draft leaves the default list. Nothing is deleted. Idempotent: discarding an already-discarded draft returns it unchanged rather than erroring, because a caller retrying a click has not done anything wrong.
- Request body (JSON, required):
  - `draft_id` — string (required)
- Response (JSON):
  - `draft` — object
    - `id` — string
    - `visible_id` — string — The quotable id a person sees, e.g. IMP-01J8ABCD
    - `multisig_id` — string
    - `title` — string | null — What the importer called it. An untitled draft stays untitled.
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `creator` — string
    - `digest` — string — SHA-256 of the governed message this draft becomes — the value a reviewer approves
    - `payload_sha256` — string — Identifies the submitted bytes without retaining them
    - `message_version` — "legacy" | "v0" — What arrived, not what was compiled
    - `instruction_count` — integer
    - `disclosures[]` — string[] — What normalization REMOVED, in the normalizer's own words — the difference between what was submitted and what will execute
    - `status` — "active" | "discarded"
    - `discarded_at` — integer | null
    - `discarded_by` — string | null
    - `created_at` — integer
    - `updated_at` — integer
  - `summary` — string

### `POST /v1/wallet-os/imports/restore` — Restore a discarded imported draft
- Action: `write` · MCP tool: `wallet-os.restore_imported_draft`
- Returns it to the list and clears the discard record — a restored draft that still named who discarded it would show a removal that no longer happened.
- Request body (JSON, required):
  - `draft_id` — string (required)
- Response (JSON):
  - `draft` — object
    - `id` — string
    - `visible_id` — string — The quotable id a person sees, e.g. IMP-01J8ABCD
    - `multisig_id` — string
    - `title` — string | null — What the importer called it. An untitled draft stays untitled.
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `creator` — string
    - `digest` — string — SHA-256 of the governed message this draft becomes — the value a reviewer approves
    - `payload_sha256` — string — Identifies the submitted bytes without retaining them
    - `message_version` — "legacy" | "v0" — What arrived, not what was compiled
    - `instruction_count` — integer
    - `disclosures[]` — string[] — What normalization REMOVED, in the normalizer's own words — the difference between what was submitted and what will execute
    - `status` — "active" | "discarded"
    - `discarded_at` — integer | null
    - `discarded_by` — string | null
    - `created_at` — integer
    - `updated_at` — integer
  - `summary` — string

### `GET /v1/wallet-os/org/policies` — List the org's reusable policies with their applications
- Action: `read` · MCP tool: `wallet-os.list_org_policies`
- Every policy carries its plane and the honesty copy for it: hosted_guardrail is a review-time aid (direct on-chain action can bypass it — it says so); on_chain is consensus-enforced once its governed config proposal executes. Applications attach a policy to a project or a specific multisig.
- Response (JSON):
  - `policies[]` — object[]
    - `policy_id` — string
    - `name` — string
    - `kind` — "destination_allowlist" | "spending_limit_template"
    - `plane` — "hosted_guardrail" | "on_chain"
    - `definition` — object | null
    - `definition_error` — string | null
    - `status` — "active" | "archived" | "quarantined"
    - `version` — integer
    - `created_at` — integer
    - `updated_at` — integer
    - `archived` — boolean
    - `applications[]` — object[]
      - `project_id` — string | null
      - `multisig_id` — string | null
      - `applied_at` — integer
      - `applied_by` — string
      - `application_id` — string
      - `removed_at` — integer | null
      - `state` — "pending" | "submitted" | "effective" | "removal_pending" | "removed" | "failed" | "cancelled" | "stale" | "remove_failed" | string | string
      - `target_counts` — object
      - `next_step` — string | null
      - `targets[]` — object[]
    - `plane_note` — string

### `POST /v1/wallet-os/org/policies/upsert` — Create, update, or archive a reusable policy
- Action: `write` · MCP tool: `wallet-os.upsert_org_policy`
- Org admin. Kinds: destination_allowlist (hosted_guardrail — prepare_action flags non-allowlisted destinations at review, never a rejection) and spending_limit_template  (on_chain — applying emits a governed AddSpendingLimit proposal). The kind/plane pairing is enforced with corrective errors. Writes are recorded in the org audit log.
- Request body (JSON, required):
  - `policy_id` — string — Omit to create; pass to edit, archive, or restore
  - `name` — string (required)
  - `kind` — "destination_allowlist" | "spending_limit_template" (required)
  - `plane` — "hosted_guardrail" | "on_chain" (required)
  - `definition` — object (required) — Kind-specific definition — validated against the kind's schema
  - `archive` — boolean — true archives, false restores, omitted leaves the archival state alone
  - `expected_version` — integer — The version you read — the edit is rejected if someone else changed it first
- Response (JSON):
  - `policy_id` — string
  - `name` — string
  - `kind` — "destination_allowlist" | "spending_limit_template"
  - `plane` — "hosted_guardrail" | "on_chain"
  - `definition` — object | null
  - `definition_error` — string | null
  - `status` — "active" | "archived" | "quarantined"
  - `version` — integer
  - `created_at` — integer
  - `updated_at` — integer
  - `archived` — boolean
  - `applications[]` — object[]
    - `project_id` — string | null
    - `multisig_id` — string | null
    - `applied_at` — integer
    - `applied_by` — string
    - `application_id` — string
    - `removed_at` — integer | null
    - `state` — "pending" | "submitted" | "effective" | "removal_pending" | "removed" | "failed" | "cancelled" | "stale" | "remove_failed" | string | string
    - `target_counts` — object
    - `next_step` — string | null
    - `targets[]` — object[]
      - `multisig_id` — string
      - `cluster` — string
      - `config_address` — string
      - `state` — "pending" | "submitted" | "effective" | "removal_pending" | "removed" | "failed" | "cancelled" | "stale" | "remove_failed"
      - `transaction_index` — integer | null
      - `action_digest` — string | null
      - `proposal_signature` — string | null
      - `failure_code` — string | null
      - `failure_message` — string | null
      - `attempt` — integer
      - `updated_at` — integer
  - `plane_note` — string
  - `created` — boolean
  - `summary` — string

### `GET /v1/wallet-os/org/policies/history` — The append-only revision history of one policy
- Action: `read` · MCP tool: `wallet-os.get_org_policy_history`
- Every content change, archive and restore appends an immutable revision with its author and time. An edit never rewrites history, so an approver can see exactly what a policy said when it was applied.
- Query param `policy_id` (string, required)
- Response (JSON):
  - `policy_id` — string
  - `name` — string
  - `status` — "active" | "archived" | "quarantined"
  - `version` — integer
  - `revisions[]` — object[]
    - `version` — integer
    - `name` — string
    - `definition` — any
    - `status` — "active" | "archived" | "quarantined"
    - `change_kind` — string
    - `actor` — string
    - `created_at` — integer

### `POST /v1/wallet-os/org/policies/apply` — Apply a policy to a project or a multisig
- Action: `write` · MCP tool: `wallet-os.apply_policy`
- Org admin. The governed proposal is PREPARED FIRST and the application is recorded second, so a failed application is never recorded as though the lifecycle moved forward. Each target multisig gets its own state (pending → submitted → effective), so a project-wide application whose second multisig failed reports `partial` with exact counts. An on-chain template is never called effective until its proposal executes; hosted guardrails are review-time flags in prepare_action and say they are bypassable.
- Request body (JSON, required):
  - `policy_id` — string (required)
  - `project_id` — string — Apply to every multisig under this project
  - `multisig_id` — string — Apply to one multisig (exactly one of project_id/multisig_id)
  - `creator` — string — Required when applying a spending_limit_template — the Initiate member who signs each governed proposal
- Response (JSON):
  - `policy_id` — string
  - `application_id` — string
  - `project_id` — string | null
  - `multisig_id` — string | null
  - `applied` — boolean
  - `plane` — "hosted_guardrail" | "on_chain"
  - `plane_note` — string
  - `state` — "pending" | "submitted" | "effective" | "removal_pending" | "removed" | "failed" | "cancelled" | "stale" | "remove_failed" | string | string
  - `target_counts` — object
  - `next_step` — string | null
  - `targets[]` — object[]
    - `multisig_id` — string
    - `cluster` — string
    - `config_address` — string
    - `state` — "pending" | "submitted" | "effective" | "removal_pending" | "removed" | "failed" | "cancelled" | "stale" | "remove_failed"
    - `transaction_index` — integer | null
    - `action_digest` — string | null
    - `proposal_signature` — string | null
    - `failure_code` — string | null
    - `failure_message` — string | null
    - `attempt` — integer
    - `updated_at` — integer
  - `governed_proposal` — object | null
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `transaction_base64` — string
    - `message_base64` — string
    - `required_signers[]` — string[]
    - `recent_blockhash` — string
    - `last_valid_block_height` — integer
    - `authorization` — object
      - `cu_limit` — integer
      - `cu_price_micro_lamports` — integer
      - `base_fee_lamports` — integer
      - `priority_fee_lamports` — integer
      - `max_total_network_cost_lamports` — integer
      - `limit_basis` — "simulated" | "implicit_runtime_budget"
      - `simulated_units` — integer | null
      - `quote_id` — string | null
      - `quote_confidence` — "high" | "medium" | "low" | null
      - `envelope_revision` — integer
    - `transaction_index` — integer
    - `transaction_address` — string
    - `spending_limit_accounts[]` — string[]
    - `digest` — string
    - `replacement` — object
      - `old_address` — string
      - `new_address` — string
      - `old_remaining` — string
      - `old_spent` — string
      - `new_immediate_allowance` — string
      - `restored_allowance` — string
      - `requires_confirmation` — boolean
      - `period_reset` — string
      - `next_reset_unix` — integer | null
      - `rent_to` — string
      - `rent_lamports` — integer
      - `destinations[]` — string[]
    - `summary` — string
  - `summary` — string

### `POST /v1/wallet-os/org/policies/remove-application` — Unapply a policy from a project or one multisig
- Action: `write` · MCP tool: `wallet-os.remove_policy_application`
- Org admin. A hosted guardrail is removed immediately (a versioned, audited hosted change). An EXECUTED on-chain spending limit cannot be retracted by K256: this PREPARES the compensating governed proposal, returns it for signature, and holds the target at removal_pending until that proposal executes. This removes no on-chain member, multisig, proposal, vault or asset.
- Request body (JSON, required):
  - `application_id` — string (required)
  - `multisig_id` — string — Remove one target of a project-wide application; omit to remove every target
  - `creator` — string — Required when a target's on-chain spending limit already EXECUTED — the Initiate member who will sign the compensating governed proposal
- Response (JSON):
  - `application_id` — string
  - `policy_id` — string
  - `state` — "pending" | "submitted" | "effective" | "removal_pending" | "removed" | "failed" | "cancelled" | "stale" | "remove_failed" | string | string
  - `target_counts` — object
  - `targets[]` — object[]
    - `multisig_id` — string
    - `cluster` — string
    - `config_address` — string
    - `state` — "pending" | "submitted" | "effective" | "removal_pending" | "removed" | "failed" | "cancelled" | "stale" | "remove_failed"
    - `transaction_index` — integer | null
    - `action_digest` — string | null
    - `proposal_signature` — string | null
    - `failure_code` — string | null
    - `failure_message` — string | null
    - `attempt` — integer
    - `updated_at` — integer
  - `skipped[]` — object[]
    - `multisig_id` — string
    - `state` — "pending" | "submitted" | "effective" | "removal_pending" | "removed" | "failed" | "cancelled" | "stale" | "remove_failed"
    - `reason` — string
  - `next_step` — string | null
  - `governed_proposal` — object | null
    - `cluster` — "mainnet" | "devnet" | "testnet"
    - `transaction_base64` — string
    - `message_base64` — string
    - `required_signers[]` — string[]
    - `recent_blockhash` — string
    - `last_valid_block_height` — integer
    - `authorization` — object
      - `cu_limit` — integer
      - `cu_price_micro_lamports` — integer
      - `base_fee_lamports` — integer
      - `priority_fee_lamports` — integer
      - `max_total_network_cost_lamports` — integer
      - `limit_basis` — "simulated" | "implicit_runtime_budget"
      - `simulated_units` — integer | null
      - `quote_id` — string | null
      - `quote_confidence` — "high" | "medium" | "low" | null
      - `envelope_revision` — integer
    - `transaction_index` — integer
    - `transaction_address` — string
    - `spending_limit_accounts[]` — string[]
    - `digest` — string
    - `replacement` — object
      - `old_address` — string
      - `new_address` — string
      - `old_remaining` — string
      - `old_spent` — string
      - `new_immediate_allowance` — string
      - `restored_allowance` — string
      - `requires_confirmation` — boolean
      - `period_reset` — string
      - `next_reset_unix` — integer | null
      - `rent_to` — string
      - `rent_lamports` — integer
      - `destinations[]` — string[]
    - `summary` — string
  - `summary` — string

### `GET /v1/wallet-os/org/session-trust` — Session trust signals for the current caller
- Action: `read` · MCP tool: `wallet-os.get_session_trust`
- Device label/recognition, geo (from the edge), and session age for the current session. Every field degrades to an explicit neutral (null + a notes entry, trust_pipeline 'degraded') when a signal pipeline has no data — never an error, never a fabricated value.
- Response (JSON):
  - `trust_pipeline` — "ok" | "degraded"
  - `org_id` — string
  - `workos_user_id` — string | null
  - `role` — string | null
  - `device` — object
    - `label` — string | null
    - `recognized` — boolean | null
  - `geo` — object | null
    - `country_code` — string | null
    - `city` — string | null
    - `region` — string | null
    - `timezone` — string | null
    - `asn` — integer | null
    - `ip_present` — boolean
  - `session` — object
    - `age_seconds` — integer | null
    - `issued_at` — integer | null
  - `notes[]` — string[]

### `GET /v1/wallet-os/org/inbox` — The caller's notification inbox
- Action: `read` · MCP tool: `wallet-os.get_inbox`
- The caller's items plus org-wide ones (workspace events: projects created, signers bound, policies applied), newest first. v1: org-wide items share their read state.
- Response (JSON):
  - `items[]` — object[]
    - `inbox_id` — string
    - `kind` — string
    - `title` — string
    - `body` — string
    - `link` — string | null
    - `outcome_id` — string | null
    - `org_wide` — boolean
    - `created_at` — integer
    - `read` — boolean
    - `read_at` — integer | null
  - `unread` — integer
  - `channels[]` — object[]
    - `channel` — string
    - `enabled` — boolean
    - `note` — string — what this channel does today, in plain words

### `POST /v1/wallet-os/org/inbox/read` — Mark an inbox item read
- Action: `write` · MCP tool: `wallet-os.mark_inbox_read`
- Marks one of the caller's (or an org-wide) inbox items read.
- Request body (JSON, required):
  - `inbox_id` — string (required)
- Response (JSON):
  - `inbox_id` — string
  - `read` — boolean
  - `read_at` — integer | null
  - `summary` — string

### `POST /v1/wallet-os/org/notification-prefs` — Set the caller's notification channel preferences
- Action: `write` · MCP tool: `wallet-os.update_notification_prefs`
- Per-channel toggles for the caller. The inbox channel is live; the email channel stores the preference only — delivery lands in a later slice and the response says so.
- Request body (JSON, required):
  - `channel` — "inbox" | "email" (required) — "email" stores the preference only — delivery lands in a later slice
  - `enabled` — boolean (required)
- Response (JSON):
  - `prefs[]` — object[]
    - `channel` — string
    - `enabled` — boolean
    - `updated_at` — integer
  - `summary` — string

### `GET /v1/wallet-os/activity/{multisig_id}` — The multisig's receipt index — durable history that survives account closure
- Action: `read` · MCP tool: `wallet-os.get_activity`
- One page of the multisig's immutable lifecycle receipts (create, open, activate, approve, reject, cancel, execute, failure, close — chain evidence captured durably BEFORE rent-bearing accounts are closed, with the actual network signature where one was observed) merged with the workspace audit events about this multisig. Reclaiming rent removes nothing here. Cursor paging is keyset over the immutable hosted record time plus receipt id (never chain block time, which stays an evidence field); every page carries the REAL total under the active filters (plane, kind, actor, from/to, index, object). A live proposal scan runs on the first page and appends any independently verifiable chain fact not yet receipted; a proof field with no recorded evidence is null and the response names the exact sentence to render for it. The org-wide feed is wallet-os.get_org_audit; one receipt resolves by id via wallet-os.get_receipt.
- Path param `multisig_id` (string, required)
- Query param `cursor` (string) — next_cursor from the previous page; omit for the first page
- Query param `limit` (string) — Rows per page, 1-100 (default 30)
- Query param `plane` ("chain" | "workspace") — chain = immutable lifecycle receipts; workspace = hosted org audit
- Query param `kind` (string) — Comma-separated event kinds: create, open, activate, approve, reject, cancel, execute, failure, close, stage
- Query param `actor` (string) — Signer address (chain) or workspace user (workspace), exact
- Query param `from` (string) — Inclusive lower bound — unix seconds or ISO date
- Query param `to` (string) — Inclusive upper bound — unix seconds or ISO date
- Query param `index` (string) — One transaction index
- Query param `object` (string) — An object the receipt names: proposal/transaction/config address or signature
- Response (JSON):
  - `multisig_id` — string
  - `items[]` — object[]
    - `receipt_id` — string
    - `plane` — "chain" | "workspace"
    - `evidence` — "chain" | "hosted"
    - `kind` — string
    - `effect` — string
    - `actor` — string | null
    - `authority` — string | null
    - `state` — string | null
    - `next_step` — string | null
    - `recorded_at` — integer
    - `transaction_index` — integer | null
    - `cluster` — string | null
    - `config_address` — string | null
    - `proposal_address` — string | null
    - `transaction_address` — string | null
    - `digest` — string | null
    - `signature` — string | null
    - `observed_slot` — integer | null
    - `observed_block_time` — integer | null
    - `votes[]` — object[] | null
      - `actor` — string
      - `vote` — string
    - `result` — any | null
    - `target` — string | null
  - `next_cursor` — string | null
  - `total` — integer
  - `plane_note` — string
  - `no_receipt_sentence` — string
  - `chain_sweep_note` — string | null

### `GET /v1/wallet-os/receipts/{receipt_id}` — Resolve one receipt by id
- Action: `read` · MCP tool: `wallet-os.get_receipt`
- One receipt with its full proof (effect, actor, authority, state, transaction index, proposal/transaction accounts, digest, votes, network signature, observed slot) plus every submission attempt of its logical step. Deep links stay resolvable after the row falls outside the first activity page or its rent-bearing on-chain accounts are closed.
- Path param `receipt_id` (string, required) — A receipt id from wallet-os.get_activity (wosr1_… or oaudit_…)
- Response (JSON):
  - `receipt` — object
    - `receipt_id` — string
    - `plane` — "chain" | "workspace"
    - `evidence` — "chain" | "hosted"
    - `kind` — string
    - `effect` — string
    - `actor` — string | null
    - `authority` — string | null
    - `state` — string | null
    - `next_step` — string | null
    - `recorded_at` — integer
    - `transaction_index` — integer | null
    - `cluster` — string | null
    - `config_address` — string | null
    - `proposal_address` — string | null
    - `transaction_address` — string | null
    - `digest` — string | null
    - `signature` — string | null
    - `observed_slot` — integer | null
    - `observed_block_time` — integer | null
    - `votes[]` — object[] | null
      - `actor` — string
      - `vote` — string
    - `result` — any | null
    - `target` — string | null
  - `attempts[]` — object[]
    - `attempt_id` — string
    - `status` — "confirmed" | "finalized" | "failed" | "rejected" | "unconfirmed"
    - `signature` — string
    - `signed_message_hash` — string
    - `envelope_revision` — integer
    - `slot` — integer | null
    - `error` — any | null
    - `recorded_at` — integer
  - `multisig_id` — string | null

### `GET /v1/wallet-os/activity/{multisig_id}/export` — Resumable full-range CSV export of the receipt index
- Action: `read` · MCP tool: `wallet-os.export_activity`
- A full requested-range export against a FROZEN receipt-ID cutoff — later appends never change the artifact, and it is chunked rather than an unbounded synchronous response. Call without export_id to freeze a new export (the same filters as get_activity) and receive the first CSV chunk; pass export_id + next_after to fetch the following chunks, and re-fetch any chunk after an interruption — chunks are deterministic. Rows identify chain versus hosted evidence, carry the actual network signature (or the exact no-receipt sentence), and the completed export records a verifiable chained artifact digest.
- Path param `multisig_id` (string, required)
- Query param `export_id` (string) — Omit to freeze a new export; pass to fetch/resume its chunks
- Query param `after` (string) — next_after from the previous chunk; omit for the first chunk
- Query param `format` ("csv") — CSV is the audit artifact format
- Query param `plane` ("chain" | "workspace") — chain = immutable lifecycle receipts; workspace = hosted org audit
- Query param `kind` (string) — Comma-separated event kinds: create, open, activate, approve, reject, cancel, execute, failure, close, stage
- Query param `actor` (string) — Signer address (chain) or workspace user (workspace), exact
- Query param `from` (string) — Inclusive lower bound — unix seconds or ISO date
- Query param `to` (string) — Inclusive upper bound — unix seconds or ISO date
- Query param `index` (string) — One transaction index
- Query param `object` (string) — An object the receipt names: proposal/transaction/config address or signature
- Response (JSON):
  - `multisig_id` — string
  - `export_id` — string
  - `format` — "csv"
  - `content_type` — string
  - `csv` — string
  - `rows` — integer
  - `rows_total` — integer
  - `rows_emitted` — integer
  - `next_after` — string | null
  - `complete` — boolean
  - `artifact_digest` — string | null
  - `export_status` — "in_progress" | "complete"
  - `cutoff` — object
    - `recorded_at` — integer
    - `receipt_id` — string

### `GET /v1/wallet-os/org/audit` — The org audit log (workspace events + the platform write-audit)
- Action: `read` · MCP tool: `wallet-os.get_org_audit`
- Workspace-plane org events (actor, action, target, plane tag) plus the platform write-audit trail for the org's wallet-os ops (op, result, request id — signing submissions included). Cursor-paged with the real total disclosed — never a silent cap. The platform trail degrades to available:false where the store is not bound — never an error.
- Query param `cursor` (string) — next_cursor from the previous page; omit for the first page
- Query param `limit` (string) — Rows per page, 1-200 (default 100)
- Response (JSON):
  - `events[]` — object[]
    - `audit_id` — string
    - `action` — string
    - `plane` — string
    - `actor` — string
    - `target` — string | null
    - `meta` — any
    - `created_at` — integer
  - `next_cursor` — string | null
  - `total` — integer
  - `platform_audit` — object
    - `available` — boolean
    - `events[]` — object[]
      - `action` — string
      - `result` — string
      - `actor_id` — string
      - `actor_type` — string
      - `timestamp` — integer
      - `request_id` — string | null
    - `note` — string
  - `plane_note` — string
