Prices, sizes, and units
There are two number systems to know about:
- On the wire (the gRPC APIs),
Decimalfields express prices, sizes, balances and USD amounts as decimal strings in human units. - On-chain, everything is an integer on the contract's grid — sizes in lots, prices in ticks — or a fixed-point number.
Use on-chain units when you build instructions, decode accounts or reconcile against the chain.
Decimal strings on the wire
Market, account and fill money fields in flint.margin.user.v1 and
flint.margin.maker.v1 use Decimal { value } wrapping a string
such as "155.14". Parse it with a fixed-precision type, never a
float:
| Language | Library |
|---|---|
| Rust | rust_decimal::Decimal |
| TypeScript | bignumber.js or decimal.js |
| Field | Example | Meaning |
|---|---|---|
price, mark_price, entry_price, limit_price | "150.125" | Settlement asset per base unit (USDC per SOL) |
index_price | "150.12531" | Oracle fair price per base unit, before tick rounding |
quantity (positions) | "-2.5" | Base units, signed: positive long, negative short |
quantity (orders, fills, book) | "2.5" | Base units, unsigned; the side is a separate field |
*_usd | "1298.25" | US dollars |
balances (raw_deposit, lent, borrowed) | "1.5" | Token units, already scaled by the mint's decimals |
initial_margin_rate, maintenance_margin_rate | "0.1" | Fractions of notional |
taker_fee_rate, maker_rebate_rate | "0.001" | Fractions of notional |
FundingRate.rate | "0.0002" | Fraction of notional per interval_seconds (3600); positive means longs pay |
The server converts these Decimal fields using the contract grid and mint
decimals. Raw quoting state is an exception: GetQuotingState.payload_json
contains grid integers, including ticks and lots. Transaction bytes, account
bytes and fields explicitly named in lamports or other raw units also require
their documented on-chain units.
Sign conventions
| Field | Positive means |
|---|---|
PerpPosition.quantity | Long |
FundingRate.rate | Longs pay shorts |
PerpPosition.accrued_funding_usd, FundingPayment.amount_usd | A credit to the account |
PerpPnlHistoryPoint.fees_usd | Net rebates (fees are negative) |
AccountHealth.deficit | Below maintenance, by that much |
The contract grid
Each perp fixes two scales at listing, and every on-chain size and price lives on them:
1 lot = 10^(−lot_scale) units of the underlying
1 tick = tick_size atoms of the PnL currency
price = ticks per lotA price on-chain is therefore "how many ticks one lot costs". The human forms follow directly:
lot_size = 10^(−lot_scale) base units
price_step = tick_size / 10^(settlement decimals) / lot_size settlement per base unit
quantity = lots × lot_size
price = ticks × price_step
fill value = lots × ticks × tick_size settlement atomsPerpMarketService.ListPerpMarkets returns lot_size and
tick_size already in human form: its tick_size is the
price_step above (the price increment per base unit), not the raw
on-chain atom count. min_order_quantity is one lot.
Illustrative grids that settle in USDC (6 decimals). Read the current grid from
ListPerpMarkets before building a transaction:
| Contract | lot_scale | tick_size | Lot | Price step | $150 / $100,000 as ticks |
|---|---|---|---|---|---|
SOL-USDC-PERP | 3 | 1 | 0.001 SOL | $0.001 | 150,000 |
BTC-USDC-PERP | 5 | 1 | 0.00001 BTC | $0.10 | 1,000,000 |
JLP-USDC-PERP | 2 | 1 | 0.01 JLP | $0.0001 | — |
So "buy 2.5 SOL with a worst price of $150.25" on SOL-USDC-PERP is
lots = 2,500, limit = 150,250.
Converting a limit price
Round a limit toward the side that protects you: a buy limit down, a sell limit up, so the on-chain price is never worse than the one you meant.
import BigNumber from "bignumber.js";
const priceStep = new BigNumber(market.tickSize!.value); // from ListPerpMarkets
const lotSize = new BigNumber(market.lotSize!.value);
const lots = new BigNumber("2.5").div(lotSize); // must be a whole number
const raw = new BigNumber("150.25").div(priceStep);
const limit = side === "buy"
? raw.integerValue(BigNumber.ROUND_FLOOR)
: raw.integerValue(BigNumber.ROUND_CEIL);use rust_decimal::Decimal;
use std::str::FromStr;
let price_step = Decimal::from_str(&market.tick_size.unwrap().value)?;
let lot_size = Decimal::from_str(&market.lot_size.unwrap().value)?;
let lots = Decimal::from_str("2.5")? / lot_size; // must be a whole number
let raw = Decimal::from_str("150.25")? / price_step;
let limit = if is_buy { raw.floor() } else { raw.ceil() };Reject a size that is not a whole number of lots rather than rounding it silently.
Grid limits
| Quantity | On-chain type | Bound |
|---|---|---|
| Position size | i32 lots | ±2,147,483,647 lots |
Order size (lots) | u32, refused past the i32 position width | 2,147,483,647 lots |
| Mark and limit price | u32 ticks per lot | 1 to 4,294,967,295; zero is refused |
| Entry notional | i64 ticks | Fits by construction: i32 × u32 |
A listing is only accepted if one lot is a whole number of the underlying's atoms and the oracle can resolve one lot's price to within half a tick.
On-chain number formats
| Concept | Representation | Notes |
|---|---|---|
| Token amounts | u64 atoms | Scaled by the mint's decimals |
| USD amounts | atoms, 1 USD = 10^6 | Account totals are 96-bit; per-balance values fit i64 |
| Oracle price | U32.32 fixed point | USD atoms per token atom |
| Spot share prices | Q64.64 fixed point | Token atoms per pool share |
| Margin fractions, weights | LessThanOneX64 (Q0.64) | imf, mmf, unpl_weight, deposit_weight, borrow_imf, bands. Value = raw / 2^64, always below 1 |
| Fee rates | PercentU32 | taker_fee, maker_rebate_share, liquidation_fee, liquidator_share. Value = raw / (2^32 − 1), so 100% is representable |
| Perp position size | i32 lots | Signed |
| Entry notional and unsettled legs | i64 ticks | Realized PnL, funding and fees are separate legs |
| Funding index | i128 ticks per lot, 32 fractional bits | One per side, cumulative |
| Funding rate | i64 ticks per lot per second, 32 fractional bits | Hourly fraction = rate × 3600 / 2^32 / mark_ticks |
| Carry | Signed Q0.63 fraction | Hourly, of mark |
| Account leverage cap | u64, Q0.64 fraction | 1 / max leverage; zero means no cap. Set with setAccountLeverageCap |
| Resting order | quantity in u32 lots; expiry a u64 slot | One order per trader per price in the contract's LinearPerpOrderBook |
| Order-book key | OrderKey { rank: u32, trader_id: u64 } | Ascending rank is best price first: price ticks for asks, u32::MAX − price for bids. No time priority within a price |
Fee rates quoted in basis points are stored truncated, so a quoted whole-tick fee lands exactly and any other one is at most one tick under the quoted rate. Every value the program derives rounds in the protocol's favour: margins and fees up, collateral and rebates down.
Identifiers
| Id | Width | On the wire |
|---|---|---|
TraderId | u64, chosen by the account's creator | TraderId { id } |
LinearPerpId | u32, dense from 0 | PerpId { id } |
SpotId | u32, dense from 0 | SpotId { id } |
OracleId | u32 | OracleInfo.oracle_id |
product_id | catalog-wide id across spots and perps | SpotMetadata.product_id |
Trader accounts are PDAs seeded by ["trader_account", trader_id];
contract, quoting and cache accounts are PDAs of the program too.
See On-chain clients for derivation helpers.
Time
| Where | Unit |
|---|---|
Wire Timestamp | Unix microseconds (micros); zero means unset |
| On-chain clocks, funding, timelocks | Unix seconds |
slot | Solana slot; quote expiry is counted in slots |
Gtd order expires_at_slot | The order is expired from this slot on; a Gtc order never expires. An expired order keeps reserving margin until pruned |
| History windows | start inclusive, end exclusive |
Every live response carries a DataProvenance: the commitment it
was read at, the slot, and when the server observed it. Treat it as
an observation, not proof of finality.
Enums on the wire
| Enum | Values |
|---|---|
TradeSide | TRADE_SIDE_BUY, TRADE_SIDE_SELL |
OrderType | ORDER_TYPE_LIMIT, ORDER_TYPE_MARKET, … (stop and take-profit types are rejected) |
TimeInForce | Not read by PreviewOrder; on-chain the order's PerpOrderKind decides |
PerpPositionStatus | OPEN, CLOSED, LIQUIDATED |
PerpMarketStatus | Indexed markets report ACTIVE |
Commitment | PROCESSED, CONFIRMED, FINALIZED |
LimitConstraint | Which rule bounds an action, e.g. LIMIT_CONSTRAINT_INITIAL_MARGIN |
On-chain, an order's side is a Direction — Long or Short — the
way the taker wants to end up positioned. Its PerpOrderKind is
Gtc, Gtd, Ioc or Fok. Price, expiry and post-only options belong to the
selected kind; see On-chain clients.
A resting order's side
is a MakerSide, Buy or Sell.
SDK values vs on-chain values
| Layer | Takes | Example |
|---|---|---|
gRPC clients (flint-margin-user-client, flint-margin-maker-client) | Decimal strings in human units | quantity: { value: "2.5" } |
Instruction builders (@superis-labs/flint-idl, flint-margin-onchain) | Raw integers on the grid | lots: 2500, kind: { __kind: "Gtc", limit: 150250, postOnly: true } |
| Account decoders | Raw fixed-point and grid integers | entry_contract_lots, entry_ticks |
Convert exactly once, at the boundary where you build the
instruction, with the grid from ListPerpMarkets. See
TypeScript, Rust and
On-chain clients.
