Skip to main content
What you’ll build:
  • 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
=== WALLETS ===
Main (0xd8dA6B...): $12,017.49 (+2.33% 24h)
Trading (0x42b9df...): $3,450.21 (-0.85% 24h)

Combined total: $15,467.70

=== BY CHAIN ===
  ethereum: $9,214.01 (59.6%)
  base:     $3,573.03 (23.1%)
  arbitrum: $2,680.66 (17.3%)

=== TOP HOLDINGS (ALL WALLETS) ===
  ETH on ethereum (Main): 2.52 ($8,102.45)
  USDC on base (Trading): 2573.03 ($2,573.03)
  ARB on arbitrum (Main): 1450.00 ($1,230.45)
  ETH on base (Trading): 0.25 ($804.15)
  USDC on arbitrum (Main): 458.35 ($458.36)

=== MAIN — 1 MONTH CHART ===
  2/11/2026: $10,245.30
  2/18/2026: $11,102.88
  2/25/2026: $10,875.41
  3/4/2026:  $11,893.22
  3/11/2026: $12,017.49
Time: ~15 minutes

Prerequisites

  • A Zerion API key (get one here)
  • One or more wallet addresses to track
  • Node.js 18+ (for native fetch)

Steps

1
Get a wallet’s portfolio summary
2
A single call to the portfolio endpoint returns the wallet’s total value, chain distribution, and 24h change.
3
JavaScript
const API_KEY = process.env.ZERION_API_KEY;
const address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const headers = {
  accept: "application/json",
  authorization: `Basic ${btoa(API_KEY + ":")}`,
};

const response = await fetch(
  `https://api.zerion.io/v1/wallets/${address}/portfolio?currency=usd`,
  { headers }
);
if (!response.ok) throw new Error(`API error: ${response.status}`);

const { data } = await response.json();
const attrs = data.attributes;

console.log(`Total Value:  $${attrs.total.positions.toFixed(2)}`);
console.log(`24h Change:   ${(attrs.changes.percent_1d * 100).toFixed(2)}%`);
console.log(`Chains:`, attrs.positions_distribution_by_chain);
Python
import os, requests

api_key = os.environ["ZERION_API_KEY"]
address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"

response = requests.get(
    f"https://api.zerion.io/v1/wallets/{address}/portfolio",
    params={"currency": "usd"},
    auth=(api_key, ""),
)
response.raise_for_status()

data = response.json()["data"]["attributes"]
print(f"Total Value:  ${data['total']['positions']:.2f}")
print(f"24h Change:   {data['changes']['percent_1d'] * 100:.2f}%")
print(f"Chains:       {data['positions_distribution_by_chain']}")
cURL
curl -s -u "YOUR_API_KEY:" \
  "https://api.zerion.io/v1/wallets/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/portfolio?currency=usd" \
  | jq '.data.attributes | {total: .total.positions, change_24h: .changes.percent_1d, chains: .positions_distribution_by_chain}'
4
The response includes:
5
FieldDescriptiontotal.positionsTotal USD value of all holdingschanges.absolute_1dDollar change in the last 24 hourschanges.percent_1dPercentage change in the last 24 hourspositions_distribution_by_chainValue breakdown by chain (ethereum, base, etc.)positions_distribution_by_typeValue split by position type (wallet, staked, deposited, locked)
6
List token holdings
7
Fetch individual positions sorted by value. Use filter[trash]=only_non_trash to exclude spam tokens.
8
JavaScript
const positionsRes = await fetch(
  `https://api.zerion.io/v1/wallets/${address}/positions/?currency=usd&sort=-value&filter[trash]=only_non_trash`,
  { headers }
);
if (!positionsRes.ok) throw new Error(`API error: ${positionsRes.status}`);

const { data: positions, links } = await positionsRes.json();

for (const pos of positions) {
  const { fungible_info, value, quantity, price, changes } = pos.attributes;
  const chain = pos.relationships.chain.data.id;
  console.log(
    `${fungible_info.symbol} on ${chain}: ${quantity.float} (${value != null ? `$${value.toFixed(2)}` : "N/A"})`
  );
}
Python
positions_response = requests.get(
    f"https://api.zerion.io/v1/wallets/{address}/positions/",
    params={
        "currency": "usd",
        "sort": "-value",
        "filter[trash]": "only_non_trash",
    },
    auth=(api_key, ""),
)
positions_response.raise_for_status()

for pos in positions_response.json()["data"]:
    info = pos["attributes"]["fungible_info"]
    value = pos["attributes"]["value"]
    qty = pos["attributes"]["quantity"]["float"]
    chain = pos["relationships"]["chain"]["data"]["id"]
    print(f"{info['symbol']} on {chain}: {qty} ({f'${value:.2f}' if value is not None else 'N/A'})")
cURL
curl -s -u "YOUR_API_KEY:" \
  "https://api.zerion.io/v1/wallets/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/positions/?currency=usd&sort=-value&filter[trash]=only_non_trash" \
  | jq '.data[] | "\(.attributes.fungible_info.symbol) on \(.relationships.chain.data.id): \(.attributes.quantity.float) ($\(.attributes.value))"'
9
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.
10
Handling pagination — if a wallet holds many tokens, use links.next to fetch all pages:
11
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;
}
12
Aggregate multiple wallets
13
Fetch portfolio summaries for each wallet and combine them into a single view.
14
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)}`);
15
The full working example below also merges the per-chain breakdowns from each wallet to compare value across chains.
16
Fetch balance charts
17
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.
18
JavaScript
async function getBalanceChart(address, period = "month") {
  const res = await fetch(
    `${BASE_URL}/wallets/${address}/charts/${period}?currency=usd`,
    { headers }
  );
  return res.json();
}

const chart = await getBalanceChart(wallets[0].address, "month");
const points = chart.data.attributes.points;

for (const [timestamp, value] of points) {
  const date = new Date(timestamp * 1000).toLocaleDateString();
  console.log(`  ${date}: $${value.toFixed(2)}`);
}
Python
chart_response = requests.get(
    f"https://api.zerion.io/v1/wallets/{address}/charts/month",
    params={"currency": "usd"},
    auth=(api_key, ""),
)

for timestamp, value in chart_response.json()["data"]["attributes"]["points"]:
    from datetime import datetime
    date = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d")
    print(f"  {date}: ${value:.2f}")
cURL
curl -u "YOUR_API_KEY:" \
  "https://api.zerion.io/v1/wallets/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/charts/month?currency=usd"
19
The response contains data.attributes.points — an array of [timestamp, value] pairs where:
20
  • timestamp — Unix timestamp (seconds)
  • value — portfolio value at that point in USD
  • 21
    Full working example
    22
    Save as multi-wallet-tracker.mjs and run with node multi-wallet-tracker.mjs:
    23
    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