
Research
/Security News
CanisterWorm: npm Publisher Compromise Deploys Backdoor Across 29+ Packages
The worm-enabled campaign hit @emilgroup and @teale.io, then used an ICP canister to deliver follow-on payloads.
@metamask/core-backend
Advanced tools
@metamask/core-backendCore 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-backend
or
npm install @metamask/core-backend
WebSocket 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
The WebSocket connects when ALL 3 conditions are true:
isEnabled() callback returns true (feature flag)AuthenticationController.isSignedIn = trueKeyringController.isUnlocked = truePlus: Platform code must call connect() when app opens/foregrounds and disconnect() when app closes/backgrounds.
Idempotent connect():
Auto-Reconnect:
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 |
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 stateHigh-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 activityAccountActivityService:balanceUpdated - Real-time balance changesAccountActivityService:transactionUpdated - Transaction status updatesAccountActivityService:statusChanged - Chain/service status changesFAQs
Core backend services for MetaMask
The npm package @metamask/core-backend receives a total of 29,723 weekly downloads. As such, @metamask/core-backend popularity was classified as popular.
We found that @metamask/core-backend demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 4 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
The worm-enabled campaign hit @emilgroup and @teale.io, then used an ICP canister to deliver follow-on payloads.

Research
/Security News
Attackers compromised Trivy GitHub Actions by force-updating tags to deliver malware, exposing CI/CD secrets across affected pipelines.

Security News
ENISA’s new package manager advisory outlines the dependency security practices companies will need to demonstrate as the EU’s Cyber Resilience Act begins enforcing software supply chain requirements.