Q96 Math Utilities
Prices in the CCA V2 program are stored as Q96 fixed-point numbers: 128-bit unsigned integers where the lower 96 bits represent the fractional part.
human_price = Q96_value / 2^96
Where 2^96 = 79228162514264337593543950336.
The SDK provides encoding and decoding functions that use pure BigInt arithmetic to avoid any floating-point precision loss.
encodeQ96
Encode a human-readable price (decimal string or number) into a Q96 bigint.
function encodeQ96(value: number | string): bigint
Parameters
| Parameter | Type | Description |
|---|---|---|
value | number | string | The decimal price to encode. String is preferred for precision. |
Examples
import { encodeQ96 } from "@runner-protocol/sdk";
// Encode from string (recommended for precision)
const price = encodeQ96("0.05");
// => 3961408125713216879677197517n
// Encode from number (safe for simple values)
const price2 = encodeQ96(1.0);
// => 79228162514264337593543950336n (exactly 2^96)
// Encode a very small price
const price3 = encodeQ96("0.000001");
// => 79228162514264337593544n
Always use string input for prices that require precision beyond ~15 significant digits. The number overload calls toFixed(18) internally, but JavaScript number only has ~15 digits of precision.
decodeQ96
Decode a Q96 value to a JavaScript number. Safe for typical price values where the whole part is less than 2^53.
function decodeQ96(value: string | bigint): number
Parameters
| Parameter | Type | Description |
|---|---|---|
value | string | bigint | The Q96 value to decode. Strings are parsed as BigInt. |
Examples
import { decodeQ96 } from "@runner-protocol/sdk";
// Decode from bigint
const price = decodeQ96(3961408125713216879677197517n);
// => 0.05
// Decode from string (as returned by indexer API)
const price2 = decodeQ96("79228162514264337593543950336");
// => 1.0
// Zero
const price3 = decodeQ96(BigInt(0));
// => 0
decodeQ96 internally calls decodeQ96ToString with 12 decimal places and then parseFloat. For prices with extremely high precision requirements, use decodeQ96ToString directly.
decodeQ96ToString
Decode a Q96 value to a string with configurable decimal precision. Uses only BigInt arithmetic internally -- no floating-point intermediate values.
function decodeQ96ToString(
value: string | bigint,
decimals?: number // default: 6
): string
Parameters
| Parameter | Type | Description |
|---|---|---|
value | string | bigint | The Q96 value to decode |
decimals | number | Number of decimal places in the output (default: 6) |
Examples
import { decodeQ96ToString } from "@runner-protocol/sdk";
// Default 6 decimal places
const price = decodeQ96ToString("3961408125713216879677197517");
// => "0.050000"
// High precision (18 decimal places)
const precise = decodeQ96ToString("3961408125713216879677197517", 18);
// => "0.050000000000000000"
// Integer price
const whole = decodeQ96ToString("79228162514264337593543950336", 2);
// => "1.00"
// Zero decimals
const integer = decodeQ96ToString("158456325028528675187087900672", 0);
// => "2"
Common Pitfalls
1. Never use Number() intermediate conversions
Q96 values are 128-bit integers. Converting to Number before BigInt operations causes precision loss:
// WRONG - precision loss
const bad = BigInt(Number("79228162514264337593543950336"));
// CORRECT - direct BigInt conversion
const good = BigInt("79228162514264337593543950336");
2. Indexer returns prices as strings
The indexer API returns Q96 values as numeric strings. Always pass them directly to the decode functions:
const auction = await fetch(`${INDEXER_URL}/api/auctions/${pubkey}`).then(r => r.json());
// CORRECT - pass string directly
const price = decodeQ96(auction.clearing_price);
// WRONG - don't parse as Number first
const bad = decodeQ96(Number(auction.clearing_price)); // precision loss!
3. Encoding for instruction parameters
When building instructions, use encodeQ96 to convert human-readable prices to the Q96 format expected by the program:
import { encodeQ96, buildPlaceBidInstruction } from "@runner-protocol/sdk";
const ix = buildPlaceBidInstruction({
// ...
maxPrice: encodeQ96("0.05"), // Q96-encoded
amount: BigInt(1_000_000), // raw atomic units, NOT Q96
// ...
});
The amount field in PlaceBidParams is in raw atomic units (e.g., lamports or USDC base units), not Q96-encoded. Only price fields use Q96 encoding.