Skip to main content

Relayer (Gas Payer)

The relayer is an off-chain service (or script) that provides the “gasless” UX. It receives user-signed meta-transaction requests and submits real on-chain transactions to the trusted forwarder, paying gas from a funded relayer wallet.

What the relayer does

  • Receives { request, signature } from a frontend or client.
  • Validates policy (recommended):
    • Only allow known recipient contracts (request.to)
    • Optionally only allow specific function selectors
    • Apply rate limits/quotas to prevent abuse
  • Verifies the request against the forwarder:
    • Call forwarder.verify(...) (or equivalent) before sending an on-chain tx
  • Executes on-chain:
    • Call forwarder.execute(...) using the relayer signer
    • The relayer pays gas
  • Returns the resulting transaction hash/status to the caller

What you must operate

  • A relayer private key stored securely (secret manager / vault).
  • A relayer address funded with native gas token on each supported network.
  • Monitoring:
    • Relayer balance alerts
    • Request volume, failures, and common error reasons
  • Target allowlist: do not relay arbitrary contracts.
  • Method allowlist: optionally restrict to specific selectors to avoid unexpected calls.
  • Request size limits: reject overly large calldata.
  • Replay/nonce awareness: treat nonce errors as a signal of retry or race conditions.

Example relayer logic (from watr-gasless-transactions/relay-server)

The relay server in this repo follows a simple, production-friendly pattern:

  1. Reject unknown targets (allowlist)
  2. Validate request with forwarder.verify(...) off-chain
  3. Submit forwarder.execute(...) on-chain (relayer pays gas)
// 1) Target must be in an allowlist
// 2) forwarder.verify(request) must be true
// 3) forwarder.execute(request) sends an on-chain tx (relayer pays gas)
const valid = await forwarder.verify(request);
if (!valid) throw new Error("Relay rejected: forwarder.verify() returned false.");

const tx = await forwarder.execute(request, {
gasLimit: BigInt(request.gas) + 60_000n
});
const receipt = await tx.wait();
return { txHash: tx.hash, block: receipt.blockNumber };

See also