Verify: wallet reads bypass the primary validator
Live since 2026-06-20 after the incident in CLAUDE.md "Wallet read paths — never hit the primary validator".
scan-rpc.testnet.sanect.com is a public CNAME (Cloudflare, DNS-only) pointing at the snapshot-publisher Railway service (archive node, PRUNING=nothing). The staking-app uses it for all heavy read-only RPC.
What should hit which RPC
| Method | RPC | Why |
|---|---|---|
eth_sendRawTransaction | rpc.testnet.sanect.com | Tx broadcast needs a validator |
personal_sign, eth_signTypedData_v4 | (no RPC — local MetaMask) | Pure client signing |
eth_getBalance (user's own SNCT) | rpc.testnet.sanect.com | MetaMask polls this on its own — out of our control |
eth_getBalance (ShieldedPool TVL) | scan-rpc.testnet.sanect.com | Our code routes it via getReadProvider() |
eth_call (pool view fns: commitmentCount, verifier) | scan-rpc.testnet.sanect.com | Same |
eth_getLogs, eth_getBlockByNumber (memo scan fallback) | scan-rpc.testnet.sanect.com | Same |
Memo indexer HTTP /api/shielded/memos | scan.testnet.sanect.com (explorer-backend) | Normal path; not RPC |
How to verify after a staking-app deploy
- Open
https://app.testnet.sanect.comin a fresh browser profile - DevTools → Network tab → filter
Fetch/XHR - Connect MetaMask, click 🛡 Shielded → Balance
- Watch the table — within 30 seconds you should see:
- Multiple rows to
scan-rpc.testnet.sanect.com(TVL poll fires immediately, then every 30s) - One or two rows to
scan.testnet.sanect.com/api/shielded/...(indexer status + memos) - Zero rows to
rpc.testnet.sanect.comfrom the panel itself
- Multiple rows to
- Click Make Private and approve a small shield amount
- Now you should see one
eth_sendRawTransactionrow torpc.testnet.sanect.com— that's expected, it's the tx broadcast
If step 4 still shows rpc.testnet.sanect.com taking the read traffic, the env didn't bake into the build:
# In the Railway build logs for the staking-app deploy, look for:
ENV NEXT_PUBLIC_SCAN_RPC_URL=https://scan-rpc.testnet.sanect.com
# Or grep the built bundle:
docker run --rm sanect-staking sh -c 'grep -r scan-rpc /app/.next/static | head -1'If the env is not in the bundle, re-check the Railway build args (NOT just variables — must be added as Build Arg in the service settings so it's available at npm run build time, when the NEXT_PUBLIC_* envs get inlined).
Diagnosing the "red 200" pattern
If /api/shielded/indexer/status shows 200 OK but the row is red, it means the JS fetch threw AFTER headers arrived — usually AbortSignal.timeout firing mid-body on a cold Cloudflare connection. This was the silent trigger for the 2026-06-20 OOM (it flipped useIndexer=false, RPC fallback then scanned from block 0 and OOM-killed the primary).
PR #290 increased the timeout 5s → 12s and added a retry with 600 ms backoff. If you still see red 200s after deploy:
- Cloudflare cold-start latency: the explorer-backend's first request after idle can take 6–8 s. Hit
https://scan.testnet.sanect.com/api/shielded/indexer/statusdirectly in a browser tab first to warm the connection, then reload the staking app. - Real backend slowness:
curl -w "%{time_total}\n" -o /dev/null https://scan.testnet.sanect.com/api/shielded/indexer/status— should be < 1 s under load. If it's not, check the explorer-backend's Postgres query for the status endpoint.
When to update this doc
- ShieldedPool address changes → update
NEXT_PUBLIC_POOL_DEPLOY_BLOCKin Railway and bump CLAUDE.md - Archive node moved to a new host → update the Cloudflare CNAME for
scan-rpc.testnet.sanect.com - Mainnet launch → same pattern applies (
scan-rpc.sanect.comagainst the mainnet archive node)