cryptoinfraDocs

#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.

Open as MarkdownFor AI assistants

#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:

OperationFee direction
DepositFree. amount_credited = amount, less only your own merchant_fee if you charge one
WithdrawalCharged 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 / exchangeTaken out of sell_amount — send_amount = sell_amount − fee is published so the numbers add up
Card issue / loadOn 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.