Question it answers
“Give me every NFT marketplace trade: for collection 0x…, for a single (collection, token_id), or for wallet 0x… as seller or buyer.”
A single sync serves all three access paths. It mirrors the on-chain subset of Moralis GET /nft/{address}/trades (by collection), GET /nft/{address}/{token_id}/trades (single token), and GET /wallets/{address}/nfts/trades (by wallet). It is an on-chain trade log with no off-chain enrichment: no USD price, no reliable marketplace human-name, no address labels.
What you get
One row per traded NFT: a bundle/batch fill that moves several NFTs lands one row per(trade, token_id), each carrying the bundle’s per-item average price:
To collapse a bundle back to one logical trade,
GROUP BY (tx_hash, log_index) and groupArray(token_id).
Source
The recipe consumes the per-blocktradeTransactions array, which is fully flattened: each element is one item (an NFT transfer, a token transfer, or an internal tx) for one participant in one trade. A single sale is spread across many rows. The transform re-joins those parts into one trade: it identifies the buyer (a participant who received an NFT and paid), reads the seller and collection off the received-NFT row, derives the price from the buyer’s native or token payment, and emits one row per received NFT.
Destination
ClickHouse uses the collapsing log-table pattern (see the recipes overview): every trade row in a block shares the block’s hash, so a per-block reorg corrects all of that block’s trades together. The fact table’s sort key is collection-first, so by-collection is a prefix scan and by-
(collection, token_id) is a tighter prefix; by-wallet is served by the bloom-filter indexes. Read canonical state with FINAL or a sign-aware aggregate, never a bare WHERE sign = 1.
Full schema
Below is the complete read table this recipe produces. It’s a starting point: keep the columns you need and drop the rest (see Schema & flexibility).token_id and price are raw uint256 values that exceed numeric precision, so they’re stored as text / wide decimals.
ClickHouse, fact_nft_trades
ClickHouse, fact_nft_trades
sign column drives reorg collapsing: read with FINAL or sum(sign). A single-node setup can use CollapsingMergeTree(sign) without the replication path. The seller/buyer bloom-filter indexes ship enabled because by-wallet is a first-class access path here.Postgres, nft_trades
Postgres, nft_trades
DECIMAL(76,0) for amount / price and the equivalent keys. position is the block-level cursor used during backfill.Example reads
All trades of an NFT collection, newest first (ClickHouse):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 needs a single-column
UNIQUE on the position column, but position is block-level; many trades share one block. Array-expanded trade rows can only carry a composite unique, so run realtime/hybrid on ClickHouse (its log table corrects reorgs per-block). The Postgres / MySQL configs are intended for historical backfill.Multichain
The recipe is chain-parametrized: point it at any supported EVM chain or Solana. Solana marketplace trades (Magic Eden, Tensor, and others) populate the sametradeTransactions shape. Solana assigns the same log_index to multiple events in one instruction, so the trade’s identity is widened with (seller, buyer, token_address, token_id) to keep rows distinct; the trade log it produces is identical in shape.
Fidelity gaps
The recipe lands the columns the trade array carries. Fields a Moralis NFT trades endpoint surfaces that have no reliable on-chain source here are omitted:- USD price.
priceis the raw amount inprice_token_addressunits (native ETH whenprice_token_address = ''). Converting to USD needs a same-block Token Prices join or an external price feed, which is out of scope for this on-chain trade log. - Marketplace human name. The
marketplacefield is frequently empty/unreliable on-chain and is landed as-is (''when absent).marketplace_addressis always present; map it to a human name with an off-chain marketplace directory. - Address labels (
seller_address_label,buyer_address_label): off-chain entity tags, not on-chain data. - Collection metadata (
token_name,token_symbol): from the NFT contract or a metadata indexer, not the trade event (see NFT Collection Metadata). - Smallest-total price-token quirk. When a buyer pays in tokens across multiple token addresses, the price token is chosen as the smallest summed total. This is preserved for parity with the production transformer; it is a likely upstream quirk. Native-paid trades (the common case) are unaffected.
Related
NFT Transfers
Every NFT movement (mints, transfers, burns), the untraded counterpart to this trade log.
NFT Marketplace
The use case this trade feed powers.

