Instant Trading in v0.8.5

The biggest change to instant trading in the current version is that now the broad delegateAccess() is gone and replaced by a more specific, signature-driven flow on the new Instant Layer that lives next to a standalone Account Layer diamond.

The frontend's job is to set up the user's account on the Account Layer (formerly MultiAccount), optionally bind them to a single PartyB so Muon checks can be skipped (for faster trade finality), then issue scoped delegations on the Instant Layer so a session key (or a TP/SL bot) can sign trade operations on the user's behalf. From that point on the user's wallet doesn't need to come out — every open, close, and TP/SL request is signed by the session key, and the solver's HTTP API accepts the signed operations directly.

End-to-end Summary

The lifecycle for a brand-new user, all the way from wallet connect to placing TP/SL on a live position:

ONE-TIME (signed by user EOA, expires never)
  1. createSubAccounts        AccountLayer
  2. depositForAccount        AccountLayer  (Symmio Core is the actual ERC20 spender)
  3. bindToPartyB             AccountLayer._call(sub, [bindToPartyB(hedger)])

PER-SESSION (signed by user EOA, expires in ~24h)
  4. grantDelegation          InstantLayer  (session key:  trading + COH + 0x00000001)
  5. grantDelegation          InstantLayer  (TPSL bot:     close selectors + 0x00000001)

PER-TRADE (signed by SESSION KEY only)
  6. POST /api/instant_trade/instant_open    {addMargin SignedOp, sendQuote SignedOp}
  7. POST /api/instant_trade/instant_close   [close SignedOp]
  8. POST /api/v5/                            {ConditionalOrder typed data + sig}

The session key signs steps 6–8. The user's wallet only ever has to come out for steps 1–5.

1. Create a sub-account on the Account Layer

createSubAccounts(affiliate, [creation]) creates the user's top-level trading account on the Account Layer. The creation struct pins the affiliate, the Symmio core instance, the isolation type, and singleVAMode. singleVAMode=True is required if you want TP/SL through the Conditional Orders Handler to work — the solver's conditional order handler service validates that the position's VA matches predictNextVirtualAccountAddress(sub, iso, symbol), and without single-VA mode each quote spawns a fresh VA address that breaks that check.

Code Snippet:

isolationType constants from _common.py:

Pick what fits your strategy: POSITION for fully isolated per-trade margin, MARKET to share margin across all trades on a symbol, MARKET_DIRECTION to share within a (symbol, direction) bucket, CUSTOM for caller-supplied VA addresses.

SUB_ISO_MARKET_DIRECTION is the most common accountType to use


2. Deposit collateral

depositForAccount(sub, amount) lives on the Account Layer, but the actual ERC20 transferFrom happens inside the Symmio Core diamond — so Symmio Core is the address that needs the allowance, not the Account Layer.

The to_collateral_units helper reads collateral.decimals() once at startup so the same code works for 6-decimal USDC and 18-decimal stablecoins. Internally Symmio uses 18-decimal fixed-point math, but the actual ERC20 transfer is in the token's native decimals.


3. Bind the sub-account to a PartyB (oracle-less mode)

This is the key v0.8.5 optimization. Without a bind, every sendQuote runs full LibMuon signature validation — and the solver's instant-trade flow uses an empty Muon signature (the solver injects the real signature itself when it submits the batch). Without the bind, the contract rejects that empty signature with LibMuon: Expired signature.

Once bound, the sub-account trusts that PartyB; Muon checks are skipped on the trading path against that PartyB, and the solver's template flow works.

From steps/02b_bind_partyb.py:

The full script also reads back getBindState(sub) on Symmio Core and skips the tx if the sub-account is already bound. To unbind later: requestToUnbindFromPartyB → wait the cooldown → completeUnbindRequest.


4. Generate a session key and grant delegations

This is the core of the new flow. On every session you:

  1. Generate a fresh session key in-process.

  2. Call grantDelegation on the Instant Layer to give the session key the trading selectors.

  3. Call grantDelegation again to give the TPSL bot its own narrower close-selector set.

Both of those grantDelegation calls are signed by the user EOA — that's the only time the wallet is needed during this session. From there on, every signed operation is produced by the session key's private key.

Selectors:

grantDelegation from steps/03_grant_delegation.py:

Delegations expire at expiryTimestamp and can be revoked early through the standard two-step cooldown: initiateRevokeDelegation → wait revocationCooldownfinalizeRevokeDelegation.


5. The EIP-712 SignedOperation helper

Every per-trade call (open, close, TP/SL) is an EIP-712 SignedOperation signed by the session key. The helper builds the typed-data dict, signs it with eth_account.sign_typed_data, and returns the operation in the wire format the solver expects.

  • signerAccount.addr differs by operation. On open it's the sub-account (because sendQuote runs from the sub-account, which then creates the VA). On close it's the virtual account (because the position lives on the VA and the close acts on it).

  • Salt-only nonces. nonce=0 lets multiple operations execute in any order as long as each salt is unique. Use sequential nonces only when ordering matters.

  • maxUses=1 for standard ops. maxUses=0 means unlimited until the deadline, useful for flex-field flows.

  • String-encoded numerics on the wire. The solver's HTTP layer expects "0" / "1" / decimal strings for nonce, maxUses, deadline even though they're typed as uint256. The signed hash uses the integers; the wire format stringifies.


6. Open a position (instant_open)

Two SignedOperation ops, posted as one batch:

  1. addMarginToNextVA(subAccount, vaIsoType, symbolId, marginWei) on the Account Layer — pre-funds the next predicted VA address with margin transferred from the sub-account.

  2. sendQuoteWithAffiliateAndData(...) on Symmio Core — sends the quote from the new VA, with an empty Muon-sig sentinel (the solver injects the real one) and the data field carrying a client-side correlation ID.

Calldata encoders from steps/_common.py:

Opening a position:

The solver returns a temp_quote_id immediately. The real on-chain quote_id arrives a few seconds later via the notifications websocket.

Subscribe to the WS first, POST inside the same coroutine, then block on SendQuoteTransaction(temp=…, status=success) and read out quote_id and va_address. Save those to your state — close and TP/SL both need the VA.

Locked-param formulas

Locked params come from GET {SOLVER_BASE}/get_locked_params/<symbol>?leverage=<n> — values are percentages.

v0.8.5 nuances on the open path

  • Custom quote data. The data field on sendQuoteWithAffiliateAndData persists arbitrary bytes on the quote. The reference implementation stuffs a UUID into it via abi_encode(["(string)"], [(uuid,)]) so the off-chain system can correlate websocket reports back to the trade.

  • Empty Muon sig. Because the sub-account is bound to PartyB (step 3), the contract skips Muon validation — the solver's empty sentinel passes. Don't try to fill in real Muon data; the solver injects what it needs server-side.

  • Cross-mode solvers may report nonce: 0 on PartyB-side signatures (parallel-safe). Doesn't change anything you send.


7. Close a position (instant_close)

Single SignedOperation. The signer is still the session key, but signerAccount.addr is the virtual account the position lives on — not the sub-account.

The request body is a JSON array of one wrapped op (the close endpoint takes a list, even when there's only one).

Code Snippet:

Slippage direction is opposite to entry: closing a LONG accepts a price down to mark*(1-slip), closing a SHORT accepts a price up to mark*(1+slip).


8. Set Take-Profit / Stop-Loss (Conditional Orders Handler)

TP/SL is a separate EIP-712 typed-data scheme signed by the session key and posted to the Conditional Orders Handler service. It is not a SignedOperation. The COH stores the signed order and watches prices; when the trigger hits, the COH submits a requestToClosePosition itself, using its own delegation that you granted in step 4.

The EIP-712 domain and types:

Build, sign, submit:

NOTE: Every numeric string in the order should match the symbol's price_precision / quantity_precision. The COH may reject unbounded decimals with error 407 "provided values do not meet the required precision."

Authentication. Some Conditional Order Handler services validate the session key's authorization by calling isDelegationActive(subAccount, signer, 0x00000001) on-chain — so the session key must be granted 0x00000001 (and the COH opaque close selectors) in step 4.

To set only one leg, pass the other as None and leg(None) returns the ZERO_LEG payload the COH expects.

Last updated