Skip to content
Logo

Collateral & margin

Every trader account is cross-margined: all collateral, all borrows and all perp positions fold into one health number. There is no isolated-margin mode — a loss on one perp draws on the same equity as every other position, and a gain on one supports the rest. "Leverage" in the app is a sizing helper over that one account, not a per-position setting. On-chain, an account can opt into one account-wide leverage cap.

The health formula

The program keeps four account totals in USD:

net_equity   = Σ deposit_value × deposit_weight
             − Σ borrow_value
             + Σ perp PnL (routed through the settlement asset's balance)

net_exposure = Σ borrow_value + Σ perp notional

IM           = Σ borrow_value × borrow_imf  +  Σ perp notional × imf
                                          +  Σ order reserve × imf
MM           = Σ borrow_value × borrow_mmf  +  Σ perp notional × mmf

perp notional = |size| × mark
order reserve = lots your resting orders would add if filled × mark

and two gates:

ConditionMeaning
net_equity ≥ IMYou may add risk: open or grow a position, rest an order, borrow, withdraw. With a leverage cap set, also net_equity ≥ cap × net_exposure
net_equity ≥ MMYou are not liquidatable

IM ≥ MM always holds (a listing with mmf > imf is refused), so between them sits a band where you cannot add risk but are not liquidated — the buffer that stops a fresh position from being liquidatable the moment it opens.

The API reports these as AccountHealth: net_equity, initial_margin, maintenance_margin, free_collateral (net_equity − IM, floored at zero), deficit (MM − net_equity, floored at zero) and health_ratio (net_equity / MM; below 1 is liquidatable).

Every margin value rounds up and every collateral value rounds down, so rounding never works against the protocol.

How a perp position enters health

A perp contributes in two places:

  • Market risk. Its notional adds to net_exposure, and notional × imf / notional × mmf add to IM and MM. Notional uses the contract's mark, valued in USD through the settlement asset's price.
  • PnL. Its PnL is added to your balance of the contract's settlement asset (USDC today), and that balance is then valued like any other spot balance.

The PnL that flows into the settlement balance has two legs:

LegTreatment
Unrealized PnL size × mark − entryA gain is haircut by unpl_weight; a loss counts in full
Realized PnL, accrued funding, unsettled feesCounted at face value — already crystallized

Because the PnL lands in a spot balance, the settlement asset's own weights apply on top:

  • While your USDC balance (deposits plus PnL) is positive, it counts at USDC's deposit_weight. A paper gain is therefore haircut twice: by unpl_weight, then by deposit_weight.
  • If losses push that balance negative, it is treated exactly like a USDC borrow: it subtracts in full from equity and adds borrow_imf / borrow_mmf of itself to IM and MM.

Resting orders

A resting limit order that isn't reduce-only reserves initial margin on the lots it would add to the position if it filled whole, valued like the position at the mark. Bids and asks on one contract don't add up: the reserve is the worse of the two sides, and lots that would only shrink the position reserve nothing. Reduce-only orders reserve nothing at all.

The reserve is in IM only. It isn't exposure and doesn't touch MM, so resting orders never make an account liquidatable. They do use up free collateral, and once an account falls below IM anyone may remove its margin-reserving orders with force_cancel_perp_orders. See Orders.

Accepted collateral

Any listed spot market is collateral. Each one has its own parameters, read with MarketService.ListMarkets (MarketInfo):

ParameterMeaning
deposit_weightFraction of a deposit's USD value counted as equity
borrow_imf / borrow_mmfMargin charged on a borrow of the asset
liquidation_bonusWhat a liquidator earns per dollar seized in this asset
deposit_limit / borrow_limitMarket-wide caps
flow capsRate limits on deposits and withdrawals over time

On devnet the listed collateral is SOL, cbBTC, JLP and USDC; each network lists its own set.

A deposit lands as idle balance: collateral immediately, earning nothing. You can lend it into the market's pool to earn interest (still collateral), or borrow against your account. Borrowing, withdrawing, and lending are spot operations on the same account and are gated by the same health check.

Prices are oracle prices. For risk-increasing actions (opening, borrowing, withdrawing) collateral is valued at the oracle's collateral-credit price and debts and perp notional at its liability-cover price — the fair price shaded against you by a confidence band. Liquidation uses the unbanded fair price. See How Flint perps work.

Leverage limits

A contract's maximum leverage on equity is 1 / imf. On devnet SOL-USDC-PERP has imf = 10% (10×) and liquidates at mmf = 5% (20× on equity). Because collateral counts at its deposit_weight, leverage measured against your deposit is lower:

max notional = net_equity / imf = deposit × deposit_weight / imf

There are no tiered margin rates on-chain: imf and mmf are flat fractions of notional at any size. What bounds size instead is the contract's open-interest cap and max_position_lots, the depth makers quote, and the i32 lot width of a position. AccountService.GetTradingLimits currently returns UNIMPLEMENTED. Use OrderService.PreviewOrder for an IOC estimate against maker quotes. It excludes resting orders, and a market preview uses a fixed 0.5% price band. Simulate the transaction you intend to send over Solana RPC to check its actual order kind, bounds, and account list. AccountService.GetActionLimits reports limits for collateral actions, not perp order sizes.

Account leverage cap

An account can hold itself to a lower maximum leverage than the markets allow. set_account_leverage_cap (owner or a TRADE delegate, passkeys included) stores 1 / max_leverage on the trader account; zero lifts the cap. From then on, every move gated on initial margin must also leave

net_equity ≥ leverage_cap × net_exposure        (rounded up)

or it fails with AccountLeverageExceeded. That covers opening or growing a perp position as taker or maker (a maker's levels are trimmed to it), resting or enlarging an order, withdrawing, borrowing, ending a flash loan, backstopping another account, and repaying debt as a spot liquidator. The cap never applies to maintenance, so it can't make an account liquidatable, and an account already past a newly set cap can still reduce.

import { getSetAccountLeverageCapInstruction } from "@superis-labs/flint-idl";
 
// Hold the account to 5× of net equity.
const ix = getSetAccountLeverageCapInstruction({
  authority: wallet,
  traderAccount,
  globalConfig,
  leverageCap: (1n << 64n) / 5n, // Q0.64 fraction; 0n removes the cap
  auth: { __kind: "Wallet" },
});

The app doesn't set or show the cap yet.

Worked example

Parameters (devnet listings, read live values before relying on them): SOL-USDC-PERP imf = 0.10, mmf = 0.05, unpl_weight = 0.9, taker fee 0.10%; USDC deposit_weight = 0.9. USDC is taken at $1 and the oracle confidence band is ignored.

1. Deposit 1,000 USDC.

net_equity = 1,000 × 0.9 = 900
max notional = 900 / 0.10 = 9,000

2. Buy 50 SOL-PERP at $150 (notional $7,500).

taker fee           = 7,500 × 0.10%        = 7.50
USDC balance        = 1,000 − 7.50         = 992.50
net_equity          = 992.50 × 0.9         = 893.25
IM                  = 7,500 × 0.10         = 750.00
MM                  = 7,500 × 0.05         = 375.00
free collateral     = 893.25 − 750         = 143.25
health ratio        = 893.25 / 375         = 2.38

3a. SOL falls to $140. Unrealized PnL is −$500.

USDC balance + PnL  = 992.50 − 500         = 492.50
net_equity          = 492.50 × 0.9         = 443.25
IM / MM             = 700 / 350
health ratio        = 443.25 / 350         = 1.27

Equity is below IM, so the account can only reduce exposure, not add it — but it is above MM, so it is not liquidatable.

3b. SOL rises to $160 instead. Unrealized PnL is +$500.

weighted gain       = 500 × 0.9 (unpl_weight)      = 450
net_equity          = (992.50 + 450) × 0.9         = 1,298.25
IM / MM             = 800 / 400

4. Liquidation price. The account is liquidatable once 0.9 × (992.50 + 50 × (P − 150)) < 0.05 × 50 × P:

893.25 + 45P − 6,750 = 2.5P   →   P ≈ $137.81

The API's liquidation_price fields solve the same equation from the live account; they are indicative and do not reproduce every fixed-point rounding boundary. The liquidation page continues this example below $137.81.

Withdrawals

A withdrawal spends idle balance only and must leave the account at or above initial margin. Beyond that:

  • Unsettled PnL is not cash. A perp gain becomes withdrawable after settle_realised_pnl moves it into your balance, and a gain is paid only as far as the contract's settle pool holds losses already paid in. Keepers run settlement periodically; an unpaid gain stays owed to you.
  • Lent balance must be redeemed first, and a redeem is refused while the pool's utilisation is at its throttle_redeem_utilisation.
  • Market caps apply: a market's withdrawal flow cap, and any pause on withdrawals for that market.

AccountService.GetActionLimits returns the largest amount of each spot action the account can take right now and which constraint binds (LIMIT_CONSTRAINT_INITIAL_MARGIN, …_FLOW_CAP, and so on).

Borrowing

Borrowing mints debt in a spot market and credits the tokens to your idle balance. It needs initial margin after the borrow and is refused at the market's throttle_borrow_utilisation. Debt accrues interest on the market's utilisation curve (see Fees).

You can also end up borrowing without asking: when a realized perp loss is settled and your idle settlement-asset balance cannot cover it, the shortfall is borrowed. That debt is margined and accrues interest like any other.

Limits per account

LimitWhere it lives
Spot positions per accountGlobalConfig.max_spot_positions
Perp positions per accountGlobalConfig.max_linear_perp_positions
Open interest per contractLinearPerp.max_open_interest_lots
Net size per account per contractLinearPerp.max_position_lots, resting orders counted as filled
Resting orders per account per contract16
Position sizei32 lots (about ±2.1 billion lots)

A perp fill that would take you past the position-count limit is refused; a maker without room for a new contract is skipped rather than filled. Resting orders on a contract take a position slot even while you hold no position there.