ZecpassGet a pass

Relayer

The relayer is one Node process in relayer/, deployed on Fly.io as zecpass-relayer. It owns the order lifecycle, talks to NEAR Intents and Relay, and sends two kinds of transactions: the Relay deposit on Arbitrum One and the deliver / sweep calls on Robinhood Chain.

One key, two jobs

The relayer key (0x5C9A100eC739Bd7d80F1502e97Be9f549986951F) is:

  • the recipient on Arbitrum One of every NEAR Intents settlement, and the sender of every Relay deposit from there;
  • the signer on Robinhood Chain of deliver, sweep and, on launch day, setToken.

It needs a little ETH on both chains for gas. It never holds $ZECPASS. In bridge mode it holds one order's ETH on Arbitrum One for the seconds between the settlement and the Relay deposit. In instant mode it holds nothing: the hub vault pays and the Arbitrum ETH is bridged to the hub afterward.

Storage

DATA_DIR/orders.json, written atomically (temp file plus rename) after every change. No database. Each order:

{
  "id": "3f9c1a7e2b6d4c80",
  "createdAt": "2026-09-21T10:00:00.000Z",
  "amountZec": "0.25",
  "beneficiary": "0x…",
  "refundTo": "t1…",
  "pass": "0x…",
  "rail": "relay",
  "depositAddress": "t1…",
  "quote": { "amountOut": "142000000000000000", "amountOutFormatted": "0.142", "amountOutUsd": "375.10", "timeEstimate": 454, "deadline": "2026-09-21T10:20:00.000Z" },
  "state": "awaiting_deposit",
  "events": [{ "at": "…", "state": "awaiting_deposit", "note": "quote 0.25 ZEC → 0.142 ETH (arb)" }],
  "ethOnArbWei": null, "arbTxHash": null, "relayRequestId": null, "hoodTxHash": null,
  "deliverTxHash": null, "tokensOut": null, "ethIn": null, "fee": null, "mode": null
}

Terminal orders older than thirty days are pruned on start.

The state machine, edge by edge

From To Condition
(request) awaiting_deposit validation passed and 1Click returned a deposit address
awaiting_deposit deposit_seen 1Click status KNOWN_DEPOSIT_TX or INCOMPLETE_DEPOSIT
awaiting_deposit expired no deposit and now > quote deadline + 30 min
deposit_seen settling 1Click status PROCESSING
awaiting_deposit, deposit_seen, settling settled_arb 1Click status SUCCESS; records ethOnArbWei and arbTxHash from swapDetails
any open refunded 1Click status REFUNDED
any open failed 1Click status FAILED, or a Zecpass transaction reverted (reason recorded)
settled_arb delivering (instant) INSTANT_MODE on, hub vault balance ≥ ethOnArbWei, ethOnArbWei ≤ maxPerOrder, day spend + amount ≤ dailyCap, token set, hub not paused
settled_arb bridging (bridge) otherwise; the Relay deposit transaction was sent from Arbitrum One with recipient = pass
delivering (instant) delivered deliver receipt is success; tokensOut, ethIn, fee read from the Delivered log; then a Relay deposit of ethOnArbWei toward the hub is sent to refill the vault (refillRequestId)
bridging bridged Relay status success; hoodTxHash recorded
bridged delivering token set and hub not paused; sweep(beneficiary, 0) sent
bridged bridged token not set or hub paused: note recorded, retried every tick; the beneficiary can withdraw meanwhile
delivering (bridge) delivered sweep receipt is success; fields read from the Swept log

Sending discipline: a transaction hash is persisted before the receipt is awaited, so a crash between the two never resends. On restart, orders in delivering or bridging with a hash are resumed by fetching the receipt or the Relay status, not by sending again.

Environment

Variable Default Meaning
RELAYER_PK required the relayer key
ZECPASS_HUB required hub address
ZECPASS_TOKEN empty $ZECPASS; optional, the hub's token() is the source of truth
ROBINHOOD_RPC https://rpc.mainnet.chain.robinhood.com
ARBITRUM_RPC https://arb1.arbitrum.io/rpc
DATA_DIR /data where orders.json lives
PORT 8080
TICK_MS 20000 loop period
ONECLICK_JWT empty partner key; when set it is sent as X-API-Key and removes 1Click's 25 bps unauthenticated surcharge
APP_FEE_BPS / APP_FEE_RECIPIENT 0 / empty an optional 1Click app fee, charged in ZEC on the input; off in version one
MIN_ZEC / MAX_ZEC 0.01 / 5 order bounds
INSTANT_MODE true use the vault when it can cover an order
SITE_URL for CORS notes and logs only

HTTP API

All responses are JSON. CORS is open for GET and POST.

Method and path Body / query Returns
POST /orders {amountZec, beneficiary, refundTo} the new order (state awaiting_deposit)
GET /orders/:id the order with events and hashes
POST /orders/:id/deposit {txHash} forwards the Zcash transaction id to 1Click; {ok:true}
GET /quote?amountZec=0.25 dry quote through both rails plus the hub fee, see API
GET /rails direct rail and Relay availability, vault balance and caps, token phase, relayer balances
GET /stats hub totals plus order counts by state
GET /health {ok:true, tick, openOrders}

Validation on POST /orders: amountZec within bounds, beneficiary a checksummed or lowercase EVM address, refundTo matching ^t[13][a-km-zA-HJ-NP-Z1-9]{33}$. Errors are 400 with {error}.

Probes

pnpm probe prints a live picture: a dry ZEC to Arbitrum quote, whether the direct rail has liquidity, a dry Relay quote for the resulting amount, and the relayer's balances on both chains. Run it before a launch and whenever the stats page looks wrong.

Deployment on Fly

cd relayer
fly launch --no-deploy --copy-config --name zecpass-relayer
fly volumes create zecpass_data --size 1 --region cdg
fly secrets set RELAYER_PK=0x… ZECPASS_HUB=0x… ZECPASS_TOKEN=0x…
fly deploy
fly logs

One shared-cpu-1x machine with 512 MB, min_machines_running = 1, never auto-stopped. The volume is the only state; back it up with fly ssh console -C "cat /data/orders.json" if you care about the history.

When an order is stuck

Symptom Cause Action
awaiting_deposit for long the user has not sent, or sent after the deadline nothing; it expires or settles at market
settling for more than 30 min 1Click slow, or Zcash congestion curl https://1click.chaindefuser.com/v0/status?depositAddress=…; if REFUNDED, the loop will pick it up
settled_arb for more than 2 ticks the relayer has no gas on Arbitrum One, or Relay refused the quote check GET /rails for relayerArb.balanceWei; top up; the loop retries
bridging for more than 5 min Relay did not fill check https://api.relay.link/intents/status?requestId=…; on refund, the ETH is back at the relayer on Arbitrum, resend by clearing relayRequestId in the store
bridged and not moving token unset or hub paused the ETH is safe in the pass; set the token or unpause; the user can withdraw any time
delivering for more than 2 ticks a Robinhood transaction is pending or reverted look at the hash in the order; a revert reason is written to the events