A step-by-step guide for deploying the Integra mainnet with 4 validators across 4 providers.
| Property | Value |
|---|---|
| Chain ID | integra-1 |
| EVM Chain ID | 26217 (hex: 0x6669) |
| Token | IRL / airl (1 IRL = 10^18 airl) |
| Validators | 4 (Gateway, Archive, Signer-1, Signer-2) |
| Total Supply | 100,000,000,000 IRL |
| Inflation | 3% fixed per year |
| Server | Moniker | Provider | Location | Role |
|---|---|---|---|---|
| Mainnet-Gateway | Integra-Helsinki |
Hetzner | Helsinki, Finland | Validator + Public APIs + Blockscout |
| Archive | Integra-Paris |
OVH | Paris, France | Archive validator + Cosmos Explorer |
| Signer-1 | Integra-Amsterdam |
Vultr | Amsterdam, Netherlands | Silent signer only |
| Signer-2 | Integra-NewYork |
DigitalOcean | New York, USA | Silent signer only |
IMPORTANT: Generate ALL keys on your own machine. Share ONLY the integra1... addresses with Adam. Never share mnemonics.
Get the intgd binary from Adam (or build from source on your machine).
# Mainnet keys
intgd keys add mainnet-treasury --keyring-backend file --algo eth_secp256k1
intgd keys add mainnet-gateway-validator --keyring-backend file --algo eth_secp256k1
intgd keys add archive-validator --keyring-backend file --algo eth_secp256k1
intgd keys add signer1-mainnet-validator --keyring-backend file --algo eth_secp256k1
intgd keys add signer2-mainnet-validator --keyring-backend file --algo eth_secp256k1
# Testnet keys (if redoing testnet)
intgd keys add testnet-treasury --keyring-backend file --algo eth_secp256k1
intgd keys add testnet-gateway-validator --keyring-backend file --algo eth_secp256k1
intgd keys add signer1-testnet-validator --keyring-backend file --algo eth_secp256k1
intgd keys add signer2-testnet-validator --keyring-backend file --algo eth_secp256k1Each command outputs:
- address (
integra1...) — share this with Adam - 24-word mnemonic — write it down, store securely, NEVER share
Send Adam a table like this (addresses only):
mainnet-treasury: integra1abc...
mainnet-gateway-validator: integra1def...
archive-validator: integra1ghi...
signer1-mainnet-validator: integra1jkl...
signer2-mainnet-validator: integra1mno...
Adam installs on each server:
- Go 1.23.8
intgdbinary at/usr/local/bin/intgd- UFW firewall rules
- Caddy (Gateway and Archive only)
Build gotcha: Must build on Linux with
CGO_ENABLED=1. Cross-compiling from macOS fails because Cosmos EVM uses C bindings for secp256k1.
Adam performs the genesis ceremony using ONLY the public addresses Piyush provided.
# On each server
intgd init <moniker> --chain-id integra-1Piyush SSHs into each server and imports the relevant key:
# On Mainnet-Gateway
intgd keys add mainnet-gateway-validator --recover --keyring-backend file --algo eth_secp256k1
# Enter the 24-word mnemonic for the gateway validator
# On Archive
intgd keys add archive-validator --recover --keyring-backend file --algo eth_secp256k1
# On Signer-1
intgd keys add signer1-mainnet-validator --recover --keyring-backend file --algo eth_secp256k1
# On Signer-2
intgd keys add signer2-mainnet-validator --recover --keyring-backend file --algo eth_secp256k1
# On Mainnet-Gateway (treasury too)
intgd keys add mainnet-treasury --recover --keyring-backend file --algo eth_secp256k1GENESIS=$HOME/.intgd/config/genesis.json
# Treasury: 99,999,996,000 IRL (4 validators x 1,000 IRL = 4,000 IRL reserved)
intgd genesis add-genesis-account <treasury-address> 99999996000000000000000000000airl
# 4 Validators: 1,000 IRL each
intgd genesis add-genesis-account <gateway-address> 1000000000000000000000airl
intgd genesis add-genesis-account <archive-address> 1000000000000000000000airl
intgd genesis add-genesis-account <signer1-address> 1000000000000000000000airl
intgd genesis add-genesis-account <signer2-address> 1000000000000000000000airlApply ALL of these via jq. The critical ones are marked.
GENESIS=$HOME/.intgd/config/genesis.json
# Token metadata
jq '.app_state.bank.denom_metadata = [{
"description": "The native staking and governance token of Integra Layer",
"denom_units": [
{"denom": "airl", "exponent": 0, "aliases": ["attoirl"]},
{"denom": "irl", "exponent": 18}
],
"base": "airl", "display": "irl", "name": "Integra", "symbol": "IRL"
}]' $GENESIS > tmp.json && mv tmp.json $GENESIS
# EVM denom
jq '.app_state.vm.params.evm_denom = "airl"' $GENESIS > tmp.json && mv tmp.json $GENESIS
# *** CRITICAL: FeeMarket — base_fee MUST be "0" ***
# If base_fee is non-zero, gentx transactions fail with "insufficient fee"
# because gentxs are created without fees but DeliverTx enforces the fee floor.
# The EIP-1559 base fee adjusts upward organically after block 1.
# The actual minimum gas price is set in app.toml (minimum-gas-prices).
jq '.app_state.feemarket.params.no_base_fee = false' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.feemarket.params.base_fee = "0"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.feemarket.params.min_gas_price = "0.000000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.feemarket.params.base_fee_change_denominator = 8' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.feemarket.params.elasticity_multiplier = 2' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.feemarket.params.min_gas_multiplier = "0.500000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
# *** CRITICAL: Mint — goal_bonded MUST be > 0 ***
# Setting goal_bonded to "0.0" causes "goal bonded must be positive" error.
# Use "0.010000000000000000" (1%) as minimum.
jq '.app_state.mint.params.mint_denom = "airl"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.params.inflation_rate_change = "0.000000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.params.inflation_max = "0.030000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.params.inflation_min = "0.030000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.params.goal_bonded = "0.010000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.params.blocks_per_year = "6311520"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.mint.minter.inflation = "0.030000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
# Staking
jq '.app_state.staking.params.bond_denom = "airl"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.staking.params.unbonding_time = "1814400s"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.staking.params.max_validators = 100' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.staking.params.max_entries = 7' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.staking.params.historical_entries = 10000' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.staking.params.min_commission_rate = "0.000000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
# Governance
jq '.app_state.gov.params.min_deposit = [{"denom":"airl","amount":"1000000000000000000000000"}]' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.expedited_min_deposit = [{"denom":"airl","amount":"5000000000000000000000000"}]' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.max_deposit_period = "604800s"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.voting_period = "432000s"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.expedited_voting_period = "86400s"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.quorum = "0.334000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.threshold = "0.500000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.veto_threshold = "0.334000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.min_initial_deposit_ratio = "0.250000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.gov.params.burn_vote_veto = true' $GENESIS > tmp.json && mv tmp.json $GENESIS
# Slashing
jq '.app_state.slashing.params.signed_blocks_window = "10000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.slashing.params.min_signed_per_window = "0.050000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.slashing.params.downtime_jail_duration = "600s"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.slashing.params.slash_fraction_double_sign = "0.050000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.slashing.params.slash_fraction_downtime = "0.000100000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
# Distribution
jq '.app_state.distribution.params.community_tax = "0.000000000000000000"' $GENESIS > tmp.json && mv tmp.json $GENESIS
jq '.app_state.distribution.params.withdraw_addr_enabled = true' $GENESIS > tmp.json && mv tmp.json $GENESISSame flow as testnet — see Testnet Deployment Report for exact commands.
Key difference: 4 validators instead of 3, so 4 gentxs to collect.
sha256sum /root/.intgd/config/genesis.json
# Must be identical on all 4 serversminimum-gas-prices = "5000000000000airl"
[evm]
chain-id = "26217"chain-id = "integra-1"[p2p]
persistent_peers = "<gateway-id>@<gateway-ip>:26656,<archive-id>@<archive-ip>:26656,<signer1-id>@<signer1-ip>:26656,<signer2-id>@<signer2-ip>:26656"[api]
enable = true
address = "tcp://0.0.0.0:1317"
[json-rpc]
enable = true
address = "0.0.0.0:8545"
ws-address = "0.0.0.0:8546"
[state-sync]
snapshot-interval = 1000pruning = "nothing" # Archive mode — keep ALL historypruning = "everything"
[json-rpc]
enable = false
[tx_index]
indexer = "null"ufw allow 22/tcp # SSH
ufw allow 80/tcp # Let's Encrypt ACME
ufw allow 443/tcp # HTTPS
ufw allow 1317/tcp # REST API
ufw allow 8545/tcp # EVM HTTP
ufw allow 8546/tcp # EVM WS
ufw allow 26656/tcp # P2P
ufw enableufw allow 22/tcp # SSH
ufw allow 26656/tcp # P2P only
ufw enable# /etc/systemd/system/intgd.service
[Unit]
Description=Integra Mainnet Node
After=network-online.target
Wants=network-online.target
[Service]
User=root
ExecStart=/usr/local/bin/intgd start
Restart=always
RestartSec=3
LimitNOFILE=65535
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload && systemctl enable intgd# From coordinator, start all nodes rapidly
systemctl start intgd # Local node
ssh root@<archive-ip> "systemctl start intgd" # Archive
ssh root@<signer1-ip> "systemctl start intgd" # Signer-1
ssh root@<signer2-ip> "systemctl start intgd" # Signer-2# Check blocks are being produced
intgd status | jq '.sync_info.latest_block_height'
# Check all 4 validators are bonded
intgd query staking validators --output json | jq '.validators[] | {moniker, status}'Delegate 250,000,000 IRL from Treasury to each of the 4 validators:
# 250M IRL = 250000000000000000000000000 airl
AMOUNT="250000000000000000000000000airl"
FLAGS="--from mainnet-treasury --keyring-backend file --gas auto --gas-adjustment 1.5 --gas-prices 5000000000000airl --chain-id integra-1 -y"
intgd tx staking delegate <gateway-valoper> $AMOUNT $FLAGS
intgd tx staking delegate <archive-valoper> $AMOUNT $FLAGS
intgd tx staking delegate <signer1-valoper> $AMOUNT $FLAGS
intgd tx staking delegate <signer2-valoper> $AMOUNT $FLAGSintgd query staking validators --output json | \
jq '.validators[] | {moniker: .description.moniker, tokens: .tokens, status: .status}'
# Each should show ~250,001,000 IRL (250M delegated + 1,000 self-stake)| Subdomain | Points To |
|---|---|
rpc.integralayer.com |
Gateway IP |
rest.integralayer.com |
Gateway IP |
evm-rpc.integralayer.com |
Gateway IP |
scan.integralayer.com |
Archive IP |
rpc.integralayer.com {
reverse_proxy localhost:26657
}
rest.integralayer.com {
reverse_proxy localhost:1317
}
evm-rpc.integralayer.com {
handle {
reverse_proxy localhost:8545
}
handle /ws {
reverse_proxy localhost:8546
}
}
| Resource | Access Level |
|---|---|
| SSH root on all servers | Full access to all files |
priv_validator_key.json |
Can read (generated during intgd init) |
node_key.json |
Can read |
| Config files | Can read and modify |
| Server logs | Can read |
- Validator consensus keys (
priv_validator_key.json) — used for block signing - Node keys (
node_key.json) — used for P2P identity - All config files —
config.toml,app.toml,client.toml
- Wallet mnemonics — the 24-word recovery phrases
- Keyring passwords — the password used with
--keyring-backend file - Private spending keys — unless Piyush imports them to a server Adam has SSH access to
| Risk | Severity | Details |
|---|---|---|
priv_validator_key.json exposure |
Medium | Anyone with this file can sign blocks as that validator. This enables double-signing (slashing risk: 5% of stake burned) but does NOT allow spending tokens. |
| Keyring on server | High if imported | If Piyush imports wallet mnemonics to servers via --recover, Adam can extract the keyring files and potentially decrypt them. |
| Treasury mnemonic on server | Critical | If the treasury mnemonic is ever on a server, anyone with SSH root can extract it. The treasury controls 99.999% of all tokens. |
- Piyush generates ALL keys on his personal machine — never shares mnemonics
- Piyush imports keys to servers himself via SSH — Adam provides server access but doesn't handle key import
- Treasury mnemonic stays offline — never imported to any server. Delegation commands run from Piyush's local machine or a temporary import that's immediately deleted.
- Post-launch: Piyush can rotate SSH keys on all servers, removing Adam's access if desired
- Future: Implement sentry node architecture to isolate validator signing keys from public-facing nodes
- The treasury mnemonic is the only key that controls funds. Keep it offline, in a hardware wallet or encrypted backup. Never put it on a server.
priv_validator_key.jsonon servers allows block signing (slashing risk) but NOT spending tokens.- Adam having SSH root means he CAN read
priv_validator_key.json— this is standard for any server admin. The mitigation is trust + the fact that these keys cannot move funds.
When: Running intgd genesis gentx
Cause: goal_bonded set to "0.000000000000000000" in mint params
Fix: Set to "0.010000000000000000" (1% minimum)
When: Running intgd genesis gentx or intgd genesis collect-gentxs
Cause: Feemarket base_fee is non-zero in genesis. Gentxs don't include fees.
Fix: Set base_fee = "0" and min_gas_price = "0.000000000000000000" in feemarket genesis params. The app.toml minimum-gas-prices provides the actual floor post-launch.
When: Starting Caddy — "Timeout during connect (likely firewall problem)"
Cause: UFW blocking ports 80/443 needed for Let's Encrypt HTTP-01 challenge
Fix: ufw allow 80/tcp && ufw allow 443/tcp && ufw reload
If Caddy has cached failed attempts:
rm -rf /var/lib/caddy/.local/share/caddy/certificates
rm -rf /var/lib/caddy/.local/share/caddy/acme
systemctl restart caddyWhen: Building intgd with CGO_ENABLED=0 or cross-compiling from macOS
Cause: secp256k1 C bindings require CGO
Fix: Build on a Linux machine with CGO_ENABLED=1
Mainnet (4 validators): Need 3/4 online (75% > 67% threshold) Testnet (3 validators): Need 2/3 online (67% — barely passes)
If chain halts:
- Check which validators are down:
intgd query staking validators --output json | jq '.validators[] | {moniker, jailed: .jailed, status: .status}' - Restart downed nodes:
systemctl restart intgd - If jailed for downtime:
intgd tx slashing unjail --from <validator-key> --keyring-backend file --gas auto --gas-adjustment 1.5 --gas-prices 5000000000000airl --chain-id integra-1 -y
Symptom: MetaMask shows wrong chain or transactions fail
Cause: Default EVM chain ID after intgd init is 262144, not 26217
Fix: Set in app.toml:
[evm]
chain-id = "26217" # mainnet
# chain-id = "26218" # testnetSymptom: Every CLI command requires --chain-id flag
Cause: Cosmos SDK v0.47+ validates chain-id from client.toml
Fix: Set in client.toml:
chain-id = "integra-1"| Amount | airl Value |
|---|---|
| 1 IRL | 1000000000000000000 (10^18) |
| 1,000 IRL | 1000000000000000000000 (10^21) |
| 1,000,000 IRL | 1000000000000000000000000 (10^24) |
| 200,000,000 IRL | 200000000000000000000000000 (10^26.3) |
| 250,000,000 IRL | 250000000000000000000000000 |
Always use: --gas-prices 5000000000000airl (5000 gwei)
With auto gas estimation: --gas auto --gas-adjustment 1.5
All commands use: --keyring-backend file
This stores keys encrypted on disk. You'll be prompted for a password.