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
GET /gasless/quotewithtaker. The answer holdstrade.eip712, the typed data to sign, andtrade.hash, its EIP-712 digest. (GET /gasless/pricereports a short taker balance inissues.balance; the quote answers400 INSUFFICIENT_BALANCE.)- First use of a sell token: the taker sends one normal
approve(Permit2, amount)transaction and pays its gas.issues.allowance(spender Permit20x000000000022D473030F116dDEE9F6B43aC78BA3) shows when it is needed.approvalis alwaysnullin v1. - Before signing, check the typed data against what you asked for:
domain.verifyingContractis Permit2,domain.chainIdis your chain,message.spenderis the settlement contract of the chain (Contracts and fees),witness.recipientis the taker,permitted.tokenandpermitted.amountare your sell token and amount, and the deadline is at most 600 seconds ahead. The SDK'sassertGaslessTradedoes all of this. - The taker signs
trade.eip712witheth_signTypedData_v4. POST /gasless/submitwith the typed data unchanged and the signature split intov,r,s. The answer is{ tradeHash, type, zid };tradeHashequalstrade.hash.GET /gasless/status/{tradeHash}?chainId=untilconfirmedorfailed. If the submit timed out, or answered503 RELAYER_UNAVAILABLEwithdata.tradeHash(relayer_timeout,relayer_unreachable), read this status before quoting and signing again: the trade may have been accepted (404means it was not).
{
"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). |
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 (
fillOrderWithPermit2on the settlement contract), checks the Permit2 nonce, the balance and the Permit2 allowance (400 INSUFFICIENT_BALANCE_OR_ALLOWANCE), and runs the exact transaction witheth_callon 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
failedwithorder_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).
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 |
This page as Markdown: gasless.md. All pages for tools: llms.txt.