兑换端点¶
请求兑换报价¶
身份验证: 无
请求体¶
{
"from_token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
"to_token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
"from_amount": "1000000",
"owner_address": "0x...",
"recipient": "0x...",
"expiration": 1713254400,
"gas_mode": "receive_less"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
from_token | address | 是 | ERC-20 输入代币地址 |
to_token | address | 是 | ERC-20 输出代币地址 |
from_amount | string | 是 | 正 uint256 字符串形式的原始输入金额 |
owner_address | address | 是 | 负责签名并支付兑换的地址 |
recipient | address | 是 | 最终接收输出代币的地址。可以与 owner_address 不同——设置为任意地址即可将输出代币发送给第三方钱包。该值会被锁定在已签名的 Intent 中,合约会强制要求最后一段腿恰好将输出支付到此地址。 |
expiration | integer | 是 | 作为 Intent 截止时间使用的 Unix 时间戳,必须满足 now < expiration <= now + 365 天 - 300 秒 |
gas_mode | string | 否 | receive_less 或 pay_more;默认值为 receive_less |
from_token 和 to_token 不能相同。
如果 from_amount 低于 GET /tokens 给出的代币最低成交门槛,接口会返回 HTTP 400,并在结构化 detail.code 中给出 AMOUNT_BELOW_MIN,同时返回必须满足的原始值和十进制展示值。
响应¶
{
"uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
"route_params": {
"taker": "0x...",
"inputToken": "0x965d4b4546716e416e950bc30467d128455d2d0e",
"outputToken": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
"maxInputAmount": "1000000",
"minOutputAmount": "923450",
"recipient": "0x...",
"initialDepositAmount": "1000000",
"uuid": "6431994229952211760403847975151123456789012345678901234567",
"deadline": 1713254400
},
"fee_breakdown": {
"gas_cost_usd": "0.12",
"gas_cost_from_token": "0.120000"
},
"expires_at": 1713250830,
"permit": {
"permit_supported": true,
"permit_required": true,
"token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
"spender": "0x...",
"owner": "0x...",
"value_raw": "1000000",
"current_allowance_raw": "1000000",
"nonce": 4,
"suggested_deadline": 1713254400,
"domain": {
"name": "USD Coin",
"version": "2",
"chainId": 11155111,
"verifyingContract": "0x965d4b4546716e416e950bc30467d128455d2d0e"
},
"eip712": {
"domain": {
"name": "USD Coin", "version": "2",
"chainId": 11155111,
"verifyingContract": "0x965d4b4546716e416e950bc30467d128455d2d0e"
},
"primaryType": "Permit",
"types": {
"Permit": [
{ "name": "owner", "type": "address" },
{ "name": "spender", "type": "address" },
{ "name": "value", "type": "uint256" },
{ "name": "nonce", "type": "uint256" },
{ "name": "deadline", "type": "uint256" }
]
},
"message": {
"owner": "0x...",
"spender": "0x...",
"value": "1000000",
"nonce": 4,
"deadline": 1713254400
}
}
}
}
响应说明¶
uuid是传给POST /swap的报价记录 ID。route_params.uuid是绑定到签名 Intent 的组合 uint256。expires_at的生命周期很短;当前实现中报价大约只保存 30 秒。fee_breakdown仅覆盖 gas 成本;底层定价输入不属于响应契约的一部分。initialDepositAmount > 0时permit非空。若permit_supported=true,签署返回的eip712消息。nonce 读取链上值,报价不会预留一组未来 nonce;提交时重新检查链上 nonce,并原子认领该签名 nonce。nonce 已改变或已被认领时可能需要重新报价。纯权益报价(initialDepositAmount=0)的permit=null。permit_supported = false表示输入代币不支持 EIP-2612 — 改为POST /approve流程(该分支不会带eip712字段)。
Gas 模式¶
| 模式 | 行为 |
|---|---|
receive_less | 保持输入金额不变,由 gas 减少输出 |
pay_more | 保持输出目标不变,由输入预算吸收 gas |
Note
Intent 签名无效或缺少必填 permit 字段会在认领报价之前失败。并发或重复的有效提交按下方身份规则处理,不会统一返回 410。
Note
某些报价会故意返回 route_params.minOutputAmount = "0",表示在该请求规模下没有可执行路径。这类报价仅供参考,POST /swap 会拒绝它们。
Note
不支持 permit 且 allowance 不足时,使用交易辅助接口向报价指定的 spender 授权,本地签名后通过 POST /tx/send 广播。等待确认并获取新报价后再签名/提交 Swap。已有足够 allowance 时无需再次 approve。
批量请求兑换报价¶
鉴权: 无
在一次往返请求中获取多达 50 个兑换报价。适合做市商或需要同时为多个交易对定价的交易者 —— 把 N 次串行的 /swap/quote 调用合并成一次 HTTP 请求,服务端以受限并发量处理。
请求体¶
{
"quotes": [
{
"from_token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
"to_token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
"from_amount": "1000000",
"owner_address": "0x...",
"recipient": "0x...",
"expiration": 1713254400,
"gas_mode": "receive_less"
}
]
}
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
quotes | array | 是 | 1–50 条报价请求,每条与 POST /swap/quote 请求体格式一致 |
每条报价的字段校验与 POST /swap/quote 完全一致。如果任意一条未通过 Pydantic 校验(无效地址、自交易、错误的 gas_mode 等),整体请求返回 422 且不会进入业务处理。运行时错误(预言机、订单核算、超时)以单条 error 包装返回 —— 整体请求仍返回 200。
响应¶
{
"items": [
{
"ok": true,
"quote": {
"uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
"route_params": { "...": "..." },
"fee_breakdown": { "gas_cost_usd": "0.12", "gas_cost_from_token": "0.120000" },
"expires_at": 1713250830,
"permit": null
},
"error": null
},
{
"ok": false,
"quote": null,
"error": {
"rejectionCategory": "no_liquidity",
"message": "No liquidity"
}
}
]
}
items[i] 与 quotes[i] 一一对应 —— 顺序相同,长度相同。每条要么是成功报价 (ok: true),要么是带类型的错误包装 (ok: false)。
错误包装¶
rejectionCategory 是字符串。上游类型化错误码原样转发,包括报价专用错误。与执行共用的 AMOUNT_BELOW_MIN、NO_LIQUIDITY 等可共用处理逻辑,同时必须兼容未知错误码。
rejectionCategory | 触发条件 |
|---|---|
| 类型化错误码 | 原样转发;报价专用错误码不一定属于执行接口的错误集合。 |
SWAP_GAS_PRICE_UNAVAILABLE | 缺少用于 gas 成本折算的有效美元价格。批量外层仍为 HTTP 200,该项 ok:false,message 可为空;稍后重试。 |
no_liquidity | 定价返回旧版字符串形式的无流动性(小写变体,等价于类型化的 NO_LIQUIDITY) |
invalid_quote | 定价引擎其他未携带类型化错误码的 4xx |
upstream_unavailable | 无类型码的上游 5xx / 账务服务不可达;类型码优先。 |
quote_timeout | 单条报价计算超过服务器端预算(默认 10 秒) |
internal_error | 未处理的服务端错误 |
单条报价或 Exact Output 报价在 gas 定价不可用时可返回通用 HTTP 503,不能要求清洗后的响应一定带 SWAP_GAS_PRICE_UNAVAILABLE。批量和签名还须满足整体请求体上限。
运维须知¶
- 上限: 每次请求 50 条。超出会在请求校验阶段返回
422。剩余交易对请分批提交。 - 并发: 服务端将单条计算的并发量限制在固定值(默认 20),即使一次提交 50 条,也最多有 20 条同时与订单核算交互。
- 单条超时: 每条报价的计算时间被独立限制,单个慢交易对不会拖累整批次。慢条返回
quote_timeout,其他仍可成功。 - 报价快照: 各交易对独立计算,不存在跨交易对的统一快照。如需跨对原子性定价,本接口不适用。
- 无批次幂等: 每条成功结果都生成新的报价
uuid。重试同样的批次会产出新的 UUID。 - 每条返回的报价 UUID 相互独立 —— 与
POST /swap/quote一样,各自配上签名 Intent 提交到POST /swap。
执行兑换¶
身份验证: 请求体中携带的 EIP-712 Intent 签名
请求体¶
{
"uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
"signature": "0x...",
"permit_signature": "0x...",
"permit_deadline": 1713254400
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
uuid | string | 是 | 由 POST /swap/quote 返回的报价 ID |
signature | hex string | 是 | 对 quote.route_params 的签名 |
permit_signature | hex string | permit_required && permit_supported 时 | 签署返回的 permit.eip712,支持 64/65 字节签名。不支持 permit 时完成 approve 并省略此字段。 |
permit_deadline | integer | 与 permit_signature 配套 | permit 签名中的截止时间;使用报价建议值,两个字段一起提交。 |
响应¶
{
"success": true,
"trade_id": "85c92fcb-21b9-43ba-bb36-01d7b21eaa8d",
"status": "pending",
"fee_breakdown": {
"gas_cost_usd": "0.12",
"gas_cost_from_token": "0.120000"
}
}
trade_id 是后续可通过 GET /orders/{order_id} 查询,或通过 GET /orders?type=swap 过滤的订单 ID。
fee_breakdown 为可选字段,在部分失败或降级响应路径中可能省略。
要签名的 Intent 类型¶
const types = {
Intent: [
{ name: 'taker', type: 'address' },
{ name: 'inputToken', type: 'address' },
{ name: 'outputToken', type: 'address' },
{ name: 'maxInputAmount', type: 'uint256' },
{ name: 'minOutputAmount', type: 'uint256' },
{ name: 'recipient', type: 'address' },
{ name: 'initialDepositAmount', type: 'uint256' },
{ name: 'uuid', type: 'uint256' },
{ name: 'deadline', type: 'uint48' }
]
};
请严格按返回值原样签名 quote.route_params,不要在客户端重新生成或标准化字段。
错误情况¶
| 状态码 | 原因 |
|---|---|
400 | 请求无效、签名不匹配、缺少必填 permit 字段,或报价不可执行(minOutputAmount = 0) |
409 | 准入前 permit nonce 过时可能返回字符串 detail,且尚未消费报价。其他报价冲突可能使用类型化错误。请检查响应与当前订单状态,不能假定所有 409 的原因或报价消费状态相同。 |
410 | 报价不存在、未认领报价过期或结果已不再保留;已保留的相同成功请求仍可返回缓存结果 |
429 | 触发钱包级速率限制 |
503 | 服务暂时不可用;请按 Retry-After 重试 |
报价展示字段¶
单条和批量成功报价还可能包含 route_metadata.leg_count 和 quote_breakdown:
quote_breakdown 字段 | 说明 |
|---|---|
gas_mode、input_token、output_token | gas 选择与代币身份 |
input_amount_before_gas、input_amount_after_gas | gas 前后输入数量,输入代币 raw 单位 |
output_amount_before_gas、output_amount_after_gas | gas 前后输出数量,输出代币 raw 单位 |
gas_cost_input_token、gas_cost_output_token | 以对应代币 raw 单位表示的 gas 成本 |
gas_cost_usd | 美元成本,十进制字符串 |
这些是展示估计值,不是确认结算金额。请原样签名 route_params。批量报价按请求中的报价条数计费;强制公共限流开启时,quote 组预算为每秒 50 条。
指定必须收到的输出金额时,请使用独立的 Exact Output API。在现有输入金额报价上设置 pay_more,不等于使用该接口契约。
Swap 错误包装¶
类型化提交失败使用 { "detail": { "detail": "Swap execution failed", "error_code": "NO_LIQUIDITY" } }。错误码包括 ALLOWANCE_INSUFFICIENT、INTENT_DEADLINE_EXPIRED、SLIPPAGE_EXCEEDED、NO_LIQUIDITY、QUOTE_STALE、AMOUNT_BELOW_MIN、STP_BLOCKED、COMPLIANCE_BLOCKED、TRANSIENT_SETTLEMENT_FAILURE。签名缺失、请求模型无效等预检失败也可能使用字符串 detail。批量报价的 rejectionCategory 是单项报价错误,不证明兑换已提交。
客户端应按结构化 error_code 判断错误类别,不要按展示文本 detail 分支。各码含义参见订单错误说明;兑换重试必须遵循下方的请求身份与结果核对规则。
提交身份与重试¶
首次提交前持久化 uuid、signature、permit_signature、permit_deadline。这些值共同标识请求;重试时保留原签名编码与 permit 值。
| 已有报价状态 | 相同请求 | 不同请求 |
|---|---|---|
| 成功结果仍被保留 | HTTP 200 返回原结果,不执行第二次 | 409 冲突 |
| 正在提交 | 已知结果可恢复时返回结果,否则返回 409 进行中 | 409 冲突 |
| 已拒绝 | 保留期内返回 409 与拒绝码,合规拒绝为 403 | 不会成为新报价 |
| 不存在或认领前过期 | 410 | 410 |
超时或 503 后,重试完全相同的签名请求以恢复已保存结果。响应给出 trade_id 后,使用该 ID 和所有者 API Key 查询 GET /orders/{order_id}。“正在提交”的 409 不表示终态失败。只有确认前次已拒绝或不能执行后,才重新报价。保留期有限,清理后不保证无限期重放。Exact Output 有独立的状态接口,以 quote_uuid 查询。
区分三种 Swap 标识¶
| 标识 | 用途 |
|---|---|
报价响应的 uuid | 作为 POST /swap 请求的 uuid,标识已保存报价与重试请求 |
route_params.uuid | 在 Intent 中签名的复合 uint256 |
执行响应的 trade_id | 已持久化订单 ID,用于 GET /orders/{order_id} 与 /fills/{order_id} |
持久化订单 ID 会另外生成。普通 Swap 没有 Exact Output 那样按报价查询状态的独立接口;首次响应丢失时,应通过相同的 POST /swap 请求恢复,不能将报价 ID 填入订单查询 URL。