Skip to Content
DevelopersConceptsAccount Model

Account Model

Velocity is a Solana program (smart contract) that manages user accounts, positions, orders, and markets. Understanding the onchain data model helps you work effectively with the Velocity SDK (TypeScript) and understand how state updates propagate.

Velocity has no Python SDK. A Rust client (velocity-rs) exists in the velocity-v1 monorepo  but is source-only and not published to crates.io — the interfaces below are TypeScript.

Core accounts

Velocity uses several account types, each serving a specific purpose:

State account

Single global account holding protocol-wide configuration:

  • Oracle guards - Stale price thresholds and validity checks (oracleGuardRails)
  • Fee structures - Separate default fee tiers for perpetual and spot markets
  • Admin controls - A tiered cold / warm / hot admin key model (see below), plus protocol fee treasuries
  • Solvency status - Bitflag gating bankruptcy/deficit-resolution instructions independently of the withdraw-pause flags
  • Feature flags - exchangeStatus, featureBitFlags, lpPoolFeatureBitFlags

The State account is a singleton — there’s only one per deployment. It’s read by almost every instruction to apply protocol-level rules.

Velocity replaced Drift’s single State.admin with a tiered key model: coldAdmin, warmAdmin, pauseAdmin, and a set of narrowly-scoped hot* keys (hotAmmCrank, hotLpCache, hotFeatureFlag, hotFeeWithdraw, hotMmOracleCrank, etc.) — each authorizing only the specific instructions it needs, rather than one address with full control. Code that reads state.admin directly no longer compiles against the current IDL.

View TypeScript interface (abridged)

interface StateAccount { coldAdmin: PublicKey; warmAdmin: PublicKey; pauseAdmin: PublicKey; hotFeeWithdraw: PublicKey; // ...additional narrowly-scoped hot* keys protocolFeeRecipientPerp: PublicKey; protocolFeeRecipientSpot: PublicKey; exchangeStatus: number; // bitmask, see ExchangeStatus whitelistMint: PublicKey; discountMint: PublicKey; oracleGuardRails: OracleGuardRails; numberOfAuthorities: BN; numberOfSubAccounts: BN; numberOfMarkets: number; numberOfSpotMarkets: number; minPerpAuctionDuration: number; // slots defaultMarketOrderTimeInForce: number; // seconds defaultSpotAuctionDuration: number; // slots liquidationMarginBufferRatio: number; // MARGIN_PRECISION (1e4) settlementDuration: number; // seconds maxNumberOfSubAccounts: number; signer: PublicKey; signerNonce: number; perpFeeStructure: FeeStructure; spotFeeStructure: FeeStructure; initialPctToLiquidate: number; // LIQUIDATION_PCT_PRECISION (1e4) liquidationDuration: number; // seconds maxInitializeUserFee: number; featureBitFlags: number; // bitmask, see FeatureBitFlags lpPoolFeatureBitFlags: number; solvencyStatus: number; // bitmask, see SolvencyStatus }

See StateAccount in packages/sdk/src/types.ts for the full field list.

Market accounts

PerpMarketAccount One per perp market — 1304 bytes on-chain

Manages perpetual futures markets with:

  • AMM state (amm) - Base/quote reserves and liquidity parameters for the constant-product vAMM
  • Oracle fields - oracle, oracleSource live at the top level of PerpMarketAccount (moved off amm.* when the AMM was decoupled)
  • Market stats (marketStats) - mark/oracle TWAPs, volume, and MM-oracle snapshot, shared across makers
  • Hedge config (hedgeConfig) - this market’s relationship to its hedging VLP pool (poolId, status, pausedOperations, exchangeFeeExclusionScalar, feeTransferScalar) — replaces Drift’s flat lp* LP-share fields
  • Fee ledger (feeLedger) - consolidated fee-split accounting (pending protocol/IF/AMM carveouts) from the fee redesign
  • Risk parameters - marginRatioInitial / marginRatioMaintenance (MARGIN_PRECISION, 1e4), imfFactor, contractTier
  • Market status - status: MarketStatus (see the discriminant note below)

Velocity has no vAMM LP shares — PerpPosition.lpShares, lastQuoteAssetAmountPerLp, and perLpBase from Drift do not exist, and the five flat LP-pool fields on PerpMarketAccount (lpPoolId, lpStatus, lpPausedOperations, lpFeeTransferScalar, lpExchangeFeeExcluscionScalar) were replaced by the single hedgeConfig object above. High leverage mode, protected maker mode, and fuel tracking were also removed — there is no PerpMarket.highLeverageMarginRatioInitial, protectedMaker*, or fuelBoost*.

View TypeScript interface (abridged)

interface PerpMarketAccount { status: MarketStatus; contractType: ContractType; contractTier: ContractTier; marketIndex: number; pubkey: PublicKey; name: number[]; amm: AMM; marketStats: MarketStats; marginRatioInitial: number; // MARGIN_PRECISION (1e4) marginRatioMaintenance: number; // MARGIN_PRECISION (1e4) pnlPool: PoolBalance; protocolFeePool: PoolBalance; feeLedger: FeeLedger; liquidatorFee: number; // LIQUIDATOR_FEE_PRECISION (1e6) ifLiquidationFee: number; protocolLiquidationFee: number; feePoolBufferTarget: BN; // QUOTE_PRECISION (1e6) imfFactor: number; unrealizedPnlImfFactor: number; unrealizedPnlMaxImbalance: BN; unrealizedPnlInitialAssetWeight: number; unrealizedPnlMaintenanceAssetWeight: number; insuranceClaim: { /* revenue withdraw caps and used insurance */ }; quoteSpotMarketIndex: number; feeAdjustment: number; pausedOperations: number; // bitmask, see PerpOperation poolId: number; hedgeConfig: { poolId: number; status: number; pausedOperations: number; exchangeFeeExclusionScalar: number; feeTransferScalar: number; }; oracle: PublicKey; oracleSource: OracleSource; baseAssetAmountLong: BN; // BASE_PRECISION (1e9) baseAssetAmountShort: BN; fundingClampThreshold: number; // BPS_PRECISION (1e4) fundingRampSlope: number; // PERCENTAGE_PRECISION (1e6) orderStepSize: BN; orderTickSize: BN; }

The current on-chain size is exactly 1304 bytes (up from Drift’s 1216, growing across the Anchor 1.0 alignment fix, the AMM decoupling, and the fee redesign). Any custom (non-IDL) decoder must be rebuilt against sdk/src/idl/velocity.json — see PerpMarketAccount in types.ts for the full field list.

SpotMarketAccount One per spot market

Manages spot markets and lending pools with:

  • Interest rates - Dynamic deposit/borrow rates based on utilization
  • Insurance fund (insuranceFund) - Now 100% staker-owned: totalFactor/userFactor were replaced by a single ifFeeFactor (the carveout of deposit-interest gains routed to stakers); there is no protocol-owned IF share anymore
  • Protocol fee pool (protocolFeePool) - Withdrawable protocol fee claim in this market’s token
  • Oracle integration - Price feeds for the spot asset (Pyth, Pyth Lazer; legacy pull oracles and Switchboard are deprecated, see below)
  • Asset/liability weights - Collateral weights for risk calculations

Velocity disabled the spot DLOBplaceSpotOrder, placeAndTakeSpotOrder, placeAndMakeSpotOrder, and fillSpotOrder no longer exist (calls fail with SpotDlobTradingDisabled). Spot markets still exist for collateral/borrow-lend and swaps, just not order-book trading. External fulfillment (Serum/Phoenix/OpenBook v2) is also removed entirely.

View TypeScript interface (abridged)

interface SpotMarketAccount { status: MarketStatus; assetTier: AssetTier; name: number[]; marketIndex: number; pubkey: PublicKey; mint: PublicKey; vault: PublicKey; oracle: PublicKey; oracleSource: OracleSource; historicalOracleData: HistoricalOracleData; historicalIndexData: HistoricalIndexData; insuranceFund: { vault: PublicKey; totalShares: BN; userShares: BN; ifFeeFactor: number; // IF_FACTOR_PRECISION (1e6) }; revenuePool: PoolBalance; protocolFeePool: PoolBalance; ifLiquidationFee: number; protocolLiquidationFee: number; protocolFeeFactor: number; decimals: number; optimalUtilization: number; optimalBorrowRate: number; maxBorrowRate: number; cumulativeDepositInterest: BN; cumulativeBorrowInterest: BN; depositBalance: BN; // SPOT_BALANCE_PRECISION (1e9) scaled balance borrowBalance: BN; maxTokenDeposits: BN; initialAssetWeight: number; // SPOT_WEIGHT_PRECISION (1e4) maintenanceAssetWeight: number; initialLiabilityWeight: number; maintenanceLiabilityWeight: number; liquidatorFee: number; imfFactor: number; withdrawGuardThreshold: BN; }

Markets are identified by numeric indices (market 0, market 1, etc). The SDK caches market accounts for fast lookups.

User accounts

UserAccount Holds all trading state for a specific subaccount

Each UserAccount stores:

  • Perp positions - Market index, base amount, quote entry, last funding index
  • Spot positions - Deposits and borrows per market
  • Open orders - Up to 32 active orders per user, stored inline
  • Special/status bitmasks - status (UserStatus), specialUserStatus (SpecialUserStatus, e.g. VammHedger)
  • Permissions - Delegate address and access controls

Velocity removed high leverage mode: there is no marginMode field or MarginMode enum. Leverage is governed entirely by each market’s marginRatioInitial/marginRatioMaintenance and the account’s maxMarginRatio override.

View TypeScript interface

interface UserAccount { authority: PublicKey; delegate: PublicKey; name: number[]; subAccountId: number; spotPositions: SpotPosition[]; perpPositions: PerpPosition[]; orders: Order[]; status: number; // bitmask, see UserStatus nextLiquidationId: number; nextOrderId: number; maxMarginRatio: number; // MARGIN_PRECISION (1e4); 0 = use market defaults settledPerpPnl: BN; // QUOTE_PRECISION (1e6) totalDeposits: BN; totalWithdraws: BN; totalSocialLoss: BN; cumulativePerpFunding: BN; cumulativeSpotFees: BN; liquidationMarginFreed: BN; lastActiveSlot: BN; isMarginTradingEnabled: boolean; idle: boolean; openOrders: number; hasOpenOrder: boolean; openAuctions: number; hasOpenAuction: boolean; poolId: number; specialUserStatus: number; // bitmask, see SpecialUserStatus }

UserStatsAccount Tracks aggregated stats across all subaccounts under one wallet

Maintains lifetime statistics:

  • Fee tracking - fees.totalFeePaid / totalFeeRebate / totalTokenDiscount / totalRefereeDiscount
  • Volume metrics - makerVolume30D / takerVolume30D / fillerVolume30D (rolling 30-day windows)
  • Referral data - referrer, referrerStatus (bitmask, ReferrerStatus, now including BuilderReferral)
  • Delegate permissions - delegatePermissions (gates transferDepositByDelegate)

Fuel (points/incentives) is gone entirely — there is no fuel field. The gov-token (DRIFT) stake fee discount was also removed: ifStakedGovTokenAmount was replaced by padding and no longer affects fee tiers, which are now determined purely by 30-day volume.

View TypeScript interface

interface UserStatsAccount { numberOfSubAccounts: number; numberOfSubAccountsCreated: number; makerVolume30D: BN; // QUOTE_PRECISION (1e6) takerVolume30D: BN; fillerVolume30D: BN; lastMakerVolume30DTs: BN; lastTakerVolume30DTs: BN; lastFillerVolume30DTs: BN; fees: { totalFeePaid: BN; totalFeeRebate: BN; totalTokenDiscount: BN; totalRefereeDiscount: BN; }; referrer: PublicKey; referrerStatus: number; // bitmask, see ReferrerStatus disableUpdatePerpBidAskTwap: number; pausedOperations: number; // bitmask, see UserStatsPausedOperation authority: PublicKey; delegatePermissions: number; }

Users are PDAs derived from seeds ["user", authority, subAccountId as u16 LE]. Each wallet can have multiple subaccounts (0, 1, 2, …) sharing cross-margin.

Order accounting

Orders are stored directly in the UserAccount, not as separate accounts. This reduces transaction overhead and allows up to 32 orders per user.

Each order contains:

  • Market identification - Market index and type (perp/spot)
  • Order parameters - Type (limit, market, oracle, trigger), direction, base amount, price
  • Order IDs - System order ID (orderId) and user-defined order ID (userOrderId)
  • Flags - bitFlags (OrderBitFlag) — post-only, reduce-only, and now also HasBuilder (order attaches a builder fee) and IsIsolatedPosition (order trades against an isolated-margin position)
  • Auction settings - JIT auction parameters (auctionStartPrice, auctionEndPrice, auctionDuration)

When an order fills, it’s marked as filled but not immediately removed, allowing order history tracking within the account.

View TypeScript interface

interface Order { status: OrderStatus; orderType: OrderType; marketType: MarketType; slot: BN; orderId: number; userOrderId: number; marketIndex: number; price: BN; // PRICE_PRECISION (1e6) baseAssetAmount: BN; // BASE_PRECISION (1e9) for perp baseAssetAmountFilled: BN; quoteAssetAmountFilled: BN; // QUOTE_PRECISION (1e6) direction: PositionDirection; reduceOnly: boolean; triggerPrice: BN; triggerCondition: OrderTriggerCondition; existingPositionDirection: PositionDirection; postOnly: boolean; immediateOrCancel: boolean; oraclePriceOffset: BN; // now BN (i64), was `number` on Drift auctionDuration: number; auctionStartPrice: BN; auctionEndPrice: BN; maxTs: BN; bitFlags: number; // bitmask, see OrderBitFlag postedSlotTail: number; }

Order.quoteAssetAmount from Drift is gone — it never existed on-chain (the decoder always populated it with 0); read filled quote from quoteAssetAmountFilled.

PDAs (Program Derived Addresses)

Velocity extensively uses PDAs for deterministic address generation. Seed strings are unchanged from Drift, but since the program ID is new, every derived address is different from Drift’s:

State PDA: ["velocity_state"] User PDA: ["user", authority.key(), subAccountId as u16 LE] UserStats PDA: ["user_stats", authority.key()] PerpMarket PDA: ["perp_market", marketIndex as u16 LE] SpotMarket PDA: ["spot_market", marketIndex as u16 LE] SpotMarketVault PDA: ["spot_market_vault", marketIndex as u16 LE]

The SDK provides helpers to derive these addresses without onchain calls, e.g. getUserAccountPublicKey(), getPerpMarketPublicKey(), getSpotMarketPublicKey() from @velocity-exchange/sdk.

Account relationships

State (1) ├── PerpMarket[0..N] ├── SpotMarket[0..M] └── Insurance Fund (100% staker-owned) Wallet ├── UserStats (1 per wallet) └── User[0..N] (subaccounts) ├── PerpPosition[0..8] ├── SpotPosition[0..8] └── Order[0..32]

How instructions modify state

When you call an instruction (e.g. placePerpOrder), the program:

  1. Loads accounts passed in the instruction
  2. Validates account ownership and PDAs
  3. Loads and validates oracle price data
  4. Applies protocol rules from the State account
  5. Updates UserAccount (adds order, updates positions, etc)
  6. Updates market state if needed (AMM, funding, etc)
  7. Emits event logs for offchain indexing

The SDK handles account passing automatically — you rarely need to manually construct the account list.

Remaining accounts pattern

Many instructions use “remaining accounts” to dynamically pass oracle, market, and user accounts. This lets a single instruction handle variable market sets without requiring fixed account slots. The SDK builds this list for you (VelocityClient does it internally, or use VelocityCore.remainingAccounts.getRemainingAccounts() for the stateless/subscription-free path — see Reading Data).

MarketStatus discriminants

MarketStatus is stored directly in PerpMarket.status / SpotMarket.status. Velocity removed several deprecated Drift variants, which shifted the surviving discriminants:

VariantDriftVelocity
Initialized00
Active11
ReduceOnly62
Settlement73
Delisted84

Any custom (non-IDL) decoder built against Drift’s discriminants will silently misread these states — always decode against sdk/src/idl/velocity.json, not a hardcoded enum.

Last updated on