# Gasless

The taker signs one Permit2 message; the Qiguard relayer sends the transaction and pays its gas. Gasless runs on BSC (56) and Base (8453).

Gasless is being enabled per chain; until then `POST /gasless/submit` returns `503 GASLESS_NOT_ENABLED` (`data.reason` `gasless_not_enabled`), and `GET /gasless/chains` shows `enabled: false` for that chain. `GET /gasless/price` and `GET /gasless/quote` work on both chains meanwhile and add the warning `gasless_not_enabled`.

## Flow

1. `GET /gasless/quote` with `taker`. The answer holds `trade.eip712`, the typed data to sign, and `trade.hash`, its EIP-712 digest. (`GET /gasless/price` reports a short taker balance in `issues.balance`; the quote answers `400 INSUFFICIENT_BALANCE`.)
2. First use of a sell token: the taker sends one normal `approve(Permit2, amount)` transaction and pays its gas. `issues.allowance` (spender Permit2 `0x000000000022D473030F116dDEE9F6B43aC78BA3`) shows when it is needed. `approval` is always `null` in v1.
3. Before signing, check the typed data against what you asked for: `domain.verifyingContract` is Permit2, `domain.chainId` is your chain, `message.spender` is the settlement contract of the chain ([Contracts and fees](contracts.md)), `witness.recipient` is the taker, `permitted.token` and `permitted.amount` are your sell token and amount, and the deadline is at most 600 seconds ahead. The SDK's `assertGaslessTrade` does all of this.
4. The taker signs `trade.eip712` with `eth_signTypedData_v4`.
5. `POST /gasless/submit` with the typed data unchanged and the signature split into `v`, `r`, `s`. The answer is `{ tradeHash, type, zid }`; `tradeHash` equals `trade.hash`.
6. `GET /gasless/status/{tradeHash}?chainId=` until `confirmed` or `failed`. If the submit timed out, or answered `503 RELAYER_UNAVAILABLE` with `data.tradeHash` (`relayer_timeout`, `relayer_unreachable`), read this status before quoting and signing again: the trade may have been accepted (`404` means it was not).

```json
{
  "chainId": 8453,
  "trade": {
    "type": "qiguard_permit2_witness",
    "eip712": { "types": "...", "primaryType": "PermitWitnessTransferFrom", "domain": "...", "message": "..." },
    "signature": { "signatureType": 2, "v": 27, "r": "0x...", "s": "0x..." }
  }
}
```

`trade.eip712` must be sent back exactly as the quote returned it: any change to `types`, `primaryType` or `domain` gives `400 META_TRANSACTION_INVALID`. Unknown keys in the body give `400 INPUT_INVALID`; an `approval` that is not `null` gives `400 INVALID_APPROVAL`.

## What the signature fixes

The signed message is a Permit2 `PermitWitnessTransferFrom` with an order witness:

| Field | Value |
|---|---|
| `permitted.token`, `permitted.amount` | `sellToken`, `sellAmount` |
| `spender` | The settlement contract of the chain |
| `nonce` | A random unordered Permit2 nonce |
| `deadline` | The signed deadline (`expiresAt`) |
| `witness.tokenOut` | `buyToken` |
| `witness.minAmountOut` | `minBuyAmount` (net, after the fee) |
| `witness.recipient` | The taker; the signer must be this address |
| `witness.adapter` | The liquidity source adapter |
| `witness.deadline` | Equal to `deadline` |

The contract pays at least `minAmountOut` of `tokenOut` to the recipient, or the transaction reverts and nothing moves. The route inside the adapter is not part of the signature.

## Who sees the signed message

The signed message is seen, before the transaction is included, by: the integrator's backend (it relays the signature to `/gasless/submit`), Qiguard (including the private, keyed RPC endpoint the relayer uses to simulate the transaction before it is sent), and the broadcast channel (48.club and bloXroute on BSC; the Base sequencer RPC on Base). No public RPC receives it. Any of them can submit the signature first with its own route and keep the value between the fair output and `minBuyAmount`. The taker always receives at least `minBuyAmount`. For this reason gasless slippage defaults to 50 bps and is capped at 300 bps, and gasless transactions never go to a public mempool. `protection.signatureExposure` and `protection.mev` state the same per quote.

## Rules

| Rule | Value |
|---|---|
| Tokens | ERC-20 to ERC-20; the wrapped native token is allowed, the native coin is not (`400 TOKEN_PAIR_NOT_SUPPORTED`, `data.reason` `native_not_supported`) |
| Slippage | `slippageBps` default 50, allowed 10 to 300 |
| Deadline | `deadlineSeconds` default 180, allowed 60 to 600 |
| Minimum trade | 10 USD |
| Taker wallet | An EOA. A contract wallet gives `400 SMART_WALLET_NOT_SUPPORTED`; an EOA with an EIP-7702 delegation is accepted |
| Balance | Below `sellAmount`: the quote answers `400 INSUFFICIENT_BALANCE` with `data.token`, `data.actual`, `data.expected`; the price answers `200` with the same fields in `issues.balance` |
| Trades per taker | One pending trade per chain (`400 PENDING_TRADES_ALREADY_EXIST`), 30 per hour (`429 RATE_LIMITED`, `data.reason` `taker_hourly_limit`) |
| Submit deadline | At least 15 seconds left at submit, else `400 META_TRANSACTION_EXPIRY_TOO_SOON` |

## Sponsored gas

In v1 the gas is paid by Qiguard: `fees.gasFee = { amount: "0", token, type: "gas", sponsored: true, estimatedCostUsd }`. A trade is sponsored when its estimated gas cost (with the Base L1 data fee) is at most half of its Qiguard fee, and the daily budgets below have room.

| Answer | When |
|---|---|
| `400 SELL_AMOUNT_TOO_SMALL` with `data.minSellAmount` | The trade is below 10 USD, or the gas is above half of the fee. `minSellAmount` is the smallest amount that passes. |
| `429 QUOTA_EXCEEDED`, `data.reason` `gasless_submits_per_day` | The app reached its gasless submits for the day (see [Rate limits and plans](rate-limits.md)). |
| `429 GAS_BUDGET_EXCEEDED`, `data.reason` `app_daily`, `chain_daily` or `free_tier_daily` | The sponsored-gas budget of the app, of the chain, or of all free-plan apps together is used up for the day. |
| `403 TAKER_NOT_AUTHORIZED_FOR_TRADE`, `data.reason` `gasless_blocked` or `denied` | The taker is blocked for gasless (for example after 3 failed relays in 24 hours) or is on the deny list. |

Budgets reset at 00:00 UTC.

## Submit and relay

- The relayer rebuilds the typed data from its own constants, recovers the signer and checks it is `witness.recipient` (`400 INVALID_SIGNATURE`, `400 INVALID_SIGNER`).
- It builds the calldata itself (`fillOrderWithPermit2` on the settlement contract), checks the Permit2 nonce, the balance and the Permit2 allowance (`400 INSUFFICIENT_BALANCE_OR_ALLOWANCE`), and runs the exact transaction with `eth_call` on a private RPC endpoint before sending. A transaction whose simulation reverts is never sent.
- If more than 3 seconds pass between the simulation and the broadcast, it simulates again.
- BSC transactions go only to 48.club and bloXroute; Base transactions go to the Base sequencer (Base has no public mempool).
- A transaction that is not mined is sent again after 15 seconds and replaced with a 12.5% higher gas price after 30 seconds (at most twice). Near the deadline the relayer cancels it with a 0-value transaction to itself; the trade ends `failed` with `order_expired`.

## Status

| `status` | Meaning |
|---|---|
| `pending` | Accepted, not yet broadcast |
| `submitted` | Broadcast; `transactions[]` lists every hash sent (replacements included) |
| `succeeded` | Mined with receipt status 1 |
| `confirmed` | 5 confirmations on BSC, 3 on Base |
| `failed` | Ended without a trade; `reason` is one of `transaction_simulation_failed`, `order_expired`, `transaction_reverted`, `insufficient_allowance`, `insufficient_balance`, `internal_error`; `detail` adds the cause when known (for example `slippage` or `permit_consumed_by_other_tx`) |

Only the app that submitted a trade can read its status; any other app gets `404 META_TRANSACTION_STATUS_NOT_FOUND`.

## Submitting twice

`POST /gasless/submit` is idempotent by `tradeHash`: the same trade from the same app returns `200` with the same body; from another app it returns `400 TRADE_ALREADY_SUBMITTED`. The `Idempotency-Key` header is honoured as well (see [Idempotency](idempotency.md)).

## Relay errors

| Answer | Meaning |
|---|---|
| `503 GASLESS_NOT_ENABLED` | Gasless is not switched on for the chain (`data.reason` `gasless_not_enabled`) |
| `503 RELAYER_UNAVAILABLE`, `data.reason` `relayer_busy` | The relayer queue of the chain is full; `Retry-After: 3` |
| `503 RELAYER_UNAVAILABLE`, `data.reason` `relayer_low_balance`, `relayer_stalled`, `gas_price_above_cap` | The relayer cannot send now; retry later |
| `503 RELAYER_UNAVAILABLE`, `data.reason` `relayer_timeout` or `relayer_unreachable`, with `data.tradeHash` | The answer did not come in time; read `GET /gasless/status/{tradeHash}` before quoting again (`404`: not accepted, submit again or quote again) |
| `400 META_TRANSACTION_INVALID`, `data.reason` `price_moved` | The signed minimum is no longer reachable; quote and sign again |
| `400 TRADE_ALREADY_SUBMITTED`, `data.reason` `permit_nonce_used` | The Permit2 nonce of the signature is already used |
