# Account Model

> Canonical: https://docs.velocity.exchange/developers/concepts/account-model

Velocity is a Solana program that manages user accounts, positions, orders, and markets. This page is the field-level reference for the accounts it owns: what each one holds, how it is addressed, and where the layout differs from what a reader porting an older integration would expect. The [Velocity SDK](/developers/velocity-sdk/setup.md) wraps all of it, so read this when a decoded value does not mean what it appeared to.

> **Info:**
>
> Velocity has no Python SDK. A Rust client (`velocity-rs`) exists in the `velocity-v1` monorepo, which is not public, and it is not published to crates.io either. The interfaces below are TypeScript.

## Core accounts

Four account types carry essentially all the state an integration reads: one global `State`, one account per market, one per subaccount, and one per wallet.

### State account

There is exactly one `State` account per deployment, and almost every instruction loads it to apply protocol-level rules. It holds:

- **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, plus protocol fee treasuries
- **Solvency status**: Bitflag gating bankruptcy and deficit-resolution instructions independently of the withdraw-pause flags
- **Feature flags**: `exchangeStatus`, `featureBitFlags`, `lpPoolFeatureBitFlags`
- **Slot duration**: `slotDurationMs`, `pendingSlotDurationMs`, and `slotDurationEffectiveSlot`, covered in [Slot duration](/developers/concepts/slot-duration.md)

> **Info:**
>
> Velocity uses a **tiered key model** instead of a single admin key: `coldAdmin`, `warmAdmin`, `pauseAdmin`, and a set of narrowly scoped `hot*` keys (`hotAmmCrank`, `hotLpCache`, `hotFeatureFlag`, `hotFeeWithdraw`, `hotMmOracleCrank`, and others), each authorizing only the instructions it needs rather than one address with full control. There is no `state.admin` field.

<details>
<summary>View TypeScript interface (abridged)</summary>

```typescript
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;      // legacy 400ms slot-duration units, decode with millisFromStoredUnits
  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;         // legacy 400ms slot-duration units, decode with millisFromStoredUnits
  maxInitializeUserFee: number;
  featureBitFlags: number;             // bitmask, see FeatureBitFlags
  lpPoolFeatureBitFlags: number;
  solvencyStatus: number;              // bitmask, see SolvencyStatus
  slotDurationMs: number;              // u16; 0 = unset, resolves to the 400ms baseline
  pendingSlotDurationMs: number;       // u16; 0 = nothing staged
  slotDurationEffectiveSlot: BN;       // u64; slot the staged value takes effect
}
```

See `StateAccount` in the SDK's `types.ts` for the full field list.

</details>

### Market accounts

One account per market, in two flavours. Both are addressed by a numeric market index (market 0, market 1, and so on) rather than by name, and the SDK caches them after subscription so a lookup by index costs nothing.

#### PerpMarketAccount

One per perp market, 1560 bytes onchain. It carries:

- **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 [Velocity liquidity pool](/developers/vault-managers.md) (`poolId`, `status`, `pausedOperations`, `exchangeFeeExclusionScalar`, `feeTransferScalar`), replacing the earlier 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)

> **Warning:**
>
> Velocity has no vAMM LP shares: `PerpPosition.lpShares`, `lastQuoteAssetAmountPerLp`, and `perLpBase` 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*`.

<details>
<summary>View TypeScript interface (abridged)</summary>

```typescript
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;             // LIQUIDATION_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;
}
```

`PerpMarket::SIZE` is **1560 bytes** onchain, having grown across the Anchor 1.0 alignment fix, the AMM decoupling, and the fee redesign. Any custom (non-IDL) decoder must be rebuilt against the SDK's `velocity.json` IDL. See `PerpMarketAccount` in `types.ts` for the full field list.

</details>

#### SpotMarketAccount

One per spot market, 1064 bytes onchain. A spot market is both a collateral type and a lending pool, so the account carries both sets of parameters:

- **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

> **Warning:**
>
> Velocity disabled the **spot DLOB**. `placeSpotOrder`, `placeAndTakeSpotOrder`, `placeAndMakeSpotOrder`, and `fillSpotOrder` are still public on the client, but each is now a stub that always throws client-side before building a transaction. There is nothing to call onchain either: the spot order instructions were dropped from the program entirely, so they carry no discriminant in the IDL.
>
> `SpotDlobTradingDisabled` is still a live error code, raised by the shared order instructions through `validate_spot_dlob_trading_enabled_for_market_type` when they are handed a `MarketType::Spot`. Spot markets still exist for collateral, borrow-lend, and swaps, just not orderbook trading. External fulfillment through Serum, Phoenix, and OpenBook v2 is removed entirely.

<details>
<summary>View TypeScript interface (abridged)</summary>

```typescript
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;            // 1e6, the insurance-fund carveout share
  };
  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_MARKET_BALANCE_PRECISION (1e9) scaled balance
  borrowBalance: BN;
  maxTokenDeposits: BN;
  initialAssetWeight: number;       // SPOT_MARKET_WEIGHT_PRECISION (1e4)
  maintenanceAssetWeight: number;
  initialLiabilityWeight: number;
  maintenanceLiabilityWeight: number;
  liquidatorFee: number;
  imfFactor: number;
  withdrawGuardThreshold: BN;
}
```

</details>

### User accounts

#### UserAccount

One per subaccount, 4496 bytes onchain, holding all of that subaccount's trading state:

- **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

> **Warning:**
>
> Velocity removed high leverage mode: the onchain `MarginMode` enum and the `User.marginMode` field are gone. The TypeScript `MarginMode` class is still exported from the SDK, reduced to `DEFAULT` only, so existing imports do not break. Leverage is governed entirely by each market's `marginRatioInitial`/`marginRatioMaintenance` and the account's `maxMarginRatio` override.

<details>
<summary>View TypeScript interface</summary>

```typescript
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
}
```

</details>

#### UserStatsAccount

One per wallet, aggregating across every subaccount that wallet owns. Fee tiering reads it, so it is the account that decides the tier a wallet trades at:

- **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`)

> **Warning:**
>
> 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.

<details>
<summary>View TypeScript interface</summary>

```typescript
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;
  ifStakedQuoteAssetAmount: BN;
  delegatePermissions: number;
}
```

</details>

Each wallet can hold multiple subaccounts, numbered 0, 1, 2, and so on, each with its own collateral and its own liquidation risk. Cross-margin is shared within one subaccount, never across them; see [Cross-margin and subaccounts](/developers/concepts/program-structure.md#cross-margin-and-subaccounts).

### Order accounting

Orders live inside the `UserAccount` itself rather than in separate accounts, which is why the per-user cap is a fixed 32 and why placing an order does not create rent-paying state. Each order carries:

- **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**: `postOnly`, `reduceOnly`, and `immediateOrCancel` are their own boolean fields on `Order`, not bits. `bitFlags` is a separate bitmask holding the `OrderBitFlag` values: `SignedMessage` (1), `OracleTriggerMarket` (2), `SafeTriggerOrder` (4), `NewTriggerReduceOnly` (8), `HasBuilder` (16, order attaches a builder fee)
- **Auction settings**: JIT auction parameters (`auctionStartPrice`, `auctionEndPrice`, `auctionDuration`). `auctionDuration` is **not** a slot count: it stores wall-clock units of 400ms, so `10` is 4 seconds whatever the live slot duration is, and `get_auction_duration` clamps the sanitized value to between 1 and 180 units (0.4s to 72s). Decode it with `millisFromStoredUnits`, never with the live slot duration. See [Slot duration](/developers/concepts/slot-duration.md#legacy-stored-fields).

A fully filled order has its `status` set to `Filled` rather than being zeroed, so it stays readable in the array until a later order takes the slot. Only orders with `status == Open` count against the cap: the program picks the first slot whose status is anything else, so a `Filled` or `Canceled` entry is free space. Reading 32 non-empty entries therefore does not mean 32 live orders, and `MaxNumberOfOrders` is raised only when all 32 are `Open`.

<details>
<summary>View TypeScript interface</summary>

```typescript
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;             // i64 offset from oracle price
  auctionDuration: number;
  auctionStartPrice: BN;
  auctionEndPrice: BN;
  maxTs: BN;
  bitFlags: number;                  // bitmask, see OrderBitFlag
  postedSlotTail: number;
}
```

The `quoteAssetAmount` field is gone. It never existed onchain (the decoder always populated it with `0`); read filled quote from `quoteAssetAmountFilled`.

</details>

## PDAs (Program Derived Addresses)

Velocity extensively uses PDAs for deterministic address generation, derived from the following seeds:

```
State:              ["velocity_state"]
User:               ["user", authority, subAccountId as u16 LE]
UserStats:          ["user_stats", authority]
PerpMarket:         ["perp_market", marketIndex as u16 LE]
SpotMarket:         ["spot_market", marketIndex as u16 LE]
SpotMarketVault:    ["spot_market_vault", marketIndex as u16 LE]
InsuranceFundVault: ["insurance_fund_vault", marketIndex as u16 LE]
InsuranceFundStake: ["insurance_fund_stake", authority, marketIndex as u16 LE]
ReferrerName:       ["referrer_name", name]
```

Derive these with the SDK rather than by hand: `getUserAccountPublicKey()`, `getPerpMarketPublicKey()`, and `getSpotMarketPublicKey()` from `@velocity-exchange/sdk` all run offchain and cost no RPC call. A derived address depends on the program ID as well as the seeds, so an address computed against a different deployment is a different account even where the seeds match exactly. See [Program and vault addresses](/developers/concepts/program-vault-addresses.md).

## 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 an instruction touches these accounts

Every instruction follows the same shape. Taking `placePerpOrder` as the example, the program loads the accounts the transaction passed, validates their ownership and PDA derivation, loads and validates the oracle price, applies the protocol rules from `State`, writes the change into `UserAccount`, updates market state where the change requires it (AMM, funding), and emits an event log for offchain indexers.

The account list itself is not fixed. Because a margin check has to price every market the account touches, most instructions take oracle, market, and user accounts through Solana's "remaining accounts" tail rather than through named slots, so one instruction handles an account with one position and an account with eight. Callers rarely build that list by hand: `VelocityClient` assembles it internally, and `VelocityCore.remainingAccounts.getRemainingAccounts()` does the same thing on the stateless path that has no subscription. See [Reading Data](/developers/ecosystem-builders/reading-data.md).

## Account sizes can grow

Every account above is a fixed-size, zero-copy struct, so new fields normally go into reserved padding: the size and every field offset stay put. When a struct genuinely runs out of padding, it grows, and existing accounts are physically resized onchain by the `extend_account` instruction.

`extend_account` reads the target account's discriminator, resolves `8 + size_of::<T>()` for that type from the **deployed** program, transfers the rent shortfall from a payer, and grows the account data. The runtime zero-fills the new tail, so newly added fields read as zero until code writes them. It is:

- **Grow-only.** The target size is compiled in, so the instruction cannot shrink an account or inflate one to an arbitrary size.
- **Idempotent.** An account already at or beyond target size is a success no-op, so cranking twice is harmless.
- **Permissioned.** It requires the `AccountExtension` hot role (or the warm/cold admin). Extension never corrupts contents, but growing accounts costs every reader bandwidth, so the timing is the protocol's decision.
- **Zero-copy only.** Supported types include `User`, `UserStats`, `ReferrerName`, `PerpMarket`, `SpotMarket`, `State`, `InsuranceFundStake`, `PrelaunchOracle`, `PythLazerOracle`, `RevenueShare`, `LPPool`, and `Constituent`. Anything else fails with `InvalidAccountExtension`. Borsh accounts (`SignedMsgUserOrders`, `RevenueShareEscrow`, the LP-pool mapping accounts) version their layouts or ship dedicated resize instructions instead, and are deliberately rejected.

> **Warning:**
>
> **Decode length-tolerantly.** Treat the struct size the client compiled against as the number of bytes to **read**, never as the buffer length to **expect**. Concretely: never assert `data.length === EXPECTED_SIZE`, never derive a slice end from the buffer length, and never filter `getProgramAccounts` by `dataSize`. A `dataSize` filter silently matches nothing the first time a type is extended. Filter by the 8-byte discriminator with a `memcmp` instead.

The sizes quoted on this page (`PerpMarket` 1560 bytes, `SpotMarket` 1064, `User` 4496) are the sizes the current program compiles in, not permanent constants. The TypeScript SDK is already length-tolerant: Anchor's borsh coder and the custom `decodeUser` fast path both read start-relative offsets, so an extended account decodes unchanged. See [Reading Data](/developers/ecosystem-builders/reading-data.md#decoding-accounts-without-a-subscription-velocitycore) for the decode paths.

## MarketStatus discriminants

`MarketStatus` is stored directly in `PerpMarket.status` / `SpotMarket.status`. The discriminants are:

| Variant | Value |
|---|---|
| `Initialized` | 0 |
| `Active` | 1 |
| `ReduceOnly` | 2 |
| `Settlement` | 3 |
| `Delisted` | 4 |

Always decode against the SDK's `velocity.json` IDL rather than a hardcoded enum. When porting a raw decoder from an earlier program version, see the [migration guide](/developers/migrate-from-drift/agent-guide.md#5-behavioral-divergences-stop-and-report) for the discriminants it replaced.
