> ## 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 an NFT Portfolio Viewer

> Fetch a wallet's NFT holdings grouped by collection, with floor prices and chain breakdown.

**What you'll build:**

* Fetch a wallet's total NFT value broken down by chain
* List NFT collections sorted by floor value
* Drill into individual NFT positions with metadata
* Filter by specific collections

```
NFT value by chain:
  ethereum: $1,820.50
  polygon:  $37.56

Collections:
  Bored Ape Yacht Club:  1 NFT(s) — floor value $1,802.40
  Art Blocks Curated:    2 NFT(s) — floor value $55.11
  Polygon Apes:          3 NFT(s) — floor value $37.56

Individual NFTs:
  Bored Ape #7495 (Bored Ape Yacht Club)  — $1,802.40
  Chromie Squiggle #4821 (Art Blocks)     — $37.55
  Fidenza #479 (Art Blocks)               — $17.56
  Polygon Ape #102 (Polygon Apes)         — $12.52
```

**Time:** \~10 minutes

## Prerequisites

* A Zerion API key ([get one here](https://dashboard.zerion.io))
* A wallet address to query

## Steps

<Steps>
  ### Get the NFT portfolio overview

  Start by fetching the wallet's [NFT portfolio](/api-reference/wallets/get-wallet-nft-portfolio) to get total NFT value broken down by chain.

  <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}/nft-portfolio?currency=usd`,
      { headers }
    );

    const { data } = await response.json();
    const byChain = data.attributes.positions_distribution_by_chain;
    console.log("NFT value by chain:", byChain);
    ```

    ```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}/nft-portfolio",
        params={"currency": "usd"},
        auth=(api_key, ""),
    )

    data = response.json()["data"]
    by_chain = data["attributes"]["positions_distribution_by_chain"]
    print("NFT value by chain:", by_chain)
    ```

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

  The response includes `positions_distribution_by_chain` — a map of chain IDs to their total NFT floor value (e.g., `ethereum: 1820.50`, `polygon: 37.56`).

  <Note>
    The API may return a `202` status if the wallet's NFT data is still being indexed. Poll the endpoint until you receive a `200` response.
  </Note>

  ### Fetch NFT collections

  Get the wallet's [NFT collections](/api-reference/wallets/get-wallet-nft-collections) grouped by collection, sorted by total floor price.

  <CodeGroup>
    ```javascript JavaScript theme={null}
    const collectionsRes = await fetch(
      `https://api.zerion.io/v1/wallets/${address}/nft-collections/?currency=usd&sort=-total_floor_price`,
      { headers }
    );

    const collections = await collectionsRes.json();

    for (const collection of collections.data) {
      const { collection_info, nfts_count, total_floor_price } = collection.attributes;
      console.log(
        `${collection_info.name}: ${nfts_count} NFT(s) — floor value ${total_floor_price != null ? `$${total_floor_price.toFixed(2)}` : "N/A"}`
      );
    }
    ```

    ```python Python theme={null}
    collections_response = requests.get(
        f"https://api.zerion.io/v1/wallets/{address}/nft-collections/",
        params={"currency": "usd", "sort": "-total_floor_price"},
        auth=(api_key, ""),
    )

    for collection in collections_response.json()["data"]:
        attrs = collection["attributes"]
        name = attrs.get("collection_info", {}).get("name", "Unknown")
        count = attrs["nfts_count"]
        floor = attrs.get("total_floor_price")
        print(f"{name}: {count} NFT(s) — floor value {f'${floor:.2f}' if floor is not None else 'N/A'}")
    ```

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

  Each collection entry includes:

  * `collection_info.name` — the collection name
  * `nfts_count` — how many NFTs from this collection the wallet holds
  * `total_floor_price` — combined floor value of the wallet's positions in this collection
  * `min_changed_at` / `max_changed_at` — acquisition timestamps

  ### Fetch individual NFT positions

  Drill into the specific [NFT positions](/api-reference/wallets/get-wallet-nft-positions) held by the wallet, sorted by floor price.

  <CodeGroup>
    ```javascript JavaScript theme={null}
    const positionsRes = await fetch(
      `https://api.zerion.io/v1/wallets/${address}/nft-positions/?currency=usd&sort=-floor_price`,
      { headers }
    );

    const positions = await positionsRes.json();

    for (const position of positions.data) {
      const { nft_info, collection_info, value } = position.attributes;
      console.log(
        `${nft_info?.name} (${collection_info?.name}) — ${value != null ? `$${value.toFixed(2)}` : "N/A"}`
      );
    }
    ```

    ```python Python theme={null}
    positions_response = requests.get(
        f"https://api.zerion.io/v1/wallets/{address}/nft-positions/",
        params={"currency": "usd", "sort": "-floor_price"},
        auth=(api_key, ""),
    )

    for position in positions_response.json()["data"]:
        attrs = position["attributes"]
        nft_name = attrs.get("nft_info", {}).get("name", "Unknown")
        collection_name = attrs.get("collection_info", {}).get("name", "Unknown")
        value = attrs.get("value")
        print(f"{nft_name} ({collection_name}) — {f'${value:.2f}' if value is not None else 'N/A'}")
    ```

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

  Using `sort=-floor_price` orders NFTs by their floor price (highest first). Each position includes:

  * `nft_info.name` — the NFT's name or token ID
  * `collection_info.name` — the collection it belongs to
  * `value` — estimated USD value based on the collection's floor price

  ### Filter by collection (optional)

  To show NFTs from a specific collection, use the `filter[collection_ids]` parameter. Collection IDs are UUIDs found in the `relationships.collection.data.id` field of each NFT position returned by the endpoints above.

  ```bash theme={null}
  curl -u "YOUR_API_KEY:" \
    "https://api.zerion.io/v1/wallets/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/nft-positions/?currency=usd&filter[collection_ids]=bc31571c-d2a8-45e4-8012-37dbbf8b7038"
  ```

  ### Full working example

  Save as `nft-portfolio.mjs` and run with `node nft-portfolio.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 + ":")}`,
  };

  async function fetchWithRetry(url, maxRetries = 10) {
    for (let i = 0; i < maxRetries; i++) {
      const res = await fetch(url, { headers });
      if (res.status === 202) {
        const retryAfter = res.headers.get("Retry-After") || "3";
        console.log(`NFT data still indexing, retrying in ${retryAfter}s... (${i + 1}/${maxRetries})`);
        await new Promise((r) => setTimeout(r, parseInt(retryAfter) * 1000));
        continue;
      }
      if (!res.ok) throw new Error(`API error: ${res.status}`);
      return res.json();
    }
    throw new Error("Timed out waiting for NFT data after " + maxRetries + " retries");
  }

  async function displayNFTPortfolio(address) {
    // 1. Portfolio overview
    const portfolio = await fetchWithRetry(
      `${BASE_URL}/wallets/${address}/nft-portfolio?currency=usd`
    );
    const byChain = portfolio.data.attributes.positions_distribution_by_chain;

    console.log("=== NFT VALUE BY CHAIN ===");
    for (const [chain, value] of Object.entries(byChain).sort((a, b) => b[1] - a[1])) {
      console.log(`  ${chain}: $${value.toFixed(2)}`);
    }

    // 2. Collections
    const collections = await fetchWithRetry(
      `${BASE_URL}/wallets/${address}/nft-collections/?currency=usd&sort=-total_floor_price`
    );

    console.log("\n=== COLLECTIONS ===");
    for (const c of collections.data) {
      const { collection_info, nfts_count, total_floor_price } = c.attributes;
      console.log(`  ${collection_info.name}: ${nfts_count} NFT(s) — ${total_floor_price != null ? `$${total_floor_price.toFixed(2)}` : "N/A"}`);
    }

    // 3. Individual NFTs
    const positions = await fetchWithRetry(
      `${BASE_URL}/wallets/${address}/nft-positions/?currency=usd&sort=-floor_price`
    );

    console.log("\n=== INDIVIDUAL NFTs ===");
    for (const pos of positions.data) {
      const { nft_info, collection_info, value } = pos.attributes;
      console.log(`  ${nft_info?.name} (${collection_info?.name}) — ${value != null ? `$${value.toFixed(2)}` : "N/A"}`);
    }
  }

  displayNFTPortfolio("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
  ```
</Steps>

## Next steps

* Use [pagination](/pagination-and-filtering) to handle wallets with large NFT collections
* Include the `nft_collections` relationship in the `include` parameter to get full collection metadata in one request
* Combine with the [transactions endpoint](/api-reference/wallets/get-wallet-transactions) to track NFT purchase and sale history
