> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.walletstech.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.walletstech.com/_mcp/server.

# Buying and selling

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 send       | You fix               | The service derives |
| -------------- | --------------------- | ------------------- |
| `fiatAmount`   | how much fiat moves   | how much crypto     |
| `cryptoAmount` | how much crypto moves | how much fiat       |

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

```bash
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:

```json
{
  "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.

| Status      | Meaning                                       | What to do                                 |
| ----------- | --------------------------------------------- | ------------------------------------------ |
| `open`      | a quote, nothing committed                    | approve it, cancel it, or let it lapse     |
| `approved`  | accepted at the confirmed price, being worked | poll `/trade`                              |
| `executing` | placed; outcome not yet confirmed             | poll `/trade` — **never approve again**    |
| `completed` | filled                                        | done; the amounts are what actually traded |
| `failed`    | rejected before anything traded               | read `failureReason`, quote again          |
| `expired`   | lapsed before approval                        | quote again                                |
| `cancelled` | you dropped it                                | quote 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 `executing` → `409 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`.