Skip to main content

Question it answers

“Give me the collection metadata (name, symbol, contract type) for NFT collection 0x….” Mirrors Moralis GET /nft/{address}/metadata.
This is the NFT-collection sibling of Token Metadata. Both read the same per-block deployed-contracts stream, but this recipe keeps only NFT-type contracts (ERC-721 / ERC-1155) and drops ERC-20 tokens. Collection metadata is fixed at deploy time, so there is normally exactly one row per (chain_id, token_address): the latest deploy by (block_number, transaction_index) is canonical (defensive against a CREATE2 re-deploy at a reused address).

What you get

One row per NFT-type contract deploy, keyed by the collection (token_address): Unlike the ERC-20 path, symbol-less contracts are not dropped: name/symbol are best-effort on-chain fields, not filter keys. Far fewer rows land than Token Metadata: only a handful of NFT contracts deploy per ~1000 blocks.

Source

The transform reads the per-block deployed-contracts stream and keeps the NFT slice, contracts whose type contains NFT (ERC-721 / ERC-1155). A contract is kept when its deployer_address is non-empty. contract_type is derived from the contract’s supportedInterfaces (EIP-165 ids): 0xd9b67a26ERC1155, else 0x80ac58cdERC721, else the NFT fallback.

Destination

ClickHouse uses the collapsing log-table pattern (see the recipes overview) so a reorg of a deploy block self-corrects. Because a collection can in principle deploy more than once at the same address (CREATE2), reads resolve latest deploy wins with argMax(…, (block_number, transaction_index)) rather than assuming a single row. Postgres derives the current-metadata surface as a DISTINCT ON (token_address) materialized view; MySQL maintains it incrementally with a latest-wins trigger.

Full schema

The complete read table this recipe produces. Keep the columns you need, name/symbol/contract_type are usually enough for a metadata lookup (see Schema & flexibility). name and symbol are stored as text; they can contain quotes and odd characters, which are parameter-bound on insert so there is no quoting breakage.
The sign column drives reorg collapsing, read with FINAL or a sign-aware aggregate, never a bare WHERE sign = 1. A single-node setup can use CollapsingMergeTree(sign) without the replication path. The collection-keyed sort order makes a single-collection lookup a tight prefix scan.
The sink appends to nft_collection_metadata_observations; refresh the current-metadata view on a schedule with REFRESH MATERIALIZED VIEW CONCURRENTLY nft_collection_metadata;. MySQL mirrors this shape, VARCHAR(255) for name/symbol, with the current-metadata nft_collection_metadata table maintained incrementally by an AFTER INSERT latest-wins trigger.

Example reads

Metadata for one collection, latest deploy wins (ClickHouse):
All NFT collections deployed in a block range (ClickHouse):
Metadata for one collection (Postgres, after REFRESH MATERIALIZED VIEW CONCURRENTLY nft_collection_metadata;):

Modes

Shipped defaults: ClickHouse hybrid (backfill → realtime), Postgres / MySQL historical (one-shot backfill). For live/reorg-safe ingestion, use ClickHouse, see the overview.
The realtime reorg path on Postgres / MySQL needs a single-column UNIQUE on the block-level position, but a single block can deploy several NFT contracts, so the array-expanded rows can only carry a composite unique. Run realtime/hybrid on ClickHouse, where the collapsing log table corrects reorgs per-block. The Postgres / MySQL configs are intended for historical backfill, which is the dominant use of this recipe, a once-off metadata census.

EVM only

This recipe is EVM-shaped: it reads EVM contract deploys. Solana NFT (Metaplex) collection metadata does not flow through the deployed-contracts stream the same way EVM contract deploys do, so the recipe targets EVM chains. It is chain-parametrized via the chain setting across any supported EVM chain.

Fidelity gaps

GET /nft/{address}/metadata returns enrichment fields this recipe does not source, all off-chain indexer or editorial state:
  • collection_logo / collection_banner_image, off-chain CDN assets.
  • collection_category: editorial categorisation.
  • project_url / discord_url / telegram_url / wiki_url, social links.
  • possible_spam / verified_collection, spam & verification heuristics from a separate pipeline.
  • synced_at (wall-clock), this recipe carries on-chain block_number / block_timestamp instead.
The core on-chain fields, token_address, name, symbol, contract_type, plus block_number and deployer_address, are fully sourced.

Migrating from the REST API

This recipe replaces the collection-metadata side of the NFT REST endpoints. 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/metadata

A collection’s identity. Replaced directly by this recipe’s nft_collection_metadata table, name, symbol, and contract_type land from the contract’s deploy, keyed by token_address. Optionally add NFT Trades for last_sale. The editorial layer (logo, banner, category, description, social links) and floor price are off-chain, bring your own collection directory and floor source, or drop them. The lookup (Postgres, adapt to your destination):
Gotchas
  • name/symbol can be empty for some ERC-1155 collections (no on-chain name), expected, not an error.
  • Backfill from the collection’s deploy block: metadata is recorded at deployment, so the deploy block must be in your indexed window. See History & backfill.
  • contract_type may be the NFT fallback when a contract advertises neither EIP-165 interface; the REST endpoint made a similar best-effort call.

POST /nft/getMultipleNFTs

Batch NFT lookup by (token_address, token_id). The onchain half, ownership, mint provenance, contract type, collection identity, realized sales, is replaced by a composition of recipes, with no 25-NFT batch cap.
This endpoint needs multiple recipes, and it is mostly a metadata product. Run NFT Owners by Contract (current owner per token_id) plus this recipe (collection name / symbol), NFT Transfers (mint provenance), and NFT Trades (last_sale) against the same database. The resolved token JSON, media, rarity, and floor/listing prices are built off-chain from each token’s tokenURI and marketplace data, pair the recipes with your own metadata resolver and pricing sources, or drop those fields. If resolved metadata is the entire job, keep a metadata indexer for that layer; the recipes supply the ownership and provenance to pair with it.
The batch lookup (Postgres, adapt to your destination):
Gotchas
  • Backfill each collection from its deploy block: a token’s current owner is the cumulative result of every transfer, so a late start returns stale or missing owners.
  • ERC-1155 token_ids can have many owners, you get (owner, amount) rows rather than a single owner_of.
  • token_id is a uint256 stored as text, compare as strings, never as numbers.
  • No batch cap and no per-call limits, the 25-NFT ceiling disappears; batch size is just the length of your IN list.

NFT Owners by Contract

Current holders of a collection, pair with this metadata for a complete collection view.

NFT Marketplace

The use case this collection metadata powers.