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 × markand two gates:
| Condition | Meaning |
|---|---|
net_equity ≥ IM | You 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 ≥ MM | You 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, andnotional × imf/notional × mmfadd 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:
| Leg | Treatment |
|---|---|
Unrealized PnL size × mark − entry | A gain is haircut by unpl_weight; a loss counts in full |
| Realized PnL, accrued funding, unsettled fees | Counted 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: byunpl_weight, then bydeposit_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_mmfof 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):
| Parameter | Meaning |
|---|---|
deposit_weight | Fraction of a deposit's USD value counted as equity |
borrow_imf / borrow_mmf | Margin charged on a borrow of the asset |
liquidation_bonus | What a liquidator earns per dollar seized in this asset |
deposit_limit / borrow_limit | Market-wide caps |
| flow caps | Rate 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 / imfThere 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,0002. 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.383a. 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.27Equity 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 / 4004. 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.81The 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_pnlmoves 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
| Limit | Where it lives |
|---|---|
| Spot positions per account | GlobalConfig.max_spot_positions |
| Perp positions per account | GlobalConfig.max_linear_perp_positions |
| Open interest per contract | LinearPerp.max_open_interest_lots |
| Net size per account per contract | LinearPerp.max_position_lots, resting orders counted as filled |
| Resting orders per account per contract | 16 |
| Position size | i32 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.