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: 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:
| Column | Description |
|---|---|
token_address | NFT collection address |
token_id | Token ID traded (uint256, stored as text) |
seller_address | The NFT sender on the trade |
buyer_address | The NFT recipient on the trade |
amount | ERC-1155 quantity (1 for ERC-721) |
price | Raw price in price_token_address units; for bundles, the per-item average |
price_token_address | Payment token; '' means native (ETH) |
marketplace_address | The marketplace contract — the canonical join key for naming |
marketplace | Marketplace human name when decoded ('' otherwise) |
tx_hash, block_number, log_index, transaction_index | On-chain ordering and provenance |
event_ts | Block time |
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
| Destination | Table | Read pattern |
|---|---|---|
| ClickHouse (first-class) | fact_nft_trades | Prefix scan on (chain_id, token_address, token_id, block_number); bloom_filter skip-indexes on seller_address / buyer_address; read with FINAL |
| Postgres | nft_trades | Indexes on (token_address, block_number DESC), (token_address, token_id, block_number DESC), and seller/buyer |
| MySQL | nft_trades | Same indexes (without DESC on the key) |
(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 — 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.

