Average leg-sum per intent
Daily leg-sum divided by daily intents. A rough measure of ticket size; it counts both sides of the swap, so the value a user actually swaps is about half.
Average leg-sum per intent
Rises while intent count falls; cause untested.
Source · BigQuery public NEAR dataset (execution_outcomes, intents.near logs); prices from DefiLlama coins API. · 2026-01-01 → 2026-10-28
$1.7k
Latest (2026-10-28)
$3.2k
Peak (2026-09-17)
$356
Low (2026-01-01)
$1.0k
Mean of shown points
301
Points
How it is computed
- leg_sum_usd / intents from the daily volume query, computed in dashboard/build.py.
Read this before quoting it
- Ratio of two public-only series; a shift of small flows to Confidential Intents would raise it with no change in behaviour.
- Imputed legs are included in the numerator.
The query
Download daily_volume.csvRuns in DuckDB over the local cache of BigQuery logs. Re-run with make metrics or python -m pipeline.metrics daily_volume. Metric queries read the base tables built by 00_setup.sql.
1-- Daily swap volume under two explicit definitions. Failed receipts are excluded.2-- leg_sum_usd : sum of positive token_diff legs across every entry (user AND solver side). This is the3-- definition that reproduces NEAR's reported public volume (see METHODOLOGY).4-- net_swap_usd: leg_sum_usd / 2, the value that actually changed hands once per swap.5-- leg_sum_usd = leg_sum_priced_usd (price table) + leg_sum_imputed_usd (value-conservation imputation, see 00_setup).6-- Positive legs still without a value after both steps are counted in unvalued_pos_legs: leg_sum_usd is a lower bound.7SELECT date,8 coalesce(sum(usd_best) FILTER (WHERE amount_raw > 0 AND NOT failed), 0) AS leg_sum_usd,9 coalesce(sum(usd_best) FILTER (WHERE amount_raw > 0 AND NOT failed), 0) / 2 AS net_swap_usd,10 coalesce(sum(usd) FILTER (WHERE amount_raw > 0 AND NOT failed), 0) AS leg_sum_priced_usd,11 coalesce(sum(usd_imputed) FILTER (WHERE amount_raw > 0 AND NOT failed), 0) AS leg_sum_imputed_usd,12 count(*) FILTER (WHERE amount_raw > 0 AND NOT failed) AS pos_legs,13 count(*) FILTER (WHERE amount_raw > 0 AND NOT failed AND usd IS NULL) AS unpriced_pos_legs,14 count(*) FILTER (WHERE amount_raw > 0 AND NOT failed AND usd_best IS NULL) AS unvalued_pos_legs,15 count(DISTINCT account_id) FILTER (WHERE NOT failed) AS active_accounts,16 count(DISTINCT intent_hash) FILTER (WHERE NOT failed) AS intents,17 count(DISTINCT receipt_id) FILTER (WHERE NOT failed) AS swap_receipts,18 count(DISTINCT intent_hash) FILTER (WHERE failed) AS failed_intents19FROM legs_usd20GROUP BY date ORDER BY date;pipeline/sql/00_setup.sql (base tables: legs, prices, roles)
1-- Base tables. Inputs: raw parquet cache ({RAW}) plus the `assets` and `prices` tables registered by metrics.py2-- from data/reference/. Nothing here is aggregated; every metric file below builds on these.34-- forward-filled daily price per key (calendar x key), so a missing day reuses the last known price5CREATE OR REPLACE TABLE prices_filled AS6WITH cal AS (SELECT CAST(d AS DATE) AS date FROM generate_series(DATE '2024-12-31', DATE '2026-10-30', INTERVAL 1 DAY) t(d)),7 keys AS (SELECT DISTINCT price_key FROM prices)8SELECT k.price_key, c.date,9 last_value(p.price IGNORE NULLS) OVER (PARTITION BY k.price_key ORDER BY c.date) AS price10FROM keys k CROSS JOIN cal c LEFT JOIN prices p ON p.price_key = k.price_key AND p.date = c.date;1112-- one row per (token_diff entry, asset); entry_id identifies one token_diff entry (one account's side of a swap)13CREATE OR REPLACE TABLE legs AS14SELECT entry_id, CAST(ts AS DATE) AS date, ts, failed, intent_hash, account_id, receipt_id,15 e.key AS asset, TRY_CAST(e.value AS DOUBLE) AS amount_raw16FROM (SELECT row_number() OVER () AS entry_id, *,17 unnest(map_entries(CAST(diff::JSON AS MAP(VARCHAR, VARCHAR)))) AS e18 FROM read_parquet('{RAW}/swaps/*.parquet'));1920-- usd: priced from the price table. usd_imputed: for an entry with exactly ONE unpriced asset, the value implied21-- by value conservation (minus the sum of the priced legs of the same entry), only if its sign matches the leg.22-- usd_best = usd, else usd_imputed. Imputed values are always reported separately, never silently mixed in.23CREATE OR REPLACE TABLE legs_usd AS24WITH base AS (25 SELECT l.*, a.symbol, a.origin_chain,26 l.amount_raw / pow(10, a.decimals) AS qty,27 CASE WHEN a.price_key = 'USD' THEN 1.0 ELSE p.price END AS price,28 l.amount_raw / pow(10, a.decimals) * CASE WHEN a.price_key = 'USD' THEN 1.0 ELSE p.price END AS usd29 FROM legs l30 LEFT JOIN assets a ON a.asset = l.asset31 LEFT JOIN prices_filled p ON p.price_key = a.price_key AND p.date = l.date),32e AS (33 SELECT *, sum(usd) OVER (PARTITION BY entry_id) AS priced_sum,34 count(*) FILTER (WHERE usd IS NULL) OVER (PARTITION BY entry_id) AS n_unpriced35 FROM base)36SELECT * EXCLUDE (priced_sum, n_unpriced),37 CASE WHEN usd IS NULL AND n_unpriced = 1 AND priced_sum IS NOT NULL38 AND sign(-priced_sum) = sign(amount_raw) THEN -priced_sum END AS usd_imputed,39 coalesce(usd, CASE WHEN usd IS NULL AND n_unpriced = 1 AND priced_sum IS NOT NULL40 AND sign(-priced_sum) = sign(amount_raw) THEN -priced_sum END) AS usd_best41FROM e;4243-- fees_collected per entry, priced the same way44CREATE OR REPLACE TABLE fees_usd AS45SELECT CAST(f.ts AS DATE) AS date, f.failed, f.e.key AS asset,46 TRY_CAST(f.e.value AS DOUBLE) / pow(10, a.decimals) *47 CASE WHEN a.price_key = 'USD' THEN 1.0 ELSE p.price END AS usd48FROM (SELECT ts, failed, unnest(map_entries(CAST(fees::JSON AS MAP(VARCHAR, VARCHAR)))) AS e49 FROM read_parquet('{RAW}/swaps/*.parquet') WHERE fees <> '{}') f50LEFT JOIN assets a ON a.asset = f.e.key51LEFT JOIN prices_filled p ON p.price_key = a.price_key AND p.date = CAST(f.ts AS DATE);5253-- NEP-245 mint/burn (deposits, withdrawals, refunds); token_ids and amounts are parallel arrays54CREATE OR REPLACE TABLE mt_usd AS55SELECT CAST(m.ts AS DATE) AS date, m.failed, m.event, m.memo, m.owner_id, m.asset,56 TRY_CAST(m.amt AS DOUBLE) / pow(10, a.decimals) *57 CASE WHEN a.price_key = 'USD' THEN 1.0 ELSE p.price END AS usd,58 a.origin_chain, a.symbol, a.asset IS NULL AS unknown_asset59FROM (SELECT ts, failed, event, memo, owner_id,60 unnest(from_json(token_ids::JSON, '["VARCHAR"]')) AS asset,61 unnest(from_json(amounts::JSON, '["VARCHAR"]')) AS amt62 FROM read_parquet('{RAW}/mt/*.parquet')) m63LEFT JOIN assets a ON a.asset = m.asset64LEFT JOIN prices_filled p ON p.price_key = a.price_key AND p.date = CAST(m.ts AS DATE)65WHERE m.asset LIKE 'nep141:%' OR m.asset LIKE 'nep245:%';666768-- Swap pairing: a receipt whose successful entries belong to exactly two accounts is one user<->solver swap.69CREATE OR REPLACE TABLE pair_receipts AS70SELECT receipt_id, min(account_id) AS a, max(account_id) AS b71FROM (SELECT DISTINCT receipt_id, account_id FROM legs WHERE NOT failed)72GROUP BY receipt_id HAVING count(*) = 2;7374-- Solver heuristic (documented in METHODOLOGY): an account is a solver if it faced >= 100 distinct counterparties75-- across the window. Users face a handful of solvers; solvers face thousands of users. Not an on-chain label.76CREATE OR REPLACE TABLE account_roles AS77WITH sides AS (SELECT a AS account_id, b AS cp FROM pair_receipts UNION ALL SELECT b, a FROM pair_receipts)78SELECT account_id, count(DISTINCT cp) AS n_counterparties, count(*) AS n_swaps,79 CASE WHEN count(DISTINCT cp) >= 100 THEN 'solver' ELSE 'user' END AS role80FROM sides GROUP BY account_id;