> ## 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 Portfolio Tracker

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

**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>
  ### 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`) |

  ### 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 -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;
  }
  ```

  ### 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.

  ### 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

  ### 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();
  ```
</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
