Skip to main content

Amounts and Percentages

The partner API (/v2/admin/**, /v2/users/** and webhooks) carries every monetary value as a decimal string: "12.5", not 12.5. Field names and /v2 paths are unchanged. Only the JSON type of the value changed.

Why strings​

A JSON number is read as a 64-bit double by almost every client. A double cannot hold 0.1 exactly and keeps only about 16 significant digits. That was enough to change values silently: a price a user typed was not the price written to the chain, and a balance with 18 decimals read back from the chain came out rounded. A string reaches the server digit for digit. The server keeps it as an exact decimal and converts it to the chain's integer units only at the last step.

Which fields are strings​

A field is a string when it holds money or a rate. A field that counts whole tokens or items stays a JSON number.

FamilyFields (decimal string)
Price objectsamount inside every {id, amount, decimal, symbol, address} object: token token_price, total_value, salable_value, rental_income, oracle per_price, deal and listing price, …
Token valuesalable_percentage
Feesfee value percentages; fee amount and percentage in OTC, secondary-market and purchase breakdowns; total_fee_amount, total_fee_percentage and their buyer/seller variants
Prices and price bandsmin_sell_price, max_sell_price, min_price, max_price, unit_price, price_amount
OTC order rulesabove_amount, below_amount, above_percent, below_percent
Walletsbalances and usd / try valuations in wallet summaries (amount, balance_native, …)
Analyticsvolumes and fee revenue
Currency tokens and nodescurrency-token total_supply; mint, send and withdraw amount; node withdraw amount
Purchase quotesunit price, subtotal, total, seller proceeds
Market datatoken-history start_value, end_value, rate; spot prices; gas fee quotes (safe_gas_price, propose_gas_price, fast_gas_price)

These stay JSON numbers: quantity, max_supply, listed_supply, sold_supply, purchased_supply, min_purchase_quantity, max_purchase_quantity, decimal, page, per_page, type, status, and numeric timestamps.

total depends on where it appears

In a paginated list, total is the number of rows and stays a number. In a purchase quote, total is the amount the buyer pays and is a string.

Sending a value​

Send money as digits with an optional fractional part, for example "1000.000001".

  • No sign (+ or -), no exponent (1e3), no whitespace.
  • No thousands or digit separators (, _ '). The only separator allowed is the decimal point ..
  • At least one digit on each side of the point: "0.5" is accepted, ".5" and "5." are refused.
  • No leading zeros: "0.5" is accepted, "01" is refused.
  • No more decimals than the asset declares. The limit is the decimal of the currency the amount is in. A 6-decimal USDT amount accepts "12.123456" and refuses "12.1234567". The server never rounds a value to make it fit.
  • A fee percentage is a rate from "0" to "100" with at most one decimal. "2.5" means 2.5 %. "2.55" is refused.
  • Some fields must also be greater than zero, for example unit_price when you list on the secondary market or OTC. "0" is refused there.
Value sentResultdetails[].message
"1000.000001"Accepted—
"12"Accepted—
"0.5"Accepted—
""Refusedamount must not be empty
"-5", "+5"Refusedamount must not carry a sign
"1e3"Refusedamount must not use exponent notation
"1,000.50"Refusedamount must not contain digit separators
"1 000"Refusedamount must not contain whitespace
".5"Refusedamount must have at least one digit before the decimal point
"5.", "12.5.1"Refusedamount must be digits with an optional fractional part
"01", "007.5"Refusedamount must not have leading zeros
"12.1234567" for a 6-decimal assetRefusedamount has more decimals than the asset supports
"0" as a listing unit_priceRefusedamount must be greater than zero
Fee "2.55"Refusedpercent must not have more than one decimal
Fee "100.5"Refusedpercent must not exceed 100

When a value is refused​

A malformed value is refused with HTTP 400 and code 10015 (invalid request payload). The details array names the rule that failed:

{
"code": 10015,
"domain": 100,
"message": "invalid request payload: amount: amount must not carry a sign",
"details": [
{
"field": "amount",
"message": "amount must not carry a sign"
}
]
}

field is "amount" or "percent". It names the kind of rule that failed, not your JSON key. The JSON decoder does not report which key held the value, so check the fields you sent against the rule in message. A percentage that breaks a general format rule, such as a sign or a separator, is reported with "field": "amount". The percentage-only rules (one decimal, 100 at most) are reported with "field": "percent".

What responses contain​

Responses always return these fields as strings, in their shortest form:

  • "12.5", never "12.50".
  • "12", never "12.0".
  • "0.00000003", never "3e-8".
  • usd and try valuations are rounded to 8 decimals.
  • A derived value can be negative. For example, the token-history rate can be "-35.71". A request never accepts a sign.

Parse these fields with a decimal type, such as decimal.js or big.js in JavaScript, decimal.Decimal in Python, BigDecimal in Java or shopspring/decimal in Go. If you pass them through parseFloat or float64, you bring back the rounding the strings exist to avoid.

Transition​

  • Responses: strings from now on. If your client reads any of the fields above as a number, update it now. This is the breaking part of the change.
  • Requests: send strings. For now, the API still accepts a JSON number on input. A number is treated as the float it is: it keeps the 15 significant digits a double holds reliably, so 12.5 is "12.5" and a float sum's 0.30000000000000004 is "0.3". The same rules then apply (no sign, no more decimals than the asset). If you need more than 15 significant digits, send a string; its digits are exact. Support for numbers ends in a later release. The changelog will announce the date at least 30 days in advance.

Example​

Send 250.5 units of a currency token. An amount is a string, and it may have up to as many decimals as the token. Send Currency Token by ID:

POST /v2/admin/currency-tokens/{currencyTokenID}/send
Content-Type: application/json
{
"amount": "250.5",
"recipient_wallet": "0x3b4E4dd67EE4054514bA2101e981f4C4D44eFDDc"
}

An excerpt of a token as Get Token Details returns it. amount and salable_percentage are strings, and the counts are numbers:

{
"value": {
"token_price": {
"id": "...",
"amount": "12.5",
"decimal": 6,
"symbol": "USDT",
"address": "0x..."
},
"salable_percentage": "80",
"max_supply": 1000,
"listed_supply": 800,
"sold_supply": 200
}
}