> For the complete documentation index, see [llms.txt](https://docs.native.org/native-dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.native.org/native-dev/build-with-native/native-core/concepts/decimals-units.md).

# Decimals & Units

How Native Core converts human decimal strings to the integer atoms it executes on.

Native Core executes on integers only. Public order and modify payloads accept human decimal strings for `price` and `quantity`; the write path converts them to the raw integer atoms encoded in the signed transaction bytes. Integer-only fields such as ids, timestamps, nonces, and accounting amounts remain decimal integer strings. Public query responses use asset and market decimal metadata to return human-readable display strings where that is useful.

## Decimal units

Each asset has one balance decimal field:

* `balance_decimals`: internal balance and settlement atom scale.

Balances use `balance_decimals`:

```
raw_balance_atoms = display_asset_amount * 10^balance_decimals
display_asset_amount = raw_balance_atoms / 10^balance_decimals
```

Order quantities use the market's `base_quantity_decimals`:

```
raw_quantity = display_quantity * 10^market.base_quantity_decimals
display_quantity = raw_quantity / 10^market.base_quantity_decimals
```

For example, ETH has `balance_decimals = 8`, while ETH/USDC uses `base_quantity_decimals = 4`:

```
1 ETH      = 100000000 balance atoms
0.0001 ETH = 1 order quantity atom on ETH/USDC
```

USDC has `balance_decimals = 8`, while USDC/USDT uses `base_quantity_decimals = 2`:

```
1 USDC balance      = 100000000 balance atoms
0.01 USDC order qty = 1 order quantity atom on USDC/USDT
```

`userBalances` returns display amounts for `available` and `locked` using the asset's `balance_decimals`. `spotCreditPositions` returns both raw signed balance atom strings (`pending_exposure_qty`, `actual_qty`) and display strings (`pending_exposure_display`, `actual_display`) using `balance_decimals` when asset metadata is available.

Asset metadata validation:

* `asset_id` is a `u32`.
* `symbol` is 1 to 16 ASCII alphanumeric characters. Display casing is preserved.
* `symbol` must be unique under ASCII case-insensitive matching (`cbBTC`, `CBBTC`, and `cbbtc` collide).
* `asset_id` must be unique.
* `balance_decimals` must be `0..=18`.

## Market decimals

Each market has a base asset and quote asset. Public `quantity` is a display base amount and public `price` is a display quote-per-base price. The write path uses the market's `base_quantity_decimals` and `price_decimals` to convert those display strings into raw integer atoms before assembling signed bytes.

The market response includes:

* `base_quantity_decimals`: market-level order quantity lot scale for the base asset.
* `quote_balance_decimals`: `balance_decimals` of the quote asset.
* `price_decimals`: decimal places for raw prices.
* `max_price_sig_figs`: max significant figures for non-integer display prices, always in `1..=18`.

`base_quantity_decimals`, `price_decimals`, and `max_price_sig_figs` are canonical market metadata. `quote_balance_decimals` is returned on each market so clients can convert quote notional without separately joining against the `assets` response.

Display price means quote asset units per 1 base asset:

```
display_price = raw_price / 10^price_decimals
raw_price = display_price * 10^price_decimals
```

Markets must satisfy:

```
base_quantity_decimals <= base.balance_decimals
price_decimals + base_quantity_decimals <= quote.balance_decimals
```

Quote notional atoms are:

```
quote_atoms =
  raw_price * raw_quantity * 10^(quote.balance.decimals - price_decimals - base_quantity_decimals)
```

For ETH/USDC:

```
ETH/USDC base_quantity_decimals = 4
USDC balance_decimals = 8
price_decimals = 2

display price 3500 USDC / ETH => raw_price = 3500 * 10^2 = 350000
display qty   1 ETH           => raw_quantity = 1 * 10^4 = 10000
raw notional quote atoms      => 350000 * 10000 * 10^2 = 350000000000
display notional              => 350000000000 / 10^8 = 3500 USDC
```

`l2Book`, `openOrders`, `userFills`, and `orderStatus` return display `price` and display quantity fields. Write actions use the same display convention for `price` and `quantity`; the canonical signed payload still contains raw atoms.

Market metadata validation:

* `market_id` is a `u32`.
* `base_asset_id` and `quote_asset_id` must exist.
* `base_asset_id != quote_asset_id`.
* `quote_asset_id` must be enabled in canonical `protocol.quote_assets`.
* `base_quantity_decimals <= base.balance_decimals`.
* `price_decimals + base_quantity_decimals <= quote.balance_decimals`.
* `max_price_sig_figs` must be in `1..=18`.
* A base/quote pair may be opened only once.
* Market ids are unique.

The genesis quote allowlist is USDC asset `1` with `min_quantity="10"`, USDT asset `2` with `min_quantity="10"`, ETH asset `3` with `min_quantity="0.01"`, and BNB asset `4` with `min_quantity="0.02"`. Read the live per-quote-asset minimums from [`POST /info` `quoteAssets`](/native-dev/build-with-native/native-core/reference/post-info.md#quoteassets).

## Write-time numeric validation

`POST /trade` validates signed action numbers before reconstructing the canonical signed payload:

* Integer fields accept a base-10 integer string or an unsigned JSON integer (no decimal point, no sign, no exponent notation, no commas); the string form is preferred for values above 2^53.
* Order and modify `price` / `quantity` are human decimal strings: no sign, no exponent notation, no commas, and no more fractional digits than the market's `price_decimals` / `base_quantity_decimals`. A value that overflows `u64` after conversion is rejected with `InvalidPriceOverflow` / `InvalidQuantityOverflow`.
* `market_id` parses as `u32`.
* `oid`, `nonce`, `agent_epoch`, and `expires_after_ms` parse as `u64`.
* `quantity` greater than zero is enforced at **execution**, not at this admission stage — a zero quantity passes shaping and is rejected when the order runs.
* `price` must be present for both limit orders and protected market orders in the current protocol encoding.
* `market` orders are valid only with `ioc` or `fok`, and `gtc` / `alo` only with limit orders — both pairings are enforced at **execution**, not at this admission stage.
* non-integer display prices may have at most `max_price_sig_figs` significant figures (enforced at execution, not admission — see [Valid / invalid examples](#valid-invalid-examples)); integer display prices are always allowed.
* quote notional is computed with widened arithmetic and must fit into `u64` quote balance atoms.
* If the configured minimum spot notional is nonzero, the raw quote notional must be at least that minimum.

## Valid / invalid examples

The pairs below use an **illustrative** market with `price_decimals = 2`, `max_price_sig_figs = 5`, and `base_quantity_decimals = 4`. These are not any specific market's real values — read each market's precision from [`POST /info` `markets`](/native-dev/build-with-native/native-core/reference/post-info.md#markets). Send the display string exactly; nothing rounds on the wire.

Two independent gates apply to a `price`, and they fail at **different layers**:

* **Fractional digits `>`** **`price_decimals`** — rejected at **admission**: `POST /trade` returns `submission_status: "rejected"` with `InvalidPricePrecision` (or `InvalidQuantityPrecision` for `quantity`).
* **Non-integer price with more than `max_price_sig_figs` significant figures** — clears admission, then **fails at execution** as [`tick`](/native-dev/build-with-native/native-core/reference/error-responses.md#execution-level-failures). Because `/trade` is synchronous, that failure comes back on the `/trade` response itself — but as a per-order leaf: `submission_status` stays `"accepted"` and the code appears as `response.status.error: "tick"`, with no top-level `error`. **That leaf is the only record.** The order never entered the book, so it is never written to [`orderStatus`](/native-dev/build-with-native/native-core/reference/post-info.md#orderstatus) and reconciling its `cloid` finds nothing. The [Python SDK](/native-dev/build-with-native/native-core/python-sdk.md) saves you the round trip by rejecting it locally (`LocalValidationError: … significant figures`) before it ever signs.

`price`:

| Value      | Result                           | Why                                                                                                                                                                                                                            |
| ---------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `3500.00`  | valid                            | raw `350000` is an integer price; the significant-figures gate is skipped.                                                                                                                                                     |
| `3500.1`   | valid                            | 1 fractional digit `<=` `price_decimals`; 5 significant figures — exactly at `max_price_sig_figs`.                                                                                                                             |
| `3500.12`  | admitted, then fails `tick`      | 2 fractional digits clear admission, but 6 significant figures `>` `max_price_sig_figs` (5). `submission_status` stays `"accepted"` and the code appears as `response.status.error: "tick"`. The SDK rejects it locally first. |
| `3500.123` | rejected `InvalidPricePrecision` | 3 fractional digits `>` `price_decimals` (2) — rejected at admission.                                                                                                                                                          |
| `3500.120` | rejected `InvalidPricePrecision` | trailing zeros count — still 3 fractional digits `>` 2.                                                                                                                                                                        |

`quantity` — only the fractional-digit gate applies; `quantity` has no significant-figures cap:

| Value     | Result                              | Why                                                     |
| --------- | ----------------------------------- | ------------------------------------------------------- |
| `1.0000`  | valid                               | 4 fractional digits `<=` `base_quantity_decimals`.      |
| `0.0001`  | valid                               | smallest lot — 1 order quantity atom.                   |
| `1.2345`  | valid                               | 4 fractional digits.                                    |
| `1.12345` | rejected `InvalidQuantityPrecision` | 5 fractional digits `>` `base_quantity_decimals` (4).   |
| `1.10000` | rejected `InvalidQuantityPrecision` | trailing zeros count — still 5 fractional digits `>` 4. |

Fractional-digit precision (`price_decimals` / `base_quantity_decimals`) is a `POST /trade` **admission** rejection — see [`/trade` error codes](/native-dev/build-with-native/native-core/reference/transaction-signing.md#trade-error-codes). Significant figures (`max_price_sig_figs`) apply only to non-integer prices and are enforced one layer deeper, at **execution**, as a [`tick`](/native-dev/build-with-native/native-core/reference/error-responses.md#execution-level-failures) failure — returned synchronously on the `/trade` response, but as a per-order leaf: `submission_status` stays `"accepted"` and the code is `response.status.error: "tick"`. An integer display price such as `3500` skips that gate entirely.
