Skip to main content
This guide shows you how to run a small HTTP server that takes signed requests from your users and pays the gas to send them, without letting anyone drain its wallet.
Everything on this page uses testnet (chain 968). Get free test tokens from the faucet.

What you’ll build

  • relayer.ts: a server with POST /relay and GET /health. It only pays for one contract and one function, caps gas, rate-limits each signer and IP address, and dry-runs every request before it spends anything.
  • sign-and-relay.ts: a client that signs a request from a new, empty wallet and posts it to the relayer.
It uses Node’s built-in http module and viem, so there’s nothing else to install. For a production setup with a web app, start from the gasless app template instead.

Prerequisites

  • You understand the flow in Meta-transactions.
  • The bot-defi project from Wrap BOT.
  • A separate wallet for the relayer, funded with tBOT from the faucet. Each relay costs about 0.0027 tBOT at 20 gwei.
  • A forwarder and a target contract. This guide uses the template’s testnet UzoForwarder and GuestBook. To use your own, deploy them as in Meta-transactions and change the selector allowlist.

Steps

1

Configure the relayer

.env.relayer
Use a key that holds only what you’re willing to spend on gas. Add .env.relayer to .gitignore.
2

Write the server

relayer.ts
3

Understand the checks

The order matters. Cheap checks that don’t touch the chain run first, so junk requests cost you nothing.
  1. Target allowlist. The relayer only pays for calls to TARGET. Without this, anyone could use your gas for any contract that trusts the forwarder.
  2. Selector allowlist. Only sign(string) is paid for. Add each function you want to sponsor.
  3. Value and gas. value must be 0, so the relayer never sends tBOT. gas is capped, because the relayer pays for whatever the request asks for.
  4. Deadline. Expired requests are rejected before the RPC sees them.
  5. Rate limits. 10 requests per hour per signer and per IP address. Fresh wallets cost nothing to create, so the IP limit matters as much as the signer limit.
  6. verify and simulateContract. The forwarder checks the signature, nonce and deadline, then the whole call is dry-run. A request that would revert never costs gas.
  7. Serial sending. One transaction at a time, so two requests never get the same relayer nonce.
4

Start it

In another terminal, check its balance:
5

Write the client

sign-and-relay.ts
6

Send a request

The client needs only the contract addresses. It has no key of its own to fund.
.env.client

Verify it worked

On testnet on 2026-10-02, the client printed:
Output
and entryCount() went up by one. Open your own hash on BOTScan: the sender is the relayer, the target is the forwarder, and the guest book entry’s author is the new wallet. Then check the guards. In the same testnet run: Requests refused by the allowlists don’t count toward the rate limit. Requests that reach the limit check do, even if they fail later.
To test without spending tBOT, run the relayer against a local fork. Start anvil --fork-url https://rpc.bohr.life, set RPC_URL=http://127.0.0.1:8545 in both env files, and use one of anvil’s prefunded dev keys as RELAYER_PRIVATE_KEY. Never use those keys on a real network.

Before you go live

This server is deliberately small. Before real users depend on it:
  • Real client IPs. Behind a proxy or load balancer, remoteAddress is the proxy. Read the forwarded client IP, but only from a proxy you trust, or anyone can fake it.
  • Shared rate limits. The limits live in memory and reset on restart. Use a shared store such as Redis if you run more than one instance.
  • CORS and HTTPS. Allow only your app’s origin, and serve over HTTPS.
  • Balance alerts. Watch /health and alert before the wallet runs dry.
  • Key storage. Keep the relayer key in a secrets manager, not a file on disk.
  • Stuck transactions. If a send times out, the relayer nonce can get stuck. Track pending transactions and replace them with a higher gas price.
The gasless app template handles the first four with RELAYER_TRUST_PROXY, RELAYER_ALLOWED_ORIGINS and RELAYER_LOW_BALANCE. See Customize the gasless app.

Troubleshooting

The selector isn’t in ALLOWED_SELECTORS. Add it with toFunctionSelector("yourFunction(type)"), using the exact Solidity signature.
The client signed with a different domain or nonce, or changed a field after signing. A request that already ran also fails here, because its nonce is used.
simulateContract failed, so nothing was sent. The reason comes from the forwarder or your contract. See Meta-transactions troubleshooting.
The relayer wallet is empty. Top it up from the faucet.
The signer or IP address passed 10 requests in the last hour. Wait, or raise RATE_LIMIT for testing.

Next steps

Gasless app template

A hardened relayer with a web app.

EOA paymaster

The other way to sponsor gas.
Last modified on October 2, 2026