# Swap Integration

> Integrate Uniswap swaps into applications. Use when user says "integrate swaps", "uniswap", "trading api", "add swap functionality", "build a swap frontend", "create a swap script", "smart contract swap integration", "use Universal Router", "Trading API", or mentions swapping tokens via Uniswap.

- Skill: `solizardking/swap-integration` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add solizardking/swap-integration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/solizardking/swap-integration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: Solizardking (https://skillmd.com/u/solizardking)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/solizardking/swap-integration

---


# Swap Integration

Integrate Uniswap swaps into frontends, backends, and smart contracts.

## Prerequisites

This skill assumes familiarity with viem basics (client setup, account management, contract interactions, transaction signing). Install the **uniswap-viem** plugin for comprehensive viem/wagmi guidance: `claude plugin add @uniswap/uniswap-viem`

## Quick Decision Guide

| Building...                    | Use This Method               |
| ------------------------------ | ----------------------------- |
| Frontend with React/Next.js    | Trading API                   |
| Backend script or bot          | Trading API                   |
| Smart contract integration     | Universal Router direct calls |
| Need full control over routing | Universal Router SDK          |

### Routing Types Quick Reference

| Type     | Description                             | Chains                             |
| -------- | --------------------------------------- | ---------------------------------- |
| CLASSIC  | Standard AMM swap through Uniswap pools | All supported chains               |
| DUTCH_V2 | UniswapX Dutch auction V2               | Ethereum, Arbitrum, Base, Unichain |
| PRIORITY | MEV-protected priority order            | Base, Unichain                     |
| WRAP     | ETH to WETH conversion                  | All                                |
| UNWRAP   | WETH to ETH conversion                  | All                                |

See [Routing Types](#routing-types) for the complete list including DUTCH_V3, DUTCH_LIMIT, LIMIT_ORDER, BRIDGE, and QUICKROUTE.

## Integration Methods

### 1. Trading API (Recommended)

Best for: Frontends, backends, scripts. Handles routing optimization automatically.

**Base URL**: `https://trade-api.gateway.uniswap.org/v1`

**Authentication**: `x-api-key: <your-api-key>` header required

**Getting an API Key**: The Trading API requires an API key for authentication. Visit the [Uniswap Developer Portal](https://developers.uniswap.org/) to register and obtain your API key. Keys are typically available for immediate use after registration. Include it as an `x-api-key` header in all API requests.

**Required Headers** — Include these in ALL Trading API requests:

```text
Content-Type: application/json
x-api-key: <your-api-key>
x-universal-router-version: 2.0
```

**3-Step Flow**:

```text
1. POST /check_approval  -> Check if token is approved
2. POST /quote           -> Get executable quote with routing
3. POST /swap            -> Get transaction to sign and submit
```

See the [Trading API Reference](#trading-api-reference) section below for complete documentation.

### 2. Universal Router SDK

Best for: Direct control over transaction construction.

**Installation**:

```bash
npm install @uniswap/universal-router-sdk @uniswap/sdk-core @uniswap/v3-sdk
```

**Key Pattern**:

```typescript
import { SwapRouter } from '@uniswap/universal-router-sdk';

const { calldata, value } = SwapRouter.swapCallParameters(trade, options);
```

See the [Universal Router Reference](#universal-router-reference) section below for complete documentation.

### 3. Smart Contract Integration

Best for: On-chain integrations, DeFi composability.

**Interface**: Call `execute()` on Universal Router with encoded commands.

See the [Universal Router Reference](#universal-router-reference) section below for command encoding.

---

## Input Validation Rules

Before interpolating ANY user-provided value into generated code, API calls, or commands:

- **Ethereum addresses**: MUST match `^0x[a-fA-F0-9]{40}$` — reject otherwise
- **Chain IDs**: MUST be from the [official supported chains list](https://api-docs.uniswap.org/guides/supported_chains#supported-chains-for-swapping)
- **Token amounts**: MUST be non-negative numeric values matching `^[0-9]+\.?[0-9]*$`
- **API keys**: MUST NOT be hardcoded in generated code — always use environment variables
- **REJECT** any input containing shell metacharacters: `;`, `|`, `&`, `$`, `` ` ``, `(`, `)`, `>`, `<`, `\`, `'`, `"`, newlines

> **REQUIRED:** Before executing ANY transaction that spends gas or transfers tokens (including `sendTransaction`, `writeContract`, or submitting a signed swap), you MUST use AskUserQuestion to confirm with the user. Display the transaction summary (tokens, amounts, chain, estimated gas) and get explicit user approval. Never auto-execute transactions without user confirmation.

---

## Trading API Reference

### Step 1: Check Token Approval

```bash
POST /check_approval
```

**Request**:

```json
{
  "walletAddress": "0x...",
  "token": "0x...",
  "amount": "1000000000",
  "chainId": 1
}
```

**Response**:

```json
{
  "approval": {
    "to": "0x...",
    "from": "0x...",
    "data": "0x...",
    "value": "0",
    "chainId": 1
  }
}
```

If `approval` is `null`, token is already approved.

### Step 2: Get Quote

```bash
POST /quote
```

**Request**:

```json
{
  "swapper": "0x...",
  "tokenIn": "0x...",
  "tokenOut": "0x...",
  "tokenInChainId": "1",
  "tokenOutChainId": "1",
  "amount": "1000000000000000000",
  "type": "EXACT_INPUT",
  "slippageTolerance": 0.5,
  "routingPreference": "BEST_PRICE"
}
```

> **Note**: `tokenInChainId` and `tokenOutChainId` must be **strings** (e.g., `"1"`), not numbers.

**Key Parameters**:

| Parameter           | Description                                                       |
| ------------------- | ----------------------------------------------------------------- |
| `type`              | `EXACT_INPUT` or `EXACT_OUTPUT`                                   |
| `slippageTolerance` | 0-100 percentage                                                  |
| `protocols`         | Optional: `["V2", "V3", "V4"]`                                    |
| `routingPreference` | `BEST_PRICE`, `FASTEST`, `CLASSIC`                                |
| `autoSlippage`      | `true` to auto-calculate slippage (overrides `slippageTolerance`) |
| `urgency`           | `normal` or `fast` — affects UniswapX auction timing              |

**Response** — the shape differs by routing type. `BEST_PRICE` routing on Ethereum mainnet typically returns UniswapX (DUTCH_V2), not CLASSIC.

**CLASSIC response**:

```json
{
  "routing": "CLASSIC",
  "quote": {
    "input": { "token": "0x...", "amount": "1000000000000000000" },
    "output": { "token": "0x...", "amount": "999000000" },
    "slippage": 0.5,
    "route": [],
    "gasFee": "5000000000000000",
    "gasFeeUSD": "0.01",
    "gasUseEstimate": "150000"
  },
  "permitData": null
}
```

**UniswapX (DUTCH_V2/V3/PRIORITY) response** — different `quote` shape, no `quote.output`:

```json
{
  "routing": "DUTCH_V2",
  "quote": {
    "orderInfo": {
      "reactor": "0x...",
      "swapper": "0x...",
      "nonce": "...",
      "deadline": 1772031054,
      "cosigner": "0x...",
      "input": {
        "token": "0x...",
        "startAmount": "1000000000000000000",
        "endAmount": "1000000000000000000"
      },
      "outputs": [
        {
          "token": "0x...",
          "startAmount": "999000000",
          "endAmount": "994000000",
          "recipient": "0x..."
        }
      ],
      "chainId": 1
    },
    "encodedOrder": "0x...",
    "orderHash": "0x..."
  },
  "permitData": { "domain": {}, "types": {}, "values": {} }
}
```

> **UniswapX output amount**: Use `quote.orderInfo.outputs[0].startAmount` for the best-case fill amount. The `endAmount` is the floor after full auction decay. There is no `quote.output.amount` on UniswapX responses — accessing it will throw at runtime.
>
> **Display tip**: For CLASSIC routes, use `gasFeeUSD` (a string with the USD value) for gas cost display. Do **not** manually convert `gasFee` (wei) using a hardcoded ETH price — this leads to wildly inaccurate estimates (e.g., ~$87 instead of ~$0.01). UniswapX routes are gasless for the swapper.

See [QuoteResponse TypeScript Types](#7-quoteresponse-typescript-types) for compile-time type safety across routing types.

### Step 3: Execute Swap

```bash
POST /swap
```

**Request** - Spread the quote response directly into the body:

```typescript
// CORRECT: Spread the quote response, strip null fields
const quoteResponse = await fetchQuote(params);

// Always strip permitData/permitTransaction — handle them explicitly by routing type
const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;
const swapRequest: Record<string, unknown> = { ...cleanQuote };

const isUniswapX =
  quoteResponse.routing === 'DUTCH_V2' ||
  quoteResponse.routing === 'DUTCH_V3' ||
  quoteResponse.routing === 'PRIORITY';

if (isUniswapX) {
  // UniswapX: signature only — permitData must NOT go to /swap
  if (permit2Signature) swapRequest.signature = permit2Signature;
} else {
  // CLASSIC: both signature and permitData, or neither
  if (permit2Signature && permitData && typeof permitData === 'object') {
    swapRequest.signature = permit2Signature;
    swapRequest.permitData = permitData;
  }
}
```

**Critical**: Do NOT wrap the quote in `{quote: quoteResponse}`. The API expects the quote response fields spread into the request body.

**Permit2 Rules** (CLASSIC routes):

- `signature` and `permitData` must BOTH be present, or BOTH be absent
- Never set `permitData: null` — omit the field entirely
- The quote response often includes `permitData: null` — strip this before sending

**UniswapX Routes** (DUTCH_V2/V3/PRIORITY): `permitData` is used locally to sign the order but must be **excluded** from the `/swap` body. See [Signing vs. Submission Flow](#uniswapx-signing-vs-submission-flow).

**Response** (ready-to-sign transaction):

```json
{
  "swap": {
    "to": "0x...",
    "from": "0x...",
    "data": "0x...",
    "value": "0",
    "chainId": 1,
    "gasLimit": "250000"
  }
}
```

**Response Validation** - Always validate before broadcasting:

```typescript
function validateSwapResponse(response: SwapResponse): void {
  if (!response.swap?.data || response.swap.data === '' || response.swap.data === '0x') {
    throw new Error('swap.data is empty - quote may have expired');
  }
  if (!isAddress(response.swap.to) || !isAddress(response.swap.from)) {
    throw new Error('Invalid address in swap response');
  }
}
```

### Supported Chains

See the [official supported chains list](https://api-docs.uniswap.org/guides/supported_chains#supported-chains-for-swapping) for the current set of chains and their IDs.

### Routing Types

| Type        | Description                                   |
| ----------- | --------------------------------------------- |
| CLASSIC     | Standard AMM swap through Uniswap pools       |
| DUTCH_V2    | UniswapX Dutch auction V2                     |
| DUTCH_V3    | UniswapX Dutch auction V3                     |
| PRIORITY    | MEV-protected priority order (Base, Unichain) |
| DUTCH_LIMIT | UniswapX Dutch limit order                    |
| LIMIT_ORDER | Limit order                                   |
| WRAP        | ETH to WETH conversion                        |
| UNWRAP      | WETH to ETH conversion                        |
| BRIDGE      | Cross-chain bridge                            |
| QUICKROUTE  | Fast approximation quote                      |

**UniswapX availability**: UniswapX V2 orders are supported on Ethereum (1), Arbitrum (42161), Base (8453), and Unichain (130). The auction mechanism varies by chain — see [UniswapX Auction Types](#uniswapx-auction-types) below.

---

## Critical Implementation Notes

These are common pitfalls discovered during real-world Trading API integration. **Follow these rules to avoid on-chain reverts and API errors.**

### 1. Swap Request Body Format

The `/swap` endpoint expects the quote response **spread into the request body**, not wrapped in a `quote` field.

```typescript
// WRONG - causes "quote does not match any of the allowed types"
const badRequest = {
  quote: quoteResponse, // Don't wrap!
  signature: '0x...',
};

// CORRECT - spread the quote response
const goodRequest = {
  ...quoteResponse,
  signature: '0x...', // Only if using Permit2
};
```

### 2. Null Field Handling

The API rejects `permitData: null`. Additionally, `permitData` handling differs by routing type — see [Signing vs. Submission Flow](#uniswapx-signing-vs-submission-flow) for the full explanation.

```typescript
function prepareSwapRequest(quoteResponse: QuoteResponse, signature?: string): object {
  // Always strip permitData and permitTransaction from the spread — handle them explicitly
  const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;
  const request: Record<string, unknown> = { ...cleanQuote };

  // UniswapX (DUTCH_V2, DUTCH_V3, PRIORITY): permitData is for LOCAL signing only.
  // The /swap body must NOT include permitData — the order is encoded in
  // quote.encodedOrder. Only the signature is needed.
  const isUniswapX =
    quoteResponse.routing === 'DUTCH_V2' ||
    quoteResponse.routing === 'DUTCH_V3' ||
    quoteResponse.routing === 'PRIORITY';

  if (isUniswapX) {
    if (signature) request.signature = signature;
  } else {
    // CLASSIC: both signature and permitData required together, or both omitted.
    // The Universal Router contract needs permitData to verify the Permit2
    // authorization on-chain.
    if (signature && permitData && typeof permitData === 'object') {
      request.signature = signature;
      request.permitData = permitData;
    }
  }

  return request;
}
```

### 3. Permit2 Field Rules

The rules for `signature` and `permitData` in the `/swap` request body depend on the routing type:

**CLASSIC routes**:

| Scenario                   | `signature` | `permitData` |
| -------------------------- | ----------- | ------------ |
| Standard swap (no Permit2) | Omit        | Omit         |
| Permit2 swap               | Required    | Required     |
| **Invalid**                | Present     | Missing      |
| **Invalid**                | Missing     | Present      |
| **Invalid (API error)**    | Any         | `null`       |

**UniswapX routes (DUTCH_V2/V3/PRIORITY)**:

| Scenario       | `signature` | `permitData`             |
| -------------- | ----------- | ------------------------ |
| UniswapX order | Required    | **Omit** (do not send)   |
| **Invalid**    | Any         | Present (schema rejects) |

### 4. Pre-Broadcast Validation

Always validate the swap response before sending to the blockchain:

```typescript
import { isAddress, isHex } from 'viem';

function validateSwapBeforeBroadcast(swap: SwapTransaction): void {
  // 1. data must be non-empty hex
  if (!swap.data || swap.data === '' || swap.data === '0x') {
    throw new Error('swap.data is empty - this will revert on-chain. Re-fetch the quote.');
  }

  if (!isHex(swap.data)) {
    throw new Error('swap.data is not valid hex');
  }

  // 2. Addresses must be valid
  if (!isAddress(swap.to)) {
    throw new Error('swap.to is not a valid address');
  }

  if (!isAddress(swap.from)) {
    throw new Error('swap.from is not a valid address');
  }

  // 3. Value must be present (can be "0" for non-ETH swaps)
  if (swap.value === undefined || swap.value === null) {
    throw new Error('swap.value is missing');
  }
}
```

### 5. Browser Environment Setup

When using viem/wagmi in browser environments, you need Node.js polyfills:

**Install buffer polyfill**:

```bash
npm install buffer
```

**Add to your entry file (before other imports)**:

```typescript
// src/main.tsx or src/index.tsx
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;

// Then your other imports
import React from 'react';
import { WagmiProvider } from 'wagmi';
// ...
```

**Vite configuration** (`vite.config.ts`):

```typescript
export default defineConfig({
  define: {
    global: 'globalThis',
  },
  optimizeDeps: {
    include: ['buffer'],
  },
  resolve: {
    alias: {
      buffer: 'buffer',
    },
  },
});
```

Without this setup, you'll see: `ReferenceError: Buffer is not defined`

#### CORS Proxy Configuration

The Trading API does not support browser CORS preflight requests — `OPTIONS` requests return `415 Unsupported Media Type`. Direct `fetch()` calls from a browser will always fail. You **must** proxy API requests through your own server or dev server.

**Vite dev proxy** (merge into the same `vite.config.ts` used for the Buffer polyfill above):

```typescript
export default defineConfig({
  server: {
    proxy: {
      '/api/uniswap': {
        target: 'https://trade-api.gateway.uniswap.org/v1',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api\/uniswap/, ''),
      },
    },
  },
});
```

Then use `/api/uniswap/quote` instead of the full URL in your frontend code.

**Vercel production proxy** (`vercel.json`):

```json
{
  "rewrites": [
    {
      "source": "/api/uniswap/:path*",
      "destination": "https://trade-api.gateway.uniswap.org/v1/:path*"
    }
  ]
}
```

**Cloudflare Pages** (`public/_redirects`):

```text
/api/uniswap/* https://trade-api.gateway.uniswap.org/v1/:splat 200
```

**Next.js** (`next.config.js`):

```javascript
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/uniswap/:path*',
        destination: 'https://trade-api.gateway.uniswap.org/v1/:path*',
      },
    ];
  },
};
```

Without a proxy, you'll see: `415 Unsupported Media Type` on preflight or CORS errors in the browser console.

### 6. Quote Freshness

- Quotes expire quickly (typically 30 seconds)
- Always re-fetch if the user takes time to review
- Use the `deadline` parameter to prevent stale execution
- If `/swap` returns empty `data`, the quote likely expired

### 7. QuoteResponse TypeScript Types

The quote response shape differs by routing type. Use a discriminated union on the `routing` field to get compile-time safety instead of casting to `any`:

```typescript
type ClassicQuoteResponse = {
  routing: 'CLASSIC' | 'WRAP' | 'UNWRAP';
  quote: {
    input: { token: string; amount: string };
    output: { token: string; amount: string };
    slippage: number;
    route: unknown[];
    gasFee: string;
    gasFeeUSD: string;
    gasUseEstimate: string;
  };
  permitData: Record<string, unknown> | null;
};

type DutchOrderOutput = {
  token: string;
  startAmount: string;
  endAmount: string;
  recipient: string;
};

type UniswapXQuoteResponse = {
  routing: 'DUTCH_V2' | 'DUTCH_V3' | 'PRIORITY';
  quote: {
    orderInfo: {
      outputs: DutchOrderOutput[];
      input: { token: string; startAmount: string; endAmount: string };
      deadline: number;
      nonce: string;
    };
    encodedOrder: string;
    orderHash: string;
  };
  // EIP-712 typed data — sign locally, do NOT send to /swap
  permitData: Record<string, unknown> | null;
};

type QuoteResponse = ClassicQuoteResponse | UniswapXQuoteResponse;

// Type guard for routing-aware logic
function isUniswapXQuote(q: QuoteResponse): q is UniswapXQuoteResponse {
  return q.routing === 'DUTCH_V2' || q.routing === 'DUTCH_V3' || q.routing === 'PRIORITY';
}

// Reading the output amount by routing type
function getOutputAmount(q: QuoteResponse): string {
  if (isUniswapXQuote(q)) {
    const firstOutput = q.quote.orderInfo.outputs[0];
    if (!firstOutput) throw new Error('UniswapX quote has no outputs');
    // startAmount = best-case fill; endAmount = floor after auction decay
    return firstOutput.startAmount;
  }
  return q.quote.output.amount;
}
```

---

## Universal Router Reference

The Universal Router is a unified interface for swapping across Uniswap v2, v3, and v4.

### Core Function

```solidity
function execute(
    bytes calldata commands,
    bytes[] calldata inputs,
    uint256 deadline
) external payable;
```

### Command Encoding

Each command is a single byte:

| Bits | Name     | Purpose                             |
| ---- | -------- | ----------------------------------- |
| 0    | flag     | Allow revert (1 = continue on fail) |
| 1-2  | reserved | Use 0                               |
| 3-7  | command  | Operation identifier                |

### Swap Commands

| Code | Command           | Description               |
| ---- | ----------------- | ------------------------- |
| 0x00 | V3_SWAP_EXACT_IN  | v3 swap with exact input  |
| 0x01 | V3_SWAP_EXACT_OUT | v3 swap with exact output |
| 0x08 | V2_SWAP_EXACT_IN  | v2 swap with exact input  |
| 0x09 | V2_SWAP_EXACT_OUT | v2 swap with exact output |
| 0x10 | V4_SWAP           | v4 swap                   |

### Token Operations

| Code | Command     | Description                |
| ---- | ----------- | -------------------------- |
| 0x04 | SWEEP       | Clear router token balance |
| 0x05 | TRANSFER    | Send specific amount       |
| 0x0b | WRAP_ETH    | ETH to WETH                |
| 0x0c | UNWRAP_WETH | WETH to ETH                |

### Permit2 Commands

| Code | Command               | Description           |
| ---- | --------------------- | --------------------- |
| 0x02 | PERMIT2_TRANSFER_FROM | Single token transfer |
| 0x03 | PERMIT2_PERMIT_BATCH  | Batch approval        |
| 0x0a | PERMIT2_PERMIT        | Single approval       |

### SDK Usage

```typescript
import { SwapRouter, UniswapTrade } from '@uniswap/universal-router-sdk'
import { TradeType } from '@uniswap/sdk-core'

// Build trade using v3-sdk or router-sdk
const trade = new RouterTrade({
  v3Routes: [...],
  tradeType: TradeType.EXACT_INPUT
})

// Get calldata for Universal Router
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
  slippageTolerance: new Percent(50, 10000), // 0.5%
  recipient: walletAddress,
  deadline: Math.floor(Date.now() / 1000) + 1200 // 20 min
})

// Send transaction
const tx = await wallet.sendTransaction({
  to: UNIVERSAL_ROUTER_ADDRESS,
  data: calldata,
  value
})
```

---

## Permit2 Integration

Permit2 enables signature-based token approvals instead of on-chain approve() calls.

### Approval Target: Permit2 vs Legacy (Direct to Router)

There are two approval paths. Choose based on your integration type:

| Approach                    | Approve To       | Per-Swap Auth       | Best For                         |
| --------------------------- | ---------------- | ------------------- | -------------------------------- |
| **Permit2** (recommended)   | Permit2 contract | EIP-712 signature   | Frontends with user interaction  |
| **Legacy** (direct approve) | Universal Router | None (pre-approved) | Backend services, smart accounts |

**Permit2 flow** (frontend with user signing):

1. User approves token to Permit2 contract (one-time)
2. Each swap: user signs an EIP-712 permit message
3. Universal Router uses the signature to transfer tokens via Permit2

**Legacy flow** (backend services, ERC-4337 smart accounts):

1. Approve token directly to the Universal Router address (one-time)
2. Each swap: no additional authorization needed
3. Simpler for automated systems that cannot sign EIP-712 messages

Use the Trading API's `/check_approval` endpoint — it returns the correct approval target based on the routing type.

### How It Works

1. User approves Permit2 contract once (infinite approval)
2. For each swap, user signs a message authorizing the transfer
3. Universal Router uses signature to transfer tokens via Permit2

### Two Modes

| Mode              | Description                                |
| ----------------- | ------------------------------------------ |
| SignatureTransfer | One-time signature, no on-chain state      |
| AllowanceTransfer | Time-limited allowance with on-chain state |

### Integration Pattern

```typescript
import { getContract, maxUint256, type Address } from 'viem';

const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' as const;

// Check if Permit2 approval exists
const allowance = await publicClient.readContract({
  address: PERMIT2_ADDRESS,
  abi: permit2Abi,
  functionName: 'allowance',
  args: [userAddress, tokenAddress, spenderAddress],
});

// If not approved, user must approve Permit2 first
if (allowance.amount < requiredAmount) {
  const hash = await walletClient.writeContract({
    address: tokenAddress,
    abi: erc20Abi,
    functionName: 'approve',
    args: [PERMIT2_ADDRESS, maxUint256],
  });
  await publicClient.waitForTransactionReceipt({ hash });
}

// Then sign permit for the swap
const permitSignature = await signPermit(...);
```

---

## UniswapX Auction Types

UniswapX routes swaps through off-chain fillers who compete to execute orders at better prices than on-chain AMMs. The auction mechanism varies by chain.

### Exclusive Dutch Auction (Ethereum)

- Starts with an RFQ (Request for Quote) phase where permissioned quoters compete
- Winning quoter receives **exclusive filling rights** for a set period
- If the exclusive filler doesn't execute, falls back to an open Dutch auction where the price decays each block
- Best for large swaps where MEV protection matters most

**Trading API routing type**: `DUTCH_V2` or `DUTCH_V3`

### Open Dutch Auction (Arbitrum)

- Direct open auction without an RFQ phase
- Fillers compete on-chain through a descending price mechanism
- Leverages Arbitrum's fast 0.25-second block times for rapid price discovery
- The **Unimind algorithm** sets auction parameters based on historical pair performance

**Trading API routing type**: `DUTCH_V2`

### Priority Gas Auction (Base, Unichain)

- Fillers bid by submitting transactions with varying **priority fees** at a target block
- Highest priority fee wins the right to fill the order
- Exploits OP Stack's priority ordering mechanism
- Effective on chains where block builders respect priority ordering

**Trading API routing type**: `PRIORITY`

### Key Properties (All Auction Types)

- **Gasless for users** — fillers pay gas fees, incorporated into final pricing
- **No cost on failure** — if a swap doesn't fill, the user pays nothing
- **MEV protection** — auction mechanics prevent frontrunning and sandwich attacks
- UniswapX V2 is currently supported on Ethereum (1), Arbitrum (42161), Base (8453), and Unichain (130)

For more detail, see the [UniswapX Auction Types documentation](https://docs.uniswap.org/contracts/uniswapx/auctiontypes).

### UniswapX: Signing vs. Submission Flow

The `permitData` field in the quote response serves different purposes depending on the routing type. Conflating the two causes `RequestValidationError` on `/swap`.

**CLASSIC flow** — `permitData` goes to the server:

1. `/quote` returns `permitData` (EIP-712 typed data for the Permit2 allowance)
2. User signs `permitData` locally → produces `signature`
3. `/swap` body includes **both** `signature` and `permitData` — the Universal Router contract needs `permitData` to reconstruct and verify the Permit2 authorization on-chain

**UniswapX flow (DUTCH_V2/V3/PRIORITY)** — `permitData` stays local:

1. `/quote` returns `permitData` (EIP-712 typed data for the Dutch order)
2. User signs `permitData` locally → produces `signature`
3. `/swap` body includes **only** `signature` — the order is already fully encoded in `quote.encodedOrder`, which the off-chain filler system reads directly. Sending `permitData` to `/swap` causes a schema validation error.

| Route Type           | Sign with `permitData`? | Send `permitData` to `/swap`? | Send `signature` to `/swap`? |
| -------------------- | ----------------------- | ----------------------------- | ---------------------------- |
| CLASSIC              | Yes                     | **Yes** (router needs it)     | Yes (if using Permit2)       |
| DUTCH_V2/V3/PRIORITY | Yes                     | **No** (schema rejects it)    | Yes                          |

> **Common mistake**: The API error `"quote" does not match any of the allowed types` often points at the `quote` field, but the actual cause is `permitData` being present for a UniswapX route. Strip `permitData` before submitting — see the routing-aware `prepareSwapRequest` in [Null Field Handling](#2-null-field-handling).

---

## Direct Universal Router Integration (SDK)

For direct Universal Router integration without the Trading API, use the SDK's high-level API.

### Installation

```bash
npm install @uniswap/universal-router-sdk @uniswap/router-sdk @uniswap/sdk-core @uniswap/v3-sdk viem
```

### High-Level Approach (Recommended)

Use `RouterTrade` + `SwapRouter.swapCallParameters()` for automatic command building:

```typescript
import { SwapRouter } from '@uniswap/universal-router-sdk';
import { Trade as RouterTrade } from '@uniswap/router-sdk';
import { TradeType, Percent } from '@uniswap/sdk-core';
import { Route as V3Route, Pool } from '@uniswap/v3-sdk';

// 1. Fetch pool data (required to construct routes)
// Using viem to read on-chain pool state:
const slot0 = await publicClient.readContract({
  address: poolAddress,
  abi: [
    {
      name: 'slot0',
      type: 'function',
      stateMutability: 'view',
      inputs: [],
      outputs: [
        { name: 'sqrtPriceX96', type: 'uint160' },
        { name: 'tick', type: 'int24' },
        { name: 'observationIndex', type: 'uint16' },
        { name: 'observationCardinality', type: 'uint16' },
        { name: 'observationCardinalityNext', type: 'uint16' },
        { name: 'feeProtocol', type: 'uint8' },
        { name: 'unlocked', type: 'bool' },
      ],
    },
  ],
  functionName: 'slot0',
});
const liquidity = await publicClient.readContract({
  address: poolAddress,
  abi: [
    {
      name: 'liquidity',
      type: 'function',
      stateMutability: 'view',
      inputs: [],
      outputs: [{ type: 'uint128' }],
    },
  ],
  functionName: 'liquidity',
});

const pool = new Pool(tokenIn, tokenOut, fee, slot0[0].toString(), liquidity.toString(), slot0[1]);

// 2. Build route and trade
const route = new V3Route([pool], tokenIn, tokenOut);
const trade = RouterTrade.createUncheckedTrade({
  route,
  inputAmount: amountIn,
  outputAmount: expectedOut,
  tradeType: TradeType.EXACT_INPUT,
});

// 3. Get calldata
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
  slippageTolerance: new Percent(50, 10000), // 0.5%
  recipient: walletAddress,
  deadline: Math.floor(Date.now() / 1000) + 1800,
});

// 4. Execute with viem
const hash = await walletClient.sendTransaction({
  to: UNIVERSAL_ROUTER_ADDRESS,
  data: calldata,
  value: BigInt(value),
});
```

### Low-Level Approach (Manual Commands)

For custom flows (fee collection, complex routing), use `RoutePlanner` directly:

```typescript
import { RoutePlanner, CommandType, ROUTER_AS_RECIPIENT } from '@uniswap/universal-router-sdk';
import { encodeRouteToPath } from '@uniswap/v3-sdk';

// Special addresses
const MSG_SENDER = '0x0000000000000000000000000000000000000001';
const ADDRESS_THIS = '0x0000000000000000000000000000000000000002';
```

### Example: V3 Swap with Manual Commands

```typescript
import { RoutePlanner, CommandType } from '@uniswap/universal-router-sdk';
import { encodeRouteToPath, Route } from '@uniswap/v3-sdk';

async function swapV3Manual(route: Route, amountIn: bigint, amountOutMin: bigint) {
  const planner = new RoutePlanner();

  // Encode V3 path from route
  const path = encodeRouteToPath(route, false); // false = exactInput

  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    MSG_SENDER, // recipient
    amountIn, // amountIn
    amountOutMin, // amountOutMin
    path, // encoded path
    true, // payerIsUser
  ]);

  return executeRoute(planner);
}
```

### Example: ETH to Token (Wrap + Swap)

```typescript
async function swapEthToToken(route: Route, amountIn: bigint, amountOutMin: bigint) {
  const planner = new RoutePlanner();
  const path = encodeRouteToPath(route, false);

  // 1. Wrap ETH to WETH (keep in router)
  planner.addCommand(CommandType.WRAP_ETH, [ADDRESS_THIS, amountIn]);

  // 2. Swap WETH → Token (payerIsUser = false since using router's WETH)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    MSG_SENDER,
    amountIn,
    amountOutMin,
    path,
    false,
  ]);

  return executeRoute(planner, { value: amountIn });
}
```

### Example: Token to ETH (Swap + Unwrap)

```typescript
async function swapTokenToEth(route: Route, amountIn: bigint, amountOutMin: bigint) {
  const planner = new RoutePlanner();
  const path = encodeRouteToPath(route, false);

  // 1. Swap Token → WETH (output to router)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [
    ADDRESS_THIS,
    amountIn,
    amountOutMin,
    path,
    true,
  ]);

  // 2. Unwrap WETH to ETH
  planner.addCommand(CommandType.UNWRAP_WETH, [MSG_SENDER, amountOutMin]);

  return executeRoute(planner);
}
```

### Example: Fee Collection with PAY_PORTION

```typescript
async function swapWithFee(route: Route, amountIn: bigint, feeRecipient: Address, feeBips: number) {
  const planner = new RoutePlanner();
  const path = encodeRouteToPath(route, false);
  const outputToken = route.output.wrapped.address;

  // Swap to router (ADDRESS_THIS)
  planner.addCommand(CommandType.V3_SWAP_EXACT_IN, [ADDRESS_THIS, amountIn, 0n, path, true]);

  // Pay fee portion (e.g., 30 bips = 0.3%)
  planner.addCommand(CommandType.PAY_PORTION, [outputToken, feeRecipient, feeBips]);

  // Sweep remainder to user
  planner.addCommand(CommandType.SWEEP, [outputToken, MSG_SENDER, 0n]);

  return executeRoute(planner);
}
```

### Execute Route Helper

```typescript
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk';

const ROUTER_ABI = [
  {
    name: 'execute',
    type: 'function',
    stateMutability: 'payable',
    inputs: [
      { name: 'commands', type: 'bytes' },
      { name: 'inputs', type: 'bytes[]' },
      { name: 'deadline', type: 'uint256' },
    ],
    outputs: [],
  },
] as const;

async function executeRoute(planner: RoutePlanner, options?: { value?: bigint }) {
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 1800);
  const routerAddress = UNIVERSAL_ROUTER_ADDRESS('2.0', 1); // version, chainId

  const { request } = await publicClient.simulateContract({
    address: routerAddress,
    abi: ROUTER_ABI,
    functionName: 'execute',
    args: [planner.commands, planner.inputs, deadline],
    account,
    value: options?.value ?? 0n,
  });

  return walletClient.writeContract(request);
}
```

### Command Cheat Sheet

| Command           | Parameters                                               |
| ----------------- | -------------------------------------------------------- |
| V3_SWAP_EXACT_IN  | (recipient, amountIn, amountOutMin, path, payerIsUser)   |
| V3_SWAP_EXACT_OUT | (recipient, amountOut, amountInMax, path, payerIsUser)   |
| V2_SWAP_EXACT_IN  | (recipient, amountIn, amountOutMin, path[], payerIsUser) |
| V2_SWAP_EXACT_OUT | (recipient, amountOut, amountInMax, path[], payerIsUser) |
| WRAP_ETH          | (recipient, amount)                                      |
| UNWRAP_WETH       | (recipient, amountMin)                                   |
| SWEEP             | (token, recipient, amountMin)                            |
| TRANSFER          | (token, recipient, amount)                               |
| PAY_PORTION       | (token, recipient, bips)                                 |

### Fee Tiers

| Tier   | Value | Percentage |
| ------ | ----- | ---------- |
| LOWEST | 100   | 0.01%      |
| LOW    | 500   | 0.05%      |
| MEDIUM | 3000  | 0.30%      |
| HIGH   | 10000 | 1.00%      |

---

## Common Integration Patterns

### Frontend Swap Hook (React)

**Note**: Ensure you've set up the Buffer polyfill and CORS proxy (see Critical Implementation Notes). For wagmi v2 `useWalletClient()` pitfalls, see [wagmi v2 Integration Pitfalls](#wagmi-v2-integration-pitfalls) below.

```typescript
import { isAddress, isHex } from 'viem';
import { useWalletClient } from 'wagmi';

// In browser apps, use your CORS proxy path instead (see CORS Proxy Configuration)
// e.g., const API_URL = '/api/uniswap';
const API_URL = 'https://trade-api.gateway.uniswap.org/v1';

function useSwap() {
  const { data: walletClient } = useWalletClient();
  const [quoteResponse, setQuoteResponse] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const getQuote = async (params) => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch(`${API_URL}/quote`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': API_KEY,
          'x-universal-router-version': '2.0',
        },
        body: JSON.stringify(params),
      });
      const data = await response.json();
      if (!response.ok) throw new Error(data.detail || 'Quote failed');
      setQuoteResponse(data); // Store the FULL response, not just data.quote
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const executeSwap = async (permit2Signature?: string) => {
    if (!quoteResponse) throw new Error('No quote available');

    // Strip null fields and spread quote response into body
    const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;
    const swapRequest: Record<string, unknown> = { ...cleanQuote };

    // CRITICAL: permitData handling differs by routing type
    const isUniswapX =
      quoteResponse.routing === 'DUTCH_V2' ||
      quoteResponse.routing === 'DUTCH_V3' ||
      quoteResponse.routing === 'PRIORITY';

    if (isUniswapX) {
      // UniswapX: signature only — permitData must NOT be sent to /swap
      // (permitData is used locally to sign the order, not submitted to the API)
      if (permit2Signature) swapRequest.signature = permit2Signature;
    } else {
      // CLASSIC: both signature and permitData required together, or both omitted
      if (permit2Signature && permitData && typeof permitData === 'object') {
        swapRequest.signature = permit2Signature;
        swapRequest.permitData = permitData;
      }
    }

    const swapResponse = await fetch(`${API_URL}/swap`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': API_KEY,
        'x-universal-router-version': '2.0',
      },
      body: JSON.stringify(swapRequest),
    });
    const data = await swapResponse.json();
    if (!swapResponse.ok) throw new Error(data.detail || 'Swap failed');

    // CRITICAL: Validate response before broadcasting
    if (!data.swap?.data || data.swap.data === '' || data.swap.data === '0x') {
      throw new Error('Empty swap data - quote may have expired. Please refresh.');
    }

    // Send transaction via wallet (walletClient from useWalletClient())
    if (!walletClient) throw new Error('Wallet not connected');
    const tx = await walletClient.sendTransaction(data.swap);
    return tx;
  };

  return { quote: quoteResponse?.quote, loading, error, getQuote, executeSwap };
}
```

### wagmi v2 Integration Pitfalls

The `useWalletClient()` hook from wagmi v2 can return `undefined` even when the wallet is connected — it resolves asynchronously. This causes "wallet not connected" errors at swap time. Additionally, the returned client needs a `chain` for `sendTransaction()` to work.

**Recommended pattern** — use `@wagmi/core` action functions at swap time instead of hooks:

```typescript
import { getWalletClient, getPublicClient, switchChain } from '@wagmi/core';
import type { Config } from 'wagmi';

async function executeSwapTransaction(
  config: Config,
  chainId: number,
  swapTx: { to: string; data: string; value: string }
) {
  // 1. Ensure the wallet is on the correct chain
  await switchChain(config, { chainId });

  // 2. Get wallet client with explicit chainId — avoids undefined and missing chain
  const walletClient = await getWalletClient(config, { chainId });

  // 3. Execute the swap
  const hash = await walletClient.sendTransaction({
    to: swapTx.to as `0x${string}`,
    data: swapTx.data as `0x${string}`,
    value: BigInt(swapTx.value || '0'),
  });

  // 4. Wait for confirmation
  const publicClient = getPublicClient(config, { chainId });
  if (!publicClient) throw new Error(`No public client configured for ch

…(truncated)
