npx skills add https://github.com/uniswap/uniswap-ai --skill swap-integration将 Uniswap 交换集成到前端、后端和智能合约中。
本技能假设您熟悉 viem 基础知识(客户端设置、账户管理、合约交互、交易签名)。如需全面的 viem/wagmi 指导,请安装 uniswap-viem 插件:claude plugin add @uniswap/uniswap-viem
| 正在构建... | 使用此方法 |
|---|---|
| 使用 React/Next.js 的前端 | Trading API |
| 后端脚本或机器人 | Trading API |
| 智能合约集成 | 直接调用 Universal Router |
| 需要对路由有完全控制权 | Universal Router SDK |
| 类型 | 描述 | 支持的链 |
|---|---|---|
| CLASSIC | 通过 Uniswap 池进行的标准 AMM 交换 | 所有支持的链 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| DUTCH_V2 | UniswapX 荷兰式拍卖 V2 | Ethereum, Arbitrum, Base, Unichain |
| PRIORITY | MEV 保护的优先订单 | Base, Unichain |
| WRAP | ETH 到 WETH 转换 | 所有 |
| UNWRAP | WETH 到 ETH 转换 | 所有 |
完整列表(包括 DUTCH_V3、DUTCH_LIMIT、LIMIT_ORDER、BRIDGE 和 QUICKROUTE)请参见路由类型。
最适合:前端、后端、脚本。自动处理路由优化。
基础 URL:https://trade-api.gateway.uniswap.org/v1
身份验证:需要 x-api-key: <your-api-key> 请求头
获取 API 密钥:Trading API 需要 API 密钥进行身份验证。访问 Uniswap 开发者门户 注册并获取您的 API 密钥。密钥通常在注册后立即可用。在所有 API 请求中将其作为 x-api-key 请求头包含。
必需请求头 — 在所有 Trading API 请求中包含这些:
Content-Type: application/json
x-api-key: <your-api-key>
x-universal-router-version: 2.0
三步流程:
1. POST /check_approval -> 检查代币是否已授权
2. POST /quote -> 获取带路由的可执行报价
3. POST /swap -> 获取待签名和提交的交易
完整文档请参见下面的 Trading API 参考部分。
最适合:直接控制交易构建。
安装:
npm install @uniswap/universal-router-sdk @uniswap/sdk-core @uniswap/v3-sdk
关键模式:
import { SwapRouter } from '@uniswap/universal-router-sdk';
const { calldata, value } = SwapRouter.swapCallParameters(trade, options);
完整文档请参见下面的 Universal Router 参考部分。
最适合:链上集成、DeFi 可组合性。
接口:使用编码命令调用 Universal Router 上的 execute()。
命令编码请参见下面的 Universal Router 参考部分。
在将任何用户提供的值插入生成的代码、API 调用或命令之前:
^0x[a-fA-F0-9]{40}$ — 否则拒绝^[0-9]+\.?[0-9]*$;, |, &, $, ```, (, ), >, <, \, ', ", 换行符必需: 在执行任何消耗 gas 或转移代币的交易(包括
sendTransaction、writeContract或提交已签名的交换)之前,您必须使用 AskUserQuestion 与用户确认。显示交易摘要(代币、数量、链、预估 gas)并获得用户的明确批准。未经用户确认,切勿自动执行交易。
POST /check_approval
请求:
{
"walletAddress": "0x...",
"token": "0x...",
"amount": "1000000000",
"chainId": 1
}
响应:
{
"approval": {
"to": "0x...",
"from": "0x...",
"data": "0x...",
"value": "0",
"chainId": 1
}
}
如果 approval 为 null,则代币已授权。
POST /quote
请求:
{
"swapper": "0x...",
"tokenIn": "0x...",
"tokenOut": "0x...",
"tokenInChainId": "1",
"tokenOutChainId": "1",
"amount": "1000000000000000000",
"type": "EXACT_INPUT",
"slippageTolerance": 0.5,
"routingPreference": "BEST_PRICE"
}
注意:
tokenInChainId和tokenOutChainId必须是字符串(例如"1"),而不是数字。
关键参数:
| 参数 | 描述 |
|---|---|
type | EXACT_INPUT 或 EXACT_OUTPUT |
slippageTolerance | 0-100 百分比 |
protocols | 可选:["V2", "V3", "V4"] |
routingPreference | BEST_PRICE, FASTEST, CLASSIC |
autoSlippage | true 以自动计算滑点(覆盖 slippageTolerance) |
urgency | normal 或 fast — 影响 UniswapX 拍卖时间 |
响应 — 形状因路由类型而异。在以太坊主网上的 BEST_PRICE 路由通常返回 UniswapX (DUTCH_V2),而不是 CLASSIC。
CLASSIC 响应:
{
"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) 响应 — 不同的 quote 形状,没有 quote.output:
{
"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 输出数量:使用
quote.orderInfo.outputs[0].startAmount作为最佳情况下的填充数量。endAmount是完全拍卖衰减后的最低值。UniswapX 响应中没有quote.output.amount— 在运行时访问它会抛出错误。显示提示:对于 CLASSIC 路由,使用
gasFeeUSD(包含美元值的字符串)显示 gas 成本。请不要使用硬编码的 ETH 价格手动转换gasFee(wei)— 这会导致严重不准确的估计(例如,约 87 美元而不是约 0.01 美元)。UniswapX 路由对交换者来说是免 gas 的。
跨路由类型的编译时类型安全请参见 QuoteResponse TypeScript 类型。
POST /swap
请求 - 将报价响应直接展开到请求体中:
// 正确:展开报价响应,去除 null 字段
const quoteResponse = await fetchQuote(params);
// 始终去除 permitData/permitTransaction — 根据路由类型显式处理它们
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:仅签名 — permitData 不得发送到 /swap
if (permit2Signature) swapRequest.signature = permit2Signature;
} else {
// CLASSIC:签名和 permitData 必须同时存在,或同时省略
if (permit2Signature && permitData && typeof permitData === 'object') {
swapRequest.signature = permit2Signature;
swapRequest.permitData = permitData;
}
}
关键:请勿将报价包装在 {quote: quoteResponse} 中。API 期望将报价响应字段展开到请求体中。
Permit2 规则(CLASSIC 路由):
signature 和 permitData 必须同时存在,或同时省略permitData: null — 完全省略该字段permitData: null — 发送前去除此字段UniswapX 路由 (DUTCH_V2/V3/PRIORITY):permitData 在本地用于签署订单,但必须排除在 /swap 请求体之外。请参见签名与提交流程。
响应(准备签名的交易):
{
"swap": {
"to": "0x...",
"from": "0x...",
"data": "0x...",
"value": "0",
"chainId": 1,
"gasLimit": "250000"
}
}
响应验证 - 广播前始终验证:
function validateSwapResponse(response: SwapResponse): void {
if (!response.swap?.data || response.swap.data === '' || response.swap.data === '0x') {
throw new Error('swap.data 为空 - 报价可能已过期');
}
if (!isAddress(response.swap.to) || !isAddress(response.swap.from)) {
throw new Error('交换响应中的地址无效');
}
}
当前支持的链及其 ID 请参见 官方支持的链列表。
| 类型 | 描述 |
|---|---|
| CLASSIC | 通过 Uniswap 池进行的标准 AMM 交换 |
| DUTCH_V2 | UniswapX 荷兰式拍卖 V2 |
| DUTCH_V3 | UniswapX 荷兰式拍卖 V3 |
| PRIORITY | MEV 保护的优先订单(Base, Unichain) |
| DUTCH_LIMIT | UniswapX 荷兰式限价订单 |
| LIMIT_ORDER | 限价订单 |
| WRAP | ETH 到 WETH 转换 |
| UNWRAP | WETH 到 ETH 转换 |
| BRIDGE | 跨链桥接 |
| QUICKROUTE | 快速近似报价 |
UniswapX 可用性:UniswapX V2 订单在 Ethereum (1)、Arbitrum (42161)、Base (8453) 和 Unichain (130) 上受支持。拍卖机制因链而异 — 请参见下面的 UniswapX 拍卖类型。
这些是在实际 Trading API 集成过程中发现的常见陷阱。遵循这些规则以避免链上回退和 API 错误。
/swap 端点期望将报价响应展开到请求体中,而不是包装在 quote 字段中。
// 错误 - 导致 "quote does not match any of the allowed types"
const badRequest = {
quote: quoteResponse, // 不要包装!
signature: '0x...',
};
// 正确 - 展开报价响应
const goodRequest = {
...quoteResponse,
signature: '0x...', // 仅在使用 Permit2 时
};
API 拒绝 permitData: null。此外,permitData 的处理因路由类型而异 — 完整解释请参见签名与提交流程。
function prepareSwapRequest(quoteResponse: QuoteResponse, signature?: string): object {
// 始终从展开中去除 permitData 和 permitTransaction — 显式处理它们
const { permitData, permitTransaction, ...cleanQuote } = quoteResponse;
const request: Record<string, unknown> = { ...cleanQuote };
// UniswapX (DUTCH_V2, DUTCH_V3, PRIORITY): permitData 仅用于本地签名。
// /swap 请求体不得包含 permitData — 订单已完全编码在
// quote.encodedOrder 中。只需要签名。
const isUniswapX =
quoteResponse.routing === 'DUTCH_V2' ||
quoteResponse.routing === 'DUTCH_V3' ||
quoteResponse.routing === 'PRIORITY';
if (isUniswapX) {
if (signature) request.signature = signature;
} else {
// CLASSIC:签名和 permitData 必须同时存在,或同时省略。
// Universal Router 合约需要 permitData 来验证链上的 Permit2 授权。
if (signature && permitData && typeof permitData === 'object') {
request.signature = signature;
request.permitData = permitData;
}
}
return request;
}
/swap 请求体中 signature 和 permitData 的规则取决于路由类型:
CLASSIC 路由:
| 场景 | signature | permitData |
|---|---|---|
| 标准交换(无 Permit2) | 省略 | 省略 |
| Permit2 交换 | 必需 | 必需 |
| 无效 | 存在 | 缺失 |
| 无效 | 缺失 | 存在 |
| 无效(API 错误) | 任何 | null |
UniswapX 路由 (DUTCH_V2/V3/PRIORITY):
| 场景 | signature | permitData |
|---|---|---|
| UniswapX 订单 | 必需 | 省略(不要发送) |
| 无效 | 任何 | 存在(模式拒绝) |
发送到区块链之前始终验证交换响应:
import { isAddress, isHex } from 'viem';
function validateSwapBeforeBroadcast(swap: SwapTransaction): void {
// 1. data 必须是非空的十六进制字符串
if (!swap.data || swap.data === '' || swap.data === '0x') {
throw new Error('swap.data 为空 - 这将在链上回退。请重新获取报价。');
}
if (!isHex(swap.data)) {
throw new Error('swap.data 不是有效的十六进制字符串');
}
// 2. 地址必须有效
if (!isAddress(swap.to)) {
throw new Error('swap.to 不是有效的地址');
}
if (!isAddress(swap.from)) {
throw new Error('swap.from 不是有效的地址');
}
// 3. value 必须存在(对于非 ETH 交换可以是 "0")
if (swap.value === undefined || swap.value === null) {
throw new Error('swap.value 缺失');
}
}
在浏览器环境中使用 viem/wagmi 时,您需要 Node.js polyfill:
安装 buffer polyfill:
npm install buffer
添加到入口文件(在其他导入之前):
// src/main.tsx 或 src/index.tsx
import { Buffer } from 'buffer';
globalThis.Buffer = Buffer;
// 然后您的其他导入
import React from 'react';
import { WagmiProvider } from 'wagmi';
// ...
Vite 配置 (vite.config.ts):
export default defineConfig({
define: {
global: 'globalThis',
},
optimizeDeps: {
include: ['buffer'],
},
resolve: {
alias: {
buffer: 'buffer',
},
},
});
没有此设置,您将看到:ReferenceError: Buffer is not defined
Trading API 不支持浏览器 CORS 预检请求 — OPTIONS 请求返回 415 Unsupported Media Type。从浏览器直接 fetch() 调用将始终失败。您必须通过您自己的服务器或开发服务器代理 API 请求。
Vite 开发代理(合并到上面用于 Buffer polyfill 的同一个 vite.config.ts 中):
export default defineConfig({
server: {
proxy: {
'/api/uniswap': {
target: 'https://trade-api.gateway.uniswap.org/v1',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/uniswap/, ''),
},
},
},
});
然后在您的前端代码中使用 /api/uniswap/quote 而不是完整 URL。
Vercel 生产代理 (vercel.json):
{
"rewrites": [
{
"source": "/api/uniswap/:path*",
"destination": "https://trade-api.gateway.uniswap.org/v1/:path*"
}
]
}
Cloudflare Pages (public/_redirects):
/api/uniswap/* https://trade-api.gateway.uniswap.org/v1/:splat 200
Next.js (next.config.js):
module.exports = {
async rewrites() {
return [
{
source: '/api/uniswap/:path*',
destination: 'https://trade-api.gateway.uniswap.org/v1/:path*',
},
];
},
};
没有代理,您将在浏览器控制台中看到:415 Unsupported Media Type 预检错误或 CORS 错误。
deadline 参数防止过时执行/swap 返回空的 data,报价可能已过期报价响应的形状因路由类型而异。使用 routing 字段上的判别联合来获得编译时安全性,而不是强制转换为 any:
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 类型化数据 — 本地签名,不要发送到 /swap
permitData: Record<string, unknown> | null;
};
type QuoteResponse = ClassicQuoteResponse | UniswapXQuoteResponse;
// 用于路由感知逻辑的类型守卫
function isUniswapXQuote(q: QuoteResponse): q is UniswapXQuoteResponse {
return q.routing === 'DUTCH_V2' || q.routing === 'DUTCH_V3' || q.routing === 'PRIORITY';
}
// 按路由类型读取输出数量
function getOutputAmount(q: QuoteResponse): string {
if (isUniswapXQuote(q)) {
const firstOutput = q.quote.orderInfo.outputs[0];
if (!firstOutput) throw new Error('UniswapX 报价没有输出');
// startAmount = 最佳情况填充;endAmount = 拍卖衰减后的最低值
return firstOutput.startAmount;
}
return q.quote.output.amount;
}
Universal Router 是跨 Uniswap v2、v3 和 v4 进行交换的统一接口。
function execute(
bytes calldata commands,
bytes[] calldata inputs,
uint256 deadline
) external payable;
每个命令是一个字节:
| 位 | 名称 | 用途 |
|---|---|---|
| 0 | flag | 允许回退(1 = 失败时继续) |
| 1-2 | reserved | 使用 0 |
| 3-7 | command | 操作标识符 |
| 代码 | 命令 | 描述 |
|---|---|---|
| 0x00 | V3_SWAP_EXACT_IN | v3 精确输入交换 |
| 0x01 | V3_SWAP_EXACT_OUT | v3 精确输出交换 |
| 0x08 | V2_SWAP_EXACT_IN | v2 精确输入交换 |
| 0x09 | V2_SWAP_EXACT_OUT | v2 精确输出交换 |
| 0x10 | V4_SWAP | v4 交换 |
| 代码 | 命令 | 描述 |
|---|---|---|
| 0x04 | SWEEP | 清除路由器代币余额 |
| 0x05 | TRANSFER | 发送特定数量 |
| 0x0b | WRAP_ETH | ETH 到 WETH |
| 0x0c | UNWRAP_WETH | WETH 到 ETH |
| 代码 | 命令 | 描述 |
|---|---|---|
| 0x02 | PERMIT2_TRANSFER_FROM | 单一代币转移 |
| 0x03 | PERMIT2_PERMIT_BATCH | 批量授权 |
| 0x0a | PERMIT2_PERMIT | 单一授权 |
import { SwapRouter, UniswapTrade } from '@uniswap/universal-router-sdk'
import { TradeType } from '@uniswap/sdk-core'
// 使用 v3-sdk 或 router-sdk 构建交易
const trade = new RouterTrade({
v3Routes: [...],
tradeType: TradeType.EXACT_INPUT
})
// 获取 Universal Router 的 calldata
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
slippageTolerance: new Percent(50, 10000), // 0.5%
recipient: walletAddress,
deadline: Math.floor(Date.now() / 1000) + 1200 // 20 分钟
})
// 发送交易
const tx = await wallet.sendTransaction({
to: UNIVERSAL_ROUTER_ADDRESS,
data: calldata,
value
})
Permit2 支持基于签名的代币授权,而不是链上的 approve() 调用。
有两种授权路径。根据您的集成类型选择:
| 方法 | 授权给 | 每次交换授权 | 最适合 |
|---|---|---|---|
| Permit2(推荐) | Permit2 合约 | EIP-712 签名 | 需要用户交互的前端 |
| 旧版(直接授权) | Universal Router | 无(预授权) | 后端服务、智能账户 |
Permit2 流程(需要用户签名的前端):
旧版流程(后端服务、ERC-4337 智能账户):
使用 Trading API 的 /check_approval 端点 — 它根据路由类型返回正确的授权目标。
| 模式 | 描述 |
|---|---|
| SignatureTransfer | 一次性签名,无链上状态 |
| AllowanceTransfer | 有时限的授权,有链上状态 |
import { getContract, maxUint256, type Address } from 'viem';
const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3' as const;
// 检查 Permit2 授权是否存在
const allowance = await publicClient.readContract({
address: PERMIT2_ADDRESS,
abi: permit2Abi,
functionName: 'allowance',
args: [userAddress, tokenAddress, spenderAddress],
});
// 如果未授权,用户必须先授权 Permit2
if (allowance.amount < requiredAmount) {
const hash = await walletClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [PERMIT2_ADDRESS, maxUint256],
});
await publicClient.waitForTransactionReceipt({ hash });
}
// 然后为交换签署许可
const permitSignature = await signPermit(...);
UniswapX 通过链下填充者路由交换,这些填充者竞争以比链上 AMM 更好的价格执行订单。拍卖机制因链而异。
Trading API 路由类型:DUTCH_V2 或 DUTCH_V3
Trading API 路由类型:DUTCH_V2
Trading API 路由类型:PRIORITY
更多详情,请参见 UniswapX 拍卖类型文档。
报价响应中的 permitData 字段根据路由类型有不同的用途。混淆两者会导致 /swap 上的 RequestValidationError。
CLASSIC 流程 — permitData 发送到服务器:
/quote 返回 permitData(用于 Permit2 授权的 EIP-712 类型化数据)permitData → 产生 signature/swap 请求体包含两者 signature 和 permitData — Universal Router 合约需要 permitData 来在链上重建和验证 Permit2 授权UniswapX 流程 (DUTCH_V2/V3/PRIORITY) — permitData 保留在本地:
/quote 返回 permitData(用于荷兰式订单的 EIP-712 类型化数据)permitData → 产生 signature/swap 请求体仅包含 signature — 订单已完全编码在 quote.encodedOrder 中,链下填充者系统直接读取。将 permitData 发送到 /swap 会导致模式验证错误。| 路由类型 | 使用 permitData 签名? | 将 permitData 发送到 /swap? | 将 signature 发送到 /swap? |
|---|---|---|---|
| CLASSIC | 是 | 是(路由器需要它) | 是(如果使用 Permit2) |
| DUTCH_V2/V3/PRIORITY | 是 | 否(模式拒绝它) | 是 |
常见错误:API 错误
"quote" does not match any of the allowed types通常指向quote字段,但实际原因是 UniswapX 路由中存在permitData。提交前去除permitData— 请参见空字段处理中路由感知的prepareSwapRequest。
对于不使用 Trading API 的直接 Universal Router 集成,请使用 SDK 的高级 API。
npm install @uniswap/universal-router-sdk @uniswap/router-sdk @uniswap/sdk-core @uniswap/v3-sdk viem
使用 RouterTrade + SwapRouter.swapCallParameters() 进行自动命令构建:
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. 获取池数据(构建路由所需)
// 使用 viem 读取链上池状态:
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. 构建路由和交易
const route = new V3Route([pool], tokenIn, tokenOut);
const trade = RouterTrade.createUncheckedTrade({
route,
inputAmount: amountIn,
outputAmount: expectedOut,
tradeType: TradeType.EXACT_INPUT,
});
// 3. 获取 calldata
const { calldata, value } = SwapRouter.swapCallParameters(trade, {
slippageTolerance: new Percent(50, 10000), // 0.5%
recipient: walletAddress,
deadline: Math.floor(Date.now() / 1000) + 1800,
});
// 4. 使用 viem 执行
const hash = await walletClient.sendTransaction({
to: UNIVERSAL_ROUTER_ADDRESS,
data: calldata,
value: BigInt(value),
});
对于自定义流程(费用收集、复杂路由),直接使用 RoutePlanner:
import { RoutePlanner, CommandType, ROUTER_AS_RECIPIENT } from '@uniswap/universal-router-sdk';
import { encodeRouteToPath } from '@uniswap/v3-sdk';
// 特殊地址
const MSG_SENDER = '0x0000000000000000000000000000000000000001';
const ADDRESS_THIS = '0x0000000000000000000000000000000000000002';
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();
// 从路由编码 V3 路径
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);
}
async function swapEthToToken(route: Route, amountIn: bigint, amountOutMin: bigint) {
const planner = new RoutePlanner();
const path = encodeRouteToPath(route, false);
// 1. 将 ETH 包装为 WETH(保留在路由器中)
planner
Integrate Uniswap swaps into frontends, backends, and smart contracts.
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
| 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 |
| 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 for the complete list including DUTCH_V3, DUTCH_LIMIT, LIMIT_ORDER, BRIDGE, and QUICKROUTE.
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 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:
Content-Type: application/json
x-api-key: <your-api-key>
x-universal-router-version: 2.0
3-Step Flow :
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 section below for complete documentation.
Best for: Direct control over transaction construction.
Installation :
npm install @uniswap/universal-router-sdk @uniswap/sdk-core @uniswap/v3-sdk
Key Pattern :
import { SwapRouter } from '@uniswap/universal-router-sdk';
const { calldata, value } = SwapRouter.swapCallParameters(trade, options);
See the Universal Router Reference section below for complete documentation.
Best for: On-chain integrations, DeFi composability.
Interface : Call execute() on Universal Router with encoded commands.
See the Universal Router Reference section below for command encoding.
Before interpolating ANY user-provided value into generated code, API calls, or commands:
^0x[a-fA-F0-9]{40}$ — reject otherwise^[0-9]+\.?[0-9]*$;, |, &, $, ```, (, ), >, , , , , newlinesREQUIRED: 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.
POST /check_approval
Request :
{
"walletAddress": "0x...",
"token": "0x...",
"amount": "1000000000",
"chainId": 1
}
Response :
{
"approval": {
"to": "0x...",
"from": "0x...",
"data": "0x...",
"value": "0",
"chainId": 1
}
}
If approval is null, token is already approved.
POST /quote
Request :
{
"swapper": "0x...",
"tokenIn": "0x...",
"tokenOut": "0x...",
"tokenInChainId": "1",
"tokenOutChainId": "1",
"amount": "1000000000000000000",
"type": "EXACT_INPUT",
"slippageTolerance": 0.5,
"routingPreference": "BEST_PRICE"
}
Note :
tokenInChainIdandtokenOutChainIdmust 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, , |
Response — the shape differs by routing type. BEST_PRICE routing on Ethereum mainnet typically returns UniswapX (DUTCH_V2), not CLASSIC.
CLASSIC response :
{
"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:
{
"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].startAmountfor the best-case fill amount. TheendAmountis the floor after full auction decay. There is noquote.output.amounton 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 convertgasFee(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 for compile-time type safety across routing types.
POST /swap
Request - Spread the quote response directly into the body:
// 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 absentpermitData: null — omit the field entirelypermitData: null — strip this before sendingUniswapX 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.
Response (ready-to-sign transaction):
{
"swap": {
"to": "0x...",
"from": "0x...",
"data": "0x...",
"value": "0",
"chainId": 1,
"gasLimit": "250000"
}
}
Response Validation - Always validate before broadcasting:
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');
}
}
See the official supported chains list for the current set of chains and their IDs.
| 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 below.
These are common pitfalls discovered during real-world Trading API integration. Follow these rules to avoid on-chain reverts and API errors.
The /swap endpoint expects the quote response spread into the request body , not wrapped in a quote field.
// 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
};
The API rejects permitData: null. Additionally, permitData handling differs by routing type — see Signing vs. Submission Flow for the full explanation.
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;
}
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) |
Always validate the swap response before sending to the blockchain:
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');
}
}
When using viem/wagmi in browser environments, you need Node.js polyfills:
Install buffer polyfill :
npm install buffer
Add to your entry file (before other imports) :
// 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):
export default defineConfig({
define: {
global: 'globalThis',
},
optimizeDeps: {
include: ['buffer'],
},
resolve: {
alias: {
buffer: 'buffer',
},
},
});
Without this setup, you'll see: ReferenceError: Buffer is not defined
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):
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):
{
"rewrites": [
{
"source": "/api/uniswap/:path*",
"destination": "https://trade-api.gateway.uniswap.org/v1/:path*"
}
]
}
Cloudflare Pages (public/_redirects):
/api/uniswap/* https://trade-api.gateway.uniswap.org/v1/:splat 200
Next.js (next.config.js):
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.
deadline parameter to prevent stale execution/swap returns empty data, the quote likely expiredThe 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:
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;
}
The Universal Router is a unified interface for swapping across Uniswap v2, v3, and v4.
function execute(
bytes calldata commands,
bytes[] calldata inputs,
uint256 deadline
) external payable;
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 |
| 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 |
| 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 |
| Code | Command | Description |
|---|---|---|
| 0x02 | PERMIT2_TRANSFER_FROM | Single token transfer |
| 0x03 | PERMIT2_PERMIT_BATCH | Batch approval |
| 0x0a | PERMIT2_PERMIT | Single approval |
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 enables signature-based token approvals instead of on-chain approve() calls.
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):
Legacy flow (backend services, ERC-4337 smart accounts):
Use the Trading API's /check_approval endpoint — it returns the correct approval target based on the routing type.
| Mode | Description |
|---|---|
| SignatureTransfer | One-time signature, no on-chain state |
| AllowanceTransfer | Time-limited allowance with on-chain state |
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 routes swaps through off-chain fillers who compete to execute orders at better prices than on-chain AMMs. The auction mechanism varies by chain.
Trading API routing type : DUTCH_V2 or DUTCH_V3
Trading API routing type : DUTCH_V2
Trading API routing type : PRIORITY
For more detail, see the UniswapX Auction Types documentation.
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:
/quote returns permitData (EIP-712 typed data for the Permit2 allowance)permitData locally → produces signature/swap body includes both signature and permitData — the Universal Router contract needs permitData to reconstruct and verify the Permit2 authorization on-chainUniswapX flow (DUTCH_V2/V3/PRIORITY) — permitData stays local:
/quote returns permitData (EIP-712 typed data for the Dutch order)permitData locally → produces signature/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 typesoften points at thequotefield, but the actual cause ispermitDatabeing present for a UniswapX route. StrippermitDatabefore submitting — see the routing-awareprepareSwapRequestin Null Field Handling.
For direct Universal Router integration without the Trading API, use the SDK's high-level API.
npm install @uniswap/universal-router-sdk @uniswap/router-sdk @uniswap/sdk-core @uniswap/v3-sdk viem
Use RouterTrade + SwapRouter.swapCallParameters() for automatic command building:
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),
});
For custom flows (fee collection, complex routing), use RoutePlanner directly:
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';
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);
}
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 });
}
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);
}
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);
}
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 | 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) |
| Tier | Value | Percentage |
|---|---|---|
| LOWEST | 100 | 0.01% |
| LOW | 500 | 0.05% |
| MEDIUM | 3000 | 0.30% |
| HIGH | 10000 | 1.00% |
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 below.
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 };
}
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:
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 chainId ${chainId}`);
return publicClient.waitForTransactionReceipt({ hash });
}
Why this matters :
useWalletClient() hook returns { data: undefined } during async resolution, even after useAccount() shows connectedgetWalletClient(config, { chainId }) is a promise that resolves only when the client is ready, and includes the chainswitchChain() prevents "chain mismatch" errors when the wallet is on a different network than the swapimport { createWalletClient, createPublicClient, http, isAddress, isHex, type Address } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
const API_URL = 'https://trade-api.gateway.uniswap.org/v1';
const API_KEY = process.env.UNISWAP_API_KEY!;
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const walletClient = createWalletClient({ account, chain: mainnet, transport: http() });
// Helper to prepare /swap request body — routing-aware permitData handling
function prepareSwapRequest(quoteResponse: Record<string, unknown>, signature?: string): object {
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 already 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
if (signature && permitData && typeof permitData === 'object') {
request.signature = signature;
request.permitData = permitData;
}
}
return request;
}
// Validate swap response before broadcasting
function validateSwap(swap: { data?: string; to?: string; from?: string }): void {
if (!swap?.data || swap.data === '' || swap.data === '0x') {
throw new Error('swap.data is empty - quote may have expired');
}
if (!isHex(swap.data)) {
throw new Error('swap.data is not valid hex');
}
if (!swap.to || !isAddress(swap.to) || !swap.from || !isAddress(swap.from)) {
throw new Error('Invalid address in swap response');
}
}
async function executeSwap(tokenIn: Address, tokenOut: Address, amount: string, chainId: number) {
const ETH_ADDRESS = '0x0000000000000000000000000000000000000000';
// 1. Check approval (for ERC20 tokens, not native ETH)
if (tokenIn !== ETH_ADDRESS) {
const approvalRes = await fetch(`${API_URL}/check_approval`, {
method: 'POST',
headers: {
'x-api-key': API_KEY,
'Content-Type': 'application/json',
'x-universal-router-version': '2.0',
},
body: JSON.stringify({
walletAddress: account.address,
token: tokenIn,
amount,
chainId,
}),
});
const approvalData = await approvalRes.json();
if (approvalData.approval) {
const hash = await walletClient.sendTransaction({
to: approvalData.approval.to,
data: approvalData.approval.data,
value: BigInt(approvalData.approval.value || '0'),
});
await publicClient.waitForTransactionReceipt({ hash });
}
}
// 2. Get quote
const quoteRes = await fetch(`${API_URL}/quote`, {
method: 'POST',
headers: {
'x-api-key': API_KEY,
'Content-Type': 'application/json',
'x-universal-router-version': '2.0',
},
body: JSON.stringify({
swapper: account.address,
tokenIn,
tokenOut,
tokenInChainId: String(chainId),
tokenOutChainId: String(chainId),
amount,
type: 'EXACT_INPUT',
slippageTolerance: 0.5,
}),
});
const quoteResponse = await quoteRes.json(); // Store FULL response
if (!quoteRes.ok) {
throw new Error(quoteResponse.detail || 'Quote failed');
}
// 3. Execute swap - CRITICAL: spread quote response, strip null fields
const swapRequest = prepareSwapRequest(quoteResponse);
const swapRes = await fetch(`${API_URL}/swap`, {
method: 'POST',
headers: {
'x-api-key': API_KEY,
'Content-Type': 'application/json',
'x-universal-router-version': '2.0',
},
body: JSON.stringify(swapRequest),
});
const swapData = await swapRes.json();
if (!swapRes.ok) {
throw new Error(swapData.detail || 'Swap request failed');
}
// 4. Validate before broadcasting
validateSwap(swapData.swap);
const hash = await walletClient.sendTransaction({
to: swapData.swap.to,
data: swapData.swap.data,
value: BigInt(swapData.swap.value || '0'),
});
return publicClient.waitForTransactionReceipt({ hash });
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IUniversalRouter {
function execute(
bytes calldata commands,
bytes[] calldata inputs,
uint256 deadline
) external payable;
}
interface IERC20 {
function approve(address spender, uint256 amount) external returns (bool);
}
contract SwapIntegration {
IUniversalRouter public immutable router;
address public constant PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;
constructor(address _router) {
router = IUniversalRouter(_router);
}
function swap(
bytes calldata commands,
bytes[] calldata inputs,
uint256 deadline
) external payable {
router.execute{value: msg.value}(commands, inputs, deadline);
}
// Approve token for Permit2 (one-time setup)
function approveToken(address token) external {
IERC20(token).approve(PERMIT2, type(uint256).max);
}
}
Execute Trading API swaps through ERC-4337 smart accounts with delegation. The pattern:
// After getting swap calldata from Trading API:
const { to, data, value } = swapResponse.swap;
// Wrap in delegation execution
const execution = {
target: to, // Universal Router
callData: data,
value: BigInt(value),
};
// Submit via bundler
const userOpHash = await bundlerClient.sendUserOperation({
account: delegateSmartAccount,
calls: [
{
to: delegationManagerAddress,
data: encodeFunctionData({
abi: delegationManagerAbi,
functionName: 'redeemDelegations',
args: [[[signedDelegation]], [0], [[execution]]],
}),
value: execution.value,
},
],
});
Key considerations :
See Advanced Patterns Reference for the complete implementation with types and error handling.
On L2 chains (Base, Optimism, Arbitrum), swaps outputting ETH may deliver WETH instead of native ETH. Always check and unwrap after swaps:
import { parseAbi, type Address } from 'viem';
const WETH_ABI = parseAbi([
'function balanceOf(address) view returns (uint256)',
'function withdraw(uint256)',
]);
const WETH_ADDRESSES: Record<number, Address> = {
1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
10: '0x4200000000000000000000000000000000000006',
8453: '0x4200000000000000000000000000000000000006',
42161: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
};
// After swap completes on an L2:
const wethAddress = WETH_ADDRESSES[chainId];
if (wethAddress) {
const wethBalance = await publicClient.readContract({
address: wethAddress,
abi: WETH_ABI,
functionName: 'balanceOf',
args: [accountAddress],
});
if (wethBalance > 0n) {
const hash = await walletClient.writeContract({
address: wethAddress,
abi: WETH_ABI,
functionName: 'withdraw',
args: [wethBalance],
});
await publicClient.waitForTransactionReceipt({ hash });
}
}
See Advanced Patterns Reference for chain-specific WETH addresses and integration details.
The Trading API enforces rate limits (~10 requests/second per endpoint). For batch operations:
Add 100-200ms delays between sequential API calls
Implement exponential backoff with jitter on 429 responses
Cache approval results — approvals rarely change between calls
// Exponential backoff for 429 responses
async function fetchWithRetry(url: string, init: RequestInit, maxRetries = 5): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, init);
if (response.status !== 429 && response.status < 500) return response;
if (attempt === maxRetries) throw new Error(Failed after ${maxRetries} retries);
const delay = Math.min(200 * Math.pow(2, attempt) + Math.random() * 100, 10000);
await new Promise((resolve) => setTimeout(resolve, delay));
} throw new Error('Unreachable'); }
See Advanced Patterns Reference for batch operation patterns and full retry implementation.
Addresses are per-chain. The legacy v1 address 0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD is deprecated.
| Chain | ID | Address |
|---|---|---|
| Ethereum | 1 | 0x66a9893cc07d91d95644aedd05d03f95e1dba8af |
| Unichain | 130 | 0xef740bf23acae26f6492b10de645d6b98dc8eaf3 |
| Optimism | 10 | 0x851116d9223fabed8e56c0e6b8ad0c31d98b3507 |
| Base | 8453 | 0x6ff5693b99212da76ad316178a184ab56d299b43 |
| Arbitrum | 42161 | 0xa51afafe0263b40edaef0df8781ea9aa03e381a3 |
For testnet addresses, see Uniswap v4 Deployments.
| Chain | Address |
|---|---|
| All chains | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
| Issue | Solution |
|---|---|
| "Insufficient allowance" | Call /check_approval first and submit approval tx |
| "Quote expired" | Increase deadline or re-fetch quote |
| "Slippage exceeded" | Increase slippageTolerance or retry |
| "Insufficient liquidity" | Try smaller amount or different route |
| "Buffer is not defined" | Add Buffer polyfill (see Critical Implementation Notes) |
| On-chain revert with empty data | Validate swap.data is non-empty hex before broadcasting |
| "permitData must be of type object" | Strip permitData: null from request - omit field entirely |
| "quote does not match any of the allowed types" | Don't wrap quote in {quote: ...} — spread into request body. Also check: for UniswapX routes, must be omitted from the body (see API Validation Errors) |
| Error Message | Cause | Fix |
|---|---|---|
"permitData" must be of type object | Sending permitData: null | Omit the field entirely when null |
"quote" does not match any of the allowed types | Wrapping quote in {quote: quoteResponse} | Spread quote response: {...quoteResponse} |
"quote" does not match any of the allowed types | Including permitData in a UniswapX (DUTCH_V2/V3/PRIORITY) body |
| Code | Meaning |
|---|---|
| 400 | Invalid request parameters (see validation errors above) |
| 401 | Invalid or missing API key |
| 404 | No route found for pair |
| 429 | Rate limit exceeded |
| 500 | API error - implement exponential backoff retry |
Before sending a swap transaction to the blockchain:
swap.data is non-empty hex (not '', not '0x')swap.to and swap.from are validWeekly Installs
473
Repository
GitHub Stars
185
First Seen
Feb 12, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex444
opencode438
gemini-cli433
github-copilot432
kimi-cli427
amp426
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
136,300 周安装
<\'"FASTESTCLASSICautoSlippage | true to auto-calculate slippage (overrides slippageTolerance) |
urgency | normal or fast — affects UniswapX auction timing |
| Polygon | 137 | 0x1095692a6237d83c6a72f3f5efedb9a670c49223 |
| Blast | 81457 | 0xeabbcb3e8e415306207ef514f660a3f820025be3 |
| BNB | 56 | 0x1906c1d672b88cd1b9ac7593301ca990f94eae07 |
| Zora | 7777777 | 0x3315ef7ca28db74abadc6c44570efdf06b04b020 |
| World Chain | 480 | 0x8ac7bee993bb44dab564ea4bc9ea67bf9eb5e743 |
| Avalanche | 43114 | 0x94b75331ae8d42c1b61065089b7d48fe14aa73b7 |
| Celo | 42220 | 0xcb695bc5d3aa22cad1e6df07801b061a05a0233a |
| Soneium | 1868 | 0x4cded7edf52c8aa5259a54ec6a3ce7c6d2a455df |
| Ink | 57073 | 0x112908dac86e20e7241b0927479ea3bf935d1fa0 |
| Monad | 143 | 0x0d97dc33264bfc1c226207428a79b26757fb9dc3 |
permitData/swap| Received WETH instead of ETH on L2 | Check and unwrap WETH after swap (see WETH Handling on L2s) |
| 429 Too Many Requests | Implement exponential backoff and add delays between batch requests (see Rate Limiting) |
| 415 on OPTIONS preflight / CORS error | Set up a CORS proxy (see CORS Proxy Configuration in Browser Environment Setup) |
| walletClient is undefined when wallet is connected | Use getWalletClient() from @wagmi/core instead of the useWalletClient() hook (see wagmi v2 Integration Pitfalls) |
| "Please provide a chain with the chain argument" | Pass chainId to getWalletClient(config, { chainId }) |
| Chain mismatch error on swap | Call switchChain() before getWalletClient() (see wagmi v2 Integration Pitfalls) |
/swapOmit permitData for UniswapX routes — see Signing vs. Submission |
signature and permitData must both be present | Including only one Permit2 field (CLASSIC routes only) | Include both or neither for CLASSIC; omit permitData for UniswapX |