Swaps and USD prices
Swaps and USD prices for chainstar. Raw blockchain data to clean dimensional tables in your own warehouse, from one command.
chainstar reads DEX swaps on Ethereum, Base and Solana into fct_swaps, derives USD
prices from them into fct_token_prices, and values transfers (amount_usd), fees
(fee_usd) and swaps (usd_value) at those prices. Every price comes from on-chain
swaps: no price service is asked, nothing needs an account, and nothing is assumed
except that the USD stablecoins listed below are worth 1 USD. Where a token has no
reliable price, its USD columns are NULL rather than a guess.
Venues
Ethereum and Base. cryo's logs dataset, filtered to three Swap events by
topic0, one eth_getLogs request per block as for transfers. chainstar decodes them
itself, exactly, because their amounts are 256-bit integers (signed in Uniswap v3):
| Event layout | Signature | topic0 | Source |
|---|---|---|---|
uniswap_v2 |
Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to) |
0xd78ad95f...159d822 |
Uniswap/v2-core IUniswapV2Pair.sol |
uniswap_v3 |
Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick) |
0xc42079f9...fbcca67 |
Uniswap/v3-core IUniswapV3PoolEvents.sol; aerodrome-finance/slipstream ICLPoolEvents.sol (identical) |
solidly_v2 |
Swap(address indexed sender, address indexed to, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out) |
0xb3e27736...efffaf75b |
aerodrome-finance/contracts IPool.sol |
Any contract can emit these events, so for every pool chainstar reads token0(),
token1() and factory() (plus fee() and tickSpacing(), or stable()), then asks
that factory for the pool of the same tokens (getPair, getPool(tokens, fee),
getPool(tokens, tickSpacing) or getPool(tokens, stable)). A pool is verified when
the factory answers with the pool's own address and the factory is one of these:
| Chain | Venue | Factory | Published in |
|---|---|---|---|
| Ethereum | uniswap_v2 |
0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f |
Uniswap v2 deployments page |
| Ethereum | uniswap_v3 |
0x1f98431c8ad98523631ae4a59f267346ea31f984 |
Uniswap v3 Ethereum deployments page |
| Ethereum | sushiswap_v2 |
0xc0aee478e3658e2610c5f7a4a2e1777ce9e4f2ac |
sushi-labs/sushi sushiswap-v2.ts |
| Ethereum | sushiswap_v3 |
0xbaceb8ec6b9355dfc0269c18bac9d6e2bdc29c4f |
sushi-labs/sushi sushiswap-v3.ts |
| Base | uniswap_v2 |
0x8909dc15e40173ff4699343b6eb8132c65e18ec6 |
Uniswap v2 deployments page |
| Base | uniswap_v3 |
0x33128a8fc17869897dce68ed026d694621f6fdfd |
Uniswap v3 Base deployments page |
| Base | sushiswap_v2 |
0x71524b4f93c58fcbf659783284e38825f0622859 |
sushi-labs/sushi sushiswap-v2.ts |
| Base | sushiswap_v3 |
0xc35dadb65012ec5796536bd9864ed8773abc74c4 |
sushi-labs/sushi sushiswap-v3.ts |
| Base | aerodrome_v2 |
0x420dd381b31aef6683db6b902084cb0ffece40da |
aerodrome-finance/contracts README |
| Base | aerodrome_slipstream |
0x5e7bb104d84c7cb9b682aac2f3d509f5f406809a, 0xade65c38cd4849adba595a4323a8c7ddfe89716a, 0xf8f2eb4940cfe7d13603dddd87f123820fc061ef |
aerodrome-finance/slipstream README (three deployments) |
Each factory was also checked on chain (it has code; the Uniswap and SushiSwap v3 ones
enable fee tiers 100, 500, 3000 and 10000), and every topic and function selector is
recomputed from its signature by the tests. Swaps in other pools with the same events
(forks such as BaseSwap, whose factory address could not be found in its own
documentation) are kept with venue unknown and pool_verified false; they are never
used for prices and get no usd_value. Pool metadata is read once per pool at the chain
head and cached in data/<chain>/dex_pools/, like token metadata: reading factories'
PairCreated/PoolCreated logs instead would need every block since each factory was
deployed. A swap whose pool does not answer token0() and token1() is left out of
fct_swaps (it stays in the raw table).
Not covered: Uniswap v4 (one contract for all pools, whose tokens are known only from
its Initialize events), PancakeSwap v3 (its Swap event has two more fields, so a
different topic0), Curve, Balancer, and aggregators' own events (their swaps show up as
the pool swaps they route through).
Solana. A swap through an aggregator crosses several pools and programs; what matters is what the trader gave and got, so chainstar derives one swap per transaction from balance changes. A successful transaction counts when it calls one of these programs (each id from the program's own source or IDL, and checked on chain to be an executable program):
| Venue | Program id | Source |
|---|---|---|
raydium_amm_v4 |
675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 |
raydium-io/raydium-amm |
raydium_clmm |
CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK |
raydium-io/raydium-clmm |
raydium_cpmm |
CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C |
raydium-io/raydium-cp-swap |
orca_whirlpool |
whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc |
orca-so/whirlpools |
meteora_dlmm |
LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo |
MeteoraAg/dlmm-sdk IDL |
jupiter_v6 |
JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 |
jup-ag/jupiter-cpi |
pump_fun |
6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P |
pump-fun/pump-public-docs IDL |
pump_swap |
pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA |
pump-fun/pump-public-docs IDL |
For the first signer whose balances changed like a swap, token changes are summed over
the token accounts it owns (preTokenBalances/postTokenBalances), and SOL and wrapped
SOL count as one asset, native: its lamport change, plus the fee it paid, plus the
lamport changes of its token accounts, which nets out wrapping, unwrapping and the rent of
accounts it opens and closes. Exactly one token down and one up is a swap (SOL that
moved only for fees, tips or rent is ignored); one token against SOL moving the other way
is a swap with SOL; anything else (several tokens each way, an arbitrage that ends where
it started) is left out. Limitations: a SOL amount includes tips and rent paid to
accounts the signer does not own in the same transaction; programs not listed are not
read; a transaction gives at most one swap; Solana swaps have no pool, sender or
recipient, and dim_pools has no Solana rows.
How prices are derived
For each chain, asset and UTC hour (and, separately, UTC day), in fct_token_prices:
- USD stablecoins are 1 USD (
usd_stablecoin_peg), identified by address or mint, never by symbol.observed_priceshows what their swaps against other stablecoins said, and a dbt test warns when it strays more than 2% from 1. - Swaps against a stablecoin give one observation each: the stablecoin amount (its
USD value) divided by the other asset's amount. ETH and SOL are priced this way from
WETH's and wrapped SOL's swaps, as the asset
native. - Swaps against the native asset price other tokens through the native asset's price in the same bucket, when that price is high or medium confidence. Nothing is priced through a third token: a token traded only against other tokens has no price.
- Outliers: observations worth less than 10 USD are dropped (mostly rounding); those more than 10% from the bucket's median are rejected. The price is the median of the rest. Medians here are lower medians (for an even count, the lower middle value), always one of the observed prices, so a price is the same to the last digit on every warehouse and in incremental and full builds.
- Confidence:
highwith at least 5 observations kept, 10,000 USD of volume and 2 sources (pools, or Solana traders);mediumwith at least 3 and 1,000 USD;lowotherwise, whenusd_priceis NULL andobserved_pricestill shows the median. Every chain has a row for its native asset and its stablecoins in each bucket with blocks (nonewhen there were no observations); other tokens only where they were traded. - Bitcoin has no on-chain DEX. Its price is that of wrapped BTC (WBTC and cbBTC) on
Ethereum and Base in the same bucket (
cross_chain_wrapped_btc), at mostmedium, and only when those chains' swaps for that hour are in the same warehouse. Wrapped BTC can trade slightly away from BTC; otherwise BTC values are NULL.
Checked on real data, chainstar quickstart of 20 blocks of Base and Ethereum and 20
Solana slots into one warehouse on 21 October 2026 (22:36 to 22:51 UTC): 360, 470 and
1,285 swaps (separately, each of the 84 swaps in the test fixtures' Ethereum and Base
blocks was checked against its pool's Transfer events, to the unit). ETH came out at
2,489.33 USD on Ethereum (97 observations, 6 pools, high) and 2,489.87 on Base (21, 7
pools, high), 0.02% apart; each WETH/stablecoin Uniswap v3 and Slipstream pool's own price
after its last swap (from sqrtPriceX96) was within 0.14% of the hour's price, and the
medians of pools with two or more swaps spread 0.15% (Ethereum, 4 pools) and 0.03% (Base,
3). SOL came out at 107.37 USD (medium: 3 swaps of at least 10 USD, through Jupiter,
Orca and Meteora, 0.9% apart; a fourth, at 50,319, was rejected). USD values were found
for 52% of Ethereum's transfers, 48% of Base's and 78% of Solana's, and for every fee.
Incremental builds over the same data, in two halves, equalled a full rebuild.
Only verified pools count on Ethereum and Base. The thresholds are dbt variables in
dbt_project.yml (price_min_swap_usd, price_outlier_band, price_high_*,
price_medium_*).
The anchors:
| Chain | USD stablecoins (1 USD) | Priced as the native asset | Prices Bitcoin |
|---|---|---|---|
| Ethereum | USDC 0xa0b86991...3606eb48, USDT 0xdac17f95...831ec7, DAI 0x6b175474...71d0f |
WETH 0xc02aaa39...756cc2 |
WBTC 0x2260fac5...c2c599, cbBTC 0xcbb7c000...eed33bf |
| Base | USDC 0x833589fc...da02913, USDbC 0xd9aaec86...710b6ca, USDT 0xfde4c96c...2699bb2, DAI 0x50c57259...17db0cb |
WETH 0x42000000...00000006 |
cbBTC 0xcbb7c000...eed33bf |
| Solana | USDC EPjFWdd5...TDt1v, USDT Es9vMFrz...enwNYB |
wrapped SOL So111...112 |
(full addresses in the price_assets seed). They come from Circle's USDC addresses
page, Tether's supported-protocols page and the Uniswap Labs default token list; their
decimals were read on chain.
What gets a USD value
fct_transfers.amount_usd,fct_fees.fee_usdandfct_swaps.usd_valueuse the hourly price of the block's UTC hour, when its confidence ispeg,highormedium, and are NULL otherwise. Beside each: the price (usd_price,native_usd_price),price_method,price_confidenceandprice_bucket_start, the hour the price is for, so every value can be checked againstfct_token_prices.- A swap is valued by its stablecoin side if it has one, else its native side, else
token_in. - The
transfersexport andchainstar walletspreadsheets carry the same columns.
Wallet histories. chainstar wallet does not read every swap of the range. For each
token the wallet moved, it asks the Uniswap v2 and v3 factories (and Aerodrome's on Base)
for that token's pools against WETH and USDC, once per token (cached in
data/<chain>/.pricing_pools.json), and fetches only those pools' swaps, with
eth_getLogs filtered by address, which the public endpoints answer for a whole chunk
per request, and the blocks they are in. The lookup asks Uniswap v2, Uniswap v3 (fee
tiers 100, 500, 3000 and 10000) and, on Base, Aerodrome v2 (stable and volatile); it does
not find SushiSwap or Aerodrome Slipstream pools. Tokens without such a pool stay
unpriced, and a wallet's prices rest on fewer swaps than a full run's, so their
confidence is often lower. A Bitcoin wallet's warehouse has
no Ethereum or Base swaps, so its BTC values are NULL; run Bitcoin and Ethereum into one
warehouse to price them.
Prices under sync
fct_token_prices is incremental like the other tables. A bucket's price is computed
from all its swaps, so new blocks recompute every bucket from the start of the UTC day of
the earliest new block, for every chain at once (Bitcoin's price depends on Ethereum's
and Base's swaps). Transfers, fees and swaps are revalued for the hour of their chain's
first new block (Bitcoin's: the earliest of Bitcoin's, Ethereum's and Base's), whose
price may have changed. The tests check that two syncs give exactly the rows of a full
rebuild, and a rollback deletes a chain's swaps and prices with its other facts.
After upgrading from a release without swaps, rebuild once with
chainstar sync --full-refresh (or any chainstar run): the tables have new columns.
Blocks extracted before the upgrade have no swaps until they are extracted again: a
chainstar run over such a range fetches its swaps and reuses the other chunk files.