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-operationsit must supporteth_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_ADDRESSto thefirstProverProofSubmitteraddress in the manifest. - Set
PROVER_IDon 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(default5) 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-rangeonly 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-rpcorREAD_L1_RPC_URL) and the Aztec node (--aztec-nodeorAZTEC_NODE_URL).no deployment with portal. Pick a portal address from the manifest'sdeploymentsarray.- 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_simulateV1error 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. |