The MetaMask Multichain API

June 26, 2026 · View on GitHub

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 are integrating a dapp, you usually want the MetaMask Connect SDK 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 (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

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 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:

For MetaMask's design rationale see MIP-5. MIP-6 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 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, CAIP-10 accounts, namespace-scoped keys). See 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).

Methods

wallet_createSession

Prompts the user and grants a CAIP-25 session. paramStructure: by-name.

Params

FieldTypeRequiredNotes
requiredScopes{ [scopeString]: ScopeObject }conditionalAccepted but treated as optional (see divergences).
optionalScopes{ [scopeString]: ScopeObject }conditional
sessionProperties{ [key]: Json }noAllowlist-filtered to known keys. 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.

ScopeObject fields: methods: string[], notifications: string[], optionally accounts: CaipAccountId[] and references: string[].

Result

{
  "sessionScopes": { "<scopeString>": { "accounts": [...], "methods": [...], "notifications": [...] } },
  "sessionProperties": { /* approved, may be {} */ }
}

Example request

{
  "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

{
  "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, it returns 5100 (Requested scopes are not supported).

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.
  • 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.

wallet_invokeMethod

Invokes a method on a previously authorized scope (CAIP-27). paramStructure: by-name.

Params

FieldTypeRequiredNotes
scopeScopeStringyesMust be an authorized scope in the current session.
request{ method: string, params?: Json }yesThe 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

{
  "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).

ListScopeContents
KnownRpcMethods.eip155eip155:<chainId>All MetaMask JSON-RPC methods from @metamask/api-specs, minus the wallet-scoped and EIP-1193-only lists below
KnownWalletNamespaceRpcMethods.eip155wallet:eip155wallet_addEthereumChain
KnownWalletRpcMethodswalletwallet_registerOnboarding, wallet_scanQRCode
KnownNotifications.eip155eip155:<chainId>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.
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 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:

KeyPurpose
eip1193-compatibleMarks 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_notificationsOpt-in to accountChanged notifications for Solana scopes.
tron_accountChanged_notificationsOpt-in to accountChanged notifications for Tron scopes.
bip122_accountChanged_notificationsOpt-in to accountChanged notifications for Bitcoin scopes.

Error codes

CodeMessageWhen
5000Unknown error with requestGeneric failure.
5100Requested scopes are not supportedActually returned by wallet_createSession when no supported scopes remain after filtering.
5302Invalid sessionProperties requestedReturned by wallet_createSession when sessionProperties is present but an empty object {}.
4100UnauthorizedReturned by wallet_invokeMethod when the origin has no CAIP-25 session, or the requested scope/method is not authorized (providerErrors.unauthorized()).

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 to August 2025 (see the spec's own changelog). MetaMask implements the pre-rewrite shape. Verified against the current CAIP-25 spec and this package's handlers:

ConceptCurrent CAIP-25MetaMask implementation
Request scopesSingle scopes (optionalScopesscopes, 2025-07-30; requiredScopes removed 2025-07-31)Still requiredScopes + optionalScopes; all treated as optional
Session metadata keyproperties (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 granularityChain-scoped only (namespace-scoped removed 2025-08-03)Uses namespace-scoped objects (wallet:eip155) and a references shorthand array
Accounts formatBare addresses; CAIP-2 prefix removed (2025-08-07)Fully-qualified CAIP-10 (eip155:1:0x...)
sessionIdOptional, supported (CAIP-171 / CAIP-316)Not returned or accepted; one session per origin, tracked internally
chains shorthandchains: string[] inside the scope objectreferences: string[] (older CAIP-217 shorthand)
Invalid inputMAY errorSilently 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.
  • 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.

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 (src/scope/constants.ts, src/scope/errors.ts)
  • OpenRPC schema: @metamask/api-specsmultichain/openrpc.yaml
  • Design rationale: MIP-5 (MIP-6 is historical)
  • Dapp/SDK consumer docs: MetaMask Connect