Skip to main content
This page = whole catalog, one page, for model that write query. Nothing link out. Human? Read Human’s manual instead. Page written in caveman format. Few token, same rule. No article, no auxiliary verb, no hedge, no transition. Identifier, type, number, negation stay exact. Only grammar go. Read once. Then build table name yourself. No lookup. Name tell you grain, keys, time column, value columns. Name never lie.

Build table name

Name = dataset.table. Dataset say what kind, what about. Table say rest. Metrics table = ONE measure. Columns = level ids + timestamp + one value column. Value column name = table name minus period. Table Source = Ladder, projected? Table also has provenance: ladder or projected, the source of that row. metrics_projects.fees_daily → project_id, timestamp, fees. Done. No page needed. Do NOT select column the name not promise. Levels roll up: apps_by_chain → apps → projects. asset_tokens → assets_by_chain → assets. Use biggest level that answer question. Rollup already exist. Cheaper. Already correct.

Rules. Break rule = wrong answer or big bill.

All these seen for real.
  1. Bound time column. ALWAYS. No bound = read whole table. You pay, not us. Facts use block_timestamp. Metrics use timestamp. Reports use period_start. Screener = one row per thing as of now, no time column.
  2. Time bound alone is NOT enough. Not every table is partitioned, and a bound on an unpartitioned column prunes nothing. Every table is clustered, so also filter its cluster key: the entity id for a metrics table, token_address for token facts. Read the keys from INFORMATION_SCHEMA.COLUMNS: is_partitioning_column and clustering_ordinal_position. Filter the leading cluster column first, order matters. Empty result = the object is a VIEW, which reports no keys; take its contract from its page.
  3. Partition column naked on left. date(block_timestamp) >= x = read every partition. Function hide column from pruner. Write block_timestamp >= timestamp('2026-01-01').
  4. Bound BOTH sides of join. Bound not travel across join. Two tables, two bounds.
  5. One token = filter token_address + chain_id. NOT token_id. Same rows, but token_address is the cluster key and chain_id drops whole chains at plan time. token_id is derived, reaches no cluster key, and reads every chain: terabytes against megabytes. Keep token_id for join to dimensions.tokens.
  6. Set maximum_bytes_billed on EVERY job. Over the cap, the query fails before it runs and bills nothing; the error names the bytes it needed. Wrong query costs you an error, not money. bq query --maximum_bytes_billed=<bytes>, or maximumBytesBilled in the job config of any client library. Set it low, raise it when the failure tells you to.
  7. Bytes scanned is the bill, NOT rows returned. BigQuery on-demand charges per byte the query reads, at a rate per TiB, with a 10 MiB floor per table per query. limit changes NOTHING: limit 10 reads the same bytes as no limit. Naming columns DOES cut the bill, because storage is columnar. Never select *.
  8. No recursive CTE. No unbounded self-join. with recursive reads again every step and the bytes compound; a fan-out join multiplies rows before you see them. Size the join with a count first. A question that seems to need recursion wants a smaller question.
  9. Empty ≠ zero. Empty = no figure exist for that measure, that thing, that day. Do NOT coalesce 0 then average or sum across. Do NOT call it decline.
  10. Get unit from dimensions.metrics BEFORE format. Unit = usd, count, ratio, pct. Ratio = fraction: apy 0.05 = 5 percent, off_peg -0.02 = 2 percent under peg. NEVER assume dollar.
  11. Sum only how dimensions.metrics allow.
  • Additive (fees, volume): sum across day, sum across thing. Both fine.
  • Semi-additive (TVL, supply, open interest): sum across thing. NEVER across day. Week = last day of week.
  • Non-additive (APY, active users, share, funding rate): sum NEITHER way. Week active users = count distinct address over week from fact table. Add 7 daily counts = count same person 7 time. Wrong.
  1. Address case matter. EVM stored lowercase: wrap explorer address in lower(). Solana base58, case-sensitive, match verbatim.
  2. amount_raw = STRING. uint256 overflow every BigQuery number type. Cast BIGNUMERIC, divide by pow(10, decimals) from dimensions.tokens.
  3. Financial statements wide. reports_projects.financial_statements_* = one row per project per period, one column per line. Flows summed over period. Balances (treasury, tvl, price, market caps, supplies) = value on last day of period with value: NEVER sum balance across periods. User counts (daily_active_users, token_holders) = average daily count.
  4. Screener column = <measure>_<window>_<aggregation>. screeners.<things> = one row per thing (projects, chains, assets, assets_by_chain, asset_tokens), every window ends as_of. Window 1d, 7d, 30d, 90d, 180d, 365d. avg = mean of window. sum = total of window, flows ONLY: distinct count (daily_active_users, token_holders, *_active_addresses, tradeable_*_count) has NO _sum. change and trend compare two aggregation. change = value on as_of vs value N day before, (now - then) / abs(then); null if either end missing or base 0. trend = N-day avg vs avg of N day before it; null unless prior window has value ALL N day, null if prior avg 0. Null = unknown, NOT zero. _max_latest = value ON as_of, null if none: NOT last value before. Plus _max_ath, _max_atl. Which aggregation a measure get follows its over_time in dimensions.metrics: sum ONLY if over_time = sum; every measure get latest, avg, change, trend, ath, atl. Asset token and asset chain sum add up to the coarser level; holders, users, change, trend NOT. Rank or filter projects? Read screener. Do NOT rebuild windows from metrics tables.
  5. NEVER query table from Not served yet list. Those are spec, not table. Tell user data not available. Do NOT write query that cannot run.

Join keys

Id = lowercase name. Combined id = parts joined with dash. Every table carry at least one. That is why catalog join. Asset = one identity, all chains. Token = one contract, one chain. USDC = one row dimensions.assets, one row per chain dimensions.assets_by_chain, one row per contract dimensions.asset_tokens.

Question → table

Shortest correct path for common question. Prefer metrics table over sum facts table yourself: smaller, and additivity already settled.

All tables

Every table in catalog. Generated from pages, so complete at build time.

Dimensions

dimensions.accounts, dimensions.apps, dimensions.asset_disclosure_fields, dimensions.asset_disclosures, dimensions.asset_token_fields, dimensions.asset_tokens, dimensions.assets, dimensions.assets_by_chain, dimensions.chains, dimensions.contracts, dimensions.dex_pools, dimensions.lending_market_reserves, dimensions.lending_markets, dimensions.metric_instances, dimensions.metrics, dimensions.order_book_markets, dimensions.perp_markets, dimensions.projects, dimensions.reference_assets, dimensions.tokens, dimensions.vaults, dimensions.venues

Facts

Metrics

Reports

Screeners

screeners.asset_tokens, screeners.assets, screeners.assets_by_chain, screeners.chains, screeners.projects

Functions

functions.calculate_historical_eod_asset_token_balances, functions.calculate_historical_eod_token_balances, functions.calculate_latest_asset_token_balances, functions.calculate_latest_token_balances

Not served yet

Name documented before table land. Page = spec. Table NOT exist. Query these = fail.

Access

Catalog = BigQuery share in caller own Google Cloud project. Caller pay compute. No public endpoint. No anonymous access. Missing dataset error = share not set up. Human must arrange. Say so. Do NOT retry.