Indexer: MegaETH miniBlocks WSS stream (additive) + frontend realtime head pill #237

Open
opened 2026-05-22 06:29:38 +00:00 by PlasticDigits · 15 comments
PlasticDigits commented 2026-05-22 06:29:38 +00:00 (Migrated from gitlab.com)

Summary

Add additive MegaETH Realtime API ingestion over WebSocket (eth_subscribe → miniBlocks) so yieldomega.com can show a live mini-block head (number + timestamp in ms) next to the existing “latest indexed block” pill on the agent card. RPC block polling, per-block transactional ingest, and reorg rollback must remain unchanged — they are the authoritative path into Postgres; WSS is a best-effort realtime lane that must never be assumed reliable.

Why (MegaETH specifics)

  • MegaETH produces mini-blocks ~every 10ms; standard eth_blockNumber / ~1s EVM blocks are too coarse for “live” UX.
  • MegaETH recommends for indexers/explorers: subscribe to miniBlocks — each notification includes the full set of transactions and receipts for that mini-block (Realtime API, eth_subscribe miniBlocks).
  • WSS on MegaETH requires keepalive (eth_chainId ~every 30s); limits: 50 connections / VIP endpoint, 10 subscriptions / connection (see megaeth-developer skill — WebSocket section).
  • Local dev for WSS behavior: connect to MegaETH mainnet RPC/WSS nodes — Anvil and MegaETH testnet (6343) do not reproduce mini-block / Realtime API semantics. Chain 4326 mainnet: https://mainnet.megaeth.com/rpc (WSS URL from operator docs / env).

Related MegaETH methods (not all in scope for this issue, but document for follow-ups):

Method Use
eth_subscribe logs with fromBlock/toBlock = "pending" Live event UI
eth_subscribe stateChanges Account balance/nonce/storage watch
eth_callAfter Frontend: simulate/fetch after prior tx nonce confirms
eth_getLogsWithCursor Large historical log backfill without timeout

Non-negotiable constraints

  1. Do not replace existing ingestion::run RPC loop (indexer/src/ingestion.rs), rollback_after / find_common_ancestor (indexer/src/reorg.rs), or INV-INDEXER-140 per-block DB transactions (#140).
  2. WSS disconnects, gaps, duplicates, and reordering must be handled without corrupting chain_pointer or event tables. If WSS ever drives writes, it must reconcile against RPC before commit (default: no WSS writes in Phase 1).
  3. Provenance: API/consumers must distinguish RPC-indexed (durable, reorg-safe) vs WSS-realtime (ephemeral, best-effort). Propose explicit fields (e.g. confirmation: "indexed" | "wss_realtime") and/or separate endpoints — avoid overloading max_indexed_block semantics on GET /v1/status.
  4. Supervision: WSS task gets the same operational visibility as ingestion (#168): alive flag, last notification wall time, reconnect backoff — surfaced on status/health routes.

Proposed architecture (phased)

Phase 1 — Realtime head fan-out (this issue’s demo goal)

Indexer

  • New background task: WSS client → eth_subscribe("miniBlocks", …) (params per MegaETH docs).
  • Maintain in-memory (or Redis later) MiniBlockHead: { mini_block_number, timestamp_ms, received_at_ms } — not written to Postgres event tables in Phase 1.
  • On disconnect: exponential backoff reconnect; do not advance any chain pointer from WSS alone.
  • Optional: compare WSS head vs RPC eth_blockNumber / latest mini-block query for lag metrics (operator-only).

HTTP / streaming to frontend

  • Expose realtime head to clients, e.g.:
    • GET /v1/realtime/mini-block-head (JSON snapshot), and/or
    • GET /v1/realtime/stream (SSE) or WebSocket from indexer → browser (single upstream WSS per indexer process; do not open per-user MegaETH WSS from the browser — rate limits + secrets).
  • Schema bump + x-schema-version; document in docs/indexer/design.md.

Frontend (yieldomega.com agent card)

  • Next to existing IndexerStatusBar “latest indexed block … · live” (IndexerStatusBar.tsx, fed by GET /v1/status / IndexerConnectivityContext):
    • Add pill: “latest websockets block” with mini-block number and timestamp (ms) updating in realtime when indexer stream is healthy.
    • Clear visual distinction: indexed block = Postgres/RPC authority; WSS block = realtime hint (may run ahead of indexer; may stall on WSS outage while RPC ingest continues).
  • Mirror on TimeCurveSimpleAgentCard footer region if Simple hides global footer (#232).
  • data-testid hooks for Playwright; manual QA checklist row.

Phase 2+ (separate issues / subtasks — outline only)

  • Queue / reconcile pattern: WSS mini-block notifications enqueued; RPC confirms mini-block or parent EVM block before promoting rows to indexed tier (or mark rows provisional until RPC catch-up).
  • Decode path: Use mini-block tx/receipt payloads for low-latency UI feeds; still commit via existing per-EVM-block ingestion transaction.
  • Reorg: WSS does not change REORG_STRATEGY.md rules — RPC parent-hash walk remains canonical (#140, indexer/REORG_STRATEGY.md open question on streaming vs polling).
  • Frontend tx chaining: eth_callAfter for approval → swap / buy flows (wallet RPC, not indexer).
  • Backfill: eth_getLogsWithCursor for large ranges where eth_getLogs hits limits.

Configuration

  • INDEXER_WSS_URL (or derive from RPC_URL with wss:// scheme) — optional; when unset, realtime task idle, API returns explicit “wss disabled”.
  • INDEXER_WSS_ENABLED default off in local Anvil stack; document mainnet WSS URL for operators testing Phase 1.
  • Keep INDEXER_RPC_REQUEST_TIMEOUT_SEC on HTTP RPC only; WSS ping/keepalive separate.

Testing

Layer Requirement
Unit WSS message parse → MiniBlockHead; reconnect backoff; provenance fields on API DTOs
Integration Optional gate behind env: live mainnet WSS smoke (skipped in CI without secret URL)
Manual Mainnet indexer + production frontend: both pills move; kill WSS → WSS pill stale/offline, indexed pill still advances via RPC
Anvil CI Unchanged — no miniBlocks on Anvil; assert graceful disable

Add invariants: INV-INDEXER-*-WSS-*, INV-FRONTEND-*-WSS-HEAD-* in docs/testing/invariants-and-business-logic.md.

Acceptance criteria (Phase 1)

  • Indexer subscribes to miniBlocks on MegaETH mainnet WSS when enabled; keepalive documented and implemented.
  • RPC ingestion + reorg behavior unchanged (existing integration tests still pass).
  • Status/realtime API exposes WSS head with confirmation: wss_realtime (or equivalent) separate from max_indexed_block / chain_pointer.
  • yieldomega.com agent card shows two live readouts: latest indexed block (existing) + latest websockets block (mini-block # + timestamp ms).
  • Docs: indexer/README.md, docs/indexer/design.md — dual-path diagram + mainnet-only WSS testing note.

References

Out of scope (this issue)

  • Replacing RPC polling with WSS-only ingestion
  • Persisting mini-block-level rows into Postgres without a reconcile design
  • Per-browser MegaETH WSS connections from the static frontend
  • eth_callAfter / eth_getLogsWithCursor implementation (noted for follow-up)
## Summary Add **additive** MegaETH Realtime API ingestion over **WebSocket** (`eth_subscribe` → **`miniBlocks`**) so yieldomega.com can show a **live mini-block head** (number + timestamp in ms) next to the existing **“latest indexed block”** pill on the agent card. **RPC block polling, per-block transactional ingest, and reorg rollback must remain unchanged** — they are the authoritative path into Postgres; WSS is a **best-effort realtime lane** that must never be assumed reliable. ## Why (MegaETH specifics) - MegaETH produces **mini-blocks ~every 10ms**; standard `eth_blockNumber` / ~1s EVM blocks are too coarse for “live” UX. - MegaETH recommends for indexers/explorers: subscribe to **`miniBlocks`** — each notification includes the **full set of transactions and receipts** for that mini-block ([Realtime API](https://docs.megaeth.com/developer-docs/overview-2/realtime-api), [`eth_subscribe` miniBlocks](https://docs.megaeth.com/developer-docs/overview-2/rpc-reference/eth_subscribe.md#miniblocks)). - WSS on MegaETH requires **keepalive** (`eth_chainId` ~every **30s**); limits: **50 connections / VIP endpoint**, **10 subscriptions / connection** (see [megaeth-developer skill](https://github.com/0xBreadguy/megaeth-ai-developer-skills/blob/main/SKILL.md) — WebSocket section). - **Local dev for WSS behavior:** connect to **MegaETH mainnet** RPC/WSS nodes — **Anvil** and **MegaETH testnet (6343)** do **not** reproduce mini-block / Realtime API semantics. Chain **4326** mainnet: `https://mainnet.megaeth.com/rpc` (WSS URL from operator docs / env). Related MegaETH methods (not all in scope for this issue, but document for follow-ups): | Method | Use | |--------|-----| | `eth_subscribe` `logs` with `fromBlock`/`toBlock` = `"pending"` | Live event UI | | `eth_subscribe` `stateChanges` | Account balance/nonce/storage watch | | `eth_callAfter` | Frontend: simulate/fetch after prior tx nonce confirms | | `eth_getLogsWithCursor` | Large historical log backfill without timeout | ## Non-negotiable constraints 1. **Do not replace** existing `ingestion::run` RPC loop ([`indexer/src/ingestion.rs`](indexer/src/ingestion.rs)), `rollback_after` / `find_common_ancestor` ([`indexer/src/reorg.rs`](indexer/src/reorg.rs)), or **`INV-INDEXER-140`** per-block DB transactions ([#140](https://gitlab.com/PlasticDigits/yieldomega/-/issues/140)). 2. **WSS disconnects, gaps, duplicates, and reordering** must be handled without corrupting `chain_pointer` or event tables. If WSS ever drives writes, it must **reconcile** against RPC before commit (default: **no WSS writes in Phase 1**). 3. **Provenance:** API/consumers must distinguish **RPC-indexed (durable, reorg-safe)** vs **WSS-realtime (ephemeral, best-effort)**. Propose explicit fields (e.g. `confirmation: "indexed" | "wss_realtime"`) and/or separate endpoints — avoid overloading `max_indexed_block` semantics on [`GET /v1/status`](indexer/src/api.rs). 4. **Supervision:** WSS task gets the same operational visibility as ingestion ([#168](https://gitlab.com/PlasticDigits/yieldomega/-/issues/168)): alive flag, last notification wall time, reconnect backoff — surfaced on status/health routes. ## Proposed architecture (phased) ### Phase 1 — Realtime head fan-out (this issue’s demo goal) **Indexer** - New background task: WSS client → `eth_subscribe("miniBlocks", …)` (params per MegaETH docs). - Maintain in-memory (or Redis later) **`MiniBlockHead`**: `{ mini_block_number, timestamp_ms, received_at_ms }` — **not** written to Postgres event tables in Phase 1. - On disconnect: exponential backoff reconnect; **do not** advance any chain pointer from WSS alone. - Optional: compare WSS head vs RPC `eth_blockNumber` / latest mini-block query for **lag metrics** (operator-only). **HTTP / streaming to frontend** - Expose realtime head to clients, e.g.: - **`GET /v1/realtime/mini-block-head`** (JSON snapshot), and/or - **`GET /v1/realtime/stream`** (SSE) or **WebSocket** from indexer → browser (single upstream WSS per indexer process; **do not** open per-user MegaETH WSS from the browser — rate limits + secrets). - Schema bump + `x-schema-version`; document in [`docs/indexer/design.md`](docs/indexer/design.md). **Frontend (yieldomega.com agent card)** - Next to existing **`IndexerStatusBar`** “latest indexed block … · live” ([`IndexerStatusBar.tsx`](frontend/src/components/IndexerStatusBar.tsx), fed by [`GET /v1/status`](frontend/src/lib/indexerApi.ts) / [`IndexerConnectivityContext`](frontend/src/providers/IndexerConnectivityContext.tsx)): - Add pill: **“latest websockets block”** with **mini-block number** and **timestamp (ms)** updating in realtime when indexer stream is healthy. - Clear visual distinction: indexed block = **Postgres/RPC authority**; WSS block = **realtime hint** (may run ahead of indexer; may stall on WSS outage while RPC ingest continues). - Mirror on **`TimeCurveSimpleAgentCard`** footer region if Simple hides global footer ([#232](https://gitlab.com/PlasticDigits/yieldomega/-/issues/232)). - `data-testid` hooks for Playwright; manual QA checklist row. ### Phase 2+ (separate issues / subtasks — outline only) - **Queue / reconcile pattern:** WSS mini-block notifications enqueued; **RPC confirms** mini-block or parent EVM block before promoting rows to indexed tier (or mark rows `provisional` until RPC catch-up). - **Decode path:** Use mini-block tx/receipt payloads for **low-latency** UI feeds; still **commit** via existing per-EVM-block ingestion transaction. - **Reorg:** WSS does not change `REORG_STRATEGY.md` rules — RPC parent-hash walk remains canonical ([#140](https://gitlab.com/PlasticDigits/yieldomega/-/issues/140), [`indexer/REORG_STRATEGY.md`](indexer/REORG_STRATEGY.md) open question on streaming vs polling). - **Frontend tx chaining:** `eth_callAfter` for approval → swap / buy flows (wallet RPC, not indexer). - **Backfill:** `eth_getLogsWithCursor` for large ranges where `eth_getLogs` hits limits. ## Configuration - **`INDEXER_WSS_URL`** (or derive from `RPC_URL` with `wss://` scheme) — optional; when unset, realtime task idle, API returns explicit “wss disabled”. - **`INDEXER_WSS_ENABLED`** default off in local Anvil stack; document mainnet WSS URL for operators testing Phase 1. - Keep **`INDEXER_RPC_REQUEST_TIMEOUT_SEC`** on HTTP RPC only; WSS ping/keepalive separate. ## Testing | Layer | Requirement | |-------|-------------| | **Unit** | WSS message parse → `MiniBlockHead`; reconnect backoff; provenance fields on API DTOs | | **Integration** | Optional gate behind env: live mainnet WSS smoke (skipped in CI without secret URL) | | **Manual** | Mainnet indexer + production frontend: both pills move; kill WSS → WSS pill stale/offline, indexed pill still advances via RPC | | **Anvil CI** | Unchanged — no miniBlocks on Anvil; assert graceful disable | Add invariants: **`INV-INDEXER-*-WSS-*`**, **`INV-FRONTEND-*-WSS-HEAD-*`** in [`docs/testing/invariants-and-business-logic.md`](docs/testing/invariants-and-business-logic.md). ## Acceptance criteria (Phase 1) - [ ] Indexer subscribes to **`miniBlocks`** on MegaETH mainnet WSS when enabled; keepalive documented and implemented. - [ ] RPC ingestion + reorg behavior **unchanged** (existing integration tests still pass). - [ ] Status/realtime API exposes WSS head with **`confirmation: wss_realtime`** (or equivalent) separate from **`max_indexed_block` / `chain_pointer`**. - [ ] yieldomega.com agent card shows **two** live readouts: **latest indexed block** (existing) + **latest websockets block** (mini-block # + timestamp ms). - [ ] Docs: [`indexer/README.md`](indexer/README.md), [`docs/indexer/design.md`](docs/indexer/design.md) — dual-path diagram + mainnet-only WSS testing note. ## References - [MegaETH Realtime API overview](https://docs.megaeth.com/developer-docs/overview-2/realtime-api) - [Mini-blocks](https://docs.megaeth.com/mini-block.md) - [megaeth-ai-developer-skills SKILL.md](https://github.com/0xBreadguy/megaeth-ai-developer-skills/blob/main/SKILL.md) - Indexer reorg: [`indexer/REORG_STRATEGY.md`](indexer/REORG_STRATEGY.md) - Ingestion resilience: [#168](https://gitlab.com/PlasticDigits/yieldomega/-/issues/168) ## Out of scope (this issue) - Replacing RPC polling with WSS-only ingestion - Persisting mini-block-level rows into Postgres without a reconcile design - Per-browser MegaETH WSS connections from the static frontend - `eth_callAfter` / `eth_getLogsWithCursor` implementation (noted for follow-up)
PlasticDigits commented 2026-05-22 06:51:19 +00:00 (Migrated from gitlab.com)

Discussion notes (architecture review + Q&A)

Captured from design review thread — supplements the issue body with pitfalls, protocol choices, and prod split (ingestion indexer vs HTTP indexer).


Q: SSE or WSS for browser ↔ indexer?

A: SSE on the HTTP indexer (MegaETH WSS only on ingestion ↔ node).

Hop Protocol Rationale
MegaETH node → ingestion indexer WSS (eth_subscribe miniBlocks) Chain API; rate limits (50 conn / VIP, 10 subs); one subscription per deployment
Ingestion → HTTP indexer (cross-process) Postgres head row + NOTIFY (or throttled upsert) Shared DB you already have; no third service
HTTP indexer → yieldomega.com SSE One-way server→client; normal HTTP; EventSource + auto-reconnect; we own both sides

WSS browser↔indexer only if we later need bidirectional/binary/high-frequency per-client protocols. For a head pill, throttled SSE (4–10 Hz display) is enough.

Also expose GET /v1/realtime/mini-block-head snapshot for agents and SSE connect/bootstrap.


Q: Prod has separate HTTP indexer (client API) and ingestion indexer (same code, different env). node WSS → ingestion → Postgres → HTTP → SSE — is Postgres fast enough? Separate SSE service?

A:

  • Do not persist full miniBlocks payloads (txs + receipts) to Postgres at ~50–100 notifications/sec — wrong workload, WAL bloat, contends with INV-INDEXER-140 block transactions.
  • Do persist a single ephemeral head (mini-block number + timestamp_ms + received_at_ms), throttled to ~4–10 writes/sec even if WSS fires faster. Postgres latency for one-row upsert is sub-ms; end-to-end tens of ms is fine for UI.
  • No separate “SSE indexer” — add SSE route + in-process broadcast on the HTTP tier; fan-out from LISTEN on pg_notify (or 250ms–1s poll fallback).
  • WSS only on ingestion indexer (INDEXER_WSS_ENABLED there; off on HTTP tier). Avoid duplicate MegaETH subscriptions.

Recommended prod topology:

MegaETH WSS (miniBlocks)
        │
        ▼
┌─────────────────────────┐
│ Ingestion indexer       │  INGESTION_ENABLED=true
│  RPC ingest (authority) │  unchanged
│  WSS task               │  parse; throttle head → UPSERT + NOTIFY
└───────────┬─────────────┘
            │ Postgres (shared)
            ▼
┌─────────────────────────┐
│ HTTP indexer            │  INGESTION_ENABLED=false (typical)
│  REST /v1/*             │
│  LISTEN wss_mini_block  │  → broadcast → SSE clients
└───────────┬─────────────┘
            │ SSE
            ▼
      yieldomega.com (agent card pills)

Scale-out: multiple HTTP replicas each LISTEN + SSE OK; only one ingestion instance should hold the MegaETH WSS subscription (leader lock / single replica).

Phase 1 shortcut: skip SSE initially — throttled head row + GET poll @ 1s — then add SSE on HTTP when needed.


Architecture pitfalls (from review)

  1. Two different “heads” — max_indexed_block / chain_pointer = EVM block (RPC, reorg-safe). WSS pill = mini-block (~10ms). Numbers will diverge; never label WSS lag as “indexer blocks behind” without units. Strong UX copy: realtime preview · not indexed.

  2. False freshness — WSS head can run ahead of anything in /v1/timecurve/buys, podiums, etc. Phase 1 in-memory/head-only is safe for DB corruption but risky for product semantics.

  3. Indexer→browser streaming — SSE still needs heartbeats, stale detection (last_wss_at_ms), proxy idle timeouts; many tabs = many SSE connections on HTTP tier (acceptable; not many MegaETH WSS).

  4. WSS SPOF — VIP disconnect → pill stale; RPC ingest may continue on HTTP RPC. Reconnect must resync head via RPC after gap; detect sequence regressions / gaps.

  5. Phase 2 underestimate — Queue + “RPC confirm” before promoting WSS data to idx_* is hard: EVM-block-oriented schema today, ~100 mini-blocks/s if processing full payloads, idempotency keys, reorg retraction if we ever stream logs from WSS.

  6. Reorg — REORG_STRATEGY.md tiers are EVM-block based. WSS tier must stay non-authoritative until reconcile design exists.

  7. Mainnet-only WSS testing — Anvil/testnet won’t exercise miniBlocks; CI needs golden notification fixtures + optional mainnet smoke.

  8. Config — INDEXER_WSS_URL must match CHAIN_ID; fail closed under INDEXER_PRODUCTION=1 (same spirit as #156).

  9. Three clocks — RPC ingest pointer, chain_timer poller (#48 / #216), WSS mini-block head. Sale phase stays on /v1/timecurve/chain-timer (RPC-backed); WSS pill is display-only.

  10. Frontend — Don’t re-render agent card at 100 Hz; throttle display + mark stale if no event ~2s; a11y (constant announcements).

  11. Scope creep — eth_callAfter = wallet RPC (frontend), not indexer. eth_getLogsWithCursor = ingestion backfill, not realtime head.


Decisions / open items for implementation

Item Decision
Authority path RPC ingest + reorg unchanged (#140, #168)
WSS writes in Phase 1 Head row only; not chain_pointer / event tables
Provenance Separate API fields / confirmation: wss_realtime vs indexed
Browser transport SSE on HTTP indexer (+ GET snapshot)
Cross-service transport Postgres 1-row + NOTIFY; throttle writes
Separate SSE service No
eth_callAfter / eth_getLogsWithCursor Follow-up issues

Acceptance addendum (prod)

  • INDEXER_WSS_ENABLED only on ingestion deployment; HTTP deployment does not open MegaETH WSS.
  • Head stored under distinct key/table (e.g. indexer_state.wss_mini_block_head), not chain_pointer.
  • HTTP indexer: LISTEN + SSE route; wss_alive / last_wss_at_ms on status or realtime payload.
  • Load test: throttled head writes do not regress ingestion commit latency under normal sale load.
## Discussion notes (architecture review + Q&A) Captured from design review thread — supplements the issue body with pitfalls, protocol choices, and **prod split** (ingestion indexer vs HTTP indexer). --- ### Q: SSE or WSS for browser ↔ indexer? **A: SSE on the HTTP indexer** (MegaETH **WSS only** on ingestion ↔ node). | Hop | Protocol | Rationale | |-----|----------|-----------| | MegaETH node → ingestion indexer | **WSS** (`eth_subscribe` `miniBlocks`) | Chain API; rate limits (50 conn / VIP, 10 subs); one subscription per deployment | | Ingestion → HTTP indexer (cross-process) | **Postgres** head row + **`NOTIFY`** (or throttled upsert) | Shared DB you already have; no third service | | HTTP indexer → yieldomega.com | **SSE** | One-way server→client; normal HTTP; `EventSource` + auto-reconnect; we own both sides | **WSS browser↔indexer** only if we later need bidirectional/binary/high-frequency per-client protocols. For a head pill, **throttled SSE (4–10 Hz display)** is enough. Also expose **`GET /v1/realtime/mini-block-head`** snapshot for agents and SSE connect/bootstrap. --- ### Q: Prod has separate HTTP indexer (client API) and ingestion indexer (same code, different env). `node WSS → ingestion → Postgres → HTTP → SSE` — is Postgres fast enough? Separate SSE service? **A:** - **Do not** persist full `miniBlocks` payloads (txs + receipts) to Postgres at ~50–100 notifications/sec — wrong workload, WAL bloat, contends with **`INV-INDEXER-140`** block transactions. - **Do** persist a **single ephemeral head** (mini-block number + `timestamp_ms` + `received_at_ms`), **throttled to ~4–10 writes/sec** even if WSS fires faster. Postgres latency for one-row upsert is **sub-ms**; end-to-end tens of ms is fine for UI. - **No separate “SSE indexer”** — add SSE route + in-process `broadcast` on the **HTTP** tier; fan-out from `LISTEN` on `pg_notify` (or 250ms–1s poll fallback). - **WSS only on ingestion indexer** (`INDEXER_WSS_ENABLED` there; off on HTTP tier). Avoid duplicate MegaETH subscriptions. **Recommended prod topology:** ``` MegaETH WSS (miniBlocks) │ ▼ ┌─────────────────────────┐ │ Ingestion indexer │ INGESTION_ENABLED=true │ RPC ingest (authority) │ unchanged │ WSS task │ parse; throttle head → UPSERT + NOTIFY └───────────┬─────────────┘ │ Postgres (shared) ▼ ┌─────────────────────────┐ │ HTTP indexer │ INGESTION_ENABLED=false (typical) │ REST /v1/* │ │ LISTEN wss_mini_block │ → broadcast → SSE clients └───────────┬─────────────┘ │ SSE ▼ yieldomega.com (agent card pills) ``` **Scale-out:** multiple HTTP replicas each `LISTEN` + SSE OK; **only one** ingestion instance should hold the MegaETH WSS subscription (leader lock / single replica). **Phase 1 shortcut:** skip SSE initially — throttled head row + **`GET` poll @ 1s** — then add SSE on HTTP when needed. --- ### Architecture pitfalls (from review) 1. **Two different “heads”** — `max_indexed_block` / `chain_pointer` = **EVM block** (RPC, reorg-safe). WSS pill = **mini-block** (~10ms). Numbers will diverge; never label WSS lag as “indexer blocks behind” without units. Strong UX copy: *realtime preview · not indexed*. 2. **False freshness** — WSS head can run **ahead** of anything in `/v1/timecurve/buys`, podiums, etc. Phase 1 in-memory/head-only is safe for DB corruption but risky for product semantics. 3. **Indexer→browser streaming** — SSE still needs heartbeats, stale detection (`last_wss_at_ms`), proxy idle timeouts; many tabs = many SSE connections on HTTP tier (acceptable; not many MegaETH WSS). 4. **WSS SPOF** — VIP disconnect → pill stale; RPC ingest may continue on HTTP RPC. Reconnect must **resync** head via RPC after gap; detect sequence regressions / gaps. 5. **Phase 2 underestimate** — Queue + “RPC confirm” before promoting WSS data to `idx_*` is hard: EVM-block-oriented schema today, ~100 mini-blocks/s if processing full payloads, idempotency keys, reorg retraction if we ever stream logs from WSS. 6. **Reorg** — `REORG_STRATEGY.md` tiers are EVM-block based. WSS tier must stay **non-authoritative** until reconcile design exists. 7. **Mainnet-only WSS testing** — Anvil/testnet won’t exercise miniBlocks; CI needs **golden notification fixtures** + optional mainnet smoke. 8. **Config** — `INDEXER_WSS_URL` must match **`CHAIN_ID`**; fail closed under **`INDEXER_PRODUCTION=1`** (same spirit as #156). 9. **Three clocks** — RPC ingest pointer, `chain_timer` poller (#48 / #216), WSS mini-block head. Sale phase stays on **`/v1/timecurve/chain-timer`** (RPC-backed); WSS pill is display-only. 10. **Frontend** — Don’t re-render agent card at 100 Hz; throttle display + mark stale if no event ~2s; a11y (constant announcements). 11. **Scope creep** — `eth_callAfter` = wallet RPC (frontend), not indexer. `eth_getLogsWithCursor` = ingestion backfill, not realtime head. --- ### Decisions / open items for implementation | Item | Decision | |------|----------| | Authority path | RPC ingest + reorg unchanged (#140, #168) | | WSS writes in Phase 1 | Head row only; **not** `chain_pointer` / event tables | | Provenance | Separate API fields / `confirmation: wss_realtime` vs indexed | | Browser transport | **SSE** on HTTP indexer (+ GET snapshot) | | Cross-service transport | Postgres **1-row** + **`NOTIFY`**; throttle writes | | Separate SSE service | **No** | | `eth_callAfter` / `eth_getLogsWithCursor` | Follow-up issues | --- ### Acceptance addendum (prod) - [ ] `INDEXER_WSS_ENABLED` only on **ingestion** deployment; HTTP deployment does not open MegaETH WSS. - [ ] Head stored under distinct key/table (e.g. `indexer_state.wss_mini_block_head`), not `chain_pointer`. - [ ] HTTP indexer: `LISTEN` + SSE route; `wss_alive` / `last_wss_at_ms` on status or realtime payload. - [ ] Load test: throttled head writes do not regress ingestion commit latency under normal sale load.
PlasticDigits commented 2026-05-22 06:57:47 +00:00 (Migrated from gitlab.com)

Clarification — real goal (not just a mini-block # pill)

Follow-up discussion: the mini-block number in the UI is a pipeline smoke test. The actual product goals are:

  1. Hero timer — show chain time and countdown with correct millisecond resolution (not ~1s RPC poll + second-only block_timestamp_sec ticks).
  2. Last-buy leaderboard / live buys — update within ~ms of an onchain TimeCurve Buy, not when the EVM block is ingested into Postgres (~1s+ later).

A throttled head-only Postgres row alone does not achieve either goal.


Current bottlenecks (repo today)

Feature Today Limit
Timer useTimecurveHeroTimer → GET /v1/timecurve/chain-timer ~1s poll; block_timestamp_sec (whole seconds); setInterval(..., 1000) No ms chain clock
Recent buys fetchTimecurveBuys ~5s poll (backoff) from idx_timecurve_buy Row exists only after RPC block ingest
Last Buy podium GET /v1/timecurve/podiums live prediction from Postgres; usePodiumScoreClock ticks 1s Same ingest lag + second-only “ago” copy

Postgres is not “too slow” for queries — data arrives too late for ms UX.


Revised architecture (goal-aligned)

MegaETH WSS (ingestion indexer only)
  ├─ chain clock (mini-block timestamp / head)  → timer ms
  └─ logs subscribe ("pending") TimeCurve Buy   → provisional buys ms

Ingestion indexer
  ├─ RPC block ingest → Postgres (unchanged: #140, reorg, authoritative)
  └─ WSS decode → small events only (NOT full miniBlocks → PG)

HTTP indexer
  └─ SSE multiplex: { clock_ms } + { buy_provisional }

yieldomega.com
  ├─ Timer: chainNowMs from SSE + deadline from chain-timer / RPC (refresh on Buy)
  └─ Last buy / recent buys: merge SSE provisional + polled indexed APIs

MegaETH guidance mapping:

Goal WSS subscription Postgres?
Ms timer Mini-block timestamp (from miniBlocks head or dedicated clock field) Optional throttled chain_clock_ms row + NOTIFY; SSE is sufficient for fan-out
Ms last buy / live feed logs with fromBlock/toBlock = "pending", filter TimeCurve + Buy topic Confirmed rows still via RPC ingest → idx_timecurve_buy; provisional not authoritative
Pipeline test Mini-block # in dev UI Optional

Prefer logs (pending) over persisting full miniBlocks JSON for buy latency — same decoder as RPC path, smaller payloads.


Two kinds of “accurate”

Claim Source
“Clock / countdown at ms” WSS chain clock + SSE; deadline still from TimeCurve RPC (chain-timer ~1s OK)
“Buy / last-buy leader now” WSS-decoded Buy over SSE (confirmation: wss_realtime)
“Leaderboard / history / reorg-safe” RPC-indexed Postgres (confirmation: indexed)

UI must label provisional vs indexed until RPC confirms; retract provisional rows on reorg.


Suggested implementation notes

Timer (ms)

  • SSE: chain_timestamp_ms, mini_block_number (optional), observed_at_ms.
  • Frontend: chainNowMs from stream; remainingMs = deadlineMs - chainNowMs; display format with sub-second digits; local tick 50–100ms between SSE events if needed.
  • On each decoded Buy, refresh deadline (soft chain-timer poll or targeted eth_call) — timer resets onchain at buy; ms clock alone is insufficient.
  • Do not drive derivePhase (#48) from WSS ms alone without same sale-start/deadline/ended rules as today.

Last buy / recent buys (ms)

  • Ingestion: decode Buy with existing decoder → push SSE payload (buyer, charm, tx_hash, log_index, timer fields, block_timestamp_ms).
  • HTTP: multiplex buy events on same SSE channel as clock (or separate routes).
  • Frontend: prepend SSE buys to recent list; update Last Buy podium row immediately; keep fetchTimecurveBuys + /v1/timecurve/podiums as reconcile.
  • Show “N ms ago” from observed_at_ms or chain ts ms.

Cross-process (prod split)

  • Ingestion: WSS + decode + pg_notify small JSON or direct internal fan-out if co-located.
  • HTTP: LISTEN → broadcast → SSE to browsers (no third “SSE indexer”).
  • Do not write full miniBlocks at line rate to Postgres.

Pitfalls (goal-specific)

  1. Provisional buys may appear before RPC and disappear on reorg — handle retraction or confirm animation.
  2. Buy → deadline reset — must refresh deadline when WSS sees Buy, not only advance clock ms.
  3. False equivalence — ms clock does not mean indexed podiums/buys are current; separate copy/tiers.
  4. Phase / timer authority — keep sale phase on indexer-anchored chain-timer + ledgerSecIntForPhase (#48); WSS is display/latency layer.
  5. Volume — pending logs for one contract is OK; one MegaETH WSS per ingestion deployment; HTTP SSE scales per tab.
  6. Testing — golden WSS log fixtures in CI; mainnet smoke for pending logs; Anvil remains indexed-only.

Issue scope split (suggested)

Phase Deliverable
A WSS logs (pending) + decode Buy + SSE to client → recent buys + Last Buy row update in ms; merge with indexed poll; provisional vs indexed provenance
B WSS chain clock (mini-block ts) + SSE clock_ms → hero timer ms + deadline refresh on Buy
C Mini-block # pill / status fields (pipeline dev test only)

Phase A + B are the product; C is optional operator/debug chrome.


Acceptance criteria addendum (product)

  • With indexer + WSS enabled on mainnet, a Buy appears in Simple live buys / Last Buy before max_indexed_block advances.
  • Hero countdown displays sub-second behavior aligned with chain ms clock (not 1s stair-steps only).
  • After WSS disconnect, timer/buys fall back to indexed/RPC paths without claiming realtime authority.
  • Reorg: provisional SSE buy removed or marked invalid when RPC rollback occurs.
  • derivePhase / hero timer sale rules remain consistent with #48 when indexer chain-timer is set.

Decisions table (update)

Item Decision
Primary WSS for buys logs pending, not full miniBlocks in PG
Primary WSS for timer Mini-block / head timestamp ms via SSE
Postgres RPC ingest authoritative; optional small clock row; no full miniBlock payloads
Browser transport SSE on HTTP indexer
Mini-block # in UI Test / dev (Phase C), not the product goal
## Clarification — real goal (not just a mini-block # pill) Follow-up discussion: the **mini-block number in the UI is a pipeline smoke test**. The actual product goals are: 1. **Hero timer** — show chain time and countdown with **correct millisecond resolution** (not ~1s RPC poll + second-only `block_timestamp_sec` ticks). 2. **Last-buy leaderboard / live buys** — update within **~ms** of an onchain `TimeCurve` **`Buy`**, not when the EVM block is ingested into Postgres (~1s+ later). A **throttled head-only Postgres row** alone does **not** achieve either goal. --- ### Current bottlenecks (repo today) | Feature | Today | Limit | |--------|--------|--------| | Timer | `useTimecurveHeroTimer` → `GET /v1/timecurve/chain-timer` ~**1s** poll; `block_timestamp_sec` (whole seconds); `setInterval(..., 1000)` | No ms chain clock | | Recent buys | `fetchTimecurveBuys` ~**5s** poll (backoff) from **`idx_timecurve_buy`** | Row exists only after **RPC block ingest** | | Last Buy podium | `GET /v1/timecurve/podiums` live prediction from **Postgres**; `usePodiumScoreClock` ticks **1s** | Same ingest lag + second-only “ago” copy | Postgres is not “too slow” for queries — **data arrives too late** for ms UX. --- ### Revised architecture (goal-aligned) ```text MegaETH WSS (ingestion indexer only) ├─ chain clock (mini-block timestamp / head) → timer ms └─ logs subscribe ("pending") TimeCurve Buy → provisional buys ms Ingestion indexer ├─ RPC block ingest → Postgres (unchanged: #140, reorg, authoritative) └─ WSS decode → small events only (NOT full miniBlocks → PG) HTTP indexer └─ SSE multiplex: { clock_ms } + { buy_provisional } yieldomega.com ├─ Timer: chainNowMs from SSE + deadline from chain-timer / RPC (refresh on Buy) └─ Last buy / recent buys: merge SSE provisional + polled indexed APIs ``` **MegaETH guidance mapping:** | Goal | WSS subscription | Postgres? | |------|------------------|-------------| | Ms timer | Mini-block **timestamp** (from `miniBlocks` head or dedicated clock field) | Optional throttled `chain_clock_ms` row + `NOTIFY`; **SSE is sufficient** for fan-out | | Ms last buy / live feed | **`logs`** with `fromBlock`/`toBlock` = **`"pending"`**, filter TimeCurve + `Buy` topic | **Confirmed** rows still via RPC ingest → `idx_timecurve_buy`; provisional **not** authoritative | | Pipeline test | Mini-block # in dev UI | Optional | Prefer **`logs` (pending)** over persisting full **`miniBlocks`** JSON for buy latency — same decoder as RPC path, smaller payloads. --- ### Two kinds of “accurate” | Claim | Source | |-------|--------| | “Clock / countdown at ms” | WSS chain clock + SSE; `deadline` still from **TimeCurve** RPC (`chain-timer` ~1s OK) | | “Buy / last-buy leader now” | WSS-decoded **`Buy`** over SSE (`confirmation: wss_realtime`) | | “Leaderboard / history / reorg-safe” | **RPC-indexed Postgres** (`confirmation: indexed`) | UI must label provisional vs indexed until RPC confirms; retract provisional rows on reorg. --- ### Suggested implementation notes **Timer (ms)** - SSE: `chain_timestamp_ms`, `mini_block_number` (optional), `observed_at_ms`. - Frontend: `chainNowMs` from stream; `remainingMs = deadlineMs - chainNowMs`; display format with sub-second digits; local tick 50–100ms between SSE events if needed. - On each decoded **`Buy`**, **refresh `deadline`** (soft `chain-timer` poll or targeted `eth_call`) — timer **resets onchain** at buy; ms clock alone is insufficient. - **Do not** drive `derivePhase` (#48) from WSS ms alone without same sale-start/deadline/ended rules as today. **Last buy / recent buys (ms)** - Ingestion: decode `Buy` with existing decoder → push SSE payload (buyer, charm, tx_hash, log_index, timer fields, `block_timestamp_ms`). - HTTP: multiplex buy events on same SSE channel as clock (or separate routes). - Frontend: prepend SSE buys to recent list; update Last Buy podium row immediately; keep `fetchTimecurveBuys` + `/v1/timecurve/podiums` as reconcile. - Show “N ms ago” from `observed_at_ms` or chain ts ms. **Cross-process (prod split)** - Ingestion: WSS + decode + `pg_notify` small JSON **or** direct internal fan-out if co-located. - HTTP: `LISTEN` → `broadcast` → **SSE** to browsers (no third “SSE indexer”). - **Do not** write full miniBlocks at line rate to Postgres. --- ### Pitfalls (goal-specific) 1. **Provisional buys** may appear before RPC and **disappear on reorg** — handle retraction or confirm animation. 2. **Buy → deadline reset** — must refresh deadline when WSS sees `Buy`, not only advance clock ms. 3. **False equivalence** — ms clock does not mean indexed podiums/buys are current; separate copy/tiers. 4. **Phase / timer authority** — keep sale phase on indexer-anchored `chain-timer` + `ledgerSecIntForPhase` (#48); WSS is display/latency layer. 5. **Volume** — pending logs for one contract is OK; one MegaETH WSS per ingestion deployment; HTTP SSE scales per tab. 6. **Testing** — golden WSS log fixtures in CI; mainnet smoke for pending logs; Anvil remains indexed-only. --- ### Issue scope split (suggested) | Phase | Deliverable | |-------|-------------| | **A** | WSS `logs` (`pending`) + decode `Buy` + SSE to client → **recent buys + Last Buy row** update in ms; merge with indexed poll; provisional vs indexed provenance | | **B** | WSS chain clock (mini-block ts) + SSE `clock_ms` → **hero timer ms** + deadline refresh on `Buy` | | **C** | Mini-block # pill / status fields (pipeline dev test only) | Phase **A + B** are the product; **C** is optional operator/debug chrome. --- ### Acceptance criteria addendum (product) - [ ] With indexer + WSS enabled on mainnet, a `Buy` appears in Simple **live buys / Last Buy** before `max_indexed_block` advances. - [ ] Hero countdown displays **sub-second** behavior aligned with chain ms clock (not 1s stair-steps only). - [ ] After WSS disconnect, timer/buys fall back to indexed/RPC paths without claiming realtime authority. - [ ] Reorg: provisional SSE buy removed or marked invalid when RPC rollback occurs. - [ ] `derivePhase` / hero timer sale rules remain consistent with #48 when indexer `chain-timer` is set. --- ### Decisions table (update) | Item | Decision | |------|----------| | Primary WSS for buys | **`logs` pending**, not full miniBlocks in PG | | Primary WSS for timer | Mini-block / head **timestamp ms** via SSE | | Postgres | RPC ingest authoritative; optional small clock row; **no** full miniBlock payloads | | Browser transport | **SSE** on HTTP indexer | | Mini-block # in UI | **Test / dev** (Phase C), not the product goal |
PlasticDigits commented 2026-05-22 07:12:38 +00:00 (Migrated from gitlab.com)

Tiered realtime — different Hz per SSE stream

Principle: “Realtime” is not one global number. Each UX surface has a different freshness requirement, onchain driver, and cost (WSS decode, PG, SSE fan-out, React renders). Use separate logical streams (multiplexed on one SSE connection with type + seq, or separate URLs) each with an explicit max emit Hz (server) and max render Hz (client).


Stream catalog (proposed)

Stream ID Product surface What “accurate” means WSS / source Server max emit Hz Client render Hz Staleness UX Authority
clock Hero timer ms countdown Chain now tracks mini-block time miniBlocks head ts or clock field 20 (50 ms) — mini-blocks ~100/s; do not forward 100 Hz to browsers 20–30 display tick; interpolate between SSE “Clock stale” if no event >500 ms wss_realtime for clock; deadline from RPC
timer_params Deadline / sale ended / timer cap Matches TimeCurve at a block tag RPC chain-timer batch 1 baseline; immediate on buy / endSale / extension events 1 (or on event only) Fall back to last RPC snapshot RPC / indexed
buy Recent buys, live feed, Last Buy row New Buy visible on execute logs pending, Buy topic Event-driven (no cap); burst-safe coalesce ≤10/s per tx hash On event + merge indexed poll “Live · unconfirmed” until indexed provisional → indexed
podium_last_buy Last Buy podium ranks (live sale) Leader updates on Buy Derived from buy stream or indexer /podiums Event-driven (same as buy); full snapshot ≤2 On buy event; full reconcile ≤0.5 Show provisional leader badge PG prediction + WSS overlay
podium_snapshot Other podium rows (WarBow, streak, booster) Indexed metrics Postgres + existing polls 0.5–1 SSE optional; keep HTTP poll OK 1 Indexer offline copy indexed
warbow_feed Arena battle feed BP-moving events Indexer HTTP today + watches 2–5 (Arena already ~1.5s backoff) 2–5 Existing offline banner indexed
chain_status Agent card: indexed block, WSS alive Ops / pipeline test GET /v1/status + WSS health 0.2–1 1 Separate from game clock indexed + ops
miniblock_debug Dev: mini-block # pill Pipe health WSS mini-block # 2–5 (test only) 2–5 Hidden in prod wss_realtime

Why not one SSE at 100 Hz?

  • MegaETH can emit ~100 mini-blocks/s; fan-out to N browser tabs × 100 Hz wastes CPU and breaks a11y.
  • Buys are sparse — event-driven beats polling at 100 Hz.
  • Deadline changes only on buys, extensions, endSale, etc. — 1 Hz + triggers is enough.
  • React hero + podium should not re-render at 100 Hz.

Multiplexed SSE envelope (sketch)

{ "type": "clock", "seq": 12001, "chain_timestamp_ms": 1710000000123, "mini_block": 999 }
{ "type": "buy", "seq": 12002, "confirmation": "wss_realtime", "tx_hash": "0x…", … }
{ "type": "timer_params", "seq": 12003, "deadline_sec": "…", "trigger": "buy" }

Server-side per-type throttle + monotonic seq for gap detection (client: if seq jumps >1, soft-refetch snapshot endpoints).


Client subscription policy

  • Simple /timecurve: subscribe clock + buy + timer_params (+ podium_last_buy if not folded into buy).
  • Arena: add warbow_feed at 2–5 Hz; do not subscribe arena to 20 Hz clock unless arena shows ms timer (otherwise share one clock connection app-wide).
  • Protocol / static pages: no SSE (or chain_status only).

Use one EventSource per tab with typed dispatch, not multiple MegaETH WSS.


Ingestion → HTTP fan-out rates

From ingestion To HTTP (NOTIFY / internal) To browser SSE
WSS mini-block ts Throttle 20/s 20/s clock
WSS Buy log Immediate (dedupe by tx+log) Immediate buy
RPC chain-timer poll 1/s + on buy timer_params 1/s or on change
PG ingest commit NOTIFY on new buy row Optional buy confirmation: indexed

Pitfalls

  1. Single global throttle — starves buy events while flooding clock.
  2. Rendering WSS clock at 100 Hz — battery + layout thrash; cap client at 20–30 Hz.
  3. Using clock Hz for podium — WarBow/streak need indexed rates, not mini-block rate.
  4. Missing timer_params on Buy — ms clock correct but deadline wrong after reset.
  5. Coalescing buys — multiple logs one tx: coalesce per tx_hash, not one global bucket.

Suggested issue subtasks

  • Document stream IDs + Hz in docs/indexer/design.md and OpenAPI/SSE spec
  • Implement server-side per-type throttle (token bucket per stream)
  • Frontend RealtimeSSEProvider with typed reducers + per-stream render caps
  • Metrics: sse_emit_dropped{stream}, wss_staleness_ms{stream}
## Tiered realtime — different Hz per SSE stream **Principle:** “Realtime” is not one global number. Each UX surface has a different **freshness requirement**, **onchain driver**, and **cost** (WSS decode, PG, SSE fan-out, React renders). Use **separate logical streams** (multiplexed on one SSE connection with `type` + `seq`, or separate URLs) each with an explicit **max emit Hz** (server) and **max render Hz** (client). --- ### Stream catalog (proposed) | Stream ID | Product surface | What “accurate” means | WSS / source | Server max emit Hz | Client render Hz | Staleness UX | Authority | |-----------|---------------|----------------------|--------------|-------------------|------------------|--------------|-----------| | `clock` | Hero timer ms countdown | Chain **now** tracks mini-block time | `miniBlocks` head ts **or** clock field | **20** (50 ms) — mini-blocks ~100/s; **do not** forward 100 Hz to browsers | **20–30** display tick; interpolate between SSE | “Clock stale” if no event **>500 ms** | `wss_realtime` for clock; **deadline** from RPC | | `timer_params` | Deadline / sale ended / timer cap | Matches `TimeCurve` at a block tag | RPC `chain-timer` batch | **1** baseline; **immediate** on `buy` / `endSale` / extension events | **1** (or on event only) | Fall back to last RPC snapshot | **RPC / indexed** | | `buy` | Recent buys, live feed, Last Buy row | New `Buy` visible **on execute** | `logs` **pending**, `Buy` topic | **Event-driven** (no cap); burst-safe coalesce **≤10/s** per tx hash | **On event** + merge indexed poll | “Live · unconfirmed” until indexed | **provisional** → **indexed** | | `podium_last_buy` | Last Buy podium ranks (live sale) | Leader updates on `Buy` | Derived from `buy` stream **or** indexer `/podiums` | **Event-driven** (same as `buy`); full snapshot **≤2** | On `buy` event; full reconcile **≤0.5** | Show provisional leader badge | PG prediction + WSS overlay | | `podium_snapshot` | Other podium rows (WarBow, streak, booster) | Indexed metrics | Postgres + existing polls | **0.5–1** SSE optional; keep HTTP poll OK | **1** | Indexer offline copy | **indexed** | | `warbow_feed` | Arena battle feed | BP-moving events | Indexer HTTP today + watches | **2–5** (Arena already ~1.5s backoff) | **2–5** | Existing offline banner | **indexed** | | `chain_status` | Agent card: indexed block, WSS alive | Ops / pipeline test | `GET /v1/status` + WSS health | **0.2–1** | **1** | Separate from game clock | **indexed** + ops | | `miniblock_debug` | Dev: mini-block # pill | Pipe health | WSS mini-block # | **2–5** (test only) | **2–5** | Hidden in prod | `wss_realtime` | --- ### Why not one SSE at 100 Hz? - MegaETH can emit **~100 mini-blocks/s**; fan-out to N browser tabs × 100 Hz wastes CPU and breaks a11y. - **Buys** are **sparse** — event-driven beats polling at 100 Hz. - **Deadline** changes **only** on buys, extensions, `endSale`, etc. — 1 Hz + triggers is enough. - React hero + podium should not re-render at 100 Hz. --- ### Multiplexed SSE envelope (sketch) ```json { "type": "clock", "seq": 12001, "chain_timestamp_ms": 1710000000123, "mini_block": 999 } { "type": "buy", "seq": 12002, "confirmation": "wss_realtime", "tx_hash": "0x…", … } { "type": "timer_params", "seq": 12003, "deadline_sec": "…", "trigger": "buy" } ``` Server-side per-type throttle + monotonic `seq` for gap detection (client: if `seq` jumps >1, soft-refetch snapshot endpoints). --- ### Client subscription policy - **Simple `/timecurve`:** subscribe `clock` + `buy` + `timer_params` (+ `podium_last_buy` if not folded into `buy`). - **Arena:** add `warbow_feed` at **2–5 Hz**; do not subscribe arena to 20 Hz `clock` unless arena shows ms timer (otherwise share one `clock` connection app-wide). - **Protocol / static pages:** no SSE (or `chain_status` only). Use one `EventSource` per tab with typed dispatch, not multiple MegaETH WSS. --- ### Ingestion → HTTP fan-out rates | From ingestion | To HTTP (NOTIFY / internal) | To browser SSE | |----------------|----------------------------|----------------| | WSS mini-block ts | Throttle **20/s** | **20/s** `clock` | | WSS `Buy` log | Immediate (dedupe by tx+log) | Immediate `buy` | | RPC `chain-timer` poll | **1/s** + on buy | `timer_params` **1/s** or on change | | PG ingest commit | NOTIFY on new buy row | Optional `buy` `confirmation: indexed` | --- ### Pitfalls 1. **Single global throttle** — starves `buy` events while flooding `clock`. 2. **Rendering WSS clock at 100 Hz** — battery + layout thrash; cap client at 20–30 Hz. 3. **Using `clock` Hz for podium** — WarBow/streak need **indexed** rates, not mini-block rate. 4. **Missing `timer_params` on Buy** — ms clock correct but deadline wrong after reset. 5. **Coalescing buys** — multiple logs one tx: coalesce per `tx_hash`, not one global bucket. --- ### Suggested issue subtasks - [ ] Document stream IDs + Hz in `docs/indexer/design.md` and OpenAPI/SSE spec - [ ] Implement server-side per-type throttle (token bucket per stream) - [ ] Frontend `RealtimeSSEProvider` with typed reducers + per-stream render caps - [ ] Metrics: `sse_emit_dropped{stream}`, `wss_staleness_ms{stream}`
PlasticDigits commented 2026-05-22 07:13:53 +00:00 (Migrated from gitlab.com)

Realtime tiers — per-stream Hz (implementation comment)

Principle: “Realtime” is not one global number. Each UX surface has different freshness needs, onchain drivers, and cost (WSS decode, PG, SSE fan-out, React). Use separate logical SSE streams (one EventSource, multiplexed by type + seq) with explicit server max emit Hz and client max render Hz.

MegaETH can emit ~100 mini-blocks/s; browsers and Arena podiums must not subscribe to a single 100 Hz firehose.


Stream catalog

Stream ID Product surface What “accurate” means WSS / source Server max emit Hz Client render Hz Staleness UX Authority
clock Hero timer (ms countdown) Chain now tracks mini-block time miniBlocks head timestamp 20 (50 ms) — throttle from ~100/s node rate 20–30; interpolate between events “Clock stale” if no event >500 ms wss_realtime; deadline from RPC
timer_params Deadline, ended, timer cap Matches TimeCurve at block tag RPC chain-timer 1 baseline; immediate on buy / endSale / extension 1 or on event only Last RPC snapshot RPC / indexed
buy Recent buys, live feed, Last Buy row Buy visible on execute logs pending, Buy topic Event-driven; coalesce bursts ≤10/s per tx On event + indexed poll reconcile “Live · unconfirmed” until indexed provisional → indexed
podium_last_buy Last Buy podium (live sale) Leader updates on Buy From buy stream or /podiums Event-driven with buy; snapshot ≤2 On buy; reconcile ≤0.5 Provisional leader badge PG + WSS overlay
podium_snapshot WarBow, streak, booster rows Indexed metrics Postgres / HTTP 0.5–1 SSE optional; HTTP poll OK ~1 Indexer offline copy indexed
warbow_feed Arena battle feed BP-moving events Indexer (today ~1.5s backoff) 2–5 2–5 Existing offline UX indexed
chain_status Agent card: indexed block, WSS alive Ops / pipeline /v1/status + WSS health 0.2–1 1 Separate from game clock indexed + ops
miniblock_debug Dev: mini-block # pill Pipe health WSS mini-block # 2–5 2–5 Hide in prod wss_realtime

Why not one SSE at 100 Hz?

  • Clock can be sampled at 20 Hz; still feels ms-smooth with interpolation.
  • Buys are sparse — event-driven beats any fixed poll.
  • Deadline only changes on buys / end sale / extensions — 1 Hz + triggers.
  • Hero + podium must not re-render at 100 Hz (battery, a11y, layout).

Multiplexed SSE envelope

{ "type": "clock", "seq": 12001, "chain_timestamp_ms": 1710000000123, "mini_block": 999 }
{ "type": "buy", "seq": 12002, "confirmation": "wss_realtime", "tx_hash": "0x…", … }
{ "type": "timer_params", "seq": 12003, "deadline_sec": "…", "trigger": "buy" }
  • Monotonic seq per connection for gap detection → soft-refetch snapshot endpoints if gap detected.
  • Server: per-type token bucket throttle (do not put buy in the same bucket as clock).

Client subscription policy

Route / surface Subscribe
Simple /timecurve clock + buy + timer_params (+ podium_last_buy if not folded into buy)
Arena warbow_feed at 2–5 Hz; share app-wide clock only if Arena shows ms timer
Protocol / static No SSE (or chain_status only)

One EventSource per tab → typed dispatch. Never per-user MegaETH WSS from the browser.


Ingestion → HTTP → browser rates

Ingestion receives To HTTP (NOTIFY / internal) To browser SSE
WSS mini-block ts Throttle 20/s clock 20/s
WSS Buy log Immediate (dedupe tx_hash + log_index) buy immediate
RPC chain-timer 1/s + on buy timer_params 1/s or on change
PG ingest commit NOTIFY optional buy with confirmation: indexed

Rules of thumb

  1. clock ≠ buy — never throttle buys behind the clock bucket.
  2. timer_params on every buy — ms clock without fresh deadline is wrong after timer reset.
  3. Arena ≠ Simple Hz — WarBow feed stays 2–5 Hz, not 20 Hz clock.
  4. Phase (#48) — still from indexer chain-timer + ledgerSecIntForPhase; WSS is latency/display layer.
  5. Coalesce per tx_hash, not one global coalesce bucket.

Pitfalls

  • Single global throttle starves buy while flooding clock.
  • 100 Hz React on hero timer (battery / a11y).
  • Using mini-block rate for WarBow/streak podiums (need indexed cadence).
  • Missing timer_params push when Buy arrives.
  • Provisional buy without reorg retraction on SSE.

Product context (this issue)

Real goals: ms hero timer + ms last-buy / live buys. Mini-block # in UI = pipeline test (miniblock_debug stream only). Full miniBlocks in Postgres not required; use logs pending for buys + throttled clock for timer.

Scope: Phase A = buy stream; B = clock + timer_params; C = miniblock_debug / chain_status.


Implementation checklist

  • Document stream IDs + Hz in docs/indexer/design.md + SSE spec
  • Server per-type throttle (token bucket)
  • Frontend RealtimeSSEProvider — typed reducers + per-stream render caps
  • Metrics: sse_emit_dropped{stream}, wss_staleness_ms{stream}
## Realtime tiers — per-stream Hz (implementation comment) **Principle:** “Realtime” is not one global number. Each UX surface has different freshness needs, onchain drivers, and cost (WSS decode, PG, SSE fan-out, React). Use **separate logical SSE streams** (one `EventSource`, multiplexed by `type` + `seq`) with explicit **server max emit Hz** and **client max render Hz**. MegaETH can emit ~**100 mini-blocks/s**; browsers and Arena podiums must **not** subscribe to a single 100 Hz firehose. --- ### Stream catalog | Stream ID | Product surface | What “accurate” means | WSS / source | Server max emit Hz | Client render Hz | Staleness UX | Authority | |-----------|---------------|----------------------|--------------|-------------------|------------------|--------------|-----------| | `clock` | Hero timer (ms countdown) | Chain **now** tracks mini-block time | `miniBlocks` head timestamp | **20** (50 ms) — throttle from ~100/s node rate | **20–30**; interpolate between events | “Clock stale” if no event **>500 ms** | `wss_realtime`; **deadline** from RPC | | `timer_params` | Deadline, `ended`, timer cap | Matches `TimeCurve` at block tag | RPC `chain-timer` | **1** baseline; **immediate** on `buy` / `endSale` / extension | **1** or on event only | Last RPC snapshot | **RPC / indexed** | | `buy` | Recent buys, live feed, Last Buy row | `Buy` visible on execute | `logs` **pending**, `Buy` topic | **Event-driven**; coalesce bursts **≤10/s** per tx | **On event** + indexed poll reconcile | “Live · unconfirmed” until indexed | **provisional** → **indexed** | | `podium_last_buy` | Last Buy podium (live sale) | Leader updates on `Buy` | From `buy` stream or `/podiums` | **Event-driven** with `buy`; snapshot **≤2** | On `buy`; reconcile **≤0.5** | Provisional leader badge | PG + WSS overlay | | `podium_snapshot` | WarBow, streak, booster rows | Indexed metrics | Postgres / HTTP | **0.5–1** SSE optional; HTTP poll OK | **~1** | Indexer offline copy | **indexed** | | `warbow_feed` | Arena battle feed | BP-moving events | Indexer (today ~1.5s backoff) | **2–5** | **2–5** | Existing offline UX | **indexed** | | `chain_status` | Agent card: indexed block, WSS alive | Ops / pipeline | `/v1/status` + WSS health | **0.2–1** | **1** | Separate from game clock | **indexed** + ops | | `miniblock_debug` | Dev: mini-block # pill | Pipe health | WSS mini-block # | **2–5** | **2–5** | Hide in prod | `wss_realtime` | --- ### Why not one SSE at 100 Hz? - **Clock** can be sampled at 20 Hz; still feels ms-smooth with interpolation. - **Buys** are sparse — event-driven beats any fixed poll. - **Deadline** only changes on buys / end sale / extensions — **1 Hz + triggers**. - Hero + podium must not re-render at 100 Hz (battery, a11y, layout). --- ### Multiplexed SSE envelope ```json { "type": "clock", "seq": 12001, "chain_timestamp_ms": 1710000000123, "mini_block": 999 } { "type": "buy", "seq": 12002, "confirmation": "wss_realtime", "tx_hash": "0x…", … } { "type": "timer_params", "seq": 12003, "deadline_sec": "…", "trigger": "buy" } ``` - Monotonic **`seq`** per connection for gap detection → soft-refetch snapshot endpoints if gap detected. - Server: **per-type token bucket** throttle (do not put `buy` in the same bucket as `clock`). --- ### Client subscription policy | Route / surface | Subscribe | |-----------------|-----------| | **Simple** `/timecurve` | `clock` + `buy` + `timer_params` (+ `podium_last_buy` if not folded into `buy`) | | **Arena** | `warbow_feed` at **2–5 Hz**; share app-wide `clock` only if Arena shows ms timer | | **Protocol** / static | No SSE (or `chain_status` only) | One **`EventSource`** per tab → typed dispatch. **Never** per-user MegaETH WSS from the browser. --- ### Ingestion → HTTP → browser rates | Ingestion receives | To HTTP (NOTIFY / internal) | To browser SSE | |--------------------|----------------------------|----------------| | WSS mini-block ts | Throttle **20/s** | `clock` **20/s** | | WSS `Buy` log | Immediate (dedupe `tx_hash` + `log_index`) | `buy` immediate | | RPC `chain-timer` | **1/s** + on buy | `timer_params` **1/s** or on change | | PG ingest commit | NOTIFY optional | `buy` with `confirmation: indexed` | --- ### Rules of thumb 1. **`clock` ≠ `buy`** — never throttle buys behind the clock bucket. 2. **`timer_params` on every `buy`** — ms clock without fresh `deadline` is wrong after timer reset. 3. **Arena ≠ Simple Hz** — WarBow feed stays **2–5 Hz**, not 20 Hz clock. 4. **Phase (#48)** — still from indexer `chain-timer` + `ledgerSecIntForPhase`; WSS is latency/display layer. 5. **Coalesce** per `tx_hash`, not one global coalesce bucket. --- ### Pitfalls - Single global throttle starves `buy` while flooding `clock`. - 100 Hz React on hero timer (battery / a11y). - Using mini-block rate for WarBow/streak podiums (need **indexed** cadence). - Missing `timer_params` push when `Buy` arrives. - Provisional buy without reorg retraction on SSE. --- ### Product context (this issue) Real goals: **ms hero timer** + **ms last-buy / live buys**. Mini-block # in UI = **pipeline test** (`miniblock_debug` stream only). Full miniBlocks in Postgres **not** required; use **`logs` pending** for buys + throttled clock for timer. **Scope:** Phase **A** = `buy` stream; **B** = `clock` + `timer_params`; **C** = `miniblock_debug` / `chain_status`. --- ### Implementation checklist - [ ] Document stream IDs + Hz in `docs/indexer/design.md` + SSE spec - [ ] Server per-type throttle (token bucket) - [ ] Frontend `RealtimeSSEProvider` — typed reducers + per-stream render caps - [ ] Metrics: `sse_emit_dropped{stream}`, `wss_staleness_ms{stream}`
PlasticDigits commented 2026-05-30 13:24:02 +00:00 (Migrated from gitlab.com)

Outdated due to upgrade from timecurve to timearena. WSS and SSE should be investigated but above plan must be updated first

Outdated due to upgrade from timecurve to timearena. WSS and SSE should be investigated but above plan must be updated first
ghost1 commented 2026-06-03 13:22:15 +00:00 (Migrated from gitlab.com)

mentioned in commit f50cc8e1ce

mentioned in commit f50cc8e1ce5dad67e43d528c0b704b568237c284
PlasticDigits commented 2026-06-03 13:22:23 +00:00 (Migrated from gitlab.com)

mentioned in merge request !411

mentioned in merge request !411
PlasticDigits commented 2026-06-03 13:30:11 +00:00 (Migrated from gitlab.com)

mentioned in commit c930aa98df

mentioned in commit c930aa98df89ce17facd112b794ee75fec52971e
PlasticDigits commented 2026-06-03 13:49:17 +00:00 (Migrated from gitlab.com)

mentioned in merge request !415

mentioned in merge request !415
PlasticDigits commented 2026-06-07 11:52:00 +00:00 (Migrated from gitlab.com)

"## Verification report \u2014 agent:verify (2026-06-07)\n\nIssue: https://gitlab.com/PlasticDigits/yieldomega/-/work_items/237\n\nSummary: Phase 1 MegaETH miniBlocks WSS + frontend realtime head pill is not implemented (deferred per issue comment 2026-05-30 \u2014 Arena v2 replan required). Deferred guardrails, docs, and RPC ingest path PASS. Issue acceptance criteria for shipped Phase 1 FAIL until implementation lands.\n\n---\n\n### Acceptance criteria (Phase 1)\n\n| Criterion | Result | Evidence |\n|-----------|--------|----------|\n| Indexer subscribes to miniBlocks on MegaETH mainnet WSS when enabled; keepalive documented and implemented | FAIL | No WSS client in indexer/src \u2014 rg 'miniBlocks\\|INDEXER_WSS\\|eth_subscribe' indexer/src \u2192 empty. INDEXER_WSS_URL / INDEXER_WSS_ENABLED documented as not wired in indexer/README.md. |\n| RPC ingestion + reorg behavior unchanged | PASS | bash scripts/verify-issue-237-wss-deferred.sh (includes cargo test \u2014 31 unit + 4 integration tests). cargo test --test integration_stage2 \u2014 4 passed. cargo clippy --all-targets -- -D warnings \u2014 clean. |\n| Status/realtime API exposes WSS head with confirmation: wss_realtime separate from max_indexed_block / chain_pointer | FAIL | No GET /v1/realtime/* routes. api.rs has no wss_realtime / mini_block fields. |\n| Agent card shows two live readouts (indexed block + websockets mini-block head) | FAIL | IndexerStatusBar.tsx shows indexed block only \u2014 no WSS pill, no data-testid hooks for WSS head. |\n| Docs: indexer/README.md, docs/indexer/design.md \u2014 dual-path diagram + mainnet-only WSS testing note | PASS (deferred docs) | Design \u00a7237 documents RPC vs WSS vs browser SSE table, mainnet-only (4326) note, Arena v2 replan pointer. Invariants INV-INDEXER-237-* / INV-FRONTEND-237-WSS-HEAD-PILL in docs/testing/invariants-and-business-logic.md. Manual QA \u00a7237 in docs/testing/manual-qa-checklists.md. |\n\n---\n\n### Automated checks\n\n| Check | Command | Result |\n|-------|---------|--------|\n| Deferred guardrails | bash scripts/verify-issue-237-wss-deferred.sh | PASS |\n| Indexer clippy | cd indexer && cargo clippy --all-targets -- -D warnings | PASS |\n| Indexer integration | cargo test --test integration_stage2 (native PG :5433) | PASS (4/4) |\n| Frontend | cd frontend && npm run typecheck && npm run lint && npm test | PASS (559 tests) |\n| Postgres toolchain | bash scripts/verify-cloud-postgres.sh | PASS |\n\n---\n\n### Manual QA (\u00a7237)\n\n| Step | Result |\n|------|--------|\n| Agent card \u2014 indexed pill | PASS |\n| Agent card \u2014 WSS pill | FAIL \u2014 not implemented |\n| Kill WSS upstream | FAIL \u2014 not implemented (blocked) |\n| Anvil local stack without WSS env | PASS |\n\nMainnet WSS smoke: SKIP \u2014 no INDEXER_WSS_URL secret / feature not wired.\n\n---\n\n### Invariants (deferred state)\n\n- INV-INDEXER-237-WSS-NO-WRITE \u2014 PASS\n- INV-INDEXER-237-STATUS-SPLIT \u2014 PASS\n- INV-INDEXER-237-REALTIME-ROUTES \u2014 PASS\n- INV-FRONTEND-237-WSS-HEAD-PILL \u2014 PASS\n- INV-INDEXER-237-ANVIL-GRACEFUL \u2014 PASS\n\n---\n\n### Outcome\n\nIssue remains open. Phase 1 acceptance criteria are not met. Repo correctly documents deferral and keeps RPC ingest authoritative. No doc/invariant fixes required this pass.\n\nFollow-up for agent:implement: Replan WSS/SSE stream catalog against Arena v2 surfaces (issue comments 2026-05-22 / 2026-05-30), then implement Phase 1 per issue body \u2014 starting with ingestion WSS task + GET /v1/realtime/mini-block-head (or SSE) + second agent-card pill on IndexerStatusBar / ArenaSimpleAgentCard.\n"

"## Verification report \u2014 agent:verify (2026-06-07)\n\nIssue: https://gitlab.com/PlasticDigits/yieldomega/-/work_items/237\n\n**Summary:** Phase 1 MegaETH `miniBlocks` WSS + frontend realtime head pill is **not implemented** (deferred per issue comment 2026-05-30 \u2014 Arena v2 replan required). Deferred guardrails, docs, and RPC ingest path **PASS**. Issue acceptance criteria for shipped Phase 1 **FAIL** until implementation lands.\n\n---\n\n### Acceptance criteria (Phase 1)\n\n| Criterion | Result | Evidence |\n|-----------|--------|----------|\n| Indexer subscribes to **`miniBlocks`** on MegaETH mainnet WSS when enabled; keepalive documented and implemented | **FAIL** | No WSS client in `indexer/src` \u2014 `rg 'miniBlocks\\|INDEXER_WSS\\|eth_subscribe' indexer/src` \u2192 empty. `INDEXER_WSS_URL` / `INDEXER_WSS_ENABLED` documented as **not wired** in [`indexer/README.md`](indexer/README.md). |\n| RPC ingestion + reorg behavior **unchanged** | **PASS** | `bash scripts/verify-issue-237-wss-deferred.sh` (includes `cargo test` \u2014 31 unit + 4 integration tests). `cargo test --test integration_stage2` \u2014 4 passed. `cargo clippy --all-targets -- -D warnings` \u2014 clean. |\n| Status/realtime API exposes WSS head with **`confirmation: wss_realtime`** separate from **`max_indexed_block` / `chain_pointer`** | **FAIL** | No `GET /v1/realtime/*` routes. `api.rs` has no `wss_realtime` / `mini_block` fields. |\n| Agent card shows **two** live readouts (indexed block + websockets mini-block head) | **FAIL** | [`IndexerStatusBar.tsx`](frontend/src/components/IndexerStatusBar.tsx) shows indexed block only \u2014 no WSS pill, no `data-testid` hooks for WSS head. |\n| Docs: [`indexer/README.md`](indexer/README.md), [`docs/indexer/design.md`](docs/indexer/design.md) \u2014 dual-path diagram + mainnet-only WSS testing note | **PASS** (deferred docs) | Design \u00a7237 documents RPC vs WSS vs browser SSE table, mainnet-only (`4326`) note, Arena v2 replan pointer. Invariants **`INV-INDEXER-237-*`** / **`INV-FRONTEND-237-WSS-HEAD-PILL`** in [`docs/testing/invariants-and-business-logic.md`](docs/testing/invariants-and-business-logic.md). Manual QA \u00a7237 in [`docs/testing/manual-qa-checklists.md`](docs/testing/manual-qa-checklists.md). |\n\n---\n\n### Automated checks\n\n| Check | Command | Result |\n|-------|---------|--------|\n| Deferred guardrails | `bash scripts/verify-issue-237-wss-deferred.sh` | **PASS** |\n| Indexer clippy | `cd indexer && cargo clippy --all-targets -- -D warnings` | **PASS** |\n| Indexer integration | `cargo test --test integration_stage2` (native PG `:5433`) | **PASS** (4/4) |\n| Frontend | `cd frontend && npm run typecheck && npm run lint && npm test` | **PASS** (559 tests) |\n| Postgres toolchain | `bash scripts/verify-cloud-postgres.sh` | **PASS** |\n\n---\n\n### Manual QA (\u00a7237)\n\n| Step | Result |\n|------|--------|\n| Agent card \u2014 indexed pill | **PASS** |\n| Agent card \u2014 WSS pill | **FAIL** \u2014 not implemented |\n| Kill WSS upstream | **FAIL** \u2014 not implemented (blocked) |\n| Anvil local stack without WSS env | **PASS** |\n\nMainnet WSS smoke: **SKIP** \u2014 no `INDEXER_WSS_URL` secret / feature not wired.\n\n---\n\n### Invariants (deferred state)\n\n- **`INV-INDEXER-237-WSS-NO-WRITE`** \u2014 **PASS**\n- **`INV-INDEXER-237-STATUS-SPLIT`** \u2014 **PASS**\n- **`INV-INDEXER-237-REALTIME-ROUTES`** \u2014 **PASS**\n- **`INV-FRONTEND-237-WSS-HEAD-PILL`** \u2014 **PASS**\n- **`INV-INDEXER-237-ANVIL-GRACEFUL`** \u2014 **PASS**\n\n---\n\n### Outcome\n\n**Issue remains open.** Phase 1 acceptance criteria are not met. Repo correctly documents deferral and keeps RPC ingest authoritative. No doc/invariant fixes required this pass.\n\n**Follow-up for `agent:implement`:** Replan WSS/SSE stream catalog against Arena v2 surfaces (issue comments 2026-05-22 / 2026-05-30), then implement Phase 1 per issue body \u2014 starting with ingestion WSS task + `GET /v1/realtime/mini-block-head` (or SSE) + second agent-card pill on `IndexerStatusBar` / `ArenaSimpleAgentCard`.\n"
PlasticDigits commented 2026-06-08 08:45:13 +00:00 (Migrated from gitlab.com)

mentioned in commit 23d03125f3

mentioned in commit 23d03125f3113494f676d744a8b8117aee354840
PlasticDigits commented 2026-06-08 08:45:13 +00:00 (Migrated from gitlab.com)

mentioned in commit fd4fe72ab4

mentioned in commit fd4fe72ab40c423da00f4d724e84cd5eb8873050
PlasticDigits commented 2026-06-10 09:21:05 +00:00 (Migrated from gitlab.com)

mentioned in issue #306

mentioned in issue #306
PlasticDigits commented 2026-06-13 07:02:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #309

mentioned in issue #309
PlasticDigits commented 2026-06-19 03:09:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #331

mentioned in issue #331
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#237
No description provided.