# What is SYMMIO?

Symmio is a trustless, hybrid (combining on and off-chain) clearing house: the communication, settlement, and clearing layer for permissionless derivatives. It has no liquidity pools and no order book. Instead of trading against a pool, you trade directly against professional market makers called *solvers*, who lock collateral to back every trade. Pricing runs off-chain for speed; collateral, positions, and settlement live on-chain. It's peer-to-peer derivatives, settled on-chain.

Quoting and pricing run off-chain so they stay fast, while collateral and settlement are fully on-chain so they stay verifiable. The whole system is permissionless: anyone can build a frontend, and anyone can run a solver. The protocol is the shared infrastructure underneath both.

### Derivatives as a Service

{% hint style="success" %}

## For Crypto Natives:&#x20;

### Symmio is to Derivatives, what Celestia is for Rollups.

Celestia and Eigenlayer **enable Rollups as a Service (RaaS)**\
Symmio offers **“Derivatives as a Service” (DaaS)**

DaaS can be used by exchanges or "Subnets" to create a derivatives trading protocol without any technical implementation work or liquidity onboarding; Symmio and its partners take care of everything. Several 3rd parties are already running their own exchanges **using our DaaS (**[**Carbon**](https://carbon.inc/)**,** [**Vibe**](https://vibe.trading/)**,** [**Thena**](https://perps.thena.fi/)**)** on several blockchains, including on BNB.&#x20;

We are currently generating $7 Billion (in monthly volume with over $55 Billion in total volume (source: [Symmio Analytics](https://analytics.symm.io) — Feb. 2026).&#x20;

Symmio takes a settlement fee from all subnets that settle on Symmio. The fees generated from settlement flow 100% back to SYMM stakers.&#x20;

In this open ecosystem, actors (integration partners (protocol/exchanges), liquidity providers (MMs), oracles, clearing operators and traders) can collaborate and compete for the best prices and solutions, driving feature innovation and cost efficiency.
{% endhint %}

### What makes Symmio different

There are no liquidity pools. Every trade is a bilateral agreement between you and a solver, so there's no pool to price against, no pool-mechanic slippage and no liquidity fragmentation. A solver can make a market in anything that has a price feed, whether that's crypto, equities, commodities, FX, or prediction markets; the protocol doesn't restrict what can be listed.

Quoting and pricing run off-chain so they stay fast, while collateral and settlement are fully on-chain so they stay verifiable. The whole system is permissionless: anyone can build a frontend, and anyone can run a solver. The protocol is the shared infrastructure underneath both.

### A gateway to OTC derivatives

Because a market only needs a price feed, the range of assets a solver can list is wide:

<details>

<summary>Potential asset types</summary>

* **Cryptocurrency Futures / Perpetuals / Options, Stocks & Equities**
* **Low-Cap Perpetuals, Bonds, Yield-Swaps, Low-Cap Options**
* **Forward Rate Swaps, Commodities, Volatility Indices, Real Estate Indexes**
* **FX Pairs, Inflation-Protected Assets, Basket Indices**
* **ETFs, Options, Carbon Credits, Weather Markets**
* **Energy Contracts, Macroeconomic Indicators, Credit Default Swaps**
* **NFT Floor Prices, IPO Exposure, Metaverse Land Parcels, Prediction Markets**
* **Election Outcomes, Sporting Events**

O**r any other asset not listed here, with any possible price function.**

</details>

{% hint style="info" %}
Symmio supports [options trading](/options-protocol-architecture/overview), including call and put options across a range of assets.
{% endhint %}

### Who uses Symmio?

**Traders** reach Symmio through frontends such as Carbon, Thena, and Vibe. You don't touch the protocol directly; you use a venue built on it.

**Frontend Builders (Exchanges)** build the trading interface and own everything user-facing: UX, branding, and user acquisition. Symmio handles settlement.

**Solvers** take the other side of trades. They're professional market makers who typically hedge their risk externally and compete to quote the best price.


# Core Concepts

## The Bilateral Model: PartyA and PartyB

Most crypto derivatives trade on order-book exchanges, where traders post bids and asks to a shared book and a matching engine decides when two orders meet. Symmio works differently. Instead of a centralized order book, a trader expresses an *intent*: a request that says "I want to go long (or short) this market, at this price, this size, with this collateral."

Once submitted, the intent is visible on-chain to a network of independent counterparties called [***solvers***](/trader-documentation/glossary-of-terms). Any solver can choose to take the other side. So rather than competing for queue priority on a central book, you declare what you want and let the market come to you. There's no central matching engine, which keeps the system permissionless and peer-to-peer.

This model removes the need for a centralized matching engine and makes the system permissionless, flexible, and peer-to-peer. It allows anyone to create and trade derivatives in a trustless manner, without relying on a central exchange.

<figure><img src="/files/QhoOeRnW8tBrSYYeVaFq" alt=""><figcaption></figcaption></figure>

Symmio has three main roles:

* [***Party A***](/trader-documentation/glossary-of-terms): the trader who creates an intent.
* [***Party B***](/trader-documentation/glossary-of-terms): the solver, or hedger, who chooses to accept it.
* [***Liquidators***](/trader-documentation/glossary-of-terms): impartial actors who watch open positions and step in if collateral drops below a safe level.

{% hint style="info" %}
See the [Glossary](/trader-documentation/glossary-of-terms) for more information.
{% endhint %}

When a solver accepts an intent, the position opens on-chain and collateral from both sides is locked into the protocol. To close, the trader submits a close intent, the solver handles it, and both balances update.

## Intent-based trading: how a trade happens

A trade follows a simple path:

1. **You submit an intent**, e.g. "go 10x long on ETH at this price."
2. **A solver sees it** and decides whether to take the other side.
3. **The solver locks it in**, and both sides lock collateral.
4. **The position is open**, live against that solver.
5. **You close when ready** by submitting a close intent; settlement happens on-chain.

This is unlike an AMM, where you swap against a pool, or an order book, where you match an anonymous resting order. Here a specific professional counterparty explicitly agrees to your trade.

## Collateral, margin, and CVA

A few terms show up everywhere. *Collateral* is the money you actually deposit, usually USDC; it's your balance in the system. *Margin* is the slice of that collateral backing a specific position: at 10x leverage, a $1,000 position needs $100 of margin. *CVA*, short for Credit Valuation Adjustment, is a small buffer locked on top of margin to protect against the other side defaulting. Think of it as a security deposit that covers the worst case if a counterparty can't pay.

They relate like this:

```
Collateral = Locked Margin + CVA + Available Balance
```

Opening a position locks margin and CVA. If a trade moves against you and your losses approach your total collateral, you're at risk of liquidation.


# I'm Interested In...

Pick the path that matches what you're trying to do.

### **Trading on an existing frontend?**

Trade existing markets through a frontend built on Symmio.

Start Here: [Choosing an Exchange](/trader-documentation/choosing-an-exchange) & <https://symm.io/frontends>

### Understanding how trading works?

How the intent-based system works, market vs. limit orders, funding rates, liquidations, and querying on-chain data.

Start here:  [How Trading Works in SYMMIO](/trader-documentation/how-trading-works-in-symmio)

### Building a trading bot?

Trade execution flows, fetching trade parameters from solvers, and encoding on-chain transactions.

Start here: [Learn how to build a Trading Bot using SYMMIO](/trader-documentation/building-a-trading-bot-1)

### Becoming a Frontend Builder?

Frontend setup, deploying a MultiAccount contract, the Frontend SDK, and support for instant trading and instant withdrawal.

Start Here: [Frontend Builder Docs ](/exchange-builder-documentation/frontend-builder-introduction)

### Becoming a Solver on SYMMIO?

Solver API requirements, offering new markets, and conditional and instant order support. Start with the Liquidity Provider docs.

Start Here: [Documentation for Solvers](/liquidity-provider-documentation/role-of-a-liquidity-provider-solver)

### **Contracts Addresses and Endpoints?**

Frontend affiliate addresses, Diamond contract addresses, and solver API endpoints.&#x20;

Check the Reference: [Contract Addresses](/api-endpoints-and-deployments/symmio-perps-deployments) & [API endpoints](/api-endpoints-and-deployments/solver-addresses-and-endpoints).&#x20;

### Want to audit / bug bounty us?

Read the Contract Architecture Overview, the Audit Reports, and the Bug Bounty Program.

* [Contract Architecture Overview](/security-and-architecture/contract-architecture-overview)
* [Audit Reports](/security-and-architecture/audit-reports)
* [Bug Bounty Program / Coverage](/security-and-architecture/bug-bounty-program-coverage)

### Deeper Dive into SYMMIO's Architecture?

Detailed contract documentation, Muon API endpoints, and the Diamond SDK. See API Endpoints & Deployments and Contract Documentation.

Check [API Endpoints & Deployments](/api-endpoints-and-deployments/symmio-perps-deployments) and [Contract Documentation](/contract-documentation/symmio-perps-v0.8.5-contract-upgrade)


# Symmio Foundation

All official information about the SYMM token lives with the Symmio Foundation, including:

* **SYMM token**
* **Points Program**
* **Utility**&#x20;
* **Governance mechanisms**
* **Distribution details**
* **Development plans & Future Roadmaps**

and any associated updates are available exclusively through the following Symmio Foundation resources:

* [Symmio Foundation](https://symmio.foundation): high-level overviews, announcements, and background on the $SYMM token and the ecosystem.
* [Symmio Foundation Docs](https://docs.symmio.foundation): Explore the comprehensive documentation hub for in-depth details, technical specifications, and token-related policies.

Treat these as the only reliable sources for $SYMM token information. For anything else, the official communication channels are listed on both sites.


# How Trading Works in SYMMIO

This section covers the mechanics of trading on Symmio: how orders are expressed, how positions are closed, what happens when a counterparty or your collateral falls short, and how funds settle and move. If you're new to the protocol, read [Core Concepts](/getting-started/core-concepts) first.

* [Market vs. Limit Orders](/trader-documentation/how-trading-works-in-symmio/market-vs.-limit-orders): the two order types, and how the peer-to-peer model changes them.
* [Using Take Profit and Stop Loss](/trader-documentation/how-trading-works-in-symmio/using-take-profit-and-stop-loss): automatic exits, and what you authorize to enable them.
* [Force Closing Positions](/trader-documentation/how-trading-works-in-symmio/force-closing-positions): closing the position yourself when a solver won't fill.
* [Liquidations](/trader-documentation/how-trading-works-in-symmio/liquidations): how cross-margin accounts are wound down when collateral runs low.
* [Instant Trading](/trader-documentation/how-trading-works-in-symmio/instant-trading): session keys and signed actions for CEX-speed execution.
* [Withdrawals](/trader-documentation/how-trading-works-in-symmio/withdrawals): the 12-hour cooldown and the faster paths around it.
* [Settlement](/trader-documentation/how-trading-works-in-symmio/settlement): turning unrealized profit into spendable collateral.
* [Funding Rates](/trader-documentation/how-trading-works-in-symmio/funding-rates): the periodic payment that keeps perp prices aligned.


# Market vs. Limit Orders

## Sending a quote

Every trade on Symmio starts as a *quote*: your request to open or close a position at a given price, against a given set of [solvers](/trader-documentation/glossary-of-terms#solver). There are two order types, **market** and **limit**. Both mean roughly what they do on any exchange, but the peer-to-peer model gives them slightly different mechanics, so it's worth covering what each one means here.

### Market orders

A market order tells the solver: fill me at the current market price, right now.

There's no shared order book, so a market order is still a quote sent to a solver. When you submit one, the frontend asks a solver (or a set of solvers it has whitelisted) to fill at the current oracle price. Solvers that accept respond almost immediately, and the position opens very close to the price your frontend showed you when you clicked.

Since the point of a market order is to get in immediately, the quote price carries some slippage so the entry is guaranteed. After you set your slippage tolerance, the [frontend](/trader-documentation/glossary-of-terms#frontend-builder) does the math: it takes the oracle price from Muon and shifts it slightly depending on whether you're going long or short. That shift covers the solver's spread plus any movement in the market. The trading fee on a market order is a fraction of `oracle price * quantity`.

#### Limit orders

A limit order tells the solver: fill me at this price or better, otherwise don't fill at all.

A limit order is a quote that sits in `PENDING` (for opens) or `CLOSE_PENDING` (for closes) until a solver chooses to fill it. Solvers watch for limit orders whose price has become reachable and fill them when the market gets there. If the market never reaches your price before the quote's deadline, the quote expires unfilled.

Limit orders can be partially filled. The `price` you pass is the exact price you want to enter at, and the trading fee is a fraction of `requestedOpenPrice * quantity`.

### Closing a quote

Closing works the same way, with one difference: a market close must close the whole position, while a limit close can be partial. For a market close, you set your slippage and the frontend calculates the `closePrice`. The logic is inverted from an open: for longs the `closePrice` is decreased, and for shorts it's increased.

If your counterparty is unresponsive and a cooldown has passed, you can [force close](/trader-documentation/how-trading-works-in-symmio/force-closing-positions) the position yourself. Force close works only on limit orders.

### Quote data

When your frontend submits a quote, it can attach arbitrary data to it. That data is included in the event emitted when the quote is sent.

```solidity
	function sendQuoteWithAffiliateAndData(
		address[] memory partyBsWhiteList,
		uint256 symbolId,
		PositionType positionType,
		OrderType orderType,
		uint256 price,
		uint256 quantity,
		uint256 cva,
		uint256 lf,
		uint256 partyAmm,
		uint256 partyBmm,
		uint256 deadline,
		address affiliate,
		SingleUpnlAndPriceSig memory upnlSig,
		bytes memory data // <- arbitrary data
	) external returns (uint256 quoteId);
```

{% hint style="info" %}
A frontend can use this field to link your on-chain position to extra off-chain information it shows in its UI, for example during campaigns.
{% endhint %}


# Using Take Profit and Stop Loss

Take profit (TP) and stop loss (SL) let you exit a position automatically when the market reaches a price you picked in advance: a profit target on one side, a maximum acceptable loss on the other. You set the levels once instead of watching the chart all day, and the system closes the position for you when a level is hit.

On Symmio, every position is an on-chain agreement between you and a specific solver, so TP/SL lives outside the position itself: a small service watches the market on your behalf and submits a close request the moment your trigger is reached. Under the hood that service works one of two ways: either you delegate permission to a service allowed to request closes for you when price conditions are met, or the frontend talks directly to a solver's Conditional Orders API and the solver manages TP/SL. From your side the result is identical: when price hits your TP or SL, a close request goes out.

### How it works for you as a trader

The exact UI differs between frontends, but the flow is usually the same.

**Open a position.** Trade as usual, then go to the "Positions" or "Open Positions" tab.

**Find the TP/SL controls.** Next to your position you'll see a TP/SL button or a settings icon. Click it to open the take-profit / stop-loss panel.

<figure><img src="/files/fGAAO4EPlBbpxwWqNIwR" alt=""><figcaption><p>Setting a TP/SL on Carbon</p></figcaption></figure>

**Approve the control.** The first time you set TP/SL, you'll usually be asked to approve either conditional order functionality or instant trading; both enable conditional order management. This approval is what lets the frontend or solver request a close for you when your conditions are met.

{% hint style="info" %}
You stay in control of your funds. The service cannot withdraw or move your money; it can only send the same close requests you could send yourself.
{% endhint %}

**Choose your levels.** Set a take profit (where you're happy to lock in gains) and a stop loss (where you want to cut the trade to cap downside). Most frontend builders let you use percentages or enter exact prices if you already know your target and invalidation levels.

<figure><img src="/files/6yeY6wDdD9Otsy2qPIPt" alt=""><figcaption><p>Setting a TP/SL on Carbon</p></figcaption></figure>

Behind the scenes, the app turns your choices into a conditional order payload that includes the trigger price (where TP/SL activates) and a minimum acceptable execution price (which builds in slippage protection).

**Confirm and save.** Once you confirm, the system watches the market and attempts to close your trade whenever a threshold is reached, for as long as the position stays open and your conditions are valid.

**Adjust or remove later.** Come back to the same panel any time to move your take profit, tighten or loosen your stop loss, or cancel TP/SL entirely.

{% hint style="info" %}
Check your specific frontend's documentation for any differences.
{% endhint %}

### What you're authorizing

The delegation is deliberately minimal, and it's worth knowing exactly what it does and doesn't allow.

The service can submit `requestToClosePosition` on positions in your account, for as long as the delegation is active. That's the whole of it. It cannot:

* open new positions or modify existing ones,
* withdraw, deposit, allocate, or deallocate collateral,
* change your account settings or delegate further permissions,
* call any other function on the Symmio contracts.

The delegation carries an expiry timestamp (typically a day, or a session length, depending on the frontend) and can be revoked early if you want permission back before it lapses. Revocation runs through a short two-step cooldown so an in-flight trigger can't be cut off mid-execution.

### What happens when your trigger fires

When the service's price feed crosses your TP or SL level, it submits the close as a market order against your solver. A few consequences follow from that:

* Your solver (PartyB) fills the close, exactly as it would any other market close request.
* You pay a close fee on the filled position, deducted from your available balance as part of the solvency check that runs when the solver fills. Close fees are set by the affiliate your frontend is registered under, so different frontends may charge different rates on the same symbol.
* The price you get is your solver's fill price, not the exact level that triggered the close. In fast markets, expect some slippage between trigger and fill, especially on a stop loss during a sudden move against you.


# Force Closing Positions

Symmio is a peer-to-peer perpetuals protocol: every position you hold is against a specific solver (PartyB), and filling your close requests is that solver's job. Usually this is uneventful. You submit a close, the solver fills it, the position settles. But solvers are off-chain actors, and once in a while one could go offline, fall behind, or ignore a close it no longer likes. Force close is the backstop. If the market has clearly reached your close price and your solver still hasn't filled, you can close the position yourself, directly against the contract, without the solver's cooperation.

This page covers when force close is available, what price you get, what it costs, and the handful of things that can make it fail.

### When you can force close

* **There's an open close request.** The quote must be in `CLOSE_PENDING`, meaning you've already submitted a `requestToClosePosition` that the solver hasn't filled.
* **The close request is a LIMIT order.** Market closes can't be force-closed, because there's no specific price the solver has failed to honor.
* **The cooldowns have elapsed.** Two timers protect the solver from being force-closed on a brief spike: an initial cooldown after the close request is submitted, and a second one measured from the moment the market hits your price. Both must pass.
* **The market has reached your close price.** The contract checks this against a signed Muon oracle price.

If any of these fails, the transaction reverts and the close stays pending as usual.

### The price you actually get

Your force-close price is your requested close price, nudged slightly against you by a small gap. The gap exists so a solver isn't punished for a single wick that touches your limit and then retraces. Its size is set per symbol by `forceCloseGapRatio`, and your frontend should show the estimated force-close price before you submit, so you know what you're agreeing to.

That gap is why force close is best treated as a safety net rather than an execution strategy: you'll get a price near your limit, but not quite at it. When a solver is filling you normally, `fillCloseRequest` will almost always give you a better price.

### The close fee

Force close runs a full solvency check before it touches your position, and that check includes a close fee on top of any open-side trading fee already baked into the quote. It's computed as:

```
closeFee = filledAmount × closedPrice × quote.closeFee / 1e36
```

and subtracted from your available balance in the solvency math. The close fee rate depends on the affiliate your frontend is registered under, so different frontends can charge different close fees on the same symbol.

{% hint style="info" %}
If you're force-closing a losing position near your maintenance margin, the close fee is one more subtraction that can tip the solvency check negative. Make sure you have enough margin to cover it.
{% endhint %}

### What happens under the hood

You submit one transaction. Internally the contract walks through up to three stages:

1. **Initialize.** Validates the cooldowns and order type, reads the market price from a signed oracle snapshot, computes your force-close price including the gap, and runs the solvency check.
2. **Settle (conditional).** If you or the solver doesn't currently have enough free balance to cover the close, usually because the funds are sitting as unrealized profit in *other* open positions, this stage realizes those profits into available balance so the close can finalize. Most force closes skip it; it only kicks in when funds are tied up elsewhere.
3. **Finalize.** Refreshes the price snapshot, closes the position, transfers the uPnL, and emits a `ForceClosePosition` event.

Frontend builders can run all three in a single bundled call, `forceCloseAndSettlePositionsUnified`.


# Liquidations

Liquidation happens when an account no longer holds enough collateral to safely support its positions.

To see how it works on Symmio, start with how your collateral is tracked. When you send a quote, the margin that position requires moves into a **pending** state. You can't use it to open other trades, but it still counts toward your allocated balance when the protocol checks whether you're solvent. Once a solver accepts the quote and the position opens, that margin becomes **locked**: it's actively backing the trade. Anything that's neither pending nor locked is your **free balance**.

Every position on Symmio is **cross margin**, so your entire allocated balance, across all positions and pending quotes, works together to support your risk. As your unrealized PnL (UPnL) moves, the protocol draws on your collateral in order: first locked margin, then free balance, and finally pending locked amounts. As long as your total UPnL stays above the lowest allowed threshold, you're safe. If losses grow until even the pending locked funds aren't enough, the account is unsafe and liquidation triggers.

Because the account is cross-margined, **PartyA is liquidated as a whole**. All of PartyA's open positions and outstanding quotes move into a liquidated state together; in practice, everything is closed or cancelled at once.

When that happens, special actors called **liquidators** step in. They use signed data from the Muon network to prove your total losses and the prices of the markets you're trading. The protocol then closes your positions, redistributes collateral between you and your counterparties, and pays a small fee to the liquidator for keeping the system safe. Your CVA and margins are applied according to the protocol's rules.

### Example

Say Alice (PartyA) has $1,000 allocated for trading, with several positions open and cross-margined together:

* $500 locked in active positions
* $200 pending locked (quotes waiting to be filled)
* $300 free and available
* $100 in locked CVA

<figure><img src="/files/T6sAj7r6vjASzOQsWH2P" alt=""><figcaption><p>Stage 1: Alice's has manageable losses.</p></figcaption></figure>

<figure><img src="/files/LHupVEbUtzX6wZcoyI8x" alt=""><figcaption><p>Stage 2: Alice suffers deeper losses</p></figcaption></figure>

<figure><img src="/files/MiPUD0bmmS1ZhEVd3rTg" alt=""><figcaption><p>Stage 3: Alice has critical losses and is about to be liquidated</p></figcaption></figure>

<figure><img src="/files/uFGUXfY6vwuwfCWAec95" alt=""><figcaption><p>Stage 4: Alice is liquidated</p></figcaption></figure>


# Instant Trading

Normally, every on-chain action needs a wallet signature and a network confirmation: opening a trade, closing it, cancelling it. That's secure, but it can feel slow next to a centralized exchange. Instant Trading keeps the trustless backend while making execution feel almost as fast as a CEX. Instead of signing every trade, you set up a secure session with a solver once, and they can then execute specific actions on your behalf within strict limits you set.

### How it used to work

In earlier versions, instant trading relied on **delegated access** through the MultiAccount contract. You granted a solver permission to call specific functions, like "send quote" or "close position," and the solver could then trade for you without a wallet signature each time.

That worked, but it carried a trust trade-off: once a solver had access, it technically *could* misuse it. In practice that wasn't a concern, since solvers are trusted entities, but a compromised solver could have put your funds at risk.

### The Instant Layer

The Instant Layer replaces broad delegation with two narrower options.

#### Option 1: One-time signed actions

You sign a specific action with exact parameters, e.g. "open a long on BTCUSDT at this price, this size, with this collateral," and hand that signature to the solver. The solver bundles your signed action with their own (lock the quote, open the position) and submits everything in a single transaction. The contract verifies your signature, confirms the parameters match exactly what you signed, and executes.

The solver can only do exactly what you signed. They can't change the price, the size, or anything else, and each signature can be used only once.

#### Option 2: Session keys

For a smoother experience, the frontend generates a temporary key pair in your browser at the start of each session. You sign one transaction granting that session key permission to call specific functions, e.g. "this key can send quotes and request closes for the next 24 hours." From then on, your trading actions use the session key.

It resembles the old delegation, with a few important improvements:

* **You don't have to trust the solver.** The session key is generated locally in your browser; the solver never holds it.
* **Permissions are scoped.** You choose exactly which functions the key can call.
* **Permissions expire.** The session key has a built-in expiration timestamp.
* **You can revoke anytime.** A cooldown-based revocation process gives the solver fair notice.

#### How it feels in practice

Nothing changes about the UX. You click "Open" or "Close" and the trade executes almost instantly. The difference is entirely under the hood: your funds are better protected because the solver never gets broad access to your account.

The first time you visit a Symmio frontend on the latest version, you'll activate "instant action mode" for your account, a one-time on-chain transaction. After that, the frontend should handle session keys for you, generating a new one each session and cleaning up the old ones.

### Batched execution

A big part of the speed gain is that the solver can bundle multiple actions into one transaction. When you click "Open," the solver can submit your `sendQuote`, their `lockQuote`, and their `openPosition` together. That used to take three separate transactions. The contract processes them in order and automatically passes the quote ID from the first step to the next two.

Your position opens in one block instead of three, which cuts latency sharply.


# Withdrawals

When you trade on Symmio, your funds sit inside the protocol rather than in your wallet. You can deposit instantly and trade as much as you like. The only time there's a delay is when you withdraw back out, to your wallet or another EOA.

### The 12-hour cooldown

Symmio uses a **12-hour withdrawal cooldown**. Once you request a withdrawal (after deallocating funds from your trading sub-accounts), those funds are locked for 12 hours before they can leave the system. It's much like the withdrawal delay on an optimistic rollup.

The reason is safety. If something goes wrong, the cooldown gives the protocol's monitoring systems time to react: suspicious activity can be detected, accounts can be suspended, and damage can be contained inside the window.

For a normal trader, this means:

* You can **trade instantly**.
* You can **deposit instantly**.
* When you **withdraw**, there's a **12-hour delay** before the funds reach your wallet.

The old design had a single withdrawal path with a fixed 12-hour wait, plus a separate bridge for instant withdrawals. The latest version folds these into one flexible system with several options.

### Withdrawal options in the latest version

#### Normal withdrawal

The simplest path. You initiate the withdrawal, wait out the cooldown, and finalize. Your funds go straight to your wallet on the same chain, with no provider involved and no fees beyond gas.

#### Express withdrawal

If you need the funds now, an **express provider** can front them to you immediately, then wait out the cooldown and get reimbursed by the protocol afterward. From your side the withdrawal is near-instant. The provider typically charges a small fee.

#### Cross-chain withdrawal

Symmio runs on multiple chains, and you might want your funds on a different chain than the one you're trading on. A **virtual provider** handles this: it accepts the withdrawal on the source chain and delivers your funds on the destination chain, with the collateral reconciliation happening behind the scenes.

#### Express cross-chain withdrawal

Here an express provider fronts your funds immediately on the destination chain, combining instant payout with cross-chain delivery. The express provider and a virtual provider coordinate behind the scenes.

### How it works in practice

If instant withdrawal is available, the frontend builder should offer it; if not, it should show the estimated wait. You can always fall back to the normal 12-hour withdrawal, which is guaranteed to work regardless of provider availability.

When you initiate a withdrawal, you can split it across multiple destinations. For example, you could send 500 USDC to your wallet on Arbitrum and 300 USDC to a different address on Base, all in one request.

### Cancelling a withdrawal

You can cancel a pending withdrawal any time before it's finalized. How that works depends on the type.

For a **standard** withdrawal, cancellation needs no approval and is finalized on-chain.

For **cross-chain (virtual)** withdrawals, once the provider has accepted there's a blackout window near the end of the cooldown where you can no longer cancel on your own. Outside that window you can still cancel immediately; inside it, the cancellation moves to a pending state and needs the provider's approval to go through.

For **express** withdrawals, any cancellation after the provider has accepted needs their approval. There's no free-cancel window, because the provider has already fronted your funds.

{% hint style="info" %}
The legacy bridge withdrawal from the previous version (v0.8.4) still works but is being phased out. The new system replaces it with a more flexible architecture that supports express providers, virtual providers, cross-chain delivery, and multi-destination withdrawals.
{% endhint %}


# Settlement

When you trade on Symmio, your positions gain or lose value over time. That running total is your unrealized PnL: it exists on paper but isn't yet locked in as realized PnL. Separately, you hold an allocated balance, real collateral you can use to cover losses, open new positions, or withdraw. The protocol requires that allocated balance to stay sufficient at all times, which means you can't close a losing trade unless you have enough realized collateral to cover the loss.

That creates an awkward situation: a trader can be profitable overall yet still be blocked from closing a losing position, because all the profit is locked up as unrealized gains in other open positions. Settlement is the mechanism that converts some of those unrealized gains into usable balance.

### How it works

Say **Bob** has three open positions:

1. **Position 1** with **Rasa Hedger**: unrealized profit of **$300**
2. **Position 2** with **PerpsHub Hedger**: unrealized profit of **$100**
3. **Position 3** with **PerpsHub Hedger**: unrealized loss of **$250**

Bob is $150 in profit overall and wants to close Position 3, but his allocated balance is too low to cover the $250 loss.

The solver steps in. PerpsHub submits a signed message from the Muon oracle attesting to current market prices and the unrealized profit and loss across Bob's positions. The contract verifies the signature, checks that everyone stays solvent, and settles: $150 from Position 1 and $100 from Position 2 are realized into Bob's allocated balance. Bob now has $250 of spendable collateral, so PerpsHub can fill the close on Position 3, which has stayed unchanged throughout. Nobody takes on uncollateralized risk, and Bob isn't stuck waiting for the losing trade to recover.

In earlier versions, settlement dealt only with position PnL. In 0.8.5 it also handles accumulated funding fees, so when a solver settles your positions, position PnL and any owed funding are factored in together.

A few protections are built into the process: the updated prices must always fall between the original entry price and the signed current price, neither side is allowed to end up insolvent as a result, and multiple positions can be settled at once, letting a solver free up collateral across many trades quickly. More detail is in the [Settlement](/contract-documentation/symmio-perps-v0.8.5/diamond-core-facets/settlement) docs.


# Funding Rates&#x20;

Perpetual futures never settle, so their price can drift away from the underlying market price. **Funding rates** are a small payment exchanged between longs and shorts to keep the two in line.

When longs dominate and the perp trades above spot, longs pay shorts. When shorts dominate and the perp trades below spot, shorts pay longs. Either way, holding the more crowded side costs a little more, which nudges the price back toward the real market value.

### How it works on Symmio

Every symbol on Symmio has its own funding rate, applied at the end of fixed periods called **epochs**. When an epoch ends, positions pay or receive funding. On the contracts, the cycle length is `fundingRateEpochDuration`, and solvers charge funding during the symbol's `fundingRateWindowTime`, a short window on either side of the epoch flip.

```json
{
  "symbolId": "1",
  "name": "BTCUSDT",
  "isValid": true,
  "minAcceptableQuoteValue": "120000000000000000000",
  "minAcceptablePortionLF": "3000000000000000",
  "tradingFee": "3800000000000000",
  "maxLeverage": "100000000000000000000",
  "fundingRateEpochDuration": "14400", // <- 4 hours
  "fundingRateWindowTime": "420" // <- 7 minutes
}
```

There are two ways funding can be applied.

The older method adjusts the `openedPrice` of your position to simulate the charge. If you're long and paying funding, your open price nudges up slightly, giving you a worse entry. No tokens move. This is still used for symbols that haven't migrated to the newer system.

<figure><img src="/files/8bTdIxDMO55ZIcmetMCx" alt=""><figcaption><p>Explorer Preview</p></figcaption></figure>

{% embed url="<https://intent.symmscan.com/>" %}

The newer method tracks funding as a weighted average at the symbol level instead of touching each position. When a solver sets a rate, it blends into a running average; when funding is charged, it's deducted directly from your allocated balance rather than shifting your open price. That makes it a visible line item instead of a subtle price change, and it's far more efficient, since the solver no longer has to update every open position at each epoch.

{% hint style="info" %}
Solvers stream the rates they'll apply in the next epoch, but not necessarily the epoch durations. They can only charge during the window defined by the smart contracts, so always [check the contracts](/trader-documentation/interacting-with-contracts/querying-info-from-our-contracts) for the authoritative epoch durations.
{% endhint %}


# Trading Fees

Trading and settlement costs are documented with the solver fees, since rates are set per symbol and per affiliate. See:

{% content-ref url="/pages/uP3DUXV6Kye14LQ0mAL2" %}
[Perps - Settlement Costs](/exchange-builder-documentation/settlement-costs-in-symmio/perps-settlement-costs)
{% endcontent-ref %}

{% content-ref url="/pages/T8drk2MmmWuchawE23Bd" %}
[Pair Trading - Settlement costs](/exchange-builder-documentation/settlement-costs-in-symmio/pair-trading-settlement-costs)
{% endcontent-ref %}


# Glossary of Terms

### PartyA

The user who creates intents (trade requests).

### PartyB

PartyB is the traditional-finance term for the counterparty in a swap agreement.

{% hint style="info" %}

### PartyB, Solvers, Hedgers, Counterparty or MarketMaker describe the same entity in Symmio, they describe the Executing party of a Trade contrarian to the requesting entity, which would be referred to as PartyA, Trader or User.

{% endhint %}

{% embed url="<https://www.investopedia.com/terms/c/counterparty.asp>" %}

On Symmio, PartyB is a liquidity provider that sees user intents and decides whether to accept or reject them. PartyB can hedge accepted trades off-chain (with a centralized or decentralized broker) or stay unhedged.

### Solver&#x20;

"Solver" is the standard term for the counterparty in an intent-based system, so a PartyB on Symmio can also be called a solver. The label fits the on-chain side of a PartyB especially well: a counterparty can be split across entities, with a solver handling on-chain execution and a separate market maker or hedger handling off-chain liquidity.

### Hedger / Solver (PartyB)

A liquidity provider on Symmio that sees user intents and decides whether to accept or reject them. It can hedge trades off-chain (e.g. with a centralized or decentralized broker) or stay unhedged.

### Broker

Any external venue or strategy a hedger uses to hedge: a centralized exchange (Binance, Bybit), a DEX, an OTC desk, or an internal algorithm. Symmio never requires a hedger to hedge; it's optional, and off-chain actions have no bearing on on-chain state or trust assumptions.

### Frontend Builder

A developer or team that builds a trading interface on Symmio. They connect to one or more solvers, and provide the UI for traders (PartyA). Examples include Carbon, Thena, and Vibe. Symmio handles settlement; the frontend handles everything user-facing.

### Intent

A proposed trade from PartyA, e.g. "open a LONG position for BTCUSDT at price X with quantity Y." Solvers see intents on-chain and can lock them if they meet their criteria.

### Off-Chain vs. On-Chain

Symmio's on-chain contracts control positions, collateral, liquidations, and the rest. Off-chain hedging, if any, is the hedger's private business logic and doesn't affect the contract's rules or solvency checks.

### Directional Exposure

A hedger that chooses not to hedge is taking a directional bet on the market. A fully hedged (1:1) approach is "delta-neutral": the hedger holds no directional exposure.

### Liquidators

Once an intent opens on-chain it becomes a position. If either side (PartyA or the hedger) becomes insolvent, a liquidator can close the position by force. No trust is placed in the hedger's off-chain solvency; only on-chain collateral and UPNL count.

### Platform Fee

Charged to PartyA when a position opens. The hedger's spread is embedded in the price.

### Liquidator Fee (LF)

The incentive paid to liquidators in a forced liquidation.

### Counterparty Valuation Adjustment (CVA)

A portion of the position's notional value, paid to the surviving side when the other side is liquidated. Read more [Maintenance Margin & CVA Calculations](/liquidity-provider-documentation/solver-settings/maintenance-margin-cva-calculations).

### Funding Rate

A periodic charge, set by the counterparty. A short (whether the hedger or PartyA) may pay funding to the long side, or vice versa.


# Choosing an Exchange

{% hint style="info" %}
Symmio subnets or simply **Brokers** are protocols that are built ontop of our settlement Layer, and actively offer and settle derivatives on SYMM, also referred to as **Brokers** or **Frontend Builders**.
{% endhint %}

{% hint style="danger" %}

## Trading on SYMMIO

The Symmio Foundation does not operate its own Brokerage or engages in any Derivatives offerings, to ensure censorship resistance of the underlying bilateral & intent technology, the foundation only provides the infrastructure for third parties to provide such services. To interact with the protocol, users may choose from the list of Brokers & Subnet Operators below.\
\
Again, to be clear, there is no trading taking place directly via <https://symm.io>
{% endhint %}

## To trade Symmio Markets, please visit our [3rd party frontend providers](#for-live-status-https-symm.io-frontends)

## Currently operating Brokers:

<table data-view="cards"><thead><tr><th></th><th data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><h2>Carbon</h2><p>Smart Perp DEX with Cartel Trading.</p></td><td><a href="https://app.carbon.inc/trade">https://app.carbon.inc/trade</a></td><td data-object-fit="fill"><a href="/files/wMRcp3u0R0XFablaDcBN">/files/wMRcp3u0R0XFablaDcBN</a></td></tr><tr><td><h2>Vibe</h2><p>PumpFun of perps, list any token with 20x leverage</p></td><td><a href="https://vibe.trading/">https://vibe.trading/</a></td><td data-object-fit="contain"><a href="/files/hBR2vwdjH5vQLl5nyH9L">/files/hBR2vwdjH5vQLl5nyH9L</a></td></tr><tr><td><h2>Pear Protocol  </h2><p>Pair-Trading Perps Exchange</p></td><td><a href=" https://www.pear.garden/"> https://www.pear.garden/</a></td><td><a href="/files/beXWB4LkczkeYtmxasrR">/files/beXWB4LkczkeYtmxasrR</a></td></tr><tr><td><h2>THENA</h2><p>THE Trading Hub and Liquidity Layer Built on BNB Chain and opBNB</p></td><td><a href="https://thena.fi/">https://thena.fi/</a></td><td><a href="/files/NZo2dK98sIZTQJ781CON">/files/NZo2dK98sIZTQJ781CON</a></td></tr><tr><td><h2>Privex</h2><p>Perp-DEX for Human and AI Trading Collaboration</p></td><td><a href=" https://prvx.io/"> https://prvx.io/</a></td><td><a href="/files/6YoNoFbIEvejMpmMypgW">/files/6YoNoFbIEvejMpmMypgW</a></td></tr><tr><td><h2>BMX</h2><p>Decentralized Perpetual Trading Platform</p></td><td><a href="https://www.bmx.trade/">https://www.bmx.trade/</a></td><td><a href="/files/9fpktdyBqwwOXGysXaFy">/files/9fpktdyBqwwOXGysXaFy</a></td></tr><tr><td><h2>Echoes</h2><p>Earn, borrow, and invest smarter in the Sonic network.</p></td><td><a href="https://echoes.so/">https://echoes.so/</a></td><td data-object-fit="contain"><a href="/files/cuhHrQGL5QWiq584d1Ro">/files/cuhHrQGL5QWiq584d1Ro</a></td></tr><tr><td><h2>SpookySwap</h2><p>Decentralized Perpetual Trading Platform</p></td><td><a href="https://perps.spooky.fi/trade">https://perps.spooky.fi/trade</a></td><td data-object-fit="contain"><a href="/files/8By0eQDVlzTiiveBUvnn">/files/8By0eQDVlzTiiveBUvnn</a></td></tr><tr><td><h2>Treble</h2><p>Decentralized Perpetual Trading Platform</p></td><td><a href="https://perpetuals.trebleswap.com/">https://perpetuals.trebleswap.com/</a></td><td data-object-fit="contain"><a href="/files/ZK9zwpUQHssWrlp5efeG">/files/ZK9zwpUQHssWrlp5efeG</a></td></tr></tbody></table>

## Frontend Use Grants

## [Frontend Use Grants](/legal-and-brand/terms-of-service-and-licensing/frontend-license/frontend-use-grants)


# Building a Trading Bot

This is an integration guide for hooking a trading bot into a Symmio deployment.

There are two parts.

[**Part 1: one-time on-chain setup**](/trader-documentation/building-a-trading-bot/part-1-one-time-on-chain-setup) is everything the wallet has to do *once* before any trading happens: create the sub-account, fund it, hand a session key the right authorities, and bind the sub-account to a PartyB. After Part 1 you should never see a wallet popup again on the trade path.

[**Part 2: trading**](/trader-documentation/building-a-trading-bot/part-2-trading) is the actual trading: opening a position, closing it, and attaching a TP/SL. This is the part the session key handles end-to-end.

### Three addresses, three jobs

Three addresses do different jobs, and builders often mix them up.

* **Owner EOA** is your real wallet. It signs all the *on-chain* setup transactions in Part 1, and never signs an EIP-712 trading message.
* **SubAccount** is a deterministic address derived from your EOA plus affiliate. It holds collateral. It's a *virtual* address with no bytecode: you reference it as the "account" in the calldata for opens, and as the `signerAccount.addr` field of the EIP-712 messages you sign to open positions.
* **Virtual Account (VA)** is a deterministic address spawned the first time a `(symbol, direction)` pair is traded under a sub-account. It holds margin and the active quotes. **You sign closes against the VA, not the sub-account.** That's the single most common authorization mistake.

Whenever you build calldata for an EIP-712 op, ask yourself: *whose account is this for?* The session key always *signs*, but `signerAccount.addr` flips between the sub-account (opens) and the VA (closes and TPSL).

Every contract address and service endpoint this guide refers to is deployment-specific; gather the ones for your deployment from Reference: [Solver Addresses & Endpoints](/api-endpoints-and-deployments/solver-addresses-and-endpoints).


# Part 1: one-time on-chain setup

The wallet signs five transactions before you can trade. They all come from the **owner EOA**. Three of them happen exactly once per sub-account; the other two are repeated each time you rotate the session key (typically daily).

The list, in the order you should run them:

1. `createSubAccounts`: make the sub-account
2. `depositForAccount`: fund it with collateral
3. `bindToPartyB`: pin the sub-account to a hedger
4. `grantDelegation`: authorize the session key
5. `grantDelegation`: authorize the TPSL bot

The first three are the durable setup. Steps 4 and 5 are a *pair* of delegations done together; you'll repeat them every time you rotate the session key.

When all five have landed, check what's on-chain before your bot goes live.


# Create the sub-account

**Why.** You trade as a sub-account that your EOA owns, not as the EOA itself. The sub-account is what holds collateral, what gets bound to a PartyB, and what shows up as `signerAccount.addr` in the EIP-712 messages you sign to open positions. Your EOA is the *authority*; the sub-account is the *trader*.

**Signed by.** Owner EOA, on-chain, against the `AccountLayer` diamond.

**Function.** [`createSubAccounts(affiliate, accountsData[])`](https://intent.symmscan.com/inspector/hyperevm/account-layer?method=0x817ed7f6), where each entry has `name`, `metadata` (bytes), `symmioCore` (the diamond address), `isolationType` (uint8), and `singleVAMode` (bool).

**Isolation type** decides how virtual accounts get spawned when you trade. `2 = MARKET_DIRECTION` is the right answer for almost everyone: it gives one VA per `(symbol, direction)`, for example: all your SYMM longs share a VA, all your SYMM shorts share another, and BTC trades are kept separate from SYMM. The other modes are for specialized use cases (per-trade isolation, per-symbol pooled, fully manual).

**`singleVAMode = true` is mandatory if you want TP/SL to work.** With single mode off, every `sendQuote` call creates a fresh VA, even on the same `(symbol, direction)` key. The conditional-orders service that handles TP/SL validates that your already-open position's VA equals `predictNextVirtualAccountAddress(sub, isolationType, symbolId)`. With single mode off, the predicted "next VA" is by definition a *different* VA from the one your position lives on, so the check fails forever. Turn single mode on at creation; it can only be flipped while no VAs are active, so changing your mind later means making a new sub-account.

After this transaction lands, read the new sub-account address from either the tx receipt or [`getUserSubAccountsAddresses(owner, 0, limit)`](https://intent.symmscan.com/inspector/hyperevm/account-layer?method=0x83a08b8f). Save it; you'll need it for everything else.


# Fund the sub-account

Trades reference the sub-account's collateral balance on the Symmio core. You need balance there before you can open anything.

**Signed by:** Owner EOA, two transactions.

**Function:** First, `approve(spender = AccountLayer, amount = max)` on the collateral token, then [`depositForAccount(subAccount, amountWei)`](https://intent.symmscan.com/inspector/hyperevm/account-layer?method=0x342fcda9) on the AccountLayer.

`depositForAccount` (note: not `depositAndAllocateForAccount`) pulls the collateral from your EOA and credits it to the sub-account's balance on the Symmio core. The AccountLayer signs the inner `deposit` call to the core on your behalf via its `globalSigner` mechanism, so from the core's perspective the sub-account is the depositor.

You can re-run this any time you want to top up. You can also withdraw and re-deposit: the sub-account is sticky, the balance isn't.


# Bind the sub-account to a PartyB

Symmio core's `sendQuote` usually runs a Muon signature check on a `SingleUpnlAndPriceSig` parameter, unless the trading account is bound to a specific PartyB, in which case the check is skipped. The solver's instant-trade flow ships an empty `upnlSig` placeholder in the calldata it sends on-chain (it has no way to inject a fresh Muon sig into the EIP-712 op you signed), so without a bind your `executeTemplate` call reverts on-chain with `"LibMuon: Expired signature"`. That surfaces on the notifications feed as `SendQuoteTransaction` / `action_status = failed` / `error_code = 2000`.

Binding fixes this permanently for the sub-account. It pins the sub-account to the deployment's hedger and tells the core "I'm not using Muon oracles for this account; trust the PartyB."

**Signed by.** Owner EOA. The call goes to the AccountLayer's `_call(account, callDatas[])`, because [`bindToPartyB` ](https://intent.symmscan.com/inspector/hyperevm/diamond?method=0xcf462cb2)lives on the Symmio core and the core needs to see the *sub-account* as `msg.sender`. `_call` is the AccountLayer's mechanism for forwarding calls with the right signer context.

**Calldata to encode for the inner call:**

```
selector  = 0xcf462cb2                        // bindToPartyB(address)
arguments = abi.encode(address: hedger)
```

So the actual outer call is:

```
AccountLayer._call(
    account   = subAccount,
    callDatas = [ 0xcf462cb2 || abi.encode(hedger) ]
)
```

After it lands, sanity-check by reading [`Symmio.getBindState(subAccount)`: `status == 1`](https://intent.symmscan.com/inspector/hyperevm/diamond?method=0xa8eda21b) means bound. Once done, leave it alone. There's a `requestToUnbindFromPartyB` flow with a cooldown if you ever need to switch solvers, but you almost never will.


# Delegate the session key

Session keys are how you avoid wallet popups on every trade. Generate an ephemeral keypair, give it on-chain authority to sign for your sub-account for some bounded time window, and then let it sign all the EIP-712 trade messages off-chain. The [InstantLayer](/trader-documentation/how-trading-works-in-symmio/instant-trading#the-instant-layer) verifies these delegations on every `executeBatch` / `executeTemplate` call: when your session key's signature recovers, the InstantLayer checks that the calldata's selector is in the active delegated set for `(sub, sessionKey)`.

**Signed by.** Owner EOA. The call goes to the InstantLayer.

**Function.** `grantDelegation(DelegationInfo info)`, where `DelegationInfo` is:

```
struct DelegationInfo {
    Account  account;            // (subAccount, isPartyB=false)
    address  delegatedSigner;    // the session key's address
    bytes4[] selectors;          // what they're allowed to sign for
    uint256  expiryTimestamp;    // unix seconds
}
```

The selectors are where this gets subtle. You need five of them:

| Selector     | Meaning                                                         |
| ------------ | --------------------------------------------------------------- |
| `0xa6d66852` | `addMarginToNextVA`: pre-funding a VA before opening a position |
| `0xa7f3b34b` | `sendQuoteWithAffiliateAndData`: opening a position             |
| `0x501e891f` | `requestToClosePosition`: closing a position                    |
| `0xeaa31b19` | ID the COH (TP/SL service) keys delegations under               |
| `0x00000001` | ID services use to verify session-key auth                      |

The first three are real Symmio function selectors, and the InstantLayer checks them on every `executeBatch`. The last two are four-byte tags the off-chain services use as lookup keys when they answer "is this session key authorized to talk to me?"&#x20;

{% hint style="info" %}
**Note.** The `grantDelegation` tx is sent from your owner EOA, but once it lands, the *session key* (not the EOA) is what signs all subsequent EIP-712 messages on the trade path.
{% endhint %}


# Delegate the TPSL bot

**Why.** This is separate from delegating the session key. The conditional-orders handler (the service that holds your TP/SL orders and fires them when triggers hit) also checks that *its own* operator address is authorized on your sub-account, not just your session key. If only the session-key delegation ran, the TP/SL endpoint returns `"parent subaccount has not delegated required access to COH for selectors=REQUEST_TO_CLOSE_POSITION"` even though the session key is fine. The bot needs permission to execute closes on your behalf when a trigger fires.

**Signed by.** Owner EOA, again on the InstantLayer's [`grantDelegation`](https://intent.symmscan.com/inspector/hyperevm/instant-layer?method=0xfd7f4c5e).

**`DelegationInfo`:**

* `account`: `(subAccount, isPartyB=false)`
* `delegatedSigner`: `<TPSL bot operator address>` (the deployment's TPSL bot operator)
* `selectors`: four entries: `0x501e891f` (real `requestToClosePosition`), `0xeaa31b19` (Conditional Order Handler's ID), `0xee9ef781` (`requestToClosePosition` overload that takes a `upnlSig`), and `0x00000001`. The COH probes more than one of these depending on the position state, so granting them all is the path of least surprise.
* `expiryTimestamp`: same as the session key's expiry.

So per session refresh you do *two* `grantDelegation` calls: one for the session key with five selectors, one for the bot operator with four selectors. Same EOA signs both.


# Part 2: trading

Now we trade. Every action below is signed by the **session key**, sent over plain HTTP, and confirmed via the notifications WebSocket. Your wallet stays on the shelf.

The three actions:

* Opening a position
* Closing a position
* Take-profit / stop-loss

Before any of them, you need two pieces of market data.

**Symbols.** Fetched once at startup from the [symbols endpoint](/api-endpoints-and-deployments/solver-addresses-and-endpoints). Each symbol gives you a numeric `symbol_id`, a `name` (used as the price-feed key), `price_precision` and `quantity_precision` (number of decimals), `trading_fee` (a fraction, e.g. `0.00012` for 12 bps), `max_leverage`, and `min_notional_value`. Cache it.

**Prices.** Connect to the price WebSocket and read one frame. The server pushes a full snapshot of every tradable symbol on every tick, keyed by `name`. No subscribe message needed.


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

| Side  | Worst-acceptable entry price                               |
| ----- | ---------------------------------------------------------- |
| 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](/api-endpoints-and-deployments/solver-addresses-and-endpoints) 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 0
```

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

This 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

The `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` (plus `partyBmm` if the deployment uses it). The four values always sum to `notional / 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_fee` field is a per-side fraction (e.g. `0.00012` = 12 bps), so reserve `2 × trading_fee` of 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.

| Side  | Upper-bound margin price for sizing | Formula                                                    |
| ----- | ----------------------------------- | ---------------------------------------------------------- |
| 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 `SignedOperation`s

The InstantLayer is the gateway for both ops. Its EIP-712 domain is fixed:

```
domain = {
    name: "SymmioInstantLayer",
    version: "1",
    chainId: <chain id>,
    verifyingContract: <InstantLayer address>
}
```

The `SignedOperation` struct has seven fields, and the order matters because EIP-712 hashes are sensitive to field order:

```
struct SignedOperation {
    address              signer;
    address              target;
    bytes                callData;
    Account              signerAccount;       // (addr, isPartyB)
    FlexField[]          flexFields;          // empty for standard ops
    uint256              maxUses;             // 1 for standard
    ReplayAttackHeader   replayAttackHeader;  // (nonce, deadline, salt)
}
```

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 address
* `target`: AccountLayer
* `signerAccount.addr`: **sub-account** (not the VA, which doesn't exist yet)
* `callData`: selector `0xa6d66852` followed by ABI-encoded `(subAccount: address, vaIsolation: uint8, symbolId: uint256, amountWei: uint256)`. `vaIsolation` is `2` for LONG (`MARKET_LONG`) or `3` for SHORT (`MARKET_SHORT`) under a `MARKET_DIRECTION` sub-account.

**Op 2: send the quote.**

* `signer`: session key's address
* `target`: Symmio Core
* `signerAccount.addr`: **sub-account** (still not the VA)
* `callData`: selector `0xa7f3b34b` followed by the ABI-encoded arguments of `sendQuoteWithAffiliateAndData`:

  ```
  address[]                       partyBsWhiteList,    // [hedger]
  uint256                         symbolId,
  uint8                           positionType,        // 0 = LONG, 1 = SHORT
  uint8                           orderType,           // 1 = MARKET
  uint256                         price,               // worst-acceptable, wei
  uint256                         quantity,            // wei
  uint256                         cva,
  uint256                         lf,
  uint256                         partyAmm,
  uint256                         partyBmm,            // usually 0
  uint256                         deadline,
  address                         affiliate,
  SingleUpnlAndPriceSig           upnlSig,             // EMPTY tuple ★
  bytes                           data                 // abi.encode((string uuid))
  ```

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

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**](/api-endpoints-and-deployments/solver-addresses-and-endpoints) ***before*****&#x20;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:

```json
{
  "channel_patterns": [{
    "app_name": "<your solver app name>",
    "address": "<subAccount>",
    "primary_identifier": "*",
    "secondary_identifier": "*"
  }]
}
```

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:

```json
{ "temp_quote_id": -6188, "partyBmm": "0" }
```

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: 2000` with `va_address == ""`: the on-chain `executeTemplate` reverted with `"LibMuon: Expired signature"`. The sub-account isn't bound (see bind the sub-account).
* `error_code: 502` on `InstantRFQ`: transient solver issue. Retry.
* Other `error_code` values 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.


# Closing a position

Closing is one EIP-712 op. The trick is `signerAccount.addr`: for closes it's the **virtual account**, not the sub-account.

Why? Because as far as the Symmio core is concerned, it's that virtual address that holds the quote. The core looks up `quotes[quoteId].partyA` and checks that the caller (after the AccountLayer's signer-context forwarding) matches. That `partyA` is the VA. Sign as the sub-account and the core rejects the call with an authorization error that doesn't exactly say "wrong account"; it just fails.

### Compute close price

Slippage runs in the *opposite direction* to opens: when you close a long, you're selling, so you accept a *lower* price; when you close a short, you're buying back, so you accept a *higher* price.

| Side closing | Worst-acceptable close price          |
| ------------ | ------------------------------------- |
| CLOSE LONG   | `price = mark × (1 − slippage / 100)` |
| CLOSE SHORT  | `price = mark × (1 + slippage / 100)` |

Quantize to `price_precision` and convert to wei.

### Sign one `SignedOperation`

The envelope is the same `SignedOperation` struct as for opens, the domain is the same `SymmioInstantLayer`, and the signer is the same session key. The fields that differ:

* `target`: Symmio Core (same as `sendQuote`)
* `signerAccount.addr`: **virtual account**
* `callData`: selector `0x501e891f` followed by ABI-encoded `(quoteId: uint256, closePrice: uint256, quantityToClose: uint256, orderType: uint8, deadline: uint256)`. `orderType = 1` for MARKET.

### POST as a JSON array

```
POST <instant-close endpoint>
Content-Type: application/json

[
  {
    "signedOperation": { ... },
    "signature": "0x..."
  }
]
```

Note the array. **Open is a JSON object, close is a JSON array**. Mixing them up may cause failures on the server side.

The solver responds with `200 null` when the close is queued. The notifications WS will show `FillCloseRequest` once it fills on-chain.


# Take-profit / stop-loss

TP/SL is a deferred close. You sign a typed-data message now, the conditional-orders handler (COH) holds it, and when the trigger price is hit the COH submits a real close to the solver on your behalf. That's why the bot delegation exists.

### A different domain and message shape

Unlike opens and closes, TP/SL doesn't use the `SymmioInstantLayer` domain. It uses:

```
domain = {
    name: "ConditionalOrder",
    version: "1",
    chainId: <chain id>,
    verifyingContract: <InstantLayer address>      // same address
}
```

The message has a top level with seven scalar fields and three nested "leg" structs:

* `virtualAccount`: the VA holding the position
* `subAccount`: the parent sub-account
* `salt`: random `uint256`
* `quoteId`: `int256` (yes, `int` for some reason; the value is positive)
* `symbolId`, `positionType`, `affiliate`
* `takeProfit`: leg struct (see below)
* `stopLoss`: leg struct (see below)
* `sendQuote`: leg struct with an extra `leverage` field (see below)

**All three legs must be present** even if you're only setting one. The COH rejects messages with missing legs. Zero-fill the unused legs:

```
zero_leg = {
    "quantity": "0", "price": "0", "orderType": 0,
    "conditionalPrice": "0", "conditionalPriceType": "market"
}
zero_send = { ...zero_leg, "leverage": 0 }
```

For an active leg, the fields mean:

* `quantity`: total quantity to close (decimal string, must match symbol's `quantity_precision`)
* `price`: current mark price as a reference (decimal string, must match `price_precision`)
* `orderType`: `1` for MARKET
* `conditionalPrice`: the *trigger* price (decimal string, must match `price_precision`)
* `conditionalPriceType`: typically `"last_close"`

**Decimal precision matters here.** The COH rejects unbounded decimals with `error_code: 407` "provided values do not meet the required precision standards". If `price_precision = 6`, send `"0.013242"`, not `"0.013241567893"`. Same for `quantity` and `conditionalPrice`.

You can sanity-check your typed-data encoder: hit `GET <conditional-orders endpoint>/signing-spec` (with the deployment's `App-Name` header) and you'll get back the live, authoritative type schema plus an `expectedMessageHash` for the included example. Encode that example with your library and confirm you produce the same hash.

### POST it

```
POST <conditional-orders endpoint>
App-Name: <your app name>
Content-Type: application/json

{
  "typedData": { /* the full {types, domain, primaryType, message} */ },
  "signer": "<sessionKey address>",
  "signature": "0x..."
}
```

`200 { "successful": true }` means the order is registered. The COH will fire it on trigger, you'll see `TriggeredTPSL` on the notifications feed, and a normal close fill will follow.

Failure modes worth recognizing on sight:

* `401 "parent subaccount has not delegated required access to COH for selectors=REQUEST_TO_CLOSE_POSITION"` with `recovered_signer = 0x2471...`: the bot delegation is missing. Re-run it.
* `401 "recovered signer is neither owner nor active session key"`: the session-key delegation for `0x00000001` is gone (expired or never granted). Re-run it.
* `401 "virtualAccount X does not match predicted VA Y"`: your sub-account doesn't have `singleVAMode = true`. This can't be fixed on the existing sub-account; create a new one.
* `400 error_code 407`: precision. Quantize before sending.
* `404` on a quote that already has a TP/SL: one-shot per quote. Cancel the existing order or open a new position.


# Code Examples

Check [this repository](https://github.com/academy17/vibe_trading_bot_sample) for example codes for all the main trading bot functions (open, close and setting TP/SL)


# Interacting with Contracts


# Querying Info from our Contracts

### Querying info from the SYMMIO diamond

Symmio is built on the Diamond Standard, so the protocol's read methods are spread across many facets behind a single address. Calling them through a generic block explorer is painful: the explorer doesn't know which facet implements which selector, and the ABI surface is large. The [SYMMIO Intent Inspector](https://intent.symmscan.com/inspector) is built for this. It reads the live facet and selector map per chain, gives you a typed form for every read function, and decodes the results into something legible.

{% embed url="<https://intent.symmscan.com/inspector>" %}

{% hint style="info" %}
The Inspector works against any SYMMIO diamond. The Symmio core, the AccountLayer, and the InstantLayer are all browsable; just pick the chain and paste the diamond address you want to inspect.
{% endhint %}

### Opening the Inspector

Go to [intent.symmscan.com/inspector](https://intent.symmscan.com/inspector), pick the chain you want to query (the Inspector supports every chain Symmio is deployed on), then paste the diamond address, or use one of the suggested defaults for that chain.

<figure><img src="/files/XUoHDPan8YkLvheFMl72" alt=""><figcaption></figcaption></figure>

The URL pattern is human-readable too, so you can deep-link straight to a specific function call:

```
https://intent.symmscan.com/inspector/<chain>/diamond
https://intent.symmscan.com/inspector/<chain>/diamond?method=<selector>
```

So you can deep-link to a specific function call.

#### Browsing facets and selectors

Once a diamond is loaded, the Inspector shows every facet registered on it, along with every function selector that facet implements. This is the source of truth for whether a given function is live on a given chain: if a selector is missing from the list, the facet hasn't been deployed (or hasn't been cut into the diamond) on that chain.

<figure><img src="/files/2juHmM1PwvXV2mLQbYuF" alt=""><figcaption></figcaption></figure>

#### Calling a view function

Click any read function (`view` or `pure`) and the Inspector builds a typed input form for its parameters. Fill in the inputs, hit "call", and the result comes back decoded.

<figure><img src="/files/QA6TEWBhy64yYR0Qi440" alt=""><figcaption></figcaption></figure>

Some common reads you'll do this way:

<table><thead><tr><th width="191">Function</th><th width="145.6666259765625">Diamond</th><th>What you get</th></tr></thead><tbody><tr><td><code>getUserSubAccountsAddresses(owner, offset, limit)</code></td><td>AccountLayer</td><td>All SubAccounts owned by an EOA</td></tr><tr><td><code>getVirtualAccountsAddressesOfSubAccount(sub, offset, limit)</code></td><td>AccountLayer</td><td>Currently-active VAs on a SubAccount</td></tr><tr><td><code>getVirtualAccountQuoteIds(va, offset, limit)</code></td><td>AccountLayer</td><td>Open quote IDs the AccountLayer is tracking on a VA</td></tr><tr><td><code>predictNextVirtualAccountAddress(sub, isolation, symbolId)</code></td><td>AccountLayer</td><td>Deterministic next-VA address — use this when computing <code>addMarginToNextVA</code> calldata</td></tr><tr><td><code>getActiveVAByKey(sub, isolation, symbolId)</code></td><td>AccountLayer</td><td>The live VA for <code>(sub, isolation, symbol)</code>, or <code>0x0</code> if none</td></tr><tr><td><code>getBindState(account)</code></td><td>Symmio core</td><td><code>(status, partyB, timestamp)</code> — <code>status == 1</code> means bound</td></tr><tr><td><code>getQuote(quoteId)</code></td><td>Symmio core</td><td>The full quote struct (size, price, locked margins, state, etc.)</td></tr></tbody></table>

For the full set of read functions on each diamond, see the facet listing. Anything view-tagged is callable from the Inspector.

### Decoding calldata

If you have a raw `0x…` blob, from a failed transaction, a log, or an EIP-712 `SignedOperation`'s `callData` field, the Inspector can decode it as long as the leading selector matches one in the diamond's selector map. Paste the calldata, pick the diamond, and the Inspector resolves it to a function name and pretty-prints the arguments.

<figure><img src="/files/XXcysP1l9KmBvH6n6NkG" alt=""><figcaption></figcaption></figure>

This is the fastest way to check an Instant Layer `SignedOperation`: sign one, paste the `callData` into the decoder, and confirm it calls what you expect before posting to the solver.

### Reading bind state for an account

When you're wiring up the Instant Layer flow, the most common reason `sendQuote` reverts is that the SubAccount isn't bound to a PartyB. Bind state lives on the Symmio core, not the AccountLayer, so:

1. Open the Symmio core diamond in the Inspector.
2. Call `getBindState(<subAccount>)`.
3. Expect `status = 1` and `partyB = <your hedger>`. Anything else means `bindToPartyB` hasn't landed, or landed on a different SubAccount.

<figure><img src="/files/hVfVRo4P2PoTq8DOZFcA" alt=""><figcaption></figcaption></figure>

#### Reading a quote's state

Given a `quoteId` from the hedger's `SendQuoteTransaction` notification, call `getQuote(quoteId)` on the Symmio core to see the full quote struct: price, quantity, filled amounts, locked margins, state (`PENDING`, `LOCKED`, `OPEN`, `CLOSE_PENDING`, `CLOSED`, and so on), the bound PartyB, and the affiliate.

<figure><img src="/files/YOIAG1JYZz4SSYY9R1hW" alt=""><figcaption></figcaption></figure>


# Closing a Position On-Chain

If you need to close your position or cancel a quote to unlock collateral but the frontend is unavailable, you can follow these steps to execute the operation manually.

### Solution 1: Use the Symmio Intent Explorer

#### Step 1: Find the Wallet you made the account under

Paste the wallet address that you traded with into the [Symmio Intent Explorer](https://intent.symmscan.com/profile).

<figure><img src="/files/RuHmFUJoPbP99HFwT7aj" alt=""><figcaption><p>Symmio Intent Explorer</p></figcaption></figure>

#### Step 2: Initiate the Close and Confirm

<figure><img src="/files/iRTr5x3KXjSo73cy8Joz" alt=""><figcaption><p>How to Initiate the Close</p></figcaption></figure>

<figure><img src="/files/LFeKknndOa7RKU24fjCg" alt=""><figcaption><p>Confirmation of Close</p></figcaption></figure>

### Solution 2: Manually Close with a Block Explorer

#### Step 1: Locate the Transaction When You Opened the Position

You need to find the `SendQuote` event log that was emitted when you initially opened your position. This log contains essential information such as the `partyA` address and the `quoteId`. You can obtain this information from a block explorer (like Etherscan) by searching for your account and looking for `_call()` transactions .

Here’s an example of what the `SendQuote` emitted event might look like:

<figure><img src="/files/cFi8tlj4H1kCA4UB26tJ" alt=""><figcaption><p>sendQuote event</p></figcaption></figure>

#### Step 2: Find the `calldatas[]` for Closing the Position

You need the `calldata[]` to send to the diamond contract (in this case, the SYMMIO Clearing Engine) to request to close your position. Here's how you can do that:

1. **Find the Diamond Address**: From the `SendQuote` log (from Step 1), identify the address of the SYMMIO Clearing Engine It will look something like this:

   ```plaintext
   Address: 
   0x976c87cd3eb2de462db249cca711e4c89154537b
   (SYMMIO: Clearing Engine)
   ```
2. **Access Louper.dev**:
   * Go to [louper.dev](https://louper.dev).
   * Paste the diamond address and select the correct chain (e.g., Ethereum, BSC, etc.).
   * Connect the wallet you originally used to open the position.
3. **Request to Close the Position**:

   * Navigate to the **Write** tab in louper.dev.
   * Look for the function `requestToClosePosition` (or `requestToCancelQuote` if the quote was never responded to). Populate the fields using the data you extracted from the `SendQuote` log:

   <figure><img src="/files/YeI1dnvXzvHpUzbZpg3Z" alt=""><figcaption><p>requestToClosePosition example</p></figcaption></figure>

<figure><img src="/files/V8Hgv90ezeyoHhxGziWm" alt=""><figcaption><p>requestToCancelQuote() example</p></figcaption></figure>

Make sure you adjust the closing price to ensure it reflects current market price with some added slippage to ensure that the closing operation goes through. For longs the `closePrice` should be slightly less than the `marketPrice` and for shorts the `closePrice` should be slightly more than the `marketPrice`

Once you’ve populated the fields, click **Write** and wait for a wallet popup. Click the **View Raw** button to see the raw transaction data. This is where you’ll see the `calldata[]` generated. Copy this as a HEX string.

<figure><img src="/files/qllMXkZqmuV2JsBpcSO7" alt=""><figcaption></figcaption></figure>

#### Step 3: Execute the `_call()` on the MultiAccount Contract

Now that you have the `calldata[]`, you can manually execute the closing request:

1. **Find the MultiAccount Contract**: This is the contract you used to open the position, the same one you checked the logs for to find the SendQuote event.
2. **Perform the `_call()`**:
   * Connect your trading wallet to the appropriate block explorer depending on the chain the trade was opened on (e.g., Etherscan or another compatible tool).
   * Perform a `_call()` by pasting the `calldata[]` and `partyA` address into the appropriate fields.

{% hint style="info" %}
The `partyA` is not the wallet you use but the sub-account address. You can find the `partyA` address in the details of the sendQuote event.
{% endhint %}

<figure><img src="/files/xMvnVPCBcSF53Y4NimHz" alt=""><figcaption><p>Writing with a Block Explorer</p></figcaption></figure>

Submit the transaction on-chain to close the position by clicking **Write**. If everything was set up correctly, the position should be closed successfully.

{% hint style="info" %}
If you encounter an "is not a valid array" error, you must send the calldata as a hex string, enclosed in brackets and quotes e.g.

`["0xa63b936300000000000000000000000000000000000000000000000000000000000002ba"]`
{% endhint %}


# Building a Trading Bot

This guide provides a comprehensive walkthrough for building a trading bot that interacts via the SYMM protocol using Python. This guide will focus on using an affiliate's [MultiAccount](/contract-documentation/symmio-perps-v0.8.4/helper-contracts/multiaccount) with [Instant Trading](/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading) without relying on a frontend application.

The full trading bot and example codes can all be found in this [repository](https://github.com/academy17/symm_example_codes).

### Prerequisites

Before starting, ensure you have:

* Python 3.8+
* A wallet with private key (for signing transactions)
* A suitable RPC endpoint
* Environment variables set up in a .env file


# Creating a Sub-Account

### Step 1: Creating a Sub-Account

A sub-account allows you to manage multiple trading accounts under one main wallet. This is done through an affiliate's [MultiAccount](/exchange-builder-documentation/frontend-builder-technical-guidance/legacy-guides/multiaccount-legacy-guide) contract. Contract addresses can be found [here](broken://pages/aqT95qOMDcvaUY5aCr93).

#### How to Create a Sub-Account

To create a sub-account, we use the MultiAccount contract's `addAccount()` method, passing a name for the sub-account:

```python
from dotenv import load_dotenv
import os
import json
from web3 import Web3

load_dotenv()

CONFIG = {
    "rpc_url": os.getenv("RPC_URL"),
    "private_key": os.getenv("PRIVATE_KEY"),
    "multiaccount_address": os.getenv("MULTIACCOUNT_ADDRESS"),
}

class MultiAccountClient:
    def __init__(self, config):
        self.config = config
        
        abi_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "abi", "MultiAccount.json"))
        with open(abi_path, "r") as abi_file:
            self.abi = json.load(abi_file)
        
        self.w3 = Web3(Web3.HTTPProvider(config["rpc_url"]))
        self.account = self.w3.eth.account.from_key(config["private_key"])
        self.multiaccount = self.w3.eth.contract(
            address=Web3.to_checksum_address(config["multiaccount_address"]),
            abi=self.abi
        )
    
    def add_account(self, name: str):
        """Add a new account and retrieve its address from the emitted event"""
        try:
            txn = self.multiaccount.functions.addAccount(name).build_transaction({
                "from": self.account.address,
                "nonce": self.w3.eth.get_transaction_count(self.account.address, "pending"),
                "gas": 8000000,
                "gasPrice": self.w3.eth.gas_price,
            })
            
            signed_txn = self.w3.eth.account.sign_transaction(txn, private_key=self.config["private_key"])
            tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
            print(f"Transaction sent: {tx_hash.hex()}")
            
            receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
            print("Transaction confirmed.")
            
            event_signature_hash = self.w3.keccak(text="AddAccount(address,address,string)").hex()
            for log in receipt.logs:
                if log.topics[0].hex() == event_signature_hash:
                    decoded_event = self.multiaccount.events.AddAccount().process_log(log)  
                    account_address = decoded_event["args"]["account"]
                    print(f"New account created: {account_address}")
                    return account_address
            
            print("AddAccount event not found in transaction logs.")
            return None
        except Exception as e:
            print(f"Error adding account: {e}")
            raise

def main():
    """Main function to demonstrate adding an account"""
    client = MultiAccountClient(CONFIG)
    
    # Example: Name for the new account
    account_name = "sdk_client"  # Replace with the desired account name
    
    new_account_address = client.add_account(account_name)
    if new_account_address:
        print(f"Successfully created account with address: {new_account_address}")
    else:
        print("Failed to retrieve the new account address.")

if __name__ == "__main__":
    main()
```

When the transaction is successful, a new account address that will be used for all your trading activities will be emitted by the contract. This address doesn't have its own private key - it's controlled by your main wallet.


# Depositing and Allocating Collateral

Before trading, you need to deposit collateral to your sub-account.The process involves two steps:

1. Approving the MultiAccount contract to spend your ERC20 tokens
2. Calling the `depositAndAllocateForAccount` function

More information on this process [here](/exchange-builder-documentation/frontend-builder-technical-guidance/creating-an-account-and-depositing-funds):

```python
# Approve the MultiAccount contract to spend your tokens
client.approve_erc20(CONFIG["multiaccount_address"], deposit_amount)
    
# Deposit and allocate the tokens to your sub-account
client.deposit_and_allocate_for_account(sub_account_address, deposit_amount)
```

The deposit amount should be in wei (1 ETH = 10^18 wei). For example, to deposit 100 tokens, use:

```python
deposit_amount = Web3.to_wei(100, "ether")
```

**Example Code:**

```python
from dotenv import load_dotenv
import os
import json
from web3 import Web3


load_dotenv()


CONFIG = {
    "rpc_url": os.getenv("RPC_URL"),
    "private_key": os.getenv("PRIVATE_KEY"),
    "multiaccount_address": os.getenv("MULTIACCOUNT_ADDRESS"),
    "erc20_address": os.getenv("COLLATERAL_ADDRESS"),  
}

class MultiAccountClient:
    def __init__(self, config):
        self.config = config
        
        
        multiaccount_abi_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "abi", "MultiAccount.json"))
        erc20_abi_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "abi", "ERC20.json"))
        
        with open(multiaccount_abi_path, "r") as abi_file:
            self.multiaccount_abi = json.load(abi_file)
        
        with open(erc20_abi_path, "r") as abi_file:
            self.erc20_abi = json.load(abi_file)
        
        
        self.w3 = Web3(Web3.HTTPProvider(config["rpc_url"]))
        self.account = self.w3.eth.account.from_key(config["private_key"])
        self.multiaccount = self.w3.eth.contract(
            address=Web3.to_checksum_address(config["multiaccount_address"]),
            abi=self.multiaccount_abi
        )
        self.erc20 = self.w3.eth.contract(
            address=Web3.to_checksum_address(config["erc20_address"]),
            abi=self.erc20_abi
        )
    
    def approve_erc20(self, spender_address: str, amount: int):
        """Approve the MultiAccount contract to spend ERC20 tokens"""
        try:
            
            txn = self.erc20.functions.approve(
                Web3.to_checksum_address(spender_address),
                amount
            ).build_transaction({
                "from": self.account.address,
                "nonce": self.w3.eth.get_transaction_count(self.account.address, "pending"),
                "gas": 100000,
                "gasPrice": self.w3.eth.gas_price,
            })
            
            
            signed_txn = self.w3.eth.account.sign_transaction(txn, private_key=self.config["private_key"])
            tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
            print(f"Approval transaction sent: {tx_hash.hex()}")
            
            
            self.w3.eth.wait_for_transaction_receipt(tx_hash)
            print("Approval transaction confirmed.")
        except Exception as e:
            print(f"Error approving ERC20 tokens: {e}")
            raise

    def deposit_and_allocate_for_account(self, account_address: str, amount: int):
        """Deposit and allocate funds for a specific account"""
        try:
            
            txn = self.multiaccount.functions.depositAndAllocateForAccount(
                Web3.to_checksum_address(account_address),
                amount
            ).build_transaction({
                "from": self.account.address,
                "nonce": self.w3.eth.get_transaction_count(self.account.address, "pending"),
                "gas": 300000,
                "gasPrice": self.w3.eth.gas_price,
            })
            
            
            signed_txn = self.w3.eth.account.sign_transaction(txn, private_key=self.config["private_key"])
            tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
            print(f"Deposit and allocate transaction sent: {tx_hash.hex()}")
            
            
            self.w3.eth.wait_for_transaction_receipt(tx_hash)
            print("Deposit and allocate transaction confirmed.")
        except Exception as e:
            print(f"Error depositing and allocating for account: {e}")
            raise

def main():
    """Main function to demonstrate depositing and allocating for an account"""
    client = MultiAccountClient(CONFIG)
    
    
    sub_account_address = "0x4921a5fC974d5132b4eba7F8697236fc5851a3fA"  
    deposit_amount = Web3.to_wei(100, "ether")  #Depositing 100 tokens
    
    
    print("Approving ERC20 tokens...")
    client.approve_erc20(CONFIG["multiaccount_address"], deposit_amount)
    
    
    print("Depositing and allocating for account...")
    client.deposit_and_allocate_for_account(sub_account_address, deposit_amount)

if __name__ == "__main__":
    main()
```


# Delegating Access to the Solver

### Step 3: Delegating Access to the Solver

For instant trading, you need to delegate certain function access rights to the SYMM solver (also called PartyB). You can read more about this requirement [here](/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading/instant-login-eoa).

#### Required Function Selectors

The following function selectors are required for instant trading:

* `0x7f2755b2` - sendQuote
* `0x40f1310c` - sendQuoteWithAffiliate
* `0x501e891f` - requestToClosePosition

#### Example Code (Delegating Access via MultiAccount for a sub-account)

```python
from dotenv import load_dotenv
import os
import json
from web3 import Web3

load_dotenv()

CONFIG = {
    "rpc_url": os.getenv("RPC_URL"),
    "private_key": os.getenv("PRIVATE_KEY"),
    "multiaccount_address": os.getenv("MULTIACCOUNT_ADDRESS"),
    "sub_account_address": os.getenv("SUB_ACCOUNT_ADDRESS"),
    "party_b_address": os.getenv("PARTY_B_ADDRESS"),
}

class MultiAccountClient:
    def __init__(self, config):
        self.config = config
        
        abi_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "abi", "MultiAccount.json"))
        with open(abi_path, "r") as abi_file:
            self.abi = json.load(abi_file)
        
        self.w3 = Web3(Web3.HTTPProvider(config["rpc_url"]))
        self.account = self.w3.eth.account.from_key(config["private_key"])
        self.multiaccount = self.w3.eth.contract(
            address=Web3.to_checksum_address(config["multiaccount_address"]),
            abi=self.abi
        )
    
    def delegate_accesses(self, account_address: str, target_address: str, selectors: list, state: bool):
        """Batch delegate access for multiple function selectors"""
        try:
            selectors_bytes = [Web3.to_bytes(hexstr=s) for s in selectors]
            txn = self.multiaccount.functions.delegateAccesses(
                Web3.to_checksum_address(account_address),
                Web3.to_checksum_address(target_address),
                selectors_bytes,
                state
            ).build_transaction({
                "from": self.account.address,
                "nonce": self.w3.eth.get_transaction_count(self.account.address, "pending"),
                "gas": 400000,
                "gasPrice": self.w3.eth.gas_price,
            })
            signed_txn = self.w3.eth.account.sign_transaction(txn, private_key=self.config["private_key"])
            tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
            print(f"Delegate accesses transaction sent: {tx_hash.hex()}")
            self.w3.eth.wait_for_transaction_receipt(tx_hash)
            print("Delegate accesses transaction confirmed.")
        except Exception as e:
            print(f"Error delegating accesses: {e}")
            raise

def main():
    client = MultiAccountClient(CONFIG)
    sub_account_address = CONFIG["sub_account_address"]
    party_b_address = CONFIG["party_b_address"]
    selectors = [
        "0x7f2755b2",  # sendQuote
        "0x40f1310c",  # sendQuoteWithAffiliate
        "0x501e891f",  # requestToClosePosition
    ]
    state = True

    print("Batch delegating access for Instant Open and Instant Close...")
    client.delegate_accesses(sub_account_address, party_b_address, selectors, state)

if __name__ == "__main__":
    main()
```


# Authenticating Trades

### Authentication and Login

To interact with a solver's [instant trading endpoints](/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading), you need to authenticate using Sign-In With Ethereum (SIWE).

#### SIWE Authentication Process

1. Request a nonce from the server
2. Create a SIWE message with the nonce
3. Sign the message with your wallet
4. Send the signature to the server to get an access token

**Example Code:**

```python
import os
import requests
import json
from eth_account import Account
from eth_account.messages import encode_defunct
from dotenv import load_dotenv
from datetime import datetime, timedelta, timezone


load_dotenv()


PRIVATE_KEY = os.getenv("PRIVATE_KEY")
ACTIVE_ACCOUNT = os.getenv("SUB_ACCOUNT_ADDRESS")
HEDGER_URL = os.getenv("HEDGER_URL")
CHAIN_ID = int(os.getenv("CHAIN_ID", 42161))  
DOMAIN = "localhost"
ORIGIN = "http://localhost:3000"
LOGIN_URI = f"{HEDGER_URL}/login"


ISSUED_AT = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"

EXPIRATION_DATE = (datetime.now(timezone.utc) + timedelta(hours=2, minutes=30)).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"


wallet = Account.from_key(PRIVATE_KEY)

def get_nonce(address: str) -> str:
    """Fetch the nonce for the active account from the server."""
    url = f"{HEDGER_URL}/nonce/{address}"
    response = requests.get(url)
    response.raise_for_status()
    return response.json()["nonce"]

def build_siwe_message(domain, address, statement, uri, version, chain_id, nonce, issued_at, expiration_time):
    """Build a SIWE message string following the EIP-4361 format."""
    return f"""{domain} wants you to sign in with your Ethereum account:
{address}

{statement}

URI: {uri}
Version: {version}
Chain ID: {chain_id}
Nonce: {nonce}
Issued At: {issued_at}
Expiration Time: {expiration_time}"""

def main():
    try:
        print(f"\n[1/4] Wallet Address: {wallet.address}")
        print(f"[1.5/4] Active Account (Checksum): {ACTIVE_ACCOUNT}")

        nonce = get_nonce(ACTIVE_ACCOUNT)
        print(f"[2/4] Got nonce: {nonce}")

        message_string = build_siwe_message(
            domain=DOMAIN,
            address=wallet.address,
            statement=f"msg: {ACTIVE_ACCOUNT}",
            uri=LOGIN_URI,
            version="1",
            chain_id=CHAIN_ID,
            nonce=nonce,
            issued_at=ISSUED_AT,
            expiration_time=EXPIRATION_DATE
        )
        
        print("\n[3/4] SIWE message to sign:\n", message_string)

        message = encode_defunct(text=message_string)
        
        signed_message = wallet.sign_message(message)
        signature = "0x" + signed_message.signature.hex()
        print("\nSignature:", signature)

        body = {
            "account_address": ACTIVE_ACCOUNT,
            "expiration_time": EXPIRATION_DATE,
            "issued_at": ISSUED_AT,
            "signature": signature,
            "nonce": nonce
        }

        print("body: ", body)

        
        headers = {
            "Content-Type": "application/json",
            "Origin": ORIGIN,
            "Referer": ORIGIN,
        }

        print("\n[4/4] Sending login request...")

        
        response = requests.post(
            LOGIN_URI,
            json=body,
            headers=headers
        )
        
        
        if response.status_code != 200:
            print(f"Server response: {response.status_code}")
            print(f"Response text: {response.text}")
            
        response.raise_for_status()
        print("Login response:", response.json())
    except Exception as e:
        print(f"Error in SIWE login flow: {e}")
        
        import traceback
        traceback.print_exc()

if __name__ == "__main__":
    main()
```

The access token will be used for all subsequent API requests. Multiple access tokens can be generated so this step can be done before every instant order if the token is not saved.


# Monitoring Price

The sample trading bot makes decisions based on price movements. Because the asset being traded is hedged with Binance it's appropriate to monitor the price using their endpoints. In our sample, we are executing a basic strategy: longing when price is below or equal to our defined `ENTRY_PRICE` and closing our position when the price is equal or above our `EXIT_PRICE`.

#### Setting Up Price Monitoring

We define entry and exit prices in the configuration:

```python
CONFIG = {
    "SYMBOL": "XRPUSDT",
    "ENTRY_PRICE": "3.05",  # Enter when price falls below this
    "EXIT_PRICE": "3.1",    # Exit when price rises above this
    "QUANTITY": "6",
    # ... other configuration options
}
```

#### Monitoring Loop

The bot continuously monitors the price and compares it to our thresholds:

```python
# Main trading loop
while True:
    # Get current price from Binance
    current_price = get_binance_price(CONFIG["SYMBOL"])
    
    # Check for entry conditions
    if not in_position and current_price <= entry_price:
        # Open a position
        
    # Check for exit conditions
    elif in_position and current_price >= exit_price:
        # Close the position
    
    time.sleep(CONFIG["POLL_INTERVAL"])
```

The `get_binance_price` function fetches the current price from Binance's API:

```python
def get_binance_price(symbol):
    """Get the current price of a symbol from Binance."""
    try:
        params = {"symbol": symbol}
        response = requests.get(BINANCE_API_URL, params=params)
        response.raise_for_status()
        data = response.json()
        price = Decimal(data["price"])
        print(f"[PRICE] Current {symbol} price: {price}")
        return price
    except Exception as e:
        print(f"[ERROR] Failed to get Binance price: {e}")
        return None
```


# Opening a Position

When the entry condition is met, we need to open a position using the solver's instant trading API. More details on this can be found [here](/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading/sending-a-quote-instant-open).

#### Fetching Price from Muon Oracle

For on-chain verification of price and solvency, we need to fetch the price and a signature from the Muon oracle:

```python
def fetch_muon_price():
    try:
        response = requests.get(MUON_URL)
        data = response.json()
        fetched_price_wei = data["result"]["data"]["result"]["price"]
        return Decimal(fetched_price_wei) / Decimal('1e18')
    except Exception as e:
        print(f"[ERROR] Failed to fetch Muon price: {e}")
        return None
```

#### Applying Slippage

Since we're going LONG, we increase the price by 1% to ensure our order gets filled:

```python
# For long positions, add 1% slippage
adjusted_price = fetched_price * Decimal('1.01')
```

#### Preparing Trade Parameters

We need to calculate collateral parameters based on the notional value and parameters returned from the `get_locked_params` solver endpoint.

```python
def fetch_locked_params():
    """Fetch locked parameters for the trade."""
    try:
        response = requests.get(LOCKED_PARAMS_URL)
        response.raise_for_status()
        data = response.json()
        if data.get("message") == "Success":
            return data
        else:
            raise ValueError("Failed to fetch locked parameters")
    except Exception as e:
        print(f"[ERROR] Failed to fetch locked parameters: {e}")
        return None

def calculate_normalized_locked_value(notional, locked_param, leverage, apply_leverage=True):
    """Compute normalized locked value for a given parameter."""
    notional = Decimal(str(notional))
    locked_param = Decimal(str(locked_param))
    leverage = Decimal(str(leverage))
    
    if apply_leverage:
        return str(notional * locked_param / (Decimal('100') * leverage))
    else:
        return str(notional * locked_param / Decimal('100'))
```

#### Sending the Trade Request

```python
trade_params = {
    "symbolId": CONFIG["SYMBOL_ID"],
    "positionType": CONFIG["POSITION_TYPE"],  # 0 for long, 1 for short
    "orderType": 1,  # 1 for market order (must be market since we're using instant actrions)
    "price": str(adjusted_price),
    "quantity": CONFIG["QUANTITY"],
    "cva": normalized_cva,
    "lf": normalized_lf,
    "partyAmm": normalized_party_amm,
    "partyBmm": '0',
    "maxFundingRate": CONFIG["MAX_FUNDING_RATE"],
    "deadline": deadline
}

response = requests.post(f"{HEDGER_URL}/instant_open", json=trade_params, headers=headers)
```

The API will return a temporary quote ID that we'll use to track our order.


# Setting a Stop Loss

Once a position is successfully opened via the solver’s **instant trading API**, it's worth setting a stop loss to protect that position. This ensures risk is bounded even if the bot goes offline or market conditions change unexpectedly.

***

### High-Level Flow

1. Authenticate with the Hedger API ([SIWE login](/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading/instant-login-eoa))
2. Open an instant market position
3. Let the **temporary quote ID** resolve into a permanent `quoteId`
4. Calculate the stop-loss price
5. Submit a stop-loss request to the solver API

> ⚠️ A stop loss **must reference a permanent `quoteId`**. You cannot attach SL/TP to a temporary quote.

***

### Authentication (SIWE Login)

Before any protected actions (open, stop loss, cancel, etc.), the bot must authenticate using **Sign-In With Ethereum (SIWE)**.

1. Fetch a nonce from the hedger
2. Build a SIWE message scoped to the **active trading account**
3. Sign the message with the owner wallet
4. Exchange the signature for a JWT access token

```python
token = login()
```

The returned `access_token` is used as a Bearer token for all subsequent requests.

***

### Opening the Position (Instant Open)

The bot opens a position using the solver’s `instant_open` endpoint.

Key points:

* Orders **must be market orders** (`orderType = 1`)
* The price is derived from the **Muon Oracle**
* Slippage is applied to guarantee execution
* Collateral parameters are calculated using `get_locked_params`

```python
temp_quote_id, open_price = open_instant_trade(token)
```

#### Returned Values

* `temp_quote_id`: Temporary identifier for the order
* `open_price`: The raw Muon price used to derive SL/TP levels

***

### Resolving the Temporary Quote ID

Instant trades initially return a **temporary quote ID**.\
Before setting a stop loss, the bot must wait until this temp ID is finalized into a **permanent `quoteId`**.

This is done by polling the user’s active quotes:

```python
quote_id = poll_quote_status(token, temp_quote_id)
```

#### Why This Is Required

* Stop loss and take profit requests are validated against finalized on-chain quotes
* Using a temp ID will result in rejection

***

### Calculating the Stop Loss Price

Once the quote is confirmed, derives a stop-loss price relative to the entry price.

In this example:

* The bot is **LONG**
* Stop loss is set at **80% of entry price**

```python
sl_price = (open_price * Decimal('0.8')).quantize(
    Decimal("0.000001"),
    rounding=ROUND_DOWN
)
```

#### Notes

* Precision should match the symbol’s supported decimals
* For SHORT positions, this logic would be inverted

***

### Stop Loss Request Payload

The stop loss is submitted using the `stop_loss` endpoint.

{% hint style="info" %}
This refers to [PerpsHub TPSL Implementation](/api-endpoints-and-deployments/solver-addresses-and-endpoints/perps-hub/perpshub-tp-sl-implementation), check [Rasa's TPSL Implementation](/api-endpoints-and-deployments/solver-addresses-and-endpoints/rasa-capital/rasa-solver-tp-sl-implementation) for detailed instructions on how to construct their payload.
{% endhint %}

#### Payload Structure

```json
{
  "userAddress": "<wallet address>",
  "accountAddress": "<active account>",
  "positionSide": 0,
  "symbolId": 4,
  "requestedPrice": "ENTRY_PRICE",
  "quoteId": 12345,
  "tpPrice": "",
  "slPrice": "STOP_LOSS_PRICE",
  "timestamp": 1710000000000
}
```

#### Field Explanation

| Field            | Description                          |
| ---------------- | ------------------------------------ |
| `userAddress`    | Wallet that signed the login message |
| `accountAddress` | Trading account (EOA or sub-account) |
| `positionSide`   | `0 = Long`, `1 = Short`              |
| `symbolId`       | Internal symbol identifier           |
| `requestedPrice` | Original entry price                 |
| `quoteId`        | Permanent quote ID                   |
| `tpPrice`        | Leave empty if not setting TP        |
| `slPrice`        | Calculated stop loss price           |
| `timestamp`      | Client-side timestamp (ms)           |

***

### Sending the Stop Loss Request

```python
response = requests.post(
    f"{HEDGER_URL}/stop_loss",
    json=payload,
    headers={
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}"
    }
)
```

On success, the solver will acknowledge the stop loss and register it for on-chain execution when the trigger price is reached.

***

### Complete Execution Flow

```
Login (SIWE)
   ↓
Instant Open Position
   ↓
Receive temp_quote_id
   ↓
Poll until quoteId is finalized
   ↓
Compute stop loss price
   ↓
POST /stop_loss
```

A full example implementation can be found in our SYMM SDK.


# Monitoring Position Status

After sending the trade request via the instant open endpoint, we must monitor its status to get the permanent quote ID (this is the QID which is stored on-chain). The endpoint for this is documented [here](/liquidity-provider-documentation/building-a-solver-on-symmio/instant-trading/rasa-instant-trading-implementation#instant-open-position).

#### Polling for Quote Status

The temporary quote ID will be replaced with a permanent ID once the trade is confirmed:

```python
def poll_quote_status(token, temp_quote_id):
    """Poll for the status of a quote until it gets a permanent ID.""" #In order to track the status of the quote, we will poll the /instant_open/{address} endpoint
    print(f"[STATUS] Starting to poll for quote status of temp ID: {temp_quote_id}")
    
    headers = {
        "Authorization": f"Bearer {token}"
    }
    
    max_attempts = 120  # 60 seconds (120 * 0.5s)
    attempts = 0
    
    if isinstance(temp_quote_id, str) and temp_quote_id.startswith('-'):
        try:
            temp_quote_id = int(temp_quote_id)
        except ValueError:
            print(f"[STATUS] Warning: Could not convert temp_quote_id {temp_quote_id} to int")
    
    print(f"[STATUS] Looking for temp_quote_id: {temp_quote_id} (type: {type(temp_quote_id)})")
    
    while attempts < max_attempts:
        try:
            response = requests.get(STATUS_URL, headers=headers)
            if response.status_code != 200:
                print(f"[STATUS] Error: Response status {response.status_code}")
                print(f"[STATUS] Response text: {response.text}")
                attempts += 1
                time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
                continue
                
            data = response.json()
            print(f"[STATUS] Poll attempt {attempts+1}/{max_attempts}")
            
            if not data:
                print("[STATUS] Empty response, waiting for next update...")
                attempts += 1
                time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
                continue
                            
            quotes = []
            if isinstance(data, list):
                quotes = data
            elif isinstance(data, dict) and "quotes" in data:
                quotes = data["quotes"]
            
            if not quotes:
                print("[STATUS] No quotes found in response")
                attempts += 1
                time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
                continue
                
            print(f"[STATUS] Found {len(quotes)} quotes to check")
            
            # Look for any quote with a positive quote_id (confirmed)
            for quote in quotes:
                quote_id = quote.get("quote_id")
            
                if isinstance(quote_id, str) and quote_id.isdigit():
                    quote_id = int(quote_id)
                
                if isinstance(quote_id, int) and quote_id > 0:
                    print(f"[STATUS] ✓ CONFIRMED: Quote has permanent ID: {quote_id}")
                    return quote_id
            
            attempts += 1
            time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
            
        except Exception as e:
            print(f"[ERROR] Error polling quote status: {e}")
            traceback.print_exc()
            attempts += 1
            time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
    
    print("[STATUS] ⚠ Timed out waiting for permanent quote ID")
    return None
```

This approach looks for any quote with a positive ID, which indicates that the trade has been confirmed.&#x20;


# Closing a Position

When the exit condition is met (`EXIT_PRICE`), we need to close the position. More information on this can be found [here](/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading/closing-a-quote-instant-close).

#### Preparing the Close Request

Similar to opening a position, we need to fetch the price from Muon and apply slippage. For closing a long position, we apply negative slippage (0.99x the price):

```python
# Fetch Muon price and apply -1% slippage (for long positions)
muon_price = fetch_muon_price()
close_price = str(muon_price * Decimal("0.99"))  # 1% slippage for selling
```

#### Sending the Close Request

```python
payload = {
    "quote_id": quote_id,
    "quantity_to_close": CONFIG["QUANTITY"],
    "close_price": close_price
}

response = requests.post(f"{HEDGER_URL}/instant_close", json=payload, headers=headers)
```

#### Verifying the Close

After sending the close request, we check the response status to ensure the position was closed successfully:

```python
if response.status_code == 200:
    print("[CLOSE] Position closed successfully")
    return True
else:
    print(f"[CLOSE] Error: {response.status_code}, {response.text}")
    return False
```


# Full Example Script

The `main()` function ties everything together:

1. Login and get an access token
2. Monitor prices continuously (in this example we're trading XRP due to its low minimum acceptable quote value)
3. Open a position when the price drops below the entry threshold
4. Monitor the position status to get a permanent quote ID
5. Close the position when the price rises above the exit threshold
6. Handle errors and retry as needed

This trading bot implements a simple "buy low, sell high" strategy, but entry/exit conditions can be modified for more advanced strategies.

To help guide users in setting up the necessary environment variables, the [repository](https://github.com/academy17/symm_example_codes/tree/main) includes a `.env.example` file. This file provides a template outlining the format and naming of the required variables. Users should copy this file, rename it to `.env`, and fill in the appropriate values specific to their setup. This ensures that all required configurations are consistently specified for the trading bot to function correctly.

**Full Script**

```python
import os
import time
import requests
import json
from eth_account import Account
from eth_account.messages import encode_defunct
from dotenv import load_dotenv
from datetime import datetime, timedelta, timezone
from decimal import Decimal
import traceback

# Configuration
CONFIG = {
    "SYMBOL": "XRPUSDT",   # Trading pair on Binance
    "ENTRY_PRICE": "3.05",  # Price to enter the position (adjust as needed)
    "EXIT_PRICE": "3.1",   # Price to exit the position (adjust as needed)
    "QUANTITY": "6",      # Amount of XRP to trade
    "POSITION_TYPE": 0,     # 0 for long, 1 for short
    "SYMBOL_ID": 340,       # XRP symbol ID in Symmio
    "LEVERAGE": "1",        # Leverage value
    "MAX_FUNDING_RATE": "200",
    "DEADLINE_OFFSET": 3600,  # 1 hour
    "POLL_INTERVAL": 5,       # Seconds between price checks
    "STATUS_POLL_INTERVAL": 0.5  # Seconds between status checks
}

# Load environment variables
load_dotenv()

# Environment variables
PRIVATE_KEY = os.getenv("PRIVATE_KEY")
ACTIVE_ACCOUNT = os.getenv("SUB_ACCOUNT_ADDRESS")
HEDGER_URL = os.getenv("HEDGER_URL")
CHAIN_ID = int(os.getenv("CHAIN_ID", 42161))
MUON_BASE_URL = os.getenv("MUON_BASE_URL")
DOMAIN = "localhost"
ORIGIN = "http://localhost:3000"
LOGIN_URI = f"{HEDGER_URL}/login"
DIAMOND_ADDRESS = os.getenv("DIAMOND_ADDRESS")

# URLs
BINANCE_API_URL = "https://api.binance.com/api/v3/ticker/price"
LOCKED_PARAMS_URL = f"{HEDGER_URL}/get_locked_params/XRPUSDT?leverage={CONFIG['LEVERAGE']}"
MUON_URL = f"{MUON_BASE_URL}?app=symmio&method=uPnl_A_withSymbolPrice&params[partyA]={ACTIVE_ACCOUNT}&params[chainId]={CHAIN_ID}&params[symmio]={DIAMOND_ADDRESS}&params[symbolId]={CONFIG['SYMBOL_ID']}"
STATUS_URL = f"{HEDGER_URL}/instant_open/{ACTIVE_ACCOUNT}"

# Initialize wallet
wallet = Account.from_key(PRIVATE_KEY)

# Timestamp formats
ISSUED_AT = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
EXPIRATION_DATE = (datetime.now(timezone.utc) + timedelta(hours=2, minutes=30)).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"

def get_nonce(address: str) -> str:
    """Fetch the nonce for the active account from the server."""
    url = f"{HEDGER_URL}/nonce/{address}"
    response = requests.get(url)
    response.raise_for_status()
    return response.json()["nonce"]

def build_siwe_message(domain, address, statement, uri, version, chain_id, nonce, issued_at, expiration_time):
    """Build a SIWE message string following the EIP-4361 format."""
    return f"""{domain} wants you to sign in with your Ethereum account:
{address}

{statement}

URI: {uri}
Version: {version}
Chain ID: {chain_id}
Nonce: {nonce}
Issued At: {issued_at}
Expiration Time: {expiration_time}"""

def login():
    """Perform SIWE login and return the access token."""
    try:
        print(f"[LOGIN] Wallet Address: {wallet.address}")
        print(f"[LOGIN] Active Account: {ACTIVE_ACCOUNT}")

        # Fetch the nonce for the active account from the server
        nonce = get_nonce(ACTIVE_ACCOUNT)
        print(f"[LOGIN] Got nonce: {nonce}")

        # Create the SIWE message manually
        message_string = build_siwe_message(
            domain=DOMAIN,
            address=wallet.address,
            statement=f"msg: {ACTIVE_ACCOUNT}",
            uri=LOGIN_URI,
            version="1",
            chain_id=CHAIN_ID,
            nonce=nonce,
            issued_at=ISSUED_AT,
            expiration_time=EXPIRATION_DATE
        )

        message = encode_defunct(text=message_string)
        
        signed_message = wallet.sign_message(message)
        signature = "0x" + signed_message.signature.hex()

        body = {
            "account_address": ACTIVE_ACCOUNT,
            "expiration_time": EXPIRATION_DATE,
            "issued_at": ISSUED_AT,
            "signature": signature,
            "nonce": nonce
        }

        headers = {
            "Content-Type": "application/json",
            "Origin": ORIGIN,
            "Referer": ORIGIN,
        }

        print("[LOGIN] Sending login request...")

        response = requests.post(
            LOGIN_URI,
            json=body,
            headers=headers
        )
        
        response.raise_for_status()
        token = response.json().get("access_token")
        print(f"[LOGIN] Successfully obtained access token")
        return token
    except Exception as e:
        print(f"[ERROR] Login failed: {e}")
        traceback.print_exc()
        return None

def get_binance_price(symbol):
    """Get the current price of a symbol from Binance."""
    try:
        params = {"symbol": symbol}
        response = requests.get(BINANCE_API_URL, params=params)
        response.raise_for_status()
        data = response.json()
        price = Decimal(data["price"])
        print(f"[PRICE] Current {symbol} price: {price}")
        return price
    except Exception as e:
        print(f"[ERROR] Failed to get Binance price: {e}")
        return None

def fetch_muon_price():
    """Fetch the current price from Muon oracle."""
    try:
        response = requests.get(MUON_URL)
        response.raise_for_status()
        data = response.json()
        fetched_price_wei = data["result"]["data"]["result"]["price"]
        if not fetched_price_wei:
            raise ValueError("Muon price not found in response.")
        return Decimal(fetched_price_wei) / Decimal('1e18')
    except Exception as e:
        print(f"[ERROR] Failed to fetch Muon price: {e}")
        return None

def fetch_locked_params():
    """Fetch locked parameters for the trade."""
    try:
        response = requests.get(LOCKED_PARAMS_URL)
        response.raise_for_status()
        data = response.json()
        if data.get("message") == "Success":
            return data
        else:
            raise ValueError("Failed to fetch locked parameters")
    except Exception as e:
        print(f"[ERROR] Failed to fetch locked parameters: {e}")
        return None

def calculate_normalized_locked_value(notional, locked_param, leverage, apply_leverage=True):
    """Compute normalized locked value for a given parameter."""
    notional = Decimal(str(notional))
    locked_param = Decimal(str(locked_param))
    leverage = Decimal(str(leverage))
    
    if apply_leverage:
        return str(notional * locked_param / (Decimal('100') * leverage))
    else:
        return str(notional * locked_param / Decimal('100'))

def open_instant_trade(token):
    """Execute an instant open trade using the access token. Returns temp_quote_id."""
    try:
        print("[TRADE] Starting instant open process...")
        
        fetched_price = fetch_muon_price()
        if not fetched_price:
            return None
        print(f"[TRADE] Fetched price: {fetched_price}")
        
        # Because in this example we are going LONG, we are increasing the price by 1% before we send the order
        adjusted_price = fetched_price * Decimal('1.01')
        print(f"[TRADE] Adjusted price (+1%): {adjusted_price}")
        
        locked_params = fetch_locked_params()
        if not locked_params:
            return None
        
        notional = adjusted_price * Decimal(CONFIG["QUANTITY"])
        print(f"[TRADE] Notional: {notional}")
        
        leverage = Decimal(locked_params["leverage"])
        normalized_cva = calculate_normalized_locked_value(notional, locked_params["cva"], leverage, True)
        normalized_lf = calculate_normalized_locked_value(notional, locked_params["lf"], leverage, True)
        normalized_party_amm = calculate_normalized_locked_value(notional, locked_params["partyAmm"], leverage, True)
        
        deadline = int(time.time()) + CONFIG["DEADLINE_OFFSET"]
        trade_params = {
            "symbolId": CONFIG["SYMBOL_ID"],
            "positionType": CONFIG["POSITION_TYPE"],
            "orderType": 1,  # 1 for market order
            "price": str(adjusted_price),
            "quantity": CONFIG["QUANTITY"],
            "cva": normalized_cva,
            "lf": normalized_lf,
            "partyAmm": normalized_party_amm,
            "partyBmm": '0',
            "maxFundingRate": CONFIG["MAX_FUNDING_RATE"],
            "deadline": deadline
        }
        
        print(f"[TRADE] Trade Payload: {trade_params}")
        
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {token}"
        }
        
        response = requests.post(f"{HEDGER_URL}/instant_open", json=trade_params, headers=headers)
        
        print(f"[TRADE] Response status: {response.status_code}")
        print(f"[TRADE] Response: {response.text}")
        
        response.raise_for_status()
        result = response.json()
        
        temp_quote_id = result.get("quote_id")
        if temp_quote_id:
            print(f"[TRADE] Received temporary quote ID: {temp_quote_id}")
            return temp_quote_id
        else:
            print("[ERROR] No temporary quote ID in response")
            return None
            
    except Exception as e:
        print(f"[ERROR] Failed to open instant trade: {e}")
        traceback.print_exc()
        return None

def poll_quote_status(token, temp_quote_id):
    """Poll for the status of a quote until it gets a permanent ID.""" #In order to track the status of the quote, we will poll the /instant_open/{address} endpoint
    print(f"[STATUS] Starting to poll for quote status of temp ID: {temp_quote_id}")
    
    headers = {
        "Authorization": f"Bearer {token}"
    }
    
    max_attempts = 120  # 60 seconds (120 * 0.5s)
    attempts = 0
    
    if isinstance(temp_quote_id, str) and temp_quote_id.startswith('-'):
        try:
            temp_quote_id = int(temp_quote_id)
        except ValueError:
            print(f"[STATUS] Warning: Could not convert temp_quote_id {temp_quote_id} to int")
    
    print(f"[STATUS] Looking for temp_quote_id: {temp_quote_id} (type: {type(temp_quote_id)})")
    
    while attempts < max_attempts:
        try:
            response = requests.get(STATUS_URL, headers=headers)
            if response.status_code != 200:
                print(f"[STATUS] Error: Response status {response.status_code}")
                print(f"[STATUS] Response text: {response.text}")
                attempts += 1
                time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
                continue
                
            data = response.json()
            print(f"[STATUS] Poll attempt {attempts+1}/{max_attempts}")
            
            if not data:
                print("[STATUS] Empty response, waiting for next update...")
                attempts += 1
                time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
                continue
                            
            quotes = []
            if isinstance(data, list):
                quotes = data
            elif isinstance(data, dict) and "quotes" in data:
                quotes = data["quotes"]
            
            if not quotes:
                print("[STATUS] No quotes found in response")
                attempts += 1
                time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
                continue
                
            print(f"[STATUS] Found {len(quotes)} quotes to check")
            
            # Look for any quote with a positive quote_id (confirmed)
            for quote in quotes:
                quote_id = quote.get("quote_id")
            
                if isinstance(quote_id, str) and quote_id.isdigit():
                    quote_id = int(quote_id)
                
                if isinstance(quote_id, int) and quote_id > 0:
                    print(f"[STATUS] ✓ CONFIRMED: Quote has permanent ID: {quote_id}")
                    return quote_id
            
            attempts += 1
            time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
            
        except Exception as e:
            print(f"[ERROR] Error polling quote status: {e}")
            traceback.print_exc()
            attempts += 1
            time.sleep(CONFIG["STATUS_POLL_INTERVAL"])
    
    print("[STATUS] ⚠ Timed out waiting for permanent quote ID")
    return None

def close_instant_position(token, quote_id, current_price):
    """Close an open position."""
    try:
        print(f"[CLOSE] Preparing to close position with quote ID: {quote_id}")
        
        # Fetch Muon price and apply -1% slippage (for long positions)
        muon_price = fetch_muon_price()
        if not muon_price:
            return False
            
        close_price = str(muon_price * Decimal("0.99"))  # 1% slippage for selling
        print(f"[CLOSE] Close Price: {close_price}")
        
        payload = {
            "quote_id": quote_id,
            "quantity_to_close": CONFIG["QUANTITY"],
            "close_price": close_price
        }
        
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {token}"
        }
        
        print("[CLOSE] Sending instant close request...")
        response = requests.post(f"{HEDGER_URL}/instant_close", json=payload, headers=headers)
        
        print(f"[CLOSE] Response status: {response.status_code}")
        print(f"[CLOSE] Response: {response.text}")
        
        response.raise_for_status()
        print("[CLOSE] Position closed successfully")
        return True
        
    except Exception as e:
        print(f"[ERROR] Failed to close position: {e}")
        traceback.print_exc()
        return False

def main():
    """Main trading bot logic."""
    try:
        print("=============================================")
        print("XRP Trading Bot Starting")
        print("=============================================")
        print(f"Entry Price: {CONFIG['ENTRY_PRICE']}")
        print(f"Exit Price: {CONFIG['EXIT_PRICE']}")
        print(f"Quantity: {CONFIG['QUANTITY']}")
        print("=============================================")
        
        # Login and get access token
        access_token = login()
        if not access_token:
            print("[ERROR] Failed to login. Exiting.")
            return
        
        # Entry price as Decimal for comparison
        entry_price = Decimal(CONFIG["ENTRY_PRICE"])
        exit_price = Decimal(CONFIG["EXIT_PRICE"])
        
        # Trading state
        in_position = False
        confirmed_quote_id = None
        temp_quote_id = None
        
        print("[BOT] Starting price monitoring loop...")
        
        # Main trading loop
        while True:
            try:
                # Get current price from Binance
                current_price = get_binance_price(CONFIG["SYMBOL"])
                
                if not current_price:
                    print("[WARNING] Failed to get price, retrying...")
                    time.sleep(CONFIG["POLL_INTERVAL"])
                    continue
                
                # If not in a position and price is below entry price, enter position
                if not in_position and current_price <= entry_price:
                    print(f"[SIGNAL] Entry signal triggered at price {current_price}")
                    
                    # Execute the trade
                    temp_quote_id = open_instant_trade(access_token)
                    
                    if temp_quote_id:
                        print(f"[BOT] Trade executed with temporary quote ID: {temp_quote_id}")
                        
                        # Poll for the permanent quote ID
                        confirmed_quote_id = poll_quote_status(access_token, temp_quote_id)
                        
                        if confirmed_quote_id:
                            print(f"[BOT] Quote confirmed with ID: {confirmed_quote_id}")
                            in_position = True
                        else:
                            print("[WARNING] Failed to get confirmed quote ID. Will retry on next cycle.")
                    else:
                        print("[ERROR] Failed to execute trade")
                
                elif in_position and current_price >= exit_price:
                    print(f"[SIGNAL] Exit signal triggered at price {current_price}")
                    
                    success = close_instant_position(access_token, confirmed_quote_id, current_price)
                    
                    if success:
                        print("[BOT] Position closed successfully")
                        in_position = False
                        confirmed_quote_id = None
                        temp_quote_id = None
                        print("[BOT] Waiting for next entry opportunity...")
                    else:
                        print("[ERROR] Failed to close position. Will retry.")
                
                time.sleep(CONFIG["POLL_INTERVAL"])
                
            except Exception as e:
                print(f"[ERROR] Error in main trading loop: {e}")
                traceback.print_exc()
                time.sleep(CONFIG["POLL_INTERVAL"])
        
    except KeyboardInterrupt:
        print("\n[BOT] Trading bot stopped by user")
    except Exception as e:
        print(f"[ERROR] Unhandled exception: {e}")
        traceback.print_exc()

if __name__ == "__main__":
    main()
```


# Symmio Whitepaper

{% hint style="info" %}

#### Preamble

The Symmio system is continually evolving, and with it, our understanding and application of its potential. As we implement a fully trustless clearing system, we recognize the challenges that drive further improvements and expand our knowledge. We are eager to share a glimpse into something we've been fervently working on.

It is important to note that both our whitepaper and our protocol are works in progress. As Symmio evolves, our whitepaper will be continually refined and updated. While it already provides an overview of our long-term vision, practical implementation, and future concepts, it represents just the tip of the iceberg. Additional publications, including SYMM v2.0, Netting, and ACOs, are scheduled to follow in 2024 and 2025 as our research progresses.

Your interest fuels our commitment to excellence. As you read this version, please know that we are equally excited to present further research to you very soon.
{% endhint %}

## Full Whitepaper is available on github:

<https://github.com/SYMM-IO/docs/blob/main/Whitepaper/SYMMIO_paper_0_8.pdf><br>


# Contract Architecture Overview

Symmio is an intent-based bilateral derivatives protocol. Retail users (PartyA) trade against professional market makers (PartyB), every critical operation is gated by cryptographically verified off-chain signatures, and the whole system is built on the **Diamond Standard (EIP-2535)**, so individual facets can be added, replaced, or removed without changing the diamond's address or disturbing storage.

What was once a single diamond is now a small network of diamonds with clearly separated responsibilities, plus an externalized signature verifier and a privileged operator (the [ClearingHouse](/contract-documentation/symmio-perps-v0.8.5/diamond-core-facets/clearing-house-facet)) for the cases the decentralized flow can't handle. This page is a tour of what lives where, how the account hierarchy is structured, and how a quote moves through the system end to end.

### The Diamond contracts

Three diamonds make up the live protocol surface, plus the supporting contracts they coordinate with.

* **Symmio Core (Perps Diamond).** The main diamond for facilitating trades between the trader (PartyA) and the solver (PartyB). It handles sending quotes, locking, opening, closing, settling, and liquidating.
* **Account Layer.** A standalone diamond proxy that replaces the per-frontend MultiAccount contracts. It owns every user's `SubAccounts` and deterministic `VirtualAccounts`, registers affiliates, distributes affiliate fees to stakeholders, supports express deposits, and exposes hooks for custom on-chain logic (NFT mints, loyalty points, cashback).
* **Instant Layer.** A diamond that mediates user-signed operations using EIP-712 `SignedOperation` structs, flex fields, templates, and a delegation system. Operators with `OPERATOR_ROLE` submit batches; the Instant Layer routes them through the Account Layer's `_call` so Symmio Core sees the right trading account.

Around them:

* **`MuonSignatureVerifier`.** External contract, addressed from `GlobalAppStorage.signatureVerifier`. Manages multiple TSS public keys and gateway signers with per-`MuonFunction` authorization. See the Muon Architecture page.
* **ClearingHouse.** Privileged operator (`CLEARING_HOUSE_ROLE`) that handles cross-mode PartyB insolvency and stuck PartyA liquidations. It's trusted to provide its own UPNL and prices; the contract enforces lifecycle invariants.
* **Express Provider / Virtual Provider contracts.** Per-chain liquidity contracts that participate in the new Withdraw System.
* **PledgeFacet vault.** Holds PartyB pledge collateral (slashable), with a two-step request-approval withdrawal flow.
* **Liquidation Insurance Vault.** Receives the excess of liquidator profit above `maxLiquidationProfitPerPosition`.

### Storage

* `DiamondStorage`: contract ownership.
* `GlobalAppStorage`: collateral, fee collector, role mappings, pause states, emergency mode, balance limits, and the like.
* `AccountStorage`: user balances, allocated balances, locked values (CVA, LF, MM), pending locks, nonces, suspension status, liquidation details.
* `QuoteStorage`: quote structs, pending quote arrays, position counts, the quote ID counter.
* `MAStorage`: cooldown durations, liquidator share, PartyB registration status, liquidation timestamps, withdraw cooldown period.
* `SymbolStorage`: trading symbol configurations including leverage limits, fees, funding rate parameters, and the new **`symbolType`** mapping.
* `MuonStorage`: kept for legacy fields, though the active key set lives in `MuonSignatureVerifier`.

### Account hierarchy: SubAccounts and Virtual Accounts

```
User (EOA)
 └─ SubAccount   (bound to affiliate + Symmio core + isolation type)
     └─ Virtual Account 1   (acts as partyA on Symmio Core)
     └─ Virtual Account 2
     └─ …
```

`SubAccounts` are the user's top-level organizational unit: each one is bound to a specific frontend (affiliate), a Symmio core instance, and an isolation type chosen at creation. A user can keep one for conservative trades and another for an aggressive book.

`Virtual Accounts (VAs)` sit beneath SubAccounts and provide position isolation. Each VA is an independent trading address on Symmio Core with its own balance, allocated margin, and positions. A liquidation in one VA can't touch another. When a VA's last position closes, its funds are swept back to the parent SubAccount automatically and the address is recycled.

Funds flow:

```
User ─deposit──►  SubAccount ─addMargin─►  Virtual Account
                  SubAccount ◄─sweep────  Virtual Account (on last position close)
```

`addMarginToNextVA` lets a frontend (or a delegated service) pre-fund the *next* VA address before it exists, which is how trigger-market orders pre-fund the VA that the subsequent `sendQuoteWithAffiliateAndData` will create.

#### Isolation types

Chosen at SubAccount creation, with a "Single VA mode" toggle for the market-bucketed types:

* `POSITION`: one VA per quote. Maximum isolation; each position has its own margin pool.
* `MARKET`: one VA per market. All BTC trades share a VA, all ETH trades share another.
* `MARKET_DIRECTION`: one VA per (market, direction). BTC longs share a VA; BTC shorts share a different one.
* `CUSTOM`: caller-supplied VA addresses, useful for advanced strategies.

### Roles and admin model

Role-based access control is still anchored on `DEFAULT_ADMIN_ROLE`, but v0.8.5 adds a delegation layer so an admin team doesn't have to bottleneck every grant and revoke through one address.

```solidity
// GlobalAppStorage
mapping(bytes32 => mapping(address => bool)) roleAdmins;
```

A `roleAdmin` for a specific role can grant and revoke that role without touching any other. Combined with the existing `hasRole` mapping, the system has a clear hierarchy: **Owner → Default Admin → Role Admin → Role Holder**.

The active roles:

| Role                            | Permissions                                                                                                                              |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT_ADMIN_ROLE`            | Grant/revoke all roles, set collateral, emergency mode, unsuspend addresses, set fee collector and balance limits, designate role admins |
| `PAUSER_ROLE`                   | Pause global operations, liquidation, accounting, PartyA/PartyB actions                                                                  |
| `UNPAUSER_ROLE`                 | Unpause any paused function                                                                                                              |
| `PARTY_B_MANAGER_ROLE`          | Register and deregister PartyB addresses; toggle ADL (Auto-Deleveraging) eligibility                                                     |
| `SYMBOL_MANAGER_ROLE`           | Add symbols, configure leverage, trading fees, funding parameters, **symbol types**                                                      |
| `SETTER_ROLE`                   | Configure cooldowns, liquidator share, force close parameters, withdraw cooldown                                                         |
| `MUON_SETTER_ROLE`              | Set the `MuonSignatureVerifier` address; legacy Muon config                                                                              |
| `SUSPENDER_ROLE`                | Suspend addresses and bridge transactions                                                                                                |
| `LIQUIDATOR_ROLE`               | Decentralized liquidator path (still active for isolated PartyB and standard PartyA)                                                     |
| `CLEARING_HOUSE_ROLE`           | ClearingHouse operator for cross-mode PartyB insolvency and stuck PartyA liquidations                                                    |
| `OPERATOR_ROLE` (Instant Layer) | Submit `executeBatch` / `executeTemplate`                                                                                                |

Diamond ownership transfer is **two-step**: the current owner sets a pending owner, and the pending owner must call `acceptOwnership()` to complete the transfer. Mistakes are recoverable until the second transaction lands.

### Quote lifecycle

#### Stage 1: Quote creation (PENDING)

PartyA initiates via one of:

* `sendQuote(partyBsWhiteList, symbolId, positionType, orderType, price, quantity, cva, lf, partyAmm, partyBmm, maxFundingRate, deadline, affiliate, upnlAndPriceSig)`: the original signature.
* `sendQuoteWithAffiliate(...)`: kept for compatibility.
* `sendQuoteWithAffiliateAndData(..., bytes data)`: same as `sendQuoteWithAffiliate()`, but with arbitrary `bytes` attached to the quote.

Margin components:

* **CVA** (Credit Valuation Adjustment): penalty paid by the liquidated party to the counterparty.
* **MM** (Maintenance Margin): `partyAmm` and `partyBmm`, considered in liquidation calculations.
* **LF** (Liquidation Fee): reward for the liquidator.

Funds lock in `pendingLockedBalances[partyA]`. The `SingleUpnlAndPriceSig` Muon signature is required here, unless PartyA is bound to a single PartyB.

#### Stage 2: Quote locking (LOCKED)

PartyB calls `lockQuote(quoteId, upnlSig, increaseNonce)`. `LibMuon.verifyPartyBUpnl` checks PartyB's UPNL, `LibPartyB.checkPartyBValidationToLockQuote` enforces validation rules, and `partyBPendingLockedBalances` updates. PartyB can release via `unlockQuote()`, reverting the quote to `PENDING` or `EXPIRED`.

#### Stage 3: Position opening (OPENED)

`openPosition(quoteId, filledAmount, openedPrice, upnlSig)` supports partial fills: filling 20 of 100 units creates a new quote for the remainder. Trading fees are applied, solvency is checked via `LibSolvency.isSolventAfterOpenPosition`, and the call requires a `PairUpnlAndPriceSig` attestation.

In the current version, **`PartyBGroupActionsFacet`** adds batch variants:

```solidity
function openPositions(
  uint256[] memory quoteIds,
  uint256[] memory filledAmounts,
  uint256[] memory openedPrices,
  PairUpnlAndPricesSig memory batchSig
) external;
```

One Muon signature, many positions. The same idea applies to `fillCloseRequests`. This keeps solver response time low under heavy load.

#### Stage 4: Position closing

PartyA requests closure via `requestToClosePosition(quoteId, closePrice, quantityToClose, orderType, deadline)`, moving the quote to `CLOSE_PENDING`. PartyB executes via `fillCloseRequest(quoteId, filledAmount, closedPrice, upnlSig)`. **LIMIT** orders allow multiple partial fills; **MARKET** orders require full execution. Both check solvency via `LibSolvency.isSolventAfterClosePosition`.

* **`fillCloseRequestToLiquidation`:** when a normal close would push PartyA into insolvency, this calculates the maximum partial close that brings them exactly to the liquidation threshold.
* **ADL Close:** PartyBs (enabled with `PARTY_B_MANAGER_ROLE`) can unilaterally close to reduce risk exposure, with no solvency check on themselves. Backed by **pledge collateral** that can be slashed if ADL is abused.

#### Cancellation

The cancellation transitions are unchanged: `requestToCancelQuote` (`PENDING` → `CANCELED`, or `LOCKED` → `CANCEL_PENDING`), `acceptCancelRequest` (PartyB accepts), `requestToCancelCloseRequest` (`CLOSE_PENDING` → `CANCEL_CLOSE_PENDING`), `acceptCancelCloseRequest` (returns to `OPENED`).

### Liquidation

#### PartyA liquidation (4-step)

Trigger condition: total UPNL `< -(allocated_balance - mm - pending_locked_values)`.

1. `liquidatePartyA(partyA, upnlSig)`: records the liquidation timestamp and `LiquidationDetail`.
2. `setSymbolsPrice(partyA, priceSig)`: establishes liquidation prices for all position symbols.
3. `liquidatePendingPositionsPartyA(partyA)`: clears unlocked quotes.
4. `liquidatePositionsPartyA(partyA, quoteIds[])`: liquidates the specified open positions.

The `LiquidationDetail` captures `liquidationId`, `liquidationType` (NORMAL / LATE / OVERDUE), `upnl`, `totalUnrealizedLoss`, `deficit`, `liquidationFee`, `timestamp`, `involvedPartyBCounts`, `partyAAccumulatedUpnl`, and a `disputed` flag.

Per-position liquidator profit is capped at `maxLiquidationProfitPerPosition`; the excess flows to a configured Liquidation Insurance Vault that the protocol can reuse to reimburse PartyBs hit by late-liquidation losses.

In `LATE` and `OVERDUE` PartyA liquidations, pending fee reimbursements are routed to a ClearingHouse-controlled escrow rather than back to PartyA. Deferred excess balance (funds added after the historical insolvency point) still goes back to PartyA.

When the on-chain state is too inconsistent for the dispute resolver to fix, the ClearingHouse can take over the liquidation entirely, reset the state, and finish it with its own price feed.

#### PartyB liquidation

Isolated-mode PartyB uses a 2-step flow:

1. `liquidatePartyB(partyB, partyA, upnlSig)`.
2. `liquidatePositionsPartyB(partyB, partyA, priceSig)`.

Cross-mode PartyB doesn't use that flow. Because cross mode pools all collateral into a single `address(0)` bucket shared across PartyAs, a single insolvency event affects everyone with positions against that PartyB. The **ClearingHouse** runs the unwind:

```
liquidateCrossPartyB(partyB, liquidationId, upnl, timestamp)
   ▶ deallocateForClearingHouse(partyB, parties, allocationKeys, amounts)
   ▶ liquidatePendingPositionsForClearingHouse(partyB, partyAs)
   ▶ liquidatePositionsForClearingHouse(partyB, quoteIds, prices)
   ▶ distributeForClearingHouse(partyB, receivers, …)
   ▶ settleCrossPartyBLiquidation(partyB)
```

Settlement requires all positions closed and the deallocated pool fully distributed. The ClearingHouse provides UPNL and prices directly; the contract trusts the operator and enforces lifecycle invariants.

#### Soft liquidation

For PartyBs, the protocol supports a tiered warning system before hard liquidation. As balance approaches the danger zone, the [ClearingHouse](/contract-documentation/symmio-perps-v0.8.5/diamond-core-facets/clearing-house-facet) can issue on-chain notices and small penalties at configurable thresholds, giving the solver time to top up funds instead of disrupting every user on every frontend with an immediate close-all.

#### Cross-mode settlement during PartyA liquidation

In cross mode, a PartyB's solvency includes unrealized profit from all its other counterparties, so its raw allocated balance can be much lower than the value it actually controls. `settlePartyBUpnlForLiquidation` (on `SettlementFacet`) lets a liquidator realize a cross-mode PartyB's UPNL from positions with other (non-liquidated) PartyAs before the PartyA liquidation settlement draws funds. Cross-mode PartyBs with insufficient raw balance revert until this step runs; isolated PartyBs keep the prior capped-payment behavior.

### Funding rate mechanics

In the previous version, funding was charged per quote, so PartyB had to process every individual position at each epoch, `O(quotes)`. The current version uses an **accumulated weighted average** at the symbol level.

```solidity
struct FundingFee {
    int256 cumulativeRate;   // running weighted average, pre-multiplied by price
    uint256 lastUpdate;
    uint256 epochDuration;
    // … plus dynamic-epoch scaling fields
}
```

When a solver sets a new rate, it blends into the running average. Rates are pre-multiplied by market price at storage time, so the calculation doesn't need a price lookup. Dynamic epoch-duration changes are supported with automatic rate scaling.

**Aggregated positions** track running totals of size and notional per `(party, symbol, positionType)`, so UPNL calculations iterate over active *symbols* instead of quotes. For a solver with 5,000 positions across 50 symbols, solvency goes from `O(5000)` to `O(50)`. Aggregate funding debt is tracked the same way.

`FundingRateFacet.chargeFundingRate(partyA, quoteIds[], rates[], upnlSig)` and `chargeAccumulatedFundingFee(...)` apply periodic adjustments, with timing controlled by symbol-specific `fundingRateEpochDuration` and `fundingRateWindowTime`.

### Withdraw System

Withdrawals draw only from an account's **free (deallocated) balance**, so the flow has a mandatory first step:

```
allocated margin (in a VA)
  → deallocate  (removeMargin: deallocate + internalTransferToBalance → SubAccount balance)
  → free balance
  → initiateWithdraw  → cooldown → finalizeWithdrawRequest
```

`initiateWithdraw` requires `balances[user] >= amount` and reverts with `"WithdrawFacet : Insufficient balance"` otherwise — funds still allocated to a position can't be withdrawn. Use `removeMargin(virtualAccount, amount, upnlSig)` to deallocate and sweep margin back to the parent SubAccount before initiating.

A withdrawal request is composed of one or more receiver parts, each routed through one of four withdrawal paths.

Cooldown is anchored to the user's last deallocation, not to the withdrawal initiation — because deallocation is what produces the withdrawable balance in the first place:

```
cooldownEndTime = max(deallocateTimestamp + withdrawCooldownPeriod, block.timestamp)
```

Later deallocations don't reset the cooldown of an already-initiated withdrawal. `getWithdrawableTime(user)` returns the earliest time a withdrawal initiated now could finalize. Express providers front the funds for a fee. Virtual providers deliver cross-chain; they implement lifecycle callbacks (`onWithdrawRequest`, `acceptWithdrawRequest`, `onWithdrawComplete`) that the protocol calls during the lifecycle. Cancellation has its own state machine, with a blackout window for in-flight provider acceptances and an admin force-cancel.

| Type             | Express | Virtual | Description                                |
| ---------------- | ------- | ------- | ------------------------------------------ |
| Normal (Classic) | ✗       | ✗       | Standard 12h cooldown, same-chain delivery |
| Pure Express     | ✓       | ✗       | Instant same-chain withdrawal              |
| Pure Virtual     | ✗       | ✓       | Standard cooldown, cross-chain delivery    |
| Virtual Express  | ✓       | ✓       | Instant cross-chain withdrawal             |

**Cooldown is anchored to the user's last deallocation**, not to the withdrawal initiation:

```
cooldownEndTime = max(deallocateTimestamp + withdrawCooldownPeriod, block.timestamp)
```

Later deallocations don't reset the cooldown of an already-initiated withdrawal. `getWithdrawableTime(user)` returns the earliest time a withdrawal initiated *now* could finalize. **Express providers** front the funds for a fee. **Virtual providers** deliver cross-chain; they implement lifecycle callbacks (`onWithdrawRequest`, `acceptWithdrawRequest`, `onWithdrawComplete`) that the protocol calls during the lifecycle. **Cancellation** has its own state machine, with a blackout window for in-flight provider acceptances and an admin force-cancel.


# Muon Architecture

Muon is the off-chain computation and signing layer that feeds Symmio with verified data. Every state-changing operation that depends on an external market passes through Muon. The off-chain TSS network calculates the data, signs it, and a gateway node relays it; the on-chain protocol verifies both the threshold signature and the gateway's ECDSA signature before accepting the call.

In our current version the Muon layer keeps the same dual-signature model but pulls its trust set out of the diamond. Instead of one TSS public key and one gateway address baked into MuonStorage, the protocol now consults a standalone MuonSignatureVerifier contract that supports multiple keys, multiple gateway signers, key rotation without downtime, and per-category authorization.&#x20;

### Why Symmio needs an oracle at all

Symmio is a bilateral derivatives protocol. At any moment, the contract needs to know:

* Current prices for the trading symbols involved.
* Unrealized PnL for PartyA and PartyB.
* Whether each party is solvent enough to execute the requested action.

Calculating UPNL on-chain would require iterating every open position and fetching every price which would be expensive to do on most networks. Muon performs the calculation off-chain and ships back a signed attestation that the contracts verify cheaply. The protocol's correctness rests on two things: that the math the network performs is correct, and that the signatures can't be forged or replayed. The dual-layer verification model exists to make the second condition robust to a partial compromise.

### Dual-layer signature verification

Symmio doesn't trust a single signer. Every Muon signature passes through two independent layers before the contract accepts it.

#### Layer 1: TSS (Threshold Signature Scheme)

Muon operates a distributed network of nodes. When a signature request comes in, multiple nodes independently fetch price data, calculate the requested values (UPNL, solvency, etc.), and contribute to a threshold signature. The result is a Schnorr signature that proves a threshold of nodes agreed on the data. The contract verifies this Schnorr signature against the registered TSS public keys.

In the current version (0.8.5), the verification iterates over all registered public keys in MuonSignatureVerifier. If any one of them validates the Schnorr signature, the TSS check passes. This is what makes seamless key rotation possible; old and new keys can coexist during a transition window with no downtime.

#### Layer 2: Gateway ECDSA

After the TSS check, the contract recovers the signer from a standard ECDSA signature on the same hash and matches it against the list of registered gateway signers. The gateway is the Muon node that relayed the TSS-signed data to the user. The second signature is what prevents a compromised TSS key from being exploited without simultaneously compromising a gateway node.

### Per-category authorization

After a matching TSS key or gateway signer is found, the verifier also checks that the key/signer is explicitly authorized for the MuonFunction category being verified. The categories are:

```solidity
enum MuonFunction {
    Trading,            // SendQuote, LockQuote, OpenPosition(s), FillCloseRequest(s), EmergencyClosePosition, ClosePositions
    AccountManagement,  // Deallocate, SafeDeallocate, DeallocateForPartyB, TransferAllocation
    Settlement,         // SettleUpnl, SettleUpnlUnified
    ForceClose,         // ForceClose, InitializeForceClose, SettleUpnlForForceClose(Legacy), FinalizeForceClose
    Funding,            // ChargeFundingRate, ChargeAccumulatedFundingFee
    LiquidationPartyA,  // LiquidatePartyA, SetSymbolsPrice, DeferredLiquidatePartyA, DeferredSetSymbolsPrice
    LiquidationPartyB   // LiquidatePartyB, LiquidatePositionsPartyB
}
```

A key authorized only for Trading cannot produce valid signatures for LiquidationPartyA. Permissions are opt-in, so newly added keys and gateway signers start with no permissions and cannot validate any signatures until explicitly granted via `setPublicKeyPermissions` or `setGatewaySignerPermissions` (both restricted to `SETTER_ROLE` on the verifier).&#x20;

### The MuonSignatureVerifier contract

The verifier lives at `contracts/helpers/verification/SymmioSignatureVerifier.sol` (the contract itself is named `MuonSignatureVerifier`) and is deployed as an independent contract, not part of the diamond. The diamond stores its address in `GlobalAppStorage.signatureVerifier` and `LibMuon.verifyTSSAndGateway` delegates to it. The interface:

```solidity
// contracts/core/interfaces/IMuonSignatureVerifier.sol
enum MuonFunction {
    Trading,
    AccountManagement,
    Settlement,
    ForceClose,
    Funding,
    LiquidationPartyA,
    LiquidationPartyB
}

interface IMuonSignatureVerifier {
    struct PublicKey { uint256 x; uint8 parity; }
    struct SchnorrSign {
        uint256 signature;
        address owner;
        address nonce;
    }

    // Signature verification
    function verify(bytes32 hash, SchnorrSign memory sign, bytes calldata gatewaySignature, MuonFunction func) external view;
    function verify(bytes32 hash, SchnorrSign memory sign, bytes calldata gatewaySignature) external view; // no auth check

    // Public key management
    function addPublicKey(PublicKey memory pubKey) external;
    function removePublicKey(PublicKey memory pubKey) external;
    function getAllPublicKeys() external view returns (PublicKey[] memory);

    // Gateway signer management
    function addGatewaySigner(address signer) external;
    function removeGatewaySigner(address signer) external;
    function getAllGatewaySigners() external view returns (address[] memory);

    // Per-function authorization
    function setPublicKeyPermissions(PublicKey memory pubKey, MuonFunction[] calldata functions, bool allowed) external;
    function setGatewaySignerPermissions(address signer, MuonFunction[] calldata functions, bool allowed) external;
    function isPublicKeyAuthorized(PublicKey memory pubKey, MuonFunction func) external view returns (bool);
    function isGatewaySignerAuthorized(address signer, MuonFunction func) external view returns (bool);
}
```

### Verification flow inside the contract

```solidity
	/// @notice Verifies both the TSS Schnorr signature and the gateway ECDSA signature,
	///         and checks that both the signing key and gateway are authorized for the given category
	/// @param hash The hash of the signed data
	/// @param sign The Schnorr signature to verify against registered public keys
	/// @param gatewaySignature The ECDSA gateway signature to verify
	/// @param func The operation category requesting verification
	function verify(bytes32 hash, SchnorrSign memory sign, bytes memory gatewaySignature, MuonFunction func) external view {
		// Verify TSS via Muon
		bool verifiedTSS = false;
		for (uint256 i = 0; i < publicKeys.length; i++) {
			if (LibMuonV04ClientBase.muonVerify(uint256(hash), sign, publicKeys[i])) {
				require(publicKeyPermissions[_publicKeyId(publicKeys[i])][func], "MuonSignatureVerifier: Key not authorized for function");
				verifiedTSS = true;
				break;
			}
		}
		require(verifiedTSS, "MuonSignatureVerifier: TSS not verified");

		// Verify Gateway Signature
		address signer = hash.toEthSignedMessageHash().recover(gatewaySignature);
		bool gatewayVerified = false;
		for (uint256 i = 0; i < gatewaySigners.length; i++) {
			if (signer == gatewaySigners[i]) {
				require(gatewaySignerPermissions[signer][func], "MuonSignatureVerifier: Gateway not authorized for function");
				gatewayVerified = true;
				break;
			}
		}
		require(gatewayVerified, "MuonSignatureVerifier: Gateway is not valid");
	}
```

### Key rotation procedure

1. `addPublicKey(newKey)` on the verifier.
2. setPublicKeyPermissions(newKey, \[...], true) to authorize it for the required functions.
3. Both old and new keys now validate Schnorr signatures — no downtime.
4. Once all Muon nodes have switched to the new key, removePublicKey(oldKey).

The same flow applies independently to `gatewaySigners`.

### What gets signed

Different operations require different signature payloads. Two things are worth separating clearly:

* **Struct fields**: what the caller actually passes in the `Sig` struct (defined in `MuonStorage.sol`). Every struct carries `reqId`, `timestamp`, a `gatewaySignature`, and the `sigs` (SchnorrSign), plus the UPNL/price values listed below.
* **Hashed inputs**: the contract reconstructs the signed `keccak256` hash inside the `LibMuon*` functions. The hash additionally binds values that are **not** struct fields: `muonAppId`, `address(this)`, the relevant party address(es), the on-chain `nonce`, `chainId`, and (for price sigs) the `symbolId`. These come from chain state or function arguments, not from the struct.

#### SingleUpnlSig

Used for: `deallocate()` (MuonFunction.AccountManagement), `deallocateForPartyB()` / `transferAllocation()`, `liquidatePartyB()` (MuonFunction.LiquidationPartyB).

```solidity
struct SingleUpnlSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnl;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### SingleUpnlAndPriceSig

Used for: `sendQuote()`. This is the only signature `sendQuote` accepts.

```solidity
struct SingleUpnlAndPriceSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnl;
    uint256 price;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### SingleUpnlWithPendingBalanceSig

Muon signature attesting to a party's UPNL plus their pending withdrawal balance.

```solidity
struct SingleUpnlWithPendingBalanceSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnl;
    uint256 pendingBalance; // their pending withdrawal balance
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### PairUpnlSig

Used for: funding operations: `chargeFundingRate()` and `chargeAccumulatedFundingFee()`.&#x20;

```solidity
struct PairUpnlSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnlPartyA;
    int256 upnlPartyB;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### PairUpnlAndPriceSig

Used for: `openPosition()`, `fillCloseRequest()`, and `emergencyClosePosition()` (MuonFunction.Trading). PairUpnlSig data plus a single symbol `price`; `symbolId` is supplied as a function argument and enters the hash.

```solidity
struct PairUpnlAndPriceSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnlPartyA;
    int256 upnlPartyB;
    uint256 price;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### PairUpnlAndPricesSig (batch)

Used for: `openPositions()`, `fillCloseRequests()` (MuonFunction.Trading). Carries arrays of `symbolIds` and `prices` for the batched quotes, but the UPNL values are two aggregates per party (`upnlPartyA`, `upnlPartyB`).

```solidity
struct PairUpnlAndPricesSig {
    bytes reqId;
    uint256 timestamp;
    int256 upnlPartyA;
    int256 upnlPartyB;
    uint256[] symbolIds;
    uint256[] prices;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

#### LiquidationSig

Used for: `liquidatePartyA()` and `setSymbolsPrice()` (MuonFunction.LiquidationPartyA). **Not** used for PartyB liquidation — `liquidatePartyB()` uses `SingleUpnlSig`.

```solidity
struct LiquidationSig {
    bytes reqId;
    uint256 timestamp;
    bytes liquidationId;
    int256 upnl;
    int256 totalUnrealizedLoss;
    uint256[] symbolIds;
    uint256[] prices;
    bytes gatewaySignature;
    IMuonSignatureVerifier.SchnorrSign sigs;
}
```

### Replay protection via nonces

Muon signatures include two distinct identifiers that serve different purposes.

#### reqId (Muon Request ID)

Each Muon request generates a unique reqId that's included in the signed hash.

#### Contract nonces

The contract maintains nonce counters in AccountStorage for each PartyA / PartyB. Muon reads the current on-chain nonce when it signs, includes it in the hash, and the contract verifies that the signed nonce matches the current on-chain value. After a successful state-changing call, the relevant nonce increments making any older signature invalid.

```
1. Muon reads current on-chain nonce (e.g. 5)
2. Muon signs data including that nonce
3. User submits the signature to the contract
4. Contract verifies hash includes nonce = 5 ✓
5. State change ➜ nonce increments to 6
```

In the current version, cross-mode (cross-PartyB) solvers will automatically sign with nonce = 0 for parallel operations. Sequential nonces remain available where ordering matters.

### Signature expiration

Signatures have a limited validity window defined by upnlValidTime in `MuonStorage`. This prevents an old signature from being reused after market conditions have moved. The window is measured against the timestamp field in the signed payload, not the block timestamp at submission, so a signed message that sits in a mempool for too long simply expires rather than landing on stale state.

### Oracle-less trading: bypassing Muon for bound pairs

When a PartyA trades exclusively with a single PartyB, the dual-Muon trust path is overkill. A malicious PartyB can't harm a different PartyA because there is no different PartyA. The current version lets a PartyA opt into this by binding to a specific PartyB via bindToPartyB. Once bound (and while that PartyB is bindable), the contract skips both the Muon signature check and the on-chain solvency check for the trading path against that PartyB.

Binding is also the gate for instant trading: `activateInstantActionMode` requires `BindStatus.BOUND`.&#x20;

In bound mode the trading functions still take the full `Sig` struct, and they still read the numeric fields. What is not used are the cryptographic fields (`reqId`, `gatewaySignature`, and the `sigs` Schnorr signature), since the `verify` call is skipped entirely; callers leave those empty. So no valid Muon signature is required.

Binding requires a clean state:

* No pending quotes (zero pending locked balance).
* No open positions with any other PartyB.

Unbinding is a soft, time-windowed flow:

* `requestToUnbindFromPartyB` — moves the relationship to `PENDING_UNBIND`, records a timestamp.
* `cancelUnbindRequest` — restore to `BOUND` if the user changes their mind.
* `completeUnbindRequest` — finishes the unbind. PartyB can complete immediately. Anyone else (including PartyA) must wait out the cooldown.


# 12-Hour Fraud Proof Window

Enhancing Security and assuming optimistic Finality

## 12-Hour Fraud Proof Window

### Enhancing security with assumed optimistic finality

Symmio is structured like an optimistic L2: trades execute against the belief that everything is valid and final the moment the user sees the confirmation, regardless of the underlying chain's finality. That belief is what gives the platform its CEX-like responsiveness. The 12-hour fraud proof window is the safety net under it — a buffer in which watchdogs and the protocol's own dispute machinery can detect bad behavior, stop transactions in flight, and unwind anything that shouldn't have happened.

In the current version this system is still intact, but where the window applies has shifted. The old "deallocate, wait 12 hours, withdraw" path is now one of three withdrawal types in the unified Withdraw System. Express and virtual providers can pay users instantly while the cooldown still runs against a separate piece of liquidity that the protocol holds back; the providers reclaim the funds at the cooldown's end. The [ClearingHouse](/contract-documentation/symmio-perps-v0.8.5/diamond-core-facets/clearing-house-facet) and the dispute resolver use the same window to clear up the cases the decentralized flow can't reach.

### Why 12 hours?

The window also acts as a buffer against unknown vulnerabilities. If a class of bug is discovered post-deployment, the protocol has 12 hours to detect, verify, and intervene before the attacker can exit. Pause roles (`PAUSER_ROLE`) and the `SUSPENDER_ROLE` can stop in-flight withdrawals; the dispute resolver and ClearingHouse can correct corrupted on-chain state.

### Symmio's layered verification model

The fraud-proof window is the last layer in a stack of checks:

**Layer 1 — User ↔ Solver.** Users and solvers interact directly to execute trades. Disputes are arbitrated by the Muon oracle in the first instance: the off-chain TSS network attests to unrealized profits/losses, prices, and solvency, and the contract uses those attestations to check every state-changing operation.

**Layer 2 — Oracle fraud protection.** Should a malicious actor compromise or manipulate the oracle, fraud-proof checkers serve as an additional safeguard. They monitor the Muon attestations and the resulting on-chain state, and can suspend addresses or withdrawals when discrepancies appear. The new per-`MuonFunction` authorization in `MuonSignatureVerifier` reduces the blast radius here: a key compromised for one category cannot sign for another.

**Layer 3 — Fraud-proof checkers.** Watchdogs that look for issues like double-spends, reorganizations, or signature corruption — scenarios that on most chains would be prevented by deep finality, but which Symmio's optimistic finality leaves room for. In the cases the dispute resolver or watchdogs cannot resolve, the [**ClearingHouse** ](/contract-documentation/symmio-perps-v0.8.5/diamond-core-facets/clearing-house-facet)can take over a stuck PartyA liquidation entirely and complete it with its own price feed.

### Withdrawals

#### Cooldown is anchored to deallocation

In the current version, the cooldown is computed from the user's last deallocation:

```
cooldownEndTime = max(deallocateTimestamp + withdrawCooldownPeriod, block.timestamp)
```

If the funds have been idle for the full cooldown (typically 12 hours), a freshly initiated withdrawal can finalize immediately. Subsequent deallocations don't reset the cooldown of an already-initiated withdrawal. The view function `getWithdrawableTime(user)` returns the earliest time a withdrawal initiated *now* could finalize.

#### The four withdrawal types

A withdrawal request is built from one or more **receiver parts**, each routed through a path:

| Type             | Express | Virtual | Description                             |
| ---------------- | ------- | ------- | --------------------------------------- |
| Normal (Classic) | ✗       | ✗       | Standard cooldown, same-chain delivery  |
| Pure Express     | ✓       | ✗       | Instant same-chain withdrawal           |
| Pure Virtual     | ✗       | ✓       | Standard cooldown, cross-chain delivery |
| Virtual Express  | ✓       | ✓       | Instant cross-chain withdrawal          |

Express providers front the funds for a fee and reclaim them after the cooldown ends. **Virtual providers** sit on top of the Virtual Fund System and deliver on the user's preferred chain via lifecycle callbacks (`onWithdrawRequest`, `acceptWithdrawRequest`, `onWithdrawComplete`, `onWithdrawCancel`). A provider can be one or the other, not both.

The fraud-proof window is preserved even when the user's funds arrive instantly. The mechanism:

* The user calls `initiateWithdraw(...)`. Status: `PENDING`.
* The express provider is notified via `onWithdrawRequest` and decides whether to accept.
* On `acceptWithdrawRequest`, status moves to `PROVIDER_ACCEPTED`. The provider pays the user **out of its own liquidity**, immediately.
* The cooldown clock continues to run against the original on-chain balance, which remains held by the protocol.
* After cooldown expires, `finalizeWithdrawRequest` releases that balance to the express provider, which reclaims its outlay.

If something goes wrong inside the window the protocol can prevent finalization and the express provider's liquidity is at risk, not the user's funds.&#x20;

#### Cancellation, blackouts, and force-cancel

* Before a provider has accepted, the user may cancel any time.
* After acceptance, outside any **blackout window** the provider configured, the user may still cancel directly.
* Inside the blackout, cancellation goes to `CANCEL_REQUESTED` and waits for provider approval — protecting the provider from being cancelled on after they've already paid out.
* An admin (`SETTER_ROLE` / dispute resolver) can `forceCancelWithdraw` in pathological cases.
* Speed-up approvals for whitelisted users can shorten the cooldown for specific cases without changing the global parameter.

{% hint style="info" %}
The legacy `transferToBridge` flow has been retired in favour of the express/virtual paths; bridges in the old version are now express providers with their own liquidity pools. If the protocol detects misuse, the relevant `SUSPENDER_ROLE` can suspend an address and stop a transaction inside the window.
{% endhint %}


# Audit Reports

Comprehensive list of all audits done by SYMMIO and their versioning

## SYMM v0.85

[Sherlock Audit - Feb 13, 2026](/security-and-architecture/audit-reports/symm-0.85/sherlock-audit-feb-13-2026)

## SYMM v0.84 &#x20;

&#x20;[Sherlock Audit - Oct 3, 2024](/security-and-architecture/audit-reports/symm-0.84/sherlock-audit-oct-3-2024)

## SYMM v0.83 &#x20;

&#x20;[Sherlock Audit - Jun 17, 2024](/security-and-architecture/audit-reports/symm-0.83/sherlock-audit-jun-17-2024)

## SYMM v0.82 &#x20;

&#x20;[Sherlock Audit -Aug 30, 2023](/security-and-architecture/audit-reports/symm-0.82/sherlock-audit-aug-30-2023)

## SYMM v0.8 - 0.81

[Sherlock Audit - Jun 15, 2023](/security-and-architecture/audit-reports/symm-v0.8-0.81/sherlock-audit-jun-15-2023)

&#x20;[Smart State - Jul 2, 2023](/security-and-architecture/audit-reports/symm-v0.8-0.81/smart-state-jul-2-2023)

## Vaults

&#x20;[Sherlock Audit - Jan 2, 2024](/security-and-architecture/audit-reports/vaults/sherlock-audit-jan-2-2024)

## Staking and Vesting

&#x20;[Sherlock Audit - Mar 7, 2025](/security-and-architecture/audit-reports/staking-and-vesting/sherlock-audit-mar-7-2025)


# SYMM - 0.85


# Sherlock Audit - Feb 13, 2026

{% hint style="info" %}
Sherlock is an audit marketplace and smart contract coverage protocol built on the Ethereum blockchain. Sherlock works to protect Decentralized Finance (DeFi) users from smart contract exploits with security reviews from top auditors backed by smart contract coverage on the audited contracts.

You can find a brief overview of the Sherlock ecosystem below.&#x20;

<https://docs.sherlock.xyz/>
{% endhint %}

### Report 1:

{% file src="/files/v2LeBvSN2mOIAX4ZLxPG" %}

### Report 2:

{% file src="/files/yeiyUiRZEswHhWfaw4bH" %}


# SYMM - 0.84


# Sherlock Audit - Oct 3, 2024

{% embed url="<https://audits.sherlock.xyz/contests/577?filter=questions>" %}

{% hint style="info" %}
Sherlock is an audit marketplace and smart contract coverage protocol built on the Ethereum blockchain. Sherlock works to protect Decentralized Finance (DeFi) users from smart contract exploits with security reviews from top auditors backed by smart contract coverage on the audited contracts.

You can find a brief overview of the Sherlock ecosystem below.&#x20;

<https://docs.sherlock.xyz/>
{% endhint %}

View findings:<https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest-judging/issues>\
\
Audit report: <https://github.com/sherlock-audit/2024-09-symmio-v0-8-4-update-contest-judging/blob/main/Audit_Report.pdf>


# SYMM - 0.83


# Sherlock Audit - Jun 17, 2024

{% embed url="<https://audits.sherlock.xyz/contests/427>" %}

{% hint style="info" %}
Sherlock is an audit marketplace and smart contract coverage protocol built on the Ethereum blockchain. Sherlock works to protect Decentralized Finance (DeFi) users from smart contract exploits with security reviews from top auditors backed by smart contract coverage on the audited contracts.

You can find a brief overview of the Sherlock ecosystem below.&#x20;

<https://docs.sherlock.xyz/>
{% endhint %}

View findings: <https://github.com/sherlock-audit/2024-06-symmetrical-update-2-judging/issues>\
\
Audit report: <https://github.com/sherlock-audit/2024-06-symmetrical-update-2-judging/blob/main/Audit_Report.pdf>


# SYMM - 0.82


# Sherlock Audit -Aug 30, 2023

{% embed url="<https://audits.sherlock.xyz/contests/108>" %}

{% hint style="info" %}
Sherlock is an audit marketplace and smart contract coverage protocol built on the Ethereum blockchain. Sherlock works to protect Decentralized Finance (DeFi) users from smart contract exploits with security reviews from top auditors backed by smart contract coverage on the audited contracts.

You can find a brief overview of the Sherlock ecosystem below.&#x20;

<https://docs.sherlock.xyz/>
{% endhint %}

View findings: <https://github.com/sherlock-audit/2023-08-symmetrical-judging/issues>\
\
Audit report: <https://github.com/sherlock-audit/2023-08-symmetrical-judging/blob/main/Audit_Report.pdf>


# SYMM - V0.8 - 0.81


# Sherlock Audit - Jun 15, 2023

SYMMIO-Core (v0.8) Jun 15, 2023 17:00 GMT+2

{% embed url="<https://audits.sherlock.xyz/contests/85>" %}

{% hint style="info" %}
Sherlock is an audit marketplace and smart contract coverage protocol built on the Ethereum blockchain. Sherlock works to protect Decentralized Finance (DeFi) users from smart contract exploits with security reviews from top auditors backed by smart contract coverage on the audited contracts.

You can find a brief overview of the Sherlock ecosystem below.&#x20;

<https://docs.sherlock.xyz/>
{% endhint %}

View findings: <https://github.com/sherlock-audit/2023-06-symmetrical-judging/issues>\
\
Audit report: <https://github.com/sherlock-audit/2023-06-symmetrical-judging/blob/main/README.md>


# Smart State - Jul 2, 2023

{% @github-files/github-code-block %}

Smart State audited SYMMIO, through a collaboration (3rd Party Frontend and Hedger), who provided the audit as a grant.

### **About Smart State**

Established in 2019, Smart State has always been at the forefront of innovation. While many firms in the industry limit themselves to a standard checklist approach for smart contract audits and security reviews, Smart State believes in a more holistic approach.

### more on their website: <https://smartstate.tech/about.html>

Audit Report:

{% file src="/files/HoFydmqEiPDHRj18r0qX" %}


# Vaults


# Sherlock Audit - Jan 2, 2024

{% embed url="<https://audits.sherlock.xyz/contests/144>" %}

{% hint style="info" %}
Sherlock is an audit marketplace and smart contract coverage protocol built on the Ethereum blockchain. Sherlock works to protect Decentralized Finance (DeFi) users from smart contract exploits with security reviews from top auditors backed by smart contract coverage on the audited contracts.

You can find a brief overview of the Sherlock ecosystem below.&#x20;

<https://docs.sherlock.xyz/>
{% endhint %}

View findings:[ ](https://github.com/sherlock-audit/2023-12-symm-io-judging/issues)<https://github.com/sherlock-audit/2023-12-symm-io-judging/issues>\
\
Audit report: <https://github.com/sherlock-audit/2023-12-symm-io-judging/blob/main/Audit_Report.pdf>


# Staking and Vesting


# Sherlock Audit - Mar 7, 2025

{% embed url="<https://audits.sherlock.xyz/contests/838>" %}

{% hint style="info" %}
Sherlock is an audit marketplace and smart contract coverage protocol built on the Ethereum blockchain. Sherlock works to protect Decentralized Finance (DeFi) users from smart contract exploits with security reviews from top auditors backed by smart contract coverage on the audited contracts.

You can find a brief overview of the Sherlock ecosystem below.&#x20;

<https://docs.sherlock.xyz/>
{% endhint %}

View findings: <https://github.com/sherlock-audit/2025-03-symm-io-stacking-judging/issues>\
\
Audit report: <https://github.com/sherlock-audit/2025-03-symm-io-stacking-judging/blob/main/Audit_Report.pdf>


# Bug Bounty Program / Coverage

1. **Critical Level:**

   * Up to $800,000 or 10% of the (potential) economic damage on contracts with more funds locked than 1 million USD.
   * The 10% rule also applies to funds already removed without authorization from respective contracts. In such cases, 90% of the funds must be immediately returned, and 10% can be kept as a Whitehat bounty reward.
   * The 10% rule can also be claimed as a general bug bounty on contracts above $1m TVL, by providing a PoC or by assisting the team in creating a PoC.

   #### Critical

   * 808,808 USDC maximum payout
   * 80,000 USDC minimum payout
   * Payout shall not exceed 10% of funds at risk at time of submission

   ### Severity Criteria

   ### Critical Definition

   * Definite and significant loss of funds without limitations of external conditions
   * Definite and significant freezing of funds for >1 year without limitations of external conditions

   ### General Notes

   * Sherlock’s Criteria for Issue Validity guide (used in Sherlock audit contests) can be a helpful resource for more context on out-of-scope issues, etc. but nothing in the guide should overrule the definitions above
   * A coded Proof of Concept (POC) with instructions to run the POC is required
   * If the protocol team has the ability to take measures (upgrade the contract, pause the contract, etc.) against an exploit, the potential damage is limited to a 1-hour exploit period before it is assumed that the protocol team takes measures to prevent further damage

   ### Platform Rules

   Please review the [Sherlock Bug Bounty Platform Rules](https://docs.google.com/document/d/1fHM85t-nJb9F2T_vk28YUF34jey_uQP5aZyS9om3Dq4/edit?usp=sharing) before submitting any vulnerability.

   ### Known Issues and Acceptable Risks

   * In the liquidation system, we allow liquidators to liquidate a user if they were insolvent before. The nonce should not be changed from that time (essentially, the user's positions should remain unchanged during this period). If the user adds funds to become solvent, the liquidator can still liquidate the user and return the extra funds.

   ### Previous Audits

   * <https://audits.sherlock.xyz/contests/427>
   * <https://audits.sherlock.xyz/contests/144>
   * <https://audits.sherlock.xyz/contests/108>
   * <https://audits.sherlock.xyz/contests/85>
   * [Smart State](https://github.com/SYMM-IO/audits/blob/main/26092306_Symmio_report.pdf)

The 10% rule only applies for contracts that are live, and have a TVL more than $1M

1. **High Level:**
   * $50,000 or up to 1% of the (potential) economic damage.
   * The 10% rule, as outlined in the Critical Level section, also applies.
2. **Medium Level:**
   * USD $5,000 Payout.
   * Runnable PoC required.
3. **Low Level:**
   * USD $1,000 Payout.
   * Runnable PoC required.

{% hint style="warning" %}
Please note that for a Bug Bounty submission to be seen as critical the actual funds need to be able to be withdrawn by the Attacker from the system, that means the [12 hour withdrawal](/security-and-architecture/12-hour-fraud-proof-window) window has to be circumvented by the attacker, otherwise the submission would be counted as High.
{% endhint %}

#### Smart Contracts

| Level       | Impact                                                                                                                                                                                                                                                           |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 5. Critical | <p>- Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield<br>- Permanent freezing of funds</p>                                                                                                                               |
| 4. High     | <p>- Theft of unclaimed yield<br>- Theft of unclaimed royalties<br>- Permanent freezing of unclaimed yield<br>- Permanent freezing of unclaimed royalties<br>- Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)<br></p> |
| 3. Medium   | <p>- Block stuffing for profit<br>- Unbounded gas consumption<br>- Temporary freezing of funds</p>                                                                                                                                                               |
| 2. Low      | <p>- Contract fails to deliver promised returns, but doesn't lose value<br>- Miner-extractable value (MEV)</p>                                                                                                                                                   |
| 1. None     | <p>- Best practices<br>- Smart contract unable to operate due to lack of token funds</p>                                                                                                                                                                         |

Payouts are handled by **SYMMIO Team or DAO** directly and are denominated in **USDC or SYMM**.

Payment in maximum 70% stable & minimum 30% native token.

## How to Contact Us?

Contact us via Sherlock Bug Bounty:\
<https://audits.sherlock.xyz/bug-bounties/5?t=details>\
\
Or via email: <bugbounty@symm.io>


# Settlement Costs in Symmio

To settle perps on Symmio there is a plattform fee that is charged for each executed bilateral agreement.

**Solvers and Market Makers enjoy zero fees.**\
Symmio’s settlement cost structure is designed to encourage the participation of professional market makers and solvers. Solvers and Market Makers do **not** pay settlement fees. This approach mirrors the “Market Maker programs” seen in Order Book DEX environments, where liquid and efficient markets are rewarded by charging makers differently—often zero fees—to encourage tighter spreads and better liquidity for all users.

**User-facing Product-Specific Settlement Costs**\
Settlement costs are not one-size-fits-all. The exact fee structure varies depending on the product type.  Every product category has different settlement costs, read more in each respective category: &#x20;

[Perps - Settlement Costs](/exchange-builder-documentation/settlement-costs-in-symmio/perps-settlement-costs)

[Pair Trading - Settlement costs](/exchange-builder-documentation/settlement-costs-in-symmio/pair-trading-settlement-costs)

**Platform Fees and Affiliate Credits**\
A portion of each platform fee is allocated to the exchange’s affiliate partner—the frontend builder. This means that if you run a frontend interface integrated with Symmio, you can earn a share of the fees generated when traders execute orders through your platform. To learn more about becoming a frontend partner and start building with our toolkit, visit the [Choosing an Exchange](/trader-documentation/choosing-an-exchange) section and explore the [Frontend SDK](https://github.com/SYMM-IO/frontend-sdk).

***

**Overview**\
As a B2B-focused solution, Symmio provides a “Derivatives as a Service” model that empowers builders to launch their own decentralized perpetual or derivatives exchanges. Whenever traders execute trades through the Symmio clearing layer, a platform fee is applied. This fee supports the ecosystem, incentivizes liquidity provision, and helps maintain the high-performance clearing infrastructure.


# Perps - Settlement Costs

To settle perps on Symmio there is a plattform fee that is charged for each executed bilateral agreement.

## Settlement Costs overview: `Perpetual Products`

Settlement cost for Endusers: `3 Basis Points`

Settlement cost for Solvers: `0 Basis points`\
\
Settlement cost for Subnets (Frontends): `reduced based on Table` [#settlement-cost-reduction-table](#settlement-cost-reduction-table "mention")

{% hint style="success" %}
Settlement Costs can be for a limited period of times and for certain unique product offerings e.g. (Agentic Trading on Privex) be reduced, but not zeroed, in alignment with the Symmio Foundations approval.
{% endhint %}

## Settlement Cost reduction Table

{% hint style="info" %}
The early adopter program will be discontinued in 2026.\
\
Meaning Exchange Builders will need to achieve the Volume requirement AND hold $SYMM to receive the Settlement cost rebates. The Symmio Foundation and Team reserves the right to adjust the $SYMM amounts based on market feedback.
{% endhint %}

| Monthly Volume         | $SYMM Holdings in % | Settlement costs in % |
| ---------------------- | ------------------- | --------------------- |
| $10,000,000 ($10M)     | 0.0625%             | 70%                   |
| $25,000,000 ($25M)     | 0.125%              | 60%                   |
| $50,000,000 ($50M)     | 0.25%               | 50%                   |
| $100,000,000 ($100M)   | 0.5%                | 40% (early adopters)  |
| $1,000,000,000 ($1B)   | 0.75%               | 30%                   |
| $10,000,000,000 ($10B) | 2%                  | 20%                   |
| $30,000,000,000 ($30B) | 2.5%                | 15%                   |

#### EXAMPLE A -  VOLUME based:&#x20;

Frontend Builder A generated **10M in monthly volume** it receives a **30% fee rebate.**\
\
**EXAMPLE B - $SYMM Holding based:**

Frontend Builder B locked 0.5% of the circulating $SYMM Supply into an NFT, it receives a **60% fee rebate.**

{% hint style="info" %}
Please note that these thresholds are subject to adjustment, and we aim to optimize them in the best interest of our frontend builder partners and the wider Symmio network.\
This program aims to provide a significant fee rebate to subnets, incentivizing increased $SYMM holdings. This will, in turn, drive the overall growth of Symmio.
{% endhint %}

## Early Adopter Program

{% hint style="warning" %}

#### Early Adopter Program

ATTENTION: THE EARLY ADOPTER PROGRAM WILL BE DISCONTINUED IN 2026.

Frontend Builders enrolled into the early adopter program enjoy earning 60% of their fees immediately on day one from their launch. However, to maintain this advantage, they must meet one of the following conditions during their first year of operation:

* Achieve a monthly volume of $100,000,000+ \
  \
  AND
* Acquire at least 0.5% of SYMM tokens

In the 13th month of operation, if they fail to generate a monthly volume of $100 million and do not hold 0.5% of SYMM tokens, their profit share will be adjusted according to their actual volume and $SYMM holdings.

The early adopter program will continue until Symmio sustains daily volumes between $250 million and $500 million over a prolonged period.

For any questions regarding the early adopter program, feel free to contact us at <info@symm.io>
{% endhint %}

We understand early adopters' crucial role in successfully growing and adopting new technologies. To appreciate and reward this, we are launching an "Early Adopter Program" where the first five frontend builders will be given special deals to push our adoption at the beginning.

During the Early Adopter Program, Frontend Builders will start earning 60% of the profit right away.

### Early adopter Frontend Builders:

| Frontend                                    | Frontend Share | Protocol Share | Service Fee |
| ------------------------------------------- | -------------- | -------------- | ----------- |
| <ol><li>Thena (serviced)</li></ol>          | 51%            | 34%            | 15%         |
| <ol start="2"><li>Based Markets</li></ol>   | 60%            | 40%            | 0%          |
| <ol start="3"><li>IntentX</li></ol>         | 70%            | 30%            | 0%          |
| <ol start="4"><li>CORE (serviced)</li></ol> | 51%            | 34%            | 15%         |

### Final Note

By creating this Fee Sharing Program, we aim to foster an ecosystem where collaboration and mutual growth are at the forefront. We firmly believe that this symbiotic relationship with frontend builders will enhance the overall strength, stability, and prosperity of the SYMMIO network.

We encourage all frontend builders to participate in this program and join us in revolutionizing the SYMMIO landscape.

For any further information, please check out:

[Solving for Symmio](/liquidity-provider-documentation/solving-for-symmio)

or contact us via Discord [https://discord.gg/invite/symmio](https://discord.com/invite/symmio) or email us at <info@symm.io>


# Pair Trading - Settlement costs

To settle pairs on Symmio there is a settlement fee that is charged for each executed bilateral agreement.

{% hint style="warning" %}
Pear Protocol exclusive Use Grant for Pair Trading version of Cloverfield.\
Currently Pear Protocol enjoys an exclusivity to be the only Subnet to use the dedicated Pair Trading version of Cloverfield via our [Frontend Use Grants](/legal-and-brand/terms-of-service-and-licensing/frontend-license/frontend-use-grants).\
\
Other frontend builders are still able to build pair products ontop of our permissionless contracts from scratch.
{% endhint %}

## Settlement Costs overview: `Pair Trading`

Settlement cost for Endusers: 5 `Basis Points`

Settlement cost for Solvers: `0 Basis points`\
\
Settlement cost for Pair trading Subnets (Frontend Builders): `reduced based on Table` [#settlement-cost-reduction-table](#settlement-cost-reduction-table "mention")

## Settlement Cost reduction Table

| Monthly Volume         | $SYMM Holdings in % | Settlement costs in % |
| ---------------------- | ------------------- | --------------------- |
| $10,000,000 ($10M)     | 0.0625%             | 70%                   |
| $25,000,000 ($25M)     | 0.125%              | 60%                   |
| $50,000,000 ($50M)     | 0.25%               | 50%                   |
| $100,000,000 ($100M)   | 0.5%                | 40%                   |
| $1,000,000,000 ($1B)   | 0.75%               | 30%                   |
| $10,000,000,000 ($10B) | 1.15%               | 20%                   |
| $30,000,000,000 ($30B) | 1.5%                | 15%                   |

#### EXAMPLE A -  VOLUME based:&#x20;

Frontend A generated **10M in monthly volume** it receives a **30% fee rebate.**\
\
**EXAMPLE B - $SYMM Holding based:**

Fontend B locked 0.5% of the circulating $SYMM Supply into an NFT, it receives a **60% fee rebate.**<br>

{% hint style="info" %}
Please note that these thresholds are subject to adjustment, and we aim to optimize them in the best interest of our frontend partners and the wider Symmio network.\
This program aims to provide a significant fee rebate to subnets, incentivizing increased $SYMM holdings. This will, in turn, drive the overall growth of Symmio.
{% endhint %}

### Early Adopter Program

We understand early adopters' crucial role in successfully growing and adopting new technologies. To appreciate and reward this, we are launching an "Early Adopter Program" where the first five frontend builders will be given special deals to push our adoption at the beginning.

During the Early Adopter Program, frontend builders will start earning 60% of the profit right away.

### Early adopter Fronter Builders:

| Frontend                        | Frontend Share | Protocol Share | Service Fee |
| ------------------------------- | -------------- | -------------- | ----------- |
| <ol><li>Pear Protocol</li></ol> | 70%            | 30%            | 0%          |

### Final Note

By creating this settlement cost reduction program for subnets, we aim to foster an ecosystem where collaboration and mutual growth are at the forefront. We firmly believe that this symbiotic relationship with frontend builders will enhance the overall strength, stability, and prosperity of the SYMMIO network.

We encourage all frontend builders to participate in this program and join us in revolutionizing the SYMMIO landscape.

For any further information, please check out our

[Broken mention](broken://pages/AEl4ZlpsCsa435YklPXa)&#x20;

[Solver Documentation (OLD)](/liquidity-provider-documentation/solver-documentation-old)

[Broken mention](broken://pages/IF91C8Ox4GLh4Jas5qMG)&#x20;

or contact us via Discord [https://discord.gg/invite/symmio](https://discord.com/invite/symmio) or email us at <info@symm.io>


# Frontend Builder Introduction

Symmio is built for censorship resistance, so it provides the technical infrastructure for executing aMFQ peer-to-peer (p2p) symmetrical trades and doesn't host a frontend itself. It supplies the technology for these trades without using it to make markets or offer products to users.

To trade Symmio markets, use one of the [third-party frontend applications](https://symm.io/frontends).

### Business-to-business (B2B)

Symmio operates strictly business-to-business. The infrastructure is offered to developers and exchanges, so they can build on decentralized derivatives without handling the backend themselves.

If you're interested in launching a Symmio frontend and creating your own markets on the Symmio stack, see the [Profit Sharing Model](/exchange-builder-documentation/settlement-costs-in-symmio/perps-settlement-costs).

### Integrating with Symmio

Symmio gives you a full perpetual-exchange backend to integrate into an existing or new exchange. The functionality on the [Symmio demo frontend](https://cloverfield.exchange/trade/BTCUSDT) can be incorporated into your platform by forking the codebase under a Frontend Use Grant, or by using the Frontend SDK. For deployment details, see the Frontend Builder Technical Guidance.

This also avoids the liquidity fragmentation that comes with forking vAMM-based perps (e.g. GMX, Gains) or order-book-based ones (e.g. dYdX). Symmio routes to a large pool of institutionally-backed liquidity providers, called *Hedgers*, so liquidity is aggregated rather than fragmented.

Reach out any time for more information or support.

## Disclaimer

{% hint style="info" %}

#### Decentralized Frontend Applications <a href="#decentralized-frontends" id="decentralized-frontends"></a>

In our quest of making the system more decentralized and censorship-resistant. Symmio itself doesn't offer any trading or promote the trading of any derivatives; Symmio only provides the infrastructure for symmetrical peer-to-peer matchmaking; 3rd party providers can use that to build a fully functional on-chain derivatives exchange. Frontend Builders (Dealers and Market Makers) offer services directly via 3rd party services. To see their offering, make deposits, request trades, etc. Users thus have to use one of the frontends provided by third parties.​[ #https://symm.io/frontends](https://symm.io/frontends)

#### Which Frontend do I pick?  <a href="#which-frontend-do-i-pick" id="which-frontend-do-i-pick"></a>

Deciding which Frontend to use is a personal decision based on various factors, including UI, UX, features, tools, and the Frontend Operator's liquidity on the platform or offering of products and assets.​

#### List Disclaimer <a href="#list-disclaimer" id="list-disclaimer"></a>

Frontend Operators provide descriptions. The list of Frontend Operators is provided for informational purposes only. Neither is the list conclusive nor has Symmetry Labs AG conducted any due diligence on these operators. Accordingly, Symmetry Labs AG does not make any statement regarding technical functionality and the trustworthiness of the Frontend Operators listed below.&#x20;
{% endhint %}


# Setting up a Frontend with SYMMIO

To integrate a frontend with Symmio, follow the steps below. This is a general outline that assumes familiarity with the Symmio ecosystem, smart contract interaction, and frontend development.

### 1. Clone or build the frontend application

Start by cloning the official Symmio Frontend SDK repository, or build your own frontend using the SDK and docs. The SDK handles most of the work of talking to Symmio's APIs and smart contracts. See the Frontend SDK setup guide for how to clone and configure it, or to roll your own.

### 2. Register as an affiliate

In v0.8.5 every frontend trades through the shared **AccountLayer**, and you register as an affiliate so users can create accounts under you and your trades earn fees. Registration is two steps:

1. Request registration with your frontend name, brand color, admin address, fee stakeholder configuration, and the Symmio core instance(s) you'll use.
2. A protocol `APPROVER_ROLE` holder approves it. On approval, an **AccountManager** proxy is deployed for you (deterministic CREATE2), a fee distributor address is generated, and your affiliate is registered on each allowed core.

Once your affiliate is `ACTIVE`, users can create SubAccounts under it, and your affiliate address is what tracks volume and routes platform fees on trades that go through you:

* **User volume tracking:** the affiliate fee system tracks trading volume per affiliate, giving you insight into activity and engagement.
* **SubAccounts and VirtualAccounts:** users create SubAccounts to organize their trades, with per-position isolation handled by the AccountLayer.
* **Automated fee claiming:** trades carry your affiliate address, so collected fees route to your fee distributor automatically.

The auto-deployed AccountManager also exposes the old MultiAccount-style API (`addAccount`, `_call`, and so on) as a backward-compatible entry point. See Building a New Frontend for the account model, and Upgrading from MultiAccount (0.8.4) if you're moving an existing deployment.

### 3. Provide frontend URLs for whitelisting

When your frontend is ready to deploy, share its URL(s) with the team for whitelisting. Provide the production address so it's added to the CORS whitelist; if you have test or staging environments, share those too and the team will whitelist them for development. Whitelisting is required for features like Instant Execution, where a session key signs trades through the Instant Layer for near-instant confirmations.


# Frontend Builder SDK

## Frontend Builder SDK

### SYMM client SDK

[An SDK to interact with SYMMIO contracts, hedgers, and peripherals.](https://github.com/SYMM-IO/frontend-sdk)

{% hint style="info" %}
The official Frontend SDK targets the earlier MultiAccount flow. On Symmio v0.8.5 you build against the **AccountLayer**, with an **AccountManager** wrapper deployed for you on affiliate registration. See [Building a New Frontend](/exchange-builder-documentation/frontend-builder-technical-guidance/building-a-new-frontend) and [Upgrading from MultiAccount (0.8.4)](/exchange-builder-documentation/frontend-builder-technical-guidance/migrating-from-multiaccount-0.8.4). Where this page says "MultiAccount," read it as the AccountManager entry point onto the AccountLayer.
{% endhint %}

* To enable path-completion suggestions in VSCode, build the project first with `yarn build`, then set "TypeScript > Tsc: Auto Detect" to "on" in the VSCode settings.
* Don't use the `publicProvider` from Wagmi; it causes errors in the SDK.

[Wagmi's docs](https://wagmi.sh/core/getting-started#configure-chains) note:

> In a production app, it is not recommended to only pass publicProvider to configureChains as you will probably face rate-limiting on the public provider endpoints. It is recommended to also pass an alchemyProvider or infuraProvider as well.

### Setup guide

{% hint style="info" %}
Setup won't work with Yarn modern. Run these steps with Yarn classic (v1).
{% endhint %}

{% hint style="info" %}
The **Polygon** network is used internally and may have intermittent downtime as updates and new deployments roll out, so it isn't recommended for external partner testing. A test environment with a stable test hedger may be made available in the future.
{% endhint %}

First, run:

```
yarn install
```

Navigate to one of the app folders, for example the alpha app or the Next.js app:

```
cd apps/alpha/
```

To set up your environment:

1. Locate the `.env.example` file in the `apps/alpha` directory.
2. Rename `.env.example` to `.env`.
3. Update the fields in `.env` as required. Each chain needs a unique `HEDGER_URL` and public RPC.

Contract addresses aren't hardcoded here, because they change as new versions deploy. Pull them from the authoritative source for each one:

* **Diamond addresses** (Symmio core, AccountLayer, InstantLayer): look them up in the [SYMMIO explorer](https://intent.symmscan.com/inspector). Pick the chain and browse the deployed diamonds and their facets; Querying Info from the SYMMIO Diamond walks through it. The collateral token address is readable from the core with `getCollateral()`.
* **Hedger URLs and PartyB (solver) addresses**: consult each [hedger's API endpoints](/api-endpoints-and-deployments/symmio-perps-deployments) and reference pages under API Endpoints and Deployments, for example Rasa Capital. Each hedger publishes its own URL, PartyB address, and API base there.

Set the values you need in your `.env` per chain.

Finally, run:

```
yarn dev
```

The development server starts at `http://localhost:3000`.

**NOTE:** The Alpha and Next.js applications are identical; they just have different UIs.

### Configuring the app

**User configuration**

Supported chains are declared in the core package's `src/constants/chains.ts`:

```typescript
export enum SupportedChainId {
  NOT_SET = 0,
  MAINNET = 1,
  ROPSTEN = 3,
  RINKEBY = 4,
  BSC = 56,
  BASE = 8453,
  BSC_TESTNET = 97,
  POLYGON = 137,
  FANTOM = 250,
  ARBITRUM = 42161,
}
```

To add a chain for the user to connect to, pick one or more chains from the supported chain IDs and add them to the `ClientChain` variable in `nextjs/constants/chains.ts`:

```typescript
export const ClientChain = [
  SupportedChainId.POLYGON,
  SupportedChainId.BSC,
  SupportedChainId.BASE,
]; //Adding chains...
```

**Configuring the Hedger API**

Go to `app/constants/chains/hedgers.ts` and add the chain to `HedgerInfo`. Here's an example declaration:

```typescript
  [SupportedChainId.POLYGON]: [
    {
      apiUrl: "https://fapi.binance.com/",
      webSocketUrl: "wss://fstream.binance.com/stream",
      baseUrl: `https://${process.env.NEXT_PUBLIC_POLYGON_HEDGER_URL}`,
      webSocketUpnlUrl: `wss://${process.env.NEXT_PUBLIC_POLYGON_HEDGER_URL}/ws/upnl-ws`,
      webSocketNotificationUrl: `wss://${process.env.NEXT_PUBLIC_POLYGON_HEDGER_URL}/ws/position-state-ws3`,
      webSocketFundingRateUrl: `wss://${process.env.NEXT_PUBLIC_POLYGON_HEDGER_URL}/ws/funding-rate-ws`,
      defaultMarketId: 1,
      markets: [],
      openInterest: { total: 0, used: 0 } as OpenInterest,
      id: "",
      fetchData: true,
      clientName: "",
    },
  ],
```

The `apiUrl` and `webSocketUrl` don't change with the chain, because frontends listen directly to the Binance API for price data.

**Configuring addresses**

So the frontend interacts correctly with the deployed contracts, update `constants/chains/addresses.ts` with the right contract addresses for each chain, and add any necessary contract ABIs to the `/abi` folder. Source the diamond addresses from the [SYMMIO explorer](https://intent.symmscan.com/inspector) and the hedger/solver addresses from each hedger's API reference.

```typescript
export const CustomChain: ChainType = {
  COLLATERAL_SYMBOL: "",
  COLLATERAL_DECIMALS: ,
  COLLATERAL_ADDRESS: "",

  DIAMOND_ADDRESS: "",
  MULTI_ACCOUNT_ADDRESS: "",
  PARTY_B_WHITELIST: "",
  SIGNATURE_STORE_ADDRESS: "",

  MULTICALL3_ADDRESS: "",
  USDC_ADDRESS: "",
  WRAPPED_NATIVE_ADDRESS: "",
  ANALYTICS_SUBGRAPH_ADDRESS:
    "",
  ORDER_HISTORY_SUBGRAPH_ADDRESS:
    "",
};
```

**Configuring subgraphs**

To fetch a user's balance history or order history, determine the right GraphQL subgraph URI for the chain ID, then use Apollo Client to query it. Go to `packages/core/src/apollo/client` and edit `balanceHistory.ts` and `orderHistory.ts` to add the new chain. For example:

```typescript
const testClient = createApolloClient(
  `${getSubgraphName(SupportedChainId.TEST)}`
);
```

Then add it as a case for `getBalanceHistoryApolloClient` and `getOrderHistoryApolloClient`:

```typescript
export function getBalanceHistoryApolloClient(chainId: SupportedChainId) {
  switch (chainId) {
    case SupportedChainId.TEST:
      return testClient;
```

**Configuring Muon**

Muon verifies the price of an asset and a user's uPnL. Its signatures are required by the smart contracts to execute functions related to user positions. In most cases you won't need to change the Muon base URLs, but you can update them in `/constants/chains/misc.ts`.

### State-component relationships

### Account management (AccountLayer)

The user will, via the frontend, create and switch between SubAccounts on the **AccountLayer**. The auto-deployed **AccountManager** exposes the familiar `addAccount` / `_call` / `getAccounts` API as a backward-compatible entry point.

When a user interacts with the Symmio contracts through a SubAccount to send or modify quotes, the frontend encodes the Symmio core call and passes it to `_call(account, _callData)`: the SubAccount address plus a bytes array. The AccountLayer routes the call from the SubAccount, creating or reusing a VirtualAccount according to the SubAccount's isolation type.

Users select an account from a drop-down in the header, which lets them isolate positions across SubAccounts. Users provide a name when they create one. The `useActiveAccountAddress` hook returns the SubAccount address, and `useActiveWagmi` returns the wallet address.

### Querying the hedger

Symmio provides off-chain information to help users find a suitable hedger, in two categories: APIs and WebSockets.

These APIs aim to reduce failed user requests. They're subject to each hedger's policies: if a hedger doesn't require a whitelist, the related APIs are unnecessary.

Many endpoints take your frontend's account-contract address as a parameter, which in v0.8.5 is the **AccountManager** address. This is the address users interact with to create SubAccounts and call functions for opening and closing positions on the Symmio diamond.

### Testing guidelines

#### Testing environments

**Preferred networks for testing**

* **Base and Arbitrum (mainnets):** recommended for testing interactions, especially opening and closing positions, since they're more stable than test networks. Testing here needs a small amount of USDC to verify functionality.
* **Setting up environment variables in `.env`:** add the following to your `.env` to specify the hedger URL and public RPC URL:

  ```json
  NEXT_PUBLIC_BASE_HEDGER_URL="base-hedger82.rasa.capital"
  NEXT_PUBLIC_BASE_PUBLIC_RPC_URL="https://base.llamarpc.com"
  ```

#### Using test tokens

* For Base and Arbitrum, testing currently needs real capital (small amounts of USDC). Polygon supports test tokens, but the test hedger isn't stable enough for reliable frontend testing.

#### Setting up account contracts

* For initial testing of functions like `sendQuote`, frontend builders can trade under an existing affiliate's setup without registering their own.
* For production, register as an affiliate; an AccountManager is deployed for you on approval and registered with the solvers. You no longer deploy your own account contract.

### Hedger API

The Hedger API helps users monitor their positions. It exposes the status of positions, total open interest, notional caps, and the capital required to open positions.

Some useful endpoints:

#### GET `contract-symbols`

Returns the total number of symbols the hedger offers, with key data for each.

**Returns:**

* **`count`**: the total number of trading symbols available through the hedger.
* **`symbols`**: an array of objects, each representing a trading symbol or contract the hedger offers, with the fields below.
  * **`name`**: the name of the trading pair (e.g. BTCUSDT).
  * **`symbol`**: the ticker for the former part of the pair (e.g. BTC).
  * **`asset`**: the ticker for the latter part of the pair (e.g. USDT).
  * **`symbol_id`**: a unique integer identifier for the symbol within the hedger's system.
  * **`price_precision`**: the number of decimal places an asset's price can be specified to.
  * **`quantity_precision`**: the number of decimal places a traded quantity can be specified to.
  * **`is_valid`**: whether the symbol is currently valid for trading.
  * **`min_acceptable_quote_value`**: the minimum quote value (in collateral tokens) accepted for a trade with this symbol.
  * **`min_acceptable_portion_lf`**: the minimum portion awarded to the liquidator on liquidation.
  * **`trading_fee`**: the fee rate charged for trading this symbol.
  * **`max_leverage`**: the maximum leverage available.
  * **`max_notional_value`**: the maximum notional value of a position.
  * **`rfq_allowed`**: whether Request for Quote (RFQ) is allowed.
  * **`max_funding_rate`**: the maximum funding rate allowed.
  * **`hedger_fee_open`**: the hedger fee for opening a position with this symbol.
  * **`hedger_fee_close`**: the hedger fee for closing a position with this symbol.

**Output schema:**

```json
{
  "count": int,
  "symbols": [
    {
      "name": "",
      "symbol": "",
      "asset": "",
      "symbol_id": int,
      "price_precision": int,
      "quantity_precision": int,
      "is_valid": bool,
      "min_acceptable_quote_value": int,
      "min_acceptable_portion_lf": float,
      "trading_fee": float,
      "max_leverage": int,
      "max_notional_value": int,
      "rfq_allowed": bool,
      "max_funding_rate": int,
      "hedger_fee_open": "",
      "hedger_fee_close": ""
    },
  ]
}
```

#### GET `open-interest`

Shows the cumulative open interest available to hedgers across all trading symbols, plus the used capacity.

```
api/open-interest/
```

**Returns:**

* **`total_cap`**: the total open interest cap.
* **`used`**: how much is currently in use.

**Output schema:**

```json
{ 
  total_cap: float, 
  used: float 
}
```

The `getOpenInterest` function for this query is in the core package at `src/state/hedger/thunks.ts`.

#### GET `notional_cap`

Shows the notional cap of a symbol provided by the hedger, which represents the value of outstanding positions not yet settled for that symbol.

```
api/notional_cap/${symbolId}/
```

**Parameters:**

* **`symbolId`**: the symbol ID of the pair (e.g. for `BTCUSDT`, `symbolId` = 1).

**Returns:**

* **`total_cap`**: the total open interest cap.
* **`used`**: how much is currently in use.

**Output schema:**

```json
{ 
total_cap: float, 
used: float 
}
```

#### GET `price-range`

Provides the maximum and minimum prices at which `partyA` can place a limit order for a pair, ensuring execution by the hedger.

```
api/price-range/${symbol}
```

**Parameters:**

* **`symbol`**: the symbol (e.g. BTCUSDT).

**Returns:**

* **`min_price`**: the minimum price for limit orders.
* **`max_price`**: the maximum price for limit orders.

**Output schema:**

```json
{ 
min_price: float,
max_price: float
}
```

#### GET `error_codes`

Fetches a list of potential error labels and their associated error codes that may arise while interacting with the hedger, which helps interpret the error codes returned when querying a position state. Leaving `error_code` blank returns all error codes.

```javascript
api/error_codes/{error_code}
```

**Parameters:**

**`error_code`**: the error code to query.

**Returns:**

```
{
"error_code":"reason"
}
```

#### GET `get_locked_params`

Returns values related to collateral requirements for a pair, based on the parameters provided. These are used when opening the quote.

```plaintext
api/get_locked_params/${symbol}?leverage=${leverage}
```

**Parameters:**

* **`symbol`**: the trading symbol (e.g. BTCUSDT).
* **`leverage`**: the leverage applied to the position, which affects the collateral calculation.

**Returns:**

* **`cva`**: Credit Valuation Adjustment. Either `partyA` (the user) or `partyB` (the hedger) can be liquidated, and `cva` is the penalty the liquidated side pays to the other.
* **`partyAmm`**: Maintenance Margin for `partyA`. The amount actually behind the position, considered in liquidation.
* **`lf`**: Liquidation Fee, awarded to the liquidator that liquidates the position.
* **`leverage`**: the leverage applied to the position.

**Output schema:**

```json
{
  cva: '',
  partyAmm: '',
  lf: '',
  leverage: '',
  partyBmm: ''
}
```

#### GET `get_market_info`

Retrieves market information for whitelisted trading pairs. It requires your frontend's AccountManager (affiliate) contract address as an endpoint parameter.

`api/get_market_info/`

**Returns:**

* **`price`**: the current price of the symbol.
* **`price_change_percent`**: the percentage change in price over the last 24 hours.
* **`trade_volume`**: the total trading volume in USD over the last 24 hours.
* **`notional_cap`**: the notional cap of the symbol.

**Output schema:**

```json
{
  "<Symbol>": {
    "price": float,
    "price_change_percent": float,
    "trade_volume": float,
    "notional_cap": float
    }
}
```

This query is used in the core package at `src/state/hedger/thunks.ts`, mostly to display data on the [Market Information](https://cloverfield.exchange/markets) page.

#### GET `partyA_upnl`

Returns the unrealized profit and loss for a `partyA`.

`/partyA_upnl/{address}`

**Parameters:**

**`address`**: the sub-account address to query.

**Returns:**

The current uPnL for the user, as a float.

#### GET `get_balance_info`

Retrieves balance information for a given address and multi-account address. It returns detailed financial data for both parties, useful for managing and analyzing account balances and their parameters.

**Endpoint**

`api/get_balance_info/${address}/${multi_account_address}`

**Parameters**

* `address`: the sub-account address.
* `multi_account_address`: the multi-account address.

**Returns**

* **party\_a**: information for party A (the user).
  * `upnl`: Unrealized Profit and Loss.
  * `notional`: notional value.
  * `timestamp`: timestamp of the data.
  * `available_balance`: available balance.
  * `allocated_balance`: allocated balance.
  * `cva`: Credit Valuation Adjustment.
  * `lf`: Liquidation Fee.
  * `party_a_mm`: Maintenance Margin for party A.
  * `party_b_mm`: Maintenance Margin for party B.
  * `pending_cva`: pending Credit Valuation Adjustment.
  * `pending_lf`: pending Liquidation Fee.
  * `pending_party_a_mm`: pending Maintenance Margin for party A.
  * `pending_party_b_mm`: pending Maintenance Margin for party B.
  * `address`: address of party A.
* **party\_b**: information for party B (the counterparty).
  * `upnl`: Unrealized Profit and Loss.
  * `notional`: notional value.
  * `timestamp`: timestamp of the data.
  * `available_balance`: available balance.
  * `allocated_balance`: allocated balance.
  * `cva`: Credit Valuation Adjustment.
  * `lf`: Liquidation Fee.
  * `party_a_mm`: Maintenance Margin for party A.
  * `party_b_mm`: Maintenance Margin for party B.
  * `pending_cva`: pending Credit Valuation Adjustment.
  * `pending_lf`: pending Liquidation Fee.
  * `pending_party_a_mm`: pending Maintenance Margin for party A.
  * `pending_party_b_mm`: pending Maintenance Margin for party B.
  * `address`: address of party B.

#### GET `get_funding_info`

Retrieves funding information for trading pairs: the next funding time and the funding rates for short and long positions for each pair.

`api/get_funding_info/`

**Parameters**

This endpoint takes no URL parameters.

**Returns**

The response contains funding information for multiple trading pairs. Each pair includes:

* `next_funding_time`: the timestamp of the next funding time.
* `next_funding_rate_short`: the next funding rate for short positions.
* `next_funding_rate_long`: the next funding rate for long positions.

**Output schema:**

```javascript
  "AEVOUSDT": {
    "next_funding_time": int,
    "next_funding_rate_short": "",
    "next_funding_rate_long": ""
  },
```

#### POST `position-state`

Returns information about the state of a position, including an `action_status` that flags any internal issue in processing the quote. Useful for surfacing errors tied to a specific position.

**Endpoint:**\
`api/position-state/${start}/${size}`

**Parameters:**

* `start` and `size` are used for pagination.

**Payload:**\
The payload for this POST request should be empty.

**Returns:**

* **`create_time`**: Unix timestamp for when the position was created.
* **`modify_time`**: Unix timestamp for the last modification of the position.
* **`counterparty_address`**: the address of the counterparty involved in the position (this is `partyA`).
* **`filled_amount_close`**: the filled amount for closing the position, or `null` if it hasn't been closed.
* **`action_status`**: the success or failure status of the action, e.g. `success` for successful processing.
* **`error_code`**: an error code if there was an issue, otherwise `null`.
* **`state_type`**: the type of state the position is in.
* **`id`**: a unique identifier for the position.
* **`quote_id`**: the identifier for the associated quote.
* **`filled_amount_open`**: the filled amount for opening the position.
* **`last_seen_action`**: the last action taken on the position.
* **`failure_type`**: the type of failure if the action wasn't successful, otherwise `null`.
* **`order_type`**: numeric code for the type of order.

**Output schema:**

```json
{
  "create_time": int,
  "modify_time": int,
  "counterparty_address": "",
  "filled_amount_close": int|null,
  "action_status": "",
  "error_code": int|null,
  "state_type": "",
  "id": "",
  "quote_id": int,
  "filled_amount_open": int,
  "last_seen_action": "",
  "failure_type": "string|null",
  "order_type": int
}
```

This API is useful for showing notifications about a user's positions. It's applied in the `getNotifications` function in the core package's `src/state/notifications/thunks.ts`.

### WebSockets

Use the hedger WebSockets to stream real-time data so the frontend shows the most current information. The most commonly used ones:

#### Funding Rate WebSocket

`api/ws/funding-rate-ws`

Streams real-time funding rates for trading symbols.

**Parameters:**

* `string[]`: an array of symbol names to get funding rate updates for (e.g. `["BTCUSDT", "ETHUSDT", "XRPUSDT"]`).

**Returns:**

* **`next_funding_time`**: when the next funding rate update will occur.
* **`next_funding_rate_short`**: the funding rate applied to short positions.
* **`next_funding_rate_long`**: the funding rate applied to long positions.

**Output schema:**

```
SYMBOL: {
    next_funding_time: int,
    next_funding_rate_short: '',
    next_funding_rate_long: ''
  },
```

The `getFundingRate` function implements this WebSocket and is in the core package at `src/state/hedger/thunks.ts`.

#### Unrealized PnL WebSocket

`api/ws/upnl-ws`

Streams real-time profit and loss for a user (this may be deprecated soon).

**Parameters:**

* **`string`**: address of the sub-account.

**Returns:**

* **`upnl`**: unrealized profit and loss for the account's open positions.
* **`notional`**: the total notional value of the account's open positions.
* **`timestamp`**: the current timestamp.
* **`available_balance`**: `(allocated_balance + unpl - (cva + lf))`
* **`allocated_balance`**: the balance allocated to the sub-account.
* **`cva`**: the penalty the liquidated entity pays to the other party.
* **`lf`**: Liquidation Fee, awarded to the liquidator that liquidates the position.
* **`party_a_mm`**: Maintenance Margin of `partyA`. The amount actually behind the position, considered in liquidation.
* **`party_b_mm`**: Maintenance Margin of `partyB`. The amount actually behind the position, considered in liquidation.
* **`pending_cva`**: the pending `cva`.
* **`pending_lf`**: the pending `lf`.
* **`pending_party_a_mm`**: the pending `mm` for `partyA`.
* **`pending_party_b_mm`**: the pending `mm` for `partyB`.

**Output schema:**

```json
{
  upnl: '',
  notional: '',
  timestamp: int,
  available_balance: '',
  allocated_balance: '',
  cva: '',
  lf: '',
  party_a_mm: '',
  party_b_mm: '',
  pending_cva: '',
  pending_lf: '',
  pending_party_a_mm: '',
  pending_party_b_mm: ''
}
```

This query is implemented in the `AccountUpdater` function in `src/state/user/AllAccountsUpdater.tsx`.

#### Notification WebSocket (position state)

`api/ws/position-state-ws3`

Streams real-time data on the state of trading positions, so the frontend can show the most current state of each.

**Parameters:**

* `address[]`: an array of counterparty addresses to get position state updates for.

**Returns:**

* `id`: identifier for the position state event.
* `create_time`: when the position was created.
* `modify_time`: when the position was last modified.
* `quote_id`: an identifier for the associated quote.
* `counterparty_address`: the address of the counterparty involved in the position.
* `filled_amount_open`: the amount filled for an open.
* `filled_amount_close`: the amount filled for a close.
* `last_seen_action`: the last action observed on the position.
* `action_status`: the status of the last action (e.g. success, failure).
* `failure_type`: the type of failure if the action was unsuccessful.
* `error_code`: the error code if applicable.
* `order_type`: the type of order associated with the position.
* `state_type`: the state type of the position (e.g. alert).
* `version`: the version of the position state data.

**Output schema:**

```json
{
    "id": "",
    "create_time": int,
    "modify_time": int,
    "quote_id": int,
    "counterparty_address": "",
    "filled_amount_open": "",
    "filled_amount_close": "",
    "last_seen_action": "",
    "action_status": "",
    "failure_type": null,
    "error_code": null,
    "order_type": int,
    "state_type": "",
    "version": int
}
```

**Input parameter:**

```json
{
  "address": ["0x0000000000000000000000000000000000000000"]
}
```


# Frontend Builder Technical Guidance

Learn how to build a trading bot using SYMMIO

## Frontend Builder Technical Guidance

### Building with the Symmio architecture

Symmio is built on a modular architecture: the Diamond Standard for upgradeable contracts, the AccountLayer for account management, and a set of specialized facets that handle trading. This section is the guide to those core components, whether you're building a frontend or a utility on top of the protocol.

{% hint style="info" %}
To build a trading bot with Symmio, see [Building a Trading Bot](/trader-documentation/building-a-trading-bot).
{% endhint %}

#### Overview

These pages explain how the Symmio ecosystem is structured and how the pieces fit together: modularity, upgradeability, and separation of concerns, and how those choices keep trading secure and efficient. What's covered:

* **Diamond explainer:** the Diamond Standard (EIP-2535) at the core of Symmio, including facets, selectors, and storage management.
* **Account creation and funding:** creating a sub-account, then depositing and allocating collateral for trading.
* **Sending and closing quotes:** opening a trade by sending a quote, and the standard close flow, including the states involved and the roles of PartyA and PartyB.
* **Instant trading:** the instant-open and instant-close variants for immediate execution, and the trade-offs against the standard flow.

Each page includes function signatures, example usage, and event descriptions.


# Introduction and Diamond Explainer

A Brief Overview of the Diamond Architecture

## Introduction and Diamond Explainer

Symmio's core contracts are organized as a **Diamond**: a single deployed proxy address that internally routes calls to a set of **facets** (logic contracts), each responsible for a slice of the protocol's functionality. The Diamond pattern is defined in [EIP-2535](https://eips.ethereum.org/EIPS/eip-2535).

There are two main Diamonds in the current (0.8.5) architecture:

* The **Symmio core diamond**: quotes, positions, funding, liquidation, settlement.
* The **AccountLayer diamond**: sub-accounts, virtual accounts, affiliates, fee distribution, hooks.

The **InstantLayer** is a separate (non-Diamond) contract that sits in front of both, providing EIP-712 batched execution and delegation.

#### Why a Diamond?

The protocol is large (quote management, funding rates, liquidation, ADL, ClearingHouse, oracle-less binding, withdrawal flows), and it wouldn't fit inside a single contract under the 24 KB EIP-170 limit. The Diamond pattern splits the logic across many smaller facets while keeping a single stable address that users and integrators interact with.

It also gives per-function upgradeability: new behavior can be added or fixed without redeploying the whole protocol or changing the address that frontends, indexers, and bots point at.

#### Facets and selectors

Each facet exposes a set of external functions. The Diamond maintains a mapping from **function selector** (the first four bytes of `keccak256(signature)`) to the facet address that implements it. When a call arrives:

1. The Diamond's fallback reads the selector from `msg.sig`.
2. It looks up which facet implements that selector.
3. It `delegatecall`s into that facet.

Because the call is a `delegatecall`, the facet code runs **in the Diamond's storage context**: facets are stateless executables that operate on the Diamond's storage. This is what lets the protocol be split across many facets while preserving a single shared state.

To see the full facet/selector mapping for a given chain, use the [Intent Inspector](https://intent.symmscan.com/inspector): paste any diamond address and you'll get a live readout of every facet, every selector, and which function sits at which selector.

#### Storage layout

Sharing storage across facets is the most error-prone part of working with diamonds. Symmio uses the **Diamond Storage** pattern (sometimes called `AppStorage`): each storage area is namespaced under a fixed slot derived from a string constant, e.g.

```solidity
bytes32 constant ACCOUNT_STORAGE_SLOT = keccak256("diamond.standard.storage.accountlayer.account");
```

Library functions resolve `slot → struct` and let facets read and write the same struct from anywhere. Because slots are derived from unique strings, two unrelated storage areas can't collide as long as the strings are unique.

Notable v0.8.5 storage areas on the AccountLayer:

| Storage contract      | Slot key                                                        | Contents                                                            |
| --------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------- |
| `AccountStorage`      | `keccak256("diamond.standard.storage.accountlayer.account")`    | SubAccount/VA data, nonces, `globalSigner`, AccountManager bytecode |
| `AffiliateStorage`    | `keccak256("diamond.standard.storage.accountlayer.affiliate")`  | Affiliate configs, fee details, hooks, operators, `hookContext`     |
| `AccountLayerStorage` | `keccak256("diamond.standard.storage.accountlayer")`            | RBAC (`hasRole`, `roleAdmins`), global pause flag                   |
| Reentrancy guard      | `keccak256("diamond.standard.storage.accountlayer.reentrancy")` | Reentrancy status flag                                              |

The Symmio core diamond has its own set of storage areas for quote state, balances, funding rates, and so on.


# Building a New Frontend

This guide walks through building a new exchange frontend on Symmio Core v0.8.5, from registering your frontend to a working trade-and-withdraw flow. It assumes you're starting fresh.

You integrate with Symmio through the AccountLayer, a single shared diamond that manages every user's accounts, handles affiliate onboarding and fee distribution, and gives each trade configurable position isolation. You register as an affiliate and build against the AccountLayer directly, rather than deploying a contract per user or per frontend.

#### The account model

Funds move through two levels before they reach a trade:

```
User → deposit → SubAccount → addMargin → VirtualAccount → trade
                 SubAccount ← auto-sweep ← VirtualAccount (when its last position closes)
```

A SubAccount is a user's account under your frontend. It's bound to your affiliate, a specific Symmio core instance, and an isolation type chosen when it's created. A user can hold several, say one for conservative trades and one for aggressive ones.

A VirtualAccount (VA) sits beneath a SubAccount and holds the margin for a trade. Each VA is an independent trading address with its own balance and positions, so a liquidation in one VA can't touch another. When a VA's last position closes, the AccountLayer sweeps its funds back to the parent SubAccount and recycles the address, so you never manage that lifecycle by hand.

Both SubAccounts and VAs are virtual addresses: deterministic, CREATE2-style addresses with no deployed contract behind them. The AccountLayer tracks all their state centrally.

#### Isolation types

When a SubAccount is created, its isolation type decides how VAs are created during trading. For most retail UIs, POSITION is the safest default.

Under MARKET, each quote opens a new VA for that market by default; enabling Single VA Mode routes later quotes for the same market into the existing VA instead. You can fix the isolation type per product or expose it as a user choice.

Note there are two related enums. The SubAccount's type uses `SubAccountIsolationType` (`POSITION`, `MARKET`, `MARKET_DIRECTION`, `CUSTOM`). When you target a specific VA in the margin calls below, you use `VirtualAccountIsolationType` (`POSITION`, `MARKET`, `MARKET_LONG`, `MARKET_SHORT`). `MARKET_DIRECTION` at the SubAccount level splits into `MARKET_LONG` and `MARKET_SHORT` at the VA level.

#### Step 1: Register as an affiliate

Every frontend trades through an affiliate registration. It's a two-step process:

1. Request registration with your frontend name, brand color, admin address, fee stakeholder configuration, and the Symmio core instance(s) you'll use.
2. A protocol `APPROVER_ROLE` holder approves it. On approval, a fee distributor address is generated and your affiliate is registered on each allowed core.

Once your affiliate is `ACTIVE`, users can create SubAccounts under it. Until then, account creation fails.

#### Step 2: Create an account

A new user starts by creating a SubAccount with your chosen isolation type. `createSubAccounts` takes the affiliate address plus an **array of `SubAccountCreationData` structs**. The Symmio core, isolation type, name, metadata, and single-VA flag are *fields of the struct*, not positional arguments:

```python
# SubAccountCreationData = (name, metadata, symmioCore, isolationType, singleVAMode)
# isolationType uses SubAccountIsolationType: 0=POSITION, 1=MARKET, 2=MARKET_DIRECTION, 3=CUSTOM
sub_account_data = [(
    "My Account",          # name
    b"",                   # metadata
    symmio_core_address,   # symmioCore
    0,                     # isolationType (POSITION)
    False,                 # singleVAMode
)]

account_layer.functions.createSubAccounts(
    affiliate_address,
    sub_account_data,      # array, you can create several at once
).transact({"from": user})
```

`createSubAccounts` returns the deterministic addresses of the created SubAccounts. If your UI lets users pick an isolation type, explain the trade-off briefly: more isolation means more safety per position but a little more overhead per trade.

#### Step 3: Deposit and add margin

Depositing happens in two moves. First, collateral goes into the SubAccount:

```python
# Deposit into the SubAccount balance
account_layer.functions.depositForAccount(sub_account, amount).transact({"from": user})

# Or deposit and allocate in a single call
account_layer.functions.depositAndAllocateForAccount(sub_account, amount).transact({"from": user})
```

Then, before a trade on a non-CUSTOM SubAccount, the user funds the VA that will hold the position. For a brand-new trade you fund the *next* VA; the AccountLayer derives which VA that is from the isolation type and symbol, so you don't pass a VA address. The VA isolation type uses `VirtualAccountIsolationType` (`POSITION`, `MARKET`, `MARKET_LONG`, `MARKET_SHORT`):

```python
va_isolation_type = 0  # VirtualAccountIsolationType.POSITION
symbol_id = 1

# Optional: predict the address purely for display/indexing
next_va = view_facet.functions.predictNextVirtualAccountAddress(
    sub_account, va_isolation_type, symbol_id
).call()

# Fund the next VA: pass isolation type + symbolId, NOT a VA address
margin_facet.functions.addMarginToNextVA(
    sub_account, va_isolation_type, symbol_id, amount
).transact({"from": user})
```

To top up a VA that already exists, use `addMargin` with its address:

```python
margin_facet.functions.addMargin(virtual_account, amount).transact({"from": user})
```

Margin can also be pulled back manually with `removeMargin(virtual_account, amount, upnl_sig)`, which requires a UPNL (`SingleUpnlSig`) signature proving the VA stays solvent.

#### Step 4: Trade

Trades run through `_call()` on the AccountLayer. You encode the Symmio core call and pass it through; the AccountLayer routes it according to the SubAccount's isolation type:

```python
send_quote_calldata = symmio_core.encodeABI(
    fn_name="sendQuoteWithAffiliateAndData",
    args=[...],
)
account_layer.functions._call(sub_account, [send_quote_calldata]).transact({"from": user})
```

On a non-CUSTOM SubAccount, the AccountLayer creates or reuses a VA, routes the quote through it, and records which quoteId belongs to which VA. On a CUSTOM SubAccount, the call executes directly with no VA involved.

#### Step 5: Virtual account cleanup (automatic)

You don't manage VA lifecycle for non-CUSTOM isolation. The flow:

**Creation:** a VA is created when the first quote is sent (or reused, under Single VA Mode).

**Tracking:** the AccountLayer keeps a list of which quoteIds live in which VA.

**Cleanup:** when a position closes, Symmio core notifies the AccountLayer. If the VA has no remaining quoteIds, its margin is deallocated, funds return to the parent SubAccount, and the VA address is recycled.

Listen to `VirtualAccountCreated`, `VirtualAccountDeleted`, and `VirtualAccountReused` to keep your UI in sync.

#### Step 6: Withdrawals

Withdrawals are a two-step flow: initiate a request, wait out the cooldown (a configurable `withdrawCooldownPeriod`, measured from the user's last deallocation), then finalize. A request is made up of one or more parts, each of which can optionally route through an express or virtual provider; for a plain same-chain withdrawal, leave both as the zero address. Note `chainId` is an `int256` in the part struct.

```python
import time
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"

parts = [{
    "id": 1,
    "amount": withdraw_amount,
    "chainId": current_chain_id,      # int256
    "receiver": user_address,         # bytes, 20 bytes for EVM
    "virtualProvider": ZERO_ADDRESS,  # no cross-chain
    "expressProvider": ZERO_ADDRESS,  # no instant
}]

tx = withdraw_facet.functions.initiateWithdraw(parts, False, b"").transact({"from": user})
receipt = w3.eth.wait_for_transaction_receipt(tx)

event = withdraw_facet.events.WithdrawInitiated().process_receipt(receipt)[0]
request_id = event["args"]["requestId"]
cooldown_end_time = event["args"]["cooldownEndTime"]

while time.time() < cooldown_end_time:
    time.sleep(10)

withdraw_facet.functions.finalizeWithdrawRequest(user_address, request_id).transact({"from": user})
```

For your UI, `getWithdrawableTime(user)` gives the countdown to the next finalizable withdrawal, and `getPendingWithdrawRequests(user, start, size)` lists active requests. Show status as it moves from `PENDING` to `PROVIDER_ACCEPTED` to `COMPLETED`. Cancellation is via `requestCancelWithdraw(requestId)`; express and virtual withdrawals inside the provider blackout window move to `CANCEL_REQUESTED` and need provider approval rather than cancelling immediately.

#### Event indexing

Point your indexer at two addresses, the Symmio core diamond and the AccountLayer diamond, and index against the current v0.8.5 event signatures. The essentials for a working UI:

**Account lifecycle (AccountLayer):**

* `SubAccountCreated`, `SubAccountDeleted`
* `VirtualAccountCreated`, `VirtualAccountDeleted`, `VirtualAccountReused`
* `AddMargin`, `RemoveMargin`

**Trading (Symmio core):**

* `SendQuote(address partyA, uint256 quoteId, address[] partyBsWhiteList, address affiliate, bytes paramsData, bytes data)`: quote params are encoded into `paramsData`; decode with `abi.decode(paramsData, (uint256, uint8, uint8, uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256))`, which maps to `(symbolId, positionType, orderType, price, marketPrice, quantity, cva, lf, partyAmm, partyBmm, tradingFee, deadline)`.
* `OpenPosition`, `FillCloseRequest` (carries a `closeId`)
* Liquidation events carry a `liquidationId` (a `bytes` value) you can use to correlate multi-step flows.

**Withdrawals (Symmio core):**

* `WithdrawInitiated` (carries `cooldownEndTime`), `WithdrawAccepted`, `WithdrawFinalized`
* `WithdrawCancelRequested`, `WithdrawCancelled`, plus `WithdrawSuspended` / `WithdrawRejected` / `WithdrawSpeedUpAccepted`

**Fees and funding (Symmio core):**

* `TradingFeeCharged`, `TradeVolumeRecorded`
* `SetLongFundingFee`, `SetShortFundingFee`, `UpdateAccumulatedFundingFee`, `ChargeAccumulatedFundingFee`

Funding rates are tracked as weighted averages at the symbol level (pre-multiplied by market price at storage time), so displaying a user's funding debt is a per-symbol lookup rather than a walk over every open position.


# Migrating from MultiAccount (0.8.4)

This guide is for frontends already running on Symmio Core v0.8.4 and moving to v0.8.5. It covers what changed, the paths available to you, and the concrete steps and event updates each one needs. You don't have to move all at once: a backward-compatible wrapper lets you get onto v0.8.5 quickly and migrate fully on your own schedule.

#### What v0.8.5 introduces

The changes that affect frontends fall into six areas:

* **AccountLayer:** one Diamond proxy in place of per-frontend MultiAccounts, with SubAccounts, VirtualAccounts, and four isolation modes.
* **New Withdraw System:** multi-provider, multi-chain withdrawals in place of the single-step flow, with optional express (instant) and virtual (cross-chain) providers.
* **Instant Layer:** EIP-712 signed operations in place of delegateAccess, with templates for atomic multi-step trades.
* **Express Deposits:** affiliate-configurable deposit splitting that builds a liquidity pool for faster withdrawals.
* **Hook System:** on-chain callbacks on position open, close, cancel, and fee events for custom logic without core changes.

You have two paths, plus an optional account-import step.

#### Option A: Migrate to the AccountLayer (recommended)

Your frontend talks to SubAccounts and VirtualAccounts directly and gains the full feature set: position isolation, the affiliate fee system, express deposits, hooks, and the new withdrawal flow. Future protocol upgrades then happen centrally, with no more per-frontend redeployments.

**The account hierarchy**

The AccountLayer adds a two-level structure:

```
User (EOA)
├── SubAccount (1): bound to affiliate + Symmio core + isolation type
│   ├── VirtualAccount (11)
│   └── VirtualAccount (12)
└── SubAccount (2)
    ├── VirtualAccount (21)
    └── VirtualAccount (22)
```

A SubAccount is the user's top-level unit, bound to your affiliate, a Symmio core, and an isolation type. A VirtualAccount (VA) sits beneath it and holds the margin for a trade: an independent address with its own balance and positions, so a liquidation in one VA can't reach another. When a VA's last position closes, its funds sweep back to the parent SubAccount and the address is recycled. Both are virtual (CREATE2-style) addresses with no deployed contract; the AccountLayer holds their state.

**Two isolation enums (don't conflate them)**

There are **two** isolation enums in the AccountLayer, and several functions below take the *VirtualAccount* one rather than the *SubAccount* one:

* `SubAccountIsolationType`: set when a SubAccount is created. `POSITION (0)`, `MARKET (1)`, `MARKET_DIRECTION (2)`, `CUSTOM (3)`.
* `VirtualAccountIsolationType`: used by the margin and predict calls when targeting a specific VA. `POSITION`, `MARKET`, `MARKET_LONG`, `MARKET_SHORT`. `MARKET_DIRECTION` at the SubAccount level splits into `MARKET_LONG` and `MARKET_SHORT` at the VA level.

**Choosing an isolation type**

The SubAccount isolation type, set at creation, governs how VAs are created during trading:

* **POSITION:** one VA per trade. Maximum isolation; a liquidation on one position can't affect another. The safest default for retail UIs.
* **MARKET:** one VA per symbol. By default each quote opens a new VA for that market; enable Single VA Mode to route later quotes for the same market into the existing VA.
* **MARKET\_DIRECTION:** one VA per symbol and direction (BTC longs separate from BTC shorts).
* **CUSTOM:** no automatic VA creation; trades run directly through the SubAccount or through manually created VAs. Closest to old MultiAccount behavior.

**Step 1: Account creation**

Replace your old create-account flow. `createSubAccounts` takes the affiliate address plus an **array of `SubAccountCreationData` structs**. The Symmio core, isolation type, name, metadata, and single-VA flag are *fields of the struct*, not positional arguments.

```python
# Old: MultiAccount
multi_account.functions.addAccount("My Account").transact({"from": user})

# New: AccountLayer, create one or more SubAccounts.
# SubAccountCreationData = (name, metadata, symmioCore, isolationType, singleVAMode)
# isolationType uses SubAccountIsolationType: 0=POSITION, 1=MARKET, 2=MARKET_DIRECTION, 3=CUSTOM
sub_account_data = [(
    "My Account",          # name
    b"",                   # metadata
    symmio_core_address,   # symmioCore
    0,                     # isolationType (POSITION)
    False,                 # singleVAMode
)]

account_layer.functions.createSubAccounts(
    affiliate_address,
    sub_account_data,      # array, create multiple at once if you like
).transact({"from": user})
```

`createSubAccounts` returns the deterministic addresses of the created SubAccounts.

**Step 2: Deposit and add margin**

Funding now has an extra level. Collateral first goes into the SubAccount.

```python
account_layer.functions.depositForAccount(sub_account, amount).transact({"from": user})

# or, deposit and allocate together:
account_layer.functions.depositAndAllocateForAccount(sub_account, amount).transact({"from": user})
```

Then, before a trade on a non-CUSTOM SubAccount, fund the target VA. For a new trade you fund the *next* VA, and the AccountLayer derives which VA that is from the isolation type and symbol, so you don't pass a VA address. The VA isolation type uses `VirtualAccountIsolationType` (`POSITION`, `MARKET`, `MARKET_LONG`, `MARKET_SHORT`):

```python
va_isolation_type = 0  # VirtualAccountIsolationType.POSITION
symbol_id = 1

# Optional: predict the address purely for display/indexing
next_va = view_facet.functions.predictNextVirtualAccountAddress(
    sub_account, va_isolation_type, symbol_id
).call()

# Fund the next VA: pass isolation type + symbolId, NOT a VA address
margin_facet.functions.addMarginToNextVA(
    sub_account, va_isolation_type, symbol_id, amount
).transact({"from": user})
```

To top up an existing VA, use `addMargin(virtual_account, amount)`.

**Step 3: Trade execution**

Trades go through `_call()`, which routes based on isolation type:

```python
send_quote_calldata = symmio_core.encodeABI(fn_name="sendQuoteWithAffiliateAndData", args=[...])
account_layer.functions._call(sub_account, [send_quote_calldata]).transact({"from": user})
```

On a non-CUSTOM SubAccount the AccountLayer creates or reuses a VA, routes the quote, and tracks the quoteId. On CUSTOM it executes directly.

**Step 4: VA cleanup is automatic**

You don't manage VA lifecycle for non-CUSTOM isolation. A VA is created on the first quote, the AccountLayer tracks which quoteIds belong to it, and when a position closes with no quoteIds left the margin is deallocated, funds return to the SubAccount, and the address is recycled. Listen to `VirtualAccountCreated`, `VirtualAccountDeleted`, and `VirtualAccountReused` to keep your UI current.

**Step 5: Withdrawals**

The single-step withdraw is deprecated. It remains callable until the protocol sets the `legacyWithdrawalDeprecated` flag, after which it reverts; new integrations should use the initiate/finalize flow.

```python
import time

ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"

parts = [{
    "id": 1,
    "amount": withdraw_amount,
    "chainId": current_chain_id,        # int256
    "receiver": user_address,           # bytes — 20 bytes for an EVM address
    "virtualProvider": ZERO_ADDRESS,    # no cross-chain
    "expressProvider": ZERO_ADDRESS,    # no instant
}]

# initiateWithdraw(parts, speedUp, data)
tx = withdraw_facet.functions.initiateWithdraw(parts, False, b"").transact({"from": user})
receipt = w3.eth.wait_for_transaction_receipt(tx)

event = withdraw_facet.events.WithdrawInitiated().process_receipt(receipt)[0]
request_id = event["args"]["requestId"]
cooldown_end_time = event["args"]["cooldownEndTime"]  # based on last deallocation, not initiation

while time.time() < cooldown_end_time:
    time.sleep(10)

withdraw_facet.functions.finalizeWithdrawRequest(user_address, request_id).transact({"from": user})
```

To offer instant withdrawals, set a part's `expressProvider` to a registered provider: it fronts the funds immediately and SYMMIO reimburses it after the cooldown. For your UI, show status (`PENDING` → `PROVIDER_ACCEPTED` → `COMPLETED`), use `getWithdrawableTime(user)` for the countdown, and `getPendingWithdrawRequests(user, start, size)` for the list.&#x20;

> Cancellation is `requestCancelWithdraw(requestId)`. A request still in `PENDING` cancels outright. Once a provider has accepted the request (`PROVIDER_ACCEPTED`), behavior splits: an express withdrawal moves to `CANCEL_REQUESTED` and needs provider approval, while a pure-virtual (cross-chain) withdrawal cancels outright, with the provider notified. Pure-virtual withdrawals are additionally subject to a cancel blackout window: they can only be cancelled while more than the configured blackout period remains before `cooldownEndTime`  inside the window the call reverts and the withdrawal proceeds to completion.

#### Option B: Use the AccountManager wrapper (backward-compatible)

When your affiliate registers, an AccountManager proxy is deployed for you automatically. It exposes nearly the same API as the old MultiAccount (`addAccount`, `depositForAccount`, `withdrawFromAccount`, `_call`, `getAccounts`), so in most cases you swap a contract address and update event indexing. Internally it authenticates the caller (by temporarily setting globalSigner on the AccountLayer) and proxies the call through. It creates SubAccounts with CUSTOM isolation, which behaves like the old MultiAccount: no automatic VA creation, trades execute directly through the SubAccount.

**Step 1: Register as an affiliate**

Registration is two steps: you request with your frontend name, brand color, admin address, fee stakeholder configuration, and the Symmio core instance(s) you'll use; then a protocol `APPROVER_ROLE` holder approves it. On approval, an AccountManager proxy is deployed (deterministic CREATE2), a fee distributor address is generated, and your affiliate is registered on each allowed core. Your affiliate is then `ACTIVE` and users can create accounts under it.

**Step 2: Swap the contract address**

Replace the old MultiAccount address with the AccountManager address. The function surface matches:

```python
from web3 import Web3
w3 = Web3(Web3.HTTPProvider(RPC_URL))

# Old: MultiAccount
multi_account = w3.eth.contract(address=OLD_MULTI_ACCOUNT_ADDRESS, abi=multi_account_abi)

# New: AccountManager (same ABI for core functions)
account_manager = w3.eth.contract(address=ACCOUNT_MANAGER_ADDRESS, abi=account_manager_abi)

# These calls work identically:
account_manager.functions.addAccount("My Account").transact({"from": user})
account_manager.functions.depositForAccount(account_address, amount).transact({"from": user})
account_manager.functions.depositAndAllocateForAccount(account_address, amount).transact({"from": user})
account_manager.functions.withdrawFromAccount(account_address, amount).transact({"from": user})
account_manager.functions._call(account_address, [encoded_call_data]).transact({"from": user})
account_manager.functions.getAccounts(user_address, 0, 10).call()
```

**Step 3: Import existing accounts (optional)**

Users with old MultiAccount accounts can bring them across. Imports are **batched** through `importLegacyAccounts` (plural): you pass the legacy contract, the affiliate, the list of Symmio cores, and an array of `LegacyAccountImportData` structs. Each struct is `(account, name, coreIndex)`, where `coreIndex` points into the `symmioCores` array.

```python
# LegacyAccountImportData = (account, name, coreIndex)
accounts_data = [(
    legacy_account_address,   # account
    "My Account",             # name
    0,                        # coreIndex -> symmio_cores[0]
)]

account_layer.functions.importLegacyAccounts(
    legacy_multi_account_contract,  # legacyContract
    affiliate_address,              # affiliate
    [symmio_core_address],          # symmioCores
    accounts_data,                  # LegacyAccountImportData[]
).transact({"from": user})
```

This creates a SubAccount with CUSTOM isolation, preserving the original address and ownership.

#### Instant Layer

The Instant Layer replaces `delegateAccess`. Instead of granting a PartyB broad, indefinite permission, the user signs a specific operation (EIP-712) that the PartyB submits alongside its own. This closes the gap where a quote took a few seconds to reach PartyB through chain events, without the trust problem of open-ended delegation.

Read more about the Instant Layer's implementation.

**Event changes:** see the Instant Layer documentation.

#### Hooks and fees

Two AccountLayer features worth adopting once the core migration is done.

Hooks are on-chain callbacks on every position open, close, cancel, expiry, fee charge, and liquidation settlement: useful for campaign tracking, rebates from `onFeeCharged`, loyalty NFTs, or analytics. After approval, a protocol admin (holding `INTEGRATION_ADMIN_ROLE`) registers your hook with `control_facet.functions.registerHook(affiliate_address, hook_contract)`, and your contract implements `ISymmioHook`. Because `ISymmioHook` is an interface, you must implement **all** of its callbacks: `onOpenPosition`, `onClosePosition`, `onCancelQuote`, `onCloseExpired`, `onFeeCharged`, and `onLiquidationSettled` (leave the ones you don't need as no-ops). Keep them lean: a reverting hook blocks the operation it's attached to, hooks receive all remaining gas, and both your hook and the system-wide hook fire on every event except liquidation settlement: `onLiquidationSettled` carries no quote context to resolve an affiliate, so only the system-wide hook receives it.

Affiliate fees resolve by specificity (per user + symbol, then per user, then per symbol, then your affiliate-wide default, and finally the symbol's default trading fee if no affiliate fee is set), with open and close fees set independently via `setAffiliateFee` and `setAffiliateFeeForUser` on the ControlFacet. Collected fees split among the stakeholders configured at registration, whose shares (including `symmioShare`) must sum to exactly `1e18`.

#### Event changes

Point your indexer at two addresses, the Symmio core diamond and the AccountLayer diamond, and index against the current v0.8.5 event signatures. The essentials for a working UI:

**Account lifecycle (AccountLayer):**

* `SubAccountCreated`, `SubAccountDeleted`
* `VirtualAccountCreated`, `VirtualAccountDeleted`, `VirtualAccountReused`
* `AddMargin`, `RemoveMargin`

**Trading (Symmio core):**

* `SendQuote(address partyA, uint256 quoteId, address[] partyBsWhiteList, address affiliate, bytes paramsData, bytes data)`: quote params are encoded into `paramsData`; decode with `abi.decode(paramsData, (uint256, uint8, uint8, uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256, uint256))`, which maps to `(symbolId, positionType, orderType, price, marketPrice, quantity, cva, lf, partyAmm, partyBmm, tradingFee, deadline)`.
* `OpenPosition`, `FillCloseRequest` (carries a `closeId`)
* Liquidation events carry a `liquidationId` (a `bytes` value) you can use to correlate multi-step flows.

**Withdrawals (Symmio core):**

* `WithdrawInitiated` (carries `cooldownEndTime`), `WithdrawAccepted`, `WithdrawFinalized`
* `WithdrawCancelRequested`, `WithdrawCancelled`, plus `WithdrawSuspended` / `WithdrawRejected` / `WithdrawSpeedUpAccepted`

**Fees and funding (Symmio core):**

* `TradingFeeCharged`, `TradeVolumeRecorded`
* `SetLongFundingFee`, `SetShortFundingFee`, `UpdateAccumulatedFundingFee`, `ChargeAccumulatedFundingFee`


# Querying Info from the SYMMIO Diamond

Symmio is built on the [EIP-2535 Diamond Standard](https://eips.ethereum.org/EIPS/eip-2535), so the protocol's read methods are spread across many facets behind a single address. Calling them through a generic block explorer is painful: the explorer doesn't know which facet implements which selector, and the ABI surface is large. The [SYMMIO Intent Inspector](https://intent.symmscan.com/inspector) is built for this. It reads the live facet and selector map per chain, gives you a typed form for every read function, and decodes the results into something legible.

{% embed url="<https://intent.symmscan.com/inspector>" %}

{% hint style="info" %}
The Inspector works against any SYMMIO diamond. The Symmio core, the AccountLayer, and the InstantLayer are all browsable; just pick the chain and paste the diamond address you want to inspect.
{% endhint %}

#### Opening the Inspector

Go to [intent.symmscan.com/inspector](https://intent.symmscan.com/inspector), pick the chain you want to query (the Inspector supports every chain Symmio is deployed on), then paste the diamond address, or use one of the suggested defaults for that chain.

The URL pattern is human-readable too, so you can deep-link straight to a specific function call:

```
https://intent.symmscan.com/inspector/<chain>/diamond
https://intent.symmscan.com/inspector/<chain>/diamond?method=<selector>
```

#### Browsing facets and selectors

Once a diamond is loaded, the Inspector shows every facet registered on it, along with every function selector that facet implements. This is the source of truth for whether a given function is live on a given chain: if a selector is missing from the list, the facet hasn't been deployed (or hasn't been cut into the diamond) on that chain.

#### Calling a view function

Click any read function (`view` or `pure`) and the Inspector builds a typed input form for its parameters. Fill in the inputs, hit "call", and the result comes back decoded.

Some common reads:

<table><thead><tr><th width="323.66668701171875">Function</th><th width="145.6666259765625">Diamond</th><th>What you get</th></tr></thead><tbody><tr><td><code>getUserSubAccountsAddresses(owner, offset, limit)</code></td><td>AccountLayer</td><td>All SubAccounts owned by an EOA</td></tr><tr><td><code>getVirtualAccountsAddressesOfSubAccount(sub, offset, limit)</code></td><td>AccountLayer</td><td>Currently-active VAs on a SubAccount</td></tr><tr><td><code>getVirtualAccountQuoteIds(va, offset, limit)</code></td><td>AccountLayer</td><td>Open quote IDs the AccountLayer is tracking on a VA</td></tr><tr><td><code>predictNextVirtualAccountAddress(sub, isolation, symbolId)</code></td><td>AccountLayer</td><td>Deterministic next-VA address; use when computing <code>addMarginToNextVA</code> calldata</td></tr><tr><td><code>getActiveVAByKey(sub, isolation, symbolId)</code></td><td>AccountLayer</td><td>The live VA for <code>(sub, isolation, symbol)</code>, or <code>0x0</code> if none</td></tr><tr><td><code>getBindState(account)</code></td><td>Symmio core</td><td><code>(status, partyB, timestamp)</code>; <code>status == 1</code> means bound</td></tr><tr><td><code>getQuote(quoteId)</code></td><td>Symmio core</td><td>The full quote struct (size, price, locked margins, state, etc.)</td></tr></tbody></table>

For the full set of read functions on each diamond, see the facet listing.

#### Decoding calldata

If you have a raw `0x…` blob, from a failed transaction, a log, or an EIP-712 `SignedOperation`'s `callData` field, the Inspector can decode it as long as the leading selector matches one in the diamond's selector map. Paste the calldata, pick the diamond, and the Inspector resolves it to a function name and pretty-prints the arguments.

This is the fastest way to check an Instant Layer `SignedOperation`: sign one, paste the `callData` into the decoder, and confirm it calls what you expect before posting to the solver.

#### Reading bind state for an account

When you're wiring up the Instant Layer flow, the most common reason `sendQuote` reverts is that the SubAccount isn't bound to a PartyB. Bind state lives on the Symmio core, not the AccountLayer, so:

1. Open the Symmio core diamond in the Inspector.
2. Call `getBindState(<subAccount>)`.
3. Expect `status = 1` and `partyB = <your hedger>`. Anything else means `bindToPartyB` hasn't landed, or landed on a different SubAccount.

#### Reading a quote's state

Given a `quoteId` from the hedger's `SendQuoteTransaction` notification, call `getQuote(quoteId)` on the Symmio core to see the full quote struct: price, quantity, filled amounts, locked margins, state (`PENDING`, `LOCKED`, `OPEN`, `CLOSE_PENDING`, `CLOSED`, and so on), the bound PartyB, and the affiliate.


# Creating an Account and Depositing Funds

In Symmio v0.8.5 a user's trading capital lives in a **SubAccount** created on the **AccountLayer**, a single shared diamond that manages every frontend's accounts. You no longer deploy your own account contract: you register as an affiliate, and users create SubAccounts under your affiliate on the shared AccountLayer.

SubAccounts and the VirtualAccounts beneath them are virtual (CREATE2-style) addresses with no deployed bytecode; the AccountLayer holds their state. For the full account model and the isolation types, see Building a New Frontend.

#### Step 1: Create a SubAccount

`createSubAccounts(affiliate, accountsData[])` creates one or more SubAccounts under your affiliate. The Symmio core, isolation type, name, metadata, and single-VA flag are *fields of the `SubAccountCreationData` struct*, not positional arguments:

```python
# SubAccountCreationData = (name, metadata, symmioCore, isolationType, singleVAMode)
# isolationType uses SubAccountIsolationType: 0=POSITION, 1=MARKET, 2=MARKET_DIRECTION, 3=CUSTOM
sub_account_data = [(
    "TradingAccount",      # name
    b"",                   # metadata
    symmio_core_address,   # symmioCore
    0,                     # isolationType (POSITION)
    False,                 # singleVAMode
)]

account_layer.functions.createSubAccounts(
    affiliate_address,
    sub_account_data,      # array, create several at once if you like
).transact({"from": user})
```

`createSubAccounts` returns the deterministic addresses of the created SubAccounts and emits `SubAccountCreated` for each. To list a user's SubAccounts later, call `getUserSubAccountsAddresses(user, start, size)` on the AccountLayer.

{% hint style="info" %}
Migrating an existing MultiAccount frontend? The **AccountManager** wrapper exposes the old API (`addAccount`, `getAccounts`, `_call`) and creates CUSTOM-isolation SubAccounts that behave like the old model, so you can swap a contract address and migrate fully later. See Upgrading from MultiAccount (0.8.4).
{% endhint %}

#### Step 2: Deposit collateral

Collateral goes into the SubAccount on the AccountLayer. You can deposit alone, or deposit and allocate in a single call:

```python
# Deposit into the SubAccount balance
account_layer.functions.depositForAccount(sub_account, amount).transact({"from": user})

# Or deposit and allocate in one call
account_layer.functions.depositAndAllocateForAccount(sub_account, amount).transact({"from": user})
```

The actual ERC20 `transferFrom` happens inside the Symmio Core diamond, so **Symmio Core** is the address that needs the collateral allowance, not the AccountLayer. Approve it once before the first deposit:

```python
collateral.functions.approve(symmio_core_address, 2**256 - 1).transact({"from": user})
```

`amount` is in the token's native decimals; Symmio converts to 18-decimal fixed-point internally, so the same code works for 6-decimal USDC and 18-decimal stablecoins. Read the collateral token address from `getCollateral()` on the Symmio Core view facet and its decimals from the token's `decimals()`. On success you'll see `DepositForAccount`, plus `AllocateForAccount` if you allocated.

#### Step 3: Fund the VirtualAccount before trading

On a non-CUSTOM SubAccount the margin for a trade lives in a **VirtualAccount (VA)** beneath the SubAccount, not in the SubAccount itself. Before the first trade you fund the *next* VA; the AccountLayer derives which VA that is from the isolation type and symbol, so you don't pass a VA address. The VA isolation type uses `VirtualAccountIsolationType` (`POSITION`, `MARKET`, `MARKET_LONG`, `MARKET_SHORT`):

```python
va_isolation_type = 0  # VirtualAccountIsolationType.POSITION
symbol_id = 1

margin_facet.functions.addMarginToNextVA(
    sub_account, va_isolation_type, symbol_id, amount
).transact({"from": user})
```

To top up a VA that already exists, use `addMargin(virtual_account, amount)`. On a CUSTOM SubAccount there's no automatic VA: trades run directly through the SubAccount, like the old MultiAccount model.

Once the SubAccount is funded and the VA has margin, you're ready to send a quote.


# Sending a Quote

Sending a quote is a core action on the SYMM platform. It lets PartyA signal an intent to trade by submitting a detailed quote request. For the quote to be accepted and opened by a hedger (PartyB), follow the steps below and provide accurate parameters. Debugging is harder when the call is forwarded through the AccountLayer's `_call`, since the transaction can fail for a number of reasons.

{% hint style="info" %}
In v0.8.5 the quote is encoded and forwarded through the **AccountLayer's** `_call`, not a per-frontend MultiAccount. The current function is `sendQuoteWithAffiliateAndData()`, which takes the parameters below plus a trailing `data` bytes field (an off-chain correlation ID); the AccountLayer routes the quote to the right VirtualAccount. For the exact 0.8.5 encoder see Building a New Frontend, and for a wallet-free, session-key-signed version see the Instant Layer flow.
{% endhint %}

**Function signature**

```solidity
function sendQuoteWithAffiliate(
    address[] memory partyBsWhiteList,
    uint256 symbolId,
    PositionType positionType,
    OrderType orderType,
    uint256 price,
    uint256 quantity,
    uint256 cva,
    uint256 lf,
    uint256 partyAmm,
    uint256 partyBmm,
    uint256 maxFundingRate,
    uint256 deadline,
    address affiliate,
    SingleUpnlAndPriceSig memory upnlSig
) external returns (uint256);
```

### Parameter breakdown

#### `partyBsWhiteList[]`

An array of PartyB addresses (hedgers/solvers) that PartyA is willing to trade with.

```javascript
const partyBsWhiteList = [solverAddress1, solverAddress2];
```

#### `symbolId`

The identifier for the trading symbol.

{% hint style="info" %}
Use `getSymbols()` on the SYMM Diamond to see available symbols, or query a solver's supported symbols from the contract-symbols endpoint (RASA).
{% endhint %}

#### `positionType`

Enum for the position type:

```solidity
enum PositionType { LONG, SHORT }
```

Use `0` for LONG and `1` for SHORT.

#### `orderType`

Enum for the order type:

```solidity
enum OrderType { LIMIT, MARKET }
```

Use `0` for LIMIT and `1` for MARKET.

#### `price`

For a **LIMIT** order, this is the exact price (in 18 decimals) at which you want to open the position.

For a **MARKET** order, the executed price includes the spread the solver charges, so you send a price adjusted from the current market price by a slippage buffer (e.g. 5%) to get the quote accepted:

* **LONG order:** increase the price you get from Muon by 5% (the position opens slightly higher to reflect the hedger's spread).
* **SHORT order:** decrease the price you get from Muon by 5% (the position opens slightly lower to reflect the hedger's spread).

#### `quantity`

The total order quantity in 18-decimal format. For example, a 5 ETH order is `5e18`.

#### `cva, lf, partyAmm, partyBmm`

The locked-value parameters: fractions (as percentages) of the notional value that define the risk and margin requirements.

* **`cva`**: Credit Valuation Adjustment. Either `partyA` (the user) or `partyB` (the hedger) can be liquidated, and `cva` is the penalty the liquidated side pays the other.
* **`lf` (Liquidator Fee):** fee reserved for liquidators.
* **`partyAmm` (Party A Maintenance Margin):** margin required for PartyA.
* **`partyBmm` (Party B Maintenance Margin):** margin required for PartyB.

{% hint style="info" %}
Query the solver's `get_locked_params` endpoint. Multiply the notional value by the returned percentage (divided by 100) for each parameter. For a MARKET order, calculate the notional from the adjustedPrice you sent (with slippage included).
{% endhint %}

The general formula for these values:

**LockedParam (Wei)** = (Notional Value \* lockedParam) / (100 \* leverage)

```javascript
      //CVA
      const cvaWei = notionalValue
      * (new BigNumber(lockedParams.cva))
      / (new BigNumber(100)) 
      / (new BigNumber(lockedParams.leverage))
      / (new BigNumber(1e18));
      console.log("cva: ", cvaWei);
      //LF
      const lfWei = notionalValue
      * (new BigNumber(lockedParams.lf))
      / (new BigNumber(100)) 
      / (new BigNumber(lockedParams.leverage))
      / (new BigNumber(1e18));
      //Maintenance Margins
      const partyAmmWei = notionalValue
      * (new BigNumber(lockedParams.partyAmm))
      / (new BigNumber(100)) 
      / (new BigNumber(lockedParams.leverage))
      / (new BigNumber(1e18));
      
      const partyBmmWei = notionalValue
      * (new BigNumber(lockedParams.partyBmm))
      / (new BigNumber(100)) 
      / (new BigNumber(1e18));
```

{% hint style="info" %}
**The notional value must be calculated from the price you send to the contract, not the Muon price. It should reflect the hedger's spread.**

**For MARKET orders:**

*notionalValue = (quantity \* adjustedPrice)*

**For LIMIT orders:**

*notionalValue = (quantity \* requestedPrice)*
{% endhint %}

*Example response:*

```json
{"cva":"0.7","lf":"0.3","leverage":"1.0","partyAmm":"99.0","partyBmm":"0","message":"Success"}
```

#### `maxFundingRate`

The maximum funding rate PartyA allows, in 18 decimals (usually `200e18`).

{% hint style="info" %}
You can get this from the solver's `contract-symbols` endpoint (RASA).
{% endhint %}

#### `deadline`

A Unix timestamp marking the quote's expiry. Set it far enough in the future for a solver to act.

#### `affiliate`

The affiliate address: your registered affiliate address for this frontend, which links the quote to you for fee tracking and routing.

#### `upnlSig`

A `SingleUpnlAndPriceSig` struct that confirms:

* **`reqId`:** a unique request identifier.
* **`timestamp`:** when the signature was generated.
* **`upnl`:** the unrealized profit and loss for PartyA.
* **`price`:** the verified asset price (in 18 decimals).
* **`gatewaySignature`:** a signature from the trusted gateway.
* **`sigs`:** a Schnorr signature (with `signature`, `owner`, and `nonce` fields).

{% hint style="info" %}
This data comes from the Muon oracle by calling the `uPnl_A_withSymbolPrice` method. A SubAccount bound to a single PartyB skips this Muon check on the trade path and can send an empty `upnlSig`; see the Instant Layer flow.
{% endhint %}

*Example query:*

```
https://muon-oracle4.rasa.capital/v1/?app=symmio&method=uPnl_A_withSymbolPrice&params[partyA]=0xYourAddress&params[chainId]=42161&params[symbolId]=4&params[symmio]=0xSymmioDiamondAddress
```

*Script to fetch and format the upnlSig for `sendQuoteWithAffiliate`, using axios and web3:*

```javascript
require('dotenv').config()
const { Web3 } = require('web3');
// Provide a valid provider URL; you can use your own Infura project URL or another provider.
const web3 = new Web3(new Web3.providers.HttpProvider(process.env.PROVIDER_URL));
const axios = require("axios");

async function getMuonSig(account, chainId, contractAddress, symbolId) {
  const url = "https://muon-oracle4.rasa.capital/v1/";
  const params = {
    app: "symmio",
    method: "uPnl_A_withSymbolPrice",
    "params[partyA]": account,
    "params[chainId]": chainId.toString(),
    "params[symbolId]": symbolId.toString(),
    "params[symmio]": contractAddress
  };

  try {
    const response = await axios.get(url, { params });
    if (!response.data || !response.data.result) {
      throw new Error("Unexpected response structure: " + JSON.stringify(response.data));
    }

    const result = response.data.result;
    if (!result.data) {
      throw new Error("Missing data in result");
    }
    const data = result.data;
    const reqId = result.reqId;
    const timestamp = data.timestamp ? data.timestamp : "0";
    const uPnl = data.result && data.result.uPnl ? data.result.uPnl : "0";
    const price = data.result && data.result.price ? data.result.price : "0";
    const gatewaySignature = result.nodeSignature;
    const sigs = (result.signatures && result.signatures.length > 0)
      ? result.signatures[0]
      : { signature: "0", owner: "0x0" };
    const nonce = data.init && data.init.nonceAddress ? data.init.nonceAddress : "0x0";

    const upnlSigFormatted = {
      reqId: web3.utils.hexToBytes(reqId),
      timestamp: timestamp.toString(),
      upnl: uPnl.toString(),
      price: price.toString(),
      gatewaySignature: web3.utils.hexToBytes(gatewaySignature),
      sigs: {
        signature: sigs.signature.toString(),
        owner: sigs.owner,
        nonce: nonce
      }
    };

    return upnlSigFormatted;
  } catch (error) {
    console.error("Error fetching Muon signature:", error);
    throw error;
  }
}

// Example usage:
(async () => {
  try {
    const account = ""; // Replace with your Party A address
    const chainId = 42161; // Example: Arbitrum chain ID
    const contractAddress = ""; // SYMM Diamond address
    const symbolId = 1; // Example symbolId 1 = BTCUSDT
    const muonSig = await getMuonSig(account, chainId, contractAddress, symbolId);
    console.log("Muon Signature:", muonSig);
  } catch (error) {
    console.error(error);
  }
})();

```

#### Parameter encoding for sendQuoteWithAffiliate

When sending a quote, encode the function call with the Symmio Core ABI before forwarding it through the AccountLayer's `_call`. This formats the parameters correctly and lets the Diamond delegate the call to the right facet.

The transaction payload before encoding should look like this:

```javascript
      //Max funding and deadline
      const sendQuoteWithAffiliateParameters = [
        partyBsWhiteList,
        symbolId,
        positionType,
        orderType,
        requestPrice.toString(), //the MARKET price (with added slippage) or if LIMIT, any price
        requestedQuantityWei.toString(),
        cvaWei.toString(), 
        lfWei.toString(),
        partyAmmWei.toString(),
        partyBmmWei.toString(), 
        maxFundingRate.toString(), 
        deadline.toString(),
        affiliate,
        upnlSigFormatted //signature from muon
    ];
```

**Steps to encode and send a quote:**

1. **Encode the function call.** Use Web3's ABI encoding to turn the `sendQuoteWithAffiliate` call into a byte string:

   ```javascript
   const encodedSendQuoteWithAffiliateData = web3.eth.abi.encodeFunctionCall(
     sendQuoteWithAffiliateFunctionAbi,
     sendQuoteWithAffiliateParameters
   );
   ```

   * **sendQuoteWithAffiliateFunctionAbi:** the ABI definition for `sendQuoteWithAffiliate`.
   * **sendQuoteParameters:** an array of all parameters in the order defined by the function signature.
2. **Prepare the call data.** The AccountLayer's `_call()` method expects the SubAccount address and an array containing your encoded call:

   ```javascript
   const _callData = [ subAccountAddress, [ encodedSendQuoteWithAffiliateData ] ];
   ```
3. **Send the transaction.** Forward the encoded call through the AccountLayer:

   ```javascript
   const sendQuoteWithAffiliateReceipt = await accountLayerContract.methods._call(..._callData).send({
     from: account.address,
     gas: adjustedGasLimit.toString(),
     gasPrice: sendQuotePrice.toString()
   });
   ```

#### Finding the quote ID

Once you send a quote with `sendQuoteWithAffiliate()`, the system emits a **SendQuote** event. It confirms the quote was submitted and carries a unique identifier (the `quoteId`) plus key details. The event is defined as:

```solidity
event SendQuote(
    address partyA,
    uint256 quoteId,
    address[] partyBsWhiteList,
    uint256 symbolId,
    PositionType positionType,
    OrderType orderType,
    uint256 price,
    uint256 marketPrice,
    uint256 quantity,
    uint256 cva,
    uint256 lf,
    uint256 partyAmm,
    uint256 partyBmm,
    uint256 tradingFee,
    uint256 deadline
);
```

Use this ID to track and query the quote later with getQuote().


# Closing a Quote

Closing a quote is how PartyA exits an open position. You send a close request specifying which portion of the position to close, at what price, and by when. As with sending a quote, you encode the `requestToClosePosition` call with the Symmio Core ABI and forward it through the AccountLayer's `_call`. For a wallet-free close signed by a session key, use the Instant Layer flow instead.

#### Function signature

```solidity
function requestToClosePosition(
    uint256 quoteId,
    uint256 closePrice,
    uint256 quantityToClose,
    OrderType orderType,
    uint256 deadline
) external;
```

### Parameter breakdown

#### `quoteId`

The unique identifier of the quote you want to close.

#### `closePrice`

For a **LIMIT** order, this is the exact price (in 18 decimals) at which you want to close the position.

For a **MARKET** order, the executed price includes the spread the solver charges, so you send a price adjusted from the current market price by a slippage buffer (e.g. 5%) to make sure the close is accepted. Adjust in the direction that guarantees the fill:

* Closing a LONG (a sell): decrease the market price by your slippage percentage.
* Closing a SHORT (a buy): increase the market price by your slippage percentage.

The adjustment accounts for the hedger's spread and helps ensure your close request is accepted.

#### `quantityToClose`

The amount of the position (in 18-decimal format) you want to close.

#### `orderType`

An enum value for the closing order type:

* `0` for LIMIT orders
* `1` for MARKET orders

#### `deadline`

A Unix timestamp by which the close request must be executed. If no action is taken by then, the request expires.

#### Parameter encoding

Encode the parameters for `requestToClosePosition()` with the Symmio Core ABI, then pass the calldata to the AccountLayer's `_call()` for the SubAccount that owns the position. On a non-CUSTOM SubAccount the AccountLayer routes the close to the VirtualAccount holding that `quoteId`; on a CUSTOM SubAccount it executes directly.

```javascript
// Prepare close position parameters
const nowInSeconds = Math.floor(Date.now() / 1000);
const deadlineInSeconds = nowInSeconds + deadline; // 'deadline' is a duration in seconds

const closePositionParameters = [
  quoteId.toString(), 
  closePrice.toString(), 
  quantityToClose.toString(), 
  orderType, 
  deadlineInSeconds.toString()
];

// Encode the function call using the Symmio Core ABI
const encodedClosePositionData = web3.eth.abi.encodeFunctionCall(
  requestToClosePositionFunctionAbi, // ABI definition for requestToClosePosition
  closePositionParameters
);

// Prepare call data for the AccountLayer's _call: [subAccount, [calldata]]
const _callData = [subAccountAddress, [encodedClosePositionData]];

// Estimate gas and send the transaction through the AccountLayer
const gasPrice = await web3.eth.getGasPrice();
const gasEstimate = await accountLayerContract.methods._call(..._callData).estimateGas({ from: process.env.WALLET_ADDRESS });
const gasEstimateBigInt = BigInt(gasEstimate);
const adjustedGasLimit = gasEstimateBigInt + (gasEstimateBigInt * BigInt(20) / BigInt(100)); // 20% buffer

const transactionReceipt = await accountLayerContract.methods._call(..._callData).send({
  from: process.env.WALLET_ADDRESS,
  gas: adjustedGasLimit.toString(),
  gasPrice: gasPrice.toString()
});

console.log("Transaction Receipt:", transactionReceipt);
```


# Instant Trading

## Instant Trading for Frontend Builders: Enhancing User Experience and Capability

Instant Trading on SYMMIO introduces a new era of speed and responsiveness for decentralized derivatives trading. Before, users had to wait seconds—or even minutes—for trade confirmations and price lock-ins. With Instant Trading, frontend builders can now deliver a trading experience that closely matches the speed found on centralized exchanges, all while preserving the transparency and trustlessness of decentralized infrastructure.

### Why Instant Trading Matters for Frontend Builders

**1. Dramatic UX Improvements**:\
By integrating Instant Trading, frontend builders can provide near-instant trade confirmations (under one second) and rapid on-chain settlement (5–30 seconds depending on the network). This reduces user frustration, improves retention, and encourages more active engagement.

**2. Unlocking Advanced Trading Strategies**:\
Faster execution times enable strategies like scalping and precise position management that were previously challenging in decentralized environments. This enhances the attractiveness of your platform to both retail users and professional traders.

**3. Session Management**:\
Users delegate certain trading functions to solvers via the MultiAccount contract, allowing trades to be opened and closed without repeatedly signing individual transactions. Frontend builders can guide users through this one-time delegation process and streamline their subsequent trading activities.

### Key Requirements for Frontend Builders

**1. Delegation of Functions**:\
Frontend Builders must implement an initial user flow where the trader authorizes solver addresses to act on their behalf for specific functions. Once granted, these permissions enable quick, automated trade execution without additional signatures.

**2. Secure Authentication Flow**:\
Your frontend needs to handle authentication and session creation. Users sign a message, solvers verify it, and the frontend receives an access token. This token is crucial for subsequent requests, ensuring trades occur instantly while maintaining a secure environment. You can read more about this [here](/liquidity-provider-documentation/building-a-solver-on-symmio/instant-trading/rasa-instant-trading-implementation).

**3. Integration with Solver Endpoints**:\
While frontend builders handle the user interface and overall experience, Instant Trading relies on solver endpoints to process trades instantly. Applications connect securely with these solver APIs to submit trade requests and receive immediate confirmations.&#x20;

**4. Transparent User Communication**:\
Communicate the implications of delegating access. Emphasize that users retain control, can revoke delegated functions, and that access tokens expire.

You can read more about the solver implementations and endpoints here:

{% content-ref url="/pages/slVgRIfLoyCr1Od5s0OC" %}
[Instant Trading for Liquidity Providers](/liquidity-provider-documentation/building-a-solver-on-symmio/instant-trading/instant-trading-for-liquidity-providers)
{% endcontent-ref %}

{% content-ref url="/pages/J7hjLIuz3U0vHS19i4tS" %}
[Rasa Instant Trading Implementation](/liquidity-provider-documentation/building-a-solver-on-symmio/instant-trading/rasa-instant-trading-implementation)
{% endcontent-ref %}

{% hint style="info" %}
It's advised for solvers to follow Rasa's implementation to ensure a consistent format for API endpoints across solvers.
{% endhint %}

For a practical example of how these endpoints are called in a full script, check the guide here: [Building an Application with SYMMIO](/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading/instant-login-eoa)

### Future Outlook

With Instant Trading, frontend builders are able to offer:

* **Account-Based APIs**:\
  Simplifying integration for automated strategies and enabling more complex trading frameworks.
* **Advanced TP/SL Implementations**:\
  Leveraging instant actions to adjust or close positions with extreme precision.
* **Full Account Abstraction**:\
  Eventually, users could trade without manually managing wallets, signatures, or even addresses—operating purely via familiar Web2 methods while retaining the security of decentralized finance.


# Instant Trading in v0.8.5

The biggest change to instant trading in the current version: the broad `delegateAccess()` is gone, replaced by a more specific, signature-driven flow on the new **Instant Layer**, which 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 there, the user's wallet doesn't need to be prompted: every open, close, and TP/SL request is signed by the session key, and the solver's HTTP API accepts the signed operations directly. a

#### 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 to 8. The user's wallet only has to come out for steps 1 to 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** for TP/SL through the Conditional Orders Handler to work: the solver's conditional order handler 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, so 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, then `completeUnbindRequest`.

#### 4. Generate a session key and grant delegations

This is the core of the new flow. Each 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 `grantDelegation` calls are signed by the user EOA, which is the only time the wallet is needed during the session. After that, 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`, then `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, so 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). It 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 the delegation 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`; `leg(None)` returns the `ZERO_LEG` payload the COH expects.


# Instant Login (EOA)

### Instant Login for Instant Open/Close

Before you can perform instant open or instant close actions, your sub‑account must delegate access to the solver (Party B) for these functions. Once delegated, you must obtain an access token through a two-step SIWE (Sign-In With Ethereum) login flow. This process ensures that you are authorized to execute instant actions and that your request includes a validated expiration date.

### Flow Overview

1. **Delegate Access:**\
   First, delegate access from your sub‑account to the solver’s address so that they can call functions (such as open, request to close, or cancel close) on your behalf.
2. **Fetch a Nonce:**\
   Query the hedger’s [nonce ](/liquidity-provider-documentation/building-a-solver-on-symmio/instant-trading/rasa-instant-trading-implementation#nonce-retrieval)endpoint using your sub‑account address. The nonce ensures the uniqueness of your SIWE message and prevents replay attacks.
3. **Build and Sign the SIWE Message:**\
   Construct a SIWE message that includes your wallet address, the nonce, a statement (the sub-account), and timestamps (issued and expiration). This message must be signed by your wallet to confirm your intent.
4. **Send Login Request:**\
   Submit your signed SIWE message to the hedger’s [`/login`](/liquidity-provider-documentation/building-a-solver-on-symmio/instant-trading/rasa-instant-trading-implementation#user-login) endpoint. Upon successful verification, you will receive an access token. This token is then used for subsequent instant open or close operations.

### Delegating Access

To enable instant actions on the frontend, users must delegate specific functions to solvers so they can perform actions on the user's behalf without requiring signatures for each request.

Front-ends should ensure that the following functions are delegated to the solver addresses through the `delegateAccesses()` method on the [MultiAccount](/contract-documentation/version-history/contracts-documentation-0.8.2/multiaccount#delegateaccesses) contract, passing these function signatures.:

* `sendQuote`: **0x7f2755b2**
* `sendQuoteWithAffiliate`: **0x40f1310c**
* `requestToClosePosition`: **0x501e891f**
* `requestToCancelQuote`: **0xa8ffc7ab**
* `requestToCancelCloseRequest`: **0xa63b9363**
* `allocate`: **0x90ca796b**

**After Delegating Access you can proceed with the login process.**

#### Example Script

Below is a Node.js script (using ethers and axios) that demonstrates the nonce fetch and instant login flow:

{% hint style="info" %}
**Delegation of Access:**\
Ensure that your sub‑account has already delegated access to the solver for instant actions before starting the login flow.
{% endhint %}

```javascript
// Load environment variables from a .env file
// activeAccount is the sub-account
require("dotenv").config();

// Import required libraries: ethers for Ethereum utilities, axios for HTTP requests, and siwe for Sign-In With Ethereum
const { ethers } = require("ethers");
const axios = require("axios");
const { SiweMessage } = require("siwe");

// Retrieve the private key from environment variables
const PRIVATE_KEY = process.env.PRIVATE_KEY;

// Convert the active account address from the env into a checksummed Ethereum address.
// Using ethers.getAddress ensures the address format is correct. This is the sub-account.
const activeAccount = ethers.getAddress(process.env.activeAccount);
const wallet = new ethers.Wallet(PRIVATE_KEY);

// Define the base URL of the authentication (solver) server. In this example we'll use Perps Hub on Arbitrum
const SOLVER_BASE_URL = "https://www.perps-streaming.com/v1/42161a/0x141269E29a770644C34e05B127AB621511f20109";
const DOMAIN = "localhost";
const ORIGIN = "http://localhost:3000";
const CHAIN_ID = 42161;
const LOGIN_URI = `${SOLVER_BASE_URL}/login`;
const ISSUED_AT = new Date().toISOString();

const EXPIRATION_DATE = new Date(Date.now() + (24 * 60 * 60 * 10 * 1000)).toISOString(); //Longer expiration

//Fetching Nonce from Solver
async function getNonce(address) {
  const url = `${SOLVER_BASE_URL}/nonce/${address}`;
  const { data } = await axios.get(url);
  return data.nonce;
}

// -----------------------------
// MAIN EXECUTION BLOCK
// -----------------------------
(async function main() {
  try {
    console.log(`\n[1/4] Wallet Address: ${wallet.address}`);
    console.log(`[1.5/4] Active Account (Checksum): ${activeAccount}`);

    // Fetch the nonce for the active account from the server
    const nonce = await getNonce(activeAccount);
    console.log(`[2/4] Got nonce: ${nonce}`);

    // Create the SIWE message using the SIWE library
    const siweMessage = new SiweMessage({
      domain: DOMAIN,
      address: wallet.address,
      statement: `msg: ${activeAccount}`,
      uri: LOGIN_URI,
      version: "1",
      chainId: CHAIN_ID,
      nonce: nonce,
      issuedAt: ISSUED_AT,
      expirationTime: EXPIRATION_DATE,
    });

    // Convert the SIWE message object to string format for signing
    const messageToSign = siweMessage.prepareMessage();
    console.log("\n[3/4] SIWE message to sign:\n", messageToSign);

    // Sign the SIWE message using the wallet's private key
    const signature = await wallet.signMessage(messageToSign);
    console.log("\nSignature:", signature);

    // Construct the request body to send to the login endpoint, including the signature and nonce
    const body = {
      account_address: activeAccount,
      expiration_time: EXPIRATION_DATE,
      issued_at: ISSUED_AT,
      signature,
      nonce
    };

    console.log("body: ", body);
    // Define HTTP headers including content type and origin (used for CORS validation on the server)
    const headers = {
      "Content-Type": "application/json",
      Origin: ORIGIN,
      Referer: ORIGIN, 
    };

    console.log("\n[4/4] Sending login request...");

    // Send the login request (POST) to the server with the constructed body and headers
    const response = await axios.post(
      `${SOLVER_BASE_URL}/login`,
      body,
      { headers }
    );

    console.log("Login response:", response.data);
  } catch (err) {
    console.error("Error in SIWE login flow:", err.response?.data || err.message);
  }
})();

```

Upon successful login, the response includes an access token. This token must be attached in the headers of any instant open or close requests that you subsequently send.


# Instant Login (Account Abstracted)

This section explains how and why we the ERC-4337 “account abstraction” style login is used for Instant Actions. It will cover:

• What ERC-4337 and EIP-1271 bring to the table\
• The end-to-end flow for Instant Login (fetching a nonce, building a SIWE message, hashing/signing, local EIP-1271 validation, HTTP call)\
• A complete TypeScript code snippet showing how to call `/login` and obtain an access token

## Why ERC-4337 / Account Abstraction?

Traditional EOAs (Externally Owned Accounts) sign every transaction with their private key. Smart-contract wallets (Gnosis Safe, Argent, etc.) cannot expose a raw private key.\
ERC-4337 let’s a smart-contract wallet “`personal_sign`” arbitrary data off-chain, then verify that signature on-chain via the standard EIP-1271 `isValidSignature(bytes32, bytes)` call.

**Benefits for Instant Actions:**\
• Users grant a single “session” signature once.\
• Solvers can verify authorization on-chain or locally.\
• No need for the user to re-sign every open/close quote.

## High-Level Login Flow

1. Fetch a nonce from `/nonce/{subAccount}`
2. Construct a SIWE message (domain, wallet address, statement, nonce, issuedAt, expiration)
3. Apply EIP-191 prefix + keccak256 → “ERC-4337 hash”
4. Ask the Safe to personal\_sign the raw SIWE string (SigningMethod.ETH\_SIGN)
5. (Optional) Locally verify the signature via `protocolKit.isValidSignature(erc4337Hash, sig)`
6. POST to `/login` with\
   • `account_address`\
   • `issued_at`\
   • `expiration_time`\
   • `nonce`\
   • `signature` (packed Safe signatures)\
   • `sign_type: “ERC4337”`

{% hint style="info" %}
The `sign_type` field is essential to obtain an access token with Rasa but not necessarily required for other solvers.
{% endhint %}

Server validates on-chain via EIP-1271 and returns an access token

Use that token as\
Authorization: Bearer \<access\_token>\
for all subsequent instant open/close calls.

### Detailed Code Example (TypeScript)

```ts
import Safe, { hashSafeMessage }   from '@safe-global/protocol-kit'
import { SigningMethod }           from '@safe-global/types-kit'
import { ethers }                  from 'ethers'
import axios                       from 'axios'

async function main() {
  // ── Configuration ─────────────────────────────────────────
  const RPC_URL      = 'https://rpc.ankr.com/base/…'
  const SAFE_OWNER   = '0x…'    // your EOA private key
  const SAFE_ADDRESS = '0x…'    // your Safe contract
  const SUBACCOUNT   = '0x…'    // sub-account for login
  const SOLVER_BASE  = 'https://base-hedger82.rasa.capital'
  const DOMAIN       = 'localhost'
  const ORIGIN       = 'http://localhost:3000'
  const CHAIN_ID     = 8453

  // 1) initialize Safe SDK (this is the account compatible with ERC4337)
  const protocolKit = await Safe.init({
    provider:   RPC_URL,
    signer:     SAFE_OWNER,
    safeAddress: SAFE_ADDRESS
  })

  // 2) fetch nonce
  const nonce = await axios
    .get(`${SOLVER_BASE}/nonce/${SUBACCOUNT}`)
    .then(r => r.data.nonce)

  // 3) build SIWE message string (exactly same style as your code)
  const issuedAt       = new Date().toISOString()
  const expirationTime = new Date(Date.now() + 86_400_000).toISOString()
  const siweMessage = `${DOMAIN} wants you to sign in with your Ethereum account:
${SAFE_ADDRESS}

msg: ${SUBACCOUNT}

URI: ${SOLVER_BASE}/login
Version: 1
Chain ID: ${CHAIN_ID}
Nonce: ${nonce}
Issued At: ${issuedAt}
Expiration Time: ${expirationTime}`

  // 4) compute ERC‐4337/EIP-191 hash
  function encodeERC4337(msg: string): string {
    const hexMsg   = Buffer.from(msg, 'utf8').toString('hex')
    const prefix   = `\x19Ethereum Signed Message:\n${msg.length}`
    const prefixHex= Buffer.from(prefix, 'utf8').toString('hex')
    return ethers.keccak256('0x' + prefixHex + hexMsg)
  }
  const erc4337Hash = encodeERC4337(siweMessage)
  console.log('[ERC4337] message hash:', erc4337Hash)

  // 5) personal_sign via the Safe (EIP-191)
  const safeMsg   = protocolKit.createMessage(siweMessage)
  const signed    = await protocolKit.signMessage(
    safeMsg,
    SigningMethod.ETH_SIGN,   // personal_sign / EIP-191
    SAFE_ADDRESS
  )
  const signature = signed.encodedSignatures()

  // 6) local EIP-1271 validation (optional)
  const ok = await protocolKit.isValidSignature(erc4337Hash, signature)
  if (!ok) throw new Error('Local EIP-1271 check failed')

  // 7) send the login request
  const loginBody = {
    account_address:  SUBACCOUNT,
    issued_at:        issuedAt,
    expiration_time:  expirationTime,
    nonce,
    signature,
    sign_type:        'ERC4337'   // tell server “use EIP-1271 flow”
  }

  const resp = await axios.post(
    `${SOLVER_BASE}/login`,
    loginBody,
    {
      headers: {
        'Content-Type': 'application/json',
        Origin:          ORIGIN,
        Referer:         ORIGIN
      }
    }
  )

  console.log('Login response:', resp.data)
  // → { access_token: 'eyJ…' }
}

main().catch(console.error)

```

***

When the server receives your POST body, the solver backend will:

• Reconstruct the same SIWE message (using your domain, URI, nonce, subaccount, etc.)\
• Re-compute the ERC-4337 hash (EIP-191 prefix + keccak256)\
• Call `userSmartWallet.isValidSignature(hash, signature)` on-chain\
– If it returns `0x1626ba7e`, the signature is valid\
• If valid, mint you a JWT access\_token with an expiry and your sub-account embedded

Using Your Access Token

***

Once you have `access_token`, attach it to every instant-action request:

Authorization: Bearer \<access\_token>

The solver will trust that token (no further on-chain checks) until it expires.

You can see how to attach this token to requests in the [Building an Application with SYMMIO](/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading/sending-a-quote-instant-open) section.


# Sending a Quote (Instant Open)

## Instant Open Trade

Instant Opens enable you to open a position immediately by using a market order. This process leverages a prior SIWE login to obtain an access token and then uses that token to send your trade payload to the solver’s instant‑open endpoint.

{% hint style="info" %}
Instant Trading is only for sending MARKET orders
{% endhint %}

### **1. Fetch the Current Price and Apply Slippage**

Retrieve the asset price from Muon (returned in wei). Use the [uPnL\_A\_WithSymbolPrice](/api-endpoints-and-deployments/muon-api#upnl_a_withsymbolprice) method to get this price.

```javascript
// Fetch the asset price from Muon (price returned in wei)
async function fetchMuonPrice() {
  try {
    const response = await axios.get(MUON_URL);
    const fetchedPriceWei = response.data.result.data.result.price;
    if (!fetchedPriceWei) {
      throw new Error("Muon price not found in response.");
    }
    return fetchedPriceWei; // as a string (in wei)
  } catch (error) {
    console.error(
      "Error fetching Muon price:",
      error.response?.data || error.message
    );
    throw error;
  }
}
```

For LONG positions, you should increase the fetched price by a fixed percentage (e.g., +1%) to account for the hedger’s spread, for SHORT decrease the price by the same amount. This is the `adjustedPrice` that will be sent to the API endpoint. The adjusted price becomes the basis for calculating the notional value.

### **2. Fetch Locked Parameters**

Query the solver’s [`get_locked_params` ](/api-endpoints-and-deployments/solver-addresses-and-endpoints/rasa-capital#get-get_locked_params)endpoint to obtain CVA, LF, PartyAmm, and PartyBmm (provided as percentages). These parameters define the collateral requirements for the trade.

### **3. Calculate Notional Value**

For MARKET orders, compute the notional value as: `notionalValue = quantity * adjustedPrice`

### **4. Compute Normalized Locked Values:**

For each risk parameter (e.g., CVA, LF, PartyAmm, PartyBmm), the normalized locked value is calculated using the formula:

**Normalized Locked Value (Wei)** = (Notional Value × lockedParam) / (100 \* leverage)

{% hint style="info" %}
For `partyBmm`, leverage is not used in the calculation
{% endhint %}

For example:

* **CVA:** `(notionalValue * cva) / (100 * leverage)`
* **LF:** `(notionalValue * lf) / (100 * leverage)`
* **PartyAmm:** `(notionalValue * partyAmm) / (100 * leverage)`
* **PartyBmm:** `(notionalValue * partyBmm) / 100)`

### **5. Build the Trade Payload**

Create an object containing all the required parameters:

* [**`symbolId`**](/exchange-builder-documentation/frontend-builder-technical-guidance/sending-a-quote#symbolid)**:** The identifier for the asset (e.g., "340" for XRP).
* [**`positionType`**](/exchange-builder-documentation/frontend-builder-technical-guidance/sending-a-quote#positiontype)**:** 0 for LONG (or 1 for SHORT) – note that in instant trading, market orders are used.
* [**`orderType`**](/exchange-builder-documentation/frontend-builder-technical-guidance/sending-a-quote#ordertype)**:** Set to 1 for MARKET orders.
* [**`price`**](/exchange-builder-documentation/frontend-builder-technical-guidance/sending-a-quote#price)**:** The adjusted price (after applying slippage).
* **`quantity`:** The trade quantity (in 18‑decimals).
* **`cva`, `lf`, `partyAmm`, `partyBmm`:** The normalized locked values calculated above.
* [**`maxFundingRate`**](/contract-documentation/symmio-perps-v0.8.4/facets/partya-facet-0.8.4#sendquotewithaffiliate)**:** Maximum funding rate allowed by Party A (converted to 18 decimals).
* **`deadline`:** A Unix timestamp indicating when the trade request expires. It's recommended to use a longer dated expiry (>1 day)

### **6. Send the Instant Open Request:**

Use the access token obtained via SIWE login and send the trade payload to the `/instant_open` endpoint of the solver. This is done via an HTTP POST with appropriate headers.

## Sample Script

Below is a simplified JavaScript example (using ethers and axios) that demonstrates the instant open flow using a LONG XRP order. This includes the login component:

```javascript
require("dotenv").config();
const { ethers } = require("ethers");
const axios = require("axios");
const BigNumber = require("bignumber.js");

const PRIVATE_KEY = process.env.PRIVATE_KEY;
const activeAccount = process.env.activeAccount; // Your sub-account address
const wallet = new ethers.Wallet(PRIVATE_KEY);

// --------------------------------------------------------------------
// Trade Configuration
// --------------------------------------------------------------------

const ORIGIN = "http://localhost:3000";
const CHAIN_ID = 42161;
const LOGIN_URI = `${SOLVER_BASE_URL}/login`;
const ISSUED_AT = new Date().toISOString();
const EXPIRATION_DATE = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
// For locked parameters, append the leverage to the URL:
const LEVERAGE = "1"; // Example leverage value
const LOCKED_PARAMS_URL = `https://www.perps-streaming.com/v1/42161a/0x141269E29a770644C34e05B127AB621511f20109/get_locked_params/XRPUSDT?leverage=${LEVERAGE}`;
const SOLVER_BASE_URL =
  "https://www.perps-streaming.com/v1/42161a/0x141269E29a770644C34e05B127AB621511f20109";
const DOMAIN = "localhost";
// For XRP, the marketId is "340"
const symbolId = 340;
// Position type: 0 for long, 1 for short
const positionType = 0;
// Order type: 0 = limit, 1 = market (instant trading only uses market orders)
const orderType = 1;
// Trade quantity (6.1 XRP tokens) – ensure this exceeds the minimum notional value (e.g. ~$15)
const quantity = "6.1";
// Maximum funding rate as a string (e.g. "200")
const maxFundingRate = "200";
// Deadline: current timestamp + 1 hour (in seconds)
const deadline = Math.floor(Date.now() / 1000) + 3600;

// --------------------------------------------------------------------
// Helper Functions
// --------------------------------------------------------------------
function buildSiweMessage({
  domain,
  address,
  statement,
  uri,
  version,
  chainId,
  nonce,
  issuedAt,
  expirationTime
}) {
  return `${domain} wants you to sign in with your Ethereum account:
${address}

${statement}

URI: ${uri}
Version: ${version}
Chain ID: ${chainId}
Nonce: ${nonce}
Issued At: ${issuedAt}
Expiration Time: ${expirationTime}`;
}

async function getNonce(address) {
  const url = `${SOLVER_BASE_URL}/nonce/${address}`;
  const { data } = await axios.get(url);
  return data.nonce;
}

async function fetchMuonPrice() {
  try {
  //Replace MUON_URL with your GET endpoint for uPnL_A_WithSymbolPrice
    const MUON_URL =
      "";
    const response = await axios.get(MUON_URL);
    const fetchedPriceWei = response.data.result.data.result.price;
    if (!fetchedPriceWei) {
      throw new Error("Muon price not found in response.");
    }
    return fetchedPriceWei;
  } catch (error) {
    console.error(
      "Error fetching Muon price:",
      error.response?.data || error.message
    );
    throw error;
  }
}

async function fetchLockedParams() {
  try {
    const response = await axios.get(LOCKED_PARAMS_URL);
    if (response.data && response.data.message === "Success") {
      return response.data; // Contains: cva, lf, leverage, partyAmm, partyBmm
    } else {
      throw new Error("Failed to fetch locked parameters.");
    }
  } catch (error) {
    console.error(
      "Error fetching locked parameters:",
      error.response?.data || error.message
    );
    throw error;
  }
}

// Compute normalized locked value for a given parameter (e.g. CVA, LF, PartyAmm)
// For CVA, LF, and PartyAmm, the formula is: (notionalValue * lockedParam) / (100 * leverage)
// For PartyBmm, the formula is: (notionalValue * partyBmm) / 100
function calculateNormalizedLockedValue(notional, lockedParam, leverage, applyLeverage = true) {
  if (applyLeverage) {
    return notional.multipliedBy(new BigNumber(lockedParam)).dividedBy(100 * leverage);
  } else {
    return notional.multipliedBy(new BigNumber(lockedParam)).dividedBy(100);
  }
}

// --------------------------------------------------------------------
// openInstantTrade: Executes the instant open trade process
// --------------------------------------------------------------------
async function openInstantTrade(token) {
  try {
    // 1. Fetch the raw asset price (in wei) from Muon.
    const fetchedPriceWei = await fetchMuonPrice();
    console.log("Fetched price (wei):", fetchedPriceWei);

    // 2. Apply a fixed +5% slippage.
    const fetchedPriceBN = new BigNumber(fetchedPriceWei);
    const adjustedPriceBN = fetchedPriceBN.multipliedBy(1.05); //Because we're going LONG, adjust by increasing to account for hedger spread..
    const adjustedPrice = ethers.formatUnits(adjustedPriceBN.toFixed(), 18);
    console.log("Adjusted price (+5%):", adjustedPrice);

    // 3. Fetch locked parameters (includes leverage).
    const lockedParams = await fetchLockedParams();
    console.log("Locked parameters:", lockedParams);

    // 4. Compute the notional value (for MARKET orders: quantity * adjustedPrice)
    const notional = new BigNumber(adjustedPrice).multipliedBy(new BigNumber(quantity));
    console.log("Notional:", notional.toString());

    // 5. Compute normalized locked values:
    const leverage = new BigNumber(lockedParams.leverage);
    const normalizedCVA = calculateNormalizedLockedValue(notional, lockedParams.cva, leverage, true).toFixed();
    const normalizedLF = calculateNormalizedLockedValue(notional, lockedParams.lf, leverage, true).toFixed();
    const normalizedPartyAmm = calculateNormalizedLockedValue(notional, lockedParams.partyAmm, leverage, true).toFixed();
    const normalizedPartyBmm = calculateNormalizedLockedValue(notional, lockedParams.partyBmm, leverage, false).toFixed();

    console.log("Normalized CVA:", normalizedCVA);
    console.log("Normalized LF:", normalizedLF);
    console.log("Normalized PartyAmm:", normalizedPartyAmm);
    console.log("Normalized PartyBmm:", normalizedPartyBmm);

    // 6. Build the trade payload with normalized values.
    const tradeParams = {
      symbolId: symbolId,           // For XRP, this is 340
      positionType: positionType,   // 0 for long (or 1 for short)
      orderType: orderType,         // 1 for market order
      price: adjustedPrice,         // Adjusted price after slippage
      quantity: quantity,           // Quantity in human-readable format (e.g., "6.1")
      cva: normalizedCVA,
      lf: normalizedLF,
      partyAmm: normalizedPartyAmm,
      partyBmm: normalizedPartyBmm,
      maxFundingRate: maxFundingRate,
      deadline: deadline            // Unix timestamp for deadline
    };

    console.log("Trade Payload:", tradeParams);

    // 7. Send the instant open request using the access token.
    const headers = {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`
    };

    const response = await axios.post(`${SOLVER_BASE_URL}/instant_open`, tradeParams, { headers });
    console.log("Instant open response:", response.data);
  } catch (error) {
    console.error("Error in openInstantTrade:", error.response?.data || error.message);
  }
}

// --------------------------------------------------------------------
// MAIN FLOW: SIWE Login then Instant Open Trade
// --------------------------------------------------------------------
(async function main() {
  try {
    console.log(`\n[1/4] Wallet Address: ${wallet.address}`);
    const nonce = await getNonce(activeAccount);
    console.log(`[2/4] Got nonce: ${nonce}`);

    const siweMessage = buildSiweMessage({
      domain: DOMAIN,
      address: wallet.address,
      statement: `msg: ${activeAccount}`,
      uri: LOGIN_URI,
      version: "1",
      chainId: CHAIN_ID,
      nonce,
      issuedAt: ISSUED_AT,
      expirationTime: EXPIRATION_DATE
    });
    console.log("\n[3/4] SIWE message to sign:\n", siweMessage);

    const signature = await wallet.signMessage(siweMessage);
    console.log("\nSignature:", signature);

    const loginBody = {
      account_address: activeAccount,
      expiration_time: EXPIRATION_DATE,
      issued_at: ISSUED_AT,
      signature,
      nonce
    };
    const loginHeaders = {
      "Content-Type": "application/json",
      Origin: ORIGIN,
      Referer: ORIGIN
    };

    console.log("\n[4/4] Sending login request...");
    const loginResponse = await axios.post(`${SOLVER_BASE_URL}/login`, loginBody, { headers: loginHeaders });
    console.log("Login response:", loginResponse.data);

    const token = loginResponse.data.access_token;
    if (!token) {
      throw new Error("No access token received from login.");
    }

    await openInstantTrade(token);
  } catch (err) {
    console.error("Error in SIWE login flow:", err.response?.data || err.message);
  }
})();
```


# Closing a Quote (Instant Close)

Instant closes let you immediately close a position by calling the `/instant_close` endpoint. You must first obtain a SIWE access token via a login flow if you don't have one already, then use that token to call the instant close function.

{% hint style="info" %}
Instant Trading is only for MARKET orders
{% endhint %}

***

## Instant Close Flow

Instant close allows you to quickly exit an open position. The process requires:

1. [**SIWE Login**](/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading/instant-login-eoa) **(if you don't have an access token already)**\
   Fetch a nonce from the solver’s endpoint, build and sign a SIWE (Sign-In with Ethereum) message, and then log in to obtain an access token.
2. **Call the `/`**[**`instant_close`** ](/liquidity-provider-documentation/building-a-solver-on-symmio/instant-trading/rasa-instant-trading-implementation#instant-close-position)**Endpoint:**\
   With the access token, send a POST request that includes:
   * `quote_id`: The unique number identifying the quote/position.
   * `quantity_to_close`: The amount (as a string) you wish to close.
   * `close_price`: The price (as a string) at which you want to close the position.

{% hint style="info" %}
For market orders, you will need to adjust the close price (i.e. [apply slippage](/exchange-builder-documentation/frontend-builder-technical-guidance/instant-trading/sending-a-quote-instant-open#fetch-the-current-price-and-apply-slippage)) similar to the opening process.\
\
*For CLOSE LONG: Decrease the `price` by your slippage factor.*

*For CLOSE SHORT: Increase the `price` by your slippage factor.*
{% endhint %}

The following script shows a full example in Node.js using Axios and ethers.js, closing a LONG XRP position.

***

#### Instant Close Script Example

```javascript
require("dotenv").config();
const { ethers } = require("ethers");
const axios = require("axios");

// --------------------------------------------------------------------
// SIWE / Login Configuration
// --------------------------------------------------------------------
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const activeAccount = process.env.activeAccount; // Your sub-account address
const wallet = new ethers.Wallet(PRIVATE_KEY);

const SOLVER_BASE_URL = "https://www.perps-streaming.com/v1/42161a/0x141269E29a770644C34e05B127AB621511f20109";
const DOMAIN = "localhost";
const ORIGIN = "http://localhost:3000";
const CHAIN_ID = 42161;
const LOGIN_URI = `${SOLVER_BASE_URL}/login`;
const ISSUED_AT = new Date().toISOString();
// Set expiration to 24 hours from now for login
const EXPIRATION_DATE = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();

// --------------------------------------------------------------------
// Helper Functions
// --------------------------------------------------------------------

// SIWE message builder (EIP-4361 format)
function buildSiweMessage({ domain, address, statement, uri, version, chainId, nonce, issuedAt, expirationTime }) {
  return `${domain} wants you to sign in with your Ethereum account:
${address}

${statement}

URI: ${uri}
Version: ${version}
Chain ID: ${chainId}
Nonce: ${nonce}
Issued At: ${issuedAt}
Expiration Time: ${expirationTime}`;
}

// Fetch nonce for SIWE login
async function getNonce(address) {
  const url = `${SOLVER_BASE_URL}/nonce/${address}`;
  const { data } = await axios.get(url);
  return data.nonce;
}

// Fetch the asset price from Muon, convert the returned wei value to a standard decimal string.
async function fetchMuonPriceConverted() {
  try {
    const MUON_URL = ""; //This is the uPnL_A_WithSymbolPrice Muon API request.
    const response = await axios.get(MUON_URL);
    const fetchedPriceWei = response.data.result.data.result.price;
    if (!fetchedPriceWei) {
      throw new Error("Muon price not found in response.");
    }
    // Convert the wei value to a human-readable decimal (assumes 18 decimals)
    const priceDecimal = ethers.formatUnits(fetchedPriceWei, 18);
    return priceDecimal;
  } catch (error) {
    console.error("Error fetching Muon price:", error.response?.data || error.message);
    throw error;
  }
}

// Call the /instant_close endpoint to close a position.
// The endpoint expects an object with:
//   {
//     "quote_id": number,
//     "quantity_to_close": "string",
//     "close_price": "string"
//   }
async function closeInstantPosition(token, quote_id, quantity_to_close, close_price) {
  const payload = {
    quote_id,            // Example: 23688
    quantity_to_close,   // Example: "6.1"
    close_price,         // Example: "2.3"
  };

  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${token}`,
  };

  const response = await axios.post(`${SOLVER_BASE_URL}/instant_close`, payload, { headers });
  return response.data;
}

// --------------------------------------------------------------------
// Main Flow: SIWE Login then Instant Close Trade
// --------------------------------------------------------------------
(async function main() {
  try {
    console.log(`\n[1/4] Wallet Address: ${wallet.address}`);
    const nonce = await getNonce(activeAccount);
    console.log(`[2/4] Got nonce: ${nonce}`);

    // Build the SIWE message.
    const siweMessage = buildSiweMessage({
      domain: DOMAIN,
      address: wallet.address,
      statement: `msg: ${activeAccount}`,
      uri: LOGIN_URI,
      version: "1",
      chainId: CHAIN_ID,
      nonce,
      issuedAt: ISSUED_AT,
      expirationTime: EXPIRATION_DATE,
    });
    console.log("\n[3/4] SIWE message to sign:\n", siweMessage);

    // Sign the SIWE message.
    const signature = await wallet.signMessage(siweMessage);
    console.log("\nSignature:", signature);

    // Build the login request body.
    const loginBody = {
      account_address: activeAccount,
      expiration_time: EXPIRATION_DATE,
      issued_at: ISSUED_AT,
      signature,
      nonce,
    };

    const loginHeaders = {
      "Content-Type": "application/json",
      Origin: ORIGIN,
      Referer: ORIGIN,
    };

    console.log("\n[4/4] Sending login request...");
    const loginResponse = await axios.post(`${SOLVER_BASE_URL}/login`, loginBody, { headers: loginHeaders });
    console.log("Login response:", loginResponse.data);

    // Extract the access token.
    const token = loginResponse.data.access_token;
    if (!token) {
      throw new Error("No access token received from login.");
    }

    // Fetch and log the Muon price in standard decimal format.
    const muonPrice = await fetchMuonPriceConverted();
    console.log("Fetched Muon Price (converted):", muonPrice);

    // ----------------------------------------------------------------
    // Instant Close Parameters – Replace these with your actual values:
    const quote_id = 24718;         // The quote ID (number) for the position to close.
    const quantity_to_close = "6.1"; // The quantity to close as a string.
    const close_price = (muonPrice * 0.95).toString(); //Position was LONG, so we're going to decrease a little to account for hedger spread.
    console.log("closePrice: ", close_price);

    console.log("\nSending instant close request...");
    const closeResponse = await closeInstantPosition(token, quote_id, quantity_to_close, close_price);
    console.log("Instant close response:", closeResponse);
  } catch (err) {
    console.error("Error in SIWE login flow or closing position:", err.response?.data || err.message);
  }
})();
```

***

### Explanation

1. **SIWE Login Flow:**
   * **Nonce Retrieval:** The script calls `getNonce(activeAccount)` to get a nonce from the solver.
   * **SIWE Message:** A SIWE message is built using the provided configuration (domain, chain ID, etc.) and then signed with your wallet.
   * **Login Request:** The signed SIWE message is sent to the `/login` endpoint. On success, an access token is returned.
2. **Instant Close Request:**
   * **Parameters:**
     * **`quote_id`:** The identifier of the quote (or open position) you wish to close.
     * **`quantity_to_close`:** The amount to close (as a string).
     * **`close_price`:** The price at which you wish to close the position.\
       *Note: For market orders, remember to adjust this price for slippage if required.*
   * **API Call:**\
     The access token is included in the authorization header when making a POST request to the `/instant_close` endpoint with the above parameters.


# Instant Withdrawal for Frontend Builders

This document outlines the end-to-end user interaction flows, API endpoints, data schemas, and error codes for integrating the Symmio Instant Withdrawal feature in the frontend application. It is intended for UI developers and focuses on the requests, responses, and sequence of steps rather than backend implementation details.

## 1. Core Concepts

* **Symmio Contract**: Symmio protocol.
* **Instant Withdrawal**: A bridge contract enabling users to withdraw funds with selectable fee policies and cooldowns.
* **Backend Bot**: Off-chain API service handling policy retrieval, validation, scheduling, selecting fee policies, and triggering withdrawals.
* **Frontend**: The UI layer where users interact.
* **User**: End user interacting via wallet and UI.

## 2. API Endpoints

All endpoints are prefixed with `/v1`. Authentication uses JWT via an `Authorization: Bearer <token>` header, obtained through the SIWE login flow.

### 2.1. POST /v1/fee-options

Fetch available withdrawal policies (fee & cooldown) for a given account and amount.

* **Request (Query Parameters):**
  * `account` (string; required; EVM address)
  * `amount` (integer; required; must be > 0; amount in wei)
* **Request Headers:**
  * `Authorization: Bearer <JWT>`
* **Response Schema (200):**

```ts
interface FeeOptionsResponseSchema {
  count: number;          // number of available options
  options: FeeOption[];
}
interface FeeOption {
  fee: number;           // total fee in wei
  cooldown: number;      // cooldown period in seconds
  user: string;          // checksumed ETH address
  amount: number;        // requested amount in wei
  validTime: number;     // Unix epoch (seconds) when this option expires
}
```

* **Behavior:**
  * Validates JWT and wallet ownership of account.
  * Checks if an existing pending request exists. If yes, returns error code for locked options.
  * Computes policies to determine the fees.
* **Error Codes:**

<table><thead><tr><th width="89">Code</th><th>Message</th><th>Condition</th></tr></thead><tbody><tr><td>11</td><td>unauthorized</td><td>The owner_address from JWT does not match the owner for provided account.</td></tr><tr><td>5</td><td>Already has pending withdrawal options try in seconds</td><td>A pending request already exists.</td></tr><tr><td>6</td><td>not enough user balance</td><td>The user’s on-chain balance is less than amount.</td></tr><tr><td>10</td><td>requested amount for bridge is very low</td><td>The requested amount is less than or equal to OPERATOR_FEE.</td></tr></tbody></table>

### 2.2. POST /v1/unlock/{account}

Clear any existing lock on fee policies for the given account, allowing immediate re-fetch of options.

* **Request:**
  * `account` (string; required; EVM address)
* **Request Headers:**
  * `Authorization: Bearer <JWT>`
* **Response Schema (200):**

```ts
interface UnlockAccountResponseSchema {
  message: string;  // "Account unlocked successfully."
}
```

* **Behavior:**
  * Validates JWT and wallet ownership of account.
  * Removes pending fee options.
  * Returns `{ message: "Account <account> unlocked successfully." }`.
* **Error Codes:**

<table><thead><tr><th width="83.54541015625">Code</th><th>Message</th><th>Condition</th></tr></thead><tbody><tr><td>11</td><td>unauthorized</td><td>The owner_address from JWT does not match the owner for provided account.</td></tr></tbody></table>

***

### 2.3. GET /v1/pending-fee-policy/{account}

Retrieve currently locked fee policies for the given account, if any exist.

* **Request:**
  * `account` (string; required; EVM address)
* **Request Headers:**
  * `Authorization: Bearer <JWT>`
* **Response Schema (200):**

```ts
interface FeeOptionsResponseSchema {
  count: number;          // number of available options
  options: FeeOption[];
}
interface FeeOption {
  fee: number;           // total fee in wei
  cooldown: number;      // cooldown period in seconds
  user: string;          // checksumed ETH address
  amount: number;        // requested amount in wei
  validTime: number;     // Unix epoch (seconds) when this option expires
}
```

* **Behavior:**
  * Validates JWT and wallet ownership of account.
* **Error Codes:**

<table><thead><tr><th width="89.9090576171875">Code</th><th>Message</th><th>Condition</th></tr></thead><tbody><tr><td>11</td><td>unauthorized</td><td>The owner_address from JWT does not match the owner for provided account.</td></tr></tbody></table>

***

### 2.4. GET /v1/max-instant-value/{account}

Fetch the maximum amount (in wei) that the user can withdraw instantly at this moment.

* **Request (Path Parameter):**
  * `account` (string; required; EVM address)
* **Request Headers:**
  * `Authorization: Bearer <JWT>`
* **Response Schema (200):**

```ts
interface MaxInstantWithdrawableAmountResponseSchema {
  amount: number; // maximum instant-withdrawable amount in wei
}
```

* **Behavior:**
  * Validates JWT and wallet ownership of account.
  * Checks user balance and risk factor.
  * Returns `{ amount: <max_instant> }`.
* **Error Codes:**

<table><thead><tr><th width="86.272705078125">Code</th><th width="150">Message</th><th>Condition</th></tr></thead><tbody><tr><td>11</td><td>unauthorized</td><td>The owner_address from JWT does not match the owner for provided account.</td></tr></tbody></table>

***

### 2.5. GET /v1/get-select-receiver-message?receiver={address}\&bridgeId={bridgeId}

Construct an EIP‑712 typed-data payload that the user can sign to authorize selecting a different recipient (multi-account) for a given bridgeId.

* **Request (Query Parameters):**
  * `receiver` (string; required; checksumed EVM address)
  * `bridgeId` (integer; required)
* **Request Headers:**
  * No JWT required (open endpoint)
* **Response Schema (200):**

```ts
interface GetSelectReceiverMessageResponse {
  payload: EIP712SelectReceiverMessage;
}
interface EIP712SelectReceiverMessage {
  types: EIP712Types;
  domain: EIP712DomainModel;
  primaryType: "SelectReceiver";
  message: {
    receiver: string; // address
    bridgeId: number;
  };
}
```

* **Behavior:** Returns an on‑chain domain and typed data structure, according to EIP‑712.

### 2.6. POST /v1/select-fee-policy

Submit a signed fee policy selection for an existing bridge transaction.

* **Request Body Schema (application/json):**

```ts
interface SelectFeePolicyRequestSchema {
  symmioBridgeId: number;        // bridge transaction ID
  cooldown: number;              // chosen cooldown
  feeAmount: number;             // chosen fee (wei)
  receiver?: SpecifiedReceiver | null; // optional new recipient object
}
interface SpecifiedReceiver {
  address: string;       // checksumed Ethereum address
  signature: string;     // EIP‑712 signature hex
}
```

* **Request Headers:**
  * `Authorization: Bearer <JWT>`
* **Response Schema (200):**

```ts
interface SelectFeePolicyResponseSchema {
  bridge_id: number;      // same as symmioBridgeId
  execution_time: number; // Unix epoch (seconds) when `processWithdrawal` will be called
  fee: number;            // fee in wei
}
```

* **Behavior:**
  * Validates JWT and that the current user is either the owner or the user on-chain for `symmioBridgeId`.
  * Fetches BridgeTransaction from Symmio contract and validate data and status.
  * Calls `select_fee_policy_for_bridge` on-chain.
  * Returns `{ bridge_id, fee: feeAmount, execution_time }`.
* **Error Codes:**

<table><thead><tr><th width="89">Code</th><th width="294.5999755859375">Message</th><th>Condition</th></tr></thead><tbody><tr><td>11</td><td>unauthorized</td><td>The owner_address from JWT does not match the owner for provided account.</td></tr><tr><td>7</td><td>invalid bridge transaction</td><td>The provided bridgeId is not in RECEIVED status or does not point to the InstantWithdrawal contract.</td></tr><tr><td>8</td><td>invalid bridge policy option</td><td>No pending options exist for this user, or the selected fee/cooldown combination does not match any stored option.</td></tr><tr><td>9</td><td>You already selected a withdrawal option for bridge or it has executed.</td><td>A fee policy has already been selected for this bridge (<code>bridge.bridge_type == instant</code>).</td></tr></tbody></table>

### 2.7. POST /v1/bridges/{account}/{start}/{size}

Search and paginate historical bridge transactions for a given user account:

* **Request:**
  * `account` (string; required; EVM address)
  * `start` (integer; required; offset index)
  * `size` (integer; required; page size)
* **Request Body Schema (application/json):**

```typescript
interface BridgeTransactionRequestModel {
  bridge_state?: "pending" | "executed" | "settled" | "suspend" | null;
  bridge_type?: "instant" | "after_cool_down" | "no_policy" | null;
}
```

* **Request Headers:**
  * `Authorization: Bearer <JWT>`
* **Response Schema (200):**

  ```ts
  interface BridgeTransactionSearchResponseSchema {
    count: number;
    bridges: BridgeTransactionResponseModel[] | null;
  }
  interface BridgeTransactionResponseModel {
    bridge_id: number;
    user_address: string;
    bridge_state: "pending" | "executed" | "settled" | "suspend";
    bridge_type: "instant" | "after_cool_down" | "no_policy";
    bridge_amount: number;        // in wei
    fee_amount: number;           // in wei
    bridge_transaction_time: number; // Unix epoch seconds
    execution_time?: number;      // Unix epoch seconds or null
  }
  ```
* **Behavior:**
  * Validates JWT and wallet ownership of account.
  * Queries database, ordering by creation time descending.
  * If no records, returns `{ count: 0, bridges: null }`.
  * Otherwise returns `{ count: <number_of_rows>, bridges: [ ... ] }`.
* **Error Codes:**

  <table><thead><tr><th width="87.3333740234375">Code</th><th>Message</th><th>Condition</th></tr></thead><tbody><tr><td>11</td><td>unauthorized</td><td>The owner_address from JWT does not match the owner for provided account.</td></tr></tbody></table>

## Authentication Endpoints

### **2.8.1. POST /v1/auth/nonce**

Request a one-time nonce for SIWE login:

* **Request Body (application/json):**

  ```ts
  interface AddressSchema {
    address: string; // checksumed Ethereum address
  }
  ```
* **Response (200):**

  ```json
  { "nonce": "0x1234…abcd" }
  ```
* &#x20;**Behavior**:
  * Generates random nonce for the address.
* **Error Codes**:
  * ValueError (missing or malformed address)

### **2.8.2. GET /v1/auth/sign-in-message**

Return an EIP‑4361 formatted message for user signature:

* **Request (Query):**
  * `address` (string; required, EVM address)
  * `domain` (string; required)
  * `uri` (string; required)
  * `statement` (string; optional)
  * `version` (string; optional)
* **Response Schema (200):**

  ```ts
  interface SignInMessageResponse {
    message: string;  // full EIP‑4361 text
    params: {
      address: string;
      domain: string;
      uri: string;
      statement?: string;
      version: string;
      chainId: number;
      nonce: string;
      issuedAt: string;
      expiration_time: string;
    };
  }
  ```
* **Behavior**
  * Constructs a full EIP-4361-formatted message string comprising
  * Returns both the single message string and the underlying data object.
  * The UI shows message in the wallet’s signing modal and keeps the    \
    returned data for the subsequent **POST /v1/auth/login** call.

### **2.8.3. POST /v1/auth/login**

Validate signed SIWE message and issue JWT:

* **Request Body:**

  ```ts
  interface LoginRequest {
    message: CustomSiweMessage;   // full EIP‑4361 payload
    signature: string;            // hex signature
  }
  interface CustomSiweMessage {
    domain: string;
    address: string;
    uri: string;
    version: string;
    chainId: number;
    issuedAt: string;
    nonce: string;
    statement?: string;
    expirationTime?: string;
  }
  ```
* **Response Schema (200):**

  ```ts
  interface Token {
    access_token: string; // JWT
    token_type: string;   // "bearer"
  }
  ```
* **Error Codes:**

  <table><thead><tr><th width="93.6363525390625">Code</th><th width="158.45458984375">Message</th><th>Condition</th></tr></thead><tbody><tr><td>14</td><td>expired_nonce</td><td>No nonce found or expired.</td></tr><tr><td>18</td><td>nonce_mismatch</td><td>Provided nonce differs from stored.</td></tr><tr><td>12</td><td>invalid_signature</td><td>Signature verification failed.</td></tr></tbody></table>

### **2.8.4. GET /v1/auth/me**

Retrieve the currently authenticated user address:

* **Request Headers:**
  * `Authorization: Bearer <JWT>`
* **Response (200):**

  ```ts
  interface AddressSchema {
    address: string; // checksumed Ethereum address
  }
  ```
* **Error Codes:**

  <table><thead><tr><th width="111.8182373046875">Code</th><th>Message</th><th>Condition</th></tr></thead><tbody><tr><td>11</td><td>unauthorized</td><td>The owner_address from JWT does not match the owner for provided account.</td></tr></tbody></table>

## Bridge Enums Overview

### **Bridge States:**

| Value    | Meaning                                                                            |
| -------- | ---------------------------------------------------------------------------------- |
| pending  | The bridge transaction is recorded but process withdrawal has not been called yet. |
| executed | A fee policy was selected, and the withdrawal has been executed on-chain.          |
| settled  | The funds have been withdrawn from Symmio to the bridge.                           |
| suspend  | The bridge process is on hold (e.g., due to a suspension from Symmio).             |

### **Bridge Types:**

| Value             | Meaning                                                                                               |
| ----------------- | ----------------------------------------------------------------------------------------------------- |
| instant           | The user chose an instant-withdrawal policy with custom fee and cooldown.                             |
| after\_cool\_down | The user selected the default policy that requires waiting for Symmio cooldown before withdrawal.     |
| no\_policy        | No fee policy has been chosen yet; the bridge is awaiting the user to request options and select one. |


# Instant Withdrawal Script

An instant withdrawal lets you withdraw funds without waiting the normal 12-hour cooldown. You select a fee option matching how fast you want it processed, and withdraw through a bridge. This guide shows how to do it without a frontend.

Instant withdrawals work in two steps:

1. **On-chain:** you transfer your allocated balance to a bridge.
2. **Off-chain:** you select a fee option, and the backend schedules the withdrawal for execution.

#### Before you start

You'll need:

* **Python 3.8** or newer
* A wallet private key (for signing and paying gas)
* An RPC URL for the chain you're withdrawing from
* The Python script from the reference repo

#### Setup

Create and activate a virtual environment, then install dependencies:

```bash
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```

#### Configure .env

The script reads its configuration from a `.env` file. Use the private key for the wallet you want to withdraw from, and an RPC provider:

```dotenv
PRIVATE_KEY=0x...
RPC_URL=https://...
CHAIN_ID=8453
BASE_URL=https://instant-withdrawal-backend...
```

{% hint style="info" %}
Withdrawing 1 USDC means passing `1000000`, since USDC on Base has 6 decimals.
{% endhint %}

#### Running the script

From the project root:

```bash
python instant_withdrawal.py
```

Once started, the script walks through all the steps automatically.

#### What the script does, step by step

The flow below maps directly to the Python code.

**1. Sign in with your wallet (SIWE)**

The script proves wallet ownership with Sign-In With Ethereum and gets an access token, so it can talk to the Instant Withdrawal backend.

```python
nonce = requests.post(
    f"{BASE_URL}/v1/auth/nonce",
    json={"address": wallet_address}
).json()["nonce"]
```

```python
message = requests.get(
    f"{BASE_URL}/v1/auth/sign-in-message",
    params={"address": wallet_address, "domain": DOMAIN, "uri": ORIGIN}
).json()["message"]
```

```python
signed = account.sign_message(encode_defunct(text=message))
```

```python
token = requests.post(
    f"{BASE_URL}/v1/auth/login",
    json={"message": payload, "signature": signature}
).json()["accessToken"]
```

This token is required for all Instant Withdrawal API calls.

**2. Lock fee options for your withdrawal amount**

The script asks the backend for the fee options for a specific amount. This temporarily locks the options so rates can't change.

```python
options = requests.post(
    f"{BASE_URL}/v1/fee-options",
    params={"account": subaccount, "amount": AMOUNT},
    headers={"Authorization": f"Bearer {token}"}
).json()["options"]

```

**3. Deallocate funds from trading (on-chain)**

If your funds are currently allocated to trading, free them first. The script fetches a Muon uPNL signature, formats it for the contract, and sends the transaction.

```python
call_data = diamond.functions.deallocate(
    amount, upnl_sig
)._encode_transaction_data()
```

```python
tx = multi_account.functions._call(
    subaccount,
    [call_data]
)
```

This must complete on-chain before continuing.

**4. Transfer funds to the withdrawal bridge (on-chain)**

Once funds are free, they're transferred into the Instant Withdrawal bridge.

```python
call_data = diamond.functions.transferToBridge(
    amount, BRIDGE_ADDRESS
)._encode_transaction_data()
```

```python
tx = multi_account.functions._call(
    subaccount,
    [call_data]
)
```

After confirmation, the script extracts a `transactionId` from the emitted event. This ID is required for the next step.

**5. Fetch pending fee policies**

With the bridge transfer on-chain, the script fetches the valid fee options tied to that transfer.

```python
policies = requests.get(
    f"{BASE_URL}/v1/pending-fee-policy/{subaccount}",
    headers={"Authorization": f"Bearer {token}"}
).json()["policies"]
```

**6. Select a fee policy and schedule the withdrawal**

The script picks the fastest option and submits it.

```python
fastest = min(policies, key=lambda p: p["cooldown"])
```

```python
result = requests.post(
    f"{BASE_URL}/v1/select-fee-policy",
    json={
        "symmioBridgeId": bridge_id,
        "cooldown": fastest["cooldown"],
        "feeAmount": fastest["fee"],
    },
    headers={"Authorization": f"Bearer {token}"}
).json()
```

Once done, the withdrawal is scheduled and executes automatically.

### Reference implementation

[This repository](https://github.com/academy17/instant_withdrawal_reference) has a Python script showing the full Instant Withdrawal flow.


# User Interaction Scenarios

Instant Withdrawal Flow

Below are typical user flows. Each scenario assumes the user has a Web3 wallet connected and has retrieved a valid JWT via the SIWE login flow.

## Scenario 1: Normal Flow and Successful Instant Withdrawal

1. **Login via wallet (SIWE flow)**
   1. UI calls `POST /v1/auth/nonce` with `{ address }`.
   2. UI immediately calls `GET /v1/auth/sign-in-message` with the same address, domain, and URI; displays the returned message for signing.
   3. User signs; UI sends `POST /v1/auth/login` with `{ message, signature }`.
   4. UI receives `access_token` and sets `Authorization: Bearer <token>` on subsequent requests.
2. **Fetch Fee Options**
   1. User enters their wallet account address and desired amount in UI.
   2. UI calls `POST /v1/fee-options?account=<account>&amount=<amount>`.
   3. Backend validates, computes instant and fallback options, and locks them for `POLICY_VALID_TIME` (e.g., 30 s).
   4. UI displays the returned array of one or more `FeeOption` items.
3. **Bridge Funds**
   1. UI invokes on-chain call `symmio_contract.bridge(amount)` (or `_call(deallocate, bridge)` if bridging from a multi-account).
   2. Once on-chain transaction is mined, UI displays the `bridgeId` and instructs user to select a fee policy.
4. **Select Fee Policy**
   1. UI calls `POST /v1/bridges/{account}/{start}/{size}` to fetch the user’s bridges in `no_policy` state, e.g.:

      ```json
      {
        "bridge_state": "pending",
        "bridge_type": "no_policy"
      }
      ```
   2. For each bridge:
      1. UI calls `GET /v1/pending-fee-policy/{account}` and displays locked fee policies.
      2. User chooses one of the displayed `FeeOption`.
      3. *(Optional)* To specify a different receiver:
         1. UI calls

            ```
            GET /v1/get-select-receiver-message?receiver=<newReceiver>&bridgeId=<bridgeId>
            ```
         2. UI has user sign the returned EIP-712 payload and includes:

            ```json
            "receiver": {
              "address": "<newReceiver>",
              "signature": "<sig>"
            }
            ```
      4. UI calls `POST /v1/select-fee-policy` with:

         ```json
         {
           "symmioBridgeId": <bridgeId>,
           "cooldown": <chosenCooldown>,
           "feeAmount": <chosenFee>,
           "receiver": { … }      // optional
         }
         ```
      5. Backend validates, records in DB, calls `selectFeePolicyForBridge` on-chain, and schedules withdrawal at `execution_time = now + cooldown`.
      6. Backend returns:

         ```json
         {
           "bridge_id": <bridgeId>,
           "fee": <feeAmount>,
           "execution_time": <timestamp>
         }
         ```
5. **Automated Withdrawal**\
   After `cooldown` seconds, the backend bot automatically calls `processWithdrawal(bridgeId)` on-chain.

***

## Scenario 2: Fee Options Expire and Retry

1. Login via wallet (as in Scenario 1).
2. **Initial Fee Options**\
   UI calls `POST /v1/fee-options` but user waits beyond `POLICY_VALID_TIME` (e.g., 30 s).
3. Bridge funds and obtain `bridgeId`.
4. Lock expires (options TTL passed).
5. UI retrieves no-policy bridges via `POST /v1/bridges/{account}/{start}/{size}`.
6. **Retry Fee Options**\
   UI calls `POST /v1/fee-options?account=<account>&amount=<amount>` again; backend regenerates options and locks for another 30 s.
7. **Select & Withdraw**\
   User picks a new option, signs payload, calls `POST /v1/select-fee-policy`, and flow continues as in Scenario 1.

***

## Scenario 3: No Policy Selected (Withdrawal Pending)

#### **1. Login & Bridge Funds**

* The user logs in and initiates a bridge transaction through the frontend.
* The backend captures the bridge event and records it with\
  `bridge_type = no_policy` and `status = RECEIVED`.
* The funds are deducted from the user’s Symmio balance and reserved in the bridge contract — **no payout occurs yet.**

#### **2. User Does Not Select a Policy**

* If the user leaves or closes the UI before selecting a fee policy:
  * No withdrawal is scheduled.
  * The bridge transaction remains **pending** indefinitely.
  * The user’s reserved funds **cannot be used elsewhere**, but **can still be withdrawn later** once a policy is chosen.

#### **3. User Returns Later to Complete the Withdrawal**

* When the user logs back in, the UI must:
* Retrieve all pending bridges with\
  `POST /v1/bridges/{account}/{start}/{size}`\
  and look for entries where `bridge_type = no_policy`.
* For each pending bridge, request a **new set of fee options** using\
  `POST /v1/fee-options?account=<account>&amount=<amount>`.

{% hint style="info" %}
Previously shown fee options may have expired past their `POLICY_VALID_TIME,`in which case a new set of fee options must be fetched.
{% endhint %}

#### **4. Selecting a Policy and Processing the Withdrawal**

* Once the user selects a fee option:
  1. The frontend calls `POST /v1/select-fee-policy` with the chosen policy .
  2. The backend calls `selectFeePolicyForBridge` on-chain.
  3. After the configured cooldown period (usually 0), the backend or bot executes\
     `processWithdrawal(bridgeId)` to finalize the payout to the user.
* The transaction is then marked as **completed**, and the user receives the bridged funds.

***

## Scenario 4a: Bot Inactive, Then Reactivated

1. **Bot Down During Bridge**\
   – User bridges on-chain while event poller is offline → no record created.
2. **Bot Reactivation & Catch-Up**\
   – Bot scans missed events, creates `BridgeTransactions` with `no_policy`.
3. **User Resumes Normal Flow**\
   – UI requests fee options and follows Scenario 1 steps to select and execute withdrawal.

***

## Scenario 4b: Bot Inactive after Policy Selected, Manual Withdrawal

1. Follow Scenario 1 through fee-policy selection.
2. Bot goes down before scheduled withdrawal executes.
3. **Manual Withdrawal**
   1. UI displays “Withdraw After Cooldown” button next to bridges where `execution_time` has passed but state is still `pending`.
   2. When clicked, UI calls `processWithdrawal(bridgeId)` on-chain.
   3. Tokens arrive in user’s wallet.

***

## Scenario 5: Reset Locked Fee Options

1. Login via wallet.
2. UI calls `POST /v1/fee-options?account=<account>&amount=<amount>`, receives options.
3. UI calls `POST /v1/unlock/{account}` to clear locks.
4. UI calls `POST /v1/fee-options?account=<account>&amount=<amount>` again to fetch fresh options.

***

### 4. Additional UI Actions and Buttons

1. **Get Max Instant Value Button**
   * **Endpoint:** `GET /v1/max-instant-value/{account}`
   * **Purpose:** Show the maximum amount the user can withdraw instantly; guide the user to not bridge more than this.
2. **Refresh Fee Options Button**
   * **Combined Actions:**
     1. `POST /v1/unlock/{account}`
     2. `POST /v1/fee-options?account=<account>&amount=<amount>`
   * **Purpose:** Clear existing lock and fetch fresh fee/cooldown options in one click.
3. **Manual Process Withdrawal Button**
   * **Action:** Calls the contract’s `processWithdrawal(bridgeId)` method directly.
   * **Purpose:** Allow users to trigger on-chain withdrawal if scheduled withdrawal failed or bot is down.
   * **When to Show:** After fetching user’s bridge list, display “Withdraw Now” next to any entry where `execution_time` has passed but state is still `pending`.

***

### 5. Error Codes

All endpoints return structured error codes via the shared `ErrorInfoContainer`:

<table><thead><tr><th width="101">Code</th><th>Identifier</th><th>Message</th></tr></thead><tbody><tr><td>1</td><td><code>unhandled_error</code></td><td>Internal server error</td></tr><tr><td>2</td><td><code>could_not_get_excepted_response</code></td><td>Could not get expected response</td></tr><tr><td>3</td><td><code>model_validation_error</code></td><td>Model validation error</td></tr><tr><td>4</td><td><code>not_found_error</code></td><td>Not found</td></tr><tr><td>5</td><td><code>already_has_pending</code></td><td>Already has pending withdrawal options—try again in ? seconds</td></tr><tr><td>6</td><td><code>not_enough_balance</code></td><td>Not enough user balance</td></tr><tr><td>7</td><td><code>invalid_bridge_transaction</code></td><td>Invalid bridge transaction</td></tr><tr><td>8</td><td><code>invalid_bridge_policy_option</code></td><td>Invalid Bridge Policy Option—please check and select options again</td></tr><tr><td>9</td><td><code>already_has_withdrawal_option_or_executed</code></td><td>You already selected an option for this bridge or it has executed</td></tr><tr><td>10</td><td><code>low_amount_to_bridge</code></td><td>Requested amount for bridge is very low</td></tr><tr><td>11</td><td><code>unauthorized_access</code></td><td>Unauthorized</td></tr><tr><td>12</td><td><code>invalid_signature</code></td><td>Invalid Signature</td></tr><tr><td>13</td><td><code>signature_mismatch</code></td><td>Signature mismatch</td></tr><tr><td>14</td><td><code>expired_nonce</code></td><td>Nonce expired or not found</td></tr><tr><td>15</td><td><code>not_authenticated</code></td><td>Not authenticated</td></tr><tr><td>16</td><td><code>contract_validation_failed</code></td><td>Contract validation failed</td></tr><tr><td>17</td><td><code>rpc_connection_failed</code></td><td>RPC endpoint connection failed</td></tr><tr><td>18</td><td><code>nonce_mismatch</code></td><td>Nonce mismatch</td></tr><tr><td>19</td><td><code>user_creation_in_progress</code></td><td>User creation in progress for this address. Please retry in a moment.</td></tr></tbody></table>


# Frequently Used Queries

## Useful Query Functions

The following functions allow you to retrieve essential information from the SYMM system. These functions are designed to help frontend applications display account data. All functions are to be called on the Diamond contract, which will delegate the call to the appropriate facets. You can use a diamond inspection tool like [Louper](https://louper.dev/) to interact with the contracts in a more user-friendly way.

### **`allocatedBalanceOfPartyA()`**

Returns the allocated balance for a given sub‑account. This balance represents the collateral that is “free”—i.e., not locked in open positions. Full documentation [here](/contract-documentation/symmio-perps-v0.8.4/facets/view-facet-0.8.4#allocatedbalanceofpartya-and-allocatedbalanceofpartyb).

### **`balanceInfoOfPartyA()`**

Provides detailed balance information for Party A. This includes allocated balances, locked balances, pending locked funds, and other relevant metrics. Full documentation [here](/contract-documentation/symmio-perps-v0.8.4/facets/view-facet-0.8.4#balanceinfoofpartya).

### **`deallocateCooldown()`**

Retrieves the cooldown period required before funds can be withdrawn following a deallocation. This period is typically set to 12 hours.&#x20;

### **`getpartyAOpenPositions()`**

Returns an array of all open positions held by Party A. Full documentation [here](/contract-documentation/symmio-perps-v0.8.4/facets/view-facet-0.8.4#getpartyaopenpositions-and-getpartybopenpositions).

### **`getQuote()`**

Fetches detailed information about a specific quote. Use this function to review all parameters of a quote submitted by a user. Full documentation [here](/contract-documentation/symmio-perps-v0.8.4/facets/view-facet-0.8.4#getquote).

### **`getSymbol()`**

Returns details about a trading symbol, including its name, trading fee, and other configuration parameters. Full documentation [here](/contract-documentation/symmio-perps-v0.8.4/facets/view-facet-0.8.4#getsymbol-9-and-getsymbols).

### **`partyAPositionsCount()`**

Provides the total number of open positions held by Party A. This is useful for quickly assessing account exposure. Full documentation [here](/contract-documentation/symmio-perps-v0.8.4/facets/view-facet-0.8.4#partyapositionscount).

### **`withdrawCooldownOf()`**

Retrieves the withdrawal cooldown timestamp for a sub‑account, indicating when funds will next be available for withdrawal. Full documentation [here](/contract-documentation/symmio-perps-v0.8.4/facets/view-facet-0.8.4#withdrawcooldownof).

## Data Structures

These data structures form the backbone of SYMM’s trading engine. They define the various states and parameters associated with quotes and positions. When querying the Diamond, functions like `getQuote()` return these structures to provide a complete picture of a trade’s configuration and status.

## Structs

### **LockedValues**

This structure holds the risk and margin parameters for a quote.

```solidity
struct LockedValues {
    uint256 cva;       // Credit Value Adjustment
    uint256 lf;        // Liquidator Fee
    uint256 partyAmm;  // Party A Maintenance Margin
    uint256 partyBmm;  // Party B Maintenance Margin
}
```

### **Quote**

The `Quote` struct contains comprehensive information about a trade request, including pricing, quantities, and state.

```solidity
struct Quote {
	uint256 id;
	address[] partyBsWhiteList;
	uint256 symbolId;
	PositionType positionType;
	OrderType orderType;
	// Price of quote which PartyB opened in 18 decimals
	uint256 openedPrice;
	uint256 initialOpenedPrice;
	// Price of quote which PartyA requested in 18 decimals
	uint256 requestedOpenPrice;
	uint256 marketPrice;
	// Quantity of quote which PartyA requested in 18 decimals
	uint256 quantity;
	// Quantity of quote which PartyB has closed until now in 18 decimals
	uint256 closedAmount;
	LockedValues initialLockedValues;
	LockedValues lockedValues;
	uint256 maxFundingRate;
	address partyA;
	address partyB;
	QuoteStatus quoteStatus;
	uint256 avgClosedPrice;
	uint256 requestedClosePrice;
	uint256 quantityToClose;
	// handle partially open position
	uint256 parentId;
	uint256 createTimestamp;
	uint256 statusModifyTimestamp;
	uint256 lastFundingPaymentTimestamp;
	uint256 deadline;
	uint256 tradingFee;
	address affiliate;
}
```

#### Field Explanations

* **`id`:** Unique identifier for the quote.
* **`partyBsWhiteList`:** Array of addresses representing the Party B (hedger/solver) candidates eligible to accept the quote.
* **`symbolId`:** Identifier for the trading symbol associated with the quote.
* [**`positionType`**](#positiontype)**:** Indicates whether the position is LONG or SHORT.&#x20;
* [**`orderType`**](#ordertype)**:** Specifies the order type: LIMIT or MARKET. (See Enums)
* **`openedPrice`:** The price at which Party B opened the position .
* **`initialOpenedPrice`:** The initial opened price when the position was first executed.
* **`requestedOpenPrice`:** The price that Party A requested for opening the position (in 18 decimals).
* **`marketPrice`:** The current market price as provided by the Muon signature (in 18 decimals).
* **`quantity`:** The total quantity requested by Party A (expressed in 18 decimals).
* **`closedAmount`:** The amount of the position that has been closed so far (in 18 decimals).
* [**`initialLockedValues`**](#lockedvalues)**:** The original locked values as set by Party A.&#x20;
* [**`lockedValues`**](#lockedvalues)**:** The current locked risk parameters. In partial fill scenarios, this reflects the remaining locked values after part of the quote has been executed.
* **`maxFundingRate`:** The maximum funding rate that Party A requested for the quote (in 18 decimals).
* **`partyA`:** Party A (the trader).
* **`partyB`:** Party B (the hedger/solver), if assigned.
* [**`quoteStatus`**](#quote)**:** The current state of the quote (e.g., PENDING, OPENED, CLOSED).
* **`avgClosedPrice`:** The average price at which parts of the quote have been closed.
* **`requestedClosePrice`:** The price at which Party A requests the position to be closed.
* **`quantityToClose`:** The amount of the position that Party A intends to close.
* **`parentId`:** The identifier of a parent quote, used when the quote is partially filled.
* **`createTimestamp`:** The timestamp when the quote was created.
* **`statusModifyTimestamp`:** The timestamp of the last update to the quote’s status.
* **`lastFundingPaymentTimestamp`:** The time at which the last funding payment was charged on this quote.
* **`deadline`:** The deadline by which the quote must be executed or closed.
* **`tradingFee`:** The fee associated with trading, deducted from the quote (this is only charged on **open**)
* **`affiliate`:** The affiliate address associated with the quote, linking the quote to a frontend.

***

### Symbol

The Symbol struct contains data related to each symbol.

```solidity
struct Symbol {
	uint256 symbolId; //id
	string name;
	bool isValid; //Valid if the symbol is listed
	uint256 minAcceptableQuoteValue; //Smallest acceptable notional value
	uint256 minAcceptablePortionLF; //Minimum acceptable fee to be rewarded to the liquidator
	uint256 tradingFee; //Trading fee, this is only charged on open
	uint256 maxLeverage; //Max Leverage
	uint256 fundingRateEpochDuration; //How long each funding rate epoch is
	uint256 fundingRateWindowTime; //The window of time in which funding can be charged per epoch
}
```

## Enums

### **PositionType**

```solidity
enum PositionType {
	LONG, //0
	SHORT //1
}
```

### **OrderType**

```solidity
enum OrderType {
	LIMIT, //0
	MARKET //1
}
```

### **QuoteStatus**

```solidity
enum QuoteStatus {
	PENDING, //0
	LOCKED, //1
	CANCEL_PENDING, //2
	CANCELED, //3
	OPENED, //4
	CLOSE_PENDING, //5
	CANCEL_CLOSE_PENDING, //6
	CLOSED, //7
	LIQUIDATED, //8
	EXPIRED, //9
	LIQUIDATED_PENDING //10
}
```


# Legacy Guides


# MultiAccount (Legacy Guide)

{% hint style="info" %}
MultiAccount has been replaced with the AccountLayer in the newest 0.8.5 contracts. Refer to [Account Layer Diamond](/contract-documentation/symmio-perps-v0.8.5/account-layer-diamond) and [Broken mention](broken://pages/JUhmUe9W3CGmYkpam5oA)for more info.
{% endhint %}

The MultiAccount contract is a critical component of the SYMM architecture that enables users to create dedicated sub‑accounts.&#x20;

MultiAccounts are contracts typically deployed by **frontend builders** that are whitelisted by hedgers. They enable users to create sub-accounts to trade with.&#x20;

These sub‑accounts are smart contracts **deployed via the MultiAccount contract**. By default, all positions within an account are considered for liquidation, each account is in CROSS. By using sub‑accounts, users can isolate risk by using separate sub‑accounts.

<figure><img src="/files/BhL5PvuKHJN8eIruAYoN" alt=""><figcaption></figcaption></figure>

## Deploying a MultiAccount

Frontend Builders must deploy a MultiAccount and get whitelisted by SYMM. If you're a frontend looking to deploy a MultiAccount, please refer to this [guide](https://symmdocs.gitbook.io/multiaccount-deployment-guide/).

## MultiAccount Flow

### **How It Works:**

1. **Account Creation:**\
   Your wallet calls the MultiAccount contract and specifies which sub‑account to use for executing functions. When you create a sub‑account using `addAccount()`, a new contract is deployed using the current account implementation and linked to your wallet as the owner.
2. **Forwarding Calls:**\
   When you call the `_call()` function on MultiAccount, the call is forwarded to your sub‑account, which in turn sends the call to the SYMM Diamond. Since the Diamond’s logic operates based on the `msg.sender`, actions must originate from the sub‑account to correctly update storage and state.
3. **Parameter Encoding:**\
   All function calls are encoded using the SYMM Diamond’s ABI. This means you must encode the parameters for the desired function using the ABI, and pass that data to the `_call()` method. For example:

   ```javascript
   const encodedSendQuoteWithAffiliateData = web3.eth.abi.encodeFunctionCall(sendQuoteWithAffiliateFunctionAbi, sendQuoteParameters);
   const _callData = [ subAccountAddress, [ encodedSendQuoteData ] ];
   const sendQuoteWithAffiliateReceipt = await multiAccountContract.methods._call(..._callData).send({
     from: account.address,
     gas: adjustedGasLimit.toString(), 
     gasPrice: sendQuotePrice.toString() 
   });
   ```

{% hint style="info" %}
The MultiAccount contract address is also known as the affiliate address. The affiliate allows for automatic tracking and claiming of platform fees generated by a frontend.
{% endhint %}


# MultiAccount Deployment Guide (Legacy Guide)

{% hint style="info" %}
MultiAccount has been replaced with the AccountLayer in the newest 0.8.5 contracts. Refer to [Account Layer Diamond](/contract-documentation/symmio-perps-v0.8.5/account-layer-diamond) and [Broken mention](broken://pages/JUhmUe9W3CGmYkpam5oA)for more info.
{% endhint %}

This document outlines the required steps and responsibilities for deploying the MultiAccount contract.

1. **Responsibility and Security** The full responsibility for the deployment and ongoing management of the MultiAccount contract lies with the deploying entity. To ensure the integrity and security of the contract, please take the following actions:
   * Conduct thorough audits of the contract code.
   * Implement robust security checks.
   * Ensure regular maintenance and monitoring of the deployed contract.
2. **Deployment of Latest Version** It is essential to deploy the latest version of the MultiAccount contract. The most current version can be found at the following link:\
   [MultiAccount Contract](https://github.com/SYMM-IO/protocol-core/blob/main/contracts/multiAccount/MultiAccount.sol)

Please note that the MultiAccount contract available in this repository is a sample code. You are free to modify or customize the code to meet your specific requirements. However, be aware that any modifications or changes to the contract may extend the time required for the registration process, as the Symmio team will need to thoroughly review and approve the changes.

3. Transfer of Admin Roles and ProxyAdmin Ownership Upon successful deployment, the following steps must be completed to ensure secure management of the contract:
   * Transfer the admin roles to the appropriate entity.
   * Transfer ownership of the ProxyAdmin to a 3-day timelock contract for added security.

These measures are necessary to safeguard the contract against unauthorized access or modifications.

4. **Proposal to Symmio Team** After deploying the contract and completing the necessary security measures, a formal proposal must be submitted to the Symmio team. The proposal should request that the deployed MultiAccount contract address be registered in the Symmio Core Contracts. If any modifications have been made to the original sample contract, please note that the registration process may take longer due to the required review of the changes by the Symmio team.

**Prerequisites**

* Install Node.js and npm.
* Install Hardhat in your project directory: `npm install --save-dev hardhat`
* Install Typescript and `ts-node`: `npm install --save-dev typescript ts-node`

## Step 1: Clone the Repo

```bash
git clone https://github.com/SYMM-IO/protocol-core
cd protocol-core
```

## Step 2: Prerequisites

**Ensure you're working with Node 20 LTS** (Hardhat doesn’t support Node 21)

```bash
node -v            # should show v20.x
# if not:
nvm install 20
nvm use 20
node -v
```

## Step 3: Create a `.env`

At the repo root, create a `.env`. Within the repo you'll find a `.env.example`. Copy the example over into the .env and populate the fields appropriately.

```ini
MANTLE_API_KEY=""
MANTLE2_API_KEY=""
BNB_API_KEY=""
BLAST_API_KEY=""
BASE_API_KEY=""
POLYGON_API_KEY=""
ARBITRUM_API_KEY=""
IOTA_API_KEY=""
MODE_API_KEY=""
BERA_API_KEY=""

PRIVATE_KEY=""
PRIVATE_KEYS_STR=""
ADMIN_PUBLIC_KEY=""
HARDHAT_DOCKER_URL="http://eth-node:8545"

TEST_USER_BALANCE=5000
TEST_USER_MAX_QUOTE=100
HEDGER_WEB_SERVICE="http://127.0.0.1:8090"
TEST_USER_TIMEOUT=120000
LOG_LEVEL=debug
DEPLOY_MULTICALL=false
VALIDATION_PROBABILITY=0.2
DEPLOY_MULTICALL=true

TEST_MODE={static,pre-upgrade,fuzz}
```

The required variables for deployment are here:

```ini
ETHERSCAN_API_KEY=YOUR_ETHERSCAN_V2_KEY
PRIVATE_KEY=0xYOUR_DEPLOYER_PRIVATE_KEY
ADMIN_PUBLIC_KEY=0xYOUR_ADMIN_PUBLIC_KEY
```

{% hint style="info" %}
Use **`ETHERSCAN_API_KEY`** (single key). V1 per-network keys are now deprecated. Read more about the changes [here](https://docs.etherscan.io/).
{% endhint %}

## Step 4: Update `hardhat.config.ts` (Etherscan V2)

Replace your `etherscan` section in `hardhat.config.ts` with **one key** and **clean URLs** (no `?apikey=` anywhere): Example:

```tsx
etherscan: {
  apiKey: process.env.ETHERSCAN_API_KEY || "", //Just using the V2 API
  customChains: [
    // Keep only chains you actually verify on.
    // IMPORTANT: no "?apikey=..." in apiURL anymore.
    {
      network: "zkEvm",
      chainId: 1101,
      urls: {
        apiURL: "https://api-zkevm.polygonscan.com/api",
        browserURL: "https://zkevm.polygonscan.com",
      },
    },
    {
      network: "opbnb",
      chainId: 204,
      urls: {
        apiURL: "https://api-opbnb.bscscan.com/api",
        browserURL: "https://opbnb.bscscan.com",
      },
    },
    {
      network: "iota",
      chainId: 8822,
      urls: {
        apiURL: "https://explorer.evm.iota.org/api",
        browserURL: "https://explorer.evm.iota.org",
      },
    },
    {
      network: "mode",
      chainId: 34443,
      urls: {
        apiURL: "https://api.routescan.io/v2/network/mainnet/evm/34443/etherscan",
        browserURL: "https://modescan.io",
      },
    },
    {
      network: "blast",
      chainId: 81457,
      urls: {
        apiURL: "https://api.blastscan.io/api",
        browserURL: "https://blastscan.io",
      },
    },
    {
      network: "mantle",
      chainId: 5000,
      urls: {
        apiURL: "https://api.mantlescan.xyz/api",
        browserURL: "https://mantlescan.xyz",
      },
    },
    {
      network: "sonic",
      chainId: 146,
      urls: {
        apiURL: "https://api.sonicscan.org/api",
        browserURL: "https://sonicscan.org",
      },
    },
    {
      network: "bera",
      chainId: 80094,
      urls: {
        apiURL: "https://api.routescan.io/v2/network/mainnet/evm/80094/etherscan",
        browserURL: "https://beratrail.io",
      },
    },
  ],
},

```

## Step 5: Installing Dependencies&#x20;

**Install hardhat via the terminal (if not done already):**

```bash
npm install --save-dev hardhat
```

## Step 6: Compile the Contracts

```bash
npx hardhat compile
```

## Step 7: Deploying a MultiAccount Contract (Polygon)

In this example, we're deploying a MultiAccount contract on **Polygon**, using the address of the diamond deployed there (`--symmio-address`). A comprehensive list of all the deployed Diamond addresses can be found [here](broken://pages/aqT95qOMDcvaUY5aCr93).

```bash
npx hardhat deploy:multiAccount --symmio-address 0x976c87Cd3eB2DE462Db249cCA711E4C89154537b --admin 0x3B5aC601c7bB74999AB3135fa43cbDBc6aB74570 --network polygon
```

This will print to the terminal:

```
Running deploy:multiAccount
Deploying contracts with the account: 0x3B5aC601c7bB74999AB3135fa43cbDBc6aB74570
0x3B5aC601c7bB74999AB3135fa43cbDBc6aB74570 0x976c87Cd3eB2DE462Db249cCA711E4C89154537b
MultiAccount deployed to {
  proxy: '0x69192790420cd7e97f231BB73B9c9C5d914A2fc8',
  admin: '0xC0788bB9293e53703a4e3921409849FAE006306C',
  implementation: '0x0F0711432E871bc0C581DA0cF71fC82565f3e77b'
}
Could not read existing JSON file: Error: ENOENT: no such file or directory, open 'D:\docsmulti\protocol-core\tasks\data\deployed.json'
Deployed addresses written to JSON file
```

You can find the deployed addresses in `/tasks/data/deployed.json`. This file is used for verification in the following step.

```json
[
  {
    "name": "MultiAccountProxy",
    "address": "0x78174a787914Ae4eB8c1EaC87fEa19c547fD474D",
    "constructorArguments": [
      "0x3B5aC601c7bB74999AB3135fa43cbDBc6aB74570",
      "0x976c87Cd3eB2DE462Db249cCA711E4C89154537b",
      "0x60803461016157601f610e2d38819003918201601f191683019291906001600160401b038411838510176101665781606092849260409687528339810103126101615761004b8161017c565b9060206100648461005d83850161017c565b930161017c565b9160009182805282815285832060018060a01b0380961690818552825260ff87852054161561012a575b507febee9ae4283d502e7ed608f7bff6281dc40560c705aa66f8f31324862ed03f5a9081845283815285878520931692838552815260ff8785205416156100f2575b505050501660018060a01b0319600154161760015551610c7c90816101918239f35b8184528381528684209083855252858320600160ff19825416179055600080516020610e0d833981519152339380a4388080806100d0565b8380528382528684208185528252868420805460ff19166001179055339084600080516020610e0d8339815191528180a43861008e565b600080fd5b634e487b7160e01b600052604160045260246000fd5b51906001600160a01b03821682036101615756fe60406080815260048036101561001457600080fd5b600091823560e01c806301569e3a1461062857806301ffc9a7146105d2578063248a9ca3146105a85780632f2ff15d146104ff57806336568abe1461046d57806359f6d3921461043257806362dfbb2e146101665780637278b28f1461013d57806391d14854146100f7578063a217fddf146100d85763d547741f1461009957600080fd5b346100d457806003193601126100d4576100d191356100cc60016100bb610843565b938387528660205286200154610916565b610ab7565b80f35b8280fd5b8382346100f357816003193601126100f35751908152602090f35b5080fd5b5090346100d457816003193601126100d4578160209360ff92610118610843565b903582528186528282206001600160a01b039091168252855220549151911615158152f35b8382346100f357816003193601126100f35760015490516001600160a01b039091168152602090f35b50823461042f57602092836003193601126100f35780359067ffffffffffffffff82116100d457366023830112156100d45781810135906101a6826108b2565b926101b386519485610890565b828452868401926024913683838301011161042b57818792848b93018737860101527febee9ae4283d502e7ed608f7bff6281dc40560c705aa66f8f31324862ed03f5a808652858852868620338752885260ff878720541615610279575050600154925161026c949384938493509083906001600160a01b03165af1923d15610270573d610240816108b2565b9061024d83519283610890565b81528092863d92013e5b808051958695151586528501528301906108f1565b0390f35b60609150610257565b87935086908661028833610b52565b918351906102958261085e565b6042825287820192606036853782511561041957603084538251906001918210156104075790607860218501536041915b81831161039e5750505061037057604861036c9593859361035493610345975197889376020b1b1b2b9b9a1b7b73a3937b61d1030b1b1b7bab73a1604d1b8d86015261031c8d82519283916037890191016108ce565b8401917001034b99036b4b9b9b4b733903937b6329607d1b6037840152518093868401906108ce565b01036028810186520184610890565b5194859462461bcd60e51b86528501528301906108f1565b0390fd5b60648688878188519362461bcd60e51b8552840152820152600080516020610c508339815191526044820152fd5b909192600f811660108110156103f5576f181899199a1a9b1b9c1cb0b131b232b360811b901a6103ce8587610b2b565b53891c9280156103e3576000190191906102c6565b634e487b7160e01b825260118a528882fd5b634e487b7160e01b835260328b528983fd5b634e487b7160e01b8152603289528790fd5b634e487b7160e01b8152603288528690fd5b8680fd5b80fd5b8382346100f357816003193601126100f357602090517febee9ae4283d502e7ed608f7bff6281dc40560c705aa66f8f31324862ed03f5a8152f35b509190346100f357826003193601126100f357610488610843565b90336001600160a01b038316036104a457906100d19135610ab7565b608490602085519162461bcd60e51b8352820152602f60248201527f416363657373436f6e74726f6c3a2063616e206f6e6c792072656e6f756e636560448201526e103937b632b9903337b91039b2b63360891b6064820152fd5b5090346100d457816003193601126100d457359061051b610843565b908284528360205261053260018286200154610916565b82845260208481528185206001600160a01b039093168086529290528084205460ff161561055e578380f35b828452836020528084208285526020528320600160ff1982541617905533917f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d8480a43880808380f35b5090346100d45760203660031901126100d457816020936001923581528085522001549051908152f35b5090346100d45760203660031901126100d457359063ffffffff60e01b82168092036100d45760209250637965db0b60e01b8214918215610617575b50519015158152f35b6301ffc9a760e01b1491503861060e565b50346100d4576020908160031936011261083f576001600160a01b038335818116949085900361083b57858052858452828620338752845260ff8387205416156106b15750907ff78ccdf5924090b2ab6627ac5da4ec5affed73d47c6bc6c8a4620a0d5ed57bc891846001549483519286168352820152a16001600160a01b0319161760015580f35b919050846106be33610b52565b90808351906106cc8261085e565b6042825286820192606036853782511561082857603084538251906001918210156108155790607860218501536041915b8183116107aa5750505061077b5760486107529385936107619361036c975196879376020b1b1b2b9b9a1b7b73a3937b61d1030b1b1b7bab73a1604d1b8c86015261031c8c82519283916037890191016108ce565b01036028810185520183610890565b5193849362461bcd60e51b855284015260248301906108f1565b606485878087519262461bcd60e51b84528301526024820152600080516020610c508339815191526044820152fd5b909192600f81166010811015610802576f181899199a1a9b1b9c1cb0b131b232b360811b901a6107da8587610b2b565b53881c9280156107ef576000190191906106fd565b634e487b7160e01b825260118952602482fd5b634e487b7160e01b835260328a52602483fd5b634e487b7160e01b815260328852602490fd5b634e487b7160e01b815260328752602490fd5b8580fd5b8380fd5b602435906001600160a01b038216820361085957565b600080fd5b6080810190811067ffffffffffffffff82111761087a57604052565b634e487b7160e01b600052604160045260246000fd5b90601f8019910116810190811067ffffffffffffffff82111761087a57604052565b67ffffffffffffffff811161087a57601f01601f191660200190565b60005b8381106108e15750506000910152565b81810151838201526020016108d1565b9060209161090a815180928185528580860191016108ce565b601f01601f1916010190565b600090808252602090828252604092838120338252835260ff84822054161561093f5750505050565b61094833610b52565b918451906109558261085e565b60428252848201926060368537825115610aa35760308453825190600191821015610aa35790607860218501536041915b818311610a3557505050610a0557604861036c9386936109e9936109da985198899376020b1b1b2b9b9a1b7b73a3937b61d1030b1b1b7bab73a1604d1b8a86015261031c815180928c6037890191016108ce565b01036028810187520185610890565b5192839262461bcd60e51b8452600484015260248301906108f1565b60648486519062461bcd60e51b82528060048301526024820152600080516020610c508339815191526044820152fd5b909192600f81166010811015610a8f576f181899199a1a9b1b9c1cb0b131b232b360811b901a610a658587610b2b565b5360041c928015610a7b57600019019190610986565b634e487b7160e01b82526011600452602482fd5b634e487b7160e01b83526032600452602483fd5b634e487b7160e01b81526032600452602490fd5b9060009180835282602052604083209160018060a01b03169182845260205260ff604084205416610ae757505050565b80835282602052604083208284526020526040832060ff1981541690557ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b339380a4565b908151811015610b3c570160200190565b634e487b7160e01b600052603260045260246000fd5b604051906060820182811067ffffffffffffffff82111761087a57604052602a8252602082016040368237825115610b3c57603090538151600190811015610b3c57607860218401536029905b808211610be1575050610baf5790565b606460405162461bcd60e51b81526020600482015260206024820152600080516020610c508339815191526044820152fd5b9091600f81166010811015610c3a576f181899199a1a9b1b9c1cb0b131b232b360811b901a610c108486610b2b565b5360041c918015610c25576000190190610b9f565b60246000634e487b7160e01b81526011600452fd5b60246000634e487b7160e01b81526032600452fdfe537472696e67733a20686578206c656e67746820696e73756666696369656e74a164736f6c6343000812000a2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d"
    ]
  },
  {
    "name": "MultiAccountAdmin",
    "address": "0xB3A34B46a9103fB157b5b1Af4655ec703b1578D2",
    "constructorArguments": []
  },
  {
    "name": "MultiAccountImplementation",
    "address": "0x950BaeF761A17Cd0f77d6dB8008ebeBd011FCA4b",
    "constructorArguments": []
  }
]
```

## Step 8: Verifying the MultiAccount contract

Simply run the verification script with the appropriate network flag to verify the contracts.

```
npx hardhat verify:deployment --network polygon
```

## Troubleshooting

* **“Using deprecated V1 endpoint / API key invalid”** → ensure you have **ETHERSCAN\_API\_KEY** (single key, **not** a string) and your `apiURL`s **don’t** contain `?apikey=..`
* **Node warning** → `node -v` must be **v20.x**.
* **Compiler mismatch** → run `npx hardhat clean && npx hardhat compile`. (Stick with the repo’s existing Solidity settings.)


# Role of a Liquidity Provider (Solver)

On Symmio, traders don't match against a central order book. They express *intents*: requests that say "I want to go long or short this market, here's my size, my price, and the counterparties I'll trade with." Those intents are picked up by a network of independent counterparties called [solvers](/trader-documentation/glossary-of-terms). A solver decides whether to take the other side of a trade, quotes prices, and manages the risk that comes with it.

When you run a solver, you act as [PartyB](/trader-documentation/glossary-of-terms) in the protocol. Your address is whitelisted on the Symmio contracts, so you can lock quotes, open positions, and hold shared risk with traders. Once a trader sends a quote on-chain, your infrastructure listens for the event, pulls in the quote details, and runs internal checks: is the symbol supported, is the account allowed, do you have enough collateral, and does the price fit your strategy? If everything passes, you lock the quote and open the position on-chain, optionally hedging the exposure elsewhere at the same time.

Solvers also manage collateral and solvency on their side. Before a position opens or closes, the contracts and the [Muon oracle](/api-endpoints-and-deployments/muon-api) verify that both you and the trader have enough margin and that the prices are valid. You decide how much capital to allocate to each trader, how much open interest to allow per market, and when to settle unrealized profit so positions can close safely. Your risk model is your own, but it must satisfy the protocol's solvency rules enforced on the contracts.

Solvers also shape the user experience. Frontends rely on solver APIs to show markets, prices, and funding data. When a trader wants to [instantly open a trade](/trader-documentation/how-trading-works-in-symmio), they delegate certain functions to you so you can open and close positions without asking them to sign every transaction. When they set a [take profit or stop loss](/trader-documentation/how-trading-works-in-symmio/using-take-profit-and-stop-loss), your **conditional orders** service watches the market and submits the close request when their conditions are hit. From the trader's side, the frontend feels like a fast, responsive exchange.


# Solving for Symmio

{% hint style="info" %}

### &#x20;Solver Section: Clarifications & Caveats

The Solver section of our guide is crafted with a singular goal in mind: to provide a fundamental understanding of the Solver's role in the Symmio system to MarketMakers, Investors and everyone who is interested in becoming a solver himself. However, it's essential to note a few key points about the information it provides.

1. **Educational Use**: The Solver section is educational and not a comprehensive reflection of the entire Symmio protocol. It is structured to provide an introductory grasp of what it means to be a hedger and offers a primer on how one can undertake this role.
2. **Avoid Misconceptions**: Some readers may draw the inference from this section that the Symmio system depends on Solvers hedging themselves via risky off-chain actions, leading to assumed trust dependencies on the Solver side. However, this understanding is incorrect and could potentially lead to erroneous interpretations of the system's operation.
3. **Top-Down Relationship**: To reiterate and clarify, the relationship between a Solver and the Symmio protocol is a top-down one. This means that whatever actions a Solver takes off-chain have no bearing on the on-chain events. Thus, there are no trust assumptions regarding the Solver side of things as they use off-chain systems to hedge themselves or the connection between these offchain systems and Symmio itself.&#x20;
4. **Independent On-Chain Contracts**: The Symmio contracts function independently of any off-chain actions performed by a Solver. They operate exclusively within the on-chain environment, and are not influenced or impacted by any external actions.
5. **The** Symmio **system is fully isolated:** Symmio is fully isolated by any issues that may or may not arise from using offchain systems, centralized exchanges, trading desks or any other forms of hedging.<br>

We strongly recommend you to familiarize yourself with these points for a better understanding of the Solver's role and its interaction with the Symmio protocol. It's important to interpret the Hedger section in light of these clarifications to avoid misrepresentations of the Symmio protocol's actual functioning.
{% endhint %}

### Symmio offers a unique opportunity for Solvers <a href="#symmetrial-offers-a-unique-opportunity-for-lps" id="symmetrial-offers-a-unique-opportunity-for-lps"></a>

Symmio, a protocol for on-chain derivatives trading utilizing peer-to-peer bilateral AMFQs or Intents (Automated Request for Quotations), has paved an entirely new way to trade derivatives on-chain, providing significant advantages to early adopter solvers. As the protocol scales, its technological edge and first-mover advantage will become even more valuable, **yielding significant rewards for early adopters in its expansion.**&#x20;

Our Protocol currently offers potential Solvers a unique opportunity to be part of this growth in the early stages.&#x20;

The first Solvers soft committed up to a $20 million open interest range & aims to scale from $20 million to $100 million by onboarding MMs and LPs by incentivizing early Solvers on top of the margins they can earn.

Solvers provide liquidity and earn profits by closing the efficiency gap that on-chain derivatives trading still has to this day.&#x20;

**The first Solvers were earning up to 250% APR on their CEFFU-secured stablecoins deposits.**

## Market potential <a href="#market-potential" id="market-potential"></a>

**On-chain derivatives present a potential trillion-dollar market.**

Currently, the Market anticipation for on-chain derivatives is mimicking the market saturation of Spot on-chain trading prior to uniswap v2 launching.

<figure><img src="/files/1W18dGKxayRc5pKxmp8V" alt=""><figcaption><p>Onchain DEX trading volume vs CEX trading volume in June 2020 around 1.7% right before Uniswap v2 launched in August.</p></figcaption></figure>

The DEX vs. CEX volume 2 months before Uniswap v2’s launch.

Uniswap's V1 version was a proof-of-concept first launched in November 2018 and later improved with the introduction of the V2 version in **August 2020.** Back in June 2020, only 1.69% of the global spot trading volume was done on-chain.

<figure><img src="/files/NbJ6kOCCiuzuSMwDijGR" alt=""><figcaption><p><strong>Only 2% of crypto derivatives trading currently occurs on-chain.</strong></p></figcaption></figure>

Today, a similar market potential exists in the on-chain derivatives trading sector, where **only 2%** of crypto derivatives trading currently occurs on-chain.

This presents a substantial opportunity for Symmio to capture a significant market share in crypto and traditional derivatives. With its innovative, AMFQ instant settlement technology, Symmio is uniquely positioned to become the Uniswap v2 equivalent for on-chain derivatives, revolutionizing the industry and offering exceptional growth potential for early investors.

#### First mover advantage <a href="#first-mover-advantage" id="first-mover-advantage"></a>

We offer Solvers the exclusive opportunity to become one of the first liquidity providers on Symmio. By joining Symmio, you will be providing liquidity for a new trading primitive

1. First-mover advantage: As the first liquidity provider on Symmio, you will enjoy a competitive edge over others in the rapidly growing on-chain derivatives market.
2. Technological superiority: Our platform's technological innovations create a tangible lead in the market, distinguishing Symmio from its competitors and offering you a unique advantage as a liquidity provider.
3. Low-risk profile and risk protection: Symmio architecture ensures a low-risk investment opportunity, allowing you to invest your capital and benefit from this groundbreaking platform. At the same time, keep your funds on a trusted custody service (like CEFFU) or other institutional MPC-based trading desks.

#### Even more efficient. <a href="#even-more-efficient." id="even-more-efficient."></a>

Since quotes in the Symmio system are committed off-chain via frontend applications, Solvers do not need to lock any capital on-chain until they receive a trade request from a user. This allows Solvers to bright platforms and farm with idle capital on various platforms, such as Aave, while waiting for new orders. This increases the efficiency of the platform and yield generation even further. Capital on demand can be withdrawn from Aave and deposited into the platform's on-chain contracts. ***Liquidity is unlocked.***

#### Composability of Symmio <a href="#composability-of-symmetrial" id="composability-of-symmetrial"></a>

On the Symmio (<https://cloverfield.exchange>) MVP, we integrate Binance liquidity on-chain via our system. This enables on-chain users to trade on Binance Futures liquidity for approximately 200 crypto assets.

For providing liquidity on Symmio today, there are two options:

**Option II Invest capital into existing Solvers offering services & that use it to provide liquidity**

* Investors can inject capital directly into the current running Solver services. (private loan)
* The Solver would run all the services and software and manages maintenance.
* Solvers could offer a profit split of 70/30, 60/40, 50/50, or 40/60 that can be negotiated based on the size of Liquidity commitments. (for example, at 250% APR generated after 30 days, using a 40/60 split, up to 100% of generated APR would be paid out on stablecoin deposits).

**Option II. Become your own Solver on** Symmio&#x20;

To begin Solving services on Symmio, you need to follow these steps:

* Set up your trading entity or use an existing one.
* Run a [hedger software.](/liquidity-provider-documentation/solver-documentation-old) (maintenance can be complex at this stage.) The trading software can become outdated during upgrades; it must maintain its up time after any potential future updates to the core software.
* Obtain an institutional Exchange account.
* Have trading capital to deposit on the contract side and Binance to provide incoming trade requests.


# Solver Setup High Level Overview

## Overview

This document provides a quick guide on setting up a Solver with the SYMMIO platform.

### Prerequisites

**Read Symmio's Smart Contracts:** Understanding the smart contracts is essential. Start by reading through the contracts to familiarize yourself with their functions and events. Smart contract documentation (0.8.5) can be found [here](/contract-documentation/symmio-perps-v0.8.5).

## Steps to Set Up a Solver with SYMM

<figure><img src="/files/lG5W0jIR52fjZJxCtsjR" alt=""><figcaption><p>Recommended Architecture for Solvers (See <a href="https://github.com/SYMM-IO/docs/tree/main/hedger_docs">Github </a>Docs)</p></figcaption></figure>

### Smart Contracts and Event Handling&#x20;

To become familiar with the infrastructure it's recommended to develop a bot to read and handle events from the smart contracts. This bot should listen for specific events and then take appropriate actions depending on the function.

Symmio's smart contracts rely on data verification from the Muon network to ensure the validity of asset prices and unrealized profit and loss for parties. This process involves invoking methods on the Muon app before any action is executed on the contract. You can read more about Muon queries [here](/api-endpoints-and-deployments/muon-api).

To accept quotes, you need an address that is whitelisted to act as `partyB` on the contract side. Please contact our developers to get whitelisted.

### Setting Up a Mock Frontend

After getting familiar with the smart contracts and developing your event-handling bot, the next step is to set up a mock frontend.

1. **Obtain a Mock Frontend:**
   * Contact Symmio's development team to get access to a mock frontend. This frontend will allow you to send intents and simulate various user actions.
2. **Send Intents:**
   * Use the mock frontend to send intents to your bot. You can use these intents to test how your bot handles different events and actions.
3. **Write a Script to Accept Intents:**
   * Develop a script that will process and accept the intents sent from the mock frontend.

### Building the Position Manager / Internal Solving Strategy

This involves creating an internal strategy for solving positions based on market conditions and your hedging needs. Hedging strategies are private and may vary. Develop your unique strategy based on your risk management and market analysis.

### Building the Broker Order Pusher

When a quote is seen that adheres to the solving strategy, it's standard procedure to call `lockQuote()` on the contract side, then open a position elsewhere (Binance, etc.) to hedge the position before calling `openPosition()` on the contract. This process can also be done in parallel with Instant Execution, where a user delegates the access of their account to the solver to enable faster trading. To ensure these transactions succeed it's vital to create a module for reliable RPC calls, or use a service like Gelato Relayer.

### Build the On-Chain Balance Manager

One of the services that Solver should run is a position manager to keep tabs on solvency to prevent being liquidated across positions. Create a balance manager to track and manage the health of your positions on-chain which can adjust the collateral balances within each trade.

### Setting up Endpoints/Websockets

To ensure effective communication with frontends and users, you'll need to set up various APIs and Websockets.

**REST APIs**

Set up APIs to provide essential data such as:

* **Available Markets**: Lists the markets that the Solver is offering.
* **Total Open Interest**: Provides data on the total open interest across different markets.
* **Error Codes**: Provides a list of potential error codes and their meanings.
* **User Position Data**: Returns data related to a user's position with the Solver.

**Websockets**

Configure Websockets to provide real-time updates:

* **Funding Rate**: Reflects the current funding rate charged on each symbol.
* **User Position State**: Notifies users about the state of their position.

For an exhaustive list and detailed examples of endpoints and Websockets, please refer to this [documentation](/api-endpoints-and-deployments/solver-addresses-and-endpoints/rasa-capital).

### Build a Position Monitor & Validator

Finally set up monitoring and developer alerts to maintain uptime and reliability.


# Core Concepts


# Understanding the Contract Architecture

At the center of the protocol is a diamond contract (EIP-2535). This serves as a single "hub" address that exposes many different modules, called facets. Each facet handles one area of the system: opening and closing positions, funding, liquidations, force actions, viewing state, and so on. When you call the diamond, it forwards your call to the right facet behind the scenes while sharing the same storage. As a solver, you mostly interact with one main contract address, but still get access to all the protocol's features.

On the trader side, user accounts are managed by the [**AccountLayer Diamond**](/contract-documentation/symmio-perps-v0.8.5/account-layer-diamond)**,** which is a shared contract that all frontends build on top of. Users create SubAccounts under this contract, and each SubAccount can spin up isolated VirtualAccounts (VAs) for individual trades, markets, or directions, so a liquidation in one position can't cascade into another. As a solver, you will see intents and positions tied to these sub-account addresses (Party A), not the user's main wallet.

Your address acts as [Party B](/trader-documentation/glossary-of-terms) inside this architecture. Before you can trade, your solver must be whitelisted as a Party B on the diamond. You have two collateral modes to choose from: **isolated mode**, where you hold separate collateral per counterparty, and **cross mode**, where you pool everything into a single cross-margin bucket and solvency is evaluated against your total pool. Cross mode significantly improves capital efficiency but has implications for settlement, force close, and liquidation that are worth understanding before enabling it. See Cross Mode for details.

For [instant trading](/trader-documentation/how-trading-works-in-symmio/instant-trading) and [conditional orders](/liquidity-provider-documentation/building-a-solver-on-symmio/conditional-orders), the protocol uses the [**Instant Layer**](/options-protocol-architecture/technical-architecture/helper-contracts/instantlayer-contract). Rather than granting open-ended permissions, users sign specific EIP-712 operations with exact parameters. You bundle those with your own operations and submit everything in one transaction via `executeBatch`. Frontends can also generate a temporary session key in the browser at the start of each session, delegating a scoped set of permissions to it so trading feels seamless without repeated wallet popups. See Instant Trading for integration details.

Symbol configuration, minimum position value, funding parameters, open interest caps etc.  is exposed through [view functions](/contract-documentation/symmio-perps-v0.8.5/diamond-core-facets/view-facet) on the diamond. Solvers listen for events from the diamond, reads state from it, and reacts with its own logic.


# Intent Lifecycle and Event Monitoring

Every trade on SYMMIO starts with an **intent** from a trader. Party A sends a quote on-chain that says, in effect: *“I want to trade this symbol, in this direction, with this size and these conditions.”* That quote includes things like symbol ID, position type (long/short), order type (market/limit), requested price, quantity, and the list of Party B addresses they’re willing to trade with. Once the transaction is confirmed, the quote is stored in the diamond’s state and an event is emitted. From that moment on, it’s visible to every solver listening.

On your side, a core piece of your solver is an [**event listener**](/liquidity-provider-documentation/building-a-solver-on-symmio/2.-seeing-the-intent) that watches the chain for these quote events. Whenever a new quote shows up where your Party B address is whitelisted, your system pulls in the full details of that intent and hands it off to your risk and pricing logic. This is where you decide: *Do I want this trade or not?* You might check symbol settings, notional limits, spread, available hedging inventory, or internal risk scores before you make a decision.

If you like the intent, the next step is to [**lock** ](/liquidity-provider-documentation/building-a-solver-on-symmio/4.-opening-closing-a-position-on-chain)it. Locking a quote tells the protocol, “I’m taking this one, no one else can fill it now.” You allocate the required collateral on your side, call the appropriate Party B function on the diamond. Assuming all checks pass, the quote status moves from `PENDING` to `LOCKED`. From there you either immediately open the position or, in a two-step flow, open it shortly after once your hedge is in place. If you only want to fill part of the size, you open a partial amount and the remaining quantity can be re-posted as a new quote.

Solvers ought to track the life cycle of each quote. This is done by listening for events that tell you when a trader has requests to cancel their quote, to close a position, or cancel that close request. You monitor status changes like `OPENED`, `CLOSE_PENDING`, `CANCELED`, and `LIQUIDATED`, and keep your own internal state in sync. When something unexpected happens (like a force close or a failed transaction) you use those same events to reconcile your positions and risk engine.


# Hedging Strategies

At a high level, there are two choices with regards to hedging: **hedge** or **no-hedge**. A fully hedged solver will try to be as close to delta-neutral as possible. When you open a long for a trader on SYMMIO, you might immediately open a matching long on a CEX or another venue. If the trader wins, your off-chain hedge wins by roughly the same amount, so your net result is close to flat. Your profit then comes from spreads, fees, funding, or other edges you’ve built into your quoting model.&#x20;

On the other end of the spectrum, a solver can decide not to hedge at all, or to hedge only partially. In that case you’re acting more like a traditional market maker or prop desk: you take directional views, you might skew your quotes based on what positions you already have, and you accept that your PnL will fluctuate with the market. SYMMIO doesn’t enforce a particular style. The protocol only cares that you stay solvent under its rules.&#x20;


# Settlement and Profit Realization

On SYMMIO there’s a distinction between unrealized PnL (what a position is worth on paper) and allocated balance (real funds you can use to pay losses or withdraw). Settlement is the mechanism that lets you turn some of that unrealized profit into real, spendable balance so the system can keep functioning smoothly.

A classic situation looks like this: a trader has several winning positions with you, plus one losing position they want to close. On paper they’re in profit overall, but their allocated balance is too low to pay for the loss on the closing trade. If you try to close it directly, the protocol blocks the operation because Party A doesn’t have enough realized collateral. From your point of view, you know they’re good for it—their winners cover the loss—but the contract only sees that their allocated balance is short. That’s where you, as Party B, would invoke a [settlement](/trader-documentation/how-trading-works-in-symmio/settlement).

From a risk point of view, settlement is one of your main tools to avoid “good” traders getting stuck. If you never settle, you’ll eventually run into cases where a profitable account can’t close a losing leg because their realized balance is too low.&#x20;


# Instant Trading

Instant trading is where solvers really shape the user experience. From a trader’s perspective, it feels like a CEX: they click “Buy” or “Sell” and the trade just goes through, without a wallet popup for every action. Under the hood, that’s only possible because the trader has delegated certain permissions to you, the solver, and you’ve wired your systems to act immediately when they send an instruction.

The flow starts with the trader creating a sub-account through the Multi-Account contract and then delegating a small set of functions to your Party B address. They approve things like `sendQuote`, `requestToClosePosition`, `requestToCancelQuote`, and `requestToCancelCloseRequest` for that sub-account. This doesn’t give you custody of their funds; it just lets you submit the same trading calls they could make themselves, without needing their wallet to sign every time. On top of that, they go through a login flow (often SIWE-based), where they sign a message off-chain to prove they control the account. Your backend verifies that signature and returns an access token that frontends can attach to every API call.

Once that is in place, instant trading becomes possible through simple [API calls](/liquidity-provider-documentation/building-a-solver-on-symmio/instant-trading/instant-trading-for-liquidity-providers). When the user hits “Open Long 10x” in the UI, the frontend sends a request to your hedger API with their sub-account, market, size, and any extra parameters. Your system does all the same [work as it would when opening a regular position](/liquidity-provider-documentation/building-a-solver-on-symmio/instant-trading/instant-trading-condition-checks) and then calls the SYMMIO contracts directly using the delegated functions. There is no pause to wait for a wallet signature because you are the one submitting the transaction. When the user hits “Close,” the same thing happens: the frontend calls you, you verify, and you submit the close on-chain on their behalf.


# Solver Example Flow

Below is an example of how the hedger flow and connection could work with Symmio.

As the permissionless lightweight nature of the protocol sets only a few rules like how a Intent or Quote need to be structured and responded, MarketMakers can create their own 100% unique MarketMaking and Hedging strategies.

Frontend Builders and Drivers might require MarketMakers to provide a list of Markets and a Bid, Ask and spread for each market, as well as a Range of CVA, preference in LiquidationFee & Funding Rates can all be defined by MarketMakers, there are no requirements by the protocol.

Providing infinite customizability and a focus on fair competition to create the best long term sustainability and offer best executions for traders.&#x20;

## Symmio: the customize everything protocol

<figure><img src="/files/ma42XN3OH7QOUtglIpjM" alt=""><figcaption><p>As lightweight and customizable as the system promises to be everything can be customized.</p></figcaption></figure>

Virtually any custody or collateral solution can be used to interact with any secondary market, all markets or strategies can be used to hedge & any frontend or decentralized driver system can be used to offer services, Symmio is truly **the customize everything protocol.**

## The SYMM Solver Multiverse

#### Every individual Solver stands but as a single thread in the vast tapestry of potential infinite Solvers, and every unique "solver flow" is only a part of an infinite array of ever-competing "solver flows".

<figure><img src="/files/5lFh2XgznRUwxnEYCMOf" alt=""><figcaption></figcaption></figure>

The core contracts only inherit very little complexity, the overall system allows for infinite complexity.

As Solvers offer their Quotes to a "Solver pool" either via [Frontend Builder Introduction](/exchange-builder-documentation/frontend-builder-introduction)or Drivers (TBD) users can select to trade upon them, there are rules on how Solvers should communicate their quotes to the frontend, but there are no rules how a Solver should hedge themselfs against them.

Therefore creating an infinite amount of possible "Solver flows".

But as a Solver what is really required from you?\
Stream, Commit, Detect, Execute & Rebalance
-------------------------------------------

1. Stream a list of supported markets, so frontend builders can display them more easily.
2. Commit to a Bid and Ask spread for each market, so users dont need to request them.
3. Detect when a user requests a trade, be as fast as you can for a good UX.
4. Execute the trade when a user requests it, dont let them hanging.
5. Rebalance your collateral so your positions stay healthy, to not get liquidated.

{% hint style="info" %}
Hedgers should strive to achieve a High response rate. (not responding to quotes is currently not punishable, but will potentially be punishable in the future.)
{% endhint %}

## Let's dive into the first example Symmio Solver flow:

### Please note that this is only an EXAMPLE flow for a Liquidity Provider or Solver on the Symmio protocol, Symmio itself and the team behind Symmio only provide example software that can be used to create own Solving tools.

### Step 1 - Deposit

<figure><img src="/files/jzJtaFQBp3v3dX0GVUYB" alt=""><figcaption><p>MarketMakers deposit margin on-chain and in Binance Custody, then mirror it to the Binance Futures Account.</p></figcaption></figure>

### Step 2 - Prepare

<figure><img src="/files/cWikWL3jw6Y1pI2mHvr8" alt=""><figcaption><p>Set up the hedging software.</p></figcaption></figure>

### Step 3 - Stream

<figure><img src="/files/AVs3PngGR4Zn5w9Yu6lh" alt=""><figcaption><p>Receive, stream prices, and listen to incoming requests.</p></figcaption></figure>

### Step 4 - Request&#x20;

<figure><img src="/files/8m8PfXMyCczO6XMsCURj" alt=""><figcaption><p>A user visits the Pear Frontend, selects an asset, and sends an AMFQ request for a trade on-chain.</p></figcaption></figure>

### Step 5 - Detect, Hedge & Execute

<figure><img src="/files/ELnXqQ0QLqAt6A5TZp4O" alt=""><figcaption><p>The MarketMaker(Hedger) detects, reserves, hedges, and fills the quote on-chain.</p></figcaption></figure>

### Step 6 - Rebalance

<figure><img src="/files/6ouqMzByGZEyEyqRCaH5" alt=""><figcaption><p>Positions need to be maintained by all parties at all times, otherwise, 3rd party liquidators flag them as liquidated and earn the liquidation fee as a prize.</p></figcaption></figure>

### Step 7 - Customize, Optimize & Expand

<figure><img src="/files/eeoOUxo8YIClalrIoyC4" alt=""><figcaption><p>As all Frontend Builders, Driver systems, Exchanges, Trading Desks, margin accounts, custody solution, wallets and forms of hedging can be swapped, rotated, infinitely customized and perpetually optimized, best execution prices for traders and everlasting efficient profits for market makers are possible, the possibilities for refinement are sheer endless.</p></figcaption></figure>

#### The true innovation sparks from the adopters of the Symmio system, who perpetually iterate and enhance, continuously propelling the system into a realm of infinite optimization and continuous improvement.

#### The Symmio team is excited about all the beautiful innovations that are yet to be built on top of our core system.




---

[Next Page](/llms-full.txt/1)

