Question it answers
“Show me the full chronological event feed for wallet 0x…, newest first. Each row is one event with its actual payload (counterparty, amounts, token IDs, pair address, USD value), and I can filter by event type.”A single read returns what the public API stitches together client-side from
/wallets/{address}/erc20-transfers, /native-transactions, /nfts/transfers, /swaps, /approvals, and /defi/positions. Storing it pre-stitched, in your own database, is the value.
What you get
The recipe lands one row per wallet-bearing event. A wallet is “involved” if it is a non-emptyfrom/to (or owner/approver) on the event. Six event types share one wide, flat table; per-type columns are populated as relevant:
Each row also carries
direction (sent · received · self · minted · burned), counterparty, tx_hash, block_number, block_timestamp, and log_index.
Source
The transform reads six per-block arrays andUNIONs them into the event feed:
tokenTransfers · nativeTransfers · nftTokenTransfers · tokenSwaps · tokenApprovals · pairLiquidityChanges
USD values are computed inline from the same block’s tokenPriceUpdates, with no separate price join at read time.
Destination
ClickHouse uses the collapsing log-table pattern (see the recipes overview) so chain reorganizations self-correct. The fact table’s sort key is wallet-first, so a wallet’s feed, optionally filtered by
event_type, is a contiguous range read.
Full schema
Below is the complete read table this recipe produces. It’s the full shape: every event type’s columns in one wide row. This is a starting point: keep the columns and event types you need and drop the rest (see Schema & flexibility). Rawuint256 amounts and token_id are stored as text (they exceed numeric precision); USD columns are wide decimals so a low-decimals token × price never overflows.
ClickHouse, fact_wallet_history_full
ClickHouse, fact_wallet_history_full
sign column drives reorg collapsing; read with FINAL or sum(sign). A single-node setup can use CollapsingMergeTree(sign) without the replication path.Postgres, wallet_history_full
Postgres, wallet_history_full
DECIMAL(65,18) for the USD columns. position is the block-level cursor used during backfill.Run it
Everything runs in your infrastructure: the sink writes into a database you own and you query it with plain SQL, no API and no per-call limits. Prerequisites- Docker and Docker Compose.
- A Data Feeds key, a
cm_live_…key withcontinuum:readscope (this is not your Web3 API key).
Sign up for a Data Feeds key
Log in to the Moralis admin panel to create your account and generate a
cm_live_… key.walletHistoryFull in the Moralis Admin Panel, pick your chain and destination database, then run it with Docker. The quickstart walks through it end to end. Choose the backfill window (MORALIS_HISTORICAL_FROM_BLOCK, e.g. tip-1000) as noted below.
Size the window deliberately. This recipe lands one row per event across six event types, so history adds up fast,
tip-1000 on Ethereum is roughly 3M rows. The 9-branch transform is memory-hungry: on chain-heavy windows raise the sink’s ClickHouse memory ceiling (CLICKHOUSE_MAX_MEMORY_USAGE, default 4 GiB). See Modes for backfill vs realtime.Example reads
A wallet’s full feed, newest first (ClickHouse):event_type filter compresses the scan further):
FINAL):
Other ways to consume this data
The sink is the way to get this pre-shaped table (wallet_history_full), recommended for a queryable history you own. The same underlying data is also available as the raw decoded feed, if you’d rather shape it yourself:
- Your existing message clients: stream it with Kafka / AMQP / SQS.
- REST / Arrow Flight: pull the decoded block stream directly.
- Your warehouse: query it via Iceberg from Snowflake, BigQuery, Spark, or DuckDB.
tokenTransfers, tokenSwaps, …) to assemble yourself, not this pre-stitched wallet table. All of them read the one data lake into your infrastructure. See What are Data Feeds?.
USD valuation and fidelity gaps
- Swaps and LP changes carry per-leg decimals, so their
*_usdcolumns are true dollar values (raw / 10^decimals × price). - Token transfers have no decimals field on the transfer event, so
amount_usdisraw_amount × price(unscaled). Divide by10^token_decimalsto read dollars; fold in a decimals lookup from a Token Metadata sync if you need it pre-scaled. - Native and NFT transfers leave USD
NULL: native pricing needs a separate native-price feed, and NFT pricing is out of scope (use an NFT Trades recipe for trade-priced data). - Approvals leave USD
NULLby design: an unlimited allowance × price is meaningless.
Lightweight variant: transaction pointers
If you only need a wallet’s transaction list (one pointer per(wallet, tx), not the full payload), there’s a slimmer variant that lands just the deduplicated pointers from the transfer arrays. Use the full feed above when you need amounts, counterparties, and USD value; use the pointer variant when you only need “which transactions touched this wallet.”
Modes
Shipped defaults: ClickHousehybrid (backfill → realtime), Postgres / MySQL historical (one-shot backfill). For live/reorg-safe ingestion, use ClickHouse; see the overview.
Multichain
The recipe is chain-parametrized via thechain setting: point it at any supported EVM chain or Solana. On Solana, the event identity is widened to stay unique under Solana’s repeated log indices; the wallet feed it produces is identical in shape.
Powers these use cases
Accounting & Tax
The chronological, USD-valued event feed behind a ledger.
Compliance & AML
Full counterparty and transfer trail per address.
Migrating from the REST API
Coming from the Web3 Data API? This recipe replaces the wallet-activity endpoints below. Data Feeds are not 1:1 replicas of REST responses (you own the table and reshape it with SQL), so each subsection states exactly what maps, what’s calculated for you, and what you add yourself. For the general model see Data Feeds vs. the REST API; for the full endpoint index see the endpoint map.Wallet transaction history
GET /wallets/{address}/history
Replaced by this recipe’s wallet_history_full table: the same activity, flattened to one row per event instead of one object per transaction, with a per-event USD value the old endpoint never had.
Not a 1:1 swap. The old response nests events under each transaction and layers on a decoded/curated layer. You need this recipe for the events, plus Token Metadata for token names/symbols/decimals, plus a
GROUP BY tx_hash reshape if your UI expects the transaction-grouped shape. Transaction-level fields (gas, nonce, receipt) come from the raw block transaction rows; see The block array.
To reproduce the endpoint’s transaction-grouped shape, group events by
tx_hash (Postgres):
- The biggest migration task is the reshape: event-flat → transaction-grouped is a
GROUP BY tx_hash. Skip it entirely if your UI prefers an event feed. - You gain per-event USD the endpoint never provided. Compare with a tolerance, and expect
NULL/0 for unpriced tokens. - The decoded/categorized/labeled layer is off-chain processing, not onchain data; add it yourself or drop it.
- Only need “which transactions touched this wallet”? Use the lightweight pointer variant instead of the full feed.
- No per-call limit: page a wallet’s entire history with plain SQL.
Wallet native transactions
GET /{address}
Replaced by this recipe filtered to event_type = 'native_transfer': the native slice of the same event feed. Direction, date, block, and ordering filters become plain WHERE / ORDER BY clauses.
Not a 1:1 swap for strict parity. The REST endpoint returns every transaction where the wallet is
from or to, including value = 0 contract calls; native value movements alone miss those. For exact parity, and for gas/nonce/receipt fields, read the raw transaction rows from the block array (filter transactionFrom / transactionTo = your wallet) alongside this recipe. See The block array.
Good to know
- No wallet-size cap: the old endpoint can error on very large wallets; your own table has no such limit.
- Strict parity includes
value = 0sends, so use the block-array transaction rows when you need every transaction, not just value movements. - Run the recipe
realtime(ClickHouse recommended) for a live feed of new transactions.
Wallet active chains
GET /wallets/{address}/chains
Replaced by a roll-up you build: run this recipe on each chain you cover, keep a min/max activity summary per wallet per chain, and union the results in your app.
Not a 1:1 swap. Data Feeds are one feed per chain. This endpoint needs this recipe running on every chain you want to report, plus a small cross-chain union in your application layer. A wallet active on a chain you don’t index simply won’t appear; that coverage is yours to set.
Keep the per-chain read cheap with a summary view over the recipe’s table:
- First-seen needs full history; “active” and last-seen do not. Backfill each chain to an early block where you want a correct first transaction; a wallet’s first activity can be genesis-era.
- Every value is exact: no prices, no off-chain data, just onchain presence and the bounding transactions.
- Stream each chain’s feed to keep last-seen live as the wallet transacts.
Wallet stats
GET /wallets/{address}/stats
Replaced by plain COUNTs: three of the five stats come straight from this recipe’s table; the NFT holdings counts need a second recipe.
Not a 1:1 swap. You need this recipe plus NFTs by Wallet.
transactions, token_transfers, and nft_transfers are event_type counts over wallet_history_full; nfts and collections are counts over the wallet-NFT holdings table.- These counts are cumulative over the wallet’s life: accurate totals need the full chain history backfilled (
historical/hybridfrom an early block). See History & backfill. - For
transactions.total, native transfers missvalue = 0contract calls; count the raw block-array transaction rows for strict parity (same caveat as native transactions). - For
nfts, decide whether you count held token IDs or sum ERC-1155 amounts. The endpoint counted token IDs. - Serving many wallets? Materialize a per-wallet stats row and update it as events land.

