Skip to main content

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

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

ClickHouse uses the collapsing log-table pattern (see the recipes overview) 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). token_id and amount are stored as text / wide numeric because full-range uint256 ids and counts overflow standard numeric types.
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.
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;.

Example reads

Current owners of a collection, latest non-zero holding per token_id + wallet (ClickHouse):
Current owners (Postgres, after refreshing the materialized view):
Distinct holder count for a collection (Postgres):

Modes

Shipped defaults: ClickHouse hybrid (backfill → realtime), Postgres / MySQL historical (one-shot backfill). For live/reorg-safe ingestion, use ClickHouse (see the overview).
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.

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.

Migrating from the REST API

This recipe 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 recipe’s owner surface (nft_owners on Postgres/MySQL, argMax over the ClickHouse fact table): one row per (token_id, owner) with amount > 0.
Reproducing the full REST response needs three recipes, not one. Run this recipe plus NFT Collection Metadata (name / symbol) and 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 recipe with your own metadata resolver, or drop those fields.
The reconstruction query (Postgres; adapt to your destination):
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.
  • 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 recipe, 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 recipe 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.

NFTs by Wallet

The same ingest, sorted by wallet: every NFT a wallet holds.

NFT Transfers

The event-level feed behind these owner observations.

NFT Collection Metadata

The name, symbol, and token URIs this recipe leaves out.

NFT Marketplace

The use case this owner surface powers.