Skip to Content
Symmio Trading-SDK — the SDK surface for builders on Arbitrum
CoreCore ConceptsBalance Model

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

#BalanceWhere it livesRead (core / react)UnitsIn the UI
1Wallet collateralThe user’s wallet (ERC-20)getCollateralBalance / useCollateralBalancecollateralDecimalsThe deposit form’s “wallet balance” and max
2AvailableThe SubAccount, on the SYMMIO coregetAccountBalanceOf / useAccountBalanceOf18 decimalsThe account balance. Instant opens spend it; withdrawals draw from it
3Allocated (classic pool)The SubAccount’s classic poolgetAccountBalanceInfo / useAccountBalanceInfo18 decimalsNot shown in an instant / lowcap UI
4Locked position marginEach position’s Virtual Account (its allocatedBalance)Per-quote cva + lf + partyAmm via useManagedQuotes18 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-20

On 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 allocatedBalance automatically returns to the SubAccount’s available balance.
  • Add margin (addMargin to a VA) — moves balance from the SubAccount’s available → the VA’s allocatedBalance, topping up the position’s margin.
  • Remove margin (removeMargin) — moves balance from the VA’s allocatedBalance → 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) ──▶ available

Instant 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 / depositAndAllocateForAccount move 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:

ValueUnits
Deposit / withdraw amounts, wallet collateral balancecollateralDecimals (getChainConfig().addresses.collateralDecimals, e.g. 6 for USDC)
Gasless deposits: a GaslessWallet’s collateral balance, depositFee, minimumDeposit, the wallet creation feecollateralDecimals
Account balances (available, allocated, locks), allocate / deallocate amounts, quote values18 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 overstated

collateralToCore18 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, pass live: true so it refetches on trade settles).
  • Trade-form Max — not the raw available balance. Use useAvailableInstantOpenMargin, 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.
Last updated on