Core backend services for MetaMask, serving as the data layer between Backend services (REST APIs, WebSocket services) and Frontend applications (Extension, Mobile). Provides authenticated real-time data delivery including account activity monitoring, price updates, and WebSocket connection management with type-safe controller integration.
@metamask/core-backend
yarn add @metamask/core-backendor
npm install @metamask/core-backendWebSocket for Real-time Updates:
import {
BackendWebSocketService,
AccountActivityService,
} from '@metamask/core-backend';
// Initialize Backend WebSocket service
const backendWebSocketService = new BackendWebSocketService({
messenger: backendWebSocketServiceMessenger,
url: 'wss://api.metamask.io/ws',
timeout: 15000,
requestTimeout: 20000,
});
// Initialize Account Activity service
const accountActivityService = new AccountActivityService({
messenger: accountActivityMessenger,
});
// Connect and subscribe to account activity
await backendWebSocketService.connect();
await accountActivityService.subscribe({
address: 'eip155:0:0x742d35cc6634c0532925a3b8d40c4e0e2c6e4e6',
});
// Listen for real-time updates
messenger.subscribe('AccountActivityService:transactionUpdated', (tx) => {
console.log('New transaction:', tx);
});
messenger.subscribe(
'AccountActivityService:balanceUpdated',
({ address, updates }) => {
console.log(`Balance updated for ${address}:`, updates);
},
);HTTP API for REST Requests:
import { ApiPlatformClient } from '@metamask/core-backend';
// Create API client
const apiClient = new ApiPlatformClient({
clientProduct: 'metamask-extension',
getBearerToken: async () => authController.getBearerToken(),
});
// Fetch data with automatic caching and deduplication
const balances = await apiClient.accounts.fetchV5MultiAccountBalances([
'eip155:1:0x742d35cc6634c0532925a3b8d40c4e0e2c6e4e6',
]);
const prices = await apiClient.prices.fetchV3SpotPrices([
'eip155:1/slip44:60', // ETH
'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC
]);// Coordinate with TokenBalancesController for fallback polling
messenger.subscribe(
'BackendWebSocketService:connectionStateChanged',
(info) => {
if (info.state === 'CONNECTED') {
// Reduce polling when WebSocket is active
messenger.call(
'TokenBalancesController:updateChainPollingConfigs',
{ '0x1': { interval: 600000 } }, // 10 min backup polling
{ immediateUpdate: false },
);
} else {
// Increase polling when WebSocket is down
const defaultInterval = messenger.call(
'TokenBalancesController:getDefaultPollingInterval',
);
messenger.call(
'TokenBalancesController:updateChainPollingConfigs',
{ '0x1': { interval: defaultInterval } },
{ immediateUpdate: true },
);
}
},
);
// Listen for account changes and manage subscriptions
messenger.subscribe(
'AccountsController:selectedAccountChange',
async (selectedAccount) => {
if (selectedAccount) {
await accountActivityService.subscribe({
address: selectedAccount.address,
});
}
},
);graph TD
subgraph "FRONTEND"
subgraph "Presentation Layer"
FE[Frontend Applications<br/>MetaMask Extension, Mobile, etc.]
end
subgraph "Integration Layer"
IL[Controllers, State Management, UI]
end
subgraph "Data layer (core-backend)"
subgraph "Domain Services"
AAS[AccountActivityService]
PUS[PriceUpdateService<br/>future]
CS[Custom Services...]
end
subgraph "Transport Layer"
WSS[WebSocketService<br/>• Connection management<br/>• Automatic reconnection<br/>• Message routing<br/>• Subscription management]
HTTP[HTTP API Clients<br/>• REST API calls<br/>• Automatic caching<br/>• Request deduplication<br/>• Retry with backoff]
end
end
end
subgraph "BACKEND"
BS[Backend Services<br/>REST APIs, WebSocket Services, etc.]
end
%% Flow connections
FE --> IL
IL --> AAS
IL --> PUS
IL --> CS
AAS --> WSS
AAS --> HTTP
PUS --> WSS
PUS --> HTTP
CS --> WSS
CS --> HTTP
WSS <--> BS
HTTP <--> BS
%% Styling
classDef frontend fill:#e1f5fe
classDef backend fill:#f3e5f5
classDef service fill:#e8f5e8
classDef transport fill:#fff3e0
class FE,IL frontend
class BS backend
class AAS,PUS,CS service
class WSS,HTTP transport
graph BT
%% External Controllers
AC["AccountsController<br/>(Auto-generated types)"]
AuthC["AuthenticationController<br/>(Auto-generated types)"]
TBC["TokenBalancesController<br/>(External Integration)"]
%% Core Services
AA["AccountActivityService"]
WS["BackendWebSocketService"]
%% Dependencies & Type Imports
AC -.->|"Import types<br/>(DRY)" | AA
AuthC -.->|"Import types<br/>(DRY)" | WS
WS -->|"Messenger calls"| AA
AA -.->|"Event publishing"| TBC
%% Styling
classDef core fill:#f3e5f5
classDef integration fill:#fff3e0
classDef controller fill:#e8f5e8
class WS,AA core
class TBC integration
class AC,AuthC controller
sequenceDiagram
participant TBC as TokenBalancesController
participant AA as AccountActivityService
participant WS as BackendWebSocketService
participant HTTP as HTTP Services<br/>(APIs & RPC)
participant Backend as WebSocket Endpoint<br/>(Backend)
Note over TBC,Backend: Initial Setup
TBC->>HTTP: Initial balance fetch via HTTP<br/>(first request for current state)
WS->>Backend: WebSocket connection request
Backend->>WS: Connection established
WS->>AA: WebSocket connection status notification<br/>(BackendWebSocketService:connectionStateChanged)<br/>{state: 'CONNECTED'}
AA->>AA: call('AccountsController:getSelectedAccount')
AA->>WS: subscribe({channels, callback})
WS->>Backend: {event: 'subscribe', channels: ['account-activity.v1.eip155:0:0x123...']}
Backend->>WS: {event: 'subscribe-response', subscriptionId: 'sub-456'}
Note over WS,Backend: System notification sent automatically upon subscription
Backend->>WS: {event: 'system-notification', data: {chainIds: ['eip155:1', 'eip155:137', ...], status: 'up'}}
WS->>AA: System notification received
AA->>AA: Track chains as 'up' internally
AA->>TBC: Chain availability notification<br/>(AccountActivityService:statusChanged)<br/>{chainIds: ['0x1', '0x89', ...], status: 'up'}
TBC->>TBC: Increase polling interval from 20s to 10min<br/>(.updateChainPollingConfigs({0x89: 600000}))
Note over TBC,Backend: User Account Change
par StatusChanged Event
TBC->>HTTP: Fetch balances for new account<br/>(fill transition gap)
and Account Subscription
AA->>AA: User switched to different account<br/>(AccountsController:selectedAccountChange)
AA->>WS: subscribe (new account)
WS->>Backend: {event: 'subscribe', channels: ['account-activity.v1.eip155:0:0x456...']}
Backend->>WS: {event: 'subscribe-response', subscriptionId: 'sub-789'}
AA->>WS: unsubscribe (previous account)
WS->>Backend: {event: 'unsubscribe', subscriptionId: 'sub-456'}
Backend->>WS: {event: 'unsubscribe-response'}
end
Note over TBC,Backend: Real-time Data Flow
Backend->>WS: {event: 'notification', channel: 'account-activity.v1.eip155:0:0x123...',<br/>data: {address, tx, updates}}
WS->>AA: Direct callback routing
AA->>AA: Validate & process AccountActivityMessage
par Balance Update
AA->>TBC: Real-time balance change notification<br/>(AccountActivityService:balanceUpdated)<br/>{address, chain, updates}
TBC->>TBC: Update balance state directly<br/>(or fallback poll if error)
and Transaction and Activity Update (Not yet implemented)
AA->>AA: Process transaction data<br/>(AccountActivityService:transactionUpdated)<br/>{tx: Transaction}
Note right of AA: Future: Forward to TransactionController<br/>for transaction state management<br/>(pending → confirmed → finalized)
end
Note over TBC,Backend: System Notifications
Backend->>WS: {event: 'system-notification', data: {chainIds: ['eip155:137'], status: 'down'}}
WS->>AA: System notification received
AA->>AA: Process chain status change
AA->>TBC: Chain status notification<br/>(AccountActivityService:statusChanged)<br/>{chainIds: ['eip155:137'], status: 'down'}
TBC->>TBC: Decrease polling interval from 10min to 20s<br/>(.updateChainPollingConfigs({0x89: 20000}))
TBC->>HTTP: Fetch balances immediately
Backend->>WS: {event: 'system-notification', data: {chainIds: ['eip155:137'], status: 'up'}}
WS->>AA: System notification received
AA->>AA: Process chain status change
AA->>TBC: Chain status notification<br/>(AccountActivityService:statusChanged)<br/>{chainIds: ['eip155:137'], status: 'up'}
TBC->>TBC: Increase polling interval from 20s to 10min<br/>(.updateChainPollingConfigs({0x89: 600000}))
Note over TBC,Backend: Connection Health Management
Backend-->>WS: Connection lost
WS->>AA: WebSocket connection status notification<br/>(BackendWebSocketService:connectionStateChanged)<br/>{state: 'DISCONNECTED'}
AA->>AA: Mark all tracked chains as 'down'<br/>(flush internal tracking set)
AA->>TBC: Chain status notification for all tracked chains<br/>(AccountActivityService:statusChanged)<br/>{chainIds: ['0x1', '0x89', ...], status: 'down'}
TBC->>TBC: Decrease polling interval from 10min to 20s<br/>(.updateChainPollingConfigs({0x89: 20000}))
TBC->>HTTP: Fetch balances immediately
WS->>WS: Automatic reconnection<br/>with exponential backoff
WS->>Backend: Reconnection successful
Note over AA,Backend: Restart initial setup - resubscribe and get fresh chain status
AA->>WS: subscribe (same account, new subscription)
WS->>Backend: {event: 'subscribe', channels: ['account-activity.v1.eip155:0:0x123...']}
Backend->>WS: {event: 'subscribe-response', subscriptionId: 'sub-999'}
Backend->>WS: {event: 'system-notification', data: {chainIds: [...], status: 'up'}}
WS->>AA: System notification received
AA->>AA: Track chains as 'up' again
AA->>TBC: Chain availability notification<br/>(AccountActivityService:statusChanged)<br/>{chainIds: [...], status: 'up'}
TBC->>TBC: Increase polling interval back to 10min
- Initial Setup: BackendWebSocketService establishes connection, then AccountActivityService subscribes to selected account. Backend automatically sends a system notification with all chains that are currently up. AccountActivityService tracks these chains internally and notifies TokenBalancesController, which increases polling interval to 5 min
- Chain Status Tracking: AccountActivityService maintains an internal set of chains that are 'up' based on system notifications. On disconnect, it marks all tracked chains as 'down' before clearing the set
- System Notifications: Backend automatically sends chain status updates (up/down) upon subscription and when status changes. AccountActivityService forwards these to TokenBalancesController, which adjusts polling intervals (up: 5min, down: 30s + immediate fetch)
- User Account Changes: When users switch accounts, AccountActivityService unsubscribes from old account and subscribes to new account. Backend sends fresh system notification with current chain status for the new account
- Connection Resilience: On reconnection, AccountActivityService resubscribes to selected account and receives fresh chain status via system notification. Automatic reconnection with exponential backoff
- Real-time Updates: Backend pushes data through: Backend → BackendWebSocketService → AccountActivityService → TokenBalancesController (+ future TransactionController integration)
- Parallel Processing: Transaction and balance updates processed simultaneously - AccountActivityService publishes both transactionUpdated (future) and balanceUpdated events in parallel
- Direct Balance Processing: Real-time balance updates bypass HTTP polling and update TokenBalancesController state directly
The WebSocket connects when ALL 3 conditions are true:
- ✅ Feature enabled -
isEnabled()callback returnstrue(feature flag) - ✅ User signed in -
AuthenticationController.isSignedIn = true - ✅ Wallet unlocked -
KeyringController.isUnlocked = true
Plus: Platform code must call connect() when app opens/foregrounds and disconnect() when app closes/backgrounds.
Idempotent connect():
- Safe to call multiple times - validates conditions and returns early if already connected
- Multiple rapid calls reuse the same connection promise (no duplicate connections)
- No debouncing needed - handled automatically
Auto-Reconnect:
- ✅ Unexpected disconnects (network issues, server restart) → Auto-reconnect
- ❌ Manual disconnects (app backgrounds, wallet locks, user signs out) → Stay disconnected
The HTTP API provides type-safe clients for accessing MetaMask backend REST APIs. It uses @tanstack/query-core for intelligent caching, request deduplication, and automatic retries.
Available APIs:
| API | Base URL | Purpose |
|---|---|---|
| Accounts | accounts.api.cx.metamask.io |
Balances, transactions, NFTs, token discovery |
| Prices | price.api.cx.metamask.io |
Spot prices, exchange rates, historical prices |
| Token | token.api.cx.metamask.io |
Token metadata, trending, top gainers |
| Tokens | tokens.api.cx.metamask.io |
Bulk asset operations, supported networks |
- ✅ Automatic request deduplication - Identical concurrent requests share a single network call
- ✅ Intelligent caching - Configurable stale times per data type (prices: 30s, balances: 1min, networks: 30min)
- ✅ Automatic retries - Exponential backoff with jitter, skips 4xx errors (except 429, 408)
- ✅ Type safety - Full TypeScript support with response types
- ✅ Bearer token caching - Auth tokens cached for 5 minutes
- ✅ Unified client - Single entry point or individual API clients
import {
ApiPlatformClient,
createApiPlatformClient,
} from '@metamask/core-backend';
// Create unified client
const client = new ApiPlatformClient({
clientProduct: 'metamask-extension',
clientVersion: '12.0.0',
getBearerToken: async () => authController.getBearerToken(),
});
// Access API methods through sub-clients
const networks = await client.accounts.fetchV2SupportedNetworks();
const balances = await client.accounts.fetchV5MultiAccountBalances([
'eip155:1:0x742d35cc6634c0532925a3b8d40c4e0e2c6e4e6',
]);
const prices = await client.prices.fetchV3SpotPrices([
'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
]);
const tokenList = await client.token.fetchTokenList(1);
const assets = await client.tokens.fetchV3Assets([
'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
]);Or use individual clients:
import { AccountsApiClient, PricesApiClient } from '@metamask/core-backend';
const accountsClient = new AccountsApiClient({
clientProduct: 'metamask-extension',
});
const pricesClient = new PricesApiClient({
clientProduct: 'metamask-extension',
getBearerToken: async () => token,
});Optional parameters: options is FetchOptions (e.g. staleTime, gcTime). queryOptions are API-specific filters (e.g. networks, cursor). Each fetch* method has a matching get*QueryOptions that returns the TanStack Query options object for use with useQuery, useInfiniteQuery, useSuspenseQuery, etc.
Handles account-related operations including balances, transactions, NFTs, and token discovery.
| Method | Description |
|---|---|
fetchV1SupportedNetworks(options?) |
Get supported networks (v1) |
fetchV2SupportedNetworks(options?) |
Get supported networks (v2) |
fetchV2ActiveNetworks(accountIds, queryOptions?, options?) |
Get active networks by CAIP-10 account IDs |
fetchV2Balances(address, queryOptions?, options?) |
Get balances for single address (supports networks, filterSupportedTokens, includeTokenAddresses, includeStakedAssets) |
fetchV4MultiAccountBalances(addresses, queryOptions?, options?) |
Get balances for multiple addresses |
fetchV5MultiAccountBalances(accountIds, queryOptions?, options?) |
Get balances using CAIP-10 IDs |
fetchV1TransactionByHash(chainId, txHash, queryOptions?, options?) |
Get transaction by hash |
fetchV1AccountTransactions(address, queryOptions?, options?) |
Get account transactions |
fetchV4MultiAccountTransactions(accountAddresses, queryOptions?, options?) |
Get multi-account transactions |
fetchV1AccountRelationship(chainId, from, to, options?) |
Get address relationship |
fetchV2AccountNfts(address, queryOptions?, options?) |
Get account NFTs |
fetchV2AccountTokens(address, queryOptions?, options?) |
Get detected ERC20 tokens |
getV1SupportedNetworksQueryOptions(options?) … getV2AccountTokensQueryOptions(...) |
Return TanStack Query options for each fetch (use with useQuery, useInfiniteQuery, etc.) |
invalidateBalances() |
Invalidate all balance cache |
invalidateAccounts() |
Invalidate all account cache |
Handles price-related operations including spot prices, exchange rates, and historical data.
| Method | Description |
|---|---|
fetchPriceV1SupportedNetworks(options?) |
Get price-supported networks (v1) |
fetchPriceV2SupportedNetworks(options?) |
Get price-supported networks in CAIP format (v2) |
fetchV1ExchangeRates(baseCurrency, options?) |
Get exchange rates for base currency |
fetchV1FiatExchangeRates(options?) |
Get fiat exchange rates |
fetchV1CryptoExchangeRates(options?) |
Get crypto exchange rates |
fetchV1SpotPricesByCoinIds(coinIds, options?) |
Get spot prices by CoinGecko IDs |
fetchV1SpotPriceByCoinId(coinId, currency?, options?) |
Get single coin spot price |
fetchV1TokenPrices(chainId, addresses, queryOptions?, options?) |
Get token prices on chain |
fetchV1TokenPrice(chainId, address, currency?, options?) |
Get single token price |
fetchV2SpotPrices(chainId, addresses, queryOptions?, options?) |
Get spot prices with market data |
fetchV3SpotPrices(assetIds, queryOptions?, options?) |
Get spot prices by CAIP-19 asset IDs |
fetchV1HistoricalPricesByCoinId(coinId, queryOptions?, options?) |
Get historical prices by CoinGecko ID |
fetchV1HistoricalPricesByTokenAddresses(chainId, addresses, queryOptions?, options?) |
Get historical prices for tokens |
fetchV1HistoricalPrices(chainId, address, queryOptions?, options?) |
Get historical prices for single token |
fetchV3HistoricalPrices(chainId, assetType, queryOptions?, options?) |
Get historical prices by CAIP-19 |
fetchV1HistoricalPriceGraphByCoinId(coinId, queryOptions?, options?) |
Get price graph by CoinGecko ID |
fetchV1HistoricalPriceGraphByTokenAddress(chainId, address, queryOptions?, options?) |
Get price graph by token address |
getPriceV1SupportedNetworksQueryOptions(options?) … getV1HistoricalPriceGraphByTokenAddressQueryOptions(...) |
Return TanStack Query options for each fetch |
invalidatePrices() |
Invalidate all price cache |
Handles token metadata, lists, and trending/popular token discovery.
| Method | Description |
|---|---|
fetchNetworks(options?) |
Get all networks |
fetchNetworkByChainId(chainId, options?) |
Get network by chain ID |
fetchTokenList(chainId, queryOptions?, options?) |
Get token list for chain |
fetchV1TokenMetadata(chainId, address, queryOptions?, options?) |
Get token metadata |
fetchTokenDescription(chainId, address, options?) |
Get token description |
fetchV3TrendingTokens(chainIds, queryOptions?, options?) |
Get trending tokens |
fetchV3TopGainers(chainIds, queryOptions?, options?) |
Get top gainers/losers |
fetchV3PopularTokens(chainIds, queryOptions?, options?) |
Get popular tokens |
fetchTopAssets(chainId, options?) |
Get top assets for chain |
fetchV1SuggestedOccurrenceFloors(options?) |
Get suggested occurrence floors |
getNetworksQueryOptions(options?) … getV1SuggestedOccurrenceFloorsQueryOptions(...) |
Return TanStack Query options for each fetch |
Handles bulk token operations and supported network queries.
| Method | Description |
|---|---|
fetchTokenV1SupportedNetworks(options?) |
Get token-supported networks (v1) |
fetchTokenV2SupportedNetworks(options?) |
Get token-supported networks with full/partial support (v2) |
fetchV3Assets(assetIds, queryOptions?, fetchOptions?) |
Fetch assets by CAIP-19 IDs |
getTokenV1SupportedNetworksQueryOptions(options?) … getV3AssetsQueryOptions(...) |
Return TanStack Query options for each fetch |
invalidateTokens() |
Invalidate all token cache |
type ApiPlatformClientOptions = {
/** Client product identifier (e.g., 'metamask-extension', 'metamask-mobile') */
clientProduct: string;
/** Optional client version (default: '1.0.0') */
clientVersion?: string;
/** Function to get bearer token for authenticated requests */
getBearerToken?: () => Promise<string | undefined>;
/** Optional custom QueryClient instance for shared caching */
queryClient?: QueryClient;
};Default Stale Times:
| Data Type | Stale Time |
|---|---|
| Prices | 30 seconds |
| Balances | 1 minute |
| Transactions | 30 seconds |
| Networks | 10 minutes |
| Supported Networks | 30 minutes |
| Token Metadata | 5 minutes |
| Token List | 10 minutes |
| Exchange Rates | 5 minutes |
| Trending | 2 minutes |
| Auth Token | 5 minutes |
Override Stale Time:
// Use custom stale time for specific request
const balances = await client.accounts.fetchV5MultiAccountBalances(
accountIds,
{ networks: ['eip155:1'] },
{ staleTime: 10000 }, // 10 seconds
);// Invalidate all caches
await client.invalidateAll();
// Invalidate auth token (on logout)
await client.invalidateAuthToken();
// Domain-specific invalidation
await client.accounts.invalidateBalances();
await client.prices.invalidatePrices();
await client.tokens.invalidateTokens();
// Clear all cached data
client.clear();
// Check if query is fetching
const isFetching = client.isFetching(['accounts', 'balances']);
// Access cached data directly
const cached = client.getCachedData(['accounts', 'balances', 'v5', { ... }]);
// Set cached data
client.setCachedData(queryKey, data);
// Access underlying QueryClient for advanced usage
const queryClient = client.queryClient;The core WebSocket client providing connection management, authentication, and message routing.
interface BackendWebSocketServiceOptions {
messenger: BackendWebSocketServiceMessenger;
url: string;
timeout?: number;
reconnectDelay?: number;
maxReconnectDelay?: number;
requestTimeout?: number;
enableAuthentication?: boolean;
enabledCallback?: () => boolean;
}connect(): Promise<void>- Establish authenticated WebSocket connectiondisconnect(): Promise<void>- Close WebSocket connectionsubscribe(options: SubscriptionOptions): Promise<SubscriptionResult>- Subscribe to channelssendRequest(message: ClientRequestMessage): Promise<ServerResponseMessage>- Send request/response messageschannelHasSubscription(channel: string): boolean- Check subscription statusfindSubscriptionsByChannelPrefix(prefix: string): SubscriptionInfo[]- Find subscriptions by prefixgetConnectionInfo(): WebSocketConnectionInfo- Get detailed connection state
High-level service for monitoring account activity using WebSocket data.
interface AccountActivityServiceOptions {
messenger: AccountActivityServiceMessenger;
subscriptionNamespace?: string;
}subscribe(subscription: SubscriptionOptions): Promise<void>- Subscribe to account activityunsubscribe(subscription: SubscriptionOptions): Promise<void>- Unsubscribe from account activity
AccountActivityService:balanceUpdated- Real-time balance changesAccountActivityService:transactionUpdated- Transaction status updatesAccountActivityService:statusChanged- Chain/service status changes