Post 18

Signing Without Gas: EIP-712, Permit2, and Meta-Transactions

An off-chain signature costs nothing, confirms instantly, and can authorise a transfer, an approval, an order, or a whole batch. Here is how to build gasless approvals and intents on the front end, and where the sharp edges are.

Jul 2, 2026/12 min readWeb3
ShareY
Signing Without Gas: EIP-712, Permit2, and Meta-Transactions

Every wallet prompt is a moment the user can bail. The classic ERC-20 flow has two of them — approve, then act — and the first one costs gas to grant a permission the user does not even understand. A signed message replaces that: no gas, instant, and the wallet shows structured, human-readable data instead of a hex blob.

EIP-712: structured data the wallet can render

EIP-712 defines a way to hash typed structured data so a wallet can show the user exactly what they are signing — field names and values, not 0x1901.... Every gasless pattern is built on it.

signing a typed order
1import { useSignTypedData } from "wagmi";2 3const { signTypedDataAsync } = useSignTypedData();4 5const signature = await signTypedDataAsync({6  domain: {7    name: "MyDEX",8    version: "1",9    chainId: 8453,10    verifyingContract: SETTLEMENT_CONTRACT,11  },12  types: {13    Order: [14      { name: "maker", type: "address" },15      { name: "sell", type: "address" },16      { name: "buy", type: "address" },17      { name: "sellAmount", type: "uint256" },18      { name: "buyAmount", type: "uint256" },19      { name: "expiry", type: "uint256" },20      { name: "nonce", type: "uint256" },21    ],22  },23  primaryType: "Order",24  message: {25    maker: user, sell: USDC, buy: WETH,26    sellAmount, buyAmount, expiry, nonce,27  },28});29// the user signs a readable order; a solver/relayer submits it on-chain

ERC-2612 permit: gasless approval, per token

Tokens that implement ERC-2612 (permit) let you approve via a signature. The signature travels with your transaction and the contract calls permit first, then does its thing — one on-chain transaction, one signature, zero separate approval.

build a permit signature
1const nonce = await client.readContract({2  address: token, abi: erc2612Abi, functionName: "nonces", args: [owner],3});4const name = await client.readContract({ address: token, abi: erc2612Abi, functionName: "name" });5 6const signature = await walletClient.signTypedData({7  account: owner,8  domain: { name, version: "1", chainId, verifyingContract: token },9  types: {10    Permit: [11      { name: "owner", type: "address" },12      { name: "spender", type: "address" },13      { name: "value", type: "uint256" },14      { name: "nonce", type: "uint256" },15      { name: "deadline", type: "uint256" },16    ],17  },18  primaryType: "Permit",19  message: { owner, spender, value, nonce, deadline },20});21 22const { r, s, v } = parseSignature(signature);23// pass r, s, v, deadline into the contract call that needs the allowance
Watch out

Not every token supports permit, and some (looking at you, USDC on some chains) use a non-standard version string or domain. Always read name, nonces, and — where present — version from the token rather than hardcoding. Feature-detect and fall back to a normal approve when permit is absent.

Permit2: gasless approval for tokens that do not support permit

Uniswap's Permit2 is a single canonical contract that holds allowances on behalf of every integrating protocol. The user does one traditional approve to Permit2 ever, then all subsequent approvals — for any protocol — are signatures.

a Permit2 single-transfer signature
1import { SignatureTransfer } from "@uniswap/permit2-sdk";2 3const permit = {4  permitted: { token: USDC, amount },5  spender: MY_PROTOCOL,6  nonce,                                   // unordered nonce7  deadline: BigInt(Math.floor(Date.now() / 1000) + 1800),8};9 10const { domain, types, values } = SignatureTransfer.getPermitData(11  permit, PERMIT2_ADDRESS, chainId,12);13 14const signature = await walletClient.signTypedData({15  account: user, domain, types, primaryType: "PermitTransferFrom", message: values,16});17// your contract calls permit2.permitTransferFrom(permit, transferDetails, user, signature)

Meta-transactions: the user signs, someone else pays

The general pattern: the user signs an EIP-712 message describing an action, a relayer wraps it in a real transaction and pays the gas, and the target contract recovers the signer and acts as if it were msg.sender. ERC-2771 (_msgSender() via a trusted forwarder) is the common on-chain side.

front end: sign and hand off to a relayer
1const request = {2  from: user,3  to: TARGET_CONTRACT,4  value: 0n,5  gas: 200_000n,6  nonce: await forwarder.read.getNonce([user]),7  data: encodeFunctionData({ abi, functionName: "claim", args: [tokenId] }),8};9 10const signature = await walletClient.signTypedData({11  account: user,12  domain: forwarderDomain,13  types: forwardRequestTypes,14  primaryType: "ForwardRequest",15  message: request,16});17 18await fetch("/api/relay", {19  method: "POST",20  body: JSON.stringify({ request, signature }),21});22// your /api/relay route (or Gelato / OpenZeppelin Defender) submits it

The sharp edges

  • Phishing risk is real. A signature can authorise a token transfer. Wallets now warn on suspicious typed data — do not design flows that train users to sign things blindly.
  • Nonces and replay. Every scheme needs a nonce (ordered or unordered) and a deadline. Show the deadline to the user; expired signatures fail confusingly otherwise.
  • Smart accounts sign differently. A contract wallet's signature is verified with EIP-1271, not ecrecover. If your relayer or contract verifies signatures, support both.
  • The relayer is a trust and liveness dependency. If it is down, the gasless path is down. Offer a 'pay gas yourself' fallback.
A signature is a promise the user makes for free. The whole art is making the promise legible — so they know what they agreed to — and making the fallback obvious when the gasless rail fails.

Found this useful? Pass it on.

All posts

Want a custom write-up for your team? Get in touch.

Building something like this?

If a post here maps to a problem on your roadmap, that's usually a good sign we should talk.