Balance Model
Four different numbers can all be called “the balance” in SYMMIO. Wiring the wrong one into a UI is the single most common integration mistake, so this page fixes the model once: which balances exist, which one trading actually spends, and which read backs which part of your UI.
Instant trading spends the available balance. Fund a SubAccount with a plain deposit and trade — there is no
allocate step. allocate / deallocate / depositAndAllocateForAccount belong to the classic pool flow, which
the instant (lowcap) product does not use; allocating actually reduces what an instant open can spend. If you
learned the older SYMMIO “deposit → allocate → trade” model, unlearn it here: on this product the flow is deposit →
trade.
The four balances
| # | Balance | Where it lives | Read (core / react) | Units | In the UI |
|---|---|---|---|---|---|
| 1 | Wallet collateral | The user’s wallet (ERC-20) | getCollateralBalance / useCollateralBalance | collateralDecimals | The deposit form’s “wallet balance” and max |
| 2 | Available | The SubAccount, on the SYMMIO core | getAccountBalanceOf / useAccountBalanceOf | 18 decimals | The account balance. Instant opens spend it; withdrawals draw from it |
| 3 | Allocated (classic pool) | The SubAccount’s classic pool | getAccountBalanceInfo / useAccountBalanceInfo | 18 decimals | Not shown in an instant / lowcap UI |
| 4 | Locked position margin | Each position’s Virtual Account (its allocatedBalance) | Per-quote cva + lf + partyAmm via useManagedQuotes | 18 decimals (wei) | Per-position margin detail in the positions table |
The lifecycle
wallet ERC-20 (collateralDecimals)
│ approveCollateral → depositForAccount ← the whole funding step
▼
available balance (18-dec) = getAccountBalanceOf
│ instant open — margin moves into the position's Virtual Account (becomes its allocatedBalance)
▼
locked position margin, per-VA = cva + lf + partyAmm (locked within the VA's allocatedBalance)
│ close a position (uPnL applied):
│ • VA still holds others → margin stays in the VA's allocatedBalance
│ • last position closed → VA's allocatedBalance auto-returns to available
▼
available balance
│ initiateWithdraw → cooldown → finalizeWithdrawRequest
▼
wallet ERC-20On an instant open the margin is not burned — it is moved from the SubAccount’s available balance into the position’s Virtual Account (VA), where it becomes that VA’s allocatedBalance. Of that, the locked position margin is cva + lf + partyAmm (Credit Valuation Adjustment + Liquidation Fee + PartyA maintenance margin).
Margin flow between the SubAccount and its VAs. A VA can hold more than one position — under MARKET isolation (one VA per market) or MARKET_DIRECTION (one VA per market + side); under POSITION / CUSTOM each position gets its own VA. So closing a position does not always return funds to the SubAccount:
- Close a position while the VA still holds others — the freed margin (with realized uPnL applied) stays in the VA: it unlocks and adds to the VA’s
allocatedBalance, usable as margin for the VA’s remaining positions. - Close the last position in a VA — the VA’s entire
allocatedBalanceautomatically returns to the SubAccount’s available balance. - Add margin (
addMarginto a VA) — moves balance from the SubAccount’s available → the VA’sallocatedBalance, topping up the position’s margin. - Remove margin (
removeMargin) — moves balance from the VA’sallocatedBalance→ the SubAccount’s available, and requires a Muon uPnL signature proving the VA stays solvent after the deallocation.
The classic pool is a side branch the instant flow never takes:
available ── allocate ──▶ allocated (classic pool) ── deallocate (Muon uPnL sig) ──▶ availableInstant flow vs. classic pool
The SDK exposes both families because the contracts do, but they serve different products:
- Instant (lowcap) flow — the default on Arbitrum. Fund with
depositForAccount(useDeposit), trade with the instant open/close actions, withdraw with the withdraw slice. The only balance the user manages is available. - Classic pool flow.
allocate/deallocate/depositAndAllocateForAccountmove funds into and out of the allocated pool where the CVA / LF locks of classic (non-instant) positions sit. Not supported end-to-end in this SDK version — planned for a later release.
Do not “onboard” users through depositAndAllocateForAccount or an allocate step. Funds parked in the classic
pool are not what an instant open consumes — the trade form’s margin reads zero even though the user just deposited.
The symptom is exactly that: “deposited, but can’t trade.”
Core 18-decimal units
SYMMIO Core keeps every balance in 18 decimals, whatever the collateral token’s own decimals are. These are Core units. Token amounts stay in the token’s decimals, so two scales meet wherever funds cross between a wallet and Core:
| Value | Units |
|---|---|
| Deposit / withdraw amounts, wallet collateral balance | collateralDecimals (getChainConfig().addresses.collateralDecimals, e.g. 6 for USDC) |
Gasless deposits: a GaslessWallet’s collateral balance, depositFee, minimumDeposit, the wallet creation fee | collateralDecimals |
| Account balances (available, allocated, locks), allocate / deallocate amounts, quote values | 18 decimals (Core units) |
Operational-fee allowances and approvals, every GaslessFeeQuote amount (totalFee18, …) | 18 decimals (Core units) |
Parse deposit inputs with parseUnits(input, collateralDecimals) — not parseEther. Format an account balance with formatUnits(value, 18). Mixing the scales balloons or collapses the number by 10^(18 − collateralDecimals); the tell is a balance the size of 1e18.
Converting between the scales
Convert with collateralToCore18 and core18ToCollateral rather than hand-writing the power of ten. Both are plain bigint math, with no floating point:
import { collateralToCore18, core18ToCollateral } from "@symmio/trading-core";
const { collateralDecimals } = config.getChainConfig(chainId).addresses;
collateralToCore18(1_000000n, collateralDecimals); // 1_000000000000000000n — 1 USDC in Core units
core18ToCollateral(quote.totalFee18, collateralDecimals); // the fee in token units, rounded up
core18ToCollateral(balance18, collateralDecimals, { rounding: "down" }); // a balance, never overstatedcollateralToCore18 is always exact. core18ToCollateral rounds up by default: a fee or shortfall converted to token units must never come out short, so this is the rounding to fund with. Pass { rounding: "down" } for a value you display or pay out. Both throw COLLATERAL_DECIMALS_UNSUPPORTED unless the decimals are an integer from 0 to 18; Core accepts no collateral token with more than 18 decimals.
Operational fees are Core units too
A gasless relay charges its operational fee in Core units from the payer’s Core balance, free balance first and then allocated, after the operation executes. The payer’s operational-fee allowance caps that charge and is 18-decimal as well. An allowance adds no balance: a payer with a large allowance and too little Core balance still fails with OperationalFee: Insufficient balance. When the operation moves funds itself, budget its amount and its fee together. A relayed withdrawal, for example, needs collateralToCore18(amount, collateralDecimals) + fee18 of Core balance. See Withdraw hooks.
Which number do I show?
- Account balance / fund gate — available balance (
useAccountBalanceOf, passlive: trueso it refetches on trade settles). - Trade-form
Max— not the raw available balance. UseuseAvailableInstantOpenMargin, which shaves the balance for the open fee and worst-case slippage (the close fee is charged at close from the position, not shaved here). - Deposit modal — wallet collateral balance (
useCollateralBalance) plus the allowance (useCollateralAllowance) for the approve step. - Withdraw max — available balance. When the withdrawal is relayed, subtract its operational fee first (both in Core units).
- Per-position margin — the quote’s own locked values via
useManagedQuotes, not an account-level read.
Related
- Build a Perps DEX — Collateral — the deposit + withdraw UI recipe.
depositForAccount— the funding write.- AccountLayer — every balance read and write.
- Withdraw hooks — the cooldown-gated exit flow.
- Value helpers —
collateralToCore18andcore18ToCollateral.