> For the complete documentation index, see [llms.txt](https://docs.symm.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.symm.io/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading/instant-trading-in-v0.8.5.md).

# 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.&#x20;

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:

```python
from _common import (USER_PRIVATE_KEY, AFFILIATE, SYMMIO_CORE,
                     account_layer, send_tx, save_state,
                     SUB_ISO_MARKET_DIRECTION)
from eth_account import Account


def main():
    user = Account.from_key(USER_PRIVATE_KEY)
    creation = {
        "name":          "bot-demo",
        "metadata":      b"",
        "symmioCore":    SYMMIO_CORE,
        "isolationType": SUB_ISO_MARKET_DIRECTION,   # 2 — long/short bucketed per symbol
        "singleVAMode":  True,                       # required for TP/SL
    }
    tx = account_layer.functions.createSubAccounts(
        AFFILIATE, [creation]).build_transaction({"from": user.address})
    send_tx(USER_PRIVATE_KEY, tx, label="createSubAccounts(bot-demo)")

    subs = account_layer.functions.getUserSubAccountsAddresses(
        user.address, 0, 200).call()
    save_state(sub_account=subs[-1], owner=user.address,
               isolation_type=SUB_ISO_MARKET_DIRECTION)
```

`isolationType` constants from `_common.py`:

```python
SUB_ISO_POSITION, SUB_ISO_MARKET, SUB_ISO_MARKET_DIRECTION, SUB_ISO_CUSTOM = 0, 1, 2, 3
```

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.

{% hint style="info" %}
`SUB_ISO_MARKET_DIRECTION` is the most common `accountType` to use
{% endhint %}

***

### 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.

```python
from _common import (USER_PRIVATE_KEY, SYMMIO_CORE,
                     account_layer, collateral, send_tx,
                     to_collateral_units)
from eth_account import Account


def deposit(sub_account: str, amount):
    user          = Account.from_key(USER_PRIVATE_KEY)
    amount_native = to_collateral_units(amount)   # uses ERC20.decimals()

    # 1) approve Symmio Core if allowance is short
    cur = collateral.functions.allowance(user.address, SYMMIO_CORE).call()
    if cur < amount_native:
        tx = collateral.functions.approve(SYMMIO_CORE, 2**256 - 1).build_transaction(
            {"from": user.address, "gas": 120_000})
        send_tx(USER_PRIVATE_KEY, tx, label="approve(collateral, SymmioCore)")

    # 2) deposit
    tx = account_layer.functions.depositForAccount(
        sub_account, amount_native).build_transaction(
            {"from": user.address, "gas": 500_000})
    send_tx(USER_PRIVATE_KEY, tx,
            label=f"depositForAccount({amount_native} -> {sub_account})")
```

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`:

```python
from eth_abi import encode as abi_encode
from eth_account import Account
from _common import (USER_PRIVATE_KEY, HEDGER, SYMMIO_CORE,
                     account_layer, send_tx, w3)

SEL_BIND_TO_PARTYB = bytes.fromhex("cf462cb2")  # bindToPartyB(address)


def bind(sub_account: str):
    user = Account.from_key(USER_PRIVATE_KEY)

    # `bindToPartyB` is on Symmio Core, but the sub-account is a virtual
    # address on the Account Layer — the AccountLayer routes through `_call`
    # so Symmio Core sees the right msg.sender.
    bind_cd = SEL_BIND_TO_PARTYB + abi_encode(["address"], [HEDGER])
    tx = account_layer.functions._call(sub_account, [bind_cd]).build_transaction(
        {"from": user.address, "gas": 600_000})
    send_tx(USER_PRIVATE_KEY, tx, label=f"_call(bindToPartyB({HEDGER}))")
```

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:

```python
SEL_ADD_MARGIN_TO_NEXT_VA          = bytes.fromhex("a6d66852")
SEL_SEND_QUOTE_WITH_AFFILIATE_DATA = bytes.fromhex("a7f3b34b")
SEL_REQUEST_TO_CLOSE_POSITION      = bytes.fromhex("501e891f")

# Identifiers used by the TPSL / Conditional Orders Handler service. These are NOT real Symmio function selectors;
SEL_COH_REQUEST_TO_CLOSE           = bytes.fromhex("eaa31b19")
SEL_SESSION_KEY                    = bytes.fromhex("00000001")
```

`grantDelegation` from `steps/03_grant_delegation.py`:

```python
import time
from eth_account import Account
from _common import (USER_PRIVATE_KEY, TPSL_BOT_ADDRESS,
                     instant_layer, send_tx, save_state,
                     SEL_ADD_MARGIN_TO_NEXT_VA,
                     SEL_SEND_QUOTE_WITH_AFFILIATE_DATA,
                     SEL_REQUEST_TO_CLOSE_POSITION,
                     SEL_COH_REQUEST_TO_CLOSE,
                     SEL_SESSION_KEY)


def grant_session_and_tpsl(sub_account: str, hours: int = 24):
    session = Account.create()
    expiry  = int(time.time()) + hours * 3600

    # 1) Session key gets EVERYTHING needed for opens, closes, and TP/SL auth.
    info_session = (
        (sub_account, False),                    # Account{addr, isPartyB=False}
        session.address,                          # delegatedSigner
        [SEL_ADD_MARGIN_TO_NEXT_VA,               # a6d66852  AccountLayer  (opens)
         SEL_SEND_QUOTE_WITH_AFFILIATE_DATA,      # a7f3b34b  Symmio core    (opens)
         SEL_REQUEST_TO_CLOSE_POSITION,           # 501e891f  Symmio core    (closes)
         SEL_COH_REQUEST_TO_CLOSE,                # eaa31b19  (TPSL auth)
         SEL_SESSION_KEY],                        # 00000001  
        expiry,
    )
    tx = instant_layer.functions.grantDelegation(info_session).build_transaction(
        {"gas": 300_000})
    send_tx(USER_PRIVATE_KEY, tx,
            label=f"grantDelegation(sessionKey={session.address})")

    # 2) TPSL bot gets the close-selector set the COH validates against.
    #    The COH tries multiple selector variants — grant all of them.
    info_tpsl = (
        (sub_account, False),
        TPSL_BOT_ADDRESS,
        [SEL_REQUEST_TO_CLOSE_POSITION,           # 501e891f  real close
         SEL_COH_REQUEST_TO_CLOSE,                # eaa31b19  COH 
         bytes.fromhex("ee9ef781"),               # requestToClosePosition w/ upnlSig overload
         SEL_SESSION_KEY],                        # 00000001
        expiry,
    )
    tx = instant_layer.functions.grantDelegation(info_tpsl).build_transaction(
        {"gas": 300_000})
    send_tx(USER_PRIVATE_KEY, tx, label=f"grantDelegation(tpslBot={TPSL_BOT_ADDRESS})")

    save_state(session_pk=session.key.hex(),
               session_address=session.address,
               delegation_expiry=expiry)
```

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

***

### 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.

```python
import secrets, time
from typing import Optional
from eth_account import Account
from web3 import Web3


def _so_typed(op: dict) -> dict:
    return {
        "types": {
            "EIP712Domain": [
                {"name": "name",              "type": "string"},
                {"name": "version",           "type": "string"},
                {"name": "chainId",           "type": "uint256"},
                {"name": "verifyingContract", "type": "address"},
            ],
            "Account": [
                {"name": "addr",     "type": "address"},
                {"name": "isPartyB", "type": "bool"},
            ],
            "FlexField": [
                {"name": "offset",               "type": "uint256"},
                {"name": "length",               "type": "uint256"},
                {"name": "authorizedFlexFiller", "type": "address"},
            ],
            "ReplayAttackHeader": [
                {"name": "nonce",    "type": "uint256"},
                {"name": "deadline", "type": "uint256"},
                {"name": "salt",     "type": "bytes32"},
            ],
            "SignedOperation": [
                {"name": "signer",             "type": "address"},
                {"name": "target",             "type": "address"},
                {"name": "callData",           "type": "bytes"},
                {"name": "signerAccount",      "type": "Account"},
                {"name": "flexFields",         "type": "FlexField[]"},
                {"name": "maxUses",            "type": "uint256"},
                {"name": "replayAttackHeader", "type": "ReplayAttackHeader"},
            ],
        },
        "primaryType": "SignedOperation",
        "domain": {
            "name":              "SymmioInstantLayer",
            "version":           "1",
            "chainId":           CHAIN_ID,
            "verifyingContract": INSTANT_LAYER,
        },
        "message": op,
    }


def sign_operation(session_pk: str, target: str, call_data: bytes,
                   account_addr: str, deadline: Optional[int] = None) -> dict:
    """
    `account_addr` is the trading account the operation runs against:
        - sub-account on OPEN
        - virtual account on CLOSE
    """
    sk       = Account.from_key(session_pk)
    deadline = deadline or (int(time.time()) + 3600)
    call_hex = "0x" + call_data.hex()
    salt_hex = "0x" + secrets.token_bytes(32).hex()

    op = {
        "signer":   sk.address,
        "target":   Web3.to_checksum_address(target),
        "callData": call_hex,
        "signerAccount": {
            "addr":     Web3.to_checksum_address(account_addr),
            "isPartyB": False,
        },
        "flexFields": [],
        "maxUses":    1,
        "replayAttackHeader": {
            "nonce":    0,                  # salt-only mode (parallel-friendly)
            "deadline": deadline,
            "salt":     salt_hex,
        },
    }
    signed = sk.sign_typed_data(full_message=_so_typed(op))
    sig    = "0x" + signed.signature.hex().lstrip("0x")

    # Wire format expected by the solver: stringify the numeric fields.
    return {
        "signedOperation": {
            "signer":   sk.address,
            "target":   op["target"],
            "callData": call_hex,
            "signerAccount": {"addr": op["signerAccount"]["addr"], "isPartyB": False},
            "flexFields": [],
            "maxUses":  "1",
            "replayAttackHeader": {
                "nonce":    "0",
                "deadline": str(deadline),
                "salt":     salt_hex,
            },
        },
        "signature": sig,
    }
```

* **`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`:

```python
from eth_abi import encode as abi_encode
from web3 import Web3


def encode_add_margin_to_next_va(sub_account, va_iso, symbol_id, amount_wei) -> bytes:
    return SEL_ADD_MARGIN_TO_NEXT_VA + abi_encode(
        ["address", "uint8", "uint256", "uint256"],
        [Web3.to_checksum_address(sub_account), va_iso, symbol_id, amount_wei])


def encode_send_quote(symbol_id, position_type, order_type, price_wei, quantity_wei,
                      cva, lf, pa_mm, pb_mm, deadline, affiliate, hedger,
                      data_bytes=b"") -> bytes:
    # Empty SingleUpnlAndPriceSig sentinel; the solver injects the real Muon sig.
    # ABI tuple: (bytes, uint256, int256, uint256, bytes, (uint256, address, address))
    empty_sig = (b"", 0, 0, 0, b"", (0, "0x" + "00"*20, "0x" + "00"*20))
    return SEL_SEND_QUOTE_WITH_AFFILIATE_DATA + abi_encode(
        ["address[]", "uint256", "uint8", "uint8",
         "uint256", "uint256", "uint256", "uint256",
         "uint256", "uint256", "uint256", "address",
         "(bytes,uint256,int256,uint256,bytes,(uint256,address,address))",
         "bytes"],
        [[Web3.to_checksum_address(hedger)], symbol_id, position_type, order_type,
         price_wei, quantity_wei, cva, lf, pa_mm, pb_mm, deadline,
         Web3.to_checksum_address(affiliate), empty_sig, data_bytes])
```

Opening a position:

```python
import time, uuid, requests
from decimal import Decimal
from eth_abi import encode as abi_encode
from web3 import Web3
from _common import (ACCOUNT_LAYER, SYMMIO_CORE, AFFILIATE, HEDGER, SOLVER_BASE,
                     POSITION_LONG, POSITION_SHORT, ORDER_MARKET,
                     VA_ISO_MARKET_LONG, VA_ISO_MARKET_SHORT,
                     encode_add_margin_to_next_va, encode_send_quote,
                     sign_operation, fetch_locked_params, get_symbol,
                     price_of, to_wei)


def open_position(sub_account, session_pk, symbol_id, quantity, side,
                  leverage, slippage_pct=Decimal("5")):
    side_l        = side.lower()
    position_type = POSITION_LONG if side_l == "long" else POSITION_SHORT
    va_iso        = VA_ISO_MARKET_LONG if position_type == POSITION_LONG else VA_ISO_MARKET_SHORT
    slippage      = slippage_pct / 100

    sym         = get_symbol(symbol_id)
    price_prec  = int(sym["price_precision"])
    qty_prec    = int(sym["quantity_precision"])
    trading_fee = Decimal(str(sym["trading_fee"]))

    mark      = price_of(sym["name"])
    s_mult    = (1 + slippage) if position_type == POSITION_LONG else (1 - slippage)
    req_price = (mark * s_mult).quantize(Decimal(10) ** -price_prec)
    qty       = quantity.quantize(Decimal(10) ** -qty_prec)
    notional  = req_price * qty

    lp     = fetch_locked_params(sym["name"], leverage)
    cva    = to_wei(notional * Decimal(lp["cva"])      / (100 * leverage))
    lf     = to_wei(notional * Decimal(lp["lf"])       / (100 * leverage))
    pa_mm  = to_wei(notional * Decimal(lp["partyAmm"]) / (100 * leverage))
    pb_mm  = to_wei(notional * Decimal(lp.get("partyBmm", 0)) / 100)

    # addMargin sizing
    SHORT_BUFFER       = Decimal("1.10") #slippage buffer
    upper_margin_price = req_price if position_type == POSITION_LONG else mark * SHORT_BUFFER
    upper_notional     = upper_margin_price * qty
    base_margin        = upper_notional / Decimal(leverage)
    margin_wei         = to_wei(base_margin * (Decimal(1) + 2 * trading_fee))

    deadline = int(time.time()) + 3600

    # 1) addMarginToNextVA SignedOp  (target = AccountLayer)
    add_call = encode_add_margin_to_next_va(sub_account, va_iso, symbol_id, margin_wei)
    add_op   = sign_operation(session_pk, ACCOUNT_LAYER, add_call,
                              account_addr=sub_account, deadline=deadline)

    # 2) sendQuoteWithAffiliateAndData SignedOp  (target = Symmio Core)
    send_call = encode_send_quote(
        symbol_id=symbol_id, position_type=position_type, order_type=ORDER_MARKET,
        price_wei=to_wei(req_price), quantity_wei=to_wei(qty),
        cva=cva, lf=lf, pa_mm=pa_mm, pb_mm=pb_mm, deadline=deadline,
        affiliate=AFFILIATE, hedger=HEDGER,
        data_bytes=abi_encode(["(string)"], [(str(uuid.uuid4()),)]))
    send_op = sign_operation(session_pk, SYMMIO_CORE, send_call,
                             account_addr=sub_account, deadline=deadline)

    # 3) POST both ops as one batch
    payload = {"addMargin": add_op, "sendQuote": send_op}
    r = requests.post(f"{SOLVER_BASE}/api/instant_trade/instant_open",
                      json=payload, headers={"Content-Type": "application/json"},
                      timeout=30)
    r.raise_for_status()
    resp = r.json()
    print(f"[open] temp_quote_id={resp['temp_quote_id']}")
    return resp     # {"temp_quote_id": -N, "partyBmm": "...", ...}
```

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

```
notional   = quantity * price          (price is the worst-acceptable post-slippage price)
cva        = notional * cva%       / (100 * leverage)
lf         = notional * lf%        / (100 * leverage)
partyAmm   = notional * partyAmm%  / (100 * leverage)
partyBmm   = notional * partyBmm%  / 100        # no leverage divisor
```

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).

```python
def encode_close(quote_id, close_price_wei, qty_wei, deadline) -> bytes:
    return SEL_REQUEST_TO_CLOSE_POSITION + abi_encode(
        ["uint256", "uint256", "uint256", "uint8", "uint256"],
        [quote_id, close_price_wei, qty_wei, ORDER_MARKET, deadline])
```

Code Snippet:

```python
import time, requests
from decimal import Decimal
from _common import (SYMMIO_CORE, SOLVER_BASE, POSITION_LONG,
                     encode_close, sign_operation, get_symbol, price_of, to_wei)


def close_quote(session_pk, quote_id, position_type, va_address, symbol_id,
                quantity, slippage_pct=Decimal("5")):
    sym        = get_symbol(symbol_id)
    price_prec = int(sym["price_precision"])
    qty_prec   = int(sym["quantity_precision"])

    slip = min(slippage_pct / 100, Decimal("0.99999"))
    mark = price_of(sym["name"])
    mult = (1 - slip) if position_type == POSITION_LONG else (1 + slip)
    px   = (mark * mult).quantize(Decimal(10) ** -price_prec)
    qty  = quantity.quantize(Decimal(10) ** -qty_prec)

    deadline = int(time.time()) + 3600
    call     = encode_close(int(quote_id), to_wei(px), to_wei(qty), deadline)
    op       = sign_operation(session_pk, SYMMIO_CORE, call,
                              account_addr=va_address, deadline=deadline)

    r = requests.post(f"{SOLVER_BASE}/api/instant_trade/instant_close",
                      json=[op],   # array of ONE wrapped op
                      headers={"Content-Type": "application/json"},
                      timeout=30)
    print(f"[close] {r.status_code}  {r.text}")
    return r.json() if r.ok else {}
```

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:

```python
from _common import CHAIN_ID, INSTANT_LAYER

CONDITIONAL_ORDER_DOMAIN = {
    "name":              "ConditionalOrder",
    "version":           "1",
    "chainId":           CHAIN_ID,
    "verifyingContract": INSTANT_LAYER,
}

CONDITIONAL_ORDER_TYPES = {
    "EIP712Domain": [
        {"name": "name",              "type": "string"},
        {"name": "version",           "type": "string"},
        {"name": "chainId",           "type": "uint256"},
        {"name": "verifyingContract", "type": "address"},
    ],
    "ConditionalOrder": [
        {"name": "virtualAccount", "type": "address"},
        {"name": "subAccount",     "type": "address"},
        {"name": "salt",           "type": "uint256"},
        {"name": "quoteId",        "type": "int256"},
        {"name": "symbolId",       "type": "uint256"},
        {"name": "positionType",   "type": "uint8"},
        {"name": "affiliate",      "type": "address"},
        {"name": "takeProfit",     "type": "TakeProfit"},
        {"name": "stopLoss",       "type": "StopLoss"},
        {"name": "sendQuote",      "type": "SendQuote"},
    ],
    "TakeProfit": [
        {"name": "quantity",             "type": "string"},
        {"name": "price",                "type": "string"},
        {"name": "orderType",            "type": "uint8"},
        {"name": "conditionalPrice",     "type": "string"},
        {"name": "conditionalPriceType", "type": "string"},
    ],
    "StopLoss":  [...],   # same shape as TakeProfit
    "SendQuote": [...],   # same shape + "leverage": uint256
}

ZERO_LEG  = {"quantity": "0", "price": "0", "orderType": 0,
             "conditionalPrice": "0", "conditionalPriceType": "market"}
ZERO_SEND = {**ZERO_LEG, "leverage": 0}
```

Build, sign, submit:

```python
import json, secrets, requests
from decimal import Decimal
from typing import Optional
from eth_account import Account
from web3 import Web3
from _common import (AFFILIATE, ORDER_MARKET, TPSL_BASE, TPSL_APP_NAME,
                     get_symbol, price_of)


def set_tpsl(session_pk, sub_account, va_address, quote_id, symbol_id,
             position_type, quantity,
             tp_price: Optional[Decimal] = None,
             sl_price: Optional[Decimal] = None):
    if tp_price is None and sl_price is None:
        raise ValueError("at least one of tp_price / sl_price must be set")

    sym        = get_symbol(symbol_id)
    price_prec = int(sym["price_precision"])
    qty_prec   = int(sym["quantity_precision"])

    mark   = price_of(sym["name"]).quantize(Decimal(10) ** -price_prec)
    qty_q  = quantity.quantize(Decimal(10) ** -qty_prec)
    qp     = lambda x: None if x is None else Decimal(x).quantize(Decimal(10) ** -price_prec)

    def leg(trig):
        if trig is None:
            return ZERO_LEG
        return {"quantity": str(qty_q), "price": str(mark),
                "orderType": ORDER_MARKET,
                "conditionalPrice":     str(qp(trig)),
                "conditionalPriceType": "last_close"}

    salt_int = secrets.randbits(256)
    message  = {
        "virtualAccount": Web3.to_checksum_address(va_address),
        "subAccount":     Web3.to_checksum_address(sub_account),
        "salt":           salt_int,
        "quoteId":        int(quote_id),
        "symbolId":       symbol_id,
        "positionType":   position_type,
        "affiliate":      AFFILIATE,
        "takeProfit":     leg(tp_price),
        "stopLoss":       leg(sl_price),
        "sendQuote":      ZERO_SEND,
    }
    typed = {"domain": CONDITIONAL_ORDER_DOMAIN,
             "types":  CONDITIONAL_ORDER_TYPES,
             "primaryType": "ConditionalOrder",
             "message":     message}

    sk  = Account.from_key(session_pk)
    sig = "0x" + sk.sign_typed_data(full_message=typed).signature.hex().lstrip("0x")

    # Wire format: salt becomes a decimal string.
    wire = json.loads(json.dumps(typed, default=str))
    wire["message"]["salt"] = str(salt_int)

    r = requests.post(f"{TPSL_BASE}/api/v5/",
                      json={"typedData": wire, "signer": sk.address, "signature": sig},
                      headers={"App-Name": TPSL_APP_NAME, "Content-Type": "application/json"},
                      timeout=15)
    print(f"[tpsl] {r.status_code}  {r.text}")
    return r.json() if r.ok else {}
```

{% hint style="info" %}
**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.
{% endhint %}

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


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.symm.io/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading/instant-trading-in-v0.8.5.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
