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.
| Family | Fields (decimal string) |
|---|---|
| Price objects | amount 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 value | salable_percentage |
| Fees | fee 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 bands | min_sell_price, max_sell_price, min_price, max_price, unit_price, price_amount |
| OTC order rules | above_amount, below_amount, above_percent, below_percent |
| Wallets | balances and usd / try valuations in wallet summaries (amount, balance_native, …) |
| Analytics | volumes and fee revenue |
| Currency tokens and nodes | currency-token total_supply; mint, send and withdraw amount; node withdraw amount |
| Purchase quotes | unit price, subtotal, total, seller proceeds |
| Market data | token-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 appearsIn 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
decimalof 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_pricewhen you list on the secondary market or OTC."0"is refused there.
| Value sent | Result | details[].message |
|---|---|---|
"1000.000001" | Accepted | — |
"12" | Accepted | — |
"0.5" | Accepted | — |
"" | Refused | amount must not be empty |
"-5", "+5" | Refused | amount must not carry a sign |
"1e3" | Refused | amount must not use exponent notation |
"1,000.50" | Refused | amount must not contain digit separators |
"1 000" | Refused | amount must not contain whitespace |
".5" | Refused | amount must have at least one digit before the decimal point |
"5.", "12.5.1" | Refused | amount must be digits with an optional fractional part |
"01", "007.5" | Refused | amount must not have leading zeros |
"12.1234567" for a 6-decimal asset | Refused | amount has more decimals than the asset supports |
"0" as a listing unit_price | Refused | amount must be greater than zero |
Fee "2.55" | Refused | percent must not have more than one decimal |
Fee "100.5" | Refused | percent 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".usdandtryvaluations are rounded to 8 decimals.- A derived value can be negative. For example, the token-history
ratecan 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.5is"12.5"and a float sum's0.30000000000000004is"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
}
}