跳转至

兑换端点

请求兑换报价

POST /swap/quote

身份验证: 无

请求体

{
  "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。

批量请求兑换报价

POST /swap/quote/batch

鉴权: 无

在一次往返请求中获取多达 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。

执行兑换

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。