> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zerion.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a multi-chain portfolio tracker

> Fetch a wallet's total value and token holdings, aggregate multiple wallets, compare across chains, and chart performance over time with Zerion API.

**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](https://dashboard.zerion.io))
* One or more wallet addresses to track
* Node.js 18+ (for native `fetch`)

## Steps

<Steps>
  <Step title="Get a wallet's portfolio summary">
    A single call to the [portfolio endpoint](/api-reference/wallets/get-wallet-portfolio) returns the wallet's total value, chain distribution, and 24h change.

    <CodeGroup>
      ```javascript JavaScript theme={null}
      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 Python theme={null}
      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']}")
      ```

      ```bash cURL theme={null}
      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}'
      ```
    </CodeGroup>

    The response includes:

    | Field | Description |
    | - | - |
    | `total.positions` | Total USD value of all holdings |
    | `changes.absolute_1d` | Dollar change in the last 24 hours |
    | `changes.percent_1d` | Percentage change in the last 24 hours |
    | `positions_distribution_by_chain` | Value breakdown by chain (`ethereum`, `base`, etc.) |
    | `positions_distribution_by_type` | Value split by position type (`wallet`, `staked`, `deposited`, `locked`) |
  </Step>

  <Step title="List token holdings">
    Fetch individual [positions](/api-reference/wallets/get-wallet-fungible-positions) sorted by value. Use `filter[trash]=only_non_trash` to exclude spam tokens.

    <CodeGroup>
      ```javascript JavaScript theme={null}
      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 Python theme={null}
      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'})")
      ```

      ```bash cURL theme={null}
      curl -g -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))"'
      ```
    </CodeGroup>

    <Tip>
      **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.
    </Tip>

    **Handling pagination:** If a wallet holds many tokens, use `links.next` to fetch all pages:

    ```javascript theme={null}
    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;
    }
    ```
  </Step>

  <Step title="Aggregate multiple wallets">
    Fetch portfolio summaries for each wallet and combine them into a single view.

    ```javascript theme={null}
    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.
  </Step>

  <Step title="Fetch balance charts">
    Use the [balance chart endpoint](/api-reference/wallets/get-wallet-balance-chart) 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`.

    <CodeGroup>
      ```javascript JavaScript theme={null}
      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 Python theme={null}
      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}")
      ```

      ```bash cURL theme={null}
      curl -u "YOUR_API_KEY:" \
        "https://api.zerion.io/v1/wallets/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/charts/month?currency=usd"
      ```
    </CodeGroup>

    The response contains `data.attributes.points`, an array of `[timestamp, value]` pairs where:

    * `timestamp` - Unix timestamp (seconds)
    * `value` - portfolio value at that point in USD
  </Step>

  <Step title="Full working example">
    Save as `multi-wallet-tracker.mjs` and run with `node multi-wallet-tracker.mjs`:

    ```javascript theme={null}
    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();
    ```
  </Step>
</Steps>

## Next steps

* Add [DeFi positions](/recipes/defi-positions) to include staked and deposited assets in the totals
* Track [PnL and cost basis](/recipes/wallet-pnl-tracker) per wallet for tax reporting
* Set up [webhooks](/recipes/wallet-activity-alerts) to update the dashboard in real time when any tracked wallet transacts


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.