swap-integration — Claude Code skill: what it does, how to install
Integrates Uniswap token swaps into frontends, backend scripts, and smart contracts. Useful when connecting the Uniswap Trading API or Universal Router.
npx skills add Uniswap/uniswap-ai --skill swap-integration -a claude-codeWhat swap-integration does
The skill guides AI coding assistants in generating swap flows using Uniswap infrastructure. For React or Next.js frontends and automated scripts, it configures requests to the Uniswap Trading API, which handles route optimization through /quote and /swap endpoints. When direct control over transaction construction is necessary, it provides patterns for the @uniswap/universal-router-sdk or contract-level execute() calls.
Using the Trading API requires an API key from the Uniswap Developer Portal passed in the x-api-key header. Requests must also include x-universal-router-version set to 2.0 (or 2.2.0 or higher for permissioned pools) and an x-agent-info metadata header containing the integration name and decision origin mode.
Generated code validates inputs before execution: token amounts must be non-negative numeric strings, addresses must match the 40-character hex pattern, and shell metacharacters are rejected. Core blockchain operations assume familiarity with the viem library.
What it helps with
- integrate token swaps using the Uniswap Trading API
- build Universal Router calls with @uniswap/universal-router-sdk
- verify token allowances and permission checks before trading
- write on-chain swap logic calling execute() on Universal Router
Example prompts for the agent
- Create a script that swaps ETH for USDC using the Uniswap Trading API
- Show how to execute a token swap directly through the Universal Router smart contract
- Set up token approval check and quote requests with viem and Trading API
How to install swap-integration
1Skills CLI (Vercel)
npx skills add Uniswap/uniswap-ai --skill swap-integration -a claude-code2Manually: put the files in place
~/.claude/skills/swap-integration/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/swap-integration/
1Skills CLI (Vercel)
npx skills add Uniswap/uniswap-ai --skill swap-integration -a codex2Manually: put the files in place
.agents/skills/swap-integration/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add Uniswap/uniswap-ai --skill swap-integration -a cursor2Manually: put the files in place
.cursor/skills/swap-integration/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install Uniswap/uniswap-ai swap-integration2Manually: put the files in place
.github/skills/swap-integration/SKILL.md1Skills CLI (Vercel)
npx skills add Uniswap/uniswap-ai --skill swap-integration -a gemini-cli2Platform CLI
gemini skills install https://github.com/Uniswap/uniswap-ai --path packages/plugins/uniswap-trading/skills/swap-integrationManually: put the files in place
.devin/skills/swap-integration/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add Uniswap/uniswap-ai --skill swap-integration -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about swap-integration
How to install swap-integration in Claude Code?
Run npx skills add Uniswap/uniswap-ai --skill swap-integration -a claude-code in your terminal.
What does the swap-integration skill do?
It helps your AI agent build token swapping workflows across web applications, backend bots, and smart contracts. It covers Uniswap Trading API integration, Universal Router SDK usage, and on-chain contract calls.
What credentials are required for the Trading API?
You need an API key from the Uniswap Developer Portal passed via the x-api-key header. Requests also require the x-agent-info attribution header and x-universal-router-version.
Source text
SKILL.md · MIT
---
name: swap-integration
description: 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.
allowed-tools: Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Bash(yarn:*), Bash(curl:*), WebFetch, AskUserQuestion, Task(subagent_type:swap-integration-expert)
model: opus
license: MIT
metadata:
author: uniswap
version: '1.6.0'
---
# 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.
**Agent Attribution**: Also send the `X-Agent-Info` header (written `x-agent-info` in the examples below) on every Trading API request. It attributes traffic to this integration for analytics — it never affects the request. The value is a compact JSON object with exactly these three fields and no extras, kept under 1 KB:
- `integration_name`: always `"swap-integration"`
- `decision_origin`: **Required choice — do not copy a default.** Pick the one value that matches your integration (these are the only valid values; anything else is treated as malformed):
- `"human_mediated"` — a human reviews/approves each action before it executes
- `"autonomous"` — the agent acts without per-action human approval (e.g. scheduled jobs or agentic harnesses)
- `version`: `"1.6.0"` — tracks this skill's `metadata.version`
**If the value is malformed**: the request still succeeds normally, but the attribution is dropped — the gateway records it as malformed on the analytics side and moves on. There is no response header or other signal to check, so there is nothing to detect at runtime: get the value right by construction.
**Constraints** — trip any of these and the whole value is discarded:
- 1 KB total for the header, and 256 characters per field.
- Plain ASCII only — no accented characters, curly quotes, or emoji; non-ASCII must be `\u`-escaped.
- Omit a field rather than sending `null`. All three must be present and valid.
- Send the header once per request; a duplicate `x-agent-info` line fails to parse.
- Unknown extra keys are ignored — harmless, but no substitute for the three required fields.
**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
x-agent-info: {"integration_name":"swap-integration","decision_origin":"<human_mediated|autonomous>","version":"1.6.0"}
```
**Raise the router version for permissioned tokens.** The header block above sends `2.0`. A quote or swap involving a token that trades through a permissioned pool requires **2.2.0 or higher**. Send `x-universal-router-version: 2.2.0` for those tokens. [Step 0: Permission Pre-Check](#step-0-permission-pre-check-permissioned-pools-only) shows how to find out which they are.
**Request Flow**:
```text
0. POST /permissions -> Permissioned pools only: may this wallet trade this token?
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 0: Permission Pre-Check (Permissioned Pools Only)
Some tokens trade only through a **permissioned pool**. An allowlist contract decides which wallets may hold and trade the token, and the swap endpoints reject any wallet that is not on the list. Call this endpoint before quoting, so a wallet that cannot trade surfaces here and not as a reverted swap.
```bash
POST /permissions
```
**Request**:
```json
{
"walletAddress": "0x...",
"tokens": ["0x..."],
"chainId": 1
}
```
`tokens` takes up to two addresses. `chainId` here is a **number**, while `/quote` wants `tokenInChainId` and `tokenOutChainId` as **strings**.
**Response**:
```json
{
"requestId": "e63f1e1e-b9e9-411a-bcc8-ff18ce4e77cf",
"results": [
{
"token": "0x...",
"isPermissioned": true,
"isAllowlisted": false,
"adapterTokenAddress": "0x...",
"kycUrl": "https://your-kyc-provider.example/verify",
"issuer": "Your Issuer Name"
}
]
}
```
`adapterTokenAddress` is present when the token is permissioned. `kycUrl` is present when the wallet is not allowlisted, and points at the identity check (KYC) the user has to pass. `issuer` is the display name for that verification provider.
**Three outcomes**:
| Result | What to do |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `isPermissioned: false` | Nothing special — quote and swap normally |
| `isPermissioned: true`, `isAllowlisted: true` | Quote and swap normally |
| `isPermissioned: true`, `isAllowlisted: false` | Quote so the user can see prices, then **block submission** and render the `kycUrl` call to action |
**This endpoint needs `x-api-key` like every other Trading API call.** The [published example](https://developers.uniswap.org/docs/trading/swapping-api/swapping-permissioned-pools) for it omits the header. Without one the endpoint returns `401` and `{"errorCode":"Unauthorized","detail":"Unauthenticated api key or session"}`, so an integration copied from that example fails before it reaches the permission logic.
**Universal Router 2.2.0 or higher is required** for quotes and swaps involving a permissioned token. Send `x-universal-router-version: 2.2.0` on the `/quote` and `/swap` calls for these tokens. The plain `UniversalRouter` deployment is a different, non-permissioned router. Only the `#v2.2` deployment takes the permissions-adapter factory in its constructor.
Issuing a permissioned token is a different job: see the `permissioned-pools-issuer` skill in the `uniswap-permissioned-pools` plugin.
### 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(...);
```
---
## Un...
© uniswap · MIT · Uniswap/uniswap-ai