Mainnet archive RPC + snapshot publisher
Stand up the mainnet equivalent of testnet's snapshot-publisher service. This node serves two roles:
- Archive RPC at
archive.sanect.com— full history, used byexplorer-backend(NODE_URL) andstaking-app(NEXT_PUBLIC_SCAN_RPC_URL) so heavyeth_getLogsscans never hit the primary validator (per the 2026-06-20 incident). - Snapshot publisher — uploads compressed
data/tarballs to R2 every 6h so new joiners can fast-sync.
Both roles run on the same node because both want PRUNING=nothing and neither participates in consensus.
Why it matters
- Incident 2026-06-14: snapshot upload on the primary validator triggered I/O contention, cascading-failure across all 4 testnet nodes, full state loss. Same hardware class as mainnet → same risk.
- Incident 2026-06-20: wallet
Balancepanel hit primary with genesis-to-tipeth_getLogs, OOM-killed the node, 312s consensus halt.
Both fixed by routing heavy reads + snapshot uploads to a dedicated archive node. Mainnet must have this from Day 1 or we'll re-hit one or both incidents under public load.
Step 1 — Cloudflare R2 bucket
- Cloudflare dashboard → R2 → Create bucket
sanect-snapshots-mainnet - R2 → Manage R2 API Tokens → Create token, scope
Object Read & Writeto that bucket. Save Access Key + Secret. - Bucket → Settings → Public access → enable, OR add custom domain
snapshots.sanect.com(CNAME → bucket's public R2.dev URL).
Cost: ~$5/mo for ~50GB of rolling snapshots.
Step 2 — DNS for archive.sanect.com
Cloudflare DNS → add CNAME:
- Name:
archive - Target: (Railway public URL of the service you're about to create — fill in after Step 3 returns it)
- Proxy: Orange-cloud (proxied) — this is HTTPS RPC, Cloudflare proxy is fine
Step 3 — Railway service
- New Service from the
sanect/sanect-noderepo - Branch:
main - Dockerfile:
docker/Dockerfile TARGET_EVM_CHAIN_ID=7628(required). Add it as a normal service variable: Railway passes service variables to Dockerfile builds as build arguments, and the Railway UI no longer has a separate Build args panel. The value is baked into the binary at build time (scripts/fork-cosmos-evm.sh), so after adding or changing it you must redeploy (rebuild). Verify in the service Console:curl -s -X POST localhost:8080/ -H 'content-type: application/json' -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}'must return0x1dcc(7628);0x129ff(76287) means the build did not get it. If it was wrong, also wipe the/datavolume, because state computed with the wrong chain ID is not repaired by a rebuild. Confirmed 2026-10-04 onsnapshot-builder-archive(variable set,eth_chainId=0x1dcc).- Volume: mount at
/data(separate volume — do NOT share with primary) - Custom domain:
archive.sanect.com - Service name:
archive-snapshot-publisher
Env vars
# Chain identity (mainnet)
NETWORK=mainnet
CHAIN_ID=sanect_7628-1
EVM_CHAIN_ID=7628
HOMEDIR=/data/.sanectd
DENOM=asnct
DISPLAY=snct
MONIKER=archive-snapshot-publisher
# Join existing chain (don't bootstrap genesis)
JOIN_NETWORK=true
SEED_NODE_URL=https://rpc.sanect.com
SEED_NODE_ID=<primary's CometBFT node id — get from `sanectd cometbft show-node-id` on the primary Railway shell>
SEED_PEER_HOST=p2p.sanect.com:<port from primary's TCP proxy>
# Archive — keep all history
PRUNING=nothing
# Snapshot publisher role (deletes priv_validator_key.json on every boot
# so the safety gate is satisfied and the upload cron runs)
SNAPSHOT_PUBLISHER=true
SNAPSHOT_UPLOAD_INTERVAL_HOURS=6
SNAPSHOT_KEEP=3
# R2 (set ONLY here, never on validators)
R2_ACCOUNT_ID=<32-char Cloudflare account id>
R2_ACCESS_KEY_ID=<from Step 1>
R2_SECRET_ACCESS_KEY=<from Step 1>
R2_BUCKET=sanect-snapshots-mainnet
R2_PUBLIC_URL=https://snapshots.sanect.comAfter deploy, copy the Railway-assigned public URL and set it as the DNS target in Step 2.
Step 4 — Remove R2 vars from primary
After the archive node is healthy:
- Primary Railway service → Variables tab
- Delete:
R2_ACCOUNT_ID,R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY,R2_BUCKET,R2_PUBLIC_URL,SNAPSHOT_UPLOAD_INTERVAL_HOURS,SNAPSHOT_KEEP - Save → Redeploy
This guarantees the snapshot cron can never fire on the primary, regardless of future env edits.
Step 5 — Wire downstream services to archive.sanect.com
| Service | Variable | Value |
|---|---|---|
explorer-backend | NODE_URL | http://archive-snapshot-publisher.railway.internal:8080 (private mesh; same project) OR https://archive.sanect.com (cross-project) |
staking-app | NEXT_PUBLIC_SCAN_RPC_URL | https://archive.sanect.com (must be public — baked into browser bundle) |
staking-app | NEXT_PUBLIC_POOL_DEPLOY_BLOCK | block number of ShieldedPool v1.4 deploy on mainnet (find in deploy log) |
Redeploy both services after setting.
Step 6 — Verify
# Archive RPC reachable
curl -s https://archive.sanect.com -X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_chainId","id":1}' | jq
# expect: {"jsonrpc":"2.0","id":1,"result":"0x1dcc"} (0x1dcc = 7628)
# Archive node caught up
curl -s https://archive.sanect.com/rpc/status | jq '.result.sync_info.catching_up'
# expect: false
# Snapshot published (wait ≤6h after first boot)
curl -s https://snapshots.sanect.com/LATEST.json | jq
# expect: { height, filename, url, size_bytes, created_at, chain_id: "sanect_7628-1" }If LATEST.json is fresh (created_at within last 6h), archive is healthy. If not, check Railway logs for the snapshot-upload.sh cron output.
Cost
- Railway service: ~$10/mo (archive needs more disk than a pruning node)
- R2 storage: ~$5/mo
- R2 egress: free (Cloudflare R2 doesn't charge egress)
- Total: ~$15/mo — same as testnet. Worth it.
Future joiners use this
Validator/RPC operators joining mainnet set on their service:
SNAPSHOT_MANIFEST_URL=https://snapshots.sanect.com/LATEST.jsonEntrypoint streams + restores the snapshot in 3–5 min instead of replaying from block 0.