Skip to main content

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-block tradeTransactions 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.
The 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.
MySQL is the same shape with 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):
Sale history of a single token (collection + token_id):
All trades involving a wallet (as seller or buyer), bloom-pruned:

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 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 same tradeTransactions 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. price is the raw amount in price_token_address units (native ETH when price_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 marketplace field is frequently empty/unreliable on-chain and is landed as-is ('' when absent). marketplace_address is 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.

NFT Transfers

Every NFT movement (mints, transfers, burns), the untraded counterpart to this trade log.

NFT Marketplace

The use case this trade feed powers.