Question it answers
“Give me every DEX trade made by wallet 0x…, newest first — direct pool fills and aggregator-routed trades alike, each with its USD notional, side, and protocol.”A single prefix scan returns what the public Moralis Swaps endpoints serve, pre-shaped and keyed by trader so a wallet’s trade history is a contiguous range read.
What you get
The recipe lands one row per trade — a trade belongs to exactly one trader (fromAddress), so there’s no per-side fan-out. Both direct pool fills and aggregator-routed trades land in the same table:
| Column | Description |
|---|---|
trader_address | The wallet that made the trade |
event_ts, block_number | When the trade happened |
tx_hash | Transaction that produced the trade |
pool_address | The pair (pool fill) or aggregator (aggregator route) |
token0_address, token1_address | The two legs of the trade |
amount0, amount1 | Raw uint256 amounts moved on each leg |
side | buy / sell — decided by the base token’s pool-balance delta |
source_kind | pool_fill (direct pool) or aggregator (router) |
price_usd | Bought-leg (token1) USD price at block time |
notional_usd | Trade value: token1Amount / 10^token1Decimals × price_usd |
protocol | The DEX protocol of the fill |
fee_amount, fee_token, fee_usd | Pool-fill fees (bps fees converted to absolute), NULL for aggregator rows |
Source
The transform reads two per-block arrays and unions them into the trade feed, so every fill is captured exactly once:tokenSwaps (direct pool fills, source_kind = 'pool_fill') · aggregateTokenSwaps (aggregator routes, source_kind = 'aggregator')
The two branches are disjoint by construction. USD enrichment is computed inline from the same block’s tokenPriceUpdates — a per-block price map is built once and the bought leg is priced O(log N) per trade, with no separate price join at read time.
Destination
| Destination | Table | Read pattern |
|---|---|---|
| ClickHouse (first-class) | fact_swaps_by_wallet | Prefix scan on (chain_id, trader_address, event_ts); read with FINAL or sum(sign) |
| Postgres | swaps | Index on (trader_address, block_number DESC) |
| MySQL | swaps | Index on (trader_address, block_number) |
+1/−1 pair shares an identical key and collapses cleanly. The fact table’s sort key is trader-first, so a wallet’s trade history is a contiguous range read.
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 are stored as text in ClickHouse (they exceed numeric precision) and as NUMERIC(76, 0) in Postgres; USD columns are wide decimals so a low-decimals token × price never overflows.
ClickHouse — fact_swaps_by_wallet
ClickHouse — fact_swaps_by_wallet
sign column drives reorg collapsing — read with FINAL or sum(sign). A single-node setup can use CollapsingMergeTree(sign) without the replication path.Postgres — swaps
Postgres — swaps
DECIMAL(38,18) for the USD columns and DECIMAL(76,0) for raw amounts. position is the block-level cursor used during backfill.Example reads
All trades made by a wallet, 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 block-level position, but
position is block-level (many trades share one block), so 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, the samelogIndex can be assigned to multiple events in one instruction, so the pool-fill event identity is widened with (token0_address, token1_address, token0_amount) to keep rows distinct; the trade feed it produces is identical in shape.
Fidelity gaps
notional_usdis single-leg. The trade value is computed from the bought leg (token1) only —token1Amount / 10^token1Decimals × price_usd. If the bought token had no in-block price update,price_usd,notional_usd, andfee_usdareNULL.amount0/amount1are rawuint256. They are not decimal-scaled — divide by10^token_decimals(sourced from a Token Metadata sync) to read human amounts.- Fees are pool-fill only.
fee_*columns are populated for direct pool fills (bps fees converted to an absolute amount); aggregator rows leave themNULL.
Related
Swaps by Token
The same trade feed keyed by token instead of trader.
Token Analytics
The use case this trade feed powers.

