What you’ll build
relayer.ts: a server withPOST /relayandGET /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.
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-defiproject 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
UzoForwarderandGuestBook. To use your own, deploy them as in Meta-transactions and change the selector allowlist.
Steps
1
Configure the relayer
.env.relayer
.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.
- Target allowlist. The relayer only pays for calls to
TARGET. Without this, anyone could use your gas for any contract that trusts the forwarder. - Selector allowlist. Only
sign(string)is paid for. Add each function you want to sponsor. - Value and gas.
valuemust be 0, so the relayer never sends tBOT.gasis capped, because the relayer pays for whatever the request asks for. - Deadline. Expired requests are rejected before the RPC sees them.
- 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.
verifyandsimulateContract. The forwarder checks the signature, nonce and deadline, then the whole call is dry-run. A request that would revert never costs gas.- Serial sending. One transaction at a time, so two requests never get the same relayer nonce.
4
Start it
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
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.
Before you go live
This server is deliberately small. Before real users depend on it:- Real client IPs. Behind a proxy or load balancer,
remoteAddressis 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
/healthand 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.
RELAYER_TRUST_PROXY, RELAYER_ALLOWED_ORIGINS and RELAYER_LOW_BALANCE. See Customize the gasless app.
Troubleshooting
403 This relayer doesn't pay for that function
403 This relayer doesn't pay for that function
The selector isn’t in
ALLOWED_SELECTORS. Add it with toFunctionSelector("yourFunction(type)"), using the exact Solidity signature.400 The signature doesn't match the request
400 The signature doesn't match the request
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.
400 with a revert reason
400 with a revert reason
simulateContract failed, so nothing was sent. The reason comes from the forwarder or your contract. See Meta-transactions troubleshooting.insufficient funds for gas
insufficient funds for gas
The relayer wallet is empty. Top it up from the faucet.
429 Too many requests
429 Too many requests
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.