fix(indexer): NUMERIC overflow on rolling trader volume refresh #1277

Closed
opened 2026-09-17 02:25:48 +00:00 by PlasticDigits · 3 comments

Summary

volume_aggregator logs Failed to refresh rolling trader volumes and leaves traders.volume_24h / volume_7d / volume_30d stale whenever a sender’s trailing-window SUM(swap_events.offer_amount) cannot fit NUMERIC(38, 18).

This is the same leftover type as pre-#548 global/pair raw volume: PostgreSQL NUMERIC(38, 18) only stores |x| < 10^20. Raw 18-decimal CW20 notionals (~100 human USTR / CL8Y) already hit that bound. Pair and global rollups were widened to NUMERIC(38, 0) in 20260817120000_*. Trader rolling columns were renamed from volume_*_usd in 20260310000002_* and never widened. Positions/P&L went to NUMERIC(78, 18) in #676; this path is still the old (38, 18) raw-sum columns.

Not a +1 on:

Issue Why it is not this ticket
#577 Trailing-window decay. Landed. Constraint A3 kept LEAST(…, POWER(10,38)-1), which does not fit (38, 18).
#548 Global/pair USD census + NUMERIC(38, 0) for those raw columns. Trader rolling left behind.
#676 trader_positions / P&L storage, not rolling volume refresh.
#1276 Indexer auto-deploy + /health git SHA. Overflow is out of scope there.

Current codebase

Refresh path that errors

refresh_all_volume_windows calls traders::refresh_rolling_volumes. On sqlx::Error the 5-minute loop logs Failed to refresh rolling trader volumes (startup = false → error, GitLab #577 D5). Token / pair / global refresh in the same function can still succeed; trader windows stay at the last good stamp.

LEAST(SUM(CASE WHEN block_timestamp >= $1 THEN offer_amount ELSE 0 END), POWER(10::numeric, 38) - 1)

LEAST to 10^38-1 is the integer cap used after #548 for NUMERIC(38, 0) destinations (token_volume_stats.volume, pair_volume_24h.volume_quote, global_stats_24h.total_volume). Assigning that value into NUMERIC(38, 18) still overflows: integer width is only 20 digits.

offer_amount is already NUMERIC(38, 0) on swap_events. The overflow is the assignment into traders.volume_*, not the SUM itself.

Column types

Column Origin Type today
swap_events.offer_amount initial schema NUMERIC(38, 0)
global_stats_24h.total_volume, pair_volume_24h.volume_quote 20260817120000_* (#548) NUMERIC(38, 0)
traders.volume_24h / 7d / 30d renamed from volume_*_usd NUMERIC(38, 18)
traders.total_volume renamed from original total_volume_usd NUMERIC(38, 18)
traders.total_volume_usd 20260818140000_* (#553) NUMERIC(38, 18) USD — keep; cap is 10^20 - 10^-18

upsert_trader adds raw offer_amount into total_volume without a LEAST cap. Lifetime raw volume can overflow on ingest even when the aggregator is not running. Bundle that column in the same widen.

Existing test raw_18_decimal_volume_does_not_overflow_global_stats inserts 10^21 raw USTR and asserts global/pair refresh. It does not call refresh_rolling_volumes. Decay tests in indexer_volume_window_decay.rs use 6-decimal seed sizes that never hit 10^20.

What already works

  • Idle traders zero rolling columns when absent from the 30d sender set (#577 D2). Keep that second UPDATE; do not touch lifetime columns there.
  • USD lifetime (total_volume_usd) uses the (38, 18) / 10^20 cap on purpose. Do not widen USD.
  • Pair-scoped leaderboard SUM(se.offer_amount) is computed on GET, not stored in traders.volume_*.

Why the new implementation is needed

  1. One 18-decimal trader in the 30d window fails the whole rolling UPDATE. Every other trader’s 24h/7d/30d ranks freeze until the next successful refresh (which never comes while that sender still has huge raw volume).
  2. #577 decay cannot run if the first statement in the transaction errors (single tx; commit skipped). Ghost ranks from leftover windows return.
  3. #548 already documented this overflow class and fixed sibling rollups. Trader rolling is the remaining raw-sum (38, 18) hole on the aggregator loop.

Constraints / guardrails

  1. Widen raw rolling (+ lifetime raw) to NUMERIC(38, 0), matching offer_amount / pair_volume_24h.volume_quote / global_stats_24h.total_volume. Do not switch rolling volume to NUMERIC(78, 18) (#676 is inventory/P&L, not integer offer sums).
  2. Keep LEAST(SUM(…), POWER(10::numeric, 38) - 1) on rolling sums after the destination is (38, 0). Do not “fix” overflow by clamping rolling raw volume to 10^20 - 10^-18 (that is the USD cap; it truncates ~100 human 18-dec tokens).
  3. Do not zero or rewrite total_volume / total_volume_usd / total_trades in the idle-trader UPDATE (#577 D2 / #553).
  4. Token stats stay offer-side. Pair/global/USD ingest (volume_usd_for_swap, P522-Q) unchanged.
  5. Decay tests stay V3: mutate block_timestamp, no 24h wall-clock sleeps.
  6. No live SUM(swap_events) on trader GET for unscoped rolling windows (keep the rollup).
  7. upsert_trader must not numeric-overflow on a single 18-dec swap once total_volume is (38, 0); still cap additive lifetime raw at 10^38-1 if a running sum could exceed the type.
  8. Do not fold this into #1276 (health/SHA) or reopen #577.

Relevant files

Area Path
Aggregator loop indexer/src/indexer/volume_aggregator.rs
Rolling SQL + upsert indexer/src/db/queries/traders.rs
Schema new migration under indexer/migrations/ (widen traders.volume_24h, volume_7d, volume_30d, total_volume)
Sibling pattern indexer/migrations/20260817120000_backfill_swap_volume_usd_catalog.sql
Decay regression indexer/tests/indexer_volume_window_decay.rs
Overflow fixture indexer/tests/volume_usd_catalog.rs (10^21 raw USTR)
API strings indexer/src/api/traders.rs (bd plain decimal; no 1e+19)
Docs / skill docs/indexer-invariants.md (trailing window decay row), skills/AGENTS_INDEXER_VOLUME_WINDOW_DECAY.md
  1. Migration: ALTER TABLE traders ALTER COLUMN volume_24h TYPE NUMERIC(38, 0) (same for volume_7d, volume_30d, total_volume). Comment that these are raw offer_amount sums, not USD.
  2. SQL: keep the existing LEAST(…, POWER(10,38)-1) on rolling SUMs. Add the same cap on upsert_trader total_volume = traders.total_volume + $2.
  3. Test: seed a trader with offer_amount = 10^21 inside 24h; refresh_rolling_volumes returns Ok; stored volume_24h equals the raw sum (or 10^38-1 only if the fixture exceeds that). Repeat for 7d/30d CASE branches. Assert total_volume_usd unchanged / still (38, 18)-legal.
  4. Decay: reuse #577 I4/I5 after the huge-sum fixture ages past 30d → rolling columns 0, lifetime raw intact.
  5. make verify-issue-<iid> plus an invariants/skill sentence that rolling raw volume is (38, 0) and must not fail the aggregator.

Acceptance criteria

  • R1 Trader with a single in-window swap of 10^21 raw offer: refresh_rolling_volumes succeeds; volume_24h stores that integer (plain decimal JSON, no scientific notation).
  • R2 Mixed 6-dec + 18-dec senders in one refresh: no overflow; 6-dec rolling values unchanged vs today’s decay tests.
  • R3 Idle 30d+ trader still zeros rolling columns only (#577 D2).
  • R4 upsert_trader with 10^21 raw does not fail; total_volume holds the running raw sum (LEAST at 10^38-1).
  • R5 total_volume_usd remains NUMERIC(38, 18) with the existing 10^20 USD cap. Unpriced stays NULL.
  • R6 Docs/skill + make verify-issue-<iid>.
  • R7 Token / pair / global refresh in refresh_all_volume_windows still run when trader refresh would previously have been the only failure (after the fix, trader refresh must not be the failure).

Given / When / Then

Given traders.volume_24h (and 7d/30d) are populated from SUM(swap_events.offer_amount) and a sender has ≥ 10^20 raw offer in the trailing window
When volume_aggregator runs refresh_rolling_volumes (startup or 5-minute loop)
Then the statement commits without numeric overflow, rolling columns equal the capped raw integer, #577 zero-out still applies to idle senders, and USD lifetime columns are not rewritten

Given a new 18-decimal swap of 10^21 raw offer
When upsert_trader adds that amount to total_volume
Then ingest succeeds and total_volume is the raw running sum (capped at 10^38-1), not a (38, 18) overflow

Test plan (functional paths)

ID Case Expect
I1 24h swap offer_amount = 10^21 refresh_rolling_volumes Ok; volume_24h = 10^21
I2 Same swap aged 25h volume_24h = 0; volume_7d = 10^21
I3 Aged 31d all three rolling 0; total_volume still includes the swap
I4 Two senders: 6-dec seed + 18-dec 10^21 both refresh; 6-dec matches decay fixture
I5 Empty DB / no swaps zeros, no error
I6 upsert_trader 10^21 then refresh no overflow on either path
I7 Leaderboard sort=volume_24h 18-dec sender ranks by raw integer; JSON plain string
I8 refresh_all_volume_windows(..., false) no Failed to refresh rolling trader volumes for I1 fixture
I9 USD: priced 6-dec swap total_volume_usd still (38, 18) legal; I1 18-dec raw does not smash USD

Test plan (attack, hack, and abuse)

ID Vector Expect
A1 Hostile LCD / crafted offer_amount at 10^38 LEAST to 10^38-1; no panic; no wrap to negative
A2 Negative or non-finite offer (should be rejected at ingest) Rolling SUM ignores invalid rows already rejected by swap_events CHECK/type; do not COALESCE NULL into USD
A3 Clamp rolling raw to USD 10^20 cap “to reuse #553 SQL” Forbidden — truncates honest 18-dec volume
A4 Zero total_volume_usd to “heal” overflow Forbidden (#553 / #577 A5)
A5 SQL injection via window labels Rolling SQL stays bound timestamps only; no user window string concat
A6 Statement timeout mid-tx Keep one transaction for rolling UPDATE + idle zero-out; failed refresh logs and retries next loop (no torn 24h vs 30d)
A7 Leaderboard sort on overflowed/NULL leftovers After R1, huge raw volume sorts as integer, not NULL-first
A8 Future block_timestamp inflating the window Same as #577 A1: do not clamp ingest with Utc::now(); document
A9 Pair-scoped GET SUM(offer_amount) unbounded Out of scope for stored columns; must not 500 the unscoped rollup refresh

Verification criteria

  1. make verify-issue-<iid>: cd indexer && cargo test --test indexer_volume_window_decay --test volume_usd_catalog --test api_traders -- --test-threads=1 plus the new overflow test after make setup-indexer-postgres if needed.
  2. Direct SQL: \d traders shows volume_24h / volume_7d / volume_30d / total_volume as numeric(38,0); total_volume_usd still numeric(38,18).
  3. Fixture I1: refresh_rolling_volumes returns Ok(()); aggregator loop does not log the rolling-trader error for that dataset.
  4. #577 decay tests still pass.

Out of scope

  • Indexer Coolify auto-deploy / /health git SHA (#1276).
  • Re-opening #577 decay work or changing trailing-window copy (#576).
  • Pair-scoped leaderboard GET sums (#666).
  • Position tracker #676 types.
  • USD formula / P522-Q ingest.
  • Frontend compact-format of raw rolling fields (API-only today, #553).

First-pass model recommendation

Recommendation: grok-high

Rationale: This is a schema migration on traders raw-volume columns plus aggregator SQL and overflow/decay tests. Composer’s local-edit bar fails on migration and on cross-check with #548 / #577 / #553 type caps. Wrong cap (10^20 USD vs 10^38-1 integer) would silently truncate 18-decimal volume. Risk is data-integrity on leaderboard windows, not a three-file helper. Verify with the new 10^21 refresh test plus existing indexer_volume_window_decay and api_traders.

## Summary `volume_aggregator` logs **Failed to refresh rolling trader volumes** and leaves `traders.volume_24h` / `volume_7d` / `volume_30d` stale whenever a sender’s trailing-window `SUM(swap_events.offer_amount)` cannot fit **`NUMERIC(38, 18)`**. This is the same leftover type as pre-#548 global/pair raw volume: PostgreSQL `NUMERIC(38, 18)` only stores `|x| < 10^20`. Raw 18-decimal CW20 notionals (~100 human USTR / CL8Y) already hit that bound. Pair and global rollups were widened to **`NUMERIC(38, 0)`** in `20260817120000_*`. Trader **rolling** columns were renamed from `volume_*_usd` in `20260310000002_*` and **never widened**. Positions/P&L went to `NUMERIC(78, 18)` in #676; this path is still the old `(38, 18)` raw-sum columns. Not a +1 on: | Issue | Why it is not this ticket | | --- | --- | | [#577](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/577) | Trailing-window **decay**. Landed. Constraint A3 kept `LEAST(…, POWER(10,38)-1)`, which does **not** fit `(38, 18)`. | | [#548](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/548) | Global/pair USD census + `NUMERIC(38, 0)` for **those** raw columns. Trader rolling left behind. | | [#676](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/676) | `trader_positions` / P&L storage, not rolling volume refresh. | | [#1276](https://git.cl8y.com/code/cl8y-dex-terraclassic/issues/1276) | Indexer auto-deploy + `/health` git SHA. Overflow is **out of scope** there. | ## Current codebase ### Refresh path that errors [`refresh_all_volume_windows`](indexer/src/indexer/volume_aggregator.rs) calls [`traders::refresh_rolling_volumes`](indexer/src/db/queries/traders.rs). On `sqlx::Error` the 5-minute loop logs `Failed to refresh rolling trader volumes` (`startup = false` → **error**, GitLab #577 **D5**). Token / pair / global refresh in the same function can still succeed; trader windows stay at the last good stamp. ```sql LEAST(SUM(CASE WHEN block_timestamp >= $1 THEN offer_amount ELSE 0 END), POWER(10::numeric, 38) - 1) ``` `LEAST` to `10^38-1` is the **integer** cap used after #548 for `NUMERIC(38, 0)` destinations (`token_volume_stats.volume`, `pair_volume_24h.volume_quote`, `global_stats_24h.total_volume`). Assigning that value into **`NUMERIC(38, 18)`** still overflows: integer width is only 20 digits. `offer_amount` is already `NUMERIC(38, 0)` on `swap_events`. The overflow is the **assignment** into `traders.volume_*`, not the `SUM` itself. ### Column types | Column | Origin | Type today | | --- | --- | --- | | `swap_events.offer_amount` | initial schema | `NUMERIC(38, 0)` | | `global_stats_24h.total_volume`, `pair_volume_24h.volume_quote` | `20260817120000_*` (#548) | `NUMERIC(38, 0)` | | `traders.volume_24h` / `7d` / `30d` | renamed from `volume_*_usd` | **`NUMERIC(38, 18)`** | | `traders.total_volume` | renamed from original `total_volume_usd` | **`NUMERIC(38, 18)`** | | `traders.total_volume_usd` | `20260818140000_*` (#553) | `NUMERIC(38, 18)` USD — **keep**; cap is `10^20 - 10^-18` | [`upsert_trader`](indexer/src/db/queries/traders.rs) adds raw `offer_amount` into `total_volume` **without** a `LEAST` cap. Lifetime raw volume can overflow on ingest even when the aggregator is not running. Bundle that column in the same widen. Existing test [`raw_18_decimal_volume_does_not_overflow_global_stats`](indexer/tests/volume_usd_catalog.rs) inserts `10^21` raw USTR and asserts global/pair refresh. It does **not** call `refresh_rolling_volumes`. Decay tests in [`indexer_volume_window_decay.rs`](indexer/tests/indexer_volume_window_decay.rs) use 6-decimal seed sizes that never hit `10^20`. ### What already works - Idle traders **zero** rolling columns when absent from the 30d sender set (#577 **D2**). Keep that second `UPDATE`; do not touch lifetime columns there. - USD lifetime (`total_volume_usd`) uses the `(38, 18)` / `10^20` cap on purpose. Do not widen USD. - Pair-scoped leaderboard `SUM(se.offer_amount)` is computed on GET, not stored in `traders.volume_*`. ## Why the new implementation is needed 1. One 18-decimal trader in the 30d window **fails the whole rolling UPDATE**. Every other trader’s 24h/7d/30d ranks freeze until the next successful refresh (which never comes while that sender still has huge raw volume). 2. #577 decay cannot run if the first statement in the transaction errors (single tx; commit skipped). Ghost ranks from leftover windows return. 3. #548 already documented this overflow class and fixed sibling rollups. Trader rolling is the remaining raw-sum `(38, 18)` hole on the aggregator loop. ## Constraints / guardrails 1. **Widen raw rolling (+ lifetime raw) to `NUMERIC(38, 0)`**, matching `offer_amount` / `pair_volume_24h.volume_quote` / `global_stats_24h.total_volume`. Do **not** switch rolling volume to `NUMERIC(78, 18)` (#676 is inventory/P&L, not integer offer sums). 2. Keep `LEAST(SUM(…), POWER(10::numeric, 38) - 1)` on rolling sums **after** the destination is `(38, 0)`. Do not “fix” overflow by clamping rolling raw volume to `10^20 - 10^-18` (that is the **USD** cap; it truncates ~100 human 18-dec tokens). 3. **Do not zero or rewrite** `total_volume` / `total_volume_usd` / `total_trades` in the idle-trader `UPDATE` (#577 **D2** / #553). 4. Token stats stay **offer-side**. Pair/global/USD ingest (`volume_usd_for_swap`, P522-Q) unchanged. 5. Decay tests stay **V3**: mutate `block_timestamp`, no 24h wall-clock sleeps. 6. No live `SUM(swap_events)` on trader GET for unscoped rolling windows (keep the rollup). 7. `upsert_trader` must not numeric-overflow on a single 18-dec swap once `total_volume` is `(38, 0)`; still cap additive lifetime raw at `10^38-1` if a running sum could exceed the type. 8. Do not fold this into #1276 (health/SHA) or reopen #577. ## Relevant files | Area | Path | | --- | --- | | Aggregator loop | `indexer/src/indexer/volume_aggregator.rs` | | Rolling SQL + upsert | `indexer/src/db/queries/traders.rs` | | Schema | new migration under `indexer/migrations/` (widen `traders.volume_24h`, `volume_7d`, `volume_30d`, `total_volume`) | | Sibling pattern | `indexer/migrations/20260817120000_backfill_swap_volume_usd_catalog.sql` | | Decay regression | `indexer/tests/indexer_volume_window_decay.rs` | | Overflow fixture | `indexer/tests/volume_usd_catalog.rs` (`10^21` raw USTR) | | API strings | `indexer/src/api/traders.rs` (`bd` plain decimal; no `1e+19`) | | Docs / skill | `docs/indexer-invariants.md` (trailing window decay row), `skills/AGENTS_INDEXER_VOLUME_WINDOW_DECAY.md` | ## Recommended direction 1. **Migration:** `ALTER TABLE traders ALTER COLUMN volume_24h TYPE NUMERIC(38, 0)` (same for `volume_7d`, `volume_30d`, `total_volume`). Comment that these are raw `offer_amount` sums, not USD. 2. **SQL:** keep the existing `LEAST(…, POWER(10,38)-1)` on rolling `SUM`s. Add the same cap on `upsert_trader` `total_volume = traders.total_volume + $2`. 3. **Test:** seed a trader with `offer_amount = 10^21` inside 24h; `refresh_rolling_volumes` returns `Ok`; stored `volume_24h` equals the raw sum (or `10^38-1` only if the fixture exceeds that). Repeat for 7d/30d CASE branches. Assert `total_volume_usd` unchanged / still `(38, 18)`-legal. 4. **Decay:** reuse #577 I4/I5 after the huge-sum fixture ages past 30d → rolling columns 0, lifetime raw intact. 5. **`make verify-issue-<iid>`** plus an invariants/skill sentence that rolling raw volume is `(38, 0)` and must not fail the aggregator. ## Acceptance criteria - [ ] **R1** Trader with a single in-window swap of `10^21` raw offer: `refresh_rolling_volumes` succeeds; `volume_24h` stores that integer (plain decimal JSON, no scientific notation). - [ ] **R2** Mixed 6-dec + 18-dec senders in one refresh: no overflow; 6-dec rolling values unchanged vs today’s decay tests. - [ ] **R3** Idle 30d+ trader still zeros rolling columns only (#577 **D2**). - [ ] **R4** `upsert_trader` with `10^21` raw does not fail; `total_volume` holds the running raw sum (`LEAST` at `10^38-1`). - [ ] **R5** `total_volume_usd` remains `NUMERIC(38, 18)` with the existing `10^20` USD cap. Unpriced stays NULL. - [ ] **R6** Docs/skill + `make verify-issue-<iid>`. - [ ] **R7** Token / pair / global refresh in `refresh_all_volume_windows` still run when trader refresh would previously have been the only failure (after the fix, trader refresh must not be the failure). ### Given / When / Then **Given** `traders.volume_24h` (and 7d/30d) are populated from `SUM(swap_events.offer_amount)` and a sender has ≥ `10^20` raw offer in the trailing window **When** `volume_aggregator` runs `refresh_rolling_volumes` (startup or 5-minute loop) **Then** the statement commits without numeric overflow, rolling columns equal the capped raw integer, #577 zero-out still applies to idle senders, and USD lifetime columns are not rewritten **Given** a new 18-decimal swap of `10^21` raw offer **When** `upsert_trader` adds that amount to `total_volume` **Then** ingest succeeds and `total_volume` is the raw running sum (capped at `10^38-1`), not a `(38, 18)` overflow ## Test plan (functional paths) | ID | Case | Expect | | --- | --- | --- | | I1 | 24h swap `offer_amount = 10^21` | `refresh_rolling_volumes` Ok; `volume_24h = 10^21` | | I2 | Same swap aged 25h | `volume_24h = 0`; `volume_7d = 10^21` | | I3 | Aged 31d | all three rolling 0; `total_volume` still includes the swap | | I4 | Two senders: 6-dec seed + 18-dec `10^21` | both refresh; 6-dec matches decay fixture | | I5 | Empty DB / no swaps | zeros, no error | | I6 | `upsert_trader` `10^21` then refresh | no overflow on either path | | I7 | Leaderboard `sort=volume_24h` | 18-dec sender ranks by raw integer; JSON plain string | | I8 | `refresh_all_volume_windows(..., false)` | no `Failed to refresh rolling trader volumes` for I1 fixture | | I9 | USD: priced 6-dec swap | `total_volume_usd` still `(38, 18)` legal; I1 18-dec raw does not smash USD | ## Test plan (attack, hack, and abuse) | ID | Vector | Expect | | --- | --- | --- | | A1 | Hostile LCD / crafted `offer_amount` at `10^38` | `LEAST` to `10^38-1`; no panic; no wrap to negative | | A2 | Negative or non-finite offer (should be rejected at ingest) | Rolling SUM ignores invalid rows already rejected by `swap_events` CHECK/type; do not `COALESCE` NULL into USD | | A3 | Clamp rolling raw to USD `10^20` cap “to reuse #553 SQL” | **Forbidden** — truncates honest 18-dec volume | | A4 | Zero `total_volume_usd` to “heal” overflow | **Forbidden** (#553 / #577 A5) | | A5 | SQL injection via window labels | Rolling SQL stays bound timestamps only; no user `window` string concat | | A6 | Statement timeout mid-tx | Keep one transaction for rolling UPDATE + idle zero-out; failed refresh logs and retries next loop (no torn 24h vs 30d) | | A7 | Leaderboard sort on overflowed/NULL leftovers | After R1, huge raw volume sorts as integer, not NULL-first | | A8 | Future `block_timestamp` inflating the window | Same as #577 A1: do not clamp ingest with `Utc::now()`; document | | A9 | Pair-scoped GET `SUM(offer_amount)` unbounded | Out of scope for stored columns; must not 500 the unscoped rollup refresh | ## Verification criteria 1. `make verify-issue-<iid>`: `cd indexer && cargo test --test indexer_volume_window_decay --test volume_usd_catalog --test api_traders -- --test-threads=1` plus the new overflow test after `make setup-indexer-postgres` if needed. 2. Direct SQL: `\d traders` shows `volume_24h` / `volume_7d` / `volume_30d` / `total_volume` as `numeric(38,0)`; `total_volume_usd` still `numeric(38,18)`. 3. Fixture I1: `refresh_rolling_volumes` returns `Ok(())`; aggregator loop does not log the rolling-trader error for that dataset. 4. #577 decay tests still pass. ## Out of scope - Indexer Coolify auto-deploy / `/health` git SHA (#1276). - Re-opening #577 decay work or changing trailing-window copy (#576). - Pair-scoped leaderboard GET sums (#666). - Position tracker `#676` types. - USD formula / P522-Q ingest. - Frontend compact-format of raw rolling fields (API-only today, #553). ## First-pass model recommendation Recommendation: grok-high Rationale: This is a schema migration on `traders` raw-volume columns plus aggregator SQL and overflow/decay tests. Composer’s local-edit bar fails on **migration** and on cross-check with #548 / #577 / #553 type caps. Wrong cap (`10^20` USD vs `10^38-1` integer) would silently truncate 18-decimal volume. Risk is data-integrity on leaderboard windows, not a three-file helper. Verify with the new `10^21` refresh test plus existing `indexer_volume_window_decay` and `api_traders`.
Author
Owner

cl8y-agent-control: queued design_author job 6b144b5a-6583-4d3f-9253-15d40333c589 (not executed; no Hetzner VM).

cl8y-agent-control: queued `design_author` job `6b144b5a-6583-4d3f-9253-15d40333c589` (not executed; no Hetzner VM).
Author
Owner

Merged as PR #1292. Rolling trader volume columns are NUMERIC(38, 0).

Leftover: Coolify indexer migrate of 20260921120000_traders_rolling_volume_numeric_38_0 (before #1263's 20260921120001_pair_volume_30d). Tracked on #1300.

Merged as PR #1292. Rolling trader volume columns are NUMERIC(38, 0). Leftover: Coolify indexer migrate of `20260921120000_traders_rolling_volume_numeric_38_0` (before #1263's `20260921120001_pair_volume_30d`). Tracked on #1300.
Author
Owner

Follow-up heal PR #1303 merged (migrate + gated poller before D5). First compile on this workstation failed; #1304 landed &mut **tx.

make verify-issue-1277 6/6 after #1304. R1–R7 remain on main via #1292.

Operator leftover: Coolify apply 20260921130000_traders_lifetime_heal_from_swaps (after the widen + pair 30d versions on #1300). See #1305.

Follow-up heal PR #1303 merged (migrate + gated poller before D5). First compile on this workstation failed; #1304 landed `&mut **tx`. `make verify-issue-1277` 6/6 after #1304. R1–R7 remain on main via #1292. Operator leftover: Coolify apply `20260921130000_traders_lifetime_heal_from_swaps` (after the widen + pair 30d versions on #1300). See #1305.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#1277
No description provided.