Build a Multichain Wallet Portfolio Tracker with Blockscout PRO API
Create a Portfolio Tracker like this (Standalone HTML tracker) in minutes with Blockscout's PRO API! Follow the easy tutorial below to learn about multichain queries, available stats, data, and how to get it.
All you need is a free PRO API key to get started.
Build a Multichain Wallet Portfolio Tracker with Blockscout PRO API
Wallets don't live on one chain anymore. A typical active user has ETH on mainnet, stables on Base or Arbitrum, NFTs on Optimism, and maybe even a forgotten airdrop on Polygon. If you're building any kind of wallet experience, tax tool, or analytics product, you need a reliable way to pull a single address's full history across all of them.
This guide walks through building a simple multichain portfolio tracker using the Blockscout PRO API. The output is a single-page tool that takes one wallet address, queries activity across six EVM chains in parallel, and shows you native transactions, token transfers, holdings, and a unified timeline, without writing a separate integration for each network.
What We're Building
A single-file HTML dashboard that takes a wallet address as input and returns:
- Native transactions and contract interactions per chain
- ERC-20, ERC-721, and ERC-1155 token transfers per chain
- Current token holdings with USD values per chain
- A unified, chain-tagged timeline of everything
We'll cover six chains in the example: Ethereum, Optimism, Arbitrum, Base, Gnosis, and Polygon. Adding a seventh chain is one line in a config array.
We also include an example at the edn which uses the Multichain service, offering the most efficient way to gather data from many chains.
A note on the included demo. A live version of this dashboard is hosted at eaas.blockscout.com/portfolio-api-example. It's a reference UI, not a production app. It demonstrates the layout, the unified-feed concept, and the shape of the data you'll get back from the PRO API, using a baked-in snapshot of real responses for vitalik.eth so you can see the UI immediately. The actual fetch logic is documented below; wiring it to live data requires a server-side proxy (covered in the CORS section). Treat the demo as a starting point you'd fork, not a deployed tool.The Three Endpoints That Do the Work
The whole tool runs on three Blockscout PRO API endpoints, each called once per chain. All three live under api.blockscout.com, with the chain ID in the URL path. The same path template works on every supported chain.
1. Address info: balance and basic metadata
GET https://api.blockscout.com/{chain_id}/api/v2/addresses/{address}?apikey={key}
Returns native balance, ENS name, contract status, and proxy info if applicable. This is your fast "does this address exist on this chain, and what's the headline number" call.
2. Transactions by address: native transfers and contract calls
GET https://api.blockscout.com/{chain_id}/api/v2/addresses/{address}/transactions?apikey={key}
Returns native ETH transfers and contract method calls made by the address. Importantly, this endpoint excludes ERC-20 transfer events. You'll see calls to token contracts, but not the Transfer events themselves. For tokens, use the next endpoint. (Internal transactions, if you need them, live at a separate /addresses/{address}/internal-transactions endpoint.)
3. Token transfers by address: all token movements
GET https://api.blockscout.com/{chain_id}/api/v2/addresses/{address}/token-transfers?apikey={key}
Returns every token transfer that involved this address (ERC-20, ERC-721, and ERC-1155) with token metadata (symbol, decimals, contract). Pair this with the transactions endpoint to get a complete picture. If you only want fungibles, append &type=ERC-20 (or ERC-721 / ERC-1155) to filter.
Bonus: Token holdings, current balances with USD values
GET https://api.blockscout.com/{chain_id}/api/v2/addresses/{address}/tokens?apikey={key}
Returns enriched token holdings (ERC-20, ERC-721, ERC-1155): balance, USD exchange rate, market cap, holder count. Useful for the "current portfolio" view alongside the historical timeline. Same type=ERC-20 filter applies if you want fungibles only.
Step-by-Step Build
Step 1: Define the chain registry
The whole point of the unified API is that you can list your chains once and iterate. Each entry needs an ID and a display label.
const CHAINS = [
{ id: 1, name: "Ethereum", color: "#627EEA" },
{ id: 10, name: "Optimism", color: "#FF0420" },
{ id: 42161, name: "Arbitrum", color: "#28A0F0" },
{ id: 8453, name: "Base", color: "#0052FF" },
{ id: 100, name: "Gnosis", color: "#48A9A6" },
{ id: 137, name: "Polygon", color: "#8247E5" },
];
Adding Scroll, Soneium, MegaETH, or any other supported chain is one more line.
Step 2: Build the fetch helper
A single function takes a chain ID and a path, attaches the API key, and returns JSON. Same shape for every chain, every endpoint.
async function fetchChain(chainId, path, params = {}) {
const url = new URL(`https://api.blockscout.com/${chainId}/api/v2${path}`);
url.searchParams.set("apikey", API_KEY);
for (const [k, v] of Object.entries(params)) {
url.searchParams.set(k, v);
}
const res = await fetch(url);
if (!res.ok) throw new Error(`${chainId}: ${res.status}`);
return res.json();
}
Step 3: Fan out across chains in parallel
The expensive part of multichain tooling is usually the sequential network round-trip. With one API host, Promise.all solves it.
async function loadWalletHistory(address) {
const results = await Promise.all(
CHAINS.map(async (chain) => {
const [info, txs, transfers, tokens] = await Promise.all([
fetchChain(chain.id, `/addresses/${address}`),
fetchChain(chain.id, `/addresses/${address}/transactions`),
fetchChain(chain.id, `/addresses/${address}/token-transfers`),
fetchChain(chain.id, `/addresses/${address}/tokens`),
]);
return { chain, info, txs, transfers, tokens };
})
);
return results;
}
Six chains, four endpoints each, twenty-four requests: all firing concurrently, all hitting the same gateway, all returning the same response shape.
Step 4: Merge into a unified timeline
This is where the value shows up. Each chain's transactions and token-transfers arrays have a timestamp field. Merge them, tag each entry with its source chain, and sort.
function buildTimeline(results) {
const events = [];
for (const { chain, txs, transfers } of results) {
for (const tx of txs.items || []) {
events.push({
chain: chain.name,
chainId: chain.id,
type: "tx",
timestamp: tx.timestamp,
hash: tx.hash,
from: tx.from?.hash,
to: tx.to?.hash,
value: tx.value,
method: tx.method,
});
}
for (const t of transfers.items || []) {
events.push({
chain: chain.name,
chainId: chain.id,
type: "token",
timestamp: t.timestamp,
hash: t.transaction_hash,
token: t.token?.symbol,
amount: t.total?.value,
from: t.from?.hash,
to: t.to?.hash,
});
}
}
return events.sort((a, b) =>
new Date(b.timestamp) - new Date(a.timestamp)
);
}
Step 5: Handle pagination
The transaction and token-transfer endpoints paginate via a next_page_params object. If your wallet is heavily active, you'll need to follow pagination to get a full history. The pattern is consistent:
async function fetchAllPages(chainId, path) {
const all = [];
let next = null;
do {
const data = await fetchChain(chainId, path, next || {});
all.push(...(data.items || []));
next = data.next_page_params || null;
} while (next);
return all;
}
For most wallet UIs, the first page (50 items) is fine for an at-a-glance view; full pagination is what you reach for in tax-export and analytics flows.
Step 6: Skip unneeded chains with the Multichain Service
The build above queries every chain in CHAINS, every time. That's twenty-four requests for a typical six-chain registry, fine but wasteful when most users are only active on two or three chains. The Blockscout Multichain Service gives you a single endpoint that aggregates results across every supported chain, so you can detect where an address is active before deciding which per-chain endpoints to call.
GET https://api.blockscout.com/multichain/api/v1/clusters/multichain/search:quick?q={address}&apikey={key}
Pass an address, a transaction hash, an ENS name, or a token symbol; get back grouped results by type and chain. For a wallet flow, the useful piece is the addresses array: each match tells you which chain the address has activity on.
async function discoverChains(address) {
const url = new URL(
"https://api.blockscout.com/multichain/api/v1/clusters/multichain/search:quick"
);
url.searchParams.set("q", address);
url.searchParams.set("apikey", API_KEY);
const data = await (await fetch(url)).json();
const activeChainIds = new Set(
(data.addresses ?? []).map(a => a.chain_id)
);
return CHAINS.filter(c => activeChainIds.has(c.id));
}
Wire it into the main flow so loadWalletHistory only fans out to chains where the address has actually appeared:
const activeChains = await discoverChains(address);
const results = await Promise.all(
activeChains.map(async (chain) => { /* same as before */ })
);
For a wallet that's only active on Ethereum and Base, this turns 24 requests into 8. For an address that's never been used at all, it turns 24 requests into 1. There's also a related chains endpoint that returns the full list of chains the service indexes, useful if you want to populate your registry dynamically rather than maintaining a hardcoded list.
Dashboard View
The dashboard renders three views from this data:
Per-chain summary table. One row per chain showing native balance, transaction count, token transfer count, and unique tokens held. This is the "where is this wallet active" answer in one screen.
Here's what it looks like for vitalik.eth (the demo address baked into the dashboard), with native balances pulled live from the API:
| Chain | Balance | Txs | Token Transfers | Unique Tokens |
|---|---|---|---|---|
| Ethereum | 1.83 ETH | 1,247 | 4,203 | 12 |
| Base | 0.066 ETH | 612 | 2,891 | 19 |
| Arbitrum | 0.084 ETH | 412 | 1,178 | 24 |
| Optimism | 0.178 ETH | 289 | 374 | 18 |
| Polygon | 577.07 POL | 67 | 172 | 21 |
| Gnosis | 8.71 xDAI | 45 | 34 | 6 |
Already useful as a snapshot: this address sits on every chain we queried.
Token holdings panel. Current token balances across all chains (ERC-20, ERC-721, ERC-1155), sorted by USD value, with the source chain tagged on each row.
Unified timeline. Every transaction and token transfer in chronological order, each row tagged with the chain it happened on. This is what most users actually want when they ask "what did I do last month."
What This Enables
For wallet teams, this is the foundation of a "global activity" view that doesn't require maintaining six per-chain integrations. One config array, one fetch helper, and the entire history fans out automatically as you add chains.
For tax tooling, a unified timeline with both native and token movements is the input format every cost-basis engine needs. Export the merged array as CSV and you have a clean ledger.
For analytics teams, this same pattern works for treasury monitoring (point it at your multisig addresses), partnership due diligence (point it at a partner's known addresses), or churn analysis (point it at a cohort of user addresses and look at activity decay across chains).
For product managers, it answers questions like "of users who started on Base, how many ended up on Arbitrum?" without having to stand up indexers or stream events.
Extensions
The base tool is small on purpose. Sensible next steps:
Add USD valuation to the timeline. The /tokens endpoint already returns a current exchange_rate field for each token, so present-day values come for free. For historical valuation (pricing each transfer at its own block timestamp), you'll need an external price feed (CoinGecko, your own oracle) keyed off the timestamp on each row, since the inline rate only reflects the present.
Render NFT holdings as visuals. Since /tokens and /token-transfers already return ERC-721 and ERC-1155 data alongside fungibles, the work isn't another integration, it's UI. Branch the rendering on token type: show fungibles as a numeric balance row, show NFTs as a thumbnail card with the token ID and collection name from the same response.
Add a CSV export. Once events are merged and sorted, dumping to CSV is a few lines. This is the single most-requested feature for tax flows.
Tune the scam-token filter. The token endpoints filter known scam airdrops by default. Phishing tokens with names like "Claim USDC at https://malicious-site.com" don't appear in the response unless you explicitly opt in via the show-scam-tokens header. That's usually what you want for a wallet UI. The case for opting in: if you're building a dispute or support tool, a tax-export flow, or an analytics view that needs to show everything the address has ever touched, you'll want the unfiltered list. Pass the header on those routes, then layer your own scoring on top using the circulating_market_cap, exchange_rate, and holders_count fields to decide what to surface or warn on.
Add cross-chain matching. When the same wallet bridges (e.g. via the Optimism bridge), you can match the deposit on L1 with the corresponding mint on L2 by looking for matching values within a time window. This turns the timeline into a "true" cross-chain ledger rather than two parallel ones.
A Note on CORS
The Blockscout PRO API doesn't permit browser-origin requests by default. If you're calling these endpoints from a frontend (the case in the hosted demo), you'll get CORS errors. Two solutions:
- Server-side proxy. Proxy requests through your own backend, which calls the API and returns the JSON. This is the standard pattern for production wallet apps.
- Serverless function. Same idea, deployed as a Cloudflare Worker, Vercel Edge Function, or AWS Lambda. Lower ops overhead than a full backend.
The included dashboard is a reference UI: it ships with a baked-in snapshot of real responses so the layout renders immediately, and the fetch helper above is the template you'd drop into your own server-side handler.
Multichain by Default
The thing worth restating: this entire tool is one set of endpoints, one auth method, one response shape, and it works across 100+ EVM chains by swapping the chain ID in the URL path. Whether you support six chains today or sixty next quarter, the integration code is the same.
There are really two layers of "multichain" at work in this build. The per-chain endpoints (/{chain_id}/api/v2/...) give you identical response shapes across every supported network, so the integration is uniform. The Multichain Service (/multichain/api/v1/...) sits one level up: it aggregates across chains in a single call, so the discovery is uniform too. Together they let you build a wallet experience where the user pastes an address and you don't need to know in advance which chains it lives on. That's the practical promise of the PRO API: you write the integration once, and every new chain you ship support for is a config addition, not a code change.
Try It
Sign up for a Blockscout PRO API key at dev.blockscout.com. The free tier covers most prototyping work and includes all the endpoints used in this guide. The full chain list and reference for the wallet history endpoints lives in the Blockscout developer docs.
A live version of the dashboard described in this article is hosted at eaas.blockscout.com/portfolio-api-example. Open it in a browser to see the UI in action.