Gasless swaps
Let a wallet with no native gas swap an ERC-20 on an EVM chain. The user signs typed data, the provider's relayer sends the transaction and takes the network fee from the token.
A user holding USDC on Base but no ETH can't send a swap transaction: there is nothing to pay gas with. With gasless swaps the wallet signs EIP-712 typed data instead, and the provider's relayer sends the transaction and takes the network fee out of the token being sold. LayerHop never holds the funds and pays no gas; the signature authorizes exactly one trade (recipient, output token, minimum output and deadline are part of what is signed).
Gasless swaps are off unless the deployment enables them. While they are off,
gasless=truereturnsFORBIDDEN.
When does it apply?#
- Same-chain swaps on an EVM chain where the relayer runs: Ethereum, Arbitrum, Base, OP Mainnet, Polygon, BNB Chain, Avalanche, Unichain, World Chain, Sonic, Scroll, Berachain, Ink, Abstract, HyperEVM, Monad, Plasma, Robinhood Chain and Tempo (not Linea).
- The input is an ERC-20 token. Native coins can't be sold gaslessly (sell WETH, or use a normal swap).
- Live mode only. Test keys get
FORBIDDEN. - Today 0x is the only provider that relays gasless swaps, so only 0x quotes come back.
The input token needs an allowance for Permit2. When it has none, most popular tokens (USDC and other EIP-2612 tokens) offer a gasless approval: a second signature, signed before the trade. Tokens without one would need an approve transaction paid in gas, so they get no gasless quote.
Fees#
- LayerHop's fee is the same as for any swap (
fees.ourFeeBps). - The network fee is taken from the input token and shown as
gasless.gasFeeon the quote.dstAmountalready accounts for it. - 0x charges its own 0.15% on gasless swaps, also already in
dstAmount.
Flow#
GET /v1/quotewithgasless=true.POST /v1/orderswith thequoteId. Instead oftx, the response hasgasless: typed data to sign (approvalwhen present, thentrade) ongasless.chainId.- Sign each payload with
eth_signTypedData_v4(viem / wagmisignTypedData), unchanged. POST /v1/orders/gaslesswith the signatures. The provider's relayer sends the transaction.- Poll
GET /v1/orders/gasless/{quoteId}untilstateisfilled(withsrcTxHash) orfailed(withreason). Once the relayer's transaction is mined the order is tracked like any other (order list, webhooks).
curl "https://api.layerhop.com/v1/quote?srcChain=base&srcToken=USDC&amount=25000000\
&dstChain=base&dstToken=ETH&sender=0xYou&recipient=0xYou&gasless=true"The quote carries what the relayer takes and whether a gasless approval comes first:
"gasless": { "gasFee": { "amount": "42000", "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" }, "approval": true }Submit the signatures (65-byte hex each):
curl -X POST https://api.layerhop.com/v1/orders/gasless -H 'content-type: application/json' \
-d '{ "quoteId": "q_…", "signatures": { "approval": "0x…", "trade": "0x…" } }'With the TypeScript SDK and wagmi#
import { LayerHop } from "@layerhop/sdk";
import { signGasless, wagmiGaslessSigner } from "@layerhop/wagmi/actions";
const lh = new LayerHop({ apiKey: process.env.LAYERHOP_KEY });
const { best } = await lh.quote({
srcChain: "base", srcToken: "USDC", amount: "25000000", // 25 USDC
dstChain: "base", dstToken: "ETH",
sender: address, recipient: address, gasless: true,
});
console.log("gas paid in USDC:", best.gasless?.gasFee?.amount);
const typed = await lh.buildGasless(best.quoteId);
// Checks the typed data against what you asked for, switches chain, then 1 or 2 signatures.
const signatures = await signGasless(wagmiGaslessSigner(wagmiConfig), typed, {
expect: {
chainId: 8453, srcToken: USDC_BASE, srcAmount: "25000000",
recipient: address, dstToken: ETH, minAmountOut: best.dstAmountMin,
},
});
await lh.submitGasless(best.quoteId, signatures);
let s = await lh.gaslessStatus(best.quoteId);
while (s.state !== "filled" && s.state !== "failed") {
await new Promise((r) => setTimeout(r, 2000));
s = await lh.gaslessStatus(best.quoteId);
}signGasless takes any signer with getAccount, switchChain and signTypedData, so a viem wallet client or an embedded wallet works too.
What gets checked before signing#
Never sign gasless typed data blindly. LayerHop checks every 0x gasless quote when it is made, and signGasless checks it again in the browser against expect (the same rules are exported as gaslessProblems from @layerhop/sdk for other signers). Anything else throws GASLESS_UNSAFE before the wallet is asked:
- Trade: a Permit2
PermitWitnessTransferFromon the swap's chain, verified by the canonical Permit2 (0x000000000022D473030F116dDEE9F6B43aC78BA3), for the sell token and at most the sell amount. The spender is 0x's settler for this quote (never zero, Permit2, the token or your wallet). The witness pays the output to your recipient, in the buy token, with at least the quoted minimum. The deadline is less than 10 minutes away. - Approval (when present): signed on the sell token's own contract and chain, by your wallet, for Permit2 only. The amount is at most the sell amount, or unlimited: 0x's gasless approvals give Permit2 an unlimited allowance, and Permit2 still needs a per-trade signature like the one above to move anything.
A gasless quote stays valid until shortly before the trade's deadline (at most 2 minutes). Submitting later answers 410 QUOTE_EXPIRED: get a new quote and sign again.
Errors#
| Situation | Answer |
|---|---|
| Gasless off on this deployment, or a test key | 403 FORBIDDEN |
| Native input, cross-chain, Solana, or a chain without the relayer | 400 INVALID_PARAMS |
| Token without a gasless approval and no allowance | the quote lists UNSUPPORTED_TOKEN for 0x in errors; no route |
| Wrong number of signatures, malformed signature | 400 INVALID_PARAMS |
| Same quote submitted twice | 409 CONFLICT (409 GASLESS_PENDING while the first submission is still being relayed) |
| Quote expired before the signatures arrived | 410 QUOTE_EXPIRED: get a new quote and sign again |
| Relayer rejected the signatures (bad signature, wrong signer, balance or allowance too low) | 422 GASLESS_FAILED; nothing was sent |
| Another gasless trade of the same token from this wallet still pending | 409 GASLESS_PENDING |
Typed data doesn't match the request (checked by LayerHop and by signGasless) |
502 PROVIDER_ERROR on the quote, GASLESS_UNSAFE in the browser |
| Relayer gave up (expired, reverted, balance moved) | state: "failed" with reason on the status |
Smart-contract wallets work only when the approval is an erc7597Permit (USDC) or the allowance is already set; EIP-2612 permits need a plain ECDSA signature.
Last updated