Buying and selling

Quote a fiat↔crypto order, let the user accept it, then watch it settle.

View as MarkdownOpen in Claude

Trading is a two-step flow, deliberately. A quote is not an order, and an order is not a fill.

/order or /sell /trade/approve poll /trade
─────────────► open ─────────────► approved ─────────────► completed
│ │
│ executing
▼ │
expired · cancelled └──────────────► failed

1. Quote

/order prices a buy, /sell prices a sale. Both take exactly one amount and derive the other:

You sendYou fixThe service derives
fiatAmounthow much fiat moveshow much crypto
cryptoAmounthow much crypto moveshow much fiat

Sending both, or neither, is a 400 Provide exactly one of fiatAmount or cryptoAmount.

curl -sX POST "$BASE_URL/order" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"userId":"60f7c2d3e4f5a6b7c8d9e0f1","assetId":1,"fiat":"USD","fiatAmount":"100.00"}'

The response is a trade in status open, with a tradeId and — under quote — the numbers you show the user:

{
"result": {
"tradeId": "6a1f0c2d3e4f5a6b7c8d9e0f",
"side": "buy",
"status": "open",
"assetId": 1,
"symbol": "BTC",
"fiat": "USD",
"cryptoAmount": "0.001546",
"fiatAmount": "100",
"price": "64665.47",
"quote": {
"cryptoAmount": "0.001546",
"fiatAmount": "100",
"price": "64665.47",
"expiresAt": "2026-08-16T10:31:00.000Z"
},
"createdAt": "2026-08-16T10:30:00.000Z",
"approvedAt": null,
"completedAt": null
},
"requestId": "b7a1f0c2-3d4e-4a5b-9c6d-7e8f90a1b2c3"
}

Nothing is committed yet. A quote you never approve simply lapses at quote.expiresAt — roughly a minute out. You can drop it early with /trade/cancel, but you never have to.

2. Approve

/trade/approve is the committing step, and it is the one place a price can change:

⚠️ The quote is indicative. Approving prices the order again — holding the leg you fixed and moving the derived one — and that is the price that binds. Show the user the approval numbers before you tell them the order is done.

The confirmed figures come back on the top-level cryptoAmount / fiatAmount / price; quote still holds the original, so the difference is always yours to display.

Approving a quote that has already lapsed marks it expired and answers 409 Quote has expired — request a new one.

3. Settle

An approved order reaches completed when it fills. Depending on the asset it may settle inside the approve call or shortly after, so treat approved and executing as work in progress and poll /trade until the status is terminal.

StatusMeaningWhat to do
opena quote, nothing committedapprove it, cancel it, or let it lapse
approvedaccepted at the confirmed price, being workedpoll /trade
executingplaced; outcome not yet confirmedpoll /tradenever approve again
completedfilleddone; the amounts are what actually traded
failedrejected before anything tradedread failureReason, quote again
expiredlapsed before approvalquote again
cancelledyou dropped itquote again

Retrying safely

The trade itself is the idempotency key, so a repeated /trade/approve never trades twice:

  • already completed → the original result is replayed unchanged.
  • still executing409 This trade is already being filled — poll /trade before retrying.

The one case that needs care is an ambiguous failure. A 504 — or a 502 that is not Order rejected: … — means the outcome is unknown, and the trade stays executing on purpose rather than being marked failed. Poll /trade to find out what happened. Re-approving is the only action that could double up, which is exactly why the service refuses it.

A 502 Order rejected: … is different: nothing traded, the trade is failed, and a fresh quote is the way forward.

Money and rounding

Amounts are decimal strings, never floats — fiat to 2 places, crypto to the asset’s own decimals. Prices are fiat per one whole unit of the asset.

Responses show every amount and price to at most 6 decimal places, with trailing zeros trimmed — a fiat leg of exactly one hundred comes back as "100", not "100.00", so parse these as decimals rather than matching on their text. The extra places are cut, never rounded, so a displayed figure is never larger than the one that settles — on an 18-decimal asset the amount you show can be a dust fraction below the amount actually traded.

Rounding is directional and always conservative: the fiat leg rounds against the user (up on a buy, down on a sell) and the crypto leg always rounds down. A rounded quote can therefore never be filled short. An amount so small that its counter-leg rounds to zero is refused with 400 Quoted amount rounds to zero — increase the amount.

What trading does not do

A completed trade records what was traded and at what price. It does not move coins. Crediting or debiting the user’s wallet is a separate step on your side — a /send, or your own ledger. This is deliberate: it means a trading failure can never leave custody half-moved.

Not every asset is tradable, and not every asset is enabled for every api client — an asset you cannot trade is a 403.