# 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.
- 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
  - `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 fails the list rather than rendering stale chain state.
- 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
    - `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

### `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, and close eligibility.

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
      - `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
      - `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
      - `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
      - `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 stored vault message (raw decode — program ids, account metas with signer/writable flags, instruction data as hex; no invented meaning), the action digest (SHA-256 over the exact stored message bytes), and close eligibility per the rent close matrix. Message and digest are present for vault transactions; batch actions carry the batch fields only at M0.
- 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
  - `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
  - `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` — Record why an action was proposed
- Action: `write` · MCP tool: `wallet-os.set_action_note`
- Attaches a free-text reason to an action, shown to every approver in the queue and on the action itself. Workspace context only: it is stored off chain, cannot affect what the program executes, and is visible only to this workspace. Call it AFTER the creating transaction has landed — an index whose transaction was never submitted is reused by the next action. Idempotent: writing again replaces the 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
- Response (JSON):
  - `multisig_id` — string
  - `transaction_index` — integer
  - `note` — string
  - `updated_at` — integer

### `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
  - `transaction_index` — integer
  - `transaction_address` — string
  - `digest` — 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.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `creator` — string (required) — Member with the Initiate permission; signs
  - `vault_index` — integer
  - `intent` — object | 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
  - `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
  - `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

### `POST /v1/wallet-os/actions/simulate` — Simulate a prepared action against current state
- Action: `write` · MCP tool: `wallet-os.simulate_action`
- Independent simulation of any prepare output via the cluster's simulateTransaction (placeholder signatures, live blockhash): success or the verbatim program error (never rendered as success), logs, compute units, and a lamport/data effect diff per writable account. 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
  - `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
  - `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
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — 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.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `transaction_index` — string (required)
  - `member` — string (required) — The acting member; signs
  - `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
  - `transaction_index` — integer
  - `proposal_address` — string
  - `summary` — string

### `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
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — 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
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — 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
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `transaction_index` — integer
  - `proposal_address` — string
  - `summary` — string

### `POST /v1/wallet-os/transactions/submit` — Broadcast a signed transaction
- Action: `write` · MCP tool: `wallet-os.submit_transaction`
- Broadcasts a caller-signed transaction (from any prepare op), waits for confirmation, and reports the signature, slot, and status. An on-chain failure returns the program's error; a slow confirmation returns unconfirmed with the signature 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

### `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
  - `scanned_indices` — integer
  - `observed_slot` — integer

### `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
  - `transaction_index` — integer
  - `transaction_address` — string
  - `spending_limit_accounts[]` — string[]
  - `digest` — 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
  - `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
  - `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.
- Request body (JSON, required):
  - `multisig_id` — string (required)
  - `member` — string (required) — The batch creator; signs
  - `batch_index` — integer (required)
  - `intent` — 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
  - `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
  - `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)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — 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
  - `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
  - `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
  - `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
  - `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 AND is the required signer and fee payer of the returned transaction — no other account can submit it, so offer this only to that account. 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)
- Response (JSON):
  - `cluster` — "mainnet" | "devnet" | "testnet"
  - `transaction_base64` — string
  - `message_base64` — string
  - `required_signers[]` — string[]
  - `recent_blockhash` — string
  - `last_valid_block_height` — integer
  - `transaction_index` — integer
  - `closed_accounts[]` — string[]
  - `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
    - `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
  - `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
  - `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
  - `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
  - `buffer_address` — string
  - `reclaim_lamports` — integer
  - `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); archiving is not undone here
- 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 (required)
- Response (JSON):
  - `multisig_id` — string
  - `project_id` — string
  - `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

### `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` — any
    - `created_at` — integer
    - `updated_at` — integer
    - `archived` — boolean
    - `applications[]` — object[]
      - `project_id` — string | null
      - `multisig_id` — string | null
      - `applied_at` — integer
      - `applied_by` — string
    - `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 update (name/definition) or archive
  - `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
- Response (JSON):
  - `policy_id` — string
  - `name` — string
  - `kind` — "destination_allowlist" | "spending_limit_template"
  - `plane` — "hosted_guardrail" | "on_chain"
  - `definition` — any
  - `created_at` — integer
  - `updated_at` — integer
  - `archived` — boolean
  - `applications[]` — object[]
    - `project_id` — string | null
    - `multisig_id` — string | null
    - `applied_at` — integer
    - `applied_by` — string
  - `plane_note` — string
  - `created` — boolean
  - `summary` — string

### `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. For a spending_limit_template applied to a multisig, the response carries the governed config proposal payload (the same machinery as wallet-os.prepare_spending_limit_change), ready for signature — nothing executes here and nothing ever mutates chain state directly. Hosted guardrails are a record only: they surface as review-time flags in prepare_action.
- 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 to a multisig — the Initiate member who signs the governed proposal
- Response (JSON):
  - `policy_id` — string
  - `project_id` — string | null
  - `multisig_id` — string | null
  - `applied` — boolean
  - `plane` — "hosted_guardrail" | "on_chain"
  - `plane_note` — string
  - `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
    - `transaction_index` — integer
    - `transaction_address` — string
    - `spending_limit_accounts[]` — string[]
    - `digest` — 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
    - `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}` — Unified activity for a multisig (chain + workspace planes)
- Action: `read` · MCP tool: `wallet-os.get_activity`
- Chain-plane events (proposal states and votes from live decodes) merged with the workspace-plane events ABOUT THIS MULTISIG — an action targeting it, or one targeting something else that names it (applying a policy targets the policy and names the multisig) — each tagged with its plane, newest first. Neither plane fabricates the other. The org-wide feed is a different question: use wallet-os.get_org_audit. A timestamp is null only for a proposal mid-execution, the one on-chain status carrying none; it is never back-filled with a fake date, and nulls sort first as the newest thing there is.
- Path param `multisig_id` (string, required)
- Response (JSON):
  - `multisig_id` — string
  - `items[]` — object[]
    - `plane` — "chain" | "workspace"
    - `kind` — string
    - `summary` — string
    - `timestamp` — integer | null
    - `transaction_index` — integer
  - `plane_note` — string

### `GET /v1/wallet-os/activity/{multisig_id}/export` — Export a multisig's activity as CSV or JSON
- Action: `read` · MCP tool: `wallet-os.export_activity`
- The get_activity dataset in the requested format (format=csv|json).
- Path param `multisig_id` (string, required)
- Query param `format` ("json" | "csv")
- Response (JSON):
  - `multisig_id` — string
  - `format` — "json" | "csv"
  - `content_type` — string
  - `rows` — integer
  - `items[]` — object[]
    - `plane` — "chain" | "workspace"
    - `kind` — string
    - `summary` — string
    - `timestamp` — integer | null
    - `transaction_index` — integer
  - `csv` — 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). The platform trail degrades to available:false where the store is not bound — never an error.
- Response (JSON):
  - `events[]` — object[]
    - `audit_id` — string
    - `action` — string
    - `plane` — string
    - `actor` — string
    - `target` — string | null
    - `meta` — any
    - `created_at` — 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
