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

# NFT Owners by Contract

> Sync the current owner set for an NFT collection (every wallet holding a token_id, with live counts for ERC-1155 editions) into your own database. Mirrors the Moralis GET /nft/{address}/owners endpoint.

### Question it answers

> "Give me every current owner of NFT collection **0x…**: each `token_id`, the wallet that holds it, and how many (ERC-1155 editions)." Mirrors Moralis `GET /nft/{address}/owners`.

This is the by-collection sibling of [NFTs by Wallet](/data-feeds/templates/wallet/nfts-by-wallet): **same ingest, sorted the other way.** Each NFT transfer carries the **absolute** post-transfer holdings of both wallets, so the latest observation per `(token_address, token_id, wallet)` is the current owner. Storing the resolved owner set in your own database, keyed by collection, is the value.

### What you get

Each NFT transfer becomes **two per-wallet holding observations** (`leg = from | to`), where `amount` is the absolute count of that `token_id` held by the wallet *after* the transfer. The latest observation per `(token_address, token_id, wallet_address)` wins; keep `amount > 0` for the current owner set.

| Column                         | Description                                                                                                      |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `token_address`                | The NFT collection (contract)                                                                                    |
| `token_id`                     | The specific NFT: `uint256`, stored as text (256-bit ids overflow numeric types)                                 |
| `wallet_address`               | The holder; the EVM zero address is excluded (mint/burn counterparty)                                            |
| `amount`                       | Absolute post-transfer holding of this `token_id`: `0`/`1` for ERC-721, an arbitrary count for ERC-1155 editions |
| `contract_type`                | ERC-721 / ERC-1155 discriminator                                                                                 |
| `leg`                          | `from` or `to`: which side of the transfer produced this observation                                             |
| `block_number`, `log_index`    | Recency tuple; latest wins per holding key                                                                       |
| `event_ts` / `block_timestamp` | Block time                                                                                                       |

### Source

The transform reads one per-block array, `nftTokenTransfers`, and unpivots each event into two observations using its `fromPostBalance` / `toPostBalance` fields (the from- and to-wallet's absolute count of that `token_id` after the transfer). No price or metadata join is involved.

### Destination

| Destination                  | Table                                                        | By-collection access                                                                                             |
| ---------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| **ClickHouse** (first-class) | `fact_nft_owners_by_contract`                                | Prefix scan on `(chain_id, token_address, token_id, wallet_address, …)`; current owner via `argMax` over `FINAL` |
| **Postgres**                 | `nft_owner_observations` → `nft_owners` (materialized view)  | Partial index `(token_address, token_id, wallet_address) WHERE amount > 0`                                       |
| **MySQL**                    | `nft_owner_observations` → `nft_owners` (trigger-maintained) | PK `(token_address, token_id, wallet_address)` + `amount = 0` cleanup                                            |

ClickHouse uses the collapsing log-table pattern (see the [templates overview](/data-feeds/templates/overview#destinations)) so chain reorganizations self-correct. The fact table is sorted collection-first, so "every owner of collection X" is a contiguous range read. Ownership is **latest-wins**: an observation is current state for its holding key, not an append-only event. Read canonical state with `FINAL` then `argMax`, never a bare `WHERE sign = 1`.

### Full schema

Below is the complete read table this template produces: one row per holding observation. Keep the columns you need and drop the rest (see [Schema & flexibility](/data-feeds/templates/overview#schema--flexibility)). `token_id` and `amount` are stored as text / wide numeric because full-range `uint256` ids and counts overflow standard numeric types.

<Accordion title="ClickHouse, fact_nft_owners_by_contract">
  ```sql theme={null}
  CREATE TABLE recipe_nft_owners_by_contract.fact_nft_owners_by_contract
  (
      vendor_event_id   String,
      ingested_at       DateTime64(3),
      chain_id          UInt32,
      block_hash        String,
      block_number      UInt64,
      log_index         UInt32,
      event_ts          DateTime64(3),
      token_address     String,
      token_id          String,                   -- uint256, kept as text
      contract_type     LowCardinality(String),   -- ERC721 | ERC1155
      wallet_address    String,
      amount            String,                   -- absolute post-transfer holding of this token_id
      leg               LowCardinality(String),   -- from | to
      sign              Int8
  )
  ENGINE = ReplicatedCollapsingMergeTree(
      '/clickhouse/tables/{database}/fact_nft_owners_by_contract', '{replica}', sign)
  PARTITION BY (chain_id, toYYYYMM(event_ts))
  ORDER BY (chain_id, token_address, token_id, wallet_address, block_number, log_index, vendor_event_id, leg);
  ```

  The `sign` column drives reorg collapsing: read with `FINAL` then `argMax`, or a sign-aware aggregate. A single-node setup can use `CollapsingMergeTree(sign)` without the replication path.
</Accordion>

<Accordion title="Postgres, nft_owner_observations + nft_owners">
  ```sql theme={null}
  CREATE TABLE public.nft_owner_observations (
    position         BIGINT  NOT NULL,
    log_index        BIGINT  NOT NULL,
    block_number     BIGINT  NOT NULL,
    block_timestamp  BIGINT  NOT NULL,         -- unix seconds
    token_address    TEXT    NOT NULL,
    token_id         TEXT    NOT NULL,         -- uint256, kept as text
    contract_type    TEXT    NOT NULL,         -- ERC721 | ERC1155
    wallet_address   TEXT    NOT NULL,
    amount           NUMERIC(76, 0) NOT NULL,  -- absolute post-balance of this token_id
    leg              TEXT    NOT NULL,
    vendor_event_id  TEXT    NOT NULL
  );

  -- Recency index for the DISTINCT ON; leads with the holding key so
  -- REFRESH MATERIALIZED VIEW CONCURRENTLY avoids a sort.
  CREATE INDEX noo_owner_recency_idx
    ON public.nft_owner_observations
    (token_address, token_id, wallet_address, block_number DESC, log_index DESC);

  -- Current-owner surface: latest observation per (token, id, wallet).
  CREATE MATERIALIZED VIEW public.nft_owners AS
  SELECT DISTINCT ON (token_address, token_id, wallet_address)
    token_address, token_id, wallet_address, contract_type,
    amount, block_number, log_index, block_timestamp
  FROM public.nft_owner_observations
  ORDER BY token_address, token_id, wallet_address, block_number DESC, log_index DESC;

  CREATE UNIQUE INDEX nft_owners_pk
    ON public.nft_owners (token_address, token_id, wallet_address);

  -- Primary access path: current owners of a collection.
  CREATE INDEX nft_owners_by_contract_active_idx
    ON public.nft_owners (token_address, token_id, wallet_address) WHERE amount > 0;

  -- OPTIONAL: sibling by-wallet access path — every NFT held by a wallet.
  -- CREATE INDEX nft_owners_by_wallet_active_idx
  --   ON public.nft_owners (wallet_address, token_address, token_id) WHERE amount > 0;
  ```

  MySQL is the same shape with a trigger-maintained `nft_owners` state table and a `DELETE … WHERE amount = 0` cleanup. `position` is the block-level cursor used during backfill. Refresh the materialized view on a schedule with `REFRESH MATERIALIZED VIEW CONCURRENTLY nft_owners;`.
</Accordion>

### Example reads

Current owners of a collection, latest non-zero holding per `token_id` + wallet (ClickHouse):

```sql theme={null}
SELECT token_id, wallet_address, contract_type, current_amount
FROM (
  SELECT token_id,
         wallet_address,
         argMax(contract_type, (block_number, log_index)) AS contract_type,
         argMax(amount, (block_number, log_index))        AS current_amount
  FROM recipe_nft_owners_by_contract.fact_nft_owners_by_contract FINAL
  WHERE chain_id = 1 AND token_address = lower('0x...')
  GROUP BY token_id, wallet_address
)
WHERE current_amount != '0' AND current_amount != ''
ORDER BY token_id, wallet_address;
```

Current owners (Postgres, after refreshing the materialized view):

```sql theme={null}
REFRESH MATERIALIZED VIEW CONCURRENTLY nft_owners;

SELECT token_id, wallet_address, contract_type, amount
FROM public.nft_owners
WHERE token_address = lower('0x...') AND amount > 0
ORDER BY token_id, wallet_address;
```

Distinct holder count for a collection (Postgres):

```sql theme={null}
SELECT count(DISTINCT wallet_address)
FROM public.nft_owners
WHERE token_address = lower('0x...') AND amount > 0;
```

### Modes

Shipped defaults: **ClickHouse `hybrid`** (backfill → realtime), **Postgres / MySQL `historical`** (one-shot backfill). For live/reorg-safe ingestion, use ClickHouse (see the [overview](/data-feeds/templates/overview#modes)).

<Note>
  Realtime/hybrid on Postgres / MySQL is constrained because the block-level `position` cursor requires a single-column unique key. Run realtime/hybrid on **ClickHouse**; the Postgres / MySQL configs target `historical` backfill.
</Note>

### Multichain

The template is chain-parametrized via the `chain` setting: point it at any supported EVM chain. NFTs on Solana are SPL tokens; if a chain emits NFT transfers under the same `nftTokenTransfers` struct, set the Solana chain variables. The `vendor_event_id` already incorporates `(token_id, amount)` on top of `log_index` to stay row-unique under Solana's repeated log indices; the owner surface it produces is identical in shape.

### Fidelity gaps

This template returns on-chain ownership only. Fields a Moralis `/nft/{address}/owners` response carries that have **no onchain source** (they need a separate metadata/indexer service) are not populated:

* `name`, `symbol`, `token_uri`, `metadata`, `normalized_metadata`: collection / token metadata (use an NFT Collection Metadata template).
* `possible_spam`, verified-collection flags: risk / curation signals.
* `block_number_minted` and historical mint provenance beyond the backfill window.

`amount` is the raw on-chain holding count, not a marketplace listing. The core access key (`token_address` → owners with `token_id`, `amount`, `contract_type`) is fully sourced from `nftTokenTransfers`.

## Migrating from the REST API

This template replaces the by-collection ownership endpoints of the Web3 API. Data Feeds are **not** 1:1 replicas of those responses: you land the underlying data in your own database and reconstruct the response shape with a query. The subsections below cover each REST endpoint: what replaces it, the field mapping (**exact** = straight from the chain, **calculated** = derived from real trades, very close, **add yourself** = off-chain signal not in the feed), and the endpoint-specific gotchas.

### GET /nft/:address/owners

Every owner of every token in a collection. Replaced by this template's owner surface (`nft_owners` on Postgres/MySQL, `argMax` over the ClickHouse fact table): one row per `(token_id, owner)` with `amount > 0`.

<Warning>
  Reproducing the full REST response needs **three templates**, not one. Run this template plus [NFT Collection Metadata](/data-feeds/templates/nft/nft-collection-metadata) (`name` / `symbol`) and [NFT Transfers](/data-feeds/templates/nft/nft-transfers) (mint provenance; the mint is the transfer from the zero address) against the same database. The resolved token metadata is a different story entirely: `token_uri`, `metadata`, and `normalized_metadata` are derived from each token's `tokenURI` off-chain. Pair the template with your own metadata resolver, or drop those fields.
</Warning>

| `/nft/:address/owners`                                             | Data Feeds                                      | Fidelity   |
| ------------------------------------------------------------------ | ----------------------------------------------- | ---------- |
| `token_address`, `token_id`, `contract_type`, `owner_of`, `amount` | `nft_owners.*` (`owner_of` = `wallet_address`)  | exact      |
| `block_number`                                                     | `nft_owners.block_number`                       | exact      |
| `block_number_minted`, `minter_address`                            | `nft_transfers` (mint = from the zero address)  | exact      |
| `name`, `symbol`                                                   | `nft_collection_metadata.*`                     | exact      |
| `token_hash`                                                       | derive (hash of contract + id)                  | exact      |
| `last_sale` USD                                                    | `nftTrades` + price feed                        | calculated |
| `token_uri`, `metadata`, `normalized_metadata`                     | add yourself (resolve token URI)                | off-chain  |
| `rarity_*`, `floor_price*`, `list_price`                           | add yourself (rarity tool / marketplace source) | off-chain  |
| `possible_spam`, `verified_collection`, `*_sync` timestamps        | add yourself                                    | off-chain  |

The reconstruction query (Postgres; adapt to your destination):

```sql theme={null}
SELECT o.token_id, o.wallet_address AS owner_of, o.amount, o.contract_type,
       c.name, c.symbol, o.block_number
FROM nft_owners o
LEFT JOIN nft_collection_metadata c ON c.token_address = o.token_address
WHERE o.token_address = lower('0x...') AND o.amount > 0
ORDER BY o.token_id, o.wallet_address;
```

**Gotchas**

* **Backfill from the collection's deploy block.** Ownership is current state (the cumulative result of every transfer), so a late start returns stale or missing owners. See [History & backfill](/data-feeds/concepts/history-and-backfill).
* **ERC-1155** token\_ids can have many owners, so you get a row per `(token_id, owner)`; ERC-721 has one owner per id.
* Big collections paginate heavily; keyset on `(token_id, wallet_address)` instead of offset pages.

### GET /nft/:address

Every NFT in a collection with its current owner. Same template, same tables, same joins as `/nft/:address/owners` above: the two endpoints are one read ordered differently (`ORDER BY o.token_id` for the collection listing). The field mapping is identical, plus the listing's extra enrichment fields: `media`, the collection editorial fields (logo, banner, category, social links), and `floor_price*` are all off-chain. **Add yourself** from your metadata resolver and marketplace sources, or drop them.

**Gotchas**

* The REST endpoint was mostly a metadata product; the chain only carries ownership, mint, and identity. If resolved JSON metadata and media are the entire job, keep a metadata indexer for that layer; this template supplies the ownership and provenance to pair with it.
* The REST `totalRanges` / `range` parallel-scan pagination has no equivalent: it's your table now; scan or keyset on `token_id`.
* Backfill from the collection's deploy block, as above.

### Related

<Columns cols={2}>
  <Card title="NFTs by Wallet" href="/data-feeds/templates/wallet/nfts-by-wallet" icon="wallet">
    The same ingest, sorted by wallet: every NFT a wallet holds.
  </Card>

  <Card title="NFT Transfers" href="/data-feeds/templates/nft/nft-transfers" icon="arrow-right-arrow-left">
    The event-level feed behind these owner observations.
  </Card>

  <Card title="NFT Collection Metadata" href="/data-feeds/templates/nft/nft-collection-metadata" icon="image">
    The name, symbol, and token URIs this template leaves out.
  </Card>

  <Card title="NFT Marketplace" href="/data-feeds/use-cases/nft-marketplace" icon="store">
    The use case this owner surface powers.
  </Card>
</Columns>
