Skip to main content

Question it answers

“Give me the USD / native mark price history for token 0x…, newest first, and the freshest mark right now so I can value a position that hasn’t traded recently.”
Token Prices is the valuation join target across Data Feeds. Any “what is this worth in USD” question (a transfer’s value, a portfolio’s worth, a trade’s notional) joins a token’s mark at a given block from this feed.

What you get

The recipe lands one row per in-block price update: Moralis’ continuous mark for each (token, pair, protocol) at the block it changed. A token traded across several pairs or protocols produces several marks per block; the table keeps them all, keyed for by-token lookups. On ClickHouse, a companion latest_token_price_dict dictionary keeps the freshest mark per (chain_id, token_address) for O(1) carry-forward valuation of tokens that haven’t updated recently.

Source

The transform reads one per-block array, tokenPriceUpdates, and lands one row per entry. Each entry is Moralis’ mark for a (token, pair, protocol) at that block. There is no read-time join: every mark is already a row keyed by token.

Destination

ClickHouse uses the collapsing log-table pattern (see the recipes overview) so chain reorganizations self-correct. The fact table’s sort key is token-first, so a token’s full price history is a contiguous range read; the latest_token_price_dict dictionary holds the carry-forward mark for quiet positions.

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). usd_price and native_price are carried as text to preserve full precision across many orders of magnitude; cast at read time.
The sign column drives reorg collapsing: read with FINAL or sum(sign). A single-node setup can use CollapsingMergeTree(sign) without the replication path. Inside the dictionary the WHERE sign = 1 is safe (not the bare-WHERE anti-pattern) because FINAL has already collapsed the ±1 reorg pairs.
MySQL is the same shape with DECIMAL(38,18) for the price columns. position is the block-level cursor used during backfill. The explicit NUMERIC(38,18) precision keeps room for marks that span many orders of magnitude.

Example reads

A token’s price history, newest first (ClickHouse):
The latest carry-forward mark via the dictionary (O(1), refreshes every 30–60s):
Hourly close per token (last mark in each hour bucket, sign-aware):

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 price updates share one block), so the array-expanded 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, the same logIndex can be assigned to multiple events in one instruction, so the event identity is widened with pairAddress (or protocol) to keep marks distinct; the price feed it produces is identical in shape.

Fidelity and valuation notes

  • Precision. usd_price and native_price are carried as strings end-to-end to preserve full precision; cast at read time (toDecimal* on ClickHouse, the NUMERIC / DECIMAL columns on Postgres / MySQL).
  • Multiple marks per block. A token priced on several pairs/protocols lands one row per (pair, protocol) per block. Pick a venue (filter pair_address / protocol) or aggregate (argMax) depending on whether you want a specific venue’s mark or a representative one.
  • Carry-forward staleness. The latest_token_price_dict refreshes every 30–60s, so a quiet token’s mark is at most ~a minute stale without needing a fresh price event, but it does not interpolate between updates.

Migrating from the REST API

This recipe replaces both REST price endpoints. Their responses were not 1:1 rows in one table: prices, token details, and pair liquidity came bundled in a single payload, but they live in different parts of the dataset here.
Rebuilding the full REST response takes a combination of recipes: this one for the marks, plus Token Metadata for tokenName / tokenSymbol / tokenDecimals and Pair Reserves if you need pairTotalLiquidityUsd (reserves × price). Join them at read time on token_address / pair_address.
Two workflow changes to plan for:
  • USD prices are calculated from DEX trades: the same trades the REST endpoints priced from, so marks track closely, but compare with a small tolerance rather than expecting identical figures.
  • You pick the reference pair. The REST endpoints chose one pair (typically deepest liquidity) as “the price”; this table keeps every (pair, protocol) mark, so you define the rule: filter by pair_address, or argMax for the newest overall.
Both endpoints returned the same shape, so one field mapping covers them:

GET /erc20/:address/price: single token price

Replaced by the newest mark for one token in this recipe’s table, joined to Token Metadata for the token details the endpoint bundled in. On Postgres:
  • On ClickHouse, skip the scan for “price now”: latest_token_price_dict is the O(1) carry-forward lookup (see Example reads).
  • For “price now” you only need the feed running recently; backfill only if you want historical prices (the old to_block behavior) or a 24h-change window. See History & backfill.
  • A token that hasn’t traded recently carries an older mark; the dictionary carries it forward but does not interpolate.

POST /erc20/prices: batch token prices

Replaced by the same newest-mark-per-token read over a set of tokens: one query, no 100-token batch cap. On ClickHouse:
  • The REST 24h-change fields (24hrPercentChange, usdPrice24hr*) aren’t stored; derive them by comparing each token’s latest mark against its mark ~24 hours earlier from this same table.
  • Pin the reference pair per token (filter pair_address or order by your liquidity column) before argMax, or the “price” can flip between venues call to call.
  • Tokens with no recent DEX activity return their last mark, not null; check price_last_changed_at_block if staleness matters.

Token Transfers

The per-token transfer ledger you value against these marks.

Portfolio Tracking

Token Prices is the valuation join target behind portfolio worth.