Opening a position
Opening involves two EIP-712 operations signed and POSTed together, then a wait on the notifications feed for the on-chain confirmation.
Compute the numbers
Pick a symbol_id, a side (long/short), a leverage (integer ≥ 1), and a slippage tolerance (a percentage, e.g. 5 for 5%). Read the latest mark price from your price-feed cache.
Then derive the worst-acceptable entry price. The whole point of slippage is to bound how much the price can drift between when you sign and when the trade lands; for an open you accept slippage in the direction against you:
LONG
price = mark × (1 + slippage / 100) (will pay more)
SHORT
price = mark × (1 − slippage / 100) (will sell for less)
Now hit the locked-params endpoint with this symbol's name and your chosen leverage. You get back integer percentages: cva, lf, partyAmm, partyBmm. These define what gets locked on-chain when the trade opens. Compute the wei-denominated amounts:
notional = price × quantity // in collateral units
cva = notional × cva_pct / (100 × leverage)
lf = notional × lf_pct / (100 × leverage)
partyAmm = notional × partyAmm_pct / (100 × leverage)
partyBmm = notional × partyBmm_pct / 100 // usually 0Round price and quantity to the symbol's price_precision and quantity_precision before converting to 18-decimal wei. Dust beyond those precisions causes silent rejection.
Compute addMargin
addMarginThis deserves its own section because the formula is not symmetric between longs and shorts. Longs are simple; shorts have an extra cushion that's a UI policy, not a contract requirement.
What addMargin has to cover
addMargin has to coverThe addMarginToNextVA call moves collateral from the sub-account to the soon-to-be-created VA. The VA needs to hold enough to cover:
The locked margin for the trade,
cva + lf + partyAmm(pluspartyBmmif the deployment uses it). The four values always sum tonotional / leverage, because the locked-params percentages respect that constraint at every leverage rung.The trading fee for both sides of the round-trip. The
trading_feefield is a per-side fraction (e.g.0.00012= 12 bps), so reserve2 × trading_feeof the locked notional.For shorts, an adverse-drift buffer the frontend bakes in (see below).
The two formulas
Let slip be the user's slippage tolerance as a fraction (0.05 for 5%) and fee the symbol's per-side trading_fee fraction.
LONG
worst = mark × (1 + slip)
addMargin = worst × qty / leverage × (1 + 2 × fee)
SHORT
mark × 1.10
addMargin = mark × 1.10 × qty / leverage × (1 + 2 × fee)
For a LONG, worst is the highest price you've consented to pay, so the locked margin sized at worst already covers the worst-case fill. No extra buffer needed beyond fees.
For a SHORT, the frontend sizes margin at mark × 1.10, a flat +10% above mark, regardless of slippage or leverage. This isn't derivable from contract semantics; the contract's strict minimum is just mark × qty / leverage (covering a fill at the prevailing mark). The extra +10% is the frontend's hardcoded safety policy for shorts. It pre-funds the position for adverse upward drift between signing and fill, since a short's maintenance margin requirement grows with mark.
Both addMargin and the four locked values go into the on-chain calldata. Convert all of them to 18-decimal wei before encoding.
Sign two EIP-712 SignedOperations
SignedOperationsThe InstantLayer is the gateway for both ops. Its EIP-712 domain is fixed:
The SignedOperation struct has seven fields, and the order matters because EIP-712 hashes are sensitive to field order:
Remember to include flexFields and maxUses in your typed-data: if the EIP-712 hash on your side ends up shorter than what the InstantLayer computes, your signature recovers to a meaningless address, and the solver returns 403 "invalid signature." If you see that, check the schema first.
Both ops are signed by the session key, but they hit different contracts and different functions.
Op 1: pre-fund the VA.
signer: session key's addresstarget: AccountLayersignerAccount.addr: sub-account (not the VA, which doesn't exist yet)callData: selector0xa6d66852followed by ABI-encoded(subAccount: address, vaIsolation: uint8, symbolId: uint256, amountWei: uint256).vaIsolationis2for LONG (MARKET_LONG) or3for SHORT (MARKET_SHORT) under aMARKET_DIRECTIONsub-account.
Op 2: send the quote.
signer: session key's addresstarget: Symmio CoresignerAccount.addr: sub-account (still not the VA)callData: selector0xa7f3b34bfollowed by the ABI-encoded arguments ofsendQuoteWithAffiliateAndData:
★ The upnlSig tuple is an empty placeholder (bytes "", uint256 0, int256 0, uint256 0, bytes "", (uint256 0, address 0, address 0)). The bind step is what makes the core skip the Muon check on this empty value.
data is abi.encode((string,)) of a UUID string the solver uses, the contract doesn't read it.
Both ops share a replayAttackHeader with nonce: 0 (salt-only mode), a deadline an hour or so in the future, and a unique random salt (32 bytes). flexFields: [], maxUses: 1.
Subscribe, then POST
Subscribe to the Notifications WebSocket before you POST. This is a hard requirement, not advice. The solver fires the SendQuoteTransaction notification within seconds of accepting your trade, and if you POST before subscribing, the message can race past your first recv() and you'll never see it.
Subscription frame:
Once subscribed, POST { "addMargin": op1, "sendQuote": op2 } to the instant-open endpoint as a JSON object (not an array; closes are arrays). No auth header: the EIP-712 signatures are the auth. The solver responds:
The temp_quote_id is negative and is not your real quote ID. It's a placeholder the solver uses to correlate this RFQ to the on-chain transaction. Wait on the WS until you see a message matching this temp_quote_id whose last_seen_action == "SendQuoteTransaction" and action_status == "success". That message carries the real quote_id and the va_address. Record both. Typical latency end-to-end is 5–20 seconds.
If you see action_status == "failed" or state_type == "alert" instead, exit the wait loop. Common causes:
error_code: 2000withva_address == "": the on-chainexecuteTemplatereverted with"LibMuon: Expired signature". The sub-account isn't bound (see bind the sub-account).error_code: 502onInstantRFQ: transient solver issue. Retry.Other
error_codevalues surface from the core's revert reasons.
After a successful open the position is identified for the rest of its life by (quote_id, va_address). Save them.
Last updated

