Qiguard API docs

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), 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).
{
  "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:

FieldValue
permitted.token, permitted.amountsellToken, sellAmount
spenderThe settlement contract of the chain
nonceA random unordered Permit2 nonce
deadlineThe signed deadline (expiresAt)
witness.tokenOutbuyToken
witness.minAmountOutminBuyAmount (net, after the fee)
witness.recipientThe taker; the signer must be this address
witness.adapterThe liquidity source adapter
witness.deadlineEqual 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

RuleValue
TokensERC-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)
SlippageslippageBps default 50, allowed 10 to 300
DeadlinedeadlineSeconds default 180, allowed 60 to 600
Minimum trade10 USD
Taker walletAn EOA. A contract wallet gives 400 SMART_WALLET_NOT_SUPPORTED; an EOA with an EIP-7702 delegation is accepted
BalanceBelow 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 takerOne pending trade per chain (400 PENDING_TRADES_ALREADY_EXIST), 30 per hour (429 RATE_LIMITED, data.reason taker_hourly_limit)
Submit deadlineAt least 15 seconds left at submit, else 400 META_TRANSACTION_EXPIRY_TOO_SOON

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.

AnswerWhen
400 SELL_AMOUNT_TOO_SMALL with data.minSellAmountThe 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_dayThe 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_dailyThe 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 deniedThe 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

Status

statusMeaning
pendingAccepted, not yet broadcast
submittedBroadcast; transactions[] lists every hash sent (replacements included)
succeededMined with receipt status 1
confirmed5 confirmations on BSC, 3 on Base
failedEnded 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

AnswerMeaning
503 GASLESS_NOT_ENABLEDGasless is not switched on for the chain (data.reason gasless_not_enabled)
503 RELAYER_UNAVAILABLE, data.reason relayer_busyThe relayer queue of the chain is full; Retry-After: 3
503 RELAYER_UNAVAILABLE, data.reason relayer_low_balance, relayer_stalled, gas_price_above_capThe relayer cannot send now; retry later
503 RELAYER_UNAVAILABLE, data.reason relayer_timeout or relayer_unreachable, with data.tradeHashThe 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_movedThe signed minimum is no longer reachable; quote and sign again
400 TRADE_ALREADY_SUBMITTED, data.reason permit_nonce_usedThe Permit2 nonce of the signature is already used