How to run a relayer

A relayer is an operator in the zk.money system that moves a deposit into the network and releases a withdrawal back out to Ethereum, paying the Ethereum gas for both so that a user does not have to.

No operator can move your funds, and no privileged admin role exists in the system. However, an operator like an Oxide enclave or a relayer can go offline, and zk.money does not run relayers, so this page tells you how to run one yourself if need be.

This is an advanced operation, and you do not need to do this to use zk.money on web or on desktop. It is written for developers (or their agents) and assumes you already know Docker, Ethereum, and the Aztec Network. A relayer pays the Ethereum gas on every transaction it sends, out of an account you fund and control, and earns the fees on the work it does first.

What you need

  • Docker.
  • An Ethereum RPC URL. For l1-operations it must support eth_simulateV1; the relayer checks at startup. Do not use a Flashbots URL here.
  • An Aztec node URL. The Aztec v5 mainnet RPC also needs an API key.
  • A dedicated Ethereum account, funded with ETH for gas. This account pays the gas and receives the rewards.
  • The deployment manifest and the portal address of the deployment you want to serve. Both are given below.

The relayer keeps its cursors and transaction state in /data/oxide-relayer-{portal}.sqlite3. Always mount /data, or replacing the container loses that state.

What a relayer does

A relayer runs one or more modes. Enable them with --modes, comma-separated. The default is l1-operations. Every mode uses the same signer.

Mode What it does Reward Who should enable it
l1-operations Executes the Ethereum transactions users request from the Aztec Network, such as sweeping a deposit or releasing a withdrawal. The payout of each operation, in DAI. Everyone.
fpc-funding Converts the fees the portal collects into fee juice for the contract that pays network fees inside zk.money. A bounty in DAI. Everyone.
epoch-proofs Asks your Aztec prover node to prove withdrawals early, then claims the prover tips. Prover tips and the prover subsidy, in DAI. Operators who run an Aztec prover node. Needs extra prover setup; see below.

How l1-operations works

A user's action on the Aztec Network publishes an L1 operation: an Ethereum call, a payout token, and a condition such as "after the epoch of this withdrawal is proven". When the condition holds, the relayer screens the addresses, simulates the call, and executes it through the OperationExecutor contract. That contract reverts if the payout does not cover the gas. To release a withdrawal, the relayer also gets a signature from the deployment's enclave.

How fpc-funding works

Every deposit and withdrawal pays a small fee into the FPCFunder contract. The relayer calls it to swap those fees into fee juice and bridge them to the fee-paying contract on the Aztec Network. The bounty rises from 0.01% to 10% over the 24 hours after the last call. Only one call per Ethereum block succeeds.

How epoch-proofs works

When the prover tips and subsidy pay for an early proof, the relayer asks your prover node to prove the epoch up to the latest checkpoint, then claims the tips. The portal pays only if your prover node is set up for it:

  • Set PROVER_NODE_PROOF_SUBMISSION_TARGET_ADDRESS to the firstProverProofSubmitter address in the manifest.
  • Set PROVER_ID on the prover node to the relayer signer's address. Unset, the prover ID is the address of the prover node's publisher key, and the tips go nowhere.

How transactions are submitted

  • Ethereum mainnet. The relayer submits through Flashbots Protect. Protect drops reverting transactions and keeps them out of the public mempool. --flashbots-block-range (default 5) sets the Protect drop window.
  • Sepolia. The relayer submits to the public mempool through your RPC URL, so that RPC must accept transactions. A transaction that is included and then reverts still costs gas. --flashbots-block-range only sets the local expiry; expiring does not cancel a transaction or free its nonce.

Run on staging

Staging is Sepolia plus Aztec v5. The current deployment is v12, selected by its portal address.

docker pull azteclabs/oxide-relayer:1.0.0

docker run --rm -it \
  --name oxide-relayer-staging \
  -v relayer-data:/data \
  -e L1_PRIVATE_KEY=0x<64-hex-character-private-key> \
  -e READ_L1_RPC_URL=https://<your-sepolia-rpc> \
  -e AZTEC_NODE_URL=https://testnet-v5.rpc2.aztec-labs.com \
  -e AZTEC_NODE_API_KEY=<your-aztec-node-api-key> \
  azteclabs/oxide-relayer:1.0.0 \
  run \
  --deployment-env-manifest https://d1g9k2awa2mp7i.cloudfront.net/staging.v4.json \
  --portal 0x12E9Bf217CF5566Bb82253C54400798205E1bE62 \
  --signer env \
  --modes l1-operations,fpc-funding \
  --l1-min-priority-fee-gwei 1

Set --l1-min-priority-fee-gwei 1 on Sepolia. The default of 0.1 suits mainnet, but Sepolia builders order transactions by tip.

Run on production

Production is Ethereum mainnet plus Aztec v5. The current deployment is v6, selected by its portal address. The manifest is https://d1ylrbyes5g693.cloudfront.net/prod.v4.json.

This example uses a keystore signer; the Signer section below says why.

docker pull azteclabs/oxide-relayer:1.0.0

docker run -d \
  --name oxide-relayer-production \
  --restart unless-stopped \
  -v relayer-data:/data \
  -v /host/path/keystore.json:/run/secrets/relayer-keystore.json:ro \
  -v /host/path/keystore-password:/run/secrets/relayer-keystore-password:ro \
  -e READ_L1_RPC_URL=https://<your-ethereum-mainnet-rpc> \
  -e AZTEC_NODE_URL=https://canonical.mainnet.rpc.aztec-labs.com/ \
  -e AZTEC_NODE_API_KEY=<your-aztec-node-api-key> \
  azteclabs/oxide-relayer:1.0.0 \
  run \
  --deployment-env-manifest https://d1ylrbyes5g693.cloudfront.net/prod.v4.json \
  --portal 0xdf410ad448A0f7165181FBdB32f8896f4a0d9449 \
  --signer keystore \
  --keystore /run/secrets/relayer-keystore.json \
  --keystore-password-file /run/secrets/relayer-keystore-password \
  --modes l1-operations,fpc-funding

The Aztec v5 mainnet RPC requires an API key, set with AZTEC_NODE_API_KEY. If you do not have one, ask the Oxide team, or run your own Aztec RPC.

Signer

--signer env reads the raw key from L1_PRIVATE_KEY (or the variable named by --private-key-env). Use it only on staging and for short-lived tests.

--signer keystore reads an Ethereum V3 JSON keystore. Use it for anything longer-lived, and always on production. Give the password in a file with --keystore-password-file; a Docker run with no terminal cannot prompt for it:

docker run --rm \
  -v relayer-data:/data \
  -v /host/path/keystore.json:/run/secrets/relayer-keystore.json:ro \
  -v /host/path/keystore-password:/run/secrets/relayer-keystore-password:ro \
  <pinned-image> \
  run \
  <network-options> \
  --signer keystore \
  --keystore /run/secrets/relayer-keystore.json \
  --keystore-password-file /run/secrets/relayer-keystore-password

Never pass a secret as a command-line flag: other processes on the host can read the command line. Use a mounted file, or --env-file, which keeps the value out of the docker run line too.

For production, pin the image. azteclabs/oxide-relayer:1.0.0 is the current release; to fix the exact artifact, pin by digest.

Submission policy

Before it submits anything, the relayer simulates the transaction and estimates its gas, and it skips work whose reward does not cover that gas.

--allow-unprofitable turns those checks off for l1-operations and fpc-funding. It does not affect epoch proofs. It is fine on staging with test ETH. On mainnet it can spend real ETH on gas that no reward pays back.

For epoch proofs, set --early-proof-proving-cost-per-checkpoint to your off-chain proving cost. It and --early-proof-min-profit are USD amounts scaled by 10^18, and --early-proof-min-profit-margin-bps is in basis points of the reward. All three default to 0.

Dry run and emergency stop

Add --disable-submission, or set DISABLE_SUBMISSION=1, and the relayer signs nothing and broadcasts nothing. fpc-funding and epoch-proofs do not start. l1-operations keeps finding and recording work, but does not screen, simulate or submit it.

Address screening

l1-operations always screens the target, the payout token, and every address in the simulation logs against the OFAC SDN list. A match blocks that operation permanently.

That is narrower than what the wallet does. The wallet screens addresses through Predicate before a deposit or a withdrawal; see Limits. The examples above configure only the OFAC check, so a relayer run that way does not apply the wallet's policy. To apply it, set --predicate-api-key (or OXIDE_RELAYER_PREDICATE_API_KEY), --predicate-verification-hash and --predicate-chain. With all three set, Predicate screening runs in addition to the OFAC check.

Check your configuration

run --help lists every option with its environment variable and its default. If the environment sets a value, help shows that as the default. Secrets show as <redacted>, and RPC URLs show only their origin, because providers put API keys in the path:

docker run --rm --env-file relayer.env azteclabs/oxide-relayer:1.0.0 run --help

At startup the relayer logs its version, then Starting relayer with the resolved configuration, redacted the same way.

Troubleshooting

  • missing required config. Check --deployment-env-manifest, --portal, the Ethereum RPC (--read-l1-rpc or READ_L1_RPC_URL) and the Aztec node (--aztec-node or AZTEC_NODE_URL).
  • no deployment with portal. Pick a portal address from the manifest's deployments array.
  • Manifest validation error. Your portal's entry must conform to schema v4, or the relayer does not start.
  • Not watching a deployment. The relayer cannot read an older deployment in the manifest, so it does not relay for it. Expected on production, where older deployments use a previous manifest format.
  • eth_simulateV1 error at startup. Use an Ethereum RPC that supports it.
  • keystore signer requires --keystore-password-file. Mount a password file; Docker cannot prompt.
  • A mode is enabled but submission is disabled. Expected when you use the kill switch.
  • Work deferred as unprofitable. Expected under the default policy. On staging you can use --allow-unprofitable; on mainnet that flag can spend real ETH on work that does not pay.

Configuration reference

Every flag has an environment variable, and a flag overrides its variable. run --help prints the same list.

Deployment and endpoints

Flag Environment variable Default Description
--deployment-env-manifest OXIDE_DEPLOYMENT_ENV_MANIFEST_URL Deployment manifest URL.
--portal OXIDE_PORTAL Portal of the manifest deployment to run.
--read-l1-rpc READ_L1_RPC_URL (fallback L1_RPC_URL, ETHEREUM_HOST) Ethereum read RPC. l1-operations needs eth_simulateV1; Sepolia also submits here. Must not be Protect.
--aztec-node AZTEC_NODE_URL (fallback OXIDE_AZTEC_NODE_URL) Aztec node URL.
No flag AZTEC_NODE_API_KEY (fallback OXIDE_AZTEC_NODE_API_KEY) Aztec node API key. Required for the Aztec v5 mainnet RPC. Secret.

Modes

Flag Environment variable Default Description
--modes OXIDE_RELAYER_MODES l1-operations Comma-separated modes.
--prover-node-url OXIDE_RELAYER_PROVER_NODE_URL Prover node RPC used to request early epoch proofs. Required by epoch-proofs.

Signer

Flag Environment variable Default Description
--signer OXIDE_RELAYER_SIGNER env if the private key variable is set, otherwise keystore Signer backend: env or keystore.
--private-key-env OXIDE_RELAYER_PRIVATE_KEY_ENV L1_PRIVATE_KEY Variable holding the raw Ethereum private key.
--keystore OXIDE_RELAYER_KEYSTORE JSON keystore path.
--keystore-password OXIDE_RELAYER_KEYSTORE_PASSWORD JSON keystore password. Secret.
--keystore-password-file OXIDE_RELAYER_KEYSTORE_PASSWORD_FILE File containing the keystore password.
No flag L1_PRIVATE_KEY Raw private key for --signer env, unless --private-key-env names another variable. Secret.

State

Flag Environment variable Default Description
--state OXIDE_RELAYER_STATE_PATH /data/oxide-relayer-{portal}.sqlite3 SQLite state path. {portal} expands to each worker's portal.

Submission policy

Flag Environment variable Default Description
--disable-submission DISABLE_SUBMISSION false Disable signing and broadcasting.
--allow-unprofitable OXIDE_RELAYER_ALLOW_UNPROFITABLE false Submit L1 operations and FPC funding even when unprofitable. Does not apply to epoch proofs.
--l1-min-priority-fee-gwei OXIDE_RELAYER_L1_MIN_PRIORITY_FEE_GWEI 0.1 Floor for the priority fee. A higher market tip still wins.
--flashbots-block-range OXIDE_RELAYER_FLASHBOTS_BLOCK_RANGE 5 Protect drop window on mainnet. On Sepolia, the local expiry (12 s per block).

Screening

Flag Environment variable Default Description
--sdn-url OXIDE_RELAYER_SDN_URL https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/SDN.XML OFAC SDN list that l1-operations screens against.
--predicate-api-key OXIDE_RELAYER_PREDICATE_API_KEY Predicate API key. With the hash and chain, adds Predicate screening to the OFAC check. Secret.
--predicate-verification-hash OXIDE_RELAYER_PREDICATE_VERIFICATION_HASH Predicate managed policy id, sent as verification_hash.
--predicate-chain OXIDE_RELAYER_PREDICATE_CHAIN Predicate chain name, e.g. ethereum-mainnet.

Epoch-proof policy

Flag Environment variable Default Description
--early-proof-min-profit OXIDE_RELAYER_EARLY_PROOF_MIN_PROFIT 0 Minimum partial-epoch proof profit, USD scaled by 10^18.
--early-proof-min-profit-margin-bps OXIDE_RELAYER_EARLY_PROOF_MIN_PROFIT_MARGIN_BPS 0 Minimum profit margin, in basis points of the reward.
--early-proof-proving-cost-per-checkpoint OXIDE_RELAYER_EARLY_PROOF_PROVING_COST_PER_CHECKPOINT 0 Off-chain proving cost per checkpoint, USD scaled by 10^18.

Advanced tuning

The defaults suit most operators. Change these only to reduce RPC load or to debug.

Flag Environment variable Default Description
--l1-operations-poll-interval-ms OXIDE_RELAYER_L1_OPERATIONS_POLL_INTERVAL_MS 10000 Interval between L1 operation poll cycles.
--fpc-funding-poll-interval-ms OXIDE_RELAYER_FPC_FUNDING_POLL_INTERVAL_MS 60000 Interval between FPC funder bounty checks.
--l1-operations-retry-backoff-ms OXIDE_RELAYER_L1_OPERATIONS_RETRY_BACKOFF_MS 30000 Backoff before re-checking a deferred L1 operation.
--l1-operations-max-retries OXIDE_RELAYER_L1_OPERATIONS_MAX_RETRIES 10 Failed executor simulations before an L1 operation is dropped.
--log-scan-window OXIDE_RELAYER_LOG_SCAN_WINDOW 1000 Blocks per eth_getLogs call in the balance watcher's transfer scan.