Velocity ProtocolDevelopers
Velocity SDK

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.

ConstantValueUsed for
PRICE_PRECISION1e6Oracle prices, order prices, oracle price offsets
BASE_PRECISION1e9Perp position and order base sizes
QUOTE_PRECISION1e6USD 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); // 10

Human 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 decimals

BigNum: 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:

GroupMethods
CreateBigNum.from(val, exponent), BigNum.fromPrint(string, exponent), BigNum.zero(exponent), BigNum.fromJSON
Arithmeticadd, sub, mul, scalarMul, div, scale(numerator, denominator), abs, neg
Precisionshift(exponent), shiftTo(targetExponent)
Comparegt, lt, gte, lte, eq, plus the zero checks gtZero, ltZero, eqZero, gteZero, lteZero
Printprint, printShort, prettyPrint, toFixed, toPrecision, toRounded, toNotional, toMillified, toPercentage
Escape hatchestoString (raw BN string), toNum (JS number), toJSON

Three behaviors worth knowing before relying on it:

  • add and sub assert that both operands have the same exponent. Call shiftTo first if they do not.
  • mul returns a value whose exponent is the sum of the two exponents. scalarMul shifts 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.
  • toNum goes through parseFloat, so it loses accuracy on very large values. Keep money math in BigNum or BN and 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 fieldMeaning
slotDurationMsCurrent slot length in ms. 0 means unset and resolves to the 400ms baseline.
pendingSlotDurationMsA staged next value. 0 means nothing is staged.
slotDurationEffectiveSlotThe 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 baseline

The rounding direction matters and mirrors the program exactly:

HelperRoundingUse for
millisFromSlots(slots, d)exactTurning a measured slot delta into elapsed time
millisToSlots(m, d)downStaleness windows, where shorter is the safe direction
millisToSlotsCeil(m, d)upWindows that protect the user, such as auction lengths and minimum cooldowns
msToSlotsNum(ms, d)downThe same as millisToSlots, for plain numbers
msToSlotsCeilNum(ms, d)upDurations that must not fall below their intended wall-clock length
slotsToMsNum(slots, d)exactPlain-number version of millisFromSlots
divPeriods(m, period)downHow many whole periods fit in a duration, for legacy per-period rates

Two more things to keep straight:

  • Millis and SlotDurationMs are 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 with millis(ms) or millisFromSecs(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 with millisFromStoredUnits. 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());