Money representation
Amounts are decimal strings on the wire and integer minor units internally. Never a floating-point number, in either direction.
Every amount this API accepts or returns is a string. Not a JSON number.
{ "kesAmount": "10000.00", "usdtAmount": "100.000000" }Never parse an amount into a float
JSON.parse converts a JSON number to an IEEE-754 double before any code of
yours runs. By the time you could check it, the rounding has already
happened. That is why these fields are strings on the wire: it makes the
unsafe operation something you have to choose, rather than something the
parser does to you.
The two units
Internally, and in any field named Minor or Micro, money is an integer
count of the smallest unit. There are exactly two:
| Currency | Unit | Factor | 1 unit is |
|---|---|---|---|
| KES | cents (minor) | 100 | KES 0.01 |
| USDT | micro | 1,000,000 | 0.000001 USDT |
USDT's factor is 1e6 because Tron USDT has six decimal places. It is not 1e18 — that is a different chain's convention and using it would misstate every amount by twelve orders of magnitude.
So:
KES 10,000.00is1000000minor units.100.000000 USDTis100000000micro units.
Decimal places are enforced, not rounded
The API validates the shape of the string and refuses anything it cannot represent exactly. It does not round for you.
| Field | Maximum decimal places | Rejected examples |
|---|---|---|
kesAmount | 2 | 10.001, 1e4, +10, 10,000, 0x64, "" |
usdtAmount | 6 | 1.0000001, 1e2, -1, 1 with a trailing space |
The patterns are deliberately strict — no sign, no exponent, no separators, no
whitespace. That strictness is load-bearing rather than fussy: BigInt('0x64')
is 100 and BigInt('') is 0n, so a permissive parser silently produces an
amount nobody typed.
A value with too many decimal places is a 400. Round it yourself, in the
direction your own accounting requires, before you send it.
Working with amounts correctly
Read the string. Do not coerce it.
// Wrong — the rounding already happened.
const kes = Number(order.kesAmount) * 100;
// Right — exact, and it throws rather than silently truncating.
const kesMinor = BigInt(order.kesAmount.replace('.', '').padEnd(/* … */));In practice, use a decimal library (decimal.js, big.js) or your language's
native decimal type, and convert to an integer minor-unit count once at the
boundary.
Do arithmetic in minor units, as integers.
Add, subtract and compare integers. BigInt in JavaScript, int64/bigint
elsewhere. A KES total assembled from cents is exact; one assembled from doubles
is approximately exact, which in a ledger means wrong.
Convert back to a decimal string only to display or to send.
Format from the integer, with a fixed number of decimal places — 2 for KES, 6 for USDT. Format once, at the very last step, so no rounded value ever feeds another calculation.
Compare exchange rates as decimals, never as floats.
rate is also a string. Two rates that differ in the last place are different
rates, and a float comparison will sometimes tell you they are the same.
Amount limits
Your account carries a per-order cap and a rolling 24-hour cap, both denominated in KES minor units. The daily cap is rolling rather than calendar-based: it does not reset at midnight, precisely so that an order cannot be doubled by splitting it across the boundary.
Exceeding either returns 422 with ORDER_EXCEEDS_PER_ORDER_CAP or
ORDER_EXCEEDS_DAILY_CAP. Your specific caps are set on your account — we do not
publish a default here, because the number that matters is yours and — is more
honest than a figure that is not.
The caps are compared against the largest KES an order can cause to move, so a disclosed fee counts towards them. A fee cannot create headroom under a cap.
The daily cap is SHARED across every value path
The rolling 24-hour cap is one ceiling for your orders, your transfers and
your USDT sends together — not a separate allowance per endpoint. A day spent
mostly on transfers leaves correspondingly less room for orders, and a transfer
that would cross the shared ceiling is refused with
TRANSFER_EXCEEDS_DAILY_CAP even if you have placed no orders at all.
USDT sends count towards it at their KES equivalent, valued when the check runs — so the same send can consume slightly different headroom depending on when you make it.
If you are reconciling a refusal you did not expect, add all three together before concluding the cap is wrong.
Summary
- Amounts are strings on the wire, in both directions.
- A quote and an order carry three KES figures — principal, fee, and the total that moves. Reconcile against the total. See Fees.
- KES has 2 decimal places, USDT has 6. Over-precision is a
400, not a rounding. - Convert to integer minor units at your boundary, do all arithmetic there, and format back to a string only to display or send.
- Never let an amount pass through a floating-point number at any point.