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 whosetype 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): 0xd9b67a26 → ERC1155, else 0x80ac58cd → ERC721, 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.
ClickHouse, fact_nft_collection_metadata
ClickHouse, fact_nft_collection_metadata
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.Postgres, nft_collection_metadata
Postgres, nft_collection_metadata
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):REFRESH MATERIALIZED VIEW CONCURRENTLY nft_collection_metadata;):
Modes
Shipped defaults: ClickHousehybrid (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 thechain 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-chainblock_number/block_timestampinstead.
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’snft_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):
name/symbolcan 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_typemay be theNFTfallback 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.
The batch lookup (Postgres, adapt to your destination):
- 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 singleowner_of. token_idis auint256stored 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
INlist.
Related
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.

