Mainnet setup guide
Step-by-step from a clean slate to a working sanect_7628-1 mainnet on Railway + a few external NVMe hosts. Pairs with docs/mainnet-launch-checklist.md (higher-level checklist) — that document is the punch list; this one is the actual button-pushing.
Total wall-clock: 6-8 hours of attention. Most steps run in parallel.
Phase 0 — Offline key generation (do these FIRST)
On your laptop, in separate clean terminal sessions so mnemonics don't leak between them:
# Session 1 — deployer (for contract deploys, retired after Day 0)
cast wallet new-mnemonic --words 24
# Session 2 — genesis-treasury role-EOA
cast wallet new-mnemonic --words 24
# Session 3 — protocol-governance role-EOA
cast wallet new-mnemonic --words 24
# Session 4 — ops role-EOA
cast wallet new-mnemonic --words 24For each: paper backup × 2, separate physical locations, import into a hardware wallet (different wallet per role if possible).
Record the addresses (not the mnemonics) in a local notes file — you'll need them in later phases.
| Role | Address |
|---|---|
| deployer | 0x... |
| genesis-treasury | 0x... |
| protocol-governance | 0x... |
| ops | 0x... |
Phase 1 — Patch + push contract scripts
Edit domainapp/contracts/script/Deploy.s.sol lines 24-25:
address constant GENESIS = 0x<genesis-treasury-eoa>;
address constant TREASURY = 0x<genesis-treasury-eoa>;git checkout claude/dev # (or main if you're not branching)
git add domainapp/contracts/script/Deploy.s.sol
git commit -m "fix(domain): mainnet GENESIS + TREASURY addresses"
git pushThe scripts/mainnet/genesis-deploy.sh orchestrator refuses to run until this patch is in.
Phase 2 — Validator hosts (parallel, ~4-6h)
Provision at least 4 NVMe-backed VPS across distinct regions per docs/mainnet-validator-runbook.md. Vultr High Frequency, Hetzner CCX22, or Latitude.sh all work. Suggested: Singapore + Frankfurt + US East + Mumbai.
On each box:
ssh root@<box>
git clone https://github.com/sanect/sanect-node /opt/sanect
cd /opt/sanect
sudo bash scripts/sanect-publish-rpc.shThe wizard handles firewall (22/80/443/46430), DNS reminder, Docker install, container env, Caddy + ACME. Each box gets a fresh priv_validator_key.json — never copy keys between boxes (double-sign jail).
For now, only one validator joins (the "primary" — runs the seed image). The others will join via state sync once the chain is alive.
Phase 3 — DNS + Cloudflare (~1h, parallel)
CNAMEs at Cloudflare. All proxied (orange cloud) EXCEPT p2p.sanect.com which must be DNS-only (grey cloud) for raw TCP:
| Hostname | Target | Cloudflare proxy |
|---|---|---|
rpc.sanect.com | primary validator Railway URL | orange |
scan.sanect.com | blockscout / custom explorer Railway | orange |
app.sanect.com | staking-app Railway | orange |
swap.sanect.com | dex-app Railway | orange |
domain.sanect.com | domain-app Railway | orange |
status.sanect.com | status-app Railway | orange |
archive.sanect.com | snapshot publisher Railway | orange |
snapshots.sanect.com | R2 bucket public URL | orange |
airdrop.sanect.com | airdrop frontend (Day 0+) | orange |
airdrop-api.sanect.com | airdrop indexer | orange |
p2p.sanect.com | Railway TCP proxy ballast.proxy.rlwy.net:46430 | grey (DNS-only) |
Phase 4 — Genesis build + vesting accounts (~1h)
On the primary validator host:
NETWORK=mainnet \
CHAIN_ID=sanect_7628-1 \
EVM_CHAIN_ID=7628 \
MIN_SELF_DELEGATION=1000 \
MONIKER=sanect-mainnet-primary \
bash scripts/localnet.shVerify ~/.sanectd/config/genesis.json has:
chain_id: sanect_7628-1bond_denom: asnct- governance voting period 14 days (NOT testnet's 60s)
- min self-delegation 1000 SNCT
Then add vesting accounts BEFORE the first block per docs/tokenomics.md:
# Genesis-treasury (no vest, holds 400M treasury + receives liquid airdrop bucket)
sanectd genesis add-genesis-account \
<genesis-treasury-eoa> \
700000000000000000000000000asnct \
--home /data/.sanectd
# Foundation: 30M, 48 months linear
sanectd genesis add-genesis-account \
<protocol-governance-eoa> \
30000000000000000000000000asnct \
--vesting-amount 30000000000000000000000000asnct \
--vesting-end-time $(date -d "+48 months" +%s) \
--home /data/.sanectd
# Team: 150M, 12mo cliff + 36mo linear (use periodic vesting)
# Validator Grants: 50M, 1mo cliff + 12mo linear
# Liquidity: 70M, 12mo lock
# (full bucket commands in tokenomics.md)Total must sum to 1,000,000,000 SNCT (= 10^27 asnct).
Then start the primary:
docker compose -f /opt/sanect/docker-compose.yml up -dSanity-check the chain is alive:
curl -s https://<primary-railway-url>/rpc/status | jq '.result.sync_info.latest_block_height'
# expect a number > 0 that increases on a re-queryPhase 5 — Fund deployer + run Genesis Deploy (~30min)
From the genesis-treasury role-EOA hardware wallet, send ~10 SNCT to the deployer:
cast send <deployer-address> --value 10ether \
--rpc-url <primary-railway-url> \
--ledger # or --private-key from your offline exportThen on your laptop (where you have foundry installed):
read -s DEPLOYER_MNEMONIC
export DEPLOYER_PRIVATE_KEY=$(cast wallet private-key --mnemonic "$DEPLOYER_MNEMONIC")
unset DEPLOYER_MNEMONIC
export RPC_URL=<primary-railway-url> # can flip to https://rpc.sanect.com after DNS lands
export SAFE_ADDRESS=<genesis-treasury-eoa>
export POOL_OWNER=<protocol-governance-eoa>
bash scripts/mainnet/genesis-deploy.sh 2>&1 | tee genesis-deploy-$(date +%s).logOrchestrator deploys: Poseidon2 → HonkVerifier → ShieldedPool v1.4 → WSNCT → Factory → Router → TreasuryEmitter → MasterChef → full .snct registrar suite. Captures every address into genesis-addresses.json and verifies pool wiring.
Commit and push genesis-addresses.json so I (Claude) can update CLAUDE.md's deployed-addresses table and generate the Railway env-var sets.
Phase 6 — Wire Railway env vars (~1h)
For each Railway service, add the following env vars. Mainnet defaults work with no extra config because of the env-driven config landed earlier — only set the NEXT_PUBLIC_NETWORK_LABEL=testnet override on testnet redeploys.
staking-app
NEXT_PUBLIC_CHAIN_ID=sanect_7628-1
NEXT_PUBLIC_EVM_CHAIN_ID=7628
NEXT_PUBLIC_NODE_URL=https://rpc.sanect.com
NEXT_PUBLIC_SCAN_RPC_URL=https://archive.sanect.com # archive RPC, NOT primary
NEXT_PUBLIC_SHIELDED_POOL_ADDRESS=<from genesis-addresses.json>
NEXT_PUBLIC_POOL_DEPLOY_BLOCK=<the block ShieldedPool was deployed at>
# Do NOT set FAUCET_PRIVATE_KEY — mainnet has no faucetdex-app
NEXT_PUBLIC_EVM_CHAIN_ID=7628
NEXT_PUBLIC_NODE_URL=https://rpc.sanect.com
NEXT_PUBLIC_EXPLORER_URL=https://scan.sanect.com
NEXT_PUBLIC_DEX_FACTORY=<from genesis-addresses.json>
NEXT_PUBLIC_DEX_ROUTER=<from genesis-addresses.json>
NEXT_PUBLIC_WSNCT=<from genesis-addresses.json>
NEXT_PUBLIC_DEX_INIT_CODE_HASH=<emitted at deploy time>
NEXT_PUBLIC_MASTERCHEF=<from genesis-addresses.json>
NEXT_PUBLIC_TREASURY_EMITTER=<from genesis-addresses.json>
# Bridge addresses left empty — Hyperlane ISM upgrade is Day-7 workdomain-app
NEXT_PUBLIC_EVM_CHAIN_ID=7628
NEXT_PUBLIC_CHAIN_NAME=sanect
NEXT_PUBLIC_NODE_URL=https://rpc.sanect.com
NEXT_PUBLIC_EXPLORER_URL=https://scan.sanect.com
NEXT_PUBLIC_REGISTRY_ADDRESS=<from genesis-addresses.json>
NEXT_PUBLIC_RESOLVER_ADDRESS=<from genesis-addresses.json>
NEXT_PUBLIC_REVERSE_ADDRESS=<from genesis-addresses.json>
NEXT_PUBLIC_PRICE_ORACLE_ADDRESS=<from genesis-addresses.json>
NEXT_PUBLIC_BASE_REGISTRAR_ADDRESS=<from genesis-addresses.json>
NEXT_PUBLIC_SEALED_BID_AUCTION_ADDRESS=<from genesis-addresses.json>
NEXT_PUBLIC_MARKETPLACE_ADDRESS=<from genesis-addresses.json>
NEXT_PUBLIC_RESERVED_ADDRESS=<from genesis-addresses.json>explorer-frontend (custom explorer)
# Mainnet defaults are baked in — empty config is fine
# Backend wiring (private network):
NODE_URL=http://snapshot-publisher.railway.internal:8080blockscout backend + frontend
# Backend
CHAIN_ID=7628
COIN=SNCT
COIN_NAME=SNCT
ETHEREUM_JSONRPC_HTTP_URL=http://snapshot-publisher.railway.internal:8080
INDEXER_DISABLE_EMPTY_BLOCKS_SANITIZER=true
INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER=true
INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER=true
DISABLE_INDEXER_BLOCK_REWARDS=true
POOL_SIZE=20
# Frontend
NEXT_PUBLIC_NETWORK_ID=7628
NEXT_PUBLIC_NETWORK_NAME=sanect
NEXT_PUBLIC_IS_TESTNET=false
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.sanect.com/
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=SNCT
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true
NEXT_PUBLIC_GAS_TRACKER_ENABLED=falsestatus-app
# Mainnet defaults work; just wire Redis for history:
REDIS_URL=${{Redis.REDIS_PRIVATE_URL}}Snapshot publisher (archive RPC + R2 uploader)
NETWORK=mainnet
CHAIN_ID=sanect_7628-1
EVM_CHAIN_ID=7628
PRUNING=nothing # archive node
SNAPSHOT_UPLOAD_INTERVAL_HOURS=6
R2_ACCOUNT_ID=<cloudflare account id>
R2_ACCESS_KEY_ID=<r2 token>
R2_SECRET_ACCESS_KEY=<r2 token>
R2_BUCKET=sanect-snapshots-mainnet # fresh bucket for mainnet
R2_PUBLIC_URL=https://snapshots.sanect.com
SNAPSHOT_KEEP=3airdrop-indexer (Day 0+)
See airdrop-indexer/README.md for the full env set. Key ones:
ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/<key>
SANECT_RPC_URL=http://snapshot-publisher.railway.internal:8080
SANECT_CHAIN_ID=7628
SANECT_START_BLOCK=1
# All contract addresses from genesis-addresses.jsonPhase 7 — Verify each surface (~30min)
| URL | Expected |
|---|---|
https://rpc.sanect.com/ | Returns EVM JSON-RPC responses; eth_chainId returns 0x1dcc (= 7628) |
https://scan.sanect.com | Hero says "Sanect explorer" (NOT "Sanect testnet explorer"); no red testnet banner |
https://status.sanect.com | All endpoints reachable; uptime bars populating |
https://app.sanect.com | Header says "Sanect" (no testnet badge); chain id 7628; Need-SNCT button absent (faucet disabled) |
https://swap.sanect.com | Wallet connects to chain 7628; WSNCT address shown matches genesis-addresses.json |
https://domain.sanect.com | Same chain id; .snct registrar accepts test registrations |
If any of these still show "testnet" or wrong chain id, the Railway env vars need a recheck — those services are env-driven now.
Phase 8 — Hyperlane ISM upgrade (Day 7 follow-up)
Testnet's TrustedRelayerIsm trusts the deployer EOA. Mainnet must move to MessageIdMultisigIsm with a multi-validator set + rotated relayer key. Hyperlane CLI handles ISM deploy — no repo change required. Defer to Week 1; bridge UI stays disabled until done.
Phase 9 — Day-14 Safe migration (per launch-checklist § 2b)
By Day 14: deploy Safe singleton + SafeProxyFactory contracts on mainnet, create 3 Safe proxies, transfer ownership from EOAs to Safes, retire the EOAs. Until Safe Transaction Service supports sanect, signing happens via cast send directly into Safe.execTransaction.
Phase 10 — Launch post + announcements
Channels: X, Telegram (t.me/sanectnetwork), Discord (discord.gg/sanect), blog at sanect.com/blog.
Required disclosures in the launch post:
- EOA ownership for 14 days — "Genesis ownership held by 3 hardware-wallet-backed EOAs; migration to on-chain Safes by Day 14."
- Hyperlane bridge offline at launch — "Cross-chain bridge to Sepolia / Ethereum mainnet will activate after Day 7 ISM upgrade."
- Audit status — "Engaged with [auditor name]; full report at sanect.com/audits when complete."
- Genesis Allocation timing — "Day-0 Genesis Allocation snapshot already taken at
2026-06-21 23:59:59 UTC. Claim portal opens at airdrop.sanect.com on Day 0."