Skip to content

Mainnet Hyperlane relayer — Railway setup ​

The bridge round-tripped successfully on 2026-06-24 with the relayer running in Docker on the operator's WSL laptop. This doc moves it to a Railway service so it runs 24/7 without depending on the laptop.

What the relayer does ​

Watches the Hyperlane Mailbox on both chains (sanect + Ethereum L1) for Dispatch events, validates the message via the ISM, and submits process() on the destination Mailbox. Without it: a transferRemote sits in the source Mailbox forever, no one delivers.

Single relayer handles both directions and all enrolled chains.

State at handoff (from tonight's session) ​

  • Image: gcr.io/abacus-labs-dev/hyperlane-agent:main
  • Command: ./relayer
  • Whitelist env critical: without it, relayer wades through L1's full Hyperlane message backlog (~230k messages) and never gets to ours
  • Alchemy PAYG plan required for Ethereum RPC (free tier caps eth_getLogs to 10-block range; drpc/llama/ankr free tiers rate-limit eth_getLogs heavily under sustained load)
  • Local agent-config.json + agent DB at ~/hyperlane-relayer-data/

Railway service spec ​

New service: hyperlane-relayer-mainnet ​

FieldValue
Service typeDocker image
Imagegcr.io/abacus-labs-dev/hyperlane-agent:main
Start command./relayer
Restart policyunless-stopped
Volume mount/data (4GB, for relayer DB)
Config mount/config (read-only, holds agent-config.json)

Note: gcr.io images may need Railway's GitHub integration or pulling through a Docker Hub mirror if Railway can't auth to gcr.io directly. Workaround: re-tag and push to ghcr.io under your own account.

Environment variables (Railway Raw Editor paste-ready) ​

Open Variables tab → Raw Editor → paste this entire block, then replace the one placeholder:

bash
CONFIG_FILES=/config/agent-config.json
HYP_DB=/data
HYP_RELAYCHAINS=sanect,ethereum
HYP_DEFAULTSIGNER_TYPE=hexKey
HYP_DEFAULTSIGNER_KEY=PASTE_DEPLOYER_PRIVATE_KEY_HERE_0x...
HYP_METRICS=9090
HYP_WHITELIST=[{"senderAddress":"*","destinationDomain":["7628","1"],"recipientAddress":"*"}]

HYP_WHITELIST is the only line that LOOKS scary but pastes cleanly — Railway stores it as a string, the relayer parses it as JSON at boot. Don't quote the value, don't escape the brackets.

HYP_DEFAULTSIGNER_KEY placeholder: paste the deployer EOA private key (66 chars, starts with 0x). For Day-7 hygiene, rotate this to a dedicated relayer-only hot wallet later — see the rotation section below.

Mounting agent-config.json on Railway ​

Railway doesn't have a clean "mount a file" UI like Docker volumes. Two options:

  1. Build a small image with the config baked in. Dockerfile:
    dockerfile
    FROM gcr.io/abacus-labs-dev/hyperlane-agent:main
    COPY agent-config.json /config/agent-config.json
    ENTRYPOINT ["./relayer"]
    Then docker build -t ghcr.io/<you>/sanect-relayer:latest . and push. Railway pulls from your registry.
  2. Use a startup script that writes /config/agent-config.json from env vars before starting. More flexible (config changes without rebuild) but more moving parts.

Option 1 is simpler for a first pass.

Agent config to ship ​

json
{
  "chains": {
    "sanect": {
      "name": "sanect",
      "chainId": 7628,
      "domainId": 7628,
      "protocol": "ethereum",
      "rpcUrls": [{ "http": "https://rpc.sanect.com" }],
      "mailbox": "0x2CB7861CC3dA9148763538C75DdCf5772e5c03e1",
      "merkleTreeHook": "0x36E2D747Cc6db182607FD551aFe276aCBBfa9ce9",
      "validatorAnnounce": "0x7d2EAf5E6170F8CF80D291815783Aa62D7dE49A5",
      "interchainGasPaymaster": "0x0000000000000000000000000000000000000000",
      "blocks": { "reorgPeriod": 1, "confirmations": 1, "estimateBlockTime": 1 },
      "index": { "from": <recent_block>, "chunk": 1000 }
    },
    "ethereum": {
      "name": "ethereum",
      "chainId": 1,
      "domainId": 1,
      "protocol": "ethereum",
      "rpcUrls": [{ "http": "https://eth-mainnet.g.alchemy.com/v2/<ALCHEMY_PAYG_KEY>" }],
      "mailbox": "0xc005dc82818d67AF737725bD4bf75435d065D239",
      "interchainGasPaymaster": "0x0000000000000000000000000000000000000000",
      "blocks": { "reorgPeriod": 5, "confirmations": 1, "estimateBlockTime": 12 },
      "index": { "from": <recent_block>, "chunk": 1000 }
    }
  }
}

Set index.from to the current chain tip on each chain at the moment of deploy (or a few hundred blocks before, to catch any in-flight messages). The relayer maintains its own DB cursor after that, so the initial value only matters at first start.

Migration from laptop → Railway ​

  1. Stand up Railway service with config + env vars but don't start.
  2. On laptop: docker stop hyperlane-relayer-mainnet.
  3. Start Railway service. It will read its own (empty) DB and re-index from the configured index.from. Takes a few minutes to catch up.
  4. Send a 1-wei test transferRemote, verify it delivers within 1-2 min.
  5. Once verified, on laptop: docker rm hyperlane-relayer-mainnet and clear ~/hyperlane-relayer-data/.

Funding the relayer hot wallet ​

Whatever key is configured as HYP_DEFAULTSIGNER_KEY needs gas on every destination chain to submit process(). Current rough costs:

ChainCost per process() at typical gasRecommended hot wallet float
sanect~0.0001 SNCT5 SNCT (~50k deliveries)
Ethereum L1~0.0002 ETH @ 0.5 gwei, ~0.001 ETH @ 5 gwei0.02 ETH (~100-200 deliveries)
Base / Arbitrum / Optimism~0.0001 ETH0.01 ETH each
BSC~0.005 BNB0.05 BNB each

Top up via the operator runbook every couple of weeks.

Day-7 follow-up: rotate to dedicated relayer key ​

Currently the relayer uses the deployer key, which also owns the bridge contracts. Day-0 acceptable risk model:

The relayer can't mint or withdraw on its own — it can only submit messages already dispatched. Worst case if compromised: attacker spams process() and drains the relayer's gas balance.

Day-7 hygiene step:

  1. Generate fresh wallet, save in 1Password / Vault.
  2. Fund it with hot-wallet float on every chain.
  3. Update the TrustedRelayerIsm on both sides to trust the new address (deployer key calls setTrustedRelayer(new) on each ISM — wait, current TrustedRelayerIsm stores trustedRelayer as immutable. We'd need to redeploy the ISM with the new address, then setInterchainSecurityModule on the synthetic to point at it).
  4. Update HYP_DEFAULTSIGNER_KEY on Railway, restart relayer.
  5. Old deployer key no longer signs relayer txs.

Since TrustedRelayerIsm is immutable on the relayer field, the Day-7 upgrade is the perfect moment to also swap to MessageIdMultisigIsm 2-of-3 multi-validator (which is itself a redeploy + setISM flow).

Monitoring ​

  • HYP_METRICS=9090 exposes Prometheus metrics on port 9090.
  • Useful alerts:
    • relayer container down (Railway notifies)
    • hot wallet balance < threshold (script + cron)
    • message age > 5 min and still undelivered (Prometheus query)

See also ​

  • CLAUDE.md "Hyperlane bridge routers (mainnet, deployed 2026-06-24)"
  • bridge-addresses.json (root)
  • docs/mainnet-bridge-setup.md (original deployment runbook — superseded by the actual session record but still useful for context)
  • docs/mainnet-bridge-deploy-handoff.md (compile-blocker plumbing recipe, in case anyone needs to rebuild bridge contracts)