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_getLogsto 10-block range; drpc/llama/ankr free tiers rate-limiteth_getLogsheavily under sustained load) - Local agent-config.json + agent DB at
~/hyperlane-relayer-data/
Railway service spec
New service: hyperlane-relayer-mainnet
| Field | Value |
|---|---|
| Service type | Docker image |
| Image | gcr.io/abacus-labs-dev/hyperlane-agent:main |
| Start command | ./relayer |
| Restart policy | unless-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:
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:
- Build a small image with the config baked in. Dockerfile:dockerfileThen
FROM gcr.io/abacus-labs-dev/hyperlane-agent:main COPY agent-config.json /config/agent-config.json ENTRYPOINT ["./relayer"]docker build -t ghcr.io/<you>/sanect-relayer:latest .and push. Railway pulls from your registry. - Use a startup script that writes
/config/agent-config.jsonfrom 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
{
"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
- Stand up Railway service with config + env vars but don't start.
- On laptop:
docker stop hyperlane-relayer-mainnet. - 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. - Send a 1-wei test transferRemote, verify it delivers within 1-2 min.
- Once verified, on laptop:
docker rm hyperlane-relayer-mainnetand 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:
| Chain | Cost per process() at typical gas | Recommended hot wallet float |
|---|---|---|
| sanect | ~0.0001 SNCT | 5 SNCT (~50k deliveries) |
| Ethereum L1 | ~0.0002 ETH @ 0.5 gwei, ~0.001 ETH @ 5 gwei | 0.02 ETH (~100-200 deliveries) |
| Base / Arbitrum / Optimism | ~0.0001 ETH | 0.01 ETH each |
| BSC | ~0.005 BNB | 0.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:
- Generate fresh wallet, save in 1Password / Vault.
- Fund it with hot-wallet float on every chain.
- Update the
TrustedRelayerIsmon both sides to trust the new address (deployer key callssetTrustedRelayer(new)on each ISM — wait, current TrustedRelayerIsm stores trustedRelayer asimmutable. We'd need to redeploy the ISM with the new address, thensetInterchainSecurityModuleon the synthetic to point at it). - Update
HYP_DEFAULTSIGNER_KEYon Railway, restart relayer. - 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=9090exposes 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)