两条路:一步成交,或先锁价再成交。成交是原子的,没有中间态。
站内兑换
GET /v1/exchange/pairs ← 开了哪些方向
POST /v1/exchange/quotes ← 报价(可选锁价)
POST /v1/exchange/orders ← 成交
按方向配,不是按币对
USDT → USD 与 USD → USDT 是两条独立记录:费率不必对称,可以只开一个方向。清单里没有的方向就是没开。
⚠ 别把它建模成一个「币对」再加一个方向位。那样在你这边就只有一套费率,
而我方两个方向可以配成不一样的。
两条成交路径,按你的界面形态选
A. 一步成交(不锁价)
拿报价只是给用户看一眼,下单时按成交那一刻的价重新算。
POST /v1/exchange/quotes { from_asset, to_asset, from_amount }
POST /v1/exchange/orders { from_asset, to_asset, from_amount }
适合「输入金额、立刻确认」这种一屏完成的交互。
⚠ 不锁价时
quote_ttl_sec只是个参考值,不是承诺。响应里没有
quote_id字段(是字段不存在,不是null——这样你能分清「我没要锁价」和「我要了但没给我」)。
B. 锁价成交
要给终端用户一屏确认(「1000 USDT 换 999.5 USD,确认吗」)就锁价:
POST /v1/exchange/quotes { …, "lock": true }
→ { quote_id: "exc_…", quote_expires_at: "…", quote_ttl_sec: 120 }
POST /v1/exchange/orders { "quote_id": "exc_…" }
锁住的窗口是 quote_ttl_sec 秒(我方按币对配,库级限制在 30–600 秒)。过了这个点成交一律被拒,不会按新价悄悄成交。
⚠⚠
quote_id就是这一单的订单号。 成交后POST /v1/exchange/orders返回的
id与它逐字符相同,GET /v1/exchange/orders/{id}也认它。这带来一个很有用的性质:锁价请求超时也不要紧 ——
你手上已经有那个 id 了,直接查它就知道这一单到底成没成。
⚠ 锁价会建一张真的
quoted订单行。别为「让用户看一眼」就无脑锁:一是你自己的订单清单会灌满从未成交的
quoted行;二是一份锁定的报价对你就是一个 N 秒的免费期权(只在对你有利时才成交),
我方对滥用是有观测的。
幂等键:两个端点的规则相反
| 端点 | 幂等键 | 为什么 |
|---|---|---|
POST /v1/exchange/quotes | 可选 | 报价本身没有幂等语义。带了就顺手去重 —— 超时重发拿回同一份锁价,而不是攒出两张只有一张会用掉的报价单 |
POST /v1/exchange/orders | 必须,且重试沿用同一把 | 它动钱 |
成交是原子的
下单返回的那一刻钱已经换完了。没有「处理中」这个状态,所以也没有「取消」「重试」「退款」。
⚠ 这意味着
POST /v1/exchange/orders的网络超时是「结果不明」,不是失败。用同一把幂等键重试,或者(锁价那条路上)直接查
quote_id。当失败处理的表现是同一笔换了两次。
手续费在哪一侧
报价里有 fee_amount 和 fee_asset —— 后者告诉你费用是从哪一侧收的。
⚠ 别自己按「金额 × 费率」算。 从付出侧收和从换得侧收,
算出来的数不一样,而用户会拿你的数去对账。
直接用
fee_amount/fee_asset。
金额都是字符串定点,两侧的位数分别由 ledger_scale_from / ledger_scale_to 给出 —— 两种资产的位数往往不同,别复用一个。