Intents per day

Distinct intent hashes on successful token_diff events per UTC day. One intent is one signed user instruction; a swap settles when a solver's intent matches it.

Intents per day

Source · BigQuery public NEAR dataset (token_diff events). · 2026-01-01 → 2026-10-28

40.9k
Latest (2026-10-28)
146.4k
Peak (2026-08-20)
33.6k
Low (2026-04-16)
78.9k
Mean of shown points
301
Points

How it is computed

  1. Same legs table as daily volume.
  2. count(DISTINCT intent_hash) over non-failed entries, grouped by date.

Read this before quoting it

  • Public intents only.
  • Mean intents per day moved -52% from August to October while average size rose; the cause is untested.

Runs 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.

pipeline/sql/10_daily_volume.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 the
3-- 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_intents
19FROM legs_usd
20GROUP BY date ORDER BY date;
pipeline/sql/00_setup.sql (base tables: legs, prices, roles)
pipeline/sql/00_setup.sql
1-- Base tables. Inputs: raw parquet cache ({RAW}) plus the `assets` and `prices` tables registered by metrics.py
2-- from data/reference/. Nothing here is aggregated; every metric file below builds on these.
3
4-- forward-filled daily price per key (calendar x key), so a missing day reuses the last known price
5CREATE OR REPLACE TABLE prices_filled AS
6WITH 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 price
10FROM keys k CROSS JOIN cal c LEFT JOIN prices p ON p.price_key = k.price_key AND p.date = c.date;
11
12-- 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 AS
14SELECT 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_raw
16FROM (SELECT row_number() OVER () AS entry_id, *,
17 unnest(map_entries(CAST(diff::JSON AS MAP(VARCHAR, VARCHAR)))) AS e
18 FROM read_parquet('{RAW}/swaps/*.parquet'));
19
20-- usd: priced from the price table. usd_imputed: for an entry with exactly ONE unpriced asset, the value implied
21-- 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 AS
24WITH 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 usd
29 FROM legs l
30 LEFT JOIN assets a ON a.asset = l.asset
31 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_unpriced
35 FROM base)
36SELECT * EXCLUDE (priced_sum, n_unpriced),
37 CASE WHEN usd IS NULL AND n_unpriced = 1 AND priced_sum IS NOT NULL
38 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 NULL
40 AND sign(-priced_sum) = sign(amount_raw) THEN -priced_sum END) AS usd_best
41FROM e;
42
43-- fees_collected per entry, priced the same way
44CREATE OR REPLACE TABLE fees_usd AS
45SELECT 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 usd
48FROM (SELECT ts, failed, unnest(map_entries(CAST(fees::JSON AS MAP(VARCHAR, VARCHAR)))) AS e
49 FROM read_parquet('{RAW}/swaps/*.parquet') WHERE fees <> '{}') f
50LEFT JOIN assets a ON a.asset = f.e.key
51LEFT JOIN prices_filled p ON p.price_key = a.price_key AND p.date = CAST(f.ts AS DATE);
52
53-- NEP-245 mint/burn (deposits, withdrawals, refunds); token_ids and amounts are parallel arrays
54CREATE OR REPLACE TABLE mt_usd AS
55SELECT 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_asset
59FROM (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 amt
62 FROM read_parquet('{RAW}/mt/*.parquet')) m
63LEFT JOIN assets a ON a.asset = m.asset
64LEFT 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:%';
66
67
68-- Swap pairing: a receipt whose successful entries belong to exactly two accounts is one user<->solver swap.
69CREATE OR REPLACE TABLE pair_receipts AS
70SELECT receipt_id, min(account_id) AS a, max(account_id) AS b
71FROM (SELECT DISTINCT receipt_id, account_id FROM legs WHERE NOT failed)
72GROUP BY receipt_id HAVING count(*) = 2;
73
74-- Solver heuristic (documented in METHODOLOGY): an account is a solver if it faced >= 100 distinct counterparties
75-- 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 AS
77WITH 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 role
80FROM sides GROUP BY account_id;

More in Activity