# Swaps and exchange

A swap and an exchange share one order shape and one fee, and differ in where the money lands:

| | Swap | Exchange |
| --- | --- | --- |
| Sell side | one of our assets, from the user's balance | the same |
| Buy side | one of our assets — named by our `asset_id` | any of thousands of coins — named by the exchange's own identifier |
| Where it lands | credited to the user's balance | paid out to an address you supply |
| Amounts on the buy side | exact, with `*_base` twins | decimal estimates only — no `*_base` |
| Endpoints | `POST /v1/swap/quote`, `POST /v1/swap/orders` | `POST /v1/exchange/quote`, `POST /v1/exchange/orders` |

Both are read with `GET /v1/swap/orders/{id}`, and **neither emits a webhook**. Poll.

## Swap

1. **Quote.** `POST /v1/swap/quote` with `sell_asset_id`, `buy_asset_id` and `amount`. It
   is indicative: no reservation, no hold, re-quoted at create time — `expires_at` is when
   to ask again, not a price lock. `receive_estimate` is the estimate, `receive_minimum`
   the floor this route guarantees; the user is credited **what actually arrives**, which
   is why both are published.
2. **Create.** `POST /v1/swap/orders` with the same fields plus `user_id` (and an
   `idempotency_key`, optional but recommended). Answers `201` with the order.
3. **Poll** `GET /v1/swap/orders/{id}` while `status` is not terminal:
   `pending` → `sent` → `swapping` → `completed`, or `refunded` / `failed`.
   `GET /v1/swap/orders/{id}/detail` adds the addresses, the send leg and a timeline.

The output lands on the user's own static wallet address and is credited to their balance; there
is no destination to supply and none to validate.

**USDT ⇄ TRX is traded on-chain, on TRON itself.** The trade is signed by the user's own
address and pays back to it, so it prices small amounts at the market rate — a few dollars
swap as well as thousands. Its order goes `pending` → `sent` → `completed` within a minute or
two; there is no `swapping` step. `receive_minimum` is a hard floor: if the market moves
further before the trade lands, the trade reverts, the order ends `failed` and the held amount
returns to the balance in full — nothing is traded and nothing is charged. `received` is what
arrived, to the unit.

## Exchange

1. **Read the catalogue.** `GET /v1/exchange/assets` lists what can be bought —
   thousands of coins, refreshed hourly — and `GET /v1/exchange/chains` where they can be
   paid to. `asset` there is the exchange's identifier (`SOL.SOL`,
   `ETH.USDC-0xa0b8…`), and it is exactly what `buy_asset` expects. Take it from the
   catalogue; a value we cannot route is refused outright.
2. **Quote.** `POST /v1/exchange/quote` with `sell_asset_id`, `buy_asset`, `amount` and
   `destination_address` — the quote validates the address for that coin's chain.
3. **Create.** `POST /v1/exchange/orders` with the same fields plus `user_id`. `201`.
4. **Poll** `GET /v1/swap/orders/{id}` — the same read, the same statuses. `GET /v1/swap/orders`
   lists both kinds.

Treat the buy-side numbers of an exchange as **display estimates**: the platform does not
model that coin's chain, so it publishes the decimal it was given and stops rather than
inventing a base-unit scale. The authoritative outcome is the settled order, not the quote.

## The fee

**A percentage of the amount, with a dollar minimum, plus the order's own network cost**, on the sell side.
It is taken *out of* `amount` — `send_amount = amount − fee − merchant_fee` is published so the
numbers add up — and `fee` already includes the network part, shown on its own as
`network_fee`. Today's figures are on the [price sheet](fees#swaps-and-exchanges); your own
fee, if you set one, is `merchant_fee` on the quote and the order.

An amount the fee would swallow is refused with `400 invalid_amount`, and the message says
where swaps start right now.

## What can go wrong

| Answer | Meaning |
| --- | --- |
| `400 invalid_pair` | one side is not swappable — `swappable: false` on `GET /v1/assets`; leave it out of your picker |
| `400 no_route` | the exchange cannot carry this pair right now |
| `400 invalid_amount` | more decimal places than the sell asset supports, or too small to cover the fee — the message names today's smallest amount |
| `409 insufficient_balance` | the balance cannot cover `amount` |
| `503 swap_disabled`, `swap_unavailable`, `exchange_error` | a dependency is down; retry later, unchanged |

## Checklist

- [ ] Buy-side identifiers for an exchange taken from `GET /v1/exchange/assets`, never constructed
- [ ] `receive_minimum` shown as the floor, `receive_estimate` as the estimate
- [ ] Orders polled on `GET /v1/swap/orders/{id}` — no webhook exists
- [ ] Exchange buy-side amounts treated as display estimates until the order settles
