# 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.

Source: https://docs.layerhop.com/guides/gasless-swaps

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=true` returns `FORBIDDEN`.

## 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.gasFee` on the quote. `dstAmount` already accounts for it.
- 0x charges its own 0.15% on gasless swaps, also already in `dstAmount`.

## Flow

1. `GET /v1/quote` with `gasless=true`.
2. `POST /v1/orders` with the `quoteId`. Instead of `tx`, the response has `gasless`: typed data to sign (`approval` when present, then `trade`) on `gasless.chainId`.
3. Sign each payload with `eth_signTypedData_v4` (viem / wagmi `signTypedData`), unchanged.
4. `POST /v1/orders/gasless` with the signatures. The provider's relayer sends the transaction.
5. Poll `GET /v1/orders/gasless/{quoteId}` until `state` is `filled` (with `srcTxHash`) or `failed` (with `reason`). Once the relayer's transaction is mined the order is tracked like any other (order list, webhooks).

```bash
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:

```json
"gasless": { "gasFee": { "amount": "42000", "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" }, "approval": true }
```

Submit the signatures (65-byte hex each):

```bash
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

```ts
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 `PermitWitnessTransferFrom` on 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.
