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
- Only allow known recipient contracts (
- Verifies the request against the forwarder:
- Call
forwarder.verify(...)(or equivalent) before sending an on-chain tx
- Call
- Executes on-chain:
- Call
forwarder.execute(...)using the relayer signer - The relayer pays gas
- Call
- 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
Recommended safety controls
- 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:
- Reject unknown targets (allowlist)
- Validate request with
forwarder.verify(...)off-chain - 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 };