Precision: tick size, lot size, significant figures
A HyperCore price has at most 5 significant figures and a decimal cap set by szDecimals, a size at most szDecimals decimals, and outcomes use a 0.00001 grid.
HyperCore rejects any price or size off its grid, so you round before you sign. There’s no tick size field to read: rules decide.
Say a perp has szDecimals 2 and trades at 99.98241. Two digits sit before the point, so 5 significant figures leave 3 decimals; the cap, 6 - 2 = 4, doesn’t bind. The tick is 0.001. From 100 it’s 0.01: 99.999 is valid, 100.001 is not.
A price passes two checks
Section titled “A price passes two checks”A price is valid when both hold:
- It has at most 5 significant figures.
- It has at most
MAX_DECIMALS - szDecimalsdecimal places, whereMAX_DECIMALSis 6 for perps and 8 for spot.
Integers always pass: 123456 is valid, 12345.6 is not. szDecimals is in meta.universe for perps and on the base token in spotMeta.tokens for spot. A failure returns Price must be divisible by tick size.
The tick jumps tenfold at each power of ten
Section titled “The tick jumps tenfold at each power of ten”With d digits before the point, the significant-figure rule allows 5 - d decimals and the cap then limits that:
d = floor(log10(price)) + 1 digits before the point (negative below 0.1)decimals = clamp(5 - d, 0, MAX_DECIMALS - szDecimals)tick = 10 ^ (-decimals)For a perp (MAX_DECIMALS 6):
| szDecimals | Price | d | Decimals allowed | Tick | Valid example | Invalid example |
|---|---|---|---|---|---|---|
| 5 | 93231.23 | 5 | 0 | 1 | 93231 |
93231.2 |
| 2 | 137.23025 | 3 | 2 | 0.01 | 137.23 |
137.231 |
| 2 | 99.98241 | 2 | 3 | 0.001 | 99.982 |
99.9824 |
| 0 | 1234.56 | 4 | 1 | 0.1 | 1234.5 |
1234.56 (6 significant figures) |
| 0 | 0.0012345 | -2 | 6 | 0.000001 | 0.001234 |
0.0012345 (7 decimals) |
| 1 | 0.012345 | -1 | 5 | 0.00001 | 0.01234 |
0.012345 (more than 6 - 1 decimals) |
For spot (MAX_DECIMALS 8), 0.0001234 (7 decimals) is valid for a base szDecimals of 0 or 1 and invalid from 2, where 8 - 2 allows 6.
Sizes and outcome prices
Section titled “Sizes and outcome prices”A size has at most szDecimals decimals, in base-asset units: with szDecimals = 3, 1.001 passes and 1.0001 fails. Outcome prices skip the significant-figure rule and sit on the 0.00001 grid; sizes are whole contracts.
The numbers
Section titled “The numbers”| Parameter | Perps | Spot | Outcomes |
|---|---|---|---|
| Significant figures in a price | 5 (integers exempt) | 5 (integers exempt) | not applicable |
MAX_DECIMALS |
6 | 8 | not applicable |
| Price decimals allowed | min(5 - d, 6 - szDecimals), floor 0 |
min(5 - d, 8 - szDecimals), floor 0 |
5, from 0.00001 to 0.99999 |
| Size decimals | szDecimals |
base token szDecimals |
0 |
The tick scales with price
Section titled “The tick scales with price”Where the significant-figure rule binds, the tick is one part in 10,000 to 100,000: 0.0001 near 1 (if the cap allows four decimals) and 1 near 100,000. The smallest size is fixed per asset: 0.00001 BTC at the docs’ example szDecimals of 5.
Rounding in code
Section titled “Rounding in code”Round in the safe direction
Section titled “Round in the safe direction”SDKs round to nearest, by truncation or by side. Round limit prices by side, so rounding never makes an order more aggressive:
| Side | Round | Effect |
|---|---|---|
| Buy | down | never pays more than intended |
| Sell | up | never sells for less than intended |
Round the other way only to fill even one tick worse. Truncate sizes so an order never exceeds its balance; the docs say “rounded”, but truncation is always valid.
Count digits correctly
Section titled “Count digits correctly”Use floor(log10(p)) + 1. The tempting ceil(log10(p)) undercounts at exact powers of ten: at 100 it gives 2, a tick of 0.001 instead of 0.01, so a quote one tick up, 100.001, is rejected.
No floats
Section titled “No floats”Binary floats cannot hold most decimal prices, and JavaScript prints small numbers as 1e-7. Use a decimal library, a BigInt-backed decimal or string arithmetic:
type Side = "buy" | "sell";
// "123.45" -> { units: 12345n, scale: 2 }function toScaled(x: string): { units: bigint; scale: number } { if (!/^\d+(\.\d+)?$/.test(x)) throw new Error(`not a plain decimal string: ${x}`); const [int, frac = ""] = x.split("."); return { units: BigInt(int + frac), scale: frac.length };}
// Back to a string with trailing zeros stripped, ready to sign.function fromScaled(units: bigint, scale: number): string { const digits = units.toString().padStart(scale + 1, "0"); const int = digits.slice(0, digits.length - scale); const frac = digits.slice(digits.length - scale).replace(/0+$/, ""); return frac ? `${int}.${frac}` : int;}
// floor(log10(x)) + 1, read off the string: "100" -> 3, "0.05" -> -1.function intDigits(x: string): number { const [int, frac = ""] = x.split("."); const i = int.replace(/^0+/, ""); return i ? i.length : -(frac.match(/^0*/)?.[0].length ?? 0);}
// Keep at most `decimals` decimals. Buys round down, sells round up.function roundTo(x: string, decimals: number, side: Side): string { const { units, scale } = toScaled(x); if (scale <= decimals) return fromScaled(units, scale); const step = 10n ** BigInt(scale - decimals); const q = units / step + (side === "sell" && units % step !== 0n ? 1n : 0n); return fromScaled(q, decimals);}
// Perps: maxDecimals 6. Spot: maxDecimals 8 with the base token's szDecimals.export function roundPx(px: string, szDecimals: number, maxDecimals: 6 | 8, side: Side): string { const decimals = Math.max(0, Math.min(5 - intDigits(px), maxDecimals - szDecimals)); return roundTo(px, decimals, side);}
// Outcomes: the 0.00001 grid. Clamp to 0.00001..0.99999 separately.export const roundOutcomePx = (px: string, side: Side) => roundTo(px, 5, side);
// Sizes: truncate, never round up.export const truncSz = (sz: string, szDecimals: number) => roundTo(sz, szDecimals, "buy");| Call | Result |
|---|---|
roundPx("1234.56", 0, 6, "buy") |
1234.5 |
roundPx("0.012345", 1, 6, "buy") |
0.01234 |
roundPx("99.9999", 2, 6, "sell") |
100 |
roundPx("123456", 0, 6, "buy") |
123456, integer left alone |
Strip trailing zeros before signing
Section titled “Strip trailing zeros before signing”The exchange hashes prices and sizes with trailing zeros removed, so "0.650" and "0.65" sign differently. Produce the stripped string once, as fromScaled does, and use it in body and hash alike. See signing.
Where rounding still goes wrong
Section titled “Where rounding still goes wrong”- Outcome mids and marks land on half ticks about a third of the time: round before quoting.
- A fixed tick from
szDecimalsalone is wrong (one codebase did it). - A trailing-zero mismatch has surfaced as
Price must be divisible by tick size., not a signature error. - Tick failures caught in pre-validation return one error for the whole batch (errors).
- HyperEVM precompiles scale perp prices by
10^(6 - szDecimals)and spot by10^(8 - base szDecimals).
For where szDecimals lives, see Asset ids and naming. For tick and lot size from a trader’s side, see Contract specifications.