RPC + indexer architecture for frontend reads (concept + design)
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
- Pull globalstake from VITE_RPC_URL
- Wire the 9 fixed-interval pollers into useRpcQueryHealthForRefetch
- Drop the RPC overlays for reads where the indexer already has the data — warbowPodiumLive, usePodiumReads:219, useTimeCurveArenaModel:1938
- Either add a sale-state indexer route, or split immutables out of useTimeCurveSaleSession and tighten mutables
- Flip wallet-state reads to load-once + event-driven refetch by default, case-by-case where polling is justified
- 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?