From zero to a running relayer.
Six steps to a running relayer — but registration is gated. Your wallet must first be verified (zk-X509 certificate proof) and admin-approved (KYC). Do that onboarding in the registration wizard first; the steps below stand up the service it gates.
Where you are right now
Auto-checks of your wallet, RPC, and on-chain state. Service health needs your relayer URL.
Connect a wallet from the header to enable the rest of the checks.
Connect a wallet to check it is on Sepolia (chainId 11155111).
Pinging RPC…
Connect a wallet to read your registry status.
Connect a wallet to read.
Enter your relayer's public URL and ping it.
What you will need
Gather these first. Each one is required — the wizard later confirms them automatically.
A dedicated EVM wallet whose private key the relayer process can read. Never reuse a personal/treasury key — operator keys sign settlement txs constantly.
Hardware wallets are not supported for the relayer process. Use a fresh hot wallet held only by the operator host.
RelayerRegistry.register() reverts unless this same wallet is both isVerified (you proved an accredited certificate to zk-X509) AND kycApproved (an admin signed off your KYC). Complete both in the /register wizard before you reach the register step.
Both gates bind to the wallet that registers — prove and register from the same key, or it reverts (NotVerified / NotKycApproved).
On Sepolia testnet: at least 0.1 ETH bond plus a gas float (~0.05 ETH) on the operator wallet. Bond is recoverable on exit after the registry's exit cool-down.
A reliable RPC URL (Alchemy/Infura/your own node). For production, a fallback URL is strongly recommended.
Archive support is not required; standard JSON-RPC is enough.
Minimum 2 vCPU, 4 GB RAM, 50 GB SSD. Docker or Node.js 20+ runtime. Outbound network access to the RPC and inbound HTTPS for your public URL.
A reachable public URL (e.g. https://relayer.example.com) that you will publish on-chain. Clients route orders to this URL. Behind a reverse proxy with TLS termination.
A strong random secret (openssl rand -hex 32, ≥ 32 bytes). The relayer runs without one, but every /api/admin/* endpoint returns 403 until it is set, so pause/resume, fee updates, and sanctions management are inaccessible.
The six steps
Each step is independent — finish one before moving to the next. Time estimates assume the prerequisites above are ready.
Each package in the monorepo is standalone (no root npm workspace) — install and build inside zk-relayer/.
git clone https://github.com/tokamak-network/scatter-dex.git cd scatter-dex/zk-relayer npm install npm run build
Copy the example file and fill in the five required fields (RPC_URL, RELAYER_PRIVATE_KEY, and the three contract addresses). ADMIN_API_KEY is optional but recommended. Other fields have safe defaults.
cp .env.example .env # then edit .env
| Field | Required | Note |
|---|---|---|
| RPC_URL | required | Your EVM RPC endpoint. |
| RELAYER_PRIVATE_KEY | required | Operator wallet key. Use RELAYER_PRIVATE_KEY_FILE for Docker secrets. |
| COMMITMENT_POOL_ADDRESS | required | From contracts/deployments — must match the chain in RPC_URL. |
| PRIVATE_SETTLEMENT_ADDRESS | required | From contracts/deployments. |
| FEE_VAULT_ADDRESS | required | From contracts/deployments. |
| ADMIN_API_KEY | optional | openssl rand -hex 32 (≥ 32 bytes). Without it, /api/admin/* returns 403. |
| RELAYER_PUBLIC_URL | optional | Your HTTPS URL. Recommended in production. |
| TOKEN_LIST | optional | Per-token symbols for the treasury page. |
| MAX_GAS_PRICE_GWEI | optional | Cap above which the relayer pauses settlement. |
Run the service locally first to verify your config. Once /health returns 200, you are ready to register.
# from zk-relayer/ npm start # in another shell: curl -s http://localhost:3002/health | jq
Expected: { "status": "healthy", "uptime": <seconds>, "checks": { "rpc": "ok", "db": "ok" } }. A 503 with status: "degraded" means one of the checks failed — inspect the response body.
Connect your operator wallet and publish your public URL plus per-trade fee; the bond is escrowed in the RelayerRegistry contract. register() reverts with NotVerified unless your wallet proved its certificate to zk-X509, and with NotKycApproved unless an admin approved your KYC — so do the wizard's identity steps (KYC → zk-X509 proof → admin approval) first. The /register wizard walks all of it, then builds the tx.
Confirm registration on-chain, then check that orders can reach your endpoint. Your registry entry must list the same URL the service is exposing.
# inspect the relayer's self-reported state:
curl -s https://YOUR_PUBLIC_URL/api/info | jq
# expected shape:
# {
# name: "ScatterDEX ZK Relayer", version: "0.1.0",
# address: "0x...", fee: 30, orderCount: 0,
# commitmentPool: "0x...", privateSettlement: "0x...",
# profile: { name, description, ... }
# }
#
# cross-check on-chain registration via the leaderboard page or
# the RelayerRegistry contract directly.At minimum: poll /health from your monitoring stack and route alerts to a channel you actually watch. Phase 2 will add in-app webhooks; for now wire it externally.
- · Poll
GET /healthevery 30 s — alert on non-200. - · Tail process stdout for
[tx-recovery]and[admin]lines. - · Track operator wallet ETH balance — alert below ~0.02 ETH.
- · Watch the on-chain bond — slashing or self-exit changes it.
Core concepts
Terms you will see repeatedly in this app, the docs, and the contracts.
ETH you escrow in RelayerRegistry when registering. Acts as a registration deposit. Recoverable in full on exit after the registry's exit cool-down — the current contract has no bond-slashing mechanism (a misbehaving relayer only loses gas on failed settle() attempts).
Your per-trade fee in basis points (1 bp = 0.01%). Published on-chain at registration; updatable via /profile. Default reference: 30 bps (0.30%).
A protocol-level skim on relayer claims, capped at 50% by contract (governance default sub-10%). What you see on /treasury is net of platform fee.
On-chain contract that holds your accrued fees per token until you claim. The /treasury page is a UI on top of this contract.
Each side of a trade submits an authorize.circom proof; the relayer combines them in settleAuth. You never hold witness data — only proofs and public signals.
On-chain: SanctionsList contract — a blocklist of EVM addresses (e.g. OFAC SDN). Enforced by PrivateSettlement for claim/settle paths.
Operator-local: a JSON file (or admin-API-managed) blocklist of EdDSA pubkeys. Rejects matching orders before they reach the chain.
Time between requestExit and executeExit on the registry, set by governance (7 days by default). isActiveRelayer returns false as soon as requestExit is called, so settlement is blocked for the entire cooldown — plan to drain your queue before requesting exit.
Despite the term being common in relayer designs, the current RelayerRegistry has no slashing path. If/when it ships, it will appear on /profile.
Troubleshooting
Common failures and the first thing to check.
+/health returns 503
One of the readiness checks failed. Look at the response body — checks.rpc means your RPC_URL is unreachable; checks.db means the DB_PATH is not writable. Fix the underlying cause; the service does not need a restart once the dependency is healthy.
+Registration tx reverts with InsufficientBond
Your operator wallet does not hold enough native token to cover the minimum bond plus gas. Top up and retry — the registry returns the failure cleanly without consuming the bond.
+Orders arrive but never settle
Check the relayer's stdout for [gas-guard] or [tx-recovery] lines. If gas-guard rejected the settlement (gasPrice above MAX_GAS_PRICE_GWEI), the SettlementWorker classifies the failure as 'unknown' and does not auto-retry — the order will likely be picked up by another relayer. Raise the cap in .env (then restart) or accept that some flow will route around you during fee spikes.
+Connecting my wallet fails with 'Admin auth is not configured'
The relayer enables wallet (SIWE) auth only when RELAYER_REGISTRY_ADDRESS is set in its .env — that's what binds admin access to your on-chain relayer record. Set it (then restart) and the wallet connect flow will issue a session.
+My public URL on-chain does not match my running service
Update via /profile or POST /api/admin/profile. The leaderboard and clients route to whatever the registry says — a stale URL silently drops traffic to you.
+Settlement fails with NullifierAlreadyUsed
The order has already been settled — usually by a competing relayer. This is normal under contention; the relayer drops the order from its queue and moves on.
Ready when your checklist is green.
Once the prerequisites are gathered and the service is up, the registration flow is a single tx.