- Fetch a wallet’s total value, 24h change, and chain breakdown
- List token holdings with prices and handle pagination
- Aggregate portfolio value across multiple wallets
- Fetch balance charts to visualize value over time
Prerequisites
- A Zerion API key (get one here)
- One or more wallet addresses to track
- Node.js 18+ (for native
fetch)
Steps
A single call to the portfolio endpoint returns the wallet’s total value, chain distribution, and 24h change.
total.positionschanges.absolute_1dchanges.percent_1dpositions_distribution_by_chainethereum, base, etc.)positions_distribution_by_typewallet, staked, deposited, locked)Fetch individual positions sorted by value. Use
filter[trash]=only_non_trash to exclude spam tokens.Key parameters:
sort=-value (highest value first), filter[positions]=only_simple (wallet tokens only, excludes DeFi), filter[trash]=only_non_trash (excludes spam). Use filter[fungible_ids]=eth,0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 to query specific tokens only.async function getAllPositions(address) {
const allPositions = [];
let url = `https://api.zerion.io/v1/wallets/${address}/positions/?currency=usd&sort=-value&filter[trash]=only_non_trash`;
while (url) {
const response = await fetch(url, { headers });
const { data, links } = await response.json();
allPositions.push(...data);
url = links.next || null;
}
return allPositions;
}
const API_KEY = process.env.ZERION_API_KEY;
const BASE_URL = "https://api.zerion.io/v1";
const headers = {
accept: "application/json",
authorization: `Basic ${btoa(API_KEY + ":")}`,
};
const wallets = [
{ label: "Main", address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
{ label: "Trading", address: "0x42b9df65b219b3dd36ff330a4dd8f327a6ada990" },
];
async function getPortfolio(address) {
const res = await fetch(
`${BASE_URL}/wallets/${address}/portfolio?currency=usd`,
{ headers }
);
return res.json();
}
// Fetch all wallets in parallel
const results = await Promise.all(
wallets.map(async (w) => {
const { data } = await getPortfolio(w.address);
return { ...w, portfolio: data.attributes };
})
);
let grandTotal = 0;
for (const w of results) {
const total = w.portfolio.total.positions;
grandTotal += total;
console.log(`${w.label}: $${total.toFixed(2)}`);
}
console.log(`\nCombined: $${grandTotal.toFixed(2)}`);
The full working example below also merges the per-chain breakdowns from each wallet to compare value across chains.
Use the balance chart endpoint to visualize how portfolio value has changed over time. The period is part of the URL path. Supported periods:
day, week, month, 3months, 6months, year, 5years, max.const API_KEY = process.env.ZERION_API_KEY;
const BASE_URL = "https://api.zerion.io/v1";
const headers = {
accept: "application/json",
authorization: `Basic ${btoa(API_KEY + ":")}`,
};
const wallets = [
{ label: "Main", address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" },
{ label: "Trading", address: "0x42b9df65b219b3dd36ff330a4dd8f327a6ada990" },
];
async function getPortfolio(address) {
const res = await fetch(`${BASE_URL}/wallets/${address}/portfolio?currency=usd`, { headers });
return res.json();
}
async function getPositions(address) {
const res = await fetch(
`${BASE_URL}/wallets/${address}/positions/?currency=usd&sort=-value&filter[positions]=only_simple`,
{ headers }
);
return res.json();
}
async function getBalanceChart(address, period = "month") {
const res = await fetch(
`${BASE_URL}/wallets/${address}/charts/${period}?currency=usd`,
{ headers }
);
return res.json();
}
async function buildDashboard() {
// 1. Aggregate portfolios
const results = await Promise.all(
wallets.map(async (w) => {
const [portfolio, positions] = await Promise.all([
getPortfolio(w.address),
getPositions(w.address),
]);
return { ...w, portfolio: portfolio.data.attributes, positions: positions.data };
})
);
let grandTotal = 0;
const chainTotals = {};
console.log("=== WALLETS ===");
for (const w of results) {
const total = w.portfolio.total.positions;
const change = (w.portfolio.changes.percent_1d * 100).toFixed(2);
grandTotal += total;
console.log(`${w.label} (${w.address.slice(0, 8)}...): $${total.toFixed(2)} (${change}% 24h)`);
for (const [chain, value] of Object.entries(w.portfolio.positions_distribution_by_chain)) {
chainTotals[chain] = (chainTotals[chain] || 0) + value;
}
}
console.log(`\nCombined total: $${grandTotal.toFixed(2)}`);
// 2. Chain breakdown
console.log("\n=== BY CHAIN ===");
const sortedChains = Object.entries(chainTotals).sort((a, b) => b[1] - a[1]);
for (const [chain, value] of sortedChains) {
const pct = ((value / grandTotal) * 100).toFixed(1);
console.log(` ${chain}: $${value.toFixed(2)} (${pct}%)`);
}
// 3. Top holdings across all wallets
const allPositions = results.flatMap((w) =>
w.positions.map((p) => ({ ...p, wallet: w.label }))
);
allPositions.sort((a, b) => (b.attributes.value || 0) - (a.attributes.value || 0));
console.log("\n=== TOP HOLDINGS (ALL WALLETS) ===");
for (const pos of allPositions.slice(0, 10)) {
const { fungible_info, value, quantity } = pos.attributes;
const chain = pos.relationships.chain.data.id;
console.log(
` ${fungible_info.symbol} on ${chain} (${pos.wallet}): ${quantity.float} (${value != null ? `$${value.toFixed(2)}` : "N/A"})`
);
}
// 4. Balance chart for first wallet
const chart = await getBalanceChart(wallets[0].address, "month");
console.log(`\n=== ${wallets[0].label.toUpperCase()} — 1 MONTH CHART ===`);
// Sample 5 evenly-spaced points for a quick overview
const points = chart.data.attributes.points;
const step = Math.max(1, Math.floor(points.length / 5));
for (let i = 0; i < points.length; i += step) {
const date = new Date(points[i][0] * 1000).toLocaleDateString();
console.log(` ${date}: $${points[i][1].toFixed(2)}`);
}
}
buildDashboard();
Next steps
- Add DeFi positions to include staked and deposited assets in the totals
- Track PnL and cost basis per wallet for tax reporting
- Set up webhooks to update the dashboard in real time when any tracked wallet transacts