Skip to main content

Question it answers

“Give me every ERC-20 transfer: all transfers of token 0x…, or every transfer wallet 0x… sent or received.”
One flat event log of token transfers, served two ways from a single sync: by-token (every movement of a given token) and by-wallet (every transfer a wallet was on either side of). Each row is one transfer carrying exactly one token and one amount; there’s no USD enrichment, because a transfer event carries no price.

What you get

One row per transfer, from Moralis-indexed, normalized per-block onchain data:

Source

The transform reads a single per-block array, tokenTransfers, and lands one row per transfer. Fields map straight from the source struct (tokenAddress → token_address, fromAddress → from_address, toAddress → to_address, amount → amount, type → transfer_type, initiatedBy → initiated_by). There’s no price join: transfers are unpriced, so there is no amount_usd column. The per-transfer vendor_event_id is widened beyond (tx_hash, log_index) so the id stays unique on Solana, where logIndex is not row-unique within an instruction (see Multichain).

Destination

ClickHouse uses the collapsing log-table pattern (see the recipes overview) so chain reorganizations self-correct: the +1/−1 reorg pair for a row shares an identical key and collapses on merge. The fact table’s sort key is token-first, so all transfers of a token are a contiguous range read; by-wallet reads are accelerated by data-skipping bloom filters rather than a second sort key.

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). amount is stored raw (uint256 token units) because the transfer event carries no decimals; scale by 10^token_decimals at read time.
The sign column drives reorg collapsing: read with FINAL or sum(sign), never a bare WHERE sign = 1. A single-node setup can use CollapsingMergeTree(sign) without the replication path.
MySQL is the same shape with the same indexes. amount uses NUMERIC(76, 0) so large raw uint256 values don’t overflow; position is the block-level cursor used during backfill.

Example reads

All transfers of a token, newest first (ClickHouse):
All transfers involving a wallet on either side (bloom-pruned):
Net amount received by a wallet for one token (sign-aware, cheaper than FINAL):

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 transfers share one block), so array-expanded transfer 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 via the chain setting: point it at any supported EVM chain or Solana. On Solana, multiple events in one instruction can share a logIndex, so the vendor_event_id is widened with (from_address, to_address, token_address, amount) to keep rows distinct; the transfer log it produces is identical in shape.

Fidelity gaps

The recipe lands exactly what the tokenTransfers array carries. Fields a transfers endpoint might surface that have no onchain source in this array are omitted:
  • USD value: a transfer carries no price. Pricing requires joining the same-block price data; that’s out of scope for a plain transfer log (see Token Prices for the price-join pattern).
  • Token metadata (symbol, name, decimals, logo, verified/spam flags): these come from a separate token-metadata sync, not the transfer event. amount is therefore stored raw, not decimal-adjusted.
  • Pre/post balances: the balance surface lives in the balances recipes (Token Balances by Token / by Wallet); this transfer log stays a flat event stream.

Migrating from the REST API

This recipe replaces both ERC-20 transfer REST endpoints from one sync: the same token_transfers table serves the by-token and by-wallet reads.
Not 1:1. The REST responses inlined token_name / token_symbol / token_decimals and a pre-scaled value_decimal; this recipe’s amount is raw uint256. Pair it with Token Metadata and join on token_address to scale amounts and label tokens. token_logo, possible_spam, verified_contract, security_score, and the address *_label / *_entity fields are off-chain signals; add them from your own token/label lists or drop them.
Field mapping (both endpoints share it; calculated = derived at read time, add yourself = off-chain): Neither endpoint returned USD values (a transfer carries no price), so every mapped value is exact from the chain, and the feed adds two fields the API omitted: transfer_type and initiated_by.

GET /erc20/{address}/transfers: transfers by token

Every transfer of one token becomes a plain by-token read of token_transfers, the table’s primary sort, so it’s a contiguous range scan:
  • The endpoint’s from_date / to_date, from_block / to_block, and order params are plain WHERE / ORDER BY clauses; no fixed page size.
  • High-volume tokens have enormous histories: keyset-paginate on (block_number, log_index), not OFFSET.
  • For a token’s complete history, backfill from its deploy block (historical or hybrid mode); realtime alone only captures new transfers.

GET /{address}/erc20/transfers: transfers by wallet

A wallet’s transfers are the rows where it appears on either side; the recipe indexes both from_address and to_address, so it’s one query:
  • In each row the token_address is the token, not the wallet; derive direction by comparing the wallet to from_address / to_address, as above.
  • The endpoint’s contract_addresses filter becomes AND t.token_address = ANY($tokens); block/date windows and order map the same way as by-token.
  • On ClickHouse the by-wallet read is served by the bloom_filter skip indexes on from_address / to_address (see Example reads) rather than a second sort key.

Token Holders

The balance roll-up these transfers feed: all non-zero holders of a token.

Accounting & Tax

Per-asset transfer ledgers for reconciliation, valued via Token Prices.