Precision and Types
Nothing in the SDK is a JavaScript number where money is involved. Prices, sizes, and balances are all BN integers scaled by a fixed power of 10, because a float cannot represent them exactly and the program will not accept one. Convert with the constants and helpers on this page rather than multiplying by hand.
Precision constants
Each constant is a BN holding a power of 10. Divide a raw value by its constant to get the human-readable number; multiply to go the other way.
| Constant | Value | Used for |
|---|---|---|
PRICE_PRECISION | 1e6 | Oracle prices, order prices, oracle price offsets |
BASE_PRECISION | 1e9 | Perp position and order base sizes |
QUOTE_PRECISION | 1e6 | USD amounts: collateral, PnL, fees |
Spot token amounts are the exception: they carry the mint's own decimals rather than a protocol-wide constant, which is why convertToSpotPrecision takes a market index. Do not assume 1e6 for a spot balance.
Converting between raw and human-readable values
BN to human number
import { BASE_PRECISION, BN, PRICE_PRECISION, QUOTE_PRECISION, convertToNumber } from "@velocity-exchange/sdk";
// Oracle price: raw 150_500_000 → 150.5 USD
const rawPrice = new BN(150_500_000);
const price = convertToNumber(rawPrice, PRICE_PRECISION);
console.log(price); // 150.5
// Position size: raw 2_500_000_000 → 2.5 SOL
const rawBase = new BN(2_500_000_000);
const size = convertToNumber(rawBase, BASE_PRECISION);
console.log(size); // 2.5
// Collateral: raw 10_000_000 → 10 USDT (or dUSDT on devnet)
const rawQuote = new BN(10_000_000);
const usd = convertToNumber(rawQuote, QUOTE_PRECISION);
console.log(usd); // 10Human number to BN
import { BASE_PRECISION, BN, PRICE_PRECISION } from "@velocity-exchange/sdk";
// 1 SOL in base precision
const oneSol = new BN(1).mul(BASE_PRECISION); // BN(1_000_000_000)
// $21.23 in price precision
const price = new BN(21_230_000); // 21.23 * 1e6
// Or use the VelocityClient helpers, available once the client exists.
// Prefer these: they are what the rest of these docs use.
const size = velocityClient.convertToPerpPrecision(1); // 1 base unit, BN at 1e9
const px = velocityClient.convertToPricePrecision(21.23); // price, BN at 1e6
const spot = velocityClient.convertToSpotPrecision(0, 100); // 100 units at market 0's decimalsBigNum: a value that carries its own precision
BigNum wraps a BN together with the number of decimal places that BN is scaled by, so a value and its precision travel as one object. Use it in place of hand-rolled conversion and formatting: it prints, parses, and does arithmetic while carrying the exponent itself.
The second constructor argument is the exponent, not the precision constant. Pass PRICE_PRECISION_EXP (6), BASE_PRECISION_EXP (9), QUOTE_PRECISION_EXP (6), and so on, not PRICE_PRECISION (1e6).
import {
BN,
BigNum,
PRICE_PRECISION_EXP,
BASE_PRECISION_EXP,
} from "@velocity-exchange/sdk";
// Raw oracle price (1e6) wrapped with its exponent
const price = BigNum.from(new BN(150_500_000), PRICE_PRECISION_EXP);
price.print(); // "150.5"
price.toFixed(2); // "150.50"
price.toNotional(); // "$150.50"
price.toNum(); // 150.5
// Parse a user-entered string into the right precision
const size = BigNum.fromPrint("2.5", BASE_PRECISION_EXP);
size.toString(); // "2500000000" (the raw BN, 1e9)Useful methods, grouped by what they do:
| Group | Methods |
|---|---|
| Create | BigNum.from(val, exponent), BigNum.fromPrint(string, exponent), BigNum.zero(exponent), BigNum.fromJSON |
| Arithmetic | add, sub, mul, scalarMul, div, scale(numerator, denominator), abs, neg |
| Precision | shift(exponent), shiftTo(targetExponent) |
| Compare | gt, lt, gte, lte, eq, plus the zero checks gtZero, ltZero, eqZero, gteZero, lteZero |
print, printShort, prettyPrint, toFixed, toPrecision, toRounded, toNotional, toMillified, toPercentage | |
| Escape hatches | toString (raw BN string), toNum (JS number), toJSON |
Three behaviors worth knowing before relying on it:
addandsubassert that both operands have the same exponent. CallshiftTofirst if they do not.mulreturns a value whose exponent is the sum of the two exponents.scalarMulshifts the result back down so it stays in the original precision space, which is usually the right choice when multiplying a price by a ratio.toNumgoes throughparseFloat, so it loses accuracy on very large values. Keep money math inBigNumorBNand convert only for display.
BigNum.setLocale(locale) sets the decimal delimiter and thousands separator used by every printing method, globally for the class.
Slots versus wall clock
Solana's slot time is moving from 400ms down to 200ms through a feature-gate schedule (400, 350, 300, 250, 200). Because of that, a slot count is not a fixed amount of time. The protocol stores durations in milliseconds and converts them to slots using the live slot length, and math/time.ts provides the same conversions offchain.
The live slot length comes from the state account. Three fields drive it:
| State field | Meaning |
|---|---|
slotDurationMs | Current slot length in ms. 0 means unset and resolves to the 400ms baseline. |
pendingSlotDurationMs | A staged next value. 0 means nothing is staged. |
slotDurationEffectiveSlot | The slot at which the staged value takes over. |
Read it with activeSlotDurationFromState(state, currentSlot), which applies the staged switch once currentSlot reaches the effective slot. slotDurationFromState(raw) only resolves the 0 sentinel on the base field, so it keeps returning the pre-switch value across a gate flip.
import {
BN,
activeSlotDurationFromState,
millisFromSecs,
millisToSlotsCeil,
millisFromSlots,
msToSlotsCeilNum,
slotsToMsNum,
SLOT_DURATION_BASELINE,
} from "@velocity-exchange/sdk";
const state = velocityClient.getStateAccount();
const currentSlot = new BN(await connection.getSlot());
// The slot length the program itself would use right now
const slotDuration = activeSlotDurationFromState(state, currentSlot);
// A 10 second window, expressed in actual slots
const tenSeconds = millisFromSecs(10);
const slots = millisToSlotsCeil(tenSeconds, slotDuration); // 25 slots at 400ms, 50 at 200ms
// A measured slot delta, back to wall-clock ms
const elapsedMs = millisFromSlots(new BN(20), slotDuration);
// Plain-number variants for pacing and thresholds
const auctionSlots = msToSlotsCeilNum(8_000, slotDuration);
const auctionMs = slotsToMsNum(auctionSlots, slotDuration);
console.log(SLOT_DURATION_BASELINE); // 400, the pre-gate baselineThe rounding direction matters and mirrors the program exactly:
| Helper | Rounding | Use for |
|---|---|---|
millisFromSlots(slots, d) | exact | Turning a measured slot delta into elapsed time |
millisToSlots(m, d) | down | Staleness windows, where shorter is the safe direction |
millisToSlotsCeil(m, d) | up | Windows that protect the user, such as auction lengths and minimum cooldowns |
msToSlotsNum(ms, d) | down | The same as millisToSlots, for plain numbers |
msToSlotsCeilNum(ms, d) | up | Durations that must not fall below their intended wall-clock length |
slotsToMsNum(slots, d) | exact | Plain-number version of millisFromSlots |
divPeriods(m, period) | down | How many whole periods fit in a duration, for legacy per-period rates |
Two more things to keep straight:
MillisandSlotDurationMsare branded types. A duration in milliseconds cannot be compared against a slot count without converting through the live slot length, and the type system enforces that. Build durations withmillis(ms)ormillisFromSecs(secs).STORED_UNIT_MS(400) is a storage codec for older admin-set fields that were encoded in units of the historical 400ms slot. Decode those withmillisFromStoredUnits. Do not use it as a general slots-to-seconds conversion factor.
Token math helpers
A spot balance is not stored as a token amount. It is stored as a scaled value that grows against the market's cumulative interest index, so converting it back to tokens takes the market account as well as the balance.
getTokenAmount converts a raw scaled spot balance into a token amount, accounting for accumulated interest since the last update. Pass the user's scaledBalance, the spot market account (which contains the cumulative interest index), and the balance type (deposit or borrow).
import { SpotBalanceType, convertToNumber, getTokenAmount } from "@velocity-exchange/sdk";
const spotMarket = velocityClient.getSpotMarketAccount(0); // e.g. dUSDT on devnet, USDT on mainnet
const user = velocityClient.getUser();
const spotPosition = user.getUserAccount().spotPositions[0];
const tokenAmount = getTokenAmount(
spotPosition.scaledBalance,
spotMarket,
spotPosition.balanceType
);
console.log("Token amount:", tokenAmount.toString());getSignedTokenAmount wraps getTokenAmount to return a signed value: positive for deposits, negative for borrows. Use this to distinguish between the two in a single number.
import { getSignedTokenAmount, getTokenAmount } from "@velocity-exchange/sdk";
const spotMarket = velocityClient.getSpotMarketAccount(0);
const spotPosition = velocityClient.getUser().getUserAccount().spotPositions[0];
const tokenAmount = getTokenAmount(
spotPosition.scaledBalance,
spotMarket,
spotPosition.balanceType
);
const signed = getSignedTokenAmount(tokenAmount, spotPosition.balanceType);
// signed > 0 means deposit, signed < 0 means borrow
console.log("Signed amount:", signed.toString());