Two execution paths: execute immediately, or lock a quote before executing. Settlement is atomic, with no intermediate state.
Internal Conversions
GET /v1/exchange/pairs ← 开了哪些方向
POST /v1/exchange/quotes ← 报价(可选锁价)
POST /v1/exchange/orders ← 成交
Configured by direction, not by currency pair
USDT → USD and USD → USDT are two independent records. Their fees need not be symmetric, and only one direction may be enabled. A direction absent from the list is not enabled.
⚠ Do not model this as a single currency pair with a direction flag. That would give you only one fee configuration,
whereas we can configure the two directions differently.
Choose an execution path to match your interface
A. Immediate execution without a locked quote
The quote is for display. When the order is placed, the amounts are recalculated using the price at execution time.
POST /v1/exchange/quotes { from_asset, to_asset, from_amount }
POST /v1/exchange/orders { from_asset, to_asset, from_amount }
This suits a single-screen interaction where the user enters an amount and confirms immediately.
⚠ Without a price lock,
quote_ttl_secis indicative only, not a commitment.The response does not contain a
quote_idfield. The field is absent, rather thannull,so you can distinguish “no lock requested” from “lock requested but not provided”.
B. Execution with a locked quote
Lock the quote when the user needs a confirmation screen, such as “Exchange 1000 USDT for 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_…" }
The lock lasts quote_ttl_sec seconds, configured per pair with a database-enforced range of 30–600 seconds. Execution after that deadline is always rejected; the order is never silently executed at a new price.
⚠⚠ The
quote_idis also the order ID. After execution, theidreturned byPOST /v1/exchange/ordersis identical character for character, and
GET /v1/exchange/orders/{id}accepts it too.This is useful when a request times out on the locked-quote path: if you already have the ID,
query it directly to find out whether the order completed.
⚠ Locking a quote creates a real order row in the
quotedstate. Do not lock every quote just for display:doing so fills your order list with
quotedrows that are never executed.A locked quote also gives you a free option for N seconds, executable only when the price suits you.
We monitor for abuse.
Idempotency: the two endpoints have different requirements
| Endpoint | Idempotency key | Reason |
|---|---|---|
POST /v1/exchange/quotes | Optional | Quoting itself does not require idempotency. Providing a key deduplicates the request: a retry after timeout returns the same locked quote instead of creating two quotes when only one will be used. |
POST /v1/exchange/orders | Required; reuse the same key for retries | This endpoint moves funds. |
Execution is atomic
The conversion is complete when order creation returns. There is no “processing” state, and therefore no cancellation, execution retry or refund workflow.
⚠ A network timeout from
POST /v1/exchange/ordersmeans the outcome is unknown, not that execution failed.Retry with the same idempotency key, or query
quote_iddirectly on the locked-quote path.Treating an unknown outcome as a failure can cause the same conversion to execute twice.
Which asset pays the fee?
The quote includes fee_amount and fee_asset. The latter identifies the asset from which the fee is charged.
⚠ Do not calculate the fee yourself as amount × rate. Charging on the source side and charging on the destination side
produce different results, and users will reconcile against the amounts you display.
Use
fee_amount/fee_assetdirectly.
Amounts are fixed-point strings. The precision on each side comes from ledger_scale_from / ledger_scale_to. The two assets often have different precision, so do not reuse one scale for both.