Two access lanes, one DTO contract, /liquidations off the Starknet fan-out.
Every Supabase read in opus_frontend is now classified into one of two enforced lanes,
with a DTO mapping rule that prevents silent zero-coercion of missing values. The
/liquidations page is migrated from N parallel client-side Starknet reads to a single
server-side Supabase RPC.
↳ the incident this closes
The staking page silently shipped 0 TVL and 0% APY because a missing
data source was zero-coerced into the UI instead of surfacing as empty. The architecture work
makes that class of silent degradation impossible to ship without an observability signal
firing — the root fix lives in the DTO contract, not in any one mapper.
↳ invariants now enforced
- No zero-coercion —
null + sourceStatus: "empty", never0 - Server-complete payload — no corrective client fetch
- Credential boundary — service-role never reaches the browser
- Bounded initial payload — heavy pages declare a cap
- Empty ≠ error — fetch failure ≠ "open a trove" CTA
What landed
spec-001 architecture · spec-002 /liquidationsfixes
Ray/Wad scaled server-side; "482 troves" from MAX(trove_id).
get_absorber_apy_history(); single-sourced in SQL.
improvements
aggregate-server-lane for public reads, rls-user-lane for session-bound reads. Mixed-page composition rule.
get_liquidations_page_rows replaces N useTroveInfo calls. Pre-formatted numerics, DB-side filter of closed troves.
Before vs after — /liquidations
dev preview · pre-PR-184 · prod · PR-184 live
The dev preview is built from a branch that does not contain PR #184, so its
/liquidations page still does the legacy client-side Starknet fan-out — one chain
read per trove, after JS hydration. Prod runs PR #184 and gets the same rows in a single
server-side Supabase RPC. Stat badges agree because both read the same chain state; the cost
of getting there is what changed.
dev preview · BEFORE
no PR #184
prod · AFTER
PR #184 live
Alchemy RPC cost savings
pay-as-you-go · $0.45 / 1M CU · starknet 20 CU/call
Honest framing: at today's user volume the absolute savings are modest. The
value is in the shape of the cost curve — it now scales with server-side requests
(1 RPC / page) instead of with active troves on chain (currently 482 → 482 client calls per
user). Doubling on-chain activity no longer doubles per-user RPC cost. The same lane policy
also retires N-fan-out reads on staking, save, and borrow pages; this section measures only
the /liquidations delta we directly captured.
Assumptions & sources
- Pricing — Alchemy PAYG: $0.45 per 1M CU for the first 300M CUs/month, then $0.40/M (alchemy.com/pricing).
- CU cost per call — StarkNet JSON-RPC methods uniformly 20 CU (alchemy compute-unit-costs).
- Request counts — measured via
performance.getEntriesByType('resource')on the warm sample of each URL, filtered to RPC hosts (starknet, alchemy, blastapi, lava.build). - Scope — direct RPC requests on the
/liquidationspage only; does not include staking/save/borrow which also see request reductions under PR #184. - Not included — websocket subscriptions, cache invalidation refetches, manual user-triggered refreshes.
Timing samples
headless chromium · 2026-05-13 · single sample per state| url | sample | ttfb | fcp | dcl | load | resources |
|---|---|---|---|---|---|---|
| prod | cold | 8.32 s | 13.08 s | 11.38 s | 14.38 s | 130 |
| prod | warm | 8.12 s | 8.46 s | 8.33 s | 8.46 s | 129 |
| dev | cold | 15.88 s | 17.50 s | 17.83 s | 18.39 s | 250 |
| dev | warm | 5.20 s | 5.65 s | 12.27 s | 12.29 s | 250 |
Headline: −68% StarkNet chain requests and
−32% time to interactive on /liquidations — measured
against the pre-PR-184 dev preview. Cold samples include CDN cold-start. The payoff isn't in
TTFB; it's in the shape of the request graph: row data now travels in one server-side Supabase
RPC instead of N parallel useTroveInfo chain reads.
Architecture in three lines
↳ lanes
aggregate-server-lane uses the service-role client and is server-only.
rls-user-lane uses the anon/auth client with RLS enforced. Mixed pages compose
both sections with explicit per-section credential ownership.
↳ DTO contract
Mappers transform raw Supabase rows into slice-owned DTOs. Missing sources surface as
sourceStatus: "empty" with value: null. Raw rows do not cross
the mapper boundary into page or UI code.
↳ observability
Five named signals at lane, page, and DTO scope:
unexpected_zero_aggregate_value,
empty_hydration_for_prefetched_query,
aggregate_lane_used_in_client_context,
rls_lane_elevated_access_exception,
unbounded_initial_payload.