The star schema

The star schema for chainstar. Raw blockchain data to clean dimensional tables in your own warehouse, from one command.

Keys are readable strings that start with the chain, for example ethereum:0xdac17f958d2ee523a2206206994597c13d831ec7 or bitcoin:native, so rows from different chains never collide. Addresses and hashes are as each chain writes them: lowercase 0x hex on Ethereum and Base, Bitcoin addresses and txids as the node returns them, base58 public keys and signatures on Solana. Times are UTC.

The shared columns mean the same on every chain: chain; block_number (the Bitcoin block height, the Solana slot); block_timestamp and date_day; transaction_hash (the Bitcoin txid, the Solana transaction's first signature); and the _id keys. A column that does not apply to a chain is NULL there, as noted below.

fct_transfers

One row per value movement:

Column Notes
transfer_id chain:transaction_hash:transfer_position (on EVM chains the same as chain:transaction_hash:log_index)
chain, block_number, block_timestamp, date_day date_day joins to dim_dates
transaction_hash, transaction_index
transfer_position log index (EVM), output index (Bitcoin), instruction position (Solana: 3 is the fourth top-level instruction, 3.1 its second inner instruction)
log_index EVM only
token_id joins to dim_tokens, and on EVM chains to dim_contracts
from_wallet_id, to_wallet_id join to dim_wallets; on EVM the zero address means a mint (sender) or burn (recipient); NULL on Bitcoin where there is no single sender or no address
token_address ERC-20 contract, SPL mint, or native for BTC and SOL
from_address, to_address
input_address_count Bitcoin only: distinct addresses among the inputs
from_token_account, to_token_account Solana SPL only
script_type Bitcoin only: the output script type as the node names it (witness_v0_keyhash, nulldata, ...)
amount_raw the exact value in the asset's smallest unit (satoshis, lamports, token base units), as a whole-number string
amount amount_raw / 10^decimals as a floating-point number (about 15 significant digits); NULL when the token's decimals are unknown
amount_usd amount × usd_price; NULL when the token has no peg, high or medium price for the block's UTC hour (see Swaps and USD prices)
usd_price, price_method, price_confidence, price_bucket_start the hourly price used, from fct_token_prices, and the start of its hour

amount_raw is text because ERC-20 values are 256-bit integers and SPL amounts 64-bit unsigned, larger than any numeric type DuckDB and Postgres share. Cast it yourself when you need exact arithmetic, for example amount_raw::numeric in Postgres.

Why Solana transfers come from instructions rather than from token balance changes: balance changes (preTokenBalances / postTokenBalances) net out every movement in a transaction and cannot say who paid whom. Checked against real blocks, the parsed instructions account for every token balance change except wrapped-SOL wrapping and unwrapping (syncNative, closeAccount) and the fee that Token-2022 mints with the transfer-fee extension withhold from the recipient: for those mints amount_raw is the amount sent, and the recipient receives less. Token accounts that are created and closed within one transaction appear in no balance list; their mint and owner are read from the initializeAccount instruction that created them.

fct_fees

One row per fee-paying transaction, with what its payer paid.

Column Notes
fee_id chain:transaction_hash
chain, block_number, block_timestamp, date_day
transaction_hash, transaction_index
payer_wallet_id who paid; joins to dim_wallets
from_address the payer's address
to_address EVM only; NULL for contract creation
transaction_type EVM only: 0 legacy, 1 access list, 2 EIP-1559, 3 blob, 4 set-code, 126 OP-stack deposit (Base)
success always true on Bitcoin
is_vote Solana only
gas_limit, gas_used, effective_gas_price_wei, base_fee_per_gas_wei EVM only
execution_fee_wei, execution_fee_native EVM only, kept from earlier releases; equal to fee_raw and fee_native
fee_raw the fee in the chain's smallest unit (wei, satoshis, lamports), exact
native_symbol, fee_native the fee in ETH, BTC or SOL, as a floating-point number
fee_usd fee_native at the native asset's hourly USD price; NULL when unpriced
native_usd_price, price_method, price_confidence, price_bucket_start the hourly price used, and the start of its hour

fct_swaps

One row per DEX swap (see Swaps and USD prices for the venues): on Ethereum and Base a decoded Swap event, on Solana what one signer gave and got in a transaction that called a known DEX program.

Column Notes
swap_id chain:transaction_hash:swap_position
chain, block_number, block_timestamp, date_day
transaction_hash, transaction_index
swap_position, log_index the log index (EVM); 0 on Solana, one swap per transaction
venue EVM: the pool's factory's venue (uniswap_v2, uniswap_v3, sushiswap_v2, sushiswap_v3, aerodrome_v2, aerodrome_slipstream) or unknown; Solana: the known programs called, joined with +
swap_method the event layout (uniswap_v2, uniswap_v3, solidly_v2), or balance_changes on Solana
pool_id, pool_address, pool_verified EVM only; joins to dim_pools. Only verified pools are priced and valued
trader EVM: the transaction's sender (NULL in a wallet history); Solana: the signer
sender, recipient EVM only: the event's fields, usually a router and the receiver
token_in_id, token_out_id, token_in_address, token_out_address what the trader paid and received; native for SOL and wrapped SOL on Solana
amount_in_raw, amount_out_raw exact amounts in the tokens' smallest units, as text
amount_in, amount_out divided by 10^decimals; NULL when decimals are unknown
usd_value, usd_value_side the value at the hourly price of one side (in or out): a stablecoin side first, then the native asset, then token_in
price_method, price_confidence, price_bucket_start of the price used

On EVM, trader is whoever sent the transaction, often a bot or a relayer, and one transaction through a router can hold several swaps (one per pool crossed).

fct_token_prices

One row per chain, asset, granularity (hour or day) and UTC bucket; see How prices are derived.

Column Notes
price_id chain:token_address:granularity:bucket_start_unix
chain, token_address, token_id token_address is native for ETH, BTC and SOL, which WETH and wrapped SOL are priced as
granularity, bucket_start, bucket_start_unix, date_day the UTC hour or day
usd_price what chainstar values things at: 1 for stablecoins; the observed price at high or medium confidence; otherwise NULL
observed_price the lower median of the observations kept (for stablecoins: from swaps against other stablecoins)
price_low, price_high lowest and highest observation kept, the spread within the bucket
price_method usd_stablecoin_peg, swaps_vs_stablecoins, swaps_vs_native, swaps_vs_stablecoins_and_native or cross_chain_wrapped_btc
confidence peg, high, medium, low or none
observations, rejected_observations swaps kept, and those rejected as more than 10% from the median
volume_usd USD volume of the swaps kept, rounded to cents
sources distinct pools (EVM) or traders (Solana) among them
last_block_number the chain's newest block in the bucket

dim_pools

Every Ethereum and Base pool that emitted a swap event, as its contract and factory described it when read (once, at the chain head).

pool_id, chain, pool_address, venue, event_family, factory_address, factory_confirms, is_verified (the factory confirms the pool and is a known venue), token0_address, token1_address, token0_id, token1_id, fee (Uniswap v3-style: hundredths of a basis point, so 500 is 0.05%), tick_spacing, is_stable (Aerodrome), metadata_block_number, first_seen_block, last_seen_block, swap_events

dim_wallets

Every address that sent or received a transfer, or paid for a transaction.

wallet_id, chain, address, is_zero_address, first_seen_block, first_seen_at, last_seen_block, last_seen_at, transfers_out, transfers_in, transactions_sent

"Wallet" here means any address. Some of them are contracts or programs; this release does not tell them apart. On Bitcoin, transfers_out counts inputs spent from the address and transactions_sent the transactions it helped fund, since an output does not always have a single sender. is_zero_address applies to EVM chains only.

dim_tokens

Every asset moved in the loaded data.

token_id, chain, token_address, token_standard, name, symbol, decimals, metadata_block_number, has_metadata, first_seen_block, last_seen_block (the first and last block in the loaded data where the asset moved)

dim_contracts

Contracts chainstar can identify. In this release that is only the Ethereum and Base token contracts that emitted Transfer events, so it has one row per erc20 row of dim_tokens. Bitcoin has no contracts, and Solana programs are not extracted.

contract_id, chain, contract_address, contract_type (always erc20 for now), first_seen_block, last_seen_block, transfer_events

dim_dates

One row per UTC day from the earliest to the latest loaded block.

date_day, year, quarter, month, day_of_month, iso_day_of_week (1 = Monday), iso_week, is_weekend

Tests

chainstar run finishes with dbt build, which runs these checks and fails the run if any fail: unique and not-null keys on every table, every fact row joining to its date, token and wallet dimensions, every EVM swap joining to dim_pools, every ERC-20 token joining to dim_contracts, accepted values for chains, granularities, methods and confidences, and every USD price and value positive with each price between its lowest and highest observation. Where a column does not apply to a chain (a Bitcoin output with no single sender, for example), its not-null test skips that chain. Two checks warn instead of failing, because the chain can be right and they still fire: a stablecoin whose swaps against other stablecoins put it more than 2% from 1 USD in an hour (at least 3 swaps), and ETH priced more than 2% apart on Ethereum and Base in the same hour (both high confidence).