Skip to content

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:

bash
# 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 24

For 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.

RoleAddress
deployer0x...
genesis-treasury0x...
protocol-governance0x...
ops0x...

Phase 1 — Patch + push contract scripts

Edit domainapp/contracts/script/Deploy.s.sol lines 24-25:

solidity
address constant GENESIS  = 0x<genesis-treasury-eoa>;
address constant TREASURY = 0x<genesis-treasury-eoa>;
bash
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 push

The 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:

bash
ssh root@<box>
git clone https://github.com/sanect/sanect-node /opt/sanect
cd /opt/sanect
sudo bash scripts/sanect-publish-rpc.sh

The wizard handles firewall (22/80/443/46430), DNS reminder, Docker install, container env, Caddy + ACME. Each box gets a fresh priv_validator_key.jsonnever 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:

HostnameTargetCloudflare proxy
rpc.sanect.comprimary validator Railway URLorange
scan.sanect.comblockscout / custom explorer Railwayorange
app.sanect.comstaking-app Railwayorange
swap.sanect.comdex-app Railwayorange
domain.sanect.comdomain-app Railwayorange
status.sanect.comstatus-app Railwayorange
archive.sanect.comsnapshot publisher Railwayorange
snapshots.sanect.comR2 bucket public URLorange
airdrop.sanect.comairdrop frontend (Day 0+)orange
airdrop-api.sanect.comairdrop indexerorange
p2p.sanect.comRailway TCP proxy ballast.proxy.rlwy.net:46430grey (DNS-only)

Phase 4 — Genesis build + vesting accounts (~1h)

On the primary validator host:

bash
NETWORK=mainnet \
CHAIN_ID=sanect_7628-1 \
EVM_CHAIN_ID=7628 \
MIN_SELF_DELEGATION=1000 \
MONIKER=sanect-mainnet-primary \
bash scripts/localnet.sh

Verify ~/.sanectd/config/genesis.json has:

  • chain_id: sanect_7628-1
  • bond_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:

bash
# 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:

bash
docker compose -f /opt/sanect/docker-compose.yml up -d

Sanity-check the chain is alive:

bash
curl -s https://<primary-railway-url>/rpc/status | jq '.result.sync_info.latest_block_height'
# expect a number > 0 that increases on a re-query

Phase 5 — Fund deployer + run Genesis Deploy (~30min)

From the genesis-treasury role-EOA hardware wallet, send ~10 SNCT to the deployer:

bash
cast send <deployer-address> --value 10ether \
  --rpc-url <primary-railway-url> \
  --ledger    # or --private-key from your offline export

Then on your laptop (where you have foundry installed):

bash
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).log

Orchestrator 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

env
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 faucet

dex-app

env
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 work

domain-app

env
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)

env
# Mainnet defaults are baked in — empty config is fine
# Backend wiring (private network):
NODE_URL=http://snapshot-publisher.railway.internal:8080

blockscout backend + frontend

env
# 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=false

status-app

env
# Mainnet defaults work; just wire Redis for history:
REDIS_URL=${{Redis.REDIS_PRIVATE_URL}}

Snapshot publisher (archive RPC + R2 uploader)

env
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=3

airdrop-indexer (Day 0+)

See airdrop-indexer/README.md for the full env set. Key ones:

env
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.json

Phase 7 — Verify each surface (~30min)

URLExpected
https://rpc.sanect.com/Returns EVM JSON-RPC responses; eth_chainId returns 0x1dcc (= 7628)
https://scan.sanect.comHero says "Sanect explorer" (NOT "Sanect testnet explorer"); no red testnet banner
https://status.sanect.comAll endpoints reachable; uptime bars populating
https://app.sanect.comHeader says "Sanect" (no testnet badge); chain id 7628; Need-SNCT button absent (faucet disabled)
https://swap.sanect.comWallet connects to chain 7628; WSNCT address shown matches genesis-addresses.json
https://domain.sanect.comSame 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:

  1. EOA ownership for 14 days — "Genesis ownership held by 3 hardware-wallet-backed EOAs; migration to on-chain Safes by Day 14."
  2. Hyperlane bridge offline at launch — "Cross-chain bridge to Sepolia / Ethereum mainnet will activate after Day 7 ISM upgrade."
  3. Audit status — "Engaged with [auditor name]; full report at sanect.com/audits when complete."
  4. 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."