> ## 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/recipes/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 [recipes overview](/data-feeds/recipes/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 recipe produces — one row per holding observation. Keep the columns you need and drop the rest (see [Schema & flexibility](/data-feeds/recipes/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/recipes/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 recipe 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 recipe 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 recipe).
* `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`.

### Related

<Columns cols={2}>
  <Card title="NFTs by Wallet" href="/data-feeds/recipes/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/recipes/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/recipes/nft/nft-collection-metadata" icon="image">
    The name, symbol, and token URIs this recipe 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>
