Question it answers
“Give me every DEX trade that touched token 0x…, newest first, with the amount on each side, the USD notional, the protocol, and whether it was a buy or a sell.”A single read returns what the public Moralis Swaps endpoints serve by token, stored pre-shaped and indexed by token in your own database. Both direct pool fills and aggregator-routed trades are captured, so you see every fill of the token once.
What you get
The recipe lands one row per (trade, token side), each trade is unpivoted so it appears under both of its tokens, and a by-token lookup is a prefix scan. Key columns of the fact table:Source
The transform reads two per-block trade arrays and unions them into the trade feed:tokenSwaps (direct pool fills) · aggregateTokenSwaps (aggregator-routed trades)
The two branches are disjoint by construction, pool fills are keyed off the pair address, aggregator routes off the aggregator address, so together they give every fill exactly once. USD values are computed inline from the same block’s tokenPriceUpdates (the chronologically-last update wins on duplicate keys), so no separate price join is needed at read time.
Destination
ClickHouse uses the collapsing log-table pattern (see the recipes overview) so chain reorganizations self-correct. The fact table is token-first, so all trades for a token, newest first, are a contiguous range read. Postgres/MySQL keep a flat one-row-per-trade
swaps table and reach a token via either side’s index.
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). Rawuint256 amounts and fees are stored as text in ClickHouse and as NUMERIC(76, 0) in Postgres (explicit precision so large raw balances never overflow); USD columns are nullable wide decimals.
ClickHouse, fact_swaps_by_token
ClickHouse, fact_swaps_by_token
leg = 'token0' and 'token1') so it’s found under both tokens. leg is part of the ORDER BY so the two sides never collapse into each other, while the +1/−1 reorg pair for one side shares an identical key and collapses cleanly. Read with FINAL or sum(sign), never a bare WHERE sign = 1. A single-node setup can use CollapsingMergeTree(sign) without the replication path.Postgres, swaps
Postgres, swaps
DECIMAL(38,18) for the USD columns. The Postgres/MySQL table is one row per trade (not unpivoted), so a by-token read filters either token0_address or token1_address. position is the block-level cursor used during backfill.Example reads
All trades touching a token, newest first (ClickHouse):FINAL):
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 cursor, but
position is block-level (many trades per block), so the array-expanded trade 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.Multichain
The recipe is chain-parametrized, point it at any supported EVM chain or Solana. On Solana, multiple events in one instruction can share alogIndex, so the pool-fill event identity is widened (with the token addresses and amount) to keep rows distinct; the by-token feed it produces is identical in shape.
USD valuation and fidelity gaps
notional_usdis the bought-leg (token1) amount scaled by its decimals × the in-block price (token1Amount / 10^token1Decimals × price), so it’s a true dollar value when the token had an in-block price update.price_usd,notional_usd,fee_usdareNULLwhen the bought token had no in-block price update, there’s no off-block price backfill at write time. Join the Token Prices sync for fuller coverage.fee_*is populated for pool fills only; bps-style fees (V3/V4/CL) are converted to an absolute amount, and already-absolute fees pass through. Aggregator-routed rows leave feesNULL.side(buy/sell) is derived from the lower-priority (base) token’s pool-balance delta on the trade.
Migrating from the REST API
This recipe is the trade-level foundation for the token-analytics REST endpoint. That endpoint returns a pre-aggregated dashboard, so there is no 1:1 replacement table, you land the token’s trades with this recipe and compute the rolling windows yourself, joining sibling recipes for price, liquidity, and FDV. Every figure is calculated (aggregated from real trades, prices, reserves, and supply, very close, compare with a tolerance); there are no off-chain fields to add.GET /tokens/:tokenAddress/analytics
Buy/sell volume, buyers/sellers, trade counts, unique wallets, price change, liquidity, and FDV per rolling window (5m / 1h / 6h / 24h). On Data Feeds those windows become queries you own, and so does any other window or metric (buy/sell ratio, VWAP, …).
The trade-side aggregation for one window (Postgres, run per window: 300 / 3600 / 21600 / 86400 seconds):
usdPrice is the latest price update, pricePercentChange compares it to the price a window ago, totalLiquidityUsd sums the token’s pool reserves valued at price, and totalFullyDilutedValuation is total_supply × price.
Gotchas
- Backfill enough history to cover your longest window, a day or more for the 24h figures, plus price history for the price change. See History & backfill.
- Materialize per token and window on a schedule if you serve many tokens, don’t recompute per request.
- Thin tokens have sparse windows, and trades whose bought leg had no in-block price carry
NULLnotional and drop out of USD sums. - On ClickHouse, run the same aggregation over
fact_swaps_by_tokenwith a prefix scan ontoken_address(the unpivot means a by-token filter already sees each trade once), and make it reorg-safe withFINALor sign-aware sums, never a bareWHERE sign = 1.
Related
Swaps by Pair
The sibling, every trade keyed by pool/pair address.
Token Analytics
The use case this by-token trade feed powers.

