Skip to main content

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:
ColumnDescription
trader_addressThe wallet that made the trade
event_ts, block_numberWhen the trade happened
tx_hashTransaction that produced the trade
pool_addressThe pair (pool fill) or aggregator (aggregator route)
token0_address, token1_addressThe two legs of the trade
amount0, amount1Raw uint256 amounts moved on each leg
sidebuy / sell, decided by the base token’s pool-balance delta
source_kindpool_fill (direct pool) or aggregator (router)
price_usdBought-leg (token1) USD price at block time
notional_usdTrade value: token1Amount / 10^token1Decimals × price_usd
protocolThe DEX protocol of the fill
fee_amount, fee_token, fee_usdPool-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

DestinationTableRead pattern
ClickHouse (first-class)fact_swaps_by_walletPrefix scan on (chain_id, trader_address, event_ts); read with FINAL or sum(sign)
PostgresswapsIndex on (trader_address, block_number DESC)
MySQLswapsIndex on (trader_address, block_number)
ClickHouse uses the collapsing log-table pattern (see the recipes overview) so chain reorganizations self-correct, a trade’s +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). Raw uint256 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.
The sign column drives reorg collapsing, read with FINAL or sum(sign). A single-node setup can use CollapsingMergeTree(sign) without the replication path.
MySQL is the same shape with 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):
24h USD volume per wallet (sign-aware, cheaper than FINAL):
The same by-wallet read on Postgres / MySQL:

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 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 same logIndex 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_usd is 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, and fee_usd are NULL.
  • amount0 / amount1 are raw uint256. They are not decimal-scaled, divide by 10^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 them NULL.

Migrating from the REST API

This recipe replaces the legacy wallet-swaps endpoint. It is not a 1:1 replica of the response, you land the trades in your own database and reconstruct the response shape with a query. Fidelity labels: exact = straight from the chain, calculated = derived from real DEX trades (very close, compare with a tolerance), add yourself = off-chain signal not in the feed.

GET /wallets/:address/swaps

A wallet’s DEX trade history with bought/sold legs and USD values. Replaced directly by this recipe, trades are keyed by trader_address, so the legacy response is a single by-wallet read, with Token Metadata joined in for the leg symbols and pairLabel.
/wallets/:address/swapsData FeedsFidelity
transactionHash, transactionIndex, blockNumber, blockTimestampswaps.*exact
transactionType (buy/sell)swaps.sideexact
pairAddress, exchangeNameswaps.pool_address / protocolexact
bought / sold legs (address, amount)swaps.token0/1_address, amount0/1exact
bought/sold.symbol, name, pairLabeltoken_metadata join / deriveexact
usdPrice, usdAmount, totalValueUsd, baseQuotePriceswaps.price_usd / notional_usdcalculated
walletAddressLabel, entity, entityLogo, exchangeLogo, token logosadd yourselfadd yourself
subCategory (accumulation/distribution)classify yourselfadd yourself
The legacy read with keyset pagination (Postgres, adapt to your destination):
Gotchas
  • USD values are calculated from real trades, compare with a tolerance, and they’re NULL when the bought leg had no in-block price update (see Fidelity gaps).
  • Leg symbols and pairLabel need the token_metadata join, and amount0/amount1 land as raw uint256, scale by 10^decimals from the same table.
  • The off-chain extras (walletAddressLabel, entity, logos) come from your own directory or get dropped; subCategory is a classification rule you define over the wallet’s trades.
  • The legacy transactionTypes filter becomes WHERE side = 'buy' / 'sell'; date and block windows are plain SQL.
  • Both direct pool fills and aggregator-routed trades land in the one table, source_kind distinguishes them, matching the old endpoint’s mixed feed.

Swaps by Token

The same trade feed keyed by token instead of trader.

Token Analytics

The use case this trade feed powers.