feat(protocol): top-5 30d pair volume, TVL, and vol/LP on /protocol #1263

Closed
opened 2026-09-14 14:22:04 +00:00 by LVS · 4 comments

A request to update the statistics collection to also include a chart or table showing the top 5 pairs by volume over the last 30days, table should also include pool liquidity/TVL - a ratio should also be displayed showing volume divided by the TVL - this will provide a cost to benefit analysis of current deployed liquidity.

This is to help inform liquidity deployment for voting

A request to update the statistics collection to also include a chart or table showing the top 5 pairs by volume over the last 30days, table should also include pool liquidity/TVL - a ratio should also be displayed showing volume divided by the TVL - this will provide a cost to benefit analysis of current deployed liquidity. This is to help inform liquidity deployment for voting
Author

Do we need to start tracking other pairs like BTC, ETH, stocks to see their viability, maturity and feasibility for the CMM?

Do we need to start tracking other pairs like BTC, ETH, stocks to see their viability, maturity and feasibility for the CMM?

Approved for issue repair -> implementation. No other pair tracking at this time, should be seperate issue

Approved for issue repair -> implementation. No other pair tracking at this time, should be seperate issue

Summary

Add a top-5 factory pairs table on /protocol ranked by trailing 30-day USD volume, with current v2 AMM TVL and volume ÷ TVL (turnover of currently deployed liquidity). Bundle table + 30d volume stamp + TVL column + ratio into one issue. Do not split “chart vs table” vs “API field” vs “ratio cell” vs “gem hide.”

This is a census ranking so operators can compare which pools are working relative to locked USD. It does not execute CMM LP, open a vote, advertise APR, or track off-DEX underlyings.

Related, not duplicates (keyword overlap on “protocol” / “volume” / “TVL” is not enough):

  • #550 / #652 / #667 (closed) — DEX-wide USD tiles. Global total_volume_30d_usd, not a per-pair ranking.
  • #569 / #677 (closed) — Total liquidity one cell + 24h Δ%. No pair rows. Do not put a 30d Δ% chip back on that tile.
  • #668 / #689 / #703 (closed) — UTC grain Volume/Liquidity/Fees chart. Time series of the whole DEX, not a 5-row pair ranking. Do not add a pair category bar chart here.
  • #547 / #655 / #692 (closed) — /pool full sortable list with 24h Vol USD + v2 LP USD. No 30d pair volume, no vol/LP ratio, not on /protocol. Do not reopen those to “add a ratio column.”
  • #666 (closed) — /charts is pair-scoped; global census stays on /protocol. Do not put this table on Charts.
  • #576 / #577 (closed) — trailing-window copy and idle decay. Reuse; do not invent a calendar-month “30d.”
  • #562 / #631 (closed) — gem hide / Llama gem exclude. This table must reuse COLUMBUS5_GEM_ADDRESSES, not a new list.
  • #1207 (open, research) — farming / yields vs CMM cash flow. No farm, gauge, or APR chrome here.
  • #1223 (open, research) — BSC bStocks / tokenized equities. Off-DEX BTC / ETH / stocks are a separate issue (thread on this ticket). Do not add CEX or bridged-equity rows.
  • #558 (closed) — CMM-custodied POL ops. This table is read-only; it must not Provide / RemoveLiquidity.

Not already implemented (checked on current main): GET /api/v1/pairs has volume_usd_24h + liquidity_usd only; pair_volume_24h has no 30d column; GET /pairs/{addr}/stats is 24h; /protocol has global 30d volume and total TVL, not a top-5 pair table or vol/LP ratio.

Current codebase

/protocol (frontend-dapp/src/pages/ProtocolPage.tsx) is the DEX USD census page (P550-1): title → Global stats (protocol-global-stats) → Protocol fees → DEX hub prices → one CEX oracle card → audit contracts → hooks.

Surface What it shows today Gap
Global stats DEX-wide 24h/7d/30d volume USD, Total liquidity (sum of priced factory AMM TVL) + 24h Δ%, census counts, UTC grain chart No per-pair ranking
/pool table All listed pairs; Vol = trailing 24h USD; v2 LP USD = current pair_liquidity_usd 24h only; no vol/TVL; not on Protocol
GET /api/v1/overview O(1) global_stats_24h rollup, 60s cache (P550-5). Must not SUM 30d swap_events on GET No top-N pairs
GET /api/v1/pairs List JOIN pair_volume_24h + pair_liquidity_usd. Sort volume_usd_24h / liquidity_usd. Limit default 50, max 100 No volume_usd_30d; fetching 100 rows to slice top-5 is the wrong API
GET /pairs/{addr}/stats 24h pair stats (volume_usd) N+1 from Protocol is forbidden (P547-9)
Volume aggregator refresh_pair_volumes stamps 24h quote + USD (~5 min). Idle → 0 (D3). Unpriced / overflow ≥ 10^20 → NULL No 30d pair stamp
Liquidity aggregator refresh_protocol_liquidity stamps pair_liquidity_usd from protocol_pair_tvl (P522-Q / hub; never $1 UST1 / 2.5× USTR / vFDUSD) Current stock only — no per-pair 30d average TVL
Gems Frontend filterRetailDiscoveryIndexerPairs on production /pool. Indexer Llama/GT exclude COLUMBUS5_GEM_ADDRESSES. Protocol global volume still includes gems (P652-4) A voting-oriented top-5 must not rank EMBER…PEARL

USD volume ingest is one notional per swap (L10 / volume_usd_for_swap). TVL is both legs h0×usd0 + h1×usd1 (one catalogued leg 2×; neither → omit, not $0) (P569-5). Book escrow, parked dust, wallet LP share, wrap-mapper inventory, and V3 Grid vaults are not TVL (P655).

Why this work is needed

  1. Global 30d volume and total TVL cannot answer “which pool is earning its keep.” CMM / governance LP decisions need pair-level 30d flow vs currently locked USD.
  2. /pool already has 24h Vol and v2 LP, but operators would have to sort 24h, ignore 30d, and divide by hand. A dedicated Protocol table with a hard cap of 5 is the census surface (#666 left global ranking on /protocol).
  3. Client-side volume_usd_24h / liquidity_usd would lie about the requested 30d window. Live SUM(swap_events) on GET would violate P550-5 / PVol-3.

Constraints / guardrails

  • One table, 30d only. No Hourly/Daily/Monthly grain, no 24h/7d window chips, no horizontal-bar “chart” of the same five rows. Table is the vehicle (pair + vol + TVL + ratio).
  • Rollup-only GET. Stamp trailing 30d pair USD in the volume aggregator (~5 min). GET JOINs that stamp + pair_liquidity_usd. EXPLAIN must not scan swap_events / pair_reserves. 60s response cache keyed only by allowlisted params.
  • Hard cap 5. limit omitted or 5 only; any other limit / window / from / to / sort / ticker / junk → 400. Extra query keys must not bust the cache (same as P668-5).
  • Rank by priced 30d USD desc, then pair_id asc. Unpriced 30d volume (NULL) does not rank. Idle "0" does not take a slot unless every priced pair is idle (then show empty / “No pairs yet,” not five $0 rows).
  • Ratio = volume_usd_30d / liquidity_usd (current stock). Document as trailing-30d flow over current pool USD — not 30d-average TVL (no per-pair history table). liquidity_usd missing / ≤ 0 / overflow → JSON null, UI —. Never Infinity / NaN / 0. Display as a compact multiplier (2.4×), not a percent (2.4 is not +2.4%).
  • Gems out. Exclude pairs whose either leg is in COLUMBUS5_GEM_ADDRESSES (lockstep with #562 / #631). Production /protocol must not list EMBER…PEARL even if they dominate 30d flow. Do not burn gem rows.
  • Same USD catalog as #548 / #556 / #569. Never vFDUSD, never $1 UST1 / CL8Y, never 2.5× USTR, never CG liquidity_in_usd. Identity is contract/denom.
  • Copy (#489). Title ≤ ~5 words: Top pairs (30d). One-line lead: trailing 30-day volume vs current pool USD. Do not lecture “cost to benefit,” “voting,” “CMM,” or “deploy LP here.” No vote / Provide CTA. Pair labels are symbols, not token0.
  • Chrome (#653). Sibling shell-panel protocol-top-pairs after Global stats, before Protocol fees. Flat table (or one inner table well). No card-glass per row. Do not nest this table inside protocol-global-stats (already tiles + UTC chart).
  • Links. Pair → chartsPairHref (/charts/:pairAddr) only when the address is valid Terra bech32. Invalid / javascript: / HTML → text, no Link (T541 / A1).
  • Additive APIs. Do not change GET /overview shape. Do not add sort=volume_usd_30d to GET /pairs in this MR (that would become a /pool 30d column — out of scope). Do not N+1 GET …/stats.
  • No CosmWasm, no CMM txs, no farm, no BTC/ETH/stock rows.
  • Public pair ids and JSON fields only. No hosts, deploy pipelines, or keys.

Relevant files

Path Why
indexer/src/db/queries/volume.rs refresh_pair_volumes — add 30d pair USD stamp (sibling of 24h)
indexer/src/indexer/volume_aggregator.rs Same ~5 min loop + startup refresh
indexer/migrations/ New pair_volume_30d (or equivalently named) rollup: pair_id, nullable volume_usd, updated_at; NULLS LAST index
indexer/src/api/protocol_top_pairs.rs (new) GET /api/v1/protocol/top-pairs — join stamps, gem exclude, cap 5, ratio, 60s cache
indexer/src/api/mod.rs Route + OpenAPI
indexer/src/indexer/defillama.rs Reuse COLUMBUS5_GEM_ADDRESSES / gem_addresses_lowercased — do not fork the list
indexer/src/db/queries/pair_liquidity.rs JOIN pair_liquidity_usd (do not recompute TVL on GET)
indexer/tests/indexer_pair_volume_usd.rs Pattern: EXPLAIN no swap_events; idle zero; overflow NULL
indexer/tests/ (new) Top-5 rank, gem skip, ratio fail-closed, 400 on bad query, cache key
frontend-dapp/src/pages/ProtocolPage.tsx Mount after Global stats; include query in market-data outage detect/retry
frontend-dapp/src/components/protocol/ProtocolTopPairs.tsx (new) Table: Pair / 30d vol / v2 LP / Vol/LP
frontend-dapp/src/services/indexer/client.ts getProtocolTopPairs()
frontend-dapp/src/types/index.ts Response type; additive
frontend-dapp/src/utils/formatProtocolStats.ts formatProtocolTurnover (N× / —; never Inf)
frontend-dapp/src/utils/chartsPairRoute.ts Safe pair href
frontend-dapp/src/utils/trailingWindowCopy.ts Trailing 30d header titles (#576)
frontend-dapp/src/pages/ProtocolPage.test.tsx RTL: 5 rows, columns, em-dash, no gems, chrome
skills/AGENTS_FRONTEND_PROTOCOL_STATS.md New P1263 invariants + page-order update
docs/frontend.md / docs/indexer-invariants.md Census table + GET contract
scripts/qa/verify-issue-1263.sh Docs + tests + GET 400/cap
  1. Stamp trailing 30d pair USD in the volume aggregator, same rules as #692: SUM(swap_events.volume_usd) priced legs only; unpriced activity → NULL; overflow ≥ 10^20 → NULL; no 30d swaps → 0 (decay, D3 analog). Do not COALESCE unpriced to 0 for ranking.
  2. GET GET /api/v1/protocol/top-pairs (window implicit 30d; if a query param exists it is allowlist window=30d only):
    • JOIN pair_volume_30d + pair_liquidity_usd + pair/asset symbols.
    • WHERE not gem-legged, volume_usd IS NOT NULL, volume_usd > 0.
    • ORDER BY volume_usd DESC NULLS LAST, pair_id ASC LIMIT 5.
    • Each item: pair_address, asset_0 / asset_1 briefs (existing AssetBrief), volume_usd_30d, liquidity_usd (omit/null if unpriced), volume_per_tvl (null if TVL missing/≤0).
    • 60s cache; cold/empty → items: [] (not five synthetic zeros).
  3. UI protocol-top-pairs sibling panel. Compact USD via formatProtocolUsd / formatPairListVolumeUsd. Ratio via new helper. ≤5 rows. Phone 390: table may overflow-x-auto; do not drop the ratio column. Hide panel on 404/501 (old indexer), same as the UTC chart.
  4. Docs + verify make verify-issue-1263: skill/docs mention P1263; EXPLAIN assertion; RTL; GET 400 cases; keep verify-issue-550 / 569 / 655 / 692 / 562 / 653 green.

Do not compute the ratio in the browser from /pairs?sort=volume_usd_24h&limit=5.

Acceptance criteria

  • AC1. /protocol shows Top pairs (30d) after Global stats and before Protocol fees, with at most five factory pairs ranked by trailing 30d USD volume.
  • AC2. Columns: Pair (leg symbols) | 30d vol (USD) | v2 LP (USD) | Vol/LP (N× or —). Header titles name trailing 30d flow vs current pool USD (#576). Not calendar month. Not 24h Vol reused as 30d.
  • AC3. Indexer stamp + GET are rollup-only. Integration test EXPLAIN of the GET query contains neither swap_events nor pair_reserves. GET /overview still O(1) (no new 30d scan).
  • AC4. Ratio is volume_usd_30d / liquidity_usd computed server-side. TVL missing/≤0/overflow → JSON null, UI —, never Inf. Hostile / non-finite strings → —.
  • AC5. Gem-legged pairs never appear in the five rows (columbus-5 COLUMBUS5_GEM_ADDRESSES). Fixture: high 30d volume on a gem pair loses to a lower-volume economic pair.
  • AC6. Unpriced 30d USD does not rank (NULL last / excluded). Idle 0 does not fill empty slots. Fewer than 5 priced economic pairs → render that many. Zero → empty copy No pairs yet, not $0 theater.
  • AC7. GET /api/v1/protocol/top-pairs: limit=6 / window=24h / from= / to= / sort= / ticker= → 400. Cache ignores extra junk. Cap is 5 even if a client asks for more (400, not truncate).
  • AC8. Pair cell uses chartsPairHref; invalid bech32 / javascript: is not an href. Symbols / XSS strings render as text (no innerHTML).
  • AC9. One chrome layer: no card-glass per row; no nested shell-panel. python3 scripts/check_chrome_nesting.py green. Production copy has no “voting” / “farm” / “APR” lecture (#489 / H531-4).
  • AC10. Docs: P1263 in AGENTS_FRONTEND_PROTOCOL_STATS.md + indexer invariant + make verify-issue-1263. Existing verify-issue-550 / 569 / 655 / 692 / 653 / 562 stay green.

Test plan (all paths)

ID Path How to test
P1 Rank Three priced economic pairs, volumes 30 / 10 / 20 (30d USD) Order 30, 20, 10. Pair symbols match legs
P2 Cap Seven priced economic pairs Exactly 5 rows; 6th absent. GET never returns 6
P3 Tie Two pairs same 30d USD Stable pair_id asc after volume
P4 Decay Pair with only 31d-old swaps Stamp 0 / excluded from top-5 (D3 analog). Mutate block_timestamp, no 31-day sleep
P5 Unpriced vol Swaps with NULL volume_usd Pair does not rank; no $0 stand-in
P6 TVL omit Top-volume pair with no pair_liquidity_usd row Still ranks if volume priced; v2 LP and Vol/LP —
P7 Zero TVL liquidity_usd = 0 Ratio null / —, not Inf
P8 Overflow 30d SUM ≥ 10^20 NULL; does not rank
P9 Gems Gem pair volume > all economic Gem absent; economic rows fill
P10 Empty Fresh indexer, no priced 30d items: []; UI empty copy; panel not five dashes of fake USD
P11 GET 400 limit=5 OK; limit=1/6, window=7d, from, to, sort=volume_usd_24h 400
P12 EXPLAIN Top-pairs SQL No swap_events / pair_reserves
P13 Overview After stamp change GET /overview still rollup; no extra 30d SUM
P14 UI format 1234.5 vol, 500 TVL Compact $ vol/LP; ratio 2.47× class (compact, not +247%)
P15 Phone 390px /protocol All four columns reachable (scroll OK); no nested cards
P16 Outage Indexer 502 / missing route Panel hidden or Retry; included in detectMarketDataOutage
P17 Deep link Valid pair address Charts link; javascript:alert(1) not an href
P18 Copy Visible Protocol strings No \bfarm(ing)?\b, no “vote”, no “cost to benefit” lecture
P19 LocalTerra Optional Economic hub pair with 30d activity appears; gems still excluded

Negative / skip:

  • N1. Ranking GET /pairs?sort=volume_usd_24h&limit=5 as 30d → fail AC2.
  • N2. Client 24h vol / TVL → fail AC2 / AC4.
  • N3. Putting the table on /pool or /charts instead of /protocol → fail AC1.
  • N4. Restoring 30d Δ% on Total liquidity (#677) → fail.
  • N5. Adding BTC/ETH/stock / bStocks rows (#1223) → fail.
  • N6. Vote / Provide / farm CTA → fail AC9.

Test plan (attack, hack, and abuse)

Evaluate each vector. Do not write exploits, PoCs, or attack procedures.

ID Vector What must hold Fail if
A1 XSS / HTML symbol, volume_usd_30d, liquidity_usd contain <script> / javascript: Text nodes only; formatters → —; no innerHTML / eval Pair cell executes markup or a javascript: href
A2 Ratio bomb TVL 0, "", "0", negative, 1e309 JSON null; UI — Page paints Infinity / NaN as a “good” pool
A3 Wash / gem rank Soft-launch gem with huge 30d notional Excluded by address set (spoof ticker UST1 on a gem contract still excluded — address wins) Gem occupies a top-5 slot on production Protocol
A4 Unpriced as deep TVL Missing catalog → COALESCE 0 Unpriced vol does not rank; unpriced TVL is —, not $0 liquidity Operators “vote” a $0 pool as cheapest
A5 GET DoS limit=100000, from/to forcing a 30d swap_events scan 400; rollup only; 60s cache; junk query keys do not bust cache Unbounded JSON or request-path 30d SUM
A6 N+1 LCD Protocol first paint Zero new LCD / per-row /stats / hub-prices in the browser Pair TVL recomputed from reserves in the client
A7 Fake identity Native spoof / same-asset pair Same skip rules as P569-5 / P655-6; no rank from spoof USD Spoof uusd prints as USTC TVL
A8 Cache alias Different hostile query strings Allowlisted cache key only Attacker busts 60s cache with junk params
A9 Overview regression Parallel /overview burst Still 60s + O(1) rollup (P550-5) Top-pairs work triggers a 30d global scan
A10 Governance theater Table used as a “vote” Read-only census; no Provide / Migrate / farm CTA that moves CMM LP from this panel

Verification criteria

  • make verify-issue-1263 green (Postgres indexer tests + frontend RTL; no CosmWasm migrate).
  • Manual: /protocol shows ≤5 economic pairs; 30d vol + v2 LP + N×; gems absent; 390 and 1280 layouts; Charts link on a valid pair.
  • Reviewers can curl GET /api/v1/protocol/top-pairs and see ≤5 items, volume_per_tvl null-safe, 400 on limit=6.
  • make verify-issue-550 569 655 692 653 562 still green.
  • No follow-up issue for a Protocol bar chart, /pool 30d column, or BTC/ETH rows unless product files them separately (#1223 for off-DEX names).

Out of scope

  • Off-DEX BTC / ETH / stocks / bStocks (#1223).
  • Farming, gauges, APR, fee-split to LPs (#1207, H531-4).
  • CMM Provide / RemoveLiquidity / rebalance scripts (#558 and existing rebalance runbooks).
  • /pool 30d volume column or GET /pairs?sort=volume_usd_30d.
  • Per-pair historical TVL average (would need a new snapshot table).
  • UTC grain chart changes (#668 / #689 / #703).
  • GET /overview new fields, unique-trader headline, 30d chip on Total liquidity.
  • CosmWasm, factory whitelist, tokenlist, wrap-mapper.
  • Deploy / Coolify / host / SKU / runtime model.

First-pass model recommendation

Recommendation: grok-high

Rationale: This is a new public ranking API plus a Protocol census table. It crosses the volume aggregator (volume.rs), a new rollup table, gem-address lockstep with #562/#631, fail-closed DECIMAL ratio, 60s/400 GET contract (API4-class caps), and /protocol chrome/copy. That is more than a local known edit to ProtocolGlobalStats.tsx. Composer criteria fail on subsystem count and ranking/ratio edge cases (NULL vs 0 vs Inf vs gems). Verification is indexer EXPLAIN + RTL + GET 400, not a CSS-only patch. Advisory planning text only — not a runner argument.

## Summary Add a **top-5 factory pairs** table on `/protocol` ranked by **trailing 30-day USD volume**, with **current v2 AMM TVL** and **volume ÷ TVL** (turnover of currently deployed liquidity). Bundle table + 30d volume stamp + TVL column + ratio into **one** issue. Do **not** split “chart vs table” vs “API field” vs “ratio cell” vs “gem hide.” This is a **census ranking** so operators can compare which pools are working relative to locked USD. It does **not** execute CMM LP, open a vote, advertise APR, or track off-DEX underlyings. Related, **not duplicates** (keyword overlap on “protocol” / “volume” / “TVL” is not enough): - [#550](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/550) / [#652](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/652) / [#667](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/667) (closed) — DEX-wide USD tiles. Global `total_volume_30d_usd`, not a per-pair ranking. - [#569](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/569) / [#677](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/677) (closed) — **Total liquidity** one cell + 24h Δ%. No pair rows. Do not put a 30d Δ% chip back on that tile. - [#668](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/668) / [#689](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/689) / [#703](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/703) (closed) — UTC **grain** Volume/Liquidity/Fees chart. Time series of the whole DEX, not a 5-row pair ranking. Do not add a pair category bar chart here. - [#547](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/547) / [#655](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/655) / [#692](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/692) (closed) — `/pool` full sortable list with **24h** Vol USD + v2 LP USD. No 30d pair volume, no vol/LP ratio, not on `/protocol`. Do not reopen those to “add a ratio column.” - [#666](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/666) (closed) — `/charts` is pair-scoped; global census stays on `/protocol`. Do not put this table on Charts. - [#576](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/576) / [#577](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/577) (closed) — trailing-window copy and idle decay. Reuse; do not invent a calendar-month “30d.” - [#562](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/562) / [#631](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/631) (closed) — gem hide / Llama gem exclude. This table must reuse `COLUMBUS5_GEM_ADDRESSES`, not a new list. - [#1207](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1207) (open, research) — farming / yields vs CMM cash flow. No farm, gauge, or APR chrome here. - [#1223](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1223) (open, research) — BSC bStocks / tokenized equities. **Off-DEX BTC / ETH / stocks are a separate issue** (thread on this ticket). Do not add CEX or bridged-equity rows. - [#558](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/558) (closed) — CMM-custodied POL ops. This table is read-only; it must not Provide / RemoveLiquidity. **Not already implemented** (checked on current `main`): `GET /api/v1/pairs` has `volume_usd_24h` + `liquidity_usd` only; `pair_volume_24h` has no 30d column; `GET /pairs/{addr}/stats` is 24h; `/protocol` has global 30d volume and total TVL, not a top-5 pair table or vol/LP ratio. ## Current codebase `/protocol` (`frontend-dapp/src/pages/ProtocolPage.tsx`) is the DEX USD census page (**P550-1**): title → **Global stats** (`protocol-global-stats`) → **Protocol fees** → **DEX hub prices** → one CEX oracle card → audit contracts → hooks. | Surface | What it shows today | Gap | | --- | --- | --- | | Global stats | DEX-wide 24h/7d/30d **volume USD**, **Total liquidity** (sum of priced factory AMM TVL) + 24h Δ%, census counts, UTC grain chart | No per-pair ranking | | `/pool` table | All listed pairs; **Vol** = trailing **24h** USD; **v2 LP USD** = current `pair_liquidity_usd` | 24h only; no vol/TVL; not on Protocol | | `GET /api/v1/overview` | O(1) `global_stats_24h` rollup, 60s cache (**P550-5**). Must not `SUM` 30d `swap_events` on GET | No top-N pairs | | `GET /api/v1/pairs` | List JOIN `pair_volume_24h` + `pair_liquidity_usd`. Sort `volume_usd_24h` / `liquidity_usd`. Limit default 50, max 100 | No `volume_usd_30d`; fetching 100 rows to slice top-5 is the wrong API | | `GET /pairs/{addr}/stats` | 24h pair stats (`volume_usd`) | N+1 from Protocol is forbidden (**P547-9**) | | Volume aggregator | `refresh_pair_volumes` stamps **24h** quote + USD (~5 min). Idle → 0 (**D3**). Unpriced / overflow ≥ 10^20 → NULL | No 30d pair stamp | | Liquidity aggregator | `refresh_protocol_liquidity` stamps `pair_liquidity_usd` from `protocol_pair_tvl` (P522-Q / hub; never `$1` UST1 / `2.5×` USTR / vFDUSD) | Current stock only — no per-pair 30d average TVL | | Gems | Frontend `filterRetailDiscoveryIndexerPairs` on production `/pool`. Indexer Llama/GT exclude `COLUMBUS5_GEM_ADDRESSES`. Protocol **global** volume still includes gems (**P652-4**) | A voting-oriented top-5 must not rank EMBER…PEARL | USD volume ingest is one notional per swap (**L10** / `volume_usd_for_swap`). TVL is both legs `h0×usd0 + h1×usd1` (one catalogued leg `2×`; neither → omit, not `$0`) (**P569-5**). Book escrow, parked dust, wallet LP share, wrap-mapper inventory, and V3 Grid vaults are **not** TVL (**P655**). ## Why this work is needed 1. Global 30d volume and total TVL cannot answer “which **pool** is earning its keep.” CMM / governance LP decisions need **pair-level** 30d flow vs **currently locked** USD. 2. `/pool` already has 24h Vol and v2 LP, but operators would have to sort 24h, ignore 30d, and divide by hand. A dedicated Protocol table with a **hard cap of 5** is the census surface (#666 left global ranking on `/protocol`). 3. Client-side `volume_usd_24h / liquidity_usd` would lie about the requested 30d window. Live `SUM(swap_events)` on GET would violate **P550-5** / **PVol-3**. ## Constraints / guardrails - **One table, 30d only.** No Hourly/Daily/Monthly grain, no 24h/7d window chips, no horizontal-bar “chart” of the same five rows. Table is the vehicle (pair + vol + TVL + ratio). - **Rollup-only GET.** Stamp trailing 30d pair USD in the volume aggregator (~5 min). `GET` JOINs that stamp + `pair_liquidity_usd`. `EXPLAIN` must not scan `swap_events` / `pair_reserves`. 60s response cache keyed only by allowlisted params. - **Hard cap 5.** `limit` omitted or `5` only; any other `limit` / `window` / `from` / `to` / `sort` / `ticker` / junk → **400**. Extra query keys must not bust the cache (same as **P668-5**). - **Rank by priced 30d USD desc, then `pair_id` asc.** Unpriced 30d volume (`NULL`) does not rank. Idle `"0"` does not take a slot unless every priced pair is idle (then show empty / “No pairs yet,” not five `$0` rows). - **Ratio = `volume_usd_30d / liquidity_usd` (current stock).** Document as trailing-30d flow over **current** pool USD — not 30d-average TVL (no per-pair history table). `liquidity_usd` missing / `≤ 0` / overflow → JSON `null`, UI **—**. Never `Infinity` / `NaN` / `0`. Display as a compact multiplier (`2.4×`), **not** a percent (2.4 is not `+2.4%`). - **Gems out.** Exclude pairs whose either leg is in `COLUMBUS5_GEM_ADDRESSES` (lockstep with #562 / #631). Production `/protocol` must not list EMBER…PEARL even if they dominate 30d flow. Do not burn gem rows. - **Same USD catalog as #548 / #556 / #569.** Never vFDUSD, never `$1` UST1 / CL8Y, never `2.5×` USTR, never CG `liquidity_in_usd`. Identity is contract/denom. - **Copy (#489).** Title ≤ ~5 words: **Top pairs (30d)**. One-line lead: trailing 30-day volume vs current pool USD. Do **not** lecture “cost to benefit,” “voting,” “CMM,” or “deploy LP here.” No vote / Provide CTA. Pair labels are symbols, not `token0`. - **Chrome (#653).** Sibling `shell-panel` `protocol-top-pairs` **after Global stats, before Protocol fees**. Flat table (or one inner table well). No `card-glass` per row. Do not nest this table inside `protocol-global-stats` (already tiles + UTC chart). - **Links.** Pair → `chartsPairHref` (`/charts/:pairAddr`) only when the address is valid Terra bech32. Invalid / `javascript:` / HTML → text, no `Link` (**T541** / A1). - **Additive APIs.** Do not change `GET /overview` shape. Do not add `sort=volume_usd_30d` to `GET /pairs` in this MR (that would become a `/pool` 30d column — out of scope). Do not N+1 `GET …/stats`. - **No CosmWasm, no CMM txs, no farm, no BTC/ETH/stock rows.** - Public pair ids and JSON fields only. No hosts, deploy pipelines, or keys. ## Relevant files | Path | Why | | --- | --- | | `indexer/src/db/queries/volume.rs` | `refresh_pair_volumes` — add 30d pair USD stamp (sibling of 24h) | | `indexer/src/indexer/volume_aggregator.rs` | Same ~5 min loop + startup refresh | | `indexer/migrations/` | New `pair_volume_30d` (or equivalently named) rollup: `pair_id`, nullable `volume_usd`, `updated_at`; `NULLS LAST` index | | `indexer/src/api/protocol_top_pairs.rs` (new) | `GET /api/v1/protocol/top-pairs` — join stamps, gem exclude, cap 5, ratio, 60s cache | | `indexer/src/api/mod.rs` | Route + OpenAPI | | `indexer/src/indexer/defillama.rs` | Reuse `COLUMBUS5_GEM_ADDRESSES` / `gem_addresses_lowercased` — do not fork the list | | `indexer/src/db/queries/pair_liquidity.rs` | JOIN `pair_liquidity_usd` (do not recompute TVL on GET) | | `indexer/tests/indexer_pair_volume_usd.rs` | Pattern: EXPLAIN no `swap_events`; idle zero; overflow NULL | | `indexer/tests/` (new) | Top-5 rank, gem skip, ratio fail-closed, 400 on bad query, cache key | | `frontend-dapp/src/pages/ProtocolPage.tsx` | Mount after Global stats; include query in market-data outage detect/retry | | `frontend-dapp/src/components/protocol/ProtocolTopPairs.tsx` (new) | Table: Pair / 30d vol / v2 LP / Vol/LP | | `frontend-dapp/src/services/indexer/client.ts` | `getProtocolTopPairs()` | | `frontend-dapp/src/types/index.ts` | Response type; additive | | `frontend-dapp/src/utils/formatProtocolStats.ts` | `formatProtocolTurnover` (`N×` / **—**; never Inf) | | `frontend-dapp/src/utils/chartsPairRoute.ts` | Safe pair href | | `frontend-dapp/src/utils/trailingWindowCopy.ts` | Trailing 30d header `title`s (#576) | | `frontend-dapp/src/pages/ProtocolPage.test.tsx` | RTL: 5 rows, columns, em-dash, no gems, chrome | | `skills/AGENTS_FRONTEND_PROTOCOL_STATS.md` | New **P1263** invariants + page-order update | | `docs/frontend.md` / `docs/indexer-invariants.md` | Census table + GET contract | | `scripts/qa/verify-issue-1263.sh` | Docs + tests + GET 400/cap | ## Recommended direction 1. **Stamp** trailing 30d pair USD in the volume aggregator, same rules as #692: `SUM(swap_events.volume_usd)` priced legs only; unpriced activity → `NULL`; overflow ≥ 10^20 → `NULL`; no 30d swaps → `0` (decay, **D3** analog). Do not COALESCE unpriced to `0` for ranking. 2. **GET** `GET /api/v1/protocol/top-pairs` (window implicit 30d; if a query param exists it is allowlist `window=30d` only): - JOIN `pair_volume_30d` + `pair_liquidity_usd` + pair/asset symbols. - `WHERE` not gem-legged, `volume_usd IS NOT NULL`, `volume_usd > 0`. - `ORDER BY volume_usd DESC NULLS LAST, pair_id ASC LIMIT 5`. - Each item: `pair_address`, `asset_0` / `asset_1` briefs (existing `AssetBrief`), `volume_usd_30d`, `liquidity_usd` (omit/`null` if unpriced), `volume_per_tvl` (`null` if TVL missing/`≤0`). - 60s cache; cold/empty → `items: []` (not five synthetic zeros). 3. **UI** `protocol-top-pairs` sibling panel. Compact USD via `formatProtocolUsd` / `formatPairListVolumeUsd`. Ratio via new helper. ≤5 rows. Phone 390: table may `overflow-x-auto`; do not drop the ratio column. Hide panel on 404/501 (old indexer), same as the UTC chart. 4. **Docs + verify** `make verify-issue-1263`: skill/docs mention P1263; EXPLAIN assertion; RTL; `GET` 400 cases; keep `verify-issue-550` / `569` / `655` / `692` / `562` / `653` green. Do **not** compute the ratio in the browser from `/pairs?sort=volume_usd_24h&limit=5`. ## Acceptance criteria - [ ] **AC1.** `/protocol` shows **Top pairs (30d)** after Global stats and before Protocol fees, with at most **five** factory pairs ranked by trailing 30d USD volume. - [ ] **AC2.** Columns: Pair (leg symbols) | 30d vol (USD) | v2 LP (USD) | Vol/LP (`N×` or **—**). Header `title`s name trailing 30d flow vs current pool USD (#576). Not calendar month. Not 24h Vol reused as 30d. - [ ] **AC3.** Indexer stamp + GET are rollup-only. Integration test `EXPLAIN` of the GET query contains neither `swap_events` nor `pair_reserves`. `GET /overview` still O(1) (no new 30d scan). - [ ] **AC4.** Ratio is `volume_usd_30d / liquidity_usd` computed server-side. TVL missing/`≤0`/overflow → JSON `null`, UI **—**, never Inf. Hostile / non-finite strings → **—**. - [ ] **AC5.** Gem-legged pairs never appear in the five rows (columbus-5 `COLUMBUS5_GEM_ADDRESSES`). Fixture: high 30d volume on a gem pair loses to a lower-volume economic pair. - [ ] **AC6.** Unpriced 30d USD does not rank (NULL last / excluded). Idle 0 does not fill empty slots. Fewer than 5 priced economic pairs → render that many. Zero → empty copy **No pairs yet**, not `$0` theater. - [ ] **AC7.** `GET /api/v1/protocol/top-pairs`: `limit=6` / `window=24h` / `from=` / `to=` / `sort=` / `ticker=` → **400**. Cache ignores extra junk. Cap is 5 even if a client asks for more (400, not truncate). - [ ] **AC8.** Pair cell uses `chartsPairHref`; invalid bech32 / `javascript:` is not an href. Symbols / XSS strings render as **text** (no `innerHTML`). - [ ] **AC9.** One chrome layer: no `card-glass` per row; no nested `shell-panel`. `python3 scripts/check_chrome_nesting.py` green. Production copy has no “voting” / “farm” / “APR” lecture (#489 / **H531-4**). - [ ] **AC10.** Docs: **P1263** in `AGENTS_FRONTEND_PROTOCOL_STATS.md` + indexer invariant + `make verify-issue-1263`. Existing `verify-issue-550` / `569` / `655` / `692` / `653` / `562` stay green. ## Test plan (all paths) | ID | Path | How to test | | --- | --- | --- | | P1 Rank | Three priced economic pairs, volumes 30 / 10 / 20 (30d USD) | Order 30, 20, 10. Pair symbols match legs | | P2 Cap | Seven priced economic pairs | Exactly 5 rows; 6th absent. GET never returns 6 | | P3 Tie | Two pairs same 30d USD | Stable `pair_id` asc after volume | | P4 Decay | Pair with only 31d-old swaps | Stamp 0 / excluded from top-5 (**D3** analog). Mutate `block_timestamp`, no 31-day sleep | | P5 Unpriced vol | Swaps with NULL `volume_usd` | Pair does not rank; no `$0` stand-in | | P6 TVL omit | Top-volume pair with no `pair_liquidity_usd` row | Still ranks if volume priced; v2 LP and Vol/LP **—** | | P7 Zero TVL | `liquidity_usd = 0` | Ratio `null` / **—**, not Inf | | P8 Overflow | 30d SUM ≥ 10^20 | NULL; does not rank | | P9 Gems | Gem pair volume > all economic | Gem absent; economic rows fill | | P10 Empty | Fresh indexer, no priced 30d | `items: []`; UI empty copy; panel not five dashes of fake USD | | P11 GET 400 | `limit=5` OK; `limit=1`/`6`, `window=7d`, `from`, `to`, `sort=volume_usd_24h` | **400** | | P12 EXPLAIN | Top-pairs SQL | No `swap_events` / `pair_reserves` | | P13 Overview | After stamp change | `GET /overview` still rollup; no extra 30d SUM | | P14 UI format | `1234.5` vol, `500` TVL | Compact `$` vol/LP; ratio `2.47×` class (compact, not `+247%`) | | P15 Phone | 390px `/protocol` | All four columns reachable (scroll OK); no nested cards | | P16 Outage | Indexer 502 / missing route | Panel hidden or Retry; included in `detectMarketDataOutage` | | P17 Deep link | Valid pair address | Charts link; `javascript:alert(1)` not an href | | P18 Copy | Visible Protocol strings | No `\bfarm(ing)?\b`, no “vote”, no “cost to benefit” lecture | | P19 LocalTerra | Optional | Economic hub pair with 30d activity appears; gems still excluded | Negative / skip: - N1. Ranking `GET /pairs?sort=volume_usd_24h&limit=5` as 30d → fail AC2. - N2. Client `24h vol / TVL` → fail AC2 / AC4. - N3. Putting the table on `/pool` or `/charts` instead of `/protocol` → fail AC1. - N4. Restoring 30d Δ% on Total liquidity (#677) → fail. - N5. Adding BTC/ETH/stock / bStocks rows (#1223) → fail. - N6. Vote / Provide / farm CTA → fail AC9. ## Test plan (attack, hack, and abuse) Evaluate each vector. Do **not** write exploits, PoCs, or attack procedures. | ID | Vector | What must hold | Fail if | | --- | --- | --- | --- | | A1 XSS / HTML | `symbol`, `volume_usd_30d`, `liquidity_usd` contain `<script>` / `javascript:` | Text nodes only; formatters → **—**; no `innerHTML` / `eval` | Pair cell executes markup or a `javascript:` href | | A2 Ratio bomb | TVL `0`, `""`, `"0"`, negative, `1e309` | JSON `null`; UI **—** | Page paints `Infinity` / `NaN` as a “good” pool | | A3 Wash / gem rank | Soft-launch gem with huge 30d notional | Excluded by address set (spoof ticker `UST1` on a gem contract still excluded — address wins) | Gem occupies a top-5 slot on production Protocol | | A4 Unpriced as deep TVL | Missing catalog → COALESCE 0 | Unpriced vol does not rank; unpriced TVL is **—**, not `$0` liquidity | Operators “vote” a `$0` pool as cheapest | | A5 GET DoS | `limit=100000`, `from`/`to` forcing a 30d `swap_events` scan | **400**; rollup only; 60s cache; junk query keys do not bust cache | Unbounded JSON or request-path 30d SUM | | A6 N+1 LCD | Protocol first paint | Zero new LCD / per-row `/stats` / hub-prices in the browser | Pair TVL recomputed from reserves in the client | | A7 Fake identity | Native spoof / same-asset pair | Same skip rules as **P569-5** / **P655-6**; no rank from spoof USD | Spoof `uusd` prints as USTC TVL | | A8 Cache alias | Different hostile query strings | Allowlisted cache key only | Attacker busts 60s cache with junk params | | A9 Overview regression | Parallel `/overview` burst | Still 60s + O(1) rollup (**P550-5**) | Top-pairs work triggers a 30d global scan | | A10 Governance theater | Table used as a “vote” | Read-only census; no Provide / Migrate / farm | CTA that moves CMM LP from this panel | ## Verification criteria - `make verify-issue-1263` green (Postgres indexer tests + frontend RTL; no CosmWasm migrate). - Manual: `/protocol` shows ≤5 economic pairs; 30d vol + v2 LP + `N×`; gems absent; 390 and 1280 layouts; Charts link on a valid pair. - Reviewers can `curl` `GET /api/v1/protocol/top-pairs` and see ≤5 items, `volume_per_tvl` null-safe, 400 on `limit=6`. - `make verify-issue-550` `569` `655` `692` `653` `562` still green. - No follow-up issue for a Protocol bar chart, `/pool` 30d column, or BTC/ETH rows unless product files them separately (#1223 for off-DEX names). ## Out of scope - Off-DEX BTC / ETH / stocks / bStocks (#1223). - Farming, gauges, APR, fee-split to LPs (#1207, **H531-4**). - CMM Provide / RemoveLiquidity / rebalance scripts (#558 and existing rebalance runbooks). - `/pool` 30d volume column or `GET /pairs?sort=volume_usd_30d`. - Per-pair historical TVL average (would need a new snapshot table). - UTC grain chart changes (#668 / #689 / #703). - `GET /overview` new fields, unique-trader headline, 30d chip on Total liquidity. - CosmWasm, factory whitelist, tokenlist, wrap-mapper. - Deploy / Coolify / host / SKU / runtime model. ## First-pass model recommendation Recommendation: grok-high Rationale: This is a **new public ranking API** plus a Protocol census table. It crosses the volume aggregator (`volume.rs`), a new rollup table, gem-address lockstep with #562/#631, fail-closed DECIMAL ratio, 60s/400 GET contract (API4-class caps), and `/protocol` chrome/copy. That is more than a local known edit to `ProtocolGlobalStats.tsx`. Composer criteria fail on subsystem count and ranking/ratio edge cases (NULL vs 0 vs Inf vs gems). Verification is indexer EXPLAIN + RTL + GET 400, not a CSS-only patch. Advisory planning text only — not a runner argument.
PlasticDigits changed title from Updated statistics on the protocol page to feat(protocol): top-5 30d pair volume, TVL, and vol/LP on /protocol 2026-09-21 04:23:48 +00:00

Merged as PR #1298. Top pairs (30d) cap 5.

sqlx: pair_volume_30d is 20260921120001 so it does not collide with #1277 20260921120000_traders_rolling_volume_numeric_38_0. Coolify must apply both in that order.

A worktree git add -A also tracked a frontend-dapp/node_modules symlink; removed in PR #1299.

Leftover ops: #1300

Merged as PR #1298. Top pairs (30d) cap 5. sqlx: `pair_volume_30d` is **20260921120001** so it does not collide with #1277 `20260921120000_traders_rolling_volume_numeric_38_0`. Coolify must apply both in that order. A worktree `git add -A` also tracked a `frontend-dapp/node_modules` symlink; removed in PR #1299. Leftover ops: https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1300
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#1263
No description provided.