From 2f6d66694d35fcf79cfe9c11c3ea45b18b5bcf70 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 15:27:05 -0500 Subject: [PATCH 01/12] docs(multichain-api-middleware): add wallet-side Multichain API reference Adds MULTICHAIN_API.md, a readable reference for the CAIP-25 / CAIP-27 Multichain API as actually implemented by this package and @metamask/chain-agnostic-permission: wallet_createSession inputs/outputs, the other session methods, supported methods per namespace (static EVM vs Snap-resolved non-EVM), sessionProperties allowlist (incl. the eip1193-compatible flow), error codes, and divergences from the current upstream CAIP-25 spec. Links it from the package README. --- .../MULTICHAIN_API.md | 418 ++++++++++++++++++ packages/multichain-api-middleware/README.md | 8 + 2 files changed, 426 insertions(+) create mode 100644 packages/multichain-api-middleware/MULTICHAIN_API.md diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md new file mode 100644 index 0000000000..3cf2050e2c --- /dev/null +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -0,0 +1,418 @@ +# The MetaMask Multichain API + +A readable, wallet-side reference for MetaMask's CAIP-25 / CAIP-27 based Multichain +API, as actually implemented by `@metamask/multichain-api-middleware` and +`@metamask/chain-agnostic-permission`. + +> **Audience.** This document describes the **wallet's JSON-RPC contract** — the +> requests a caller (dapp / SDK) sends and the responses MetaMask returns. If you +> are integrating a dapp, you usually want the +> [MetaMask Connect SDK](https://github.com/MetaMask/connect-monorepo) instead, +> which wraps this API. This is the layer underneath that. + +> **Source of truth.** Behavior is described from the implementation in this +> package (`src/handlers/*.ts`) and `@metamask/chain-agnostic-permission`. The +> machine-readable schema lives in +> [`@metamask/api-specs`](https://github.com/MetaMask/api-specs) +> (`multichain/openrpc.yaml`). Where this prose and the OpenRPC schema disagree, +> the handler code is authoritative — please file an issue so we can reconcile +> them. + +## Contents + +- [Overview](#overview) +- [Concepts](#concepts) +- [Methods](#methods) + - [`wallet_createSession`](#wallet_createsession) + - [`wallet_getSession`](#wallet_getsession) + - [`wallet_revokeSession`](#wallet_revokesession) + - [`wallet_invokeMethod`](#wallet_invokemethod) +- [Notifications](#notifications) + - [`wallet_sessionChanged`](#wallet_sessionchanged) + - [`wallet_notify`](#wallet_notify) +- [Supported methods & notifications per namespace](#supported-methods--notifications-per-namespace) +- [Error codes](#error-codes) +- [Divergences from current CAIP-25](#divergences-from-current-caip-25) +- [MetaMask-specific behavior](#metamask-specific-behavior) +- [Source-of-truth pointers](#source-of-truth-pointers) + +## Overview + +The Multichain API lets a caller negotiate a single **session** that spans +multiple chains and ecosystems (EVM, Solana, Bitcoin, Tron) in one authorization, +then invoke methods on any authorized scope. It replaces the per-chain EIP-1193 +model (`eth_requestAccounts` on one chain at a time) with a chain-agnostic, +scope-based model. + +It is built on the CASA Chain Agnostic standards: + +- **[CAIP-25](https://chainagnostic.org/CAIPs/caip-25)** — `wallet_createSession`, session negotiation +- **[CAIP-27](https://chainagnostic.org/CAIPs/caip-27)** — `wallet_invokeMethod`, invoking a method on a scope +- **[CAIP-285](https://chainagnostic.org/CAIPs/caip-285)** — `wallet_revokeSession` +- **[CAIP-311](https://chainagnostic.org/CAIPs/caip-311)** — `wallet_sessionChanged` +- **[CAIP-312](https://chainagnostic.org/CAIPs/caip-312)** — `wallet_getSession` +- **[CAIP-2](https://chainagnostic.org/CAIPs/caip-2)** / **[CAIP-10](https://chainagnostic.org/CAIPs/caip-10)** / **[CAIP-217](https://chainagnostic.org/CAIPs/caip-217)** — chain IDs, account IDs, scope objects + +For MetaMask's design rationale see +[MIP-5](https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-5.md). +[MIP-6](https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-6.md) +is **historical** — it predates the current implementation and the upstream CAIP-25 +rewrite, so don't rely on it for current behavior. + +> ⚠️ **CAIP-25 moved; MetaMask did not (yet).** Upstream CAIP-25 was restructured +> in July–August 2025 (single `scopes`, `properties`/`capabilities` renames, bare +> accounts, chain-only scope keys). MetaMask still implements the **pre-rewrite** +> shape (`requiredScopes`/`optionalScopes`, `sessionProperties`/`scopedProperties`, +> CAIP-10 accounts, namespace-scoped keys). See +> [Divergences from current CAIP-25](#divergences-from-current-caip-25). + +## Concepts + +- **Scope string** — a CAIP-2 chain id (`eip155:1`, `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`) + or a CAIP-104 namespace-level scope (`wallet`, `wallet:eip155`). Pattern: + `[-a-z0-9]{3,8}(:[-_a-zA-Z0-9]{1,32})?`. +- **Scope object** — per CAIP-217, an object with `methods`, `notifications`, and + (in responses) `accounts`. In requests it may also carry `references` (namespace + shorthand). Keyed by scope string. +- **Account** — a fully-qualified **CAIP-10** id in MetaMask: `eip155:1:0xabc…`, + `solana:5eykt…:6Lm…`. +- **Session** — the set of granted scopes for an origin. MetaMask stores this as a + single CAIP-25 permission caveat per origin and **does not** issue or accept a + `sessionId` (one session per origin, tracked internally). +- **`sessionProperties`** — global session metadata (allowlisted; see below). +- **`scopedProperties`** — per-scope metadata kept **outside** `sessionScopes` + (MetaMask's term; upstream renamed this to `capabilities` and moved it inside the + scope object). + +## Methods + +### `wallet_createSession` + +Prompts the user and grants a CAIP-25 session. `paramStructure: by-name`. + +**Params** + +| Field | Type | Required | Notes | +| --- | --- | --- | --- | +| `requiredScopes` | `{ [scopeString]: ScopeObject }` | no | Accepted but **treated as optional** (see divergences). | +| `optionalScopes` | `{ [scopeString]: ScopeObject }` | no | | +| `sessionProperties` | `{ [key]: Json }` | no | Allowlist-filtered to [known keys](#supported-methods--notifications-per-namespace). An empty object is rejected with `5302`. | + +`ScopeObject` fields: `methods: string[]`, `notifications: string[]`, +optionally `accounts: CaipAccountId[]` and `references: string[]`. + +**Result** + +```jsonc +{ + "sessionScopes": { "": { "accounts": [...], "methods": [...], "notifications": [...] } }, + "sessionProperties": { /* approved, may be {} */ } +} +``` + +**Example request** + +```jsonc +{ + "id": 1, + "jsonrpc": "2.0", + "method": "wallet_createSession", + "params": { + "optionalScopes": { + "eip155:1": { + "methods": ["eth_sendTransaction", "personal_sign", "eth_getBalance"], + "notifications": ["eth_subscription"] + }, + "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": { + "methods": ["signMessage", "signAndSendTransaction"], + "notifications": [] + } + } + } +} +``` + +**Example response** + +```jsonc +{ + "id": 1, + "jsonrpc": "2.0", + "result": { + "sessionProperties": {}, + "sessionScopes": { + "eip155:1": { + "accounts": ["eip155:1:0x5cfe73b6021e818b776b421b1c4db2474086a7e1"], + "methods": ["eth_sendTransaction", "personal_sign", "eth_getBalance"], + "notifications": ["eth_subscription"] + } + } + } +} +``` + +**Behavior notes** + +- All requested scopes are treated as optional; unsupported scopes, unknown + methods/notifications, and accounts not held by the wallet are **silently + dropped** rather than erroring. +- If, after filtering, **no** scopes remain and Solana was not requested, it + returns `5100` (Requested scopes are not supported). +- **Solana opt-in:** if a Solana scope is requested but the wallet has no Solana + account, the handler sets a `promptToCreateSolanaAccount` flag and injects an + empty `wallet` scope so the request can pass the CAIP-25 caveat validator (which + otherwise rejects zero-scope requests). + +### `wallet_getSession` + +Returns the active session for the origin. `params: []`. + +**Result:** `{ "sessionScopes": { ... } }`. If there is **no** active session, +returns `{ "sessionScopes": {} }` (does **not** throw). Any `sessionId` param is +ignored. + +### `wallet_revokeSession` + +Revokes the session for the origin. Returns `true`. + +- With no params (or empty `scopes`), revokes the entire CAIP-25 permission. +- **MetaMask extension:** accepts an optional `params.scopes: string[]` for + **partial** revocation — each listed scope is removed; if no permitted accounts + remain afterward, the whole permission is revoked. +- Returns `true` even when there was no active session. Any `sessionId` param is + ignored. + +### `wallet_invokeMethod` + +Invokes a method on a previously authorized scope (CAIP-27). `paramStructure: by-name`. + +**Params** + +| Field | Type | Required | Notes | +| --- | --- | --- | --- | +| `scope` | `ScopeString` | yes | Must be an authorized scope in the current session. | +| `request` | `{ method: string, params?: Json }` | yes | The wrapped JSON-RPC request. | + +**Result:** whatever the underlying method returns. + +**Behavior notes** + +- If the origin has no CAIP-25 caveat, returns `4100` (unauthorized). +- If `request.method` is not in the authorized scope's `methods`, returns `4100`. +- EVM requests (`eip155:*`, or `wallet` / `wallet:eip155`) are routed to the + resolved `networkClientId` and passed down the middleware stack; non-EVM + requests are dispatched to the multichain router. Any `sessionId` param is + ignored — the origin's single session is used. + +**Example** + +```jsonc +{ + "id": 2, + "jsonrpc": "2.0", + "method": "wallet_invokeMethod", + "params": { + "scope": "eip155:1", + "request": { "method": "eth_getBalance", "params": ["0x5cfe…", "latest"] } + } +} +``` + +## Notifications + +### `wallet_sessionChanged` + +Published by the wallet when a session's authorization scopes change (accounts, +scopes added/removed, restoration). `paramStructure: by-name`. Payload: +`{ "sessionScopes": { ... } }` with the full updated scopes. + +### `wallet_notify` + +Delivers a scope-bound notification to the caller. Params: `scope` (an authorized +scope string) and `notification` (`{ method, params }`). Used to forward +subscription events such as `eth_subscription`. + +## Supported methods & notifications per namespace + +How a method gets into a session's `methods` array depends on the namespace. + +### EVM (`eip155`) — static, from `api-specs` + +EVM method support is enumerated statically in +`@metamask/chain-agnostic-permission` (`src/scope/constants.ts`). + +| List | Scope | Contents | +| --- | --- | --- | +| `KnownRpcMethods.eip155` | `eip155:` | All MetaMask JSON-RPC methods from `@metamask/api-specs`, **minus** the wallet-scoped and EIP-1193-only lists below | +| `KnownWalletNamespaceRpcMethods.eip155` | `wallet:eip155` | `wallet_addEthereumChain` | +| `KnownWalletRpcMethods` | `wallet` | `wallet_registerOnboarding`, `wallet_scanQRCode` | +| `KnownNotifications.eip155` | `eip155:` | `eth_subscription` | + +**EIP-1193-only methods** (`Eip1193OnlyMethods`) — explicitly **excluded** from the +Multichain API; available only via the injected EIP-1193 provider: +`wallet_switchEthereumChain`, `wallet_getPermissions`, `wallet_requestPermissions`, +`wallet_revokePermissions`, `eth_requestAccounts`, `eth_accounts`, `eth_coinbase`, +`net_version`, `metamask_logWeb3ShimUsage`, `metamask_getProviderState`, +`metamask_sendDomainMetadata`, `wallet_registerOnboarding`. + +### Non-EVM (`solana`, `bip122`, `tron`) — dynamic, from Snaps + +`KnownRpcMethods` / `KnownNotifications` are **empty** for non-EVM namespaces. Their +supported methods are resolved **at runtime** through the handler's +`getNonEvmSupportedMethods(scope)` hook, which the wallet wires to the Snaps +subsystem. + +In the extension, that hook calls +`MultichainRoutingService:getSupportedMethods(scope)` +(`@metamask/snaps-controllers`), which returns the **union** of: + +1. **Account-Snap methods** — methods declared by installed account-management + Snaps that hold an account for that scope (via + `AccountsController:listMultichainAccounts`, filtered to runnable Snaps), and +2. **Protocol-Snap methods** — methods declared by protocol Snaps that service the + scope. + +```text +getNonEvmSupportedMethods(scope) + └─ MultichainRoutingService.getSupportedMethods(scope) + = unique( accountSnap.methods[] ∪ protocolSnap.methods[] ) +``` + +Consequently the non-EVM method set depends on which Snaps the user has installed +and which accounts they hold — there is no fixed wallet-wide list. Scope support is +likewise dynamic: `isNonEvmScopeSupported(scope)` is true when at least one Snap can +service the scope. + +**Example (Solana, via the MetaMask Solana Snap).** Methods are exposed using +[Wallet Standard](https://github.com/wallet-standard/wallet-standard) naming, e.g. +`signIn`, `signMessage`, `signTransaction`, `signAndSendTransaction`, +`signAllTransactions`. These are provided by the Snap, not hardcoded here, so treat +the list as illustrative and verify against the installed Snap's manifest. + +### Known `sessionProperties` keys + +`wallet_createSession` filters `sessionProperties` to the `KnownSessionProperties` +allowlist; unknown keys are dropped. As of `@metamask/chain-agnostic-permission` +1.6.0: + +| Key | Purpose | +| --- | --- | +| `eip1193-compatible` | Marks the connection as originating from an EIP-1193 client (injected `window.ethereum` middleware or `@metamask/connect-evm`). The extension uses it to gate EVM-connection UX such as the network picker on the dapp connection bar. Pure Multichain API connections — even EVM-only ones — do not set it. | +| `solana_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Solana scopes. | +| `tron_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Tron scopes. | +| `bip122_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Bitcoin scopes. | + +> **Version note.** `eip1193-compatible` was added to the allowlist in +> `@metamask/chain-agnostic-permission` **1.6.0**. In 1.5.x (and earlier) only the +> three `*_accountChanged_notifications` keys are known, so a 1.5.x wallet would +> **drop** an incoming `eip1193-compatible` property. See +> [the eip1193-compatible flow](#the-eip1193-compatible-session-property) below. + +## Error codes + +| Code | Message | When | +| --- | --- | --- | +| `5000` | Unknown error with request | Generic failure. | +| `5100` | Requested scopes are not supported | No supported scopes remain after filtering (and Solana opt-in not applicable). | +| `5101` | Requested methods are not supported | (CAIP-25; see `chain-agnostic-permission`.) | +| `5102` | Requested notifications are not supported | (CAIP-25.) | +| `5201` | Unknown method(s) requested | Dev-mode strict validation. | +| `5202` | Unknown notification(s) requested | Dev-mode strict validation. | +| `5300` | Invalid scopedProperties requested | Malformed `scopedProperties`. | +| `5301` | scopedProperties can only be outside of sessionScopes | `scopedProperties` placement. | +| `5302` | Invalid sessionProperties requested | `sessionProperties` present but empty `{}`. | +| `4100` | Unauthorized | `wallet_invokeMethod` on an unauthorized scope/method. | + +> Note: code `5100`'s message is "Requested **scopes** are not supported" in this +> handler, while `chain-agnostic-permission` and the OpenRPC schema phrase the same +> code as "Requested **chains/networks** are not supported." Same code, slightly +> different wording. + +## Divergences from current CAIP-25 + +CAIP-25 was restructured upstream in July–August 2025 (see the spec's own +changelog). MetaMask implements the **pre-rewrite** shape. Verified against the +current [CAIP-25 spec](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md) +and this package's handlers: + +| Concept | Current CAIP-25 | MetaMask implementation | +| --- | --- | --- | +| Request scopes | Single `scopes` (`requiredScopes` removed; `optionalScopes` → `scopes`, 2025-07-31) | Still `requiredScopes` + `optionalScopes`; **all treated as optional** | +| Session metadata key | `properties` (renamed from `sessionProperties`, 2025-07-30) | Still `sessionProperties`; **allowlist-filtered** to known keys | +| Per-scope extras | `capabilities`, merged **into** the scope object (2025-08-04) | Still `scopedProperties`, kept **outside** `sessionScopes` (errors `5300`/`5301`) | +| Scope granularity | Chain-scoped only (namespace-scoped removed 2025-08-03) | Uses namespace-scoped objects (`wallet:eip155`) and a `references` shorthand array | +| Accounts format | Bare addresses; CAIP-2 prefix removed (2025-08-07) | Fully-qualified **CAIP-10** (`eip155:1:0x…`) | +| `sessionId` | Optional, supported (CAIP-171 / CAIP-316) | **Not** returned or accepted; one session per origin, tracked internally | +| `chains` shorthand | `chains: string[]` inside the scope object | `references: string[]` (older CAIP-217 shorthand) | +| Invalid input | MAY error | **Silently dropped** (invalid scopes/methods/accounts) | + +## MetaMask-specific behavior + +- **All scopes optional.** `requiredScopes` are not enforced as required; the + handler buckets everything and grants whatever is supported. +- **Lenient filtering.** Malformed scopes and unknown methods/notifications/accounts + are dropped instead of erroring (reduces fingerprinting and breakage). +- **`sessionProperties` allowlist.** Only the keys in `KnownSessionProperties` are + retained; an explicitly empty `sessionProperties: {}` errors with `5302`. +- **Solana opt-in flow.** Requesting a Solana scope with no Solana account sets + `promptToCreateSolanaAccount` and injects an empty `wallet` scope so the + zero-scope request passes the caveat validator. +- **Single session per origin.** `sessionId` is ignored across `getSession`, + `revokeSession`, and `invokeMethod`. +- **Graceful no-session results.** `wallet_getSession` returns + `{ sessionScopes: {} }` and `wallet_revokeSession` returns `true` even with no + active session. +- **Partial revoke.** `wallet_revokeSession` accepts an optional `scopes` array to + remove individual scopes; full revoke happens automatically if no accounts remain. + +### The `eip1193-compatible` session property + +This property is the bridge between the legacy EIP-1193 world and the Multichain +session model, and it's a good worked example of how `sessionProperties` flows end +to end: + +1. **Set on the request.** `@metamask/connect-evm` attaches + `sessionProperties: { 'eip1193-compatible': true }` to every + `wallet_createSession` it issues (`EIP1193_COMPATIBLE_SESSION_PROPERTY` / + `CONNECT_EVM_SESSION_PROPERTIES` in `connect-evm/src/constants.ts`). The + extension's own injected EIP-1193 middleware sets it too. +2. **Filtered on the way in.** The `wallet_createSession` handler keeps only keys in + the `KnownSessionProperties` allowlist. `eip1193-compatible` is a known key **as + of `@metamask/chain-agnostic-permission` 1.6.0**, so it survives and is persisted + into the CAIP-25 caveat. On 1.5.x it would be silently dropped — this is the one + gotcha to watch for across versions. +3. **Read by the wallet UI.** The extension selector + `getIsEip1193CompatibleConnection` (`ui/selectors/dapp.ts`) returns true when + `caveatValue.sessionProperties['eip1193-compatible'] === true`, which gates the + EVM network picker on the dapp connection bar. Pure Multichain API connections — + even EVM-only ones — omit the flag and therefore don't get that EIP-1193-specific + UX. +4. **Backfilled for old connections.** Extension migration 211 sets + `eip1193-compatible: true` on every pre-existing CAIP-25 caveat that has any + `eip155:*` scope, so connections established before this property existed don't + lose their network picker after upgrade. Solana-only connections are left + untouched. + +> **Why the apparent mismatch?** If you compare a 1.5.x `chain-agnostic-permission` +> checkout (whose `KnownSessionProperties` only lists the three +> `*_accountChanged_notifications`) against `connect-evm` sending +> `eip1193-compatible`, it looks like the property would always be filtered out. It +> isn't, in shipping builds: the key was added to the allowlist in 1.6.0, and the +> extension ships ≥1.6.x. The discrepancy is purely a version-skew artifact of +> reading an older `core` checkout. + +## Source-of-truth pointers + +- **Handlers:** `src/handlers/wallet-createSession.ts`, `wallet-getSession.ts`, + `wallet-revokeSession.ts`, `wallet-invokeMethod.ts` +- **Scope/permission semantics, constants, error codes:** + [`@metamask/chain-agnostic-permission`](https://github.com/MetaMask/core/tree/main/packages/chain-agnostic-permission) + (`src/scope/constants.ts`, `src/scope/errors.ts`) +- **OpenRPC schema:** + [`@metamask/api-specs`](https://github.com/MetaMask/api-specs) → + `multichain/openrpc.yaml` +- **Design rationale:** + [MIP-5](https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-5.md) + (MIP-6 is historical) +- **Dapp/SDK consumer docs:** + [MetaMask Connect](https://github.com/MetaMask/connect-monorepo) diff --git a/packages/multichain-api-middleware/README.md b/packages/multichain-api-middleware/README.md index e0465365d9..3f6d2207ec 100644 --- a/packages/multichain-api-middleware/README.md +++ b/packages/multichain-api-middleware/README.md @@ -2,6 +2,14 @@ JSON-RPC methods and middleware to support the the [MetaMask Multichain API](https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-5.md). +## Documentation + +See [`MULTICHAIN_API.md`](./MULTICHAIN_API.md) for a readable, wallet-side +reference of the Multichain API as implemented here: `wallet_createSession` inputs +and outputs, supported methods per namespace, error codes, and how MetaMask +currently diverges from the latest CAIP-25. The machine-readable schema lives in +[`@metamask/api-specs`](https://github.com/MetaMask/api-specs). + ## Installation `yarn add @metamask/multichain-api-middleware` From 3130c22512381186f18372a2df4a13f590df6dd4 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 15:37:35 -0500 Subject: [PATCH 02/12] docs: clarify scopes are required and drop eip1193-compatible deep-dive - wallet_createSession: note that at least one of requiredScopes / optionalScopes must resolve to a supported scope (neither -> 5100). - Remove the standalone "eip1193-compatible session property" section; the allowlist table entry and version note cover it without the extra depth. --- .../MULTICHAIN_API.md | 47 +++---------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md index 3cf2050e2c..b790d942d7 100644 --- a/packages/multichain-api-middleware/MULTICHAIN_API.md +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -94,10 +94,14 @@ Prompts the user and grants a CAIP-25 session. `paramStructure: by-name`. | Field | Type | Required | Notes | | --- | --- | --- | --- | -| `requiredScopes` | `{ [scopeString]: ScopeObject }` | no | Accepted but **treated as optional** (see divergences). | -| `optionalScopes` | `{ [scopeString]: ScopeObject }` | no | | +| `requiredScopes` | `{ [scopeString]: ScopeObject }` | conditional | Accepted but **treated as optional** (see divergences). | +| `optionalScopes` | `{ [scopeString]: ScopeObject }` | conditional | | | `sessionProperties` | `{ [key]: Json }` | no | Allowlist-filtered to [known keys](#supported-methods--notifications-per-namespace). An empty object is rejected with `5302`. | +At least one of `requiredScopes` / `optionalScopes` must be present and resolve to +a supported scope — a request with neither (or with only unsupported scopes) is +rejected with `5100`, unless it triggers the [Solana opt-in flow](#wallet_createsession). + `ScopeObject` fields: `methods: string[]`, `notifications: string[]`, optionally `accounts: CaipAccountId[]` and `references: string[]`. @@ -305,8 +309,7 @@ allowlist; unknown keys are dropped. As of `@metamask/chain-agnostic-permission` > **Version note.** `eip1193-compatible` was added to the allowlist in > `@metamask/chain-agnostic-permission` **1.6.0**. In 1.5.x (and earlier) only the > three `*_accountChanged_notifications` keys are known, so a 1.5.x wallet would -> **drop** an incoming `eip1193-compatible` property. See -> [the eip1193-compatible flow](#the-eip1193-compatible-session-property) below. +> **drop** an incoming `eip1193-compatible` property. ## Error codes @@ -365,42 +368,6 @@ and this package's handlers: - **Partial revoke.** `wallet_revokeSession` accepts an optional `scopes` array to remove individual scopes; full revoke happens automatically if no accounts remain. -### The `eip1193-compatible` session property - -This property is the bridge between the legacy EIP-1193 world and the Multichain -session model, and it's a good worked example of how `sessionProperties` flows end -to end: - -1. **Set on the request.** `@metamask/connect-evm` attaches - `sessionProperties: { 'eip1193-compatible': true }` to every - `wallet_createSession` it issues (`EIP1193_COMPATIBLE_SESSION_PROPERTY` / - `CONNECT_EVM_SESSION_PROPERTIES` in `connect-evm/src/constants.ts`). The - extension's own injected EIP-1193 middleware sets it too. -2. **Filtered on the way in.** The `wallet_createSession` handler keeps only keys in - the `KnownSessionProperties` allowlist. `eip1193-compatible` is a known key **as - of `@metamask/chain-agnostic-permission` 1.6.0**, so it survives and is persisted - into the CAIP-25 caveat. On 1.5.x it would be silently dropped — this is the one - gotcha to watch for across versions. -3. **Read by the wallet UI.** The extension selector - `getIsEip1193CompatibleConnection` (`ui/selectors/dapp.ts`) returns true when - `caveatValue.sessionProperties['eip1193-compatible'] === true`, which gates the - EVM network picker on the dapp connection bar. Pure Multichain API connections — - even EVM-only ones — omit the flag and therefore don't get that EIP-1193-specific - UX. -4. **Backfilled for old connections.** Extension migration 211 sets - `eip1193-compatible: true` on every pre-existing CAIP-25 caveat that has any - `eip155:*` scope, so connections established before this property existed don't - lose their network picker after upgrade. Solana-only connections are left - untouched. - -> **Why the apparent mismatch?** If you compare a 1.5.x `chain-agnostic-permission` -> checkout (whose `KnownSessionProperties` only lists the three -> `*_accountChanged_notifications`) against `connect-evm` sending -> `eip1193-compatible`, it looks like the property would always be filtered out. It -> isn't, in shipping builds: the key was added to the allowlist in 1.6.0, and the -> extension ships ≥1.6.x. The discrepancy is purely a version-skew artifact of -> reading an older `core` checkout. - ## Source-of-truth pointers - **Handlers:** `src/handlers/wallet-createSession.ts`, `wallet-getSession.ts`, From efed2affb8d117cd5e1c45e5012d0558142e1375 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 15:54:34 -0500 Subject: [PATCH 03/12] docs: correct error-code accuracy from review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split error table into codes actually thrown (5100, 5302, 4100) vs codes defined-but-not-returned by the handler: - 5101/5102 are filtered out during scope bucketing, not rejected - 5201/5202 are a TODO and never thrown - 5300/5301 are schema-only; the handler never reads scopedProperties - Fix divergence row claiming scopedProperties "errors 5300/5301" — the field is accepted and ignored; note this in Concepts too. - Clarify partial-revoke is middleware (not extension-specific) behavior. - Soften the eip1193-compatible claim: migration 211 backfills it onto pre-existing eip155 connections, so older Multichain-only sessions may carry it. - Fix CAIP-25 date attribution for the optionalScopes->scopes rename (2025-07-30). --- .../MULTICHAIN_API.md | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md index b790d942d7..e6973ab1a1 100644 --- a/packages/multichain-api-middleware/MULTICHAIN_API.md +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -82,7 +82,8 @@ rewrite, so don't rely on it for current behavior. - **`sessionProperties`** — global session metadata (allowlisted; see below). - **`scopedProperties`** — per-scope metadata kept **outside** `sessionScopes` (MetaMask's term; upstream renamed this to `capabilities` and moved it inside the - scope object). + scope object). Defined in the OpenRPC schema but **not currently read** by the + `wallet_createSession` handler — see [Error codes](#error-codes). ## Methods @@ -180,8 +181,9 @@ ignored. Revokes the session for the origin. Returns `true`. - With no params (or empty `scopes`), revokes the entire CAIP-25 permission. -- **MetaMask extension:** accepts an optional `params.scopes: string[]` for - **partial** revocation — each listed scope is removed; if no permitted accounts +- Accepts an optional `params.scopes: string[]` for **partial** revocation + (implemented in this middleware handler, `partialRevokePermissions`) — each + listed scope is removed; if no permitted accounts remain afterward, the whole permission is revoked. - Returns `true` even when there was no active session. Any `sessionId` param is ignored. @@ -301,7 +303,7 @@ allowlist; unknown keys are dropped. As of `@metamask/chain-agnostic-permission` | Key | Purpose | | --- | --- | -| `eip1193-compatible` | Marks the connection as originating from an EIP-1193 client (injected `window.ethereum` middleware or `@metamask/connect-evm`). The extension uses it to gate EVM-connection UX such as the network picker on the dapp connection bar. Pure Multichain API connections — even EVM-only ones — do not set it. | +| `eip1193-compatible` | Marks the connection as originating from an EIP-1193 client (injected `window.ethereum` middleware or `@metamask/connect-evm`). The extension uses it to gate EVM-connection UX such as the network picker on the dapp connection bar. Newly-created pure Multichain API sessions — even EVM-only ones — do not set it; note the extension also backfills it onto pre-existing connections with any `eip155:*` scope (migration 211), so older Multichain-only EVM connections may carry it. | | `solana_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Solana scopes. | | `tron_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Tron scopes. | | `bip122_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Bitcoin scopes. | @@ -316,15 +318,22 @@ allowlist; unknown keys are dropped. As of `@metamask/chain-agnostic-permission` | Code | Message | When | | --- | --- | --- | | `5000` | Unknown error with request | Generic failure. | -| `5100` | Requested scopes are not supported | No supported scopes remain after filtering (and Solana opt-in not applicable). | -| `5101` | Requested methods are not supported | (CAIP-25; see `chain-agnostic-permission`.) | -| `5102` | Requested notifications are not supported | (CAIP-25.) | -| `5201` | Unknown method(s) requested | Dev-mode strict validation. | -| `5202` | Unknown notification(s) requested | Dev-mode strict validation. | -| `5300` | Invalid scopedProperties requested | Malformed `scopedProperties`. | -| `5301` | scopedProperties can only be outside of sessionScopes | `scopedProperties` placement. | -| `5302` | Invalid sessionProperties requested | `sessionProperties` present but empty `{}`. | -| `4100` | Unauthorized | `wallet_invokeMethod` on an unauthorized scope/method. | +| `5100` | Requested scopes are not supported | Actually returned by `wallet_createSession` when no supported scopes remain after filtering (and the Solana opt-in does not apply). | +| `5302` | Invalid sessionProperties requested | Returned by `wallet_createSession` when `sessionProperties` is present but an empty object `{}`. | +| `4100` | Unauthorized | Returned by `wallet_invokeMethod` when the origin has no CAIP-25 session, or the requested scope/method is not authorized (`providerErrors.unauthorized()`). | + +The codes below are **defined** (in `@metamask/chain-agnostic-permission` and/or +`@metamask/api-specs`) but are **not** thrown by the current `wallet_createSession` +handler — included here so you don't expect them on the wire: + +| Code | Message | Status | +| --- | --- | --- | +| `5101` | Requested methods are not supported | **Not returned.** Unsupported methods are silently filtered out during scope bucketing (`chain-agnostic-permission` `scope/filter.ts`), not rejected. | +| `5102` | Requested notifications are not supported | **Not returned.** Same as `5101` — filtered, not rejected. | +| `5201` | Unknown method(s) requested | **Not returned.** Defined in `errors.ts` but currently only a `TODO` (intended for dev-mode strict validation). | +| `5202` | Unknown notification(s) requested | **Not returned.** Same `TODO` status as `5201`. | +| `5300` | Invalid scopedProperties requested | **Schema-only.** Present in `openrpc.yaml`, but the handler never reads `scopedProperties`, so this is never thrown. | +| `5301` | scopedProperties can only be outside of sessionScopes | **Schema-only.** Same as `5300`. | > Note: code `5100`'s message is "Requested **scopes** are not supported" in this > handler, while `chain-agnostic-permission` and the OpenRPC schema phrase the same @@ -340,9 +349,9 @@ and this package's handlers: | Concept | Current CAIP-25 | MetaMask implementation | | --- | --- | --- | -| Request scopes | Single `scopes` (`requiredScopes` removed; `optionalScopes` → `scopes`, 2025-07-31) | Still `requiredScopes` + `optionalScopes`; **all treated as optional** | +| Request scopes | Single `scopes` (`optionalScopes` → `scopes`, 2025-07-30; `requiredScopes` removed 2025-07-31) | Still `requiredScopes` + `optionalScopes`; **all treated as optional** | | Session metadata key | `properties` (renamed from `sessionProperties`, 2025-07-30) | Still `sessionProperties`; **allowlist-filtered** to known keys | -| Per-scope extras | `capabilities`, merged **into** the scope object (2025-08-04) | Still `scopedProperties`, kept **outside** `sessionScopes` (errors `5300`/`5301`) | +| Per-scope extras | `capabilities`, merged **into** the scope object (2025-08-04) | Still uses the older `scopedProperties` concept (kept **outside** `sessionScopes`); note the current `wallet_createSession` handler does not read `scopedProperties` at all — it is accepted and ignored, and codes `5300`/`5301` exist only in the OpenRPC schema | | Scope granularity | Chain-scoped only (namespace-scoped removed 2025-08-03) | Uses namespace-scoped objects (`wallet:eip155`) and a `references` shorthand array | | Accounts format | Bare addresses; CAIP-2 prefix removed (2025-08-07) | Fully-qualified **CAIP-10** (`eip155:1:0x…`) | | `sessionId` | Optional, supported (CAIP-171 / CAIP-316) | **Not** returned or accepted; one session per origin, tracked internally | From 1156b236bc6f175ae7720f77530d77d9ec58969a Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 16:05:12 -0500 Subject: [PATCH 04/12] docs: reword intro and note extension + mobile implementations --- packages/multichain-api-middleware/MULTICHAIN_API.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md index e6973ab1a1..22de887ade 100644 --- a/packages/multichain-api-middleware/MULTICHAIN_API.md +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -1,8 +1,9 @@ # The MetaMask Multichain API -A readable, wallet-side reference for MetaMask's CAIP-25 / CAIP-27 based Multichain -API, as actually implemented by `@metamask/multichain-api-middleware` and -`@metamask/chain-agnostic-permission`. +This is high-level reference documentation for MetaMask's CAIP-25 / CAIP-27 based +Multichain API. The API is powered by `@metamask/multichain-api-middleware` and +`@metamask/chain-agnostic-permission`, and is implemented on both the MetaMask +extension and mobile clients. > **Audience.** This document describes the **wallet's JSON-RPC contract** — the > requests a caller (dapp / SDK) sends and the responses MetaMask returns. If you From 425ec8778288c1318fb7e3b73d04697e024e9015 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 16:06:45 -0500 Subject: [PATCH 05/12] docs: note sessions span multiple accounts in overview --- packages/multichain-api-middleware/MULTICHAIN_API.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md index 22de887ade..a39a1e3555 100644 --- a/packages/multichain-api-middleware/MULTICHAIN_API.md +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -40,10 +40,10 @@ extension and mobile clients. ## Overview The Multichain API lets a caller negotiate a single **session** that spans -multiple chains and ecosystems (EVM, Solana, Bitcoin, Tron) in one authorization, -then invoke methods on any authorized scope. It replaces the per-chain EIP-1193 -model (`eth_requestAccounts` on one chain at a time) with a chain-agnostic, -scope-based model. +multiple chains and ecosystems (EVM, Solana, Bitcoin, Tron) — and multiple accounts +across those scopes — in one authorization, then invoke methods on any authorized +scope. It replaces the per-chain EIP-1193 model (`eth_requestAccounts` on one chain +at a time) with a chain-agnostic, scope-based model. It is built on the CASA Chain Agnostic standards: From e6f5db695bfb3ad669bdebc08b1868e76d1cf58c Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 16:13:32 -0500 Subject: [PATCH 06/12] docs: reframe scopedProperties as abandoned Remove scopedProperties from Concepts/intro (it's not actually implemented) and add a dedicated bottom section explaining it was partially implemented for EIP-3085, deprioritized, and has since been removed from the request in upstream CAIP-25 (scopedProperties -> capabilities -> dropped). Update the divergence row accordingly. --- .../MULTICHAIN_API.md | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md index a39a1e3555..dff2eba215 100644 --- a/packages/multichain-api-middleware/MULTICHAIN_API.md +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -63,7 +63,7 @@ rewrite, so don't rely on it for current behavior. > ⚠️ **CAIP-25 moved; MetaMask did not (yet).** Upstream CAIP-25 was restructured > in July–August 2025 (single `scopes`, `properties`/`capabilities` renames, bare > accounts, chain-only scope keys). MetaMask still implements the **pre-rewrite** -> shape (`requiredScopes`/`optionalScopes`, `sessionProperties`/`scopedProperties`, +> shape (`requiredScopes`/`optionalScopes`, `sessionProperties`, > CAIP-10 accounts, namespace-scoped keys). See > [Divergences from current CAIP-25](#divergences-from-current-caip-25). @@ -81,10 +81,6 @@ rewrite, so don't rely on it for current behavior. single CAIP-25 permission caveat per origin and **does not** issue or accept a `sessionId` (one session per origin, tracked internally). - **`sessionProperties`** — global session metadata (allowlisted; see below). -- **`scopedProperties`** — per-scope metadata kept **outside** `sessionScopes` - (MetaMask's term; upstream renamed this to `capabilities` and moved it inside the - scope object). Defined in the OpenRPC schema but **not currently read** by the - `wallet_createSession` handler — see [Error codes](#error-codes). ## Methods @@ -352,7 +348,7 @@ and this package's handlers: | --- | --- | --- | | Request scopes | Single `scopes` (`optionalScopes` → `scopes`, 2025-07-30; `requiredScopes` removed 2025-07-31) | Still `requiredScopes` + `optionalScopes`; **all treated as optional** | | Session metadata key | `properties` (renamed from `sessionProperties`, 2025-07-30) | Still `sessionProperties`; **allowlist-filtered** to known keys | -| Per-scope extras | `capabilities`, merged **into** the scope object (2025-08-04) | Still uses the older `scopedProperties` concept (kept **outside** `sessionScopes`); note the current `wallet_createSession` handler does not read `scopedProperties` at all — it is accepted and ignored, and codes `5300`/`5301` exist only in the OpenRPC schema | +| Per-scope request extras | Removed from the request — `scopedProperties` was renamed to `capabilities` (2025-07-30), merged into the scope object (2025-08-04), then dropped from the request entirely (2025-08-07). A response-only `capabilities` remains. | Never fully implemented; spec/type-only and now stranded — see [`scopedProperties`: abandoned](#scopedproperties-abandoned) | | Scope granularity | Chain-scoped only (namespace-scoped removed 2025-08-03) | Uses namespace-scoped objects (`wallet:eip155`) and a `references` shorthand array | | Accounts format | Bare addresses; CAIP-2 prefix removed (2025-08-07) | Fully-qualified **CAIP-10** (`eip155:1:0x…`) | | `sessionId` | Optional, supported (CAIP-171 / CAIP-316) | **Not** returned or accepted; one session per origin, tracked internally | @@ -378,6 +374,32 @@ and this package's handlers: - **Partial revoke.** `wallet_revokeSession` accepts an optional `scopes` array to remove individual scopes; full revoke happens automatically if no accounts remain. +## `scopedProperties`: abandoned + +`scopedProperties` was a request-side, per-scope metadata object (kept outside +`sessionScopes`) intended to carry things like EIP-3085 chain-definition data so the +wallet could add and authorize a not-yet-known EVM chain as part of session +creation. It got **partway implemented and then deprioritized**: + +- It exists in the OpenRPC schema (`api-specs`, with error codes `5300`/`5301`) and + as an optional field on the `Caip25Authorization` type + (`chain-agnostic-permission` `src/scope/authorization.ts`). +- But the `wallet_createSession` handler **never reads it** — `req.params` only + destructures `requiredScopes`, `optionalScopes`, and `sessionProperties`. The only + traces in the handler are comments (`// intended for future usage with eip3085 + scopedProperties`) and the `isEvmChainIdSupportable: () => false` stub that was the + hook for it. + +Meanwhile, upstream CAIP-25 has **removed the concept from the request**: +`scopedProperties` → `capabilities` (2025-07-30) → merged into the scope object +(2025-08-04) → dropped from the request entirely (2025-08-07). (A wallet-advertised +`capabilities` still exists in the *response*, but that is a separate, response-only +feature.) + +Net result: `scopedProperties` is stranded — specced and typed but inert, and no +longer part of the standard it was tracking. It is very unlikely to be implemented +as-is and is a good candidate for removal from `api-specs`. + ## Source-of-truth pointers - **Handlers:** `src/handlers/wallet-createSession.ts`, `wallet-getSession.ts`, From afc629e4fa0239288a8cdb6cbd762e08584e4944 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 16:18:59 -0500 Subject: [PATCH 07/12] docs: remove mentions of the Solana opt-in flow --- .../multichain-api-middleware/MULTICHAIN_API.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md index dff2eba215..e634d2948d 100644 --- a/packages/multichain-api-middleware/MULTICHAIN_API.md +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -98,7 +98,7 @@ Prompts the user and grants a CAIP-25 session. `paramStructure: by-name`. At least one of `requiredScopes` / `optionalScopes` must be present and resolve to a supported scope — a request with neither (or with only unsupported scopes) is -rejected with `5100`, unless it triggers the [Solana opt-in flow](#wallet_createsession). +rejected with `5100`. `ScopeObject` fields: `methods: string[]`, `notifications: string[]`, optionally `accounts: CaipAccountId[]` and `references: string[]`. @@ -158,12 +158,8 @@ optionally `accounts: CaipAccountId[]` and `references: string[]`. - All requested scopes are treated as optional; unsupported scopes, unknown methods/notifications, and accounts not held by the wallet are **silently dropped** rather than erroring. -- If, after filtering, **no** scopes remain and Solana was not requested, it - returns `5100` (Requested scopes are not supported). -- **Solana opt-in:** if a Solana scope is requested but the wallet has no Solana - account, the handler sets a `promptToCreateSolanaAccount` flag and injects an - empty `wallet` scope so the request can pass the CAIP-25 caveat validator (which - otherwise rejects zero-scope requests). +- If, after filtering, **no** scopes remain, it returns `5100` (Requested scopes + are not supported). ### `wallet_getSession` @@ -315,7 +311,7 @@ allowlist; unknown keys are dropped. As of `@metamask/chain-agnostic-permission` | Code | Message | When | | --- | --- | --- | | `5000` | Unknown error with request | Generic failure. | -| `5100` | Requested scopes are not supported | Actually returned by `wallet_createSession` when no supported scopes remain after filtering (and the Solana opt-in does not apply). | +| `5100` | Requested scopes are not supported | Actually returned by `wallet_createSession` when no supported scopes remain after filtering. | | `5302` | Invalid sessionProperties requested | Returned by `wallet_createSession` when `sessionProperties` is present but an empty object `{}`. | | `4100` | Unauthorized | Returned by `wallet_invokeMethod` when the origin has no CAIP-25 session, or the requested scope/method is not authorized (`providerErrors.unauthorized()`). | @@ -363,9 +359,6 @@ and this package's handlers: are dropped instead of erroring (reduces fingerprinting and breakage). - **`sessionProperties` allowlist.** Only the keys in `KnownSessionProperties` are retained; an explicitly empty `sessionProperties: {}` errors with `5302`. -- **Solana opt-in flow.** Requesting a Solana scope with no Solana account sets - `promptToCreateSolanaAccount` and injects an empty `wallet` scope so the - zero-scope request passes the caveat validator. - **Single session per origin.** `sessionId` is ignored across `getSession`, `revokeSession`, and `invokeMethod`. - **Graceful no-session results.** `wallet_getSession` returns From a30c6d1e707f4b0fbcf37782d3bcdbc6a428fdfa Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 16:21:30 -0500 Subject: [PATCH 08/12] docs: condense scopedProperties abandoned section --- .../MULTICHAIN_API.md | 30 +++++-------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md index e634d2948d..d3b98274b6 100644 --- a/packages/multichain-api-middleware/MULTICHAIN_API.md +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -369,29 +369,13 @@ and this package's handlers: ## `scopedProperties`: abandoned -`scopedProperties` was a request-side, per-scope metadata object (kept outside -`sessionScopes`) intended to carry things like EIP-3085 chain-definition data so the -wallet could add and authorize a not-yet-known EVM chain as part of session -creation. It got **partway implemented and then deprioritized**: - -- It exists in the OpenRPC schema (`api-specs`, with error codes `5300`/`5301`) and - as an optional field on the `Caip25Authorization` type - (`chain-agnostic-permission` `src/scope/authorization.ts`). -- But the `wallet_createSession` handler **never reads it** — `req.params` only - destructures `requiredScopes`, `optionalScopes`, and `sessionProperties`. The only - traces in the handler are comments (`// intended for future usage with eip3085 - scopedProperties`) and the `isEvmChainIdSupportable: () => false` stub that was the - hook for it. - -Meanwhile, upstream CAIP-25 has **removed the concept from the request**: -`scopedProperties` → `capabilities` (2025-07-30) → merged into the scope object -(2025-08-04) → dropped from the request entirely (2025-08-07). (A wallet-advertised -`capabilities` still exists in the *response*, but that is a separate, response-only -feature.) - -Net result: `scopedProperties` is stranded — specced and typed but inert, and no -longer part of the standard it was tracking. It is very unlikely to be implemented -as-is and is a good candidate for removal from `api-specs`. +A request-side, per-scope metadata object (intended for EIP-3085-style dynamic chain +addition) that was partly implemented then deprioritized: it lives in the OpenRPC +schema (error codes `5300`/`5301`) and the `Caip25Authorization` type, but the +handler never reads it. Upstream CAIP-25 has since removed it from the request +(`scopedProperties` → `capabilities`, 2025-07-30 → merged into the scope object, +2025-08-04 → dropped, 2025-08-07). It is stranded and a candidate for removal from +`api-specs`. ## Source-of-truth pointers From 10fd4d87534b8edb645730d6b1ae6a7273b465f7 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 16:26:00 -0500 Subject: [PATCH 09/12] docs: inline scopedProperties note into divergence table; oxfmt - Remove the standalone scopedProperties section and fold it into the divergence-table row. - Apply repo oxfmt formatting (table alignment, jsonc trailing commas). --- .../MULTICHAIN_API.md | 122 ++++++++---------- 1 file changed, 56 insertions(+), 66 deletions(-) diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md index d3b98274b6..bfd1dd0b4c 100644 --- a/packages/multichain-api-middleware/MULTICHAIN_API.md +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -90,11 +90,11 @@ Prompts the user and grants a CAIP-25 session. `paramStructure: by-name`. **Params** -| Field | Type | Required | Notes | -| --- | --- | --- | --- | -| `requiredScopes` | `{ [scopeString]: ScopeObject }` | conditional | Accepted but **treated as optional** (see divergences). | -| `optionalScopes` | `{ [scopeString]: ScopeObject }` | conditional | | -| `sessionProperties` | `{ [key]: Json }` | no | Allowlist-filtered to [known keys](#supported-methods--notifications-per-namespace). An empty object is rejected with `5302`. | +| Field | Type | Required | Notes | +| ------------------- | -------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `requiredScopes` | `{ [scopeString]: ScopeObject }` | conditional | Accepted but **treated as optional** (see divergences). | +| `optionalScopes` | `{ [scopeString]: ScopeObject }` | conditional | | +| `sessionProperties` | `{ [key]: Json }` | no | Allowlist-filtered to [known keys](#supported-methods--notifications-per-namespace). An empty object is rejected with `5302`. | At least one of `requiredScopes` / `optionalScopes` must be present and resolve to a supported scope — a request with neither (or with only unsupported scopes) is @@ -123,14 +123,14 @@ optionally `accounts: CaipAccountId[]` and `references: string[]`. "optionalScopes": { "eip155:1": { "methods": ["eth_sendTransaction", "personal_sign", "eth_getBalance"], - "notifications": ["eth_subscription"] + "notifications": ["eth_subscription"], }, "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": { "methods": ["signMessage", "signAndSendTransaction"], - "notifications": [] - } - } - } + "notifications": [], + }, + }, + }, } ``` @@ -146,10 +146,10 @@ optionally `accounts: CaipAccountId[]` and `references: string[]`. "eip155:1": { "accounts": ["eip155:1:0x5cfe73b6021e818b776b421b1c4db2474086a7e1"], "methods": ["eth_sendTransaction", "personal_sign", "eth_getBalance"], - "notifications": ["eth_subscription"] - } - } - } + "notifications": ["eth_subscription"], + }, + }, + }, } ``` @@ -187,10 +187,10 @@ Invokes a method on a previously authorized scope (CAIP-27). `paramStructure: by **Params** -| Field | Type | Required | Notes | -| --- | --- | --- | --- | -| `scope` | `ScopeString` | yes | Must be an authorized scope in the current session. | -| `request` | `{ method: string, params?: Json }` | yes | The wrapped JSON-RPC request. | +| Field | Type | Required | Notes | +| --------- | ----------------------------------- | -------- | --------------------------------------------------- | +| `scope` | `ScopeString` | yes | Must be an authorized scope in the current session. | +| `request` | `{ method: string, params?: Json }` | yes | The wrapped JSON-RPC request. | **Result:** whatever the underlying method returns. @@ -212,8 +212,8 @@ Invokes a method on a previously authorized scope (CAIP-27). `paramStructure: by "method": "wallet_invokeMethod", "params": { "scope": "eip155:1", - "request": { "method": "eth_getBalance", "params": ["0x5cfe…", "latest"] } - } + "request": { "method": "eth_getBalance", "params": ["0x5cfe…", "latest"] }, + }, } ``` @@ -240,12 +240,12 @@ How a method gets into a session's `methods` array depends on the namespace. EVM method support is enumerated statically in `@metamask/chain-agnostic-permission` (`src/scope/constants.ts`). -| List | Scope | Contents | -| --- | --- | --- | -| `KnownRpcMethods.eip155` | `eip155:` | All MetaMask JSON-RPC methods from `@metamask/api-specs`, **minus** the wallet-scoped and EIP-1193-only lists below | -| `KnownWalletNamespaceRpcMethods.eip155` | `wallet:eip155` | `wallet_addEthereumChain` | -| `KnownWalletRpcMethods` | `wallet` | `wallet_registerOnboarding`, `wallet_scanQRCode` | -| `KnownNotifications.eip155` | `eip155:` | `eth_subscription` | +| List | Scope | Contents | +| --------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- | +| `KnownRpcMethods.eip155` | `eip155:` | All MetaMask JSON-RPC methods from `@metamask/api-specs`, **minus** the wallet-scoped and EIP-1193-only lists below | +| `KnownWalletNamespaceRpcMethods.eip155` | `wallet:eip155` | `wallet_addEthereumChain` | +| `KnownWalletRpcMethods` | `wallet` | `wallet_registerOnboarding`, `wallet_scanQRCode` | +| `KnownNotifications.eip155` | `eip155:` | `eth_subscription` | **EIP-1193-only methods** (`Eip1193OnlyMethods`) — explicitly **excluded** from the Multichain API; available only via the injected EIP-1193 provider: @@ -294,12 +294,12 @@ the list as illustrative and verify against the installed Snap's manifest. allowlist; unknown keys are dropped. As of `@metamask/chain-agnostic-permission` 1.6.0: -| Key | Purpose | -| --- | --- | -| `eip1193-compatible` | Marks the connection as originating from an EIP-1193 client (injected `window.ethereum` middleware or `@metamask/connect-evm`). The extension uses it to gate EVM-connection UX such as the network picker on the dapp connection bar. Newly-created pure Multichain API sessions — even EVM-only ones — do not set it; note the extension also backfills it onto pre-existing connections with any `eip155:*` scope (migration 211), so older Multichain-only EVM connections may carry it. | -| `solana_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Solana scopes. | -| `tron_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Tron scopes. | -| `bip122_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Bitcoin scopes. | +| Key | Purpose | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `eip1193-compatible` | Marks the connection as originating from an EIP-1193 client (injected `window.ethereum` middleware or `@metamask/connect-evm`). The extension uses it to gate EVM-connection UX such as the network picker on the dapp connection bar. Newly-created pure Multichain API sessions — even EVM-only ones — do not set it; note the extension also backfills it onto pre-existing connections with any `eip155:*` scope (migration 211), so older Multichain-only EVM connections may carry it. | +| `solana_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Solana scopes. | +| `tron_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Tron scopes. | +| `bip122_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Bitcoin scopes. | > **Version note.** `eip1193-compatible` was added to the allowlist in > `@metamask/chain-agnostic-permission` **1.6.0**. In 1.5.x (and earlier) only the @@ -308,25 +308,25 @@ allowlist; unknown keys are dropped. As of `@metamask/chain-agnostic-permission` ## Error codes -| Code | Message | When | -| --- | --- | --- | -| `5000` | Unknown error with request | Generic failure. | -| `5100` | Requested scopes are not supported | Actually returned by `wallet_createSession` when no supported scopes remain after filtering. | -| `5302` | Invalid sessionProperties requested | Returned by `wallet_createSession` when `sessionProperties` is present but an empty object `{}`. | -| `4100` | Unauthorized | Returned by `wallet_invokeMethod` when the origin has no CAIP-25 session, or the requested scope/method is not authorized (`providerErrors.unauthorized()`). | +| Code | Message | When | +| ------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `5000` | Unknown error with request | Generic failure. | +| `5100` | Requested scopes are not supported | Actually returned by `wallet_createSession` when no supported scopes remain after filtering. | +| `5302` | Invalid sessionProperties requested | Returned by `wallet_createSession` when `sessionProperties` is present but an empty object `{}`. | +| `4100` | Unauthorized | Returned by `wallet_invokeMethod` when the origin has no CAIP-25 session, or the requested scope/method is not authorized (`providerErrors.unauthorized()`). | The codes below are **defined** (in `@metamask/chain-agnostic-permission` and/or `@metamask/api-specs`) but are **not** thrown by the current `wallet_createSession` handler — included here so you don't expect them on the wire: -| Code | Message | Status | -| --- | --- | --- | -| `5101` | Requested methods are not supported | **Not returned.** Unsupported methods are silently filtered out during scope bucketing (`chain-agnostic-permission` `scope/filter.ts`), not rejected. | -| `5102` | Requested notifications are not supported | **Not returned.** Same as `5101` — filtered, not rejected. | -| `5201` | Unknown method(s) requested | **Not returned.** Defined in `errors.ts` but currently only a `TODO` (intended for dev-mode strict validation). | -| `5202` | Unknown notification(s) requested | **Not returned.** Same `TODO` status as `5201`. | -| `5300` | Invalid scopedProperties requested | **Schema-only.** Present in `openrpc.yaml`, but the handler never reads `scopedProperties`, so this is never thrown. | -| `5301` | scopedProperties can only be outside of sessionScopes | **Schema-only.** Same as `5300`. | +| Code | Message | Status | +| ------ | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `5101` | Requested methods are not supported | **Not returned.** Unsupported methods are silently filtered out during scope bucketing (`chain-agnostic-permission` `scope/filter.ts`), not rejected. | +| `5102` | Requested notifications are not supported | **Not returned.** Same as `5101` — filtered, not rejected. | +| `5201` | Unknown method(s) requested | **Not returned.** Defined in `errors.ts` but currently only a `TODO` (intended for dev-mode strict validation). | +| `5202` | Unknown notification(s) requested | **Not returned.** Same `TODO` status as `5201`. | +| `5300` | Invalid scopedProperties requested | **Schema-only.** Present in `openrpc.yaml`, but the handler never reads `scopedProperties`, so this is never thrown. | +| `5301` | scopedProperties can only be outside of sessionScopes | **Schema-only.** Same as `5300`. | > Note: code `5100`'s message is "Requested **scopes** are not supported" in this > handler, while `chain-agnostic-permission` and the OpenRPC schema phrase the same @@ -340,16 +340,16 @@ changelog). MetaMask implements the **pre-rewrite** shape. Verified against the current [CAIP-25 spec](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md) and this package's handlers: -| Concept | Current CAIP-25 | MetaMask implementation | -| --- | --- | --- | -| Request scopes | Single `scopes` (`optionalScopes` → `scopes`, 2025-07-30; `requiredScopes` removed 2025-07-31) | Still `requiredScopes` + `optionalScopes`; **all treated as optional** | -| Session metadata key | `properties` (renamed from `sessionProperties`, 2025-07-30) | Still `sessionProperties`; **allowlist-filtered** to known keys | -| Per-scope request extras | Removed from the request — `scopedProperties` was renamed to `capabilities` (2025-07-30), merged into the scope object (2025-08-04), then dropped from the request entirely (2025-08-07). A response-only `capabilities` remains. | Never fully implemented; spec/type-only and now stranded — see [`scopedProperties`: abandoned](#scopedproperties-abandoned) | -| Scope granularity | Chain-scoped only (namespace-scoped removed 2025-08-03) | Uses namespace-scoped objects (`wallet:eip155`) and a `references` shorthand array | -| Accounts format | Bare addresses; CAIP-2 prefix removed (2025-08-07) | Fully-qualified **CAIP-10** (`eip155:1:0x…`) | -| `sessionId` | Optional, supported (CAIP-171 / CAIP-316) | **Not** returned or accepted; one session per origin, tracked internally | -| `chains` shorthand | `chains: string[]` inside the scope object | `references: string[]` (older CAIP-217 shorthand) | -| Invalid input | MAY error | **Silently dropped** (invalid scopes/methods/accounts) | +| Concept | Current CAIP-25 | MetaMask implementation | +| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Request scopes | Single `scopes` (`optionalScopes` → `scopes`, 2025-07-30; `requiredScopes` removed 2025-07-31) | Still `requiredScopes` + `optionalScopes`; **all treated as optional** | +| Session metadata key | `properties` (renamed from `sessionProperties`, 2025-07-30) | Still `sessionProperties`; **allowlist-filtered** to known keys | +| Per-scope request extras (`scopedProperties`) | Removed from the request — `scopedProperties` → `capabilities` (2025-07-30) → merged into the scope object (2025-08-04) → dropped from the request entirely (2025-08-07). A response-only `capabilities` remains. | **Abandoned.** Intended for EIP-3085-style dynamic chain addition; partly implemented then deprioritized. Lives in the OpenRPC schema (error codes `5300`/`5301`) and the `Caip25Authorization` type, but the handler never reads it. Stranded — candidate for removal from `api-specs`. | +| Scope granularity | Chain-scoped only (namespace-scoped removed 2025-08-03) | Uses namespace-scoped objects (`wallet:eip155`) and a `references` shorthand array | +| Accounts format | Bare addresses; CAIP-2 prefix removed (2025-08-07) | Fully-qualified **CAIP-10** (`eip155:1:0x…`) | +| `sessionId` | Optional, supported (CAIP-171 / CAIP-316) | **Not** returned or accepted; one session per origin, tracked internally | +| `chains` shorthand | `chains: string[]` inside the scope object | `references: string[]` (older CAIP-217 shorthand) | +| Invalid input | MAY error | **Silently dropped** (invalid scopes/methods/accounts) | ## MetaMask-specific behavior @@ -367,16 +367,6 @@ and this package's handlers: - **Partial revoke.** `wallet_revokeSession` accepts an optional `scopes` array to remove individual scopes; full revoke happens automatically if no accounts remain. -## `scopedProperties`: abandoned - -A request-side, per-scope metadata object (intended for EIP-3085-style dynamic chain -addition) that was partly implemented then deprioritized: it lives in the OpenRPC -schema (error codes `5300`/`5301`) and the `Caip25Authorization` type, but the -handler never reads it. Upstream CAIP-25 has since removed it from the request -(`scopedProperties` → `capabilities`, 2025-07-30 → merged into the scope object, -2025-08-04 → dropped, 2025-08-07). It is stranded and a candidate for removal from -`api-specs`. - ## Source-of-truth pointers - **Handlers:** `src/handlers/wallet-createSession.ts`, `wallet-getSession.ts`, From 11fdca6f36e610d6a412a6175bcbf0d81a52efb0 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 16:34:31 -0500 Subject: [PATCH 10/12] docs: trim error-code weeds, drop version note, remove em dashes - Remove the eip1193-compatible version note and the 5100 wording note. - Condense the "defined but not thrown" error table to a single sentence (keep signal: callers shouldn't expect those codes on the wire). - Replace em dashes with colons/semicolons/parentheses throughout; oxfmt. --- .../MULTICHAIN_API.md | 122 ++++++++---------- 1 file changed, 53 insertions(+), 69 deletions(-) diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md index bfd1dd0b4c..0d6a34f1f0 100644 --- a/packages/multichain-api-middleware/MULTICHAIN_API.md +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -5,7 +5,7 @@ Multichain API. The API is powered by `@metamask/multichain-api-middleware` and `@metamask/chain-agnostic-permission`, and is implemented on both the MetaMask extension and mobile clients. -> **Audience.** This document describes the **wallet's JSON-RPC contract** — the +> **Audience.** This document describes the **wallet's JSON-RPC contract**: the > requests a caller (dapp / SDK) sends and the responses MetaMask returns. If you > are integrating a dapp, you usually want the > [MetaMask Connect SDK](https://github.com/MetaMask/connect-monorepo) instead, @@ -16,7 +16,7 @@ extension and mobile clients. > machine-readable schema lives in > [`@metamask/api-specs`](https://github.com/MetaMask/api-specs) > (`multichain/openrpc.yaml`). Where this prose and the OpenRPC schema disagree, -> the handler code is authoritative — please file an issue so we can reconcile +> the handler code is authoritative; please file an issue so we can reconcile > them. ## Contents @@ -40,28 +40,28 @@ extension and mobile clients. ## Overview The Multichain API lets a caller negotiate a single **session** that spans -multiple chains and ecosystems (EVM, Solana, Bitcoin, Tron) — and multiple accounts -across those scopes — in one authorization, then invoke methods on any authorized +multiple chains and ecosystems (EVM, Solana, Bitcoin, Tron), and multiple accounts +across those scopes, in one authorization, then invoke methods on any authorized scope. It replaces the per-chain EIP-1193 model (`eth_requestAccounts` on one chain at a time) with a chain-agnostic, scope-based model. It is built on the CASA Chain Agnostic standards: -- **[CAIP-25](https://chainagnostic.org/CAIPs/caip-25)** — `wallet_createSession`, session negotiation -- **[CAIP-27](https://chainagnostic.org/CAIPs/caip-27)** — `wallet_invokeMethod`, invoking a method on a scope -- **[CAIP-285](https://chainagnostic.org/CAIPs/caip-285)** — `wallet_revokeSession` -- **[CAIP-311](https://chainagnostic.org/CAIPs/caip-311)** — `wallet_sessionChanged` -- **[CAIP-312](https://chainagnostic.org/CAIPs/caip-312)** — `wallet_getSession` -- **[CAIP-2](https://chainagnostic.org/CAIPs/caip-2)** / **[CAIP-10](https://chainagnostic.org/CAIPs/caip-10)** / **[CAIP-217](https://chainagnostic.org/CAIPs/caip-217)** — chain IDs, account IDs, scope objects +- **[CAIP-25](https://chainagnostic.org/CAIPs/caip-25)**: `wallet_createSession`, session negotiation +- **[CAIP-27](https://chainagnostic.org/CAIPs/caip-27)**: `wallet_invokeMethod`, invoking a method on a scope +- **[CAIP-285](https://chainagnostic.org/CAIPs/caip-285)**: `wallet_revokeSession` +- **[CAIP-311](https://chainagnostic.org/CAIPs/caip-311)**: `wallet_sessionChanged` +- **[CAIP-312](https://chainagnostic.org/CAIPs/caip-312)**: `wallet_getSession` +- **[CAIP-2](https://chainagnostic.org/CAIPs/caip-2)** / **[CAIP-10](https://chainagnostic.org/CAIPs/caip-10)** / **[CAIP-217](https://chainagnostic.org/CAIPs/caip-217)**: chain IDs, account IDs, scope objects For MetaMask's design rationale see [MIP-5](https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-5.md). [MIP-6](https://github.com/MetaMask/metamask-improvement-proposals/blob/main/MIPs/mip-6.md) -is **historical** — it predates the current implementation and the upstream CAIP-25 +is **historical**; it predates the current implementation and the upstream CAIP-25 rewrite, so don't rely on it for current behavior. > ⚠️ **CAIP-25 moved; MetaMask did not (yet).** Upstream CAIP-25 was restructured -> in July–August 2025 (single `scopes`, `properties`/`capabilities` renames, bare +> in July to August 2025 (single `scopes`, `properties`/`capabilities` renames, bare > accounts, chain-only scope keys). MetaMask still implements the **pre-rewrite** > shape (`requiredScopes`/`optionalScopes`, `sessionProperties`, > CAIP-10 accounts, namespace-scoped keys). See @@ -69,18 +69,18 @@ rewrite, so don't rely on it for current behavior. ## Concepts -- **Scope string** — a CAIP-2 chain id (`eip155:1`, `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`) +- **Scope string**: a CAIP-2 chain id (`eip155:1`, `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`) or a CAIP-104 namespace-level scope (`wallet`, `wallet:eip155`). Pattern: `[-a-z0-9]{3,8}(:[-_a-zA-Z0-9]{1,32})?`. -- **Scope object** — per CAIP-217, an object with `methods`, `notifications`, and +- **Scope object**: per CAIP-217, an object with `methods`, `notifications`, and (in responses) `accounts`. In requests it may also carry `references` (namespace shorthand). Keyed by scope string. -- **Account** — a fully-qualified **CAIP-10** id in MetaMask: `eip155:1:0xabc…`, - `solana:5eykt…:6Lm…`. -- **Session** — the set of granted scopes for an origin. MetaMask stores this as a +- **Account**: a fully-qualified **CAIP-10** id in MetaMask: `eip155:1:0xabc...`, + `solana:5eykt...:6Lm...`. +- **Session**: the set of granted scopes for an origin. MetaMask stores this as a single CAIP-25 permission caveat per origin and **does not** issue or accept a `sessionId` (one session per origin, tracked internally). -- **`sessionProperties`** — global session metadata (allowlisted; see below). +- **`sessionProperties`**: global session metadata (allowlisted; see below). ## Methods @@ -97,7 +97,7 @@ Prompts the user and grants a CAIP-25 session. `paramStructure: by-name`. | `sessionProperties` | `{ [key]: Json }` | no | Allowlist-filtered to [known keys](#supported-methods--notifications-per-namespace). An empty object is rejected with `5302`. | At least one of `requiredScopes` / `optionalScopes` must be present and resolve to -a supported scope — a request with neither (or with only unsupported scopes) is +a supported scope; a request with neither (or with only unsupported scopes) is rejected with `5100`. `ScopeObject` fields: `methods: string[]`, `notifications: string[]`, @@ -175,7 +175,7 @@ Revokes the session for the origin. Returns `true`. - With no params (or empty `scopes`), revokes the entire CAIP-25 permission. - Accepts an optional `params.scopes: string[]` for **partial** revocation - (implemented in this middleware handler, `partialRevokePermissions`) — each + (implemented in this middleware handler, `partialRevokePermissions`); each listed scope is removed; if no permitted accounts remain afterward, the whole permission is revoked. - Returns `true` even when there was no active session. Any `sessionId` param is @@ -201,7 +201,7 @@ Invokes a method on a previously authorized scope (CAIP-27). `paramStructure: by - EVM requests (`eip155:*`, or `wallet` / `wallet:eip155`) are routed to the resolved `networkClientId` and passed down the middleware stack; non-EVM requests are dispatched to the multichain router. Any `sessionId` param is - ignored — the origin's single session is used. + ignored; the origin's single session is used. **Example** @@ -212,7 +212,10 @@ Invokes a method on a previously authorized scope (CAIP-27). `paramStructure: by "method": "wallet_invokeMethod", "params": { "scope": "eip155:1", - "request": { "method": "eth_getBalance", "params": ["0x5cfe…", "latest"] }, + "request": { + "method": "eth_getBalance", + "params": ["0x5cfe...", "latest"], + }, }, } ``` @@ -235,7 +238,7 @@ subscription events such as `eth_subscription`. How a method gets into a session's `methods` array depends on the namespace. -### EVM (`eip155`) — static, from `api-specs` +### EVM (`eip155`): static, from `api-specs` EVM method support is enumerated statically in `@metamask/chain-agnostic-permission` (`src/scope/constants.ts`). @@ -247,14 +250,14 @@ EVM method support is enumerated statically in | `KnownWalletRpcMethods` | `wallet` | `wallet_registerOnboarding`, `wallet_scanQRCode` | | `KnownNotifications.eip155` | `eip155:` | `eth_subscription` | -**EIP-1193-only methods** (`Eip1193OnlyMethods`) — explicitly **excluded** from the +**EIP-1193-only methods** (`Eip1193OnlyMethods`): explicitly **excluded** from the Multichain API; available only via the injected EIP-1193 provider: `wallet_switchEthereumChain`, `wallet_getPermissions`, `wallet_requestPermissions`, `wallet_revokePermissions`, `eth_requestAccounts`, `eth_accounts`, `eth_coinbase`, `net_version`, `metamask_logWeb3ShimUsage`, `metamask_getProviderState`, `metamask_sendDomainMetadata`, `wallet_registerOnboarding`. -### Non-EVM (`solana`, `bip122`, `tron`) — dynamic, from Snaps +### Non-EVM (`solana`, `bip122`, `tron`): dynamic, from Snaps `KnownRpcMethods` / `KnownNotifications` are **empty** for non-EVM namespaces. Their supported methods are resolved **at runtime** through the handler's @@ -265,10 +268,10 @@ In the extension, that hook calls `MultichainRoutingService:getSupportedMethods(scope)` (`@metamask/snaps-controllers`), which returns the **union** of: -1. **Account-Snap methods** — methods declared by installed account-management +1. **Account-Snap methods**: methods declared by installed account-management Snaps that hold an account for that scope (via `AccountsController:listMultichainAccounts`, filtered to runnable Snaps), and -2. **Protocol-Snap methods** — methods declared by protocol Snaps that service the +2. **Protocol-Snap methods**: methods declared by protocol Snaps that service the scope. ```text @@ -278,7 +281,7 @@ getNonEvmSupportedMethods(scope) ``` Consequently the non-EVM method set depends on which Snaps the user has installed -and which accounts they hold — there is no fixed wallet-wide list. Scope support is +and which accounts they hold; there is no fixed wallet-wide list. Scope support is likewise dynamic: `isNonEvmScopeSupported(scope)` is true when at least one Snap can service the scope. @@ -291,20 +294,14 @@ the list as illustrative and verify against the installed Snap's manifest. ### Known `sessionProperties` keys `wallet_createSession` filters `sessionProperties` to the `KnownSessionProperties` -allowlist; unknown keys are dropped. As of `@metamask/chain-agnostic-permission` -1.6.0: - -| Key | Purpose | -| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `eip1193-compatible` | Marks the connection as originating from an EIP-1193 client (injected `window.ethereum` middleware or `@metamask/connect-evm`). The extension uses it to gate EVM-connection UX such as the network picker on the dapp connection bar. Newly-created pure Multichain API sessions — even EVM-only ones — do not set it; note the extension also backfills it onto pre-existing connections with any `eip155:*` scope (migration 211), so older Multichain-only EVM connections may carry it. | -| `solana_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Solana scopes. | -| `tron_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Tron scopes. | -| `bip122_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Bitcoin scopes. | +allowlist; unknown keys are dropped: -> **Version note.** `eip1193-compatible` was added to the allowlist in -> `@metamask/chain-agnostic-permission` **1.6.0**. In 1.5.x (and earlier) only the -> three `*_accountChanged_notifications` keys are known, so a 1.5.x wallet would -> **drop** an incoming `eip1193-compatible` property. +| Key | Purpose | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `eip1193-compatible` | Marks the connection as originating from an EIP-1193 client (injected `window.ethereum` middleware or `@metamask/connect-evm`). The extension uses it to gate EVM-connection UX such as the network picker on the dapp connection bar. Newly-created pure Multichain API sessions (even EVM-only ones) do not set it; note the extension also backfills it onto pre-existing connections with any `eip155:*` scope (migration 211), so older Multichain-only EVM connections may carry it. | +| `solana_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Solana scopes. | +| `tron_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Tron scopes. | +| `bip122_accountChanged_notifications` | Opt-in to `accountChanged` notifications for Bitcoin scopes. | ## Error codes @@ -315,41 +312,28 @@ allowlist; unknown keys are dropped. As of `@metamask/chain-agnostic-permission` | `5302` | Invalid sessionProperties requested | Returned by `wallet_createSession` when `sessionProperties` is present but an empty object `{}`. | | `4100` | Unauthorized | Returned by `wallet_invokeMethod` when the origin has no CAIP-25 session, or the requested scope/method is not authorized (`providerErrors.unauthorized()`). | -The codes below are **defined** (in `@metamask/chain-agnostic-permission` and/or -`@metamask/api-specs`) but are **not** thrown by the current `wallet_createSession` -handler — included here so you don't expect them on the wire: - -| Code | Message | Status | -| ------ | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| `5101` | Requested methods are not supported | **Not returned.** Unsupported methods are silently filtered out during scope bucketing (`chain-agnostic-permission` `scope/filter.ts`), not rejected. | -| `5102` | Requested notifications are not supported | **Not returned.** Same as `5101` — filtered, not rejected. | -| `5201` | Unknown method(s) requested | **Not returned.** Defined in `errors.ts` but currently only a `TODO` (intended for dev-mode strict validation). | -| `5202` | Unknown notification(s) requested | **Not returned.** Same `TODO` status as `5201`. | -| `5300` | Invalid scopedProperties requested | **Schema-only.** Present in `openrpc.yaml`, but the handler never reads `scopedProperties`, so this is never thrown. | -| `5301` | scopedProperties can only be outside of sessionScopes | **Schema-only.** Same as `5300`. | - -> Note: code `5100`'s message is "Requested **scopes** are not supported" in this -> handler, while `chain-agnostic-permission` and the OpenRPC schema phrase the same -> code as "Requested **chains/networks** are not supported." Same code, slightly -> different wording. +The OpenRPC schema and `@metamask/chain-agnostic-permission` define additional codes +(`5101`, `5102`, `5201`, `5202`, `5300`, `5301`) that the current +`wallet_createSession` handler does not emit, so callers should not expect them on +the wire. ## Divergences from current CAIP-25 -CAIP-25 was restructured upstream in July–August 2025 (see the spec's own +CAIP-25 was restructured upstream in July to August 2025 (see the spec's own changelog). MetaMask implements the **pre-rewrite** shape. Verified against the current [CAIP-25 spec](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-25.md) and this package's handlers: -| Concept | Current CAIP-25 | MetaMask implementation | -| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Request scopes | Single `scopes` (`optionalScopes` → `scopes`, 2025-07-30; `requiredScopes` removed 2025-07-31) | Still `requiredScopes` + `optionalScopes`; **all treated as optional** | -| Session metadata key | `properties` (renamed from `sessionProperties`, 2025-07-30) | Still `sessionProperties`; **allowlist-filtered** to known keys | -| Per-scope request extras (`scopedProperties`) | Removed from the request — `scopedProperties` → `capabilities` (2025-07-30) → merged into the scope object (2025-08-04) → dropped from the request entirely (2025-08-07). A response-only `capabilities` remains. | **Abandoned.** Intended for EIP-3085-style dynamic chain addition; partly implemented then deprioritized. Lives in the OpenRPC schema (error codes `5300`/`5301`) and the `Caip25Authorization` type, but the handler never reads it. Stranded — candidate for removal from `api-specs`. | -| Scope granularity | Chain-scoped only (namespace-scoped removed 2025-08-03) | Uses namespace-scoped objects (`wallet:eip155`) and a `references` shorthand array | -| Accounts format | Bare addresses; CAIP-2 prefix removed (2025-08-07) | Fully-qualified **CAIP-10** (`eip155:1:0x…`) | -| `sessionId` | Optional, supported (CAIP-171 / CAIP-316) | **Not** returned or accepted; one session per origin, tracked internally | -| `chains` shorthand | `chains: string[]` inside the scope object | `references: string[]` (older CAIP-217 shorthand) | -| Invalid input | MAY error | **Silently dropped** (invalid scopes/methods/accounts) | +| Concept | Current CAIP-25 | MetaMask implementation | +| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Request scopes | Single `scopes` (`optionalScopes` → `scopes`, 2025-07-30; `requiredScopes` removed 2025-07-31) | Still `requiredScopes` + `optionalScopes`; **all treated as optional** | +| Session metadata key | `properties` (renamed from `sessionProperties`, 2025-07-30) | Still `sessionProperties`; **allowlist-filtered** to known keys | +| Per-scope request extras (`scopedProperties`) | Removed from the request: `scopedProperties` became `capabilities` (2025-07-30), merged into the scope object (2025-08-04), then dropped from the request entirely (2025-08-07). A response-only `capabilities` remains. | **Abandoned.** Intended for EIP-3085-style dynamic chain addition; partly implemented then deprioritized. Lives in the OpenRPC schema (error codes `5300`/`5301`) and the `Caip25Authorization` type, but the handler never reads it. Stranded; candidate for removal from `api-specs`. | +| Scope granularity | Chain-scoped only (namespace-scoped removed 2025-08-03) | Uses namespace-scoped objects (`wallet:eip155`) and a `references` shorthand array | +| Accounts format | Bare addresses; CAIP-2 prefix removed (2025-08-07) | Fully-qualified **CAIP-10** (`eip155:1:0x...`) | +| `sessionId` | Optional, supported (CAIP-171 / CAIP-316) | **Not** returned or accepted; one session per origin, tracked internally | +| `chains` shorthand | `chains: string[]` inside the scope object | `references: string[]` (older CAIP-217 shorthand) | +| Invalid input | MAY error | **Silently dropped** (invalid scopes/methods/accounts) | ## MetaMask-specific behavior From fb2ec4ad7edf54af8ca7b298c3c7865071368e10 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jun 2026 16:35:15 -0500 Subject: [PATCH 11/12] docs: add changelog entry for MULTICHAIN_API.md --- packages/multichain-api-middleware/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/multichain-api-middleware/CHANGELOG.md b/packages/multichain-api-middleware/CHANGELOG.md index 7012d90d03..57573ad854 100644 --- a/packages/multichain-api-middleware/CHANGELOG.md +++ b/packages/multichain-api-middleware/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `MULTICHAIN_API.md`, a reference for the Multichain API: `wallet_createSession` and the other session methods, supported methods per namespace, error codes, and divergences from the current CAIP-25 spec ([#9258](https://github.com/MetaMask/core/pull/9258)) + ### Changed - Bump `@metamask/accounts-controller` from `^39.0.2` to `^39.0.3` ([#9231](https://github.com/MetaMask/core/pull/9231)) From 3daa2f12d9f777224792ccdf0d6c6f3618cb57c2 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Thu, 25 Jun 2026 16:02:41 -0500 Subject: [PATCH 12/12] Update packages/multichain-api-middleware/MULTICHAIN_API.md Co-authored-by: jiexi --- packages/multichain-api-middleware/MULTICHAIN_API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-api-middleware/MULTICHAIN_API.md b/packages/multichain-api-middleware/MULTICHAIN_API.md index 0d6a34f1f0..ac43df687d 100644 --- a/packages/multichain-api-middleware/MULTICHAIN_API.md +++ b/packages/multichain-api-middleware/MULTICHAIN_API.md @@ -60,7 +60,7 @@ For MetaMask's design rationale see is **historical**; it predates the current implementation and the upstream CAIP-25 rewrite, so don't rely on it for current behavior. -> ⚠️ **CAIP-25 moved; MetaMask did not (yet).** Upstream CAIP-25 was restructured +> ⚠️ **CAIP-25 moved; MetaMask has not caught up (yet).** Upstream CAIP-25 was restructured > in July to August 2025 (single `scopes`, `properties`/`capabilities` renames, bare > accounts, chain-only scope keys). MetaMask still implements the **pre-rewrite** > shape (`requiredScopes`/`optionalScopes`, `sessionProperties`,