RPC + indexer architecture for frontend reads (concept + design) #216

Closed
opened 2026-05-19 05:57:27 +00:00 by Brouie · 17 comments
Brouie commented 2026-05-19 05:57:27 +00:00 (Migrated from gitlab.com)

Concept and design doc for the polling-reduction work this week. Filing this so you can review and modify before I touch any code.

Architecture

  • Indexer for global state — leaderboards, latest buys, warbow, sale stats, contract addresses
  • RPC for wallet state — CL8Y balance, cooldown, per-user reads where the UX actually needs sub-second freshness
  • Default for wallet-state RPC reads is load once on page load or wallet event, polling only when justified by UX
  • WSS deferred until the basics are efficient — the failure modes are too different from RPC to drop in as a replacement right now
  • realtime_sendRawTransaction wired into user write flows (approve, buy, steal, guard, revenge) to kill receipt polling — drop-in replacement for eth_sendRawTransaction that returns the receipt in one call, 10s timeout falls back to normal eth_getTransactionReceipt polling

What I found mapping the codebase

Frontend is already calling indexer routes for most global state. The polling problem isn't that everything is on RPC — it's that for several reads we're running indexer AND a redundant RPC poll at the same time.

Routes that exist on indexer today and are already being called from frontend:

  • /v1/timecurve/podiums (used in usePodiumReads)
  • /v1/timecurve/warbow/leaderboard
  • /v1/timecurve/warbow/refresh-candidates
  • /v1/timecurve/warbow/battle-feed
  • /v1/timecurve/warbow/pending-revenge
  • /v1/timecurve/warbow/guard-latest
  • /v1/timecurve/chain-timer
  • /v1/timecurve/buys
  • /v1/timecurve/buyer-stats
  • /v1/timecurve/charm-redemptions
  • /v1/fee-router/fees-distributed
  • /v1/referrals/referrer-leaderboard
  • /v1/referrals/wallet-charm-summary

Three classes of polling work I see

1. Reads that should drop RPC entirely (run indexer-only)

  • warbowPodiumLive.ts:127 polls battlePoints per leaderboard buyer at 1s — but /v1/timecurve/warbow/leaderboard already returns the leaderboard. The 1s RPC overlay just races against fresh indexer data.
  • usePodiumReads.ts:219 polls top-3 podium BP at 1s — same overlay pattern.
  • useTimeCurveArenaModel.tsx:1938 polls steal candidate BP/stealsToday/guardUntil at 12s — refresh-candidates indexer route exists.

For these we can stop running both. Indexer becomes the source, RPC drops out unless we need a verified read for a specific UX moment.

2. The one read with no indexer route

useTimeCurveSaleSession.ts:356 coreContracts — total raise, current min/max buy, charm price. Polls 10+ functions at 1s. No /v1/timecurve/sale-state or similar route exists today.

Options:

  • Add an indexer sale-state route and migrate
  • Leave on RPC, split the immutables out (REFERRAL_EACH_BPS, PRESALE_CHARM_WEIGHT_BPS, doubPresaleVesting, saleStart, deadline — none change after deploy, currently polled every second for no reason), and drop the mutable poll to a longer floor

3. Wallet state — load once by default

useTimeCurveSaleSession.ts:378 polls charmWeight, charmsRedeemed, nextBuyAllowedAt, activeDefendedStreak at 1s.

Per the framing — default to load once on page load or wallet event. Cases:

  • CL8Y balance — load once on connect, refetch on Buy/Transfer for connected address
  • charmWeight / charmsRedeemed / activeDefendedStreak — load once, refetch on Buy/Redeem/Defend events for connected address
  • nextBuyAllowedAt — this one might still need a 1s poll WHEN cooldown is active, since the CTA enable/disable needs sub-second precision near zero. Possibly: compute CTA enable from a chain-time clock and only RPC-refresh the cooldown timestamp on Buy events. Open question for you.

Defensive fix that doesn't depend on any of the above

9 polling sites are using fixed refetchInterval that bypass getRpcBackoffPollMs — during a 429 storm they keep hammering while the rest of the app correctly backs off. Wiring them into useRpcQueryHealthForRefetch is cheap and lands before any of the migration work.

Sites: useTimeCurveSaleSession.ts:356,378 — warbowPodiumLive.ts:127 — usePodiumReads.ts:190,219 — useTimeCurveArenaModel.tsx:1938 — ReferralProgramEarningsSection.tsx:29 — ReferralRegisterSection.tsx:131,169

Most of these become irrelevant once the read drops RPC entirely, but the fix is independently correct.

Production config finding from this morning

rpc-megaeth-mainnet.globalstake.io in VITE_RPC_URL is dead — CORS preflight fails (no Access-Control-Allow-Origin header), some calls 503. Every fallback cascade through it is wasted. Pulling it from the env var (or replacing it) is config-only.

Suggested rollout

  1. Pull globalstake from VITE_RPC_URL
  2. Wire the 9 fixed-interval pollers into useRpcQueryHealthForRefetch
  3. Drop the RPC overlays for reads where the indexer already has the data — warbowPodiumLive, usePodiumReads:219, useTimeCurveArenaModel:1938
  4. Either add a sale-state indexer route, or split immutables out of useTimeCurveSaleSession and tighten mutables
  5. Flip wallet-state reads to load-once + event-driven refetch by default, case-by-case where polling is justified
  6. Wire realtime_sendRawTransaction for write flows

Open questions

  • For the warbow reads where RPC and indexer both run — is the RPC overlay there for a reason (verification at submit time, freshness guarantee at a specific UX moment) or is it legacy?
  • For sale state — prefer a new indexer route, or keep on RPC with tighter cadence?
  • For nextBuyAllowedAt — chain-time clock + event refetch, or keep 1s poll while cooldown is active?
  • realtime_sendRawTransaction — confirmed it's on mainnet.megaeth.com/rpc?
  • Default for wallet-state load-once — page load only, or also on wallet account switch and chain switch?
Concept and design doc for the polling-reduction work this week. Filing this so you can review and modify before I touch any code. ## Architecture - Indexer for global state — leaderboards, latest buys, warbow, sale stats, contract addresses - RPC for wallet state — CL8Y balance, cooldown, per-user reads where the UX actually needs sub-second freshness - Default for wallet-state RPC reads is load once on page load or wallet event, polling only when justified by UX - WSS deferred until the basics are efficient — the failure modes are too different from RPC to drop in as a replacement right now - realtime_sendRawTransaction wired into user write flows (approve, buy, steal, guard, revenge) to kill receipt polling — drop-in replacement for eth_sendRawTransaction that returns the receipt in one call, 10s timeout falls back to normal eth_getTransactionReceipt polling ## What I found mapping the codebase Frontend is already calling indexer routes for most global state. The polling problem isn't that everything is on RPC — it's that for several reads we're running indexer AND a redundant RPC poll at the same time. Routes that exist on indexer today and are already being called from frontend: - /v1/timecurve/podiums (used in usePodiumReads) - /v1/timecurve/warbow/leaderboard - /v1/timecurve/warbow/refresh-candidates - /v1/timecurve/warbow/battle-feed - /v1/timecurve/warbow/pending-revenge - /v1/timecurve/warbow/guard-latest - /v1/timecurve/chain-timer - /v1/timecurve/buys - /v1/timecurve/buyer-stats - /v1/timecurve/charm-redemptions - /v1/fee-router/fees-distributed - /v1/referrals/referrer-leaderboard - /v1/referrals/wallet-charm-summary ## Three classes of polling work I see ### 1. Reads that should drop RPC entirely (run indexer-only) - warbowPodiumLive.ts:127 polls battlePoints per leaderboard buyer at 1s — but /v1/timecurve/warbow/leaderboard already returns the leaderboard. The 1s RPC overlay just races against fresh indexer data. - usePodiumReads.ts:219 polls top-3 podium BP at 1s — same overlay pattern. - useTimeCurveArenaModel.tsx:1938 polls steal candidate BP/stealsToday/guardUntil at 12s — refresh-candidates indexer route exists. For these we can stop running both. Indexer becomes the source, RPC drops out unless we need a verified read for a specific UX moment. ### 2. The one read with no indexer route useTimeCurveSaleSession.ts:356 coreContracts — total raise, current min/max buy, charm price. Polls 10+ functions at 1s. No /v1/timecurve/sale-state or similar route exists today. Options: - Add an indexer sale-state route and migrate - Leave on RPC, split the immutables out (REFERRAL_EACH_BPS, PRESALE_CHARM_WEIGHT_BPS, doubPresaleVesting, saleStart, deadline — none change after deploy, currently polled every second for no reason), and drop the mutable poll to a longer floor ### 3. Wallet state — load once by default useTimeCurveSaleSession.ts:378 polls charmWeight, charmsRedeemed, nextBuyAllowedAt, activeDefendedStreak at 1s. Per the framing — default to load once on page load or wallet event. Cases: - CL8Y balance — load once on connect, refetch on Buy/Transfer for connected address - charmWeight / charmsRedeemed / activeDefendedStreak — load once, refetch on Buy/Redeem/Defend events for connected address - nextBuyAllowedAt — this one might still need a 1s poll WHEN cooldown is active, since the CTA enable/disable needs sub-second precision near zero. Possibly: compute CTA enable from a chain-time clock and only RPC-refresh the cooldown timestamp on Buy events. Open question for you. ## Defensive fix that doesn't depend on any of the above 9 polling sites are using fixed refetchInterval that bypass getRpcBackoffPollMs — during a 429 storm they keep hammering while the rest of the app correctly backs off. Wiring them into useRpcQueryHealthForRefetch is cheap and lands before any of the migration work. Sites: useTimeCurveSaleSession.ts:356,378 — warbowPodiumLive.ts:127 — usePodiumReads.ts:190,219 — useTimeCurveArenaModel.tsx:1938 — ReferralProgramEarningsSection.tsx:29 — ReferralRegisterSection.tsx:131,169 Most of these become irrelevant once the read drops RPC entirely, but the fix is independently correct. ## Production config finding from this morning rpc-megaeth-mainnet.globalstake.io in VITE_RPC_URL is dead — CORS preflight fails (no Access-Control-Allow-Origin header), some calls 503. Every fallback cascade through it is wasted. Pulling it from the env var (or replacing it) is config-only. ## Suggested rollout 1. Pull globalstake from VITE_RPC_URL 2. Wire the 9 fixed-interval pollers into useRpcQueryHealthForRefetch 3. Drop the RPC overlays for reads where the indexer already has the data — warbowPodiumLive, usePodiumReads:219, useTimeCurveArenaModel:1938 4. Either add a sale-state indexer route, or split immutables out of useTimeCurveSaleSession and tighten mutables 5. Flip wallet-state reads to load-once + event-driven refetch by default, case-by-case where polling is justified 6. Wire realtime_sendRawTransaction for write flows ## Open questions - For the warbow reads where RPC and indexer both run — is the RPC overlay there for a reason (verification at submit time, freshness guarantee at a specific UX moment) or is it legacy? - For sale state — prefer a new indexer route, or keep on RPC with tighter cadence? - For nextBuyAllowedAt — chain-time clock + event refetch, or keep 1s poll while cooldown is active? - realtime_sendRawTransaction — confirmed it's on mainnet.megaeth.com/rpc? - Default for wallet-state load-once — page load only, or also on wallet account switch and chain switch?
PlasticDigits commented 2026-05-19 06:09:17 +00:00 (Migrated from gitlab.com)

mentioned in issue #210

mentioned in issue #210
PlasticDigits commented 2026-05-19 06:14:32 +00:00 (Migrated from gitlab.com)

warbow rpc reads are legacy and need to be removed, freshness is not critical on warbow leaderboards/steals/bp (indexer is fast enough for it)

for sale state - need new indexer route

realtime_sendRawTransaction - yes on mainnet.megaeth.com/rpc and other rpc as well

Also do wallet state load on account switch but not chain switch

warbow rpc reads are legacy and need to be removed, freshness is not critical on warbow leaderboards/steals/bp (indexer is fast enough for it) for sale state - need new indexer route realtime_sendRawTransaction - yes on mainnet.megaeth.com/rpc and other rpc as well Also do wallet state load on account switch but not chain switch
PlasticDigits commented 2026-05-19 06:16:25 +00:00 (Migrated from gitlab.com)

nextBuyAllowedAt - We do not need to poll because when a user buys, the final epoch where the button should unlock (epoch+3 seconds for reducing failed tx due to outdated rpc data) doesnt change once its registered onchain

nextBuyAllowedAt - We do not need to poll because when a user buys, the final epoch where the button should unlock (epoch+3 seconds for reducing failed tx due to outdated rpc data) doesnt change once its registered onchain
PlasticDigits commented 2026-05-19 06:17:30 +00:00 (Migrated from gitlab.com)

On (2) migrate to indexer fully and drop rpc local calls

On (2) migrate to indexer fully and drop rpc local calls
PlasticDigits commented 2026-05-19 06:19:34 +00:00 (Migrated from gitlab.com)

Also for cl8y balance, we should allow user to manually update with one rpc call, with a refresh button they can click, inline with the cl8y balance

Also for cl8y balance, we should allow user to manually update with one rpc call, with a refresh button they can click, inline with the cl8y balance
PlasticDigits commented 2026-05-19 06:26:05 +00:00 (Migrated from gitlab.com)

Plan additions (review feedback)

Before implementation, please fold the following into the written plan / acceptance criteria.

1. Doc / invariant / skills pass in the same epic (especially WarBow)

INV-FRONTEND-WARBOW-PODIUM-LIVE, docs/frontend/timecurve-views.md (live WarBow podium), yieldomega guardrails, and warbowPodiumLive tests currently require the RPC battlePoints overlay and ~1s RPC poll for displayed BP.

Removing that overlay is the right call for launch RPC load, but it is not a code-only delete: update invariants, timecurve-views, guardrails skill, and tests in the same workstream. Reframe acceptance as: trust indexer WARBOW_BP_OBSERVATIONS_UNION + existing event invalidation (#182); drop RPC overlay; verify steal → revenge → flag moves top-3 on indexer-only within normal indexer poll cadence.

2. GET /v1/timecurve/sale-state — short spec before build

useTimeCurveSaleSession coreContracts is a large ~1s multicall batch. Before indexer + frontend work, nail down in the issue or a linked note:

  • Field list — mirror what Simple/Arena actually consume (saleStart, deadline, ended, bounds/price views, totalRaised, timer params, gate flags, router, etc.).
  • Anchoring — as_of_block / as_of_timestamp (same head pattern as chain-timer).
  • Derivation — head RPC snapshot vs replay from indexed events (views like currentCharmBoundsWad affect choice).
  • No-indexer fallback — when VITE_INDEXER_URL unset, keep RPC like usePodiumReads today.
  • Schema — SCHEMA_VERSION bump + invariants row; staleness/disclosure if we surface “indexed snapshot.”

Display vs submit carve-out (updates timecurve-views invariant #1): display polling moves to indexer; submit paths keep fresh RPC (readFreshTimeCurveBuySizing, WarBow preflight at click) per #82 / #101.

3. Arena steal preflight — refresh-candidates alone is insufficient

The 12s multicall at useTimeCurveArenaModel loads per-candidate battlePoints, stealsReceivedOnDay, and warbowGuardUntil. GET /v1/timecurve/warbow/refresh-candidates returns addresses only (operator tooling).

For RPC removal on steal hero rows, plan explicitly:

  • BP — warbow/leaderboard (drop BP overlay).
  • Steals today — warbow/steals-by-victim-day (wire or batch).
  • Guard — warbow/guard-latest per candidate or a batch route.
  • Submit — fresh RPC / multicall in describeStealPreflight at steal click (same pattern as buy sizing), not indexer-only at write time.

Guardrails / invariants / skills — intentional guarantee shift

This design follows new operational reality at launch: public RPC overload from redundant polling, not a belief that indexer is “more authoritative” than chain state in the abstract.

We are weakening the previous doc stance (contract RPC = primary for global sale/WarBow display) to indexer-primary for display polling, with RPC retained only where safety requires it (wallet-scoped reads, manual refresh, submit-time sizing/preflight).

That is a deliberate trade: slightly weaker “digits always match eth_call right now” on global UI, in exchange for sustainable RPC use. Guardrails, docs/testing/invariants-and-business-logic.md, docs/frontend/timecurve-views.md, and relevant skills must be updated in the same epic so agents and reviewers do not treat the old RPC-primary rules as blocking this work.

## Plan additions (review feedback) Before implementation, please fold the following into the written plan / acceptance criteria. ### 1. Doc / invariant / skills pass in the same epic (especially WarBow) `INV-FRONTEND-WARBOW-PODIUM-LIVE`, `docs/frontend/timecurve-views.md` (live WarBow podium), yieldomega guardrails, and `warbowPodiumLive` tests currently **require** the RPC `battlePoints` overlay and ~1s RPC poll for displayed BP. Removing that overlay is the right call for launch RPC load, but it is **not** a code-only delete: update **invariants**, **timecurve-views**, **guardrails skill**, and tests in the same workstream. Reframe acceptance as: trust indexer `WARBOW_BP_OBSERVATIONS_UNION` + existing event invalidation (#182); drop RPC overlay; verify steal → revenge → flag moves top-3 on indexer-only within normal indexer poll cadence. ### 2. `GET /v1/timecurve/sale-state` — short spec before build `useTimeCurveSaleSession` `coreContracts` is a large ~1s multicall batch. Before indexer + frontend work, nail down in the issue or a linked note: - **Field list** — mirror what Simple/Arena actually consume (`saleStart`, `deadline`, `ended`, bounds/price views, `totalRaised`, timer params, gate flags, router, etc.). - **Anchoring** — `as_of_block` / `as_of_timestamp` (same head pattern as `chain-timer`). - **Derivation** — head RPC snapshot vs replay from indexed events (views like `currentCharmBoundsWad` affect choice). - **No-indexer fallback** — when `VITE_INDEXER_URL` unset, keep RPC like `usePodiumReads` today. - **Schema** — `SCHEMA_VERSION` bump + invariants row; staleness/disclosure if we surface “indexed snapshot.” **Display vs submit carve-out** (updates `timecurve-views` invariant #1): **display** polling moves to indexer; **submit** paths keep fresh RPC (`readFreshTimeCurveBuySizing`, WarBow preflight at click) per #82 / #101. ### 3. Arena steal preflight — `refresh-candidates` alone is insufficient The 12s multicall at `useTimeCurveArenaModel` loads per-candidate `battlePoints`, `stealsReceivedOnDay`, and `warbowGuardUntil`. `GET /v1/timecurve/warbow/refresh-candidates` returns **addresses only** (operator tooling). For RPC removal on steal hero rows, plan explicitly: - **BP** — `warbow/leaderboard` (drop BP overlay). - **Steals today** — `warbow/steals-by-victim-day` (wire or batch). - **Guard** — `warbow/guard-latest` per candidate or a batch route. - **Submit** — fresh RPC / multicall in `describeStealPreflight` at steal click (same pattern as buy sizing), not indexer-only at write time. --- ### Guardrails / invariants / skills — intentional guarantee shift This design follows **new operational reality at launch**: public RPC overload from redundant polling, not a belief that indexer is “more authoritative” than chain state in the abstract. We are **weakening** the previous doc stance (**contract RPC = primary for global sale/WarBow display**) to **indexer-primary for display polling**, with **RPC retained only where safety requires it** (wallet-scoped reads, manual refresh, submit-time sizing/preflight). That is a deliberate trade: slightly weaker “digits always match `eth_call` right now” on global UI, in exchange for sustainable RPC use. **Guardrails, `docs/testing/invariants-and-business-logic.md`, `docs/frontend/timecurve-views.md`, and relevant skills must be updated in the same epic** so agents and reviewers do not treat the old RPC-primary rules as blocking this work.
PlasticDigits commented 2026-05-19 10:29:03 +00:00 (Migrated from gitlab.com)

mentioned in commit 3e42616af4

mentioned in commit 3e42616af4894642913d909865c89e08f0586f5a
PlasticDigits commented 2026-05-19 10:29:30 +00:00 (Migrated from gitlab.com)

Implementation summary (@Brouie — please verify)

Pushed to main in commit 3e42616 (feat(frontend,indexer): indexer-primary TimeCurve reads and RPC poll reduction (#216)).

This closes the #216 architecture epic per your thread decisions: indexer-primary global display, RPC for wallet + submit safety, no redundant WarBow/podium BP overlays, realtime_sendRawTransaction on writes.

What shipped

Area Change
Indexer New GET /v1/timecurve/sale-state (schema 1.23.0), head-RPC batch at same block as chain-timer
Simple sale session coreContracts from indexer when VITE_INDEXER_URL set; RPC multicall fallback when unset
WarBow display Removed ~1s RPC battlePoints overlays (warbowPodiumLive, usePodiumReads, arena leaderboard overlay)
Arena steal hero Indexer: leaderboard BP + steals-by-victim-day + guard-latest; fresh RPC preflight at steal click
Wallet RPC Load once + refetch on account switch (not chain switch); no nextBuyAllowedAt poll
CL8Y balance Load once + inline ↻ manual refresh on Simple
Writes Transport rewrites eth_sendRawTransaction → realtime_sendRawTransaction; waitForWriteReceipt (10s fallback to normal receipt poll) on buy / WarBow / referrals / kumbaya / protocol finalize
RPC hygiene Removed dead globalstake.io fallback; fixed-interval pollers wired to useRpcQueryHealthForRefetch + backoff
Docs Invariants §216, timecurve-views, guardrails, wallet-connection, megaeth, indexer/design

Known follow-up (non-blocking)

  • Arena still polls a ~1s RPC multicall for core TimeCurve fields (mergedArenaTc) instead of reusing indexer sale-state like Simple — separate optimization if RPC load is still high.

Verification checklist

Environment: production-like stack with VITE_INDEXER_URL, MegaETH RPC (mainnet.megaeth.com/rpc or your comma-separated list — not globalstake.io).

Indexer

  • GET /v1/timecurve/sale-state returns 200 with read_block_number, bounds, total_raised, gate flags (not 503 when chain-timer is warm)
  • GET /v1/status shows schema_version ≥ 1.23.0
  • Sale-state fields move after an onchain buy (within normal chain-timer poll cadence, ~1s)

Simple (/timecurve or Simple view)

  • Hero timer / sale stats update without hammering RPC (Network tab: no ~1s eth_call storm on totalRaised, currentMinBuyAmount, etc. when indexer is on)
  • Connect wallet → charm weight / cooldown / streaks load once; switch account → wallet rows refetch; switch chain only → wallet rows do not refetch
  • ↻ next to CL8Y balance triggers a single balanceOf refresh
  • Buy CTA cooldown: no 1s nextBuyAllowedAt poll; after buy, cooldown wall updates from receipt + refetch
  • Successful buy still uses fresh RPC sizing at submit (readFreshTimeCurveBuySizing)

Arena

  • WarBow leaderboard BP matches indexer (no parallel 1s per-buyer BP overlay)
  • Steal candidate rows show BP / steals today / guard from indexer (~1.5s poll)
  • Steal click runs fresh RPC preflight; reverts with clear message if victim state changed
  • Buy / steal / guard / revenge txs complete (receipt via realtime or fallback poll)

Podiums / WarBow live

  • Top-3 podium BP does not use 1s RPC overlay when indexer on
  • Steal → revenge → flag: top-3 / ladder updates on indexer cadence (no RPC overlay race)

Writes & RPC

  • Submit tx uses realtime_sendRawTransaction on MegaETH RPC (or falls back to receipt polling within ~10s)
  • Referrals register + protocol finalizeWarbowPodium complete
  • Under artificial 429 / RPC errors, pollers back off (not fixed 1s hammering)

No-indexer fallback

  • Unset VITE_INDEXER_URL → Simple still loads sale via RPC multicall (no crash)

Acceptance criteria

  1. Global display for sale state and WarBow podium/steal hero data is indexer-primary when VITE_INDEXER_URL is set; redundant RPC display overlays are removed.
  2. GET /v1/timecurve/sale-state exposes the same fields Simple/Arena consumed from coreContracts, anchored to chain-timer head block.
  3. Wallet-scoped reads default to load once + account-switch refetch; nextBuyAllowedAt is not polled on an interval.
  4. Submit paths retain fresh RPC for buy sizing and WarBow steal preflight at click time.
  5. User writes use realtime_sendRawTransaction with waitForWriteReceipt fallback.
  6. Docs/invariants/guardrails reflect indexer-primary display + submit-time RPC carve-out (not legacy RPC-primary WarBow overlay rules).

Please reply here with pass/fail per section (or file a follow-up issue for gaps). Thanks!

## Implementation summary (@Brouie — please verify) Pushed to **`main`** in commit **`3e42616`** (`feat(frontend,indexer): indexer-primary TimeCurve reads and RPC poll reduction (#216)`). This closes the #216 architecture epic per your thread decisions: **indexer-primary global display**, **RPC for wallet + submit safety**, **no redundant WarBow/podium BP overlays**, **`realtime_sendRawTransaction`** on writes. ### What shipped | Area | Change | |------|--------| | **Indexer** | New **`GET /v1/timecurve/sale-state`** (schema **1.23.0**), head-RPC batch at same block as `chain-timer` | | **Simple sale session** | `coreContracts` from indexer when `VITE_INDEXER_URL` set; RPC multicall fallback when unset | | **WarBow display** | Removed ~1s RPC `battlePoints` overlays (`warbowPodiumLive`, `usePodiumReads`, arena leaderboard overlay) | | **Arena steal hero** | Indexer: leaderboard BP + `steals-by-victim-day` + `guard-latest`; **fresh RPC preflight** at steal click | | **Wallet RPC** | Load once + refetch on **account switch** (not chain switch); **no** `nextBuyAllowedAt` poll | | **CL8Y balance** | Load once + inline **↻** manual refresh on Simple | | **Writes** | Transport rewrites `eth_sendRawTransaction` → `realtime_sendRawTransaction`; **`waitForWriteReceipt`** (10s fallback to normal receipt poll) on buy / WarBow / referrals / kumbaya / protocol finalize | | **RPC hygiene** | Removed dead `globalstake.io` fallback; fixed-interval pollers wired to **`useRpcQueryHealthForRefetch`** + backoff | | **Docs** | Invariants §216, `timecurve-views`, guardrails, `wallet-connection`, `megaeth`, `indexer/design` | ### Known follow-up (non-blocking) - **Arena** still polls a ~1s RPC multicall for core TimeCurve fields (`mergedArenaTc`) instead of reusing indexer `sale-state` like Simple — separate optimization if RPC load is still high. --- ## Verification checklist **Environment:** production-like stack with **`VITE_INDEXER_URL`**, MegaETH RPC (`mainnet.megaeth.com/rpc` or your comma-separated list — **not** `globalstake.io`). ### Indexer - [ ] `GET /v1/timecurve/sale-state` returns **200** with `read_block_number`, bounds, `total_raised`, gate flags (not **503** when chain-timer is warm) - [ ] `GET /v1/status` shows `schema_version` ≥ **1.23.0** - [ ] Sale-state fields move after an onchain buy (within normal chain-timer poll cadence, ~1s) ### Simple (`/timecurve` or Simple view) - [ ] Hero timer / sale stats update without hammering RPC (Network tab: no ~1s `eth_call` storm on `totalRaised`, `currentMinBuyAmount`, etc. when indexer is on) - [ ] Connect wallet → charm weight / cooldown / streaks load **once**; switch **account** → wallet rows refetch; switch **chain only** → wallet rows do **not** refetch - [ ] **↻** next to CL8Y balance triggers a single `balanceOf` refresh - [ ] Buy CTA cooldown: no 1s `nextBuyAllowedAt` poll; after buy, cooldown wall updates from receipt + refetch - [ ] Successful buy still uses fresh RPC sizing at submit (`readFreshTimeCurveBuySizing`) ### Arena - [ ] WarBow leaderboard BP matches indexer (no parallel 1s per-buyer BP overlay) - [ ] Steal candidate rows show BP / steals today / guard from indexer (~1.5s poll) - [ ] Steal click runs fresh RPC preflight; reverts with clear message if victim state changed - [ ] Buy / steal / guard / revenge txs complete (receipt via realtime or fallback poll) ### Podiums / WarBow live - [ ] Top-3 podium BP does not use 1s RPC overlay when indexer on - [ ] Steal → revenge → flag: top-3 / ladder updates on indexer cadence (no RPC overlay race) ### Writes & RPC - [ ] Submit tx uses `realtime_sendRawTransaction` on MegaETH RPC (or falls back to receipt polling within ~10s) - [ ] Referrals register + protocol `finalizeWarbowPodium` complete - [ ] Under artificial 429 / RPC errors, pollers back off (not fixed 1s hammering) ### No-indexer fallback - [ ] Unset `VITE_INDEXER_URL` → Simple still loads sale via RPC multicall (no crash) --- ## Acceptance criteria 1. **Global display** for sale state and WarBow podium/steal **hero** data is **indexer-primary** when `VITE_INDEXER_URL` is set; redundant RPC display overlays are **removed**. 2. **`GET /v1/timecurve/sale-state`** exposes the same fields Simple/Arena consumed from `coreContracts`, anchored to chain-timer head block. 3. **Wallet-scoped** reads default to **load once** + **account-switch** refetch; **`nextBuyAllowedAt`** is **not** polled on an interval. 4. **Submit paths** retain **fresh RPC** for buy sizing and WarBow steal preflight at click time. 5. User writes use **`realtime_sendRawTransaction`** with **`waitForWriteReceipt`** fallback. 6. **Docs/invariants/guardrails** reflect indexer-primary display + submit-time RPC carve-out (**not** legacy RPC-primary WarBow overlay rules). Please reply here with pass/fail per section (or file a follow-up issue for gaps). Thanks!
PlasticDigits commented 2026-05-19 11:09:47 +00:00 (Migrated from gitlab.com)

Follow-up (out of original #216 scope, same architecture)

@Brouie — follow-on work landed on main after the core #216 sale-state / RPC-split PR. This was not in the original issue checklist but addresses the same “indexer + RPC duplicate polls” problem for surfaces called out during mapping.

What shipped

Area Change
Arena useTimeCurveArenaModel uses shared GET /v1/timecurve/sale-state for global core (no ~1 Hz mergedArenaTc core multicall when VITE_INDEXER_URL is set). WarBow policy constants stay on a small load-once RPC bundle.
Indexer schema 1.24.0 Sale-state poll now includes fee_router, fee_router_sinks[5], linear_charm_base_price_wad / linear_charm_daily_increment_wad, plus Arena supplement fields (initial_timer_sec, prizes_distributed, owner).
Simple LinearCharmPrice display from sale-state when indexer is on.
FeeTransparency Current sink rows from sale-state when indexer is on (history still from /v1/fee-router/*).
RPC filter probe Session-scoped eth_getFilterChanges capability cache so event watches can prefer a capable endpoint (commit e5fca5d).

Commits: e5fca5d (frontend + filter probe), a2228fb (indexer 1.24.0 + docs/invariants).

Request: additional verification

Could you run through this checklist on a stack with VITE_INDEXER_URL + indexer ≥ 1.24.0 (restart indexer after deploy)?

  • GET /v1/timecurve/sale-state returns fee_router_sinks (5 rows), linear_charm_*, fee_router, owner, initial_timer_sec, prizes_distributed.
  • Arena (/timecurve/arena): Network tab shows no repeating ~1 Hz eth_call batches for the old 27-field TimeCurve core multicall; sale-state GET at ~2s backoff; supplement/warbow-immutable RPC at most once per session until refetch.
  • Simple (/timecurve): No ~1 Hz LinearCharmPrice RPC while sale-state is healthy.
  • Footer FeeTransparency: Current sinks render without FeeRouter sinks(i) RPC poll when indexer is on.
  • Cross-nav Simple ↔ Arena: one React Query timecurve-sale-state entry (shared key).
  • No indexer (VITE_INDEXER_URL unset): Arena still loads via full mergedArenaTc; Simple/Arena/FeeTransparency fall back to RPC.
  • Post-tx refresh: After buy / WarBow BP event, sale-state refetch updates totals/sinks/linear price without requiring full page reload.
  • Submit paths unchanged: Buy / steal preflight still uses fresh RPC sizing (not sale-state display snapshot).

Reply here if anything on the checklist fails or if you want this split into a separate tracking issue.

## Follow-up (out of original #216 scope, same architecture) @Brouie — follow-on work landed on `main` after the core #216 sale-state / RPC-split PR. This was **not** in the original issue checklist but addresses the same “indexer + RPC duplicate polls” problem for surfaces called out during mapping. ### What shipped | Area | Change | |------|--------| | **Arena** | `useTimeCurveArenaModel` uses shared `GET /v1/timecurve/sale-state` for global core (no ~1 Hz `mergedArenaTc` core multicall when `VITE_INDEXER_URL` is set). WarBow policy constants stay on a small load-once RPC bundle. | | **Indexer schema 1.24.0** | Sale-state poll now includes `fee_router`, `fee_router_sinks[5]`, `linear_charm_base_price_wad` / `linear_charm_daily_increment_wad`, plus Arena supplement fields (`initial_timer_sec`, `prizes_distributed`, `owner`). | | **Simple** | `LinearCharmPrice` display from sale-state when indexer is on. | | **FeeTransparency** | Current sink rows from sale-state when indexer is on (history still from `/v1/fee-router/*`). | | **RPC filter probe** | Session-scoped `eth_getFilterChanges` capability cache so event watches can prefer a capable endpoint (commit `e5fca5d`). | Commits: `e5fca5d` (frontend + filter probe), `a2228fb` (indexer 1.24.0 + docs/invariants). ### Request: additional verification Could you run through this checklist on a stack with **`VITE_INDEXER_URL`** + indexer **≥ 1.24.0** (restart indexer after deploy)? - [ ] **`GET /v1/timecurve/sale-state`** returns `fee_router_sinks` (5 rows), `linear_charm_*`, `fee_router`, `owner`, `initial_timer_sec`, `prizes_distributed`. - [ ] **Arena** (`/timecurve/arena`): Network tab shows **no** repeating ~1 Hz `eth_call` batches for the old 27-field TimeCurve core multicall; sale-state GET at ~2s backoff; supplement/warbow-immutable RPC at most once per session until refetch. - [ ] **Simple** (`/timecurve`): No ~1 Hz `LinearCharmPrice` RPC while sale-state is healthy. - [ ] **Footer FeeTransparency**: Current sinks render without FeeRouter `sinks(i)` RPC poll when indexer is on. - [ ] **Cross-nav** Simple ↔ Arena: one React Query `timecurve-sale-state` entry (shared key). - [ ] **No indexer** (`VITE_INDEXER_URL` unset): Arena still loads via full `mergedArenaTc`; Simple/Arena/FeeTransparency fall back to RPC. - [ ] **Post-tx refresh**: After buy / WarBow BP event, sale-state refetch updates totals/sinks/linear price without requiring full page reload. - [ ] **Submit paths unchanged**: Buy / steal preflight still uses fresh RPC sizing (not sale-state display snapshot). Reply here if anything on the checklist fails or if you want this split into a separate tracking issue.
Brouie commented 2026-05-20 00:44:17 +00:00 (Migrated from gitlab.com)

mentioned in issue #219

mentioned in issue #219
Brouie commented 2026-05-20 01:26:26 +00:00 (Migrated from gitlab.com)

mentioned in issue #221

mentioned in issue #221
Brouie commented 2026-05-20 01:28:17 +00:00 (Migrated from gitlab.com)

Walked verification end to end on production. Mixed results — most of the new infrastructure is working, but found one regression class that I filed as #221 (Arena retry storm) plus a doc/deploy mismatch on the globalstake claim.

Indexer side

  • GET /v1/timecurve/sale-state returns 200 with full payload — accepted_asset, charm_price, current_charm_bounds_min_wad / max_wad, total_raised, fee_router, fee_router_sinks (5 rows with correct weights 3000/4000/2000/0/1000), linear_charm_base_price_wad, linear_charm_daily_increment_wad, owner, initial_timer_sec, prizes_distributed, plus warbow constants. PASS
  • GET /v1/status shows schema_version: 1.24.0. PASS
  • Sale-state fields move after onchain activity — total_raised was 3110579386370662779126 wad on read, ticking up as sale continues. PASS

Simple side (/timecurve)

  • Indexer polling visible in network tab: sale-state, chain-timer, podiums all firing at expected cadence. PASS
  • No more 1s eth_call storm on totalRaised / currentMinBuyAmount / etc — Simple page request volume in a 60s window was 5,906 requests, down from yesterday's ~164 req/s to ~99 req/s. Real reduction. PARTIAL PASS (improvement clear but not order-of-magnitude)
  • FeeTransparency component renders Current onchain sinks block with addresses, all 5 sinks present. PASS
  • Cooldown CTA no longer polling visibly. Not walked under active cooldown state, will follow up if I trigger one.

Arena side (/timecurve/arena) — FILED #221

  • Walked Arena under same conditions and observed 126,893 requests in 60s with majority (failed) or (pending). Tested in Incognito to rule out wallet extensions. Direct curl to mainnet.megaeth.com/rpc returned a healthy block number while this was happening, so the underlying RPC is alive — the failures are browser-layer.
  • Hypothesis: Chrome per-origin connection cap exhaustion + retry layer not backing off on Arena's mergedArenaTc multicall.
  • Possibly the same surface as your Known follow-up note (mergedArenaTc not yet sharing sale-state) but the observed behavior under degradation is launch-impact class IMO. Details in #221.

Globalstake removed from VITE_RPC_URL claim — doc/deploy mismatch

  • Verification doc says removed.
  • Production /timecurve build-info panel currently shows VITE_RPC_URL = rpc-megaeth-mainnet.globalstake.io, megaeth.drpc.org, gateway.liquify.com/api/MEGAETHM2ONGJ26QTI0N1N6, mainnet.megaeth.com/rpc — globalstake is still in position 1, with three additional endpoints added behind it.
  • Probably what was intended is the additional fallbacks landed but globalstake stayed in the list. Either way the deployed VITE_RPC_URL doesn't match the verification doc. Worth a quick fix to either the doc or the env var.

Wallet behavior — not walked yet

  • Switch account refetch / switch chain no-refetch / manual CL8Y refresh button — none walked yet. Will follow up.

Writes — not walked yet

  • Did not trigger a buy in this session. No realtime_sendRawTransaction observation. Will follow up if I do a manual test buy.

429 backoff — not walked yet

  • Did not artificially trigger 429. The Arena retry storm in #221 is the closest thing to RPC-degradation observation I have, and the data there suggests the backoff hook isn't engaging on the mergedArenaTc multicall.

No-indexer fallback — not walked

  • Did not test with VITE_INDEXER_URL unset (would need a local build with that env, which I tried yesterday and hit WSL filesystem case-sensitivity issues on /mnt/d). If you want this verified empirically, easiest path is a staging deploy with the env unset.

Summary

  • Core indexer migration: shipped and working
  • Simple polling reduction: real but modest (~40% drop in raw request rate, not order-of-magnitude)
  • Arena: regression-class storm under RPC stress, filed #221
  • globalstake removal: not deployed yet (or doc is wrong)
  • Wallet / writes / 429 / no-indexer: not walked yet, will follow up
Walked verification end to end on production. Mixed results — most of the new infrastructure is working, but found one regression class that I filed as #221 (Arena retry storm) plus a doc/deploy mismatch on the globalstake claim. Indexer side - GET /v1/timecurve/sale-state returns 200 with full payload — accepted_asset, charm_price, current_charm_bounds_min_wad / max_wad, total_raised, fee_router, fee_router_sinks (5 rows with correct weights 3000/4000/2000/0/1000), linear_charm_base_price_wad, linear_charm_daily_increment_wad, owner, initial_timer_sec, prizes_distributed, plus warbow constants. PASS - GET /v1/status shows schema_version: 1.24.0. PASS - Sale-state fields move after onchain activity — total_raised was 3110579386370662779126 wad on read, ticking up as sale continues. PASS Simple side (/timecurve) - Indexer polling visible in network tab: sale-state, chain-timer, podiums all firing at expected cadence. PASS - No more 1s eth_call storm on totalRaised / currentMinBuyAmount / etc — Simple page request volume in a 60s window was 5,906 requests, down from yesterday's ~164 req/s to ~99 req/s. Real reduction. PARTIAL PASS (improvement clear but not order-of-magnitude) - FeeTransparency component renders Current onchain sinks block with addresses, all 5 sinks present. PASS - Cooldown CTA no longer polling visibly. Not walked under active cooldown state, will follow up if I trigger one. Arena side (/timecurve/arena) — FILED #221 - Walked Arena under same conditions and observed 126,893 requests in 60s with majority (failed) or (pending). Tested in Incognito to rule out wallet extensions. Direct curl to mainnet.megaeth.com/rpc returned a healthy block number while this was happening, so the underlying RPC is alive — the failures are browser-layer. - Hypothesis: Chrome per-origin connection cap exhaustion + retry layer not backing off on Arena's mergedArenaTc multicall. - Possibly the same surface as your Known follow-up note (mergedArenaTc not yet sharing sale-state) but the observed behavior under degradation is launch-impact class IMO. Details in #221. Globalstake removed from VITE_RPC_URL claim — doc/deploy mismatch - Verification doc says removed. - Production /timecurve build-info panel currently shows VITE_RPC_URL = rpc-megaeth-mainnet.globalstake.io, megaeth.drpc.org, gateway.liquify.com/api/MEGAETHM2ONGJ26QTI0N1N6, mainnet.megaeth.com/rpc — globalstake is still in position 1, with three additional endpoints added behind it. - Probably what was intended is the additional fallbacks landed but globalstake stayed in the list. Either way the deployed VITE_RPC_URL doesn't match the verification doc. Worth a quick fix to either the doc or the env var. Wallet behavior — not walked yet - Switch account refetch / switch chain no-refetch / manual CL8Y refresh button — none walked yet. Will follow up. Writes — not walked yet - Did not trigger a buy in this session. No realtime_sendRawTransaction observation. Will follow up if I do a manual test buy. 429 backoff — not walked yet - Did not artificially trigger 429. The Arena retry storm in #221 is the closest thing to RPC-degradation observation I have, and the data there suggests the backoff hook isn't engaging on the mergedArenaTc multicall. No-indexer fallback — not walked - Did not test with VITE_INDEXER_URL unset (would need a local build with that env, which I tried yesterday and hit WSL filesystem case-sensitivity issues on /mnt/d). If you want this verified empirically, easiest path is a staging deploy with the env unset. Summary - Core indexer migration: shipped and working - Simple polling reduction: real but modest (~40% drop in raw request rate, not order-of-magnitude) - Arena: regression-class storm under RPC stress, filed #221 - globalstake removal: not deployed yet (or doc is wrong) - Wallet / writes / 429 / no-indexer: not walked yet, will follow up
Brouie commented 2026-05-20 05:21:58 +00:00 (Migrated from gitlab.com)

mentioned in issue #215

mentioned in issue #215
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-21 12:03:09 +00:00
PlasticDigits commented 2026-05-22 00:14:56 +00:00 (Migrated from gitlab.com)

mentioned in issue #236

mentioned in issue #236
PlasticDigits commented 2026-05-22 06:51:19 +00:00 (Migrated from gitlab.com)

mentioned in issue #237

mentioned in issue #237
PlasticDigits commented 2026-05-29 08:06:25 +00:00 (Migrated from gitlab.com)

mentioned in issue #264

mentioned in issue #264
PlasticDigits commented 2026-07-07 02:28:01 +00:00 (Migrated from gitlab.com)

mentioned in issue #361

mentioned in issue #361
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/yieldomega#216
No description provided.