# Money and amounts

Getting money encoding wrong is the failure mode that costs real money and is discovered
late. The API is designed so that the correct thing is available on every response — but
it is available *alongside* the convenient thing, and it is easy to reach for the wrong
one.

## The rule

Every monetary value in a response is returned **twice**:

- a human-readable **decimal string** — `"1.5"`, in the asset's major units, and
- an exact **integer base-unit string** in a sibling field suffixed `_base` —
  `"1500000"`, in sun or the token's own smallest unit.

```json
{
  "asset_id": "usdt_trc20",
  "amount": "1.5",
  "amount_base": "1500000"
}
```

**Compute with `_base`. Display `amount`.** That is the whole rule.

`_base` is a *string* rather than a number because base units can exceed
`Number.MAX_SAFE_INTEGER` — a balance of ten billion USDT is `10000000000000000` base units,
already past the point where IEEE-754 doubles stop being exact. Parse it into
your language's big-integer type (`BigInt` in JavaScript, `int` in Python, `decimal.Decimal`
or a 128-bit integer elsewhere) and never let it become a float, not even in transit
through `JSON.parse`.

```js
// wrong — silently lossy at scale
const total = Number(a.amount) + Number(b.amount);

// right
const total = BigInt(a.amount_base) + BigInt(b.amount_base);
```

The decimal string exists for humans and for logs. It is exact as written, but the moment
it passes through a binary float it stops being exact, and the error compounds across a
ledger.

## Where the `_base` twin is missing, and why

There are two systematic exceptions, and they are worth understanding rather than
working around.

**The USD figures on `GET /v1/users` have no `_base` twin.** `balance_usd`, each
`balances[].usd` and `card_balance` on that list are display estimates from the price
feed — they exist so a dashboard can sort and eyeball, not so a ledger can reconcile. Use
`GET /v1/users/{externalId}/balances` for settlement figures.

**An exchange quote's buy side has no `*_base` field.** On `POST /v1/exchange/quote`, the
sell side is an asset the platform custodies, so `sell_amount` / `sell_amount_base`,
`fee` / `fee_base` and `send_amount` / `send_amount_base` all come in pairs as usual. The
buy side does not: `buy_asset` can be any of thousands of coins routed through the
exchange, on chains the platform does not model. Without owning that chain's decimals, the
platform cannot honestly produce a base-unit integer for it — so it publishes the decimal
estimate and stops, rather than inventing a scale.

Contrast this with **swaps** (`POST /v1/swap/quote`), where both sides are assets the
platform holds. There, every amount does carry its exact `*_base` twin.

So: on an exchange, treat the buy-side numbers as **indicative decimals for display**, and
never as a settlement figure. The authoritative outcome of an exchange is what the order
read reports after it settles, not what the quote estimated.

## Amounts you send in

Amounts in *request* bodies are decimal strings in major units, and they are validated
strictly.

**More decimal places than the asset supports is a refusal, not a rounding.**
`"15.1234567"` on a six-decimal asset such as `usdt_trc20` is refused: `400 invalid_amount`
on withdrawals and swaps, `400 invalid_request` on card
amounts. Nothing is truncated and nothing is rounded up. This is the property
that makes the API safe to build a ledger on: **the amount you send is always exactly the
amount that moves.**

Read each asset's `decimals` from `GET /v1/assets` and validate on your side before you
send, so a user's typo becomes a form error rather than a `400` on a payout path.

## Whole-cent rules on cards

Card money is denominated in fiat rather than in an asset's own units, and it counts in a
whole minor unit.

**Cards settle in USD cents.** Card money settles in USDT (`usdt_trc20`), with
1 USDT = 1 USD, and USD cards only. `initial_amount` on `POST /v1/cards` is an opening
balance in USD in **whole cents** — two decimal places, no more; a sub-cent opening balance
is refused with `400 invalid_request`, as is one below the product's `min_opening_balance`
(the message names the required amount). Send top-up and withdraw amounts in whole cents
too: the rail bills in cents and our percentage fees are rounded **up** to the cent, so a
sub-cent tail buys nothing.

Read `min_opening_balance` from `GET /v1/cards/products` on every issue. Do not hard-code
it: it moves with platform policy *and* with the issuer's own floor, and it is computed per
provider rather than per product. Where a product also publishes a higher
`active_min_limit`, open the card with at least that much or it can sit at `issuing` until
you top it up.

## Fees, and which side of the amount they sit on

Fees are set by the operator: a platform-wide schedule applies to every merchant unless
your account has been given its own rate, and a rate can be `0`. Read the effective
schedule from `GET /v1/fees` — it returns yours. Where a fee lands differs by operation,
and the difference matters for what you quote your user:

| Operation | Fee direction |
| --- | --- |
| **Deposit** | Free. `amount_credited = amount`, less only your own `merchant_fee` if you charge one |
| **Withdrawal** | Charged **on top**: the hold is `amount + fee + platform_fee`, and `amount` is what the recipient receives. `fee` is the live network cost of the send, gas included; `platform_fee` is the flat platform fee. `GET /v1/withdrawal-fee-quote` shows both |
| **Swap / exchange** | Taken **out of** `sell_amount` — `send_amount = sell_amount − fee` is published so the numbers add up |
| **Card issue / load** | **On top**, from the cardholder: the opening balance or load amount plus our fee, gas and your own fee — `total` on the quote, `issue_total` on `GET /v1/cards/products` |

For a payout, `GET /v1/withdrawal-fee-quote` returns the exact figures before you commit,
so you can show your user a total they will recognise on the statement.

## A short checklist

- Parse every `*_base` field as a big integer, never a float.
- Sum, compare and reconcile on `*_base` only.
- Format for display from the decimal string, or from `_base` with the asset's `decimals`.
- Validate outbound decimal places against `GET /v1/assets` before sending.
- Treat exchange buy-side amounts as display estimates; poll the order for the truth.
- Never persist a monetary value in a floating-point column.
