> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dzap.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Gasless

> Swap and bridge without holding native gas tokens

## Overview

Gasless execution lets a user trade when their native balance is zero. Instead of the user sending
and paying for the transaction, they **sign** an authorization off-chain and DZap submits the
transaction on their behalf, recovering the gas cost from the trade output in the source token.

<CardGroup cols={2}>
  <Card title="No native token needed" icon="gas-pump">
    A user holding only USDC on Arbitrum can trade it with 0 ETH in their wallet.
  </Card>

  <Card title="Trade the full balance" icon="wallet">
    Nothing has to be held back to pay for gas, so 100% of a token can be swapped or bridged.
  </Card>

  <Card title="Same routes, same rates" icon="route">
    Gasless uses the normal DZap aggregation. Only the fee line changes.
  </Card>

  <Card title="Works across chains" icon="link">
    Available for both same-chain swaps and cross-chain bridges on supported EVM networks.
  </Card>
</CardGroup>

### How a gasless trade works

<Steps>
  <Step title="Authorize the token">
    The user permits the DZap router to move the source token — by signing an EIP-2612 permit, by a one-time ERC-20 allowance, or by a one-time
    Permit2 approval.
  </Step>

  <Step title="Get a gasless quote">
    Quotes requested with `gasless: true` return routes whose `destAmount` is already net of the relayer's gas. The fee itself is
    itemized at build time, not quote time — see [Fees](#fees).
  </Step>

  <Step title="Sign the intent">
    The user signs an EIP-712 "user intent" that binds their address, the exact trade data, a nonce and a deadline. This signature is what authorizes
    DZap to execute — and only that trade.
  </Step>

  <Step title="DZap executes and settles the fee">
    DZap submits the transaction and pays gas. The gas equivalent is deducted from the output in the source token.
  </Step>
</Steps>

<Note>
  Nothing in this flow gives DZap open-ended access to funds. The intent signature is bound to a single `txId`, carries a deadline, and is consumed by
  a per-user nonce, so it cannot be replayed.
</Note>

## Fees

The gasless fee is charged in the **source token** and is deducted from what the user receives.

Both the quote and the build return it, in different fields and at different accuracy:

| Response           | Field              | Accuracy                                                     |
| ------------------ | ------------------ | ------------------------------------------------------------ |
| `POST /v1/quotes`  | `fee.protocolFee`  | **Estimate.** Good enough to preview, may move before build. |
| `POST /v1/buildTx` | `fees.executorFee` | **Authoritative.** This is what will actually be charged.    |

Show the quote's `protocolFee` while the user is still choosing, and re-read `executorFee` from the
build for the confirmation screen. Both carry the same shape:

```json Fee entry theme={null}
{
  "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
  "chainId": 42161,
  "symbol": "USDC",
  "decimals": 6,
  "amount": "76476",
  "amountUSD": "0.07656",
  "included": true
}
```

`amount` is in the source token's smallest unit; `amountUSD` is the same fee priced in dollars.
`included: true` means it is already reflected in the quoted output — do not subtract it again.

<Warning>
  Do not read the gasless fee from `fee.gasFee`. On a gasless quote that array is always empty —
  the estimate lives in `fee.protocolFee`, and the final number in the build's `fees.executorFee`.
</Warning>

| Trade shape                          | How the fee is charged                                                |
| ------------------------------------ | --------------------------------------------------------------------- |
| One source token                     | The full gas fee is taken from that token.                            |
| Multiple source tokens (many-to-one) | The fee is split across tokens **proportionally to their USD value**. |

For a multi-token trade, **every** `srcToken` in `data[]` must be gasless-listed — one unlisted
source token makes the whole request ineligible. Check each against the list before requesting a
gasless quote. The `$1` minimum is applied **per pair**, not to the basket total.

### Minimum trade size

The fee has to be covered by the trade itself, so the API enforces a **\$1 minimum per pair** for
gasless. Below it, the pair returns an explicit error instead of routes:

```json theme={null}
{ "status": "error", "message": "Minimum $1 per pair required for gasless" }
```

Branch on that `status`/`message` rather than inferring the floor from an empty route list — the
threshold is server-side and can move with gas prices.

### Gasless or regular?

| Situation                                        | Recommendation                                                       |
| ------------------------------------------------ | -------------------------------------------------------------------- |
| User has no native token                         | **Gasless** — it is the only way to trade.                           |
| User wants to swap their entire token balance    | **Gasless** — no gas reserve needed.                                 |
| User already holds native token and trades often | **Regular** — cheaper in aggregate.                                  |
| Small trade (a few dollars)                      | **Regular** — the gas fee dominates, and gasless may be unavailable. |

## Prerequisites

<AccordionGroup>
  <Accordion title="Every source token must support gasless">
    Gasless is enabled per chain and per token, and the constraint applies to the **source** side. Every `srcToken` in `data[]` must be on the
    [gasless token list](#step-1-check-gasless-support) — if one is not, the request is not eligible for gasless. Destination tokens are unconstrained:
    you can trade into any token DZap routes to on the destination chain.

    In practice that means one constrained token selector, not two — filter the "from" list against the gasless list and leave the "to" list alone.

    Check the list before offering gasless. Do not infer support from the quote: `/v1/quotes` and `/v1/buildTx` still return `gasless: true` routes for
    an unlisted source token, so a failed lookup against the list is your only reliable pre-flight signal.
  </Accordion>

  <Accordion title="The source token must be an ERC-20">
    Native tokens (ETH, MATIC, …) cannot be traded gaslessly: moving native value requires the user to send the transaction, which is exactly what
    gasless avoids. The gasless list contains only ERC-20 tokens.
  </Accordion>

  <Accordion title="EVM chains only">
    Gasless runs on DZap's v2 EVM router. Solana, Bitcoin, Sui, Aptos, Ton and HyperLiquid trades are not gasless.
  </Accordion>

  <Accordion title="The trade must clear the $1 minimum">See [Fees](#minimum-trade-size). Below it the pair returns a `status: "error"` with an explicit message.</Accordion>

  <Accordion title="Use an API key during development">
    The gasless and build endpoints are rate-limited per IP without a key, and the limits are low enough to hit while iterating — `/v1/buildTx` allows
    **5 requests per 60s** unauthenticated. A `429` body is an error object, not a token map, so code that assumes the happy shape renders "no gasless
    tokens" and looks like an unsupported chain.

    Send your key as `x-api-key` and branch on `429` explicitly. See [Rate Limits](/api/rate-limits).
  </Accordion>
</AccordionGroup>

## Step 1 — Check gasless support

Two endpoints expose the gasless support matrix.

<CodeGroup>
  ```bash All chains theme={null}
  curl https://api.dzap.io/v1/token/gasless
  ```

  ```bash Single chain theme={null}
  curl https://api.dzap.io/v1/token/gasless/42161
  ```
</CodeGroup>

`GET /v1/token/gasless` returns tokens grouped by chain ID, then keyed by **checksummed** token
address. `GET /v1/token/gasless/{chainId}` returns just the inner map for one chain.

```json Response (trimmed) theme={null}
{
  "42161": {
    "0xaf88d065e77c8cC2239327C5EDb3A432268e5831": {
      "contract": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
      "chainId": 42161,
      "name": "USD Coin",
      "symbol": "USDC",
      "decimals": 6,
      "logo": "https://.../logo.png",
      "balance": "0",
      "price": null,
      "verified": true,
      "permit": {
        "eip2612": { "supported": true },
        "permit2": { "supported": true }
      },
      "gasless": true
    }
  }
}
```

The `permit` field tells you which authorization modes the token supports and therefore which
gasless flow to use. `balance` and `price` are not populated by this endpoint — use
`getAllTokens` / `getTokenDetails` for those.

### With the SDK

```typescript theme={null}
import { DZapClient } from '@dzapio/sdk';
import { getAddress } from 'viem';

const dzap = DZapClient.getInstance(process.env.DZAP_API_KEY);

// Every gasless token on every chain: Record<chainId, Record<address, TokenInfo>>
const allGaslessTokens = await dzap.getAllGaslessTokens();
console.log('Gasless chains:', Object.keys(allGaslessTokens));

// Just one chain: Record<address, TokenInfo>
const arbitrumGaslessTokens = await dzap.getGaslessTokens(42161);

// Keys are checksummed. getAddress() throws on anything that is not an address,
// so a bad input fails loudly instead of silently missing the lookup.
const usdc = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831';
const token = arbitrumGaslessTokens[getAddress(usdc)];

if (!token) {
  console.log('Not gasless — execute this trade with dzap.trade() instead.');
} else {
  console.log('eip2612:', token.permit?.eip2612.supported, 'permit2:', token.permit?.permit2.supported);
}
```

<Warning>
  Keys in the response are **checksummed**, and the lookup is a plain object index — a lowercased or
  uppercased key simply misses, which reads as "token not gasless".

  Do not rely on the SDK's `formatToken` to normalize here. It is a formatter, not a validator: it
  checksums lowercase input correctly, but returns **uppercase** input unchanged and non-address
  input unchanged, and never throws. Use viem's `getAddress()`, or `formatToken(addr.toLowerCase())`
  if you are already importing it.
</Warning>

## Step 2 — Choose an authorization mode

DZap can only move the source token if it is authorized to. There are three ways to do that, and
the choice determines whether the user needs gas *now* or not at all.

| Mode                 | One-time on-chain tx?                  | Signatures per trade      | Use when                                                                                                                          |
| -------------------- | -------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **EIP-2612 permit**  | No                                     | 2 (token permit + intent) | The token supports `permit`. This is the only mode that works for a wallet with **zero** native balance and no prior approval.    |
| **ERC-20 allowance** | Yes — approve the router (needs gas)   | 1 (intent)                | An allowance to the router already exists, or the user can afford one approval now and wants gasless trades afterwards.           |
| **Permit2**          | Yes — approve Permit2 once (needs gas) | 1 (batch intent)          | Trading many tokens or trading repeatedly; one approval covers all future trades, and multi-token trades need a single signature. |

<Note>
  "Needs gas" applies only to the one-time setup transaction. Once the allowance or Permit2 approval exists, every subsequent gasless trade costs the
  user zero native token.
</Note>

<Note>
  **Why `sign()` and `approve()` take different enums.** `sign()` takes a `PermitTypes` value —
  a *signature scheme*. `approve()` and `getAllowance()` take an `ApprovalModes` value — an
  *on-chain approval target* (`Default` → the router, anything else → Permit2).

  `ApprovalModes` deliberately has no `EIP2612Permit` member: an EIP-2612 permit is a signature
  and never approves anything on-chain, so there is nothing for `approve()` to do. The asymmetry
  is intentional, not a typo.
</Note>

## Step 3 — Get a gasless quote

Pass `gasless: true` to the quote request. Quotes are returned keyed by token pair
(`{srcChainId}_{srcToken}-{destChainId}_{destToken}`), which `getTokensPairKey` builds for you.

```typescript theme={null}
import { DZapClient, getTokensPairKey } from '@dzapio/sdk';

const dzap = DZapClient.getInstance();

const account = '0x...';
const fromChain = 42161;
const toChain = 42161; // same chain = swap; different chain = bridge
const srcToken = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; // USDC
const destToken = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'; // WETH
const amount = '5000000'; // 5 USDC (6 decimals) — must clear the gas-fee floor

const quotes = await dzap.getTradeQuotes({
  fromChain,
  account,
  gasless: true,
  data: [{ amount, srcToken, srcDecimals: 6, destToken, destDecimals: 18, toChain, slippage: 1 }],
});

const pairKey = getTokensPairKey({ srcToken, destToken, srcChainId: fromChain, destChainId: toChain });
const pair = quotes[pairKey];

// Below the $1 minimum the pair carries an error instead of routes.
if (pair.status === 'error') {
  throw new Error(pair.message); // e.g. "Minimum $1 per pair required for gasless"
}

// Pick a route: recommendedSource, bestReturnSource, fastestSource, or your own choice
const provider = pair.recommendedSource;
const route = pair.quoteRates![provider];

console.log('gasless route:', route.gasless); // true
console.log('user receives:', route.destAmount); // already net of the gasless fee
console.log('estimated fee:', route.fee.protocolFee[0]?.amountUSD); // estimate — build is authoritative
```

<Warning>
  `route.fee.protocolFee` is an **estimate**, fine for previewing while the user is still choosing.
  Re-read `fees.executorFee` from the build before the confirmation screen — that is the number that
  will actually be charged. `route.fee.gasFee` is always empty on gasless routes. See [Fees](#fees).
</Warning>

<Note>
  An empty `quoteRates` does not mean "not gasless". A source token that is not on the gasless list
  still returns `gasless: true` routes here — the API does not gate quotes on the list. The two
  cases you can actually distinguish are:

  * **`pair.status === 'error'`** with a `message` — below the \$1 minimum.
  * **Token absent from the gasless list** (step 1) — check this yourself before offering gasless.

  In both cases, fall back to `trade()` and let the user pay gas normally.
</Note>

<Note>
  The `$1` minimum applies **per pair**. In a multi-token request one pair can fail this check while
  the others quote normally — check `status` on each pair, not just the first.
</Note>

## Step 4 — Execute

`tradeGasless()` does the whole tail of the flow in one call: it builds the gasless transaction,
collects the intent signature from the wallet, and submits it to DZap for execution. The mode is
selected by what you pass in.

<Tabs>
  <Tab title="EIP-2612 permit">
    No on-chain setup. The user signs the token permit, then the intent — two wallet prompts, zero
    gas.

    ```typescript theme={null}
    import { DZapClient, PermitTypes, Services, TxnStatus } from '@dzapio/sdk';

    const dzap = DZapClient.getInstance();

    // 1. Sign the EIP-2612 permit for each source token (off-chain, no gas).
    const permitResult = await dzap.sign({
      chainId: fromChain,
      sender: account,
      signer,                    // viem WalletClient or ethers Signer
      service: Services.trade,
      permitType: PermitTypes.EIP2612Permit,
      tokens: [{ address: srcToken, amount }],
    });

    if (permitResult.status !== TxnStatus.success || !('tokens' in permitResult)) {
      throw new Error(`Permit signing failed: ${permitResult.code}`);
    }

    // 2. Build + sign the intent + execute. Feed the permit back in as `permitData`.
    const result = await dzap.tradeGasless({
      request: {
        fromChain,
        sender: account,
        refundee: account,       // receives funds if the trade has to be refunded
        gasless: true,
        data: [
          {
            amount,
            srcToken,
            srcDecimals: 6,
            destToken,
            destDecimals: 18,
            toChain,
            slippage: 1,
            protocol: provider,                     // the route chosen in step 3
            recipient: account,
            additionalInfo: route.additionalInfo,
            permitData: permitResult.tokens[0].permitData,
          },
        ],
      },
      signer,
      txnStatusCallback: (status) => console.log('gasless status:', status),
    });

    if (result.status === TxnStatus.success) {
      console.log('submitted by DZap:', result.txnHash);
    }
    ```

    <Note>
      `sign()` returns the tokens array with `permitData` filled in per token. Check
      `permit.eip2612.supported` from step 1 first — signing an EIP-2612 permit for a token that
      does not implement it throws.
    </Note>
  </Tab>

  <Tab title="ERC-20 allowance">
    One approval to the router (costs gas once), then every trade is a single intent signature.

    ```typescript theme={null}
    import { ApprovalModes, DZapClient, Services, TxnStatus } from '@dzapio/sdk';

    const dzap = DZapClient.getInstance();

    // 1. Check the existing allowance to the router.
    const { data: allowances } = await dzap.getAllowance({
      chainId: fromChain,
      sender: account,
      tokens: [{ address: srcToken, amount }],
      service: Services.trade,
      mode: ApprovalModes.Default,
    });

    // 2. Approve only if it is short. This transaction needs native gas.
    if (allowances[srcToken].allowance < BigInt(amount)) {
      await dzap.approve({
        chainId: fromChain,
        signer,
        tokens: [{ address: srcToken, amount }],
        service: Services.trade,
        mode: ApprovalModes.Default,
        approvalTxnCallback: async ({ txnDetails, address }) => {
          console.log(`approved ${address}:`, txnDetails.txnHash);
          return null;
        },
      });
    }

    // 3. Trade. No permitData — the router spends via the allowance.
    const result = await dzap.tradeGasless({
      request: {
        fromChain,
        sender: account,
        refundee: account,
        gasless: true,
        data: [
          {
            amount,
            srcToken,
            srcDecimals: 6,
            destToken,
            destDecimals: 18,
            toChain,
            slippage: 1,
            protocol: provider,
            recipient: account,
            additionalInfo: route.additionalInfo,
          },
        ],
      },
      signer,
    });

    console.log(result.status === TxnStatus.success ? result.txnHash : result.errorMsg);
    ```

    <Note>
      When `permitData` is omitted, the SDK sends the default (empty) permit so the router falls
      back to the on-chain allowance. The user still signs the intent — that signature, not the
      allowance, is what authorizes this specific trade.
    </Note>

    <Warning>
      `getAllowance` returns a map keyed by the address string **exactly as you passed it in**, not
      normalized. Passing a checksummed address and reading back with a lowercase one returns
      `undefined` and throws on `.allowance`. Key your reads off the same variable you passed.
    </Warning>
  </Tab>

  <Tab title="Permit2">
    One approval to the Permit2 contract, then a single batch signature per trade — including
    multi-token trades.

    ```typescript theme={null}
    import { ApprovalModes, DZapClient, Services, TxnStatus } from '@dzapio/sdk';

    const dzap = DZapClient.getInstance();

    const tokens = [{ address: srcToken, amount }];

    // 1. Approve Permit2 once per token. `approve` redirects the ERC-20 approval to the
    //    Permit2 contract automatically for any non-Default mode. Needs native gas.
    const { data: allowances } = await dzap.getAllowance({
      chainId: fromChain,
      sender: account,
      tokens,
      service: Services.trade,
      mode: ApprovalModes.PermitBatchWitnessTransferFrom,
    });

    const needsApproval = tokens.filter((t) => allowances[t.address].allowance < BigInt(t.amount));

    if (needsApproval.length > 0) {
      await dzap.approve({
        chainId: fromChain,
        signer,
        tokens: needsApproval,
        service: Services.trade,
        mode: ApprovalModes.PermitBatchWitnessTransferFrom,
      });
    }

    // 2. Trade. `hasPermit2ApprovalForAllTokens` switches the intent to a Permit2 batch
    //    witness signature — one prompt, covering every source token.
    const result = await dzap.tradeGasless({
      request: {
        fromChain,
        sender: account,
        refundee: account,
        gasless: true,
        hasPermit2ApprovalForAllTokens: true,
        data: [
          {
            amount,
            srcToken,
            srcDecimals: 6,
            destToken,
            destDecimals: 18,
            toChain,
            slippage: 1,
            protocol: provider,
            recipient: account,
            additionalInfo: route.additionalInfo,
          },
        ],
      },
      signer,
    });

    console.log(result.status === TxnStatus.success ? result.txnHash : result.errorMsg);
    ```

    <Warning>
      In Permit2 mode, **every** source token in the request must already have a sufficient Permit2
      approval, and you must set `hasPermit2ApprovalForAllTokens: true`. The two go together:

      * Approve every token first — one missing approval reverts the whole trade on transfer.
      * Then set the flag, which switches the intent to a single batch witness signature.

      Approving only some tokens, or omitting the flag after approving all of them, will not work.
    </Warning>
  </Tab>
</Tabs>

### Driving a progress UI

`txnStatusCallback` receives `TxnStatus` values. Of the eight members, these are the ones a gasless
trade emits, and they map directly onto what you should show:

| `TxnStatus`                 | What is happening                                    | Suggested UI         |
| --------------------------- | ---------------------------------------------------- | -------------------- |
| `pendingWalletConfirmation` | Waiting on the user to sign (permit, then intent)    | "Confirm in wallet…" |
| `waitingForExecution`       | Signed and handed to DZap; the relayer is submitting | "Submitting…"        |
| `mining`                    | Submitted on-chain, awaiting inclusion               | "Processing…"        |
| `success`                   | Executed                                             | Done                 |
| `partialSuccess`            | Multi-token trade where some legs settled            | Per-leg breakdown    |
| `rejected`                  | User dismissed a wallet prompt; nothing submitted    | Back to the form     |
| `error` / `reverted`        | Failed before or during execution                    | Error, with retry    |

In EIP-2612 mode `pendingWalletConfirmation` fires **twice** — once for the token permit and once
for the intent. Do not treat the second one as a stuck state.

### Reusing a build response

`tradeGasless()` builds the transaction internally. If you already built it — to show a
confirmation screen with the fee, for instance — pass it back as `txnData` to skip the rebuild:

```typescript theme={null}
const built = await dzap.buildTradeTxn({ ...request, gasless: true });

console.log('fee:', built.fees.executorFee[0].amountUSD);

const result = await dzap.tradeGasless({ request, signer, txnData: built });
```

A gasless build resolves to `GaslessBaseParamsResponse`:

```typescript theme={null}
type GaslessBaseParamsResponse = {
  status: 'success';
  txId: HexString;
  transaction: BridgeGaslessTxData | SwapGaslessTxData;
  quotes: Record<string, ParamQuotes>;
  gasless: true;
  onlySwapData: false;
  fees: {
    executorFee: FeeDetails[];
  };
};
```

`status`, `gasless` and `onlySwapData` are literal types, so a gasless build narrows cleanly — no
cast needed. `fees.executorFee` is the authoritative fee described in [Fees](#fees).

**What invalidates a build.** The intent signature the build is bound to expires after
`SignatureExpiryInSecs` — **1800 seconds (30 minutes)**, exported from the SDK. Rebuild if:

* the deadline has passed, or
* the user's router nonce was consumed by another trade in the meantime, or
* you are switching authorization mode — adding `permitData` after building changes the request, so
  the earlier build no longer matches. Rebuild on that path.

Reusing a build inside the window, with the same request and an unconsumed nonce, is safe.

## Step 5 — Track the transaction

`tradeGasless()` resolves with the hash of the transaction DZap submitted. Track it exactly like a
regular trade:

```typescript theme={null}
import { STATUS_RESPONSE } from '@dzapio/sdk';

const status = await dzap.getTradeTxnStatus({ txHash: result.txnHash!, chainId: fromChain });

console.log(status.gasless); // true

if (status.status === STATUS_RESPONSE.COMPLETED) {
  // settled
}
```

`STATUS_RESPONSE` is exported — compare against its members rather than hand-written strings.
`status.status` is one of exactly five values:

| `STATUS_RESPONSE` member | Value       | Terminal? | Meaning                                                         |
| ------------------------ | ----------- | --------- | --------------------------------------------------------------- |
| `PENDING`                | `PENDING`   | No        | Still executing. Keep polling.                                  |
| `COMPLETED`              | `COMPLETED` | Yes       | Fully settled.                                                  |
| `FAILED`                 | `FAILED`    | Yes       | Execution failed. Funds stayed with the user.                   |
| `PARTIAL`                | `PARTIAL`   | Yes       | Multi-token or cross-chain trade where only some legs settled.  |
| `REFUNDED`               | `REFUNDED`  | Yes       | Destination leg could not settle; funds returned to `refundee`. |

<Warning>
  There is no `SUCCESS` member. The completed state is `COMPLETED` — `if (status.status === 'SUCCESS')`
  is a branch that never fires.

  `PARTIAL` and `REFUNDED` are the states cross-chain bridges actually land in when the destination
  leg fails, so give them their own UI rather than folding them into a generic failure.
</Warning>

<Note>
  Do not confuse `STATUS_RESPONSE` with the separately exported `STATUS` constant — that one is an
  unrelated enum with lowercase members (`pending`, `success`, …) and will never match a trade status.
</Note>

For cross-chain gasless bridges the status covers both legs. Poll until the status leaves `PENDING`.

## Complete example

Every block above depends on identifiers from earlier steps. This one is self-contained — copy it
whole. It covers the EIP-2612 path, the one that works for a wallet with zero native balance.

<Tip>
  Prefer a running app? [**DZapIO/examples**](https://github.com/DZapIO/examples) is a Vite + wagmi
  app with a working gasless trade wired to a real wallet:

  * [`src/components/trade-gasless-viem/`](https://github.com/DZapIO/examples/tree/main/vite/src/components/trade-gasless-viem) — the gasless trade component, using a viem `WalletClient` as the signer.
  * [`src/lib/trade.ts`](https://github.com/DZapIO/examples/blob/main/vite/src/lib/trade.ts) — `executeGaslessTradeWithGasFallback()`, which falls back to a regular trade when gasless is unavailable.
  * [`vite.config.ts`](https://github.com/DZapIO/examples/blob/main/vite/vite.config.ts) — the `vite-plugin-node-polyfills` setup the SDK needs in a browser bundle.
</Tip>

```typescript theme={null}
import { DZapClient, PermitTypes, STATUS_RESPONSE, Services, TxnStatus, getTokensPairKey } from '@dzapio/sdk';
import { getAddress, type WalletClient } from 'viem';

export async function gaslessSwap(signer: WalletClient, account: `0x${string}`) {
  const dzap = DZapClient.getInstance(process.env.DZAP_API_KEY);

  const fromChain = 42161;
  const toChain = 42161;
  const srcToken = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; // USDC
  const destToken = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'; // WETH
  const amount = '5000000'; // 5 USDC — must clear the $1 minimum

  // 1. Confirm the SOURCE token is gasless-listed. Quotes do not enforce this.
  const gaslessTokens = await dzap.getGaslessTokens(fromChain);
  const listed = gaslessTokens[getAddress(srcToken)];
  if (!listed?.permit?.eip2612?.supported) {
    throw new Error('Not gasless via EIP-2612 — use allowance/Permit2 mode, or trade() normally.');
  }

  // 2. Quote.
  const quotes = await dzap.getTradeQuotes({
    fromChain,
    account,
    gasless: true,
    data: [{ amount, srcToken, srcDecimals: 6, destToken, destDecimals: 18, toChain, slippage: 1 }],
  });

  const pair = quotes[getTokensPairKey({ srcToken, destToken, srcChainId: fromChain, destChainId: toChain })];
  if (pair.status === 'error') throw new Error(pair.message); // e.g. below the $1 minimum

  const protocol = pair.recommendedSource;
  const route = pair.quoteRates![protocol];

  const request = {
    fromChain,
    sender: account,
    refundee: account,
    gasless: true,
    data: [
      {
        amount,
        srcToken,
        srcDecimals: 6,
        destToken,
        destDecimals: 18,
        toChain,
        slippage: 1,
        protocol,
        recipient: account,
        additionalInfo: route.additionalInfo,
      },
    ],
  };

  // 3. The quote's protocolFee is an estimate; build for the authoritative fee.
  console.log('estimated fee:', route.fee.protocolFee[0]?.amountUSD);

  const built = await dzap.buildTradeTxn(request);
  const fee = built.fees.executorFee[0];
  console.log(`gasless fee: ${fee.amount} ${fee.symbol} (~$${fee.amountUSD})`);

  // 4. Sign the EIP-2612 permit (off-chain, no gas).
  const permitResult = await dzap.sign({
    chainId: fromChain,
    sender: account,
    signer,
    service: Services.trade,
    permitType: PermitTypes.EIP2612Permit,
    tokens: [{ address: srcToken, amount }],
  });
  if (permitResult.status !== TxnStatus.success || !('tokens' in permitResult)) {
    throw new Error(`Permit signing failed: ${permitResult.code}`);
  }

  // 5. Execute. permitData changes the request, so this rebuilds — do not pass `built` as txnData.
  const result = await dzap.tradeGasless({
    request: {
      ...request,
      data: [{ ...request.data[0], permitData: permitResult.tokens[0].permitData }],
    },
    signer,
    txnStatusCallback: (status) => console.log('status:', status),
  });

  if (result.status !== TxnStatus.success) throw new Error(result.errorMsg);

  // 6. Poll to a terminal state.
  let status = await dzap.getTradeTxnStatus({ txHash: result.txnHash!, chainId: fromChain });
  while (status.status === STATUS_RESPONSE.PENDING) {
    await new Promise((r) => setTimeout(r, 3000));
    status = await dzap.getTradeTxnStatus({ txHash: result.txnHash!, chainId: fromChain });
  }

  return status; // COMPLETED | FAILED | PARTIAL | REFUNDED
}
```

## Advanced — the REST flow

Use this if you are not using the SDK. It is the same sequence `tradeGasless()` performs.

<Steps>
  <Step title="POST /v1/quotes">
    Body as in step 3, with `"gasless": true`. Pick a route.
  </Step>

  <Step title="POST /v1/buildTx">
    Body: the trade request with `"gasless": true`, the chosen `protocol`, `sender`, `refundee`,
    `recipient`. The gasless response is *not* raw calldata — it is the set of hashes you must sign:

    ```json theme={null}
    {
      "status": "success",
      "txId": "0x...",
      "gasless": true,
      "onlySwapData": false,
      "transaction": {
        "txType": "swap",              // "swap" | "bridge"
        "executorFeesHash": "0x...",
        "swapDataHash": "0x...",       // omitted for bridge-only
        "adapterDataHash": "0x...",    // bridge only
        "value": "0"
      },
      "quotes": { "...": {} },
      "fees": {
        "executorFee": [
          { "symbol": "USDC", "amount": "76476", "amountUSD": "0.07656", "included": true }
        ]
      }
    }
    ```

    `fees.executorFee` is the authoritative gasless fee — show this on the confirmation screen
    rather than the quote's estimate.
  </Step>

  <Step title="Sign the user intent (EIP-712)">
    Read the user's current nonce from the router. The router address for a chain comes from
    `GET /v1/chains` → `contracts.router`:

    ```typescript theme={null}
    import { createPublicClient, http } from 'viem';
    import { arbitrum } from 'viem/chains';

    const nonceAbi = [
      {
        name: 'getNonce',
        type: 'function',
        stateMutability: 'view',
        inputs: [{ name: 'user', type: 'address' }],
        outputs: [{ name: '', type: 'uint256' }],
      },
    ] as const;

    const chains = await fetch('https://api.dzap.io/v1/chains').then((r) => r.json());
    const router = chains.find((c) => c.chainId === 42161).contracts.router;

    const nonce = await createPublicClient({ chain: arbitrum, transport: http() }).readContract({
      address: router,
      abi: nonceAbi,
      functionName: 'getNonce',
      args: [account],
    });
    ```

    **Domain**

    ```json theme={null}
    {
      "name": "DZapVerifier",
      "version": "1",
      "chainId": 42161,
      "verifyingContract": "<router address>",
      "salt": "keccak256(abi.encodePacked('DZap-v0.1'))"
    }
    ```

    **Primary type** — chosen by what the build response contains:

    | Trade         | Primary type                  | Fields                                                                                     |
    | ------------- | ----------------------------- | ------------------------------------------------------------------------------------------ |
    | Swap          | `SignedGasLessSwapData`       | `txId`, `user`, `nonce`, `deadline`, `executorFeesHash`, `swapDataHash`                    |
    | Bridge        | `SignedGasLessBridgeData`     | `txId`, `user`, `nonce`, `deadline`, `executorFeesHash`, `adapterDataHash`                 |
    | Swap + bridge | `SignedGasLessSwapBridgeData` | `txId`, `user`, `nonce`, `deadline`, `executorFeesHash`, `swapDataHash`, `adapterDataHash` |

    `txId` is `bytes32`, `user` is `address`, everything else is `uint256`/`bytes32` as named.
    `deadline` is a unix timestamp. The SDK uses `SignatureExpiryInSecs` — **1800 seconds (30
    minutes)** from signing. Match it unless you have a reason not to; a shorter deadline is safer,
    a longer one widens the replay window.

    For the Permit2 mode you sign a `PermitBatchWitnessTransferFrom` instead, with the same fields
    carried as the witness, against the Permit2 domain.
  </Step>

  <Step title="POST /v1/gasless/executeTx">
    EIP-2612 or allowance mode:

    ```json theme={null}
    {
      "chainId": 42161,
      "txId": "0x...",
      "permit": {
        "permitData": [{ "token": "0x...", "amount": "5000000", "permit": "0x..." }],
        "gaslessIntentSignature": "0x...",
        "gaslessIntentNonce": "3",
        "gaslessIntentDeadline": "1753600000"
      }
    }
    ```

    Permit2 mode:

    ```json theme={null}
    {
      "chainId": 42161,
      "txId": "0x...",
      "permit": { "batchPermitData": "0x..." }
    }
    ```

    The response carries `status` and the `txnHash` DZap submitted. In allowance mode the per-token
    `permit` is the default empty permit — ABI-encoded `(uint8 mode, bytes data)` with
    `mode = 0` (EIP-2612) and `data = 0x`.
  </Step>
</Steps>

## Error handling

```typescript theme={null}
import { TxnStatus } from '@dzapio/sdk';

const result = await dzap.tradeGasless({ request, signer });

switch (result.status) {
  case TxnStatus.success:
    break;
  case TxnStatus.rejected:
    // User dismissed the permit or intent prompt in their wallet. Nothing was submitted.
    break;
  case TxnStatus.error:
    console.error(result.errorMsg, result.code, result.action);
    break;
}
```

| Symptom                                          | Cause                                                                                          | Fix                                                             |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `status: "error"`, "Minimum \$1 per pair"        | Trade value below the server-enforced \$1 gasless minimum                                      | Increase the amount, or fall back to `trade()`                  |
| Gasless routes returned for a non-listed token   | Quotes are not gated on the gasless list                                                       | Check every `srcToken` against the list before offering gasless |
| Fee on the confirm screen differs from the quote | The quote's `protocolFee` is an estimate; `executorFee` from the build is authoritative        | Re-read the fee from the build before the user signs            |
| "No gasless tokens" on a supported chain         | The token-list request was rate limited (`429`) and the error body parsed as an empty map      | Send an `x-api-key`; branch on `429` before reading the body    |
| `Token does not support EIP-2612 permits`        | `permitType: EIP2612Permit` on a token without `permit`                                        | Check `permit.eip2612.supported`, use allowance or Permit2 mode |
| Simulation failed (`action: INCREASE_SLIPPAGE`)  | Route moved between quote and execute                                                          | Re-quote, or raise slippage                                     |
| Reverts on token transfer                        | Missing allowance, or `hasPermit2ApprovalForAllTokens: true` without approvals for every token | Re-check allowances before executing                            |
| Intent signature rejected on-chain               | Deadline expired or the nonce was consumed by another trade                                    | Rebuild and re-sign; do not reuse an intent                     |

## Reference

### Endpoints

| Method | Path                          | Purpose                                                     |
| ------ | ----------------------------- | ----------------------------------------------------------- |
| `GET`  | `/v1/token/gasless`           | Gasless tokens for every chain, grouped by chain ID         |
| `GET`  | `/v1/token/gasless/{chainId}` | Gasless tokens for one chain                                |
| `POST` | `/v1/quotes`                  | Quotes; send `gasless: true`                                |
| `POST` | `/v1/buildTx`                 | Gasless build; send `gasless: true`, returns hashes to sign |
| `POST` | `/v1/gasless/executeTx`       | Submit the signed intent for execution                      |
| `GET`  | `/v1/status`                  | Status of the executed transaction                          |
| `GET`  | `/v1/chains`                  | Chain config, including `contracts.router`                  |

Base URLs: `https://api.dzap.io` (production), `https://staging.dzap.io` (staging). Send your API
key as the `x-api-key` header — see [Rate Limits](/api/rate-limits) for the unauthenticated caps.

### SDK methods

| Method                                                            | Returns                                                                  |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `getAllGaslessTokens()`                                           | `Record<chainId, Record<address, TokenInfo>>`                            |
| `getGaslessTokens(chainId)`                                       | `Record<address, TokenInfo>`                                             |
| `getTradeQuotes({ gasless: true, ... })`                          | Quotes with gasless routes; fee estimate in `fee.protocolFee`            |
| `buildTradeTxn({ gasless: true, ... })`                           | `GaslessBaseParamsResponse` — `fees.executorFee` plus the hashes to sign |
| `sign({ permitType: PermitTypes.EIP2612Permit, ... })`            | Tokens with `permitData` populated                                       |
| `approve({ mode, ... })`                                          | Approval transaction results (`Default` → router, otherwise → Permit2)   |
| `getAllowance({ mode, ... })`                                     | Allowances keyed by the address string **as passed in**                  |
| `tradeGasless({ request, signer, txnData?, txnStatusCallback? })` | `{ status, code, txnHash }`                                              |
| `getTradeTxnStatus({ txHash, chainId })`                          | Execution status, with `gasless: true`                                   |
| `getDZapContractAddress({ chainId, service: 'trade' })`           | Router address used as the intent's verifying contract                   |

### Types

| Type                            | Notes                                                                    |
| ------------------------------- | ------------------------------------------------------------------------ |
| `GaslessBaseParamsResponse`     | What a gasless `buildTradeTxn` resolves to, including `fees.executorFee` |
| `FeeDetails`                    | One fee entry — `amount`, `amountUSD`, `symbol`, `decimals`, `included`  |
| `STATUS_RESPONSE`               | Trade status constants — see [step 5](#step-5-track-the-transaction)     |
| `TxnStatus`                     | Callback status enum — see [progress UI](#driving-a-progress-ui)         |
| `PermitTypes` / `ApprovalModes` | Signature schemes and on-chain approval targets                          |
| `TradeQuotesResponse`           | Top-level quote response, keyed by token pair                            |
