# What is SYMMIO?

## The Symmio Protocol - a trustless clearing house for derivatives

Symmio is a trustless hybrid (combining on and off-chain) clearing house acting as communication, settlement & clearing layer for permissionless derivatives; without relying on traditional liquidity pools or order books. Instead of trading against a pool, you trade directly against professional market makers (called [**solvers**](/getting-started/glossary-of-terms)) who lock up collateral to guarantee the trade. It's peer-to-peer derivatives, settled on-chain.

{% 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 (**[**https://intentx.io**](https://carbon.inc/)**,** [**https://vibe.trading/**](https://vibe.trading/)**,** [**https://thena.fi**](https://perps.thena.fi/)**)** on several blockchains, including on BNB.&#x20;

We are currently generating $7 Billion in monthly volume with over $40 Billion in total volume (source: <https://analytics.symm.io>) .&#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?

* **No liquidity pools.** Trades are bilateral agreements between you and a solver. No slippage from pool mechanics, no liquidity fragmentation.
* **Any asset, any direction.** Solvers can create markets for anything with a price feed — crypto, stocks, commodities, FX, even prediction markets. The protocol doesn't limit what can be traded.
* **On-chain settlement, off-chain speed.** Quotes and pricing happen off-chain for speed. Settlement and collateral are fully on-chain for security.
* **Permissionless.** Anyone can build a frontend. Anyone can become a solver. The protocol is the shared infrastructure underneath.

## The Gateway to OTC derivatives

**S‍ymmio allows anyone to permissionlessly** issue and trade any asset **with infinite leverage, in every direction.** The only DeFi primitive this industry needs. For example through our engine solvers can create:

<details>

<summary><strong>List of potential Assets</strong></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), allowing users to place call and put options across a diverse range of assets.
{% endhint %}

### Who uses Symmio?

* **Traders** access Symmio through frontends like IntentX, Thena, Based Markets, and others. You don't interact with Symmio directly — you use an exchange built on top of it.
* **Frontends** (exchanges) build trading interfaces on Symmio's infrastructure. They handle UX, branding, and user acquisition while Symmio handles settlement.
* **Solvers** provide liquidity by taking the other side of trades. They're professional market makers who hedge risk externally and compete to offer the best prices.


# Core Concepts

## The Bilateral Model: PartyA and PartyB

Most crypto derivatives today are traded on order-book exchanges. Traders place bids and asks on a shared book, and a matching engine decides when two orders meet.

Instead of using a centralized order book, traders on SYMMIO express intents. An intent is simply a request that says: *“I want to go long or short this market, here is my price, my size, and the collateral I’m using.”* When a trader submits this intent, it becomes visible to a network of independent counterparties called [**solvers**](/getting-started/glossary-of-terms). Each solver can decide whether they want to take the other side of that trade. So rather than fighting for priority in a centralized orderbook, traders using SYMMIO simply declare what they want and the market comes to them.

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>

To make this possible, SYMMIO introduces three main roles:&#x20;

* [**Party A**](/getting-started/glossary-of-terms): the trader who creates an intent.&#x20;
* [**Party B**](/getting-started/glossary-of-terms): also called the solver or hedger, who chooses to accept that trade.&#x20;
* [**Liquidators**](/getting-started/glossary-of-terms): impartial actors who monitor positions and step in if collateral falls below safe levels.&#x20;

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

When a solver accepts a trader’s intent, the position opens on-chain, with collateral from both parties locked into the protocol. When the user wishes to close his position, he simply declares an intent to close his position, the solver handles the close request, and both parties' balances are updated.

## Intent-Based Trading: How Trades Actually Happen

Trading on Symmio follows a simple flow:

1. **You send an intent** — "I want to go 10x long on ETH at this price"
2. **A solver sees it** — They decide if they want to take the other side
3. **They lock it in** — The solver accepts, and both parties lock collateral
4. **Position is open** — You now have a live position against that solver
5. **Close when ready** — The trader can request to close; settlement happens on-chain

This is different from AMMs (where you swap against a pool) or order books (where you match with anonymous orders). Here, a professional counterparty explicitly agrees to your trade.

## Collateral, Margin, and CVA

Three terms you'll see everywhere:

**Collateral** — The actual funds you deposit (usually USDC). This is your money in the system.

**Margin** — The portion of your collateral backing a specific position. With 10x leverage, a $1,000 position requires $100 margin.

**CVA (Credit Valuation Adjustment)** — A small buffer on top of margin that protects against counterparty default. Think of it as a security deposit that covers the worst-case scenario if the other party can't pay.

Here's how they relate:

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

When you open a position, margin + CVA get locked. If the trade goes badly and your losses approach your total collateral, you risk liquidation.


# I'm Interested In...

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

* Trade existing markets on a frontend: [Choosing an Exchange](/trader-documentation/choosing-an-exchange) & <https://symm.io/frontends>

### Learning about how trading works in SYMMIO?

* How SYMMIO's intent-based system works
* Market vs Limit Orders in SYMMIO
* How Funding Rates are charged in SYMMIO
* Liquidation details in SYMMIO
* Querying on-chain data and interacting with contracts
* Higher Level Architecture

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
* Encoding and formatting on-chain transactions

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

### Becoming a Frontend Builder?

* Frontend Builder setup
* Deploying a MultiAccount contract
* Frontend SDK
* Supporting Instant Trading & Instant Withdrawal

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

### Becoming a Solver on SYMMIO?

* Solver API requirements
* Offering New Markets as a Solver
* Conditional & instant order support

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

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

* Frontend Affiliate addresses
* Diamond contract addresses
* Solver API endpoints and documentation

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?

* [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
* SYMMIO Diamond SDK&#x20;

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


# Glossary of Terms

### PartyA

A user who creates Intents (trading requests). PartyA trades through a frontend.

### PartyB

PartyB is the TradFi term used to describe a 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>" %}

PartyB is a liquidity provider on SYMMIO who sees user “Intents” (trades) and decides whether to accept or reject them. Can hedge trades off-chain (e.g., with a centralized or decentralized broker) or choose to stay unhedged.

## Solver&#x20;

Solver is the established industry term for counterpartys in intent-based systems, PartyBs in Symmio can also be referred as Solver, it makes sense to especially refer to the onchain parts of a PartyB as "Solver", there could be Counterparties that exist as multiple entities, solvers that handle the onchain execution and MarketMakers or Hedgers that handle offchain liquidity.

### Hedger / Solver (PartyB)

A liquidity provider on SYMMIO who sees user “Intents” (trades) and decides whether to accept or reject them. Can hedge trades off-chain (e.g., with a centralized or decentralized broker) or choose to stay unhedged.

### Broker

Any external platform or strategy a Hedger uses to hedge. This could be a centralized exchange (Binance, ByBit), a DEX, an OTC desk, or even an internal algorithm. SYMMIO does not require Hedgers to hedge; it is entirely optional. Off-chain actions have no bearing on on-chain states or trust assumptions.

### Frontend Builder

A developer or team that builds a trading interface on top of SYMMIO. They deploy their own MultiAccount contract, connect to one or more Solvers, and provide the UI/UX for traders (PartyA). Examples include IntentX, Thena, and Based Markets. SYMMIO handles settlement; the frontend handles everything user-facing.

### Intent

A proposed trade from PartyA. Examples: “I want to open a LONG position for BTCUSDT at price X with quantity Y.” Hedgers see these 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, etc. Off-chain hedging (if any) is the Hedger’s private business logic and does not affect the contract’s rules or solvency checks.

### Directional Exposure

If a Hedger chooses not to hedge its exposure, it is essentially taking a directional bet on the market. A fully hedged approach (1:1) is “delta-neutral,” meaning the Hedger never maintains directional exposure.

### Liquidators

Once an Intent is opened on-chain, it becomes a position. If either side (PartyA or Hedger) becomes insolvent, a liquidator can step in to close the position forcibly. No trust is placed on the Hedger’s off-chain solvency; only on-chain collateral and UPNL.

### Platform Fee:     &#x20;

Charged to PartyA upon opening a position. The hedger spread is embedded in the price.

### Liquidator Fee (LF)

Incentive for liquidators in a forced liquidation.

### Counterparty Valuation Adjustment (CVA)

Portion of notional position value paid in case one side gets liquidated and paid to the other side. Read more [here](/liquidity-provider-documentation/solver-settings/maintenance-margin-cva-calculations).

### Funding Rate

A periodic charge, often determined by oracles. If a Hedger or PartyA is short, they may pay a funding rate to the long side (or vice versa).&#x20;


# Symmio Foundation

## All official information related to&#x20;

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

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

1. [**symmio.foundation**](https://symmio.foundation)\
   Visit the Symmio Foundation's official website for high-level overviews, announcements, and foundational insights about the $SYMM token and the ecosystem.
2. [**docs.symmio.foundation**](https://docs.symmio.foundation)\
   Explore the comprehensive documentation hub for in-depth details, technical specifications, and token-related policies.

These platforms are the sole authoritative sources for $SYMM token information. Please refer to these sites to ensure you have the most accurate, up-to-date, and reliable details about the $SYMM token and its role within the Symmio ecosystem.

For any further inquiries, you can reach out to the Symmio team through the official communication channels listed on these sites.


# How Trading Works in SYMMIO


# Market vs. Limit Orders

## Sending a Quote

Every trade on Symmio begins as a quote, which is your request to open (or close) a position at a particular price, against a particular set of solvers.&#x20;

There are two order types: **market** and **limit**. This page covers what each one means on Symmio specifically, because the peer-to-peer model gives them slightly different mechanics than you'd see on a typical order book exchange.

### Market Orders

A **MARKET** order tells the solver: "fill me at whatever the current market price is, right now"*.*

On Symmio, a market order is still a quote sent to a [solver](/getting-started/glossary-of-terms); there's no shared order book. When you submit a market order, the frontend asks a solver (or a set of solvers they've whitelisted) to fill it at the current oracle price. Solvers that agree respond almost immediately, and the position opens at a price very close to the one your frontend showed you when you clicked the button.\
\
Because the intent with a market order is for the trader to enter a position immediately, the quote price is sent with some slippage, guaranteeing his entry. The [frontend ](/getting-started/glossary-of-terms)will handle the slippage calculation after the user sets his tolerance, which involves taking the oracle price from Muon and shifting it slightly depending on the position type (long or short). The shift accounts for the solver’s spread as well as any fluctuations in market price For market orders the trading fee is calculated as a fraction of the `oracle price * quantity`.

### Limit Orders&#x20;

A limit order tells the solver: "fill me at this specific price or better, or don't fill me at all"*.*&#x20;

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

Limit orders can be partially filled. The `price` passed is the exact price the trader wants to enter a position at, and the trading fee is calculated as a fraction of `requestedOpenPrice * quantity`.&#x20;

## Closing a Quote

Similar to opening a quote, a **market** order must be fully closed, whereas a **limit** order can be partially closed. The same logic for market orders applies for closes: The user will set his slippage, and the frontend will calculate the appropriate `closePrice`. Here the logic is inverted, for long positions the `closePrice` is decreased; for shorts it’s increased.&#x20;

The trader can initiate a [force close](/trader-documentation/how-trading-works-in-symmio/force-closing-positions) if the counterparty is unresponsive, and a sufficient cooldown period has passed. Force closes can **only** be performed on Limit orders.

### Quote Data

When your frontend submits a quote, it can optionally attach some 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" %}
The frontend you use may use this data it to link your on-chain position to extra off-chain information it shows you in its UI. (for example when running campaigns)
{% endhint %}


# Using Take Profit and Stop Loss

### Introduction

Most [frontend builders](/exchange-builder-documentation/frontend-builder-introduction) on SYMMIO give you a way to set take profit (TP) and stop loss (SL) levels, so you don’t have to sit and watch the chart all day. When your target or safety level is reached, the system will try to close your position for you.

Under the hood, this can work in two main ways:

* The frontend asks you to [delegate permission](/trader-documentation/building-a-trading-bot/delegating-access-to-the-solver) to a small service that is allowed to request closes on your behalf when certain price conditions are met, or the frontend application talks directly to a solver’s [Conditional orders API](/liquidity-provider-documentation/building-a-solver-on-symmio/conditional-orders), and the solver manages TP/SL for you.

Both approaches end up doing the same thing from your perspective: when price hits your TP or SL, a close request is sent for your position.

#### How it works for you as a trader

The exact UI will differ between frontends, but in most cases it will follow this pattern:

#### **Open a position**

Trade as usual. Once your position is open, go to the **“**&#x50;osition&#x73;**”** or **“**&#x4F;pen Position&#x73;**”** tab of the app.

#### **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 TP/SL control**&#x20;

The first time you set up TP/SL, you’ll usually be asked to **approve** either conditional order functionality or instant trading, which both enable conditional order management. This approval lets the frontend or the solver request to close your position for you when the conditions you set 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 Take Profit and Stop Loss**

Once TP/SL is enabled, you can set:

* A Take Profit level – where you’re happy to lock in gains
* A Stop Loss level – where you want to cut the trade to cap downside

Most frontends will let you use percentage-based settings 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 will translate your choices into a conditional order payload, including the **trigger price** (where TP/SL should activate), and the minimum acceptable execution price, which builds in some slippage protection.&#x20;

#### **Confirm and save**

After you’ve chosen your levels, confirm in the UI. From this point on, as long as your position remains open and your TP/SL conditions are valid, the system will watch the market and attempt to close your trade when your thresholds are reached.

#### **Adjust or remove later**

You can always come back to the same TP/SL panel to move your take profit higher or lower, tighten or loosen your stop loss, or cancel the TP/SL off completely.

{% hint style="info" %}
Refer to the specific frontend application's documentation for any discrepancies.
{% endhint %}

Take profit and stop loss orders let you exit a position automatically when the market reaches a price you've decided on in advance — a profit target on one side, a maximum acceptable loss on the other. On Symmio, because every position is an on-chain agreement between you and a specific solver, TP/SL isn't a field you set inside the position itself. It's a small service that watches the market on your behalf and submits a close request the moment your trigger is hit.

This page explains how that service works, what you're authorizing when you set a TP or SL, and the couple of things to be aware of so your trigger behaves the way you expect.

### What You're Authorizing

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

**What the bot can do:**

* Submit `requestToClosePosition` on positions in your account, for as long as the delegation is active.

**What the bot cannot do:**

* 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 also has an **expiry timestamp** (typically a day or a session length, depending on the frontend) and can be **revoked** early if you want to take back permission before it expires. Revocation goes through a short two-step cooldown so that an in-flight trigger can't be mid-executed and suddenly invalidated.

### What Happens When Your Trigger Fires

When the bot's price feed crosses your TP or SL level, it submits a close request as a **market order** against your solver. That means:

* Your solver (PartyB) is responsible for filling the close, just like any other market close request.
* You'll 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; different frontends may charge different rates on the same symbol. See more information here.
* The price you get is the price your solver fills at, not the exact level that triggered the bot. In fast-moving markets, expect some slippage between the trigger price and the fill price — especially on a stop-loss during a sudden move against you.

Because the bot submits a **market** close, [force close](/trader-documentation/how-trading-works-in-symmio/force-closing-positions) escape hatch does *not* apply to TP/SL fills. Force close is only available for **LIMIT** close requests that have been sitting in `CLOSE_PENDING` past a cooldown. A market close request carries no specific price the solver has failed to honor, so there's nothing for force close to enforce.


# Force Closing Positions

Symmio is a peer-to-peer perpetuals protocol: every position you hold is against a specific solver (PartyB), and it's that solver's job to fill your close requests. Most of the time this works without issue —  you submit a close, the solver fills it, and the position settles. But solvers are off-chain actors, and sometimes one goes offline, falls behind, or simply ignores a close request that no longer suits them. **Force close will allow you to close your position.** 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 needing the solver's cooperation.

This page explains when force close is available, what price you'll get, what it costs, and the handful of things that can cause it to fail.

### When You Can Force Close

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

If any of those fail, the transaction reverts and the close stays pending as normal.

### The Price You Actually Get

Your force-close price is your requested close price, nudged slightly against you by a small gap. This gap exists so solvers aren't punished for a single wick that touches your limit and retraces.

The size of that gap is set per symbol by a parameter called `forceCloseGapRatio`. Your frontend should display the estimated force-close price before you submit so you know exactly what you're agreeing to.

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

### The Close Fee

Force close runs a full solvency check before it touches your position, and that check now includes a **close fee** in addition to any open-side trading fee already baked into the quote. The fee is computed as:

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

and subtracted from your available balance in the solvency math. The close fee rate itself 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 another subtraction that can push the solvency check negative. Ensure you have enough margin to force close a position.
{% 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 against your account.
2. **Settle (conditional).** If either 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 pulls that unrealized profits into available balance so the close can finalize. Most force closes don't need this stage; it only activates when funds are tied up elsewhere.
3. **Finalize.** Refreshes the price snapshot and actually closes the position, transferring the uPnL and emitting a `ForceClosePosition` event.

Frontends can call a single bundled function that does all three in one transaction ([`forceCloseAndSettlePositionsUnified`](/contract-documentation/symmio-perps-v0.8.5/diamond-core-facets/forceclosesteps)).

{% hint style="info" %}
A **solver cannot permanently block your force close.** If the solver's free balance is insufficient even after the settle stage, the contract has a fallback path that still closes your position at your force-close price and handles the solver's shortfall separately, off your critical path. You get filled; the protocol sorts out the solver on its own.
{% endhint %}


# Liquidations

Liquidation is what happens when an account no longer has enough collateral to safely support its positions.

To understand how it works in SYMMIO, it helps to first look at how your collateral is tracked. When you send a quote, the margin required for that position moves into a **pending** state. You can’t use that amount to open other trades, but it still counts as part of your allocated balance when the system checks if you’re solvent. Once a solver accepts your quote and the position opens, that margin becomes **locked**: it actively backs your open trade. Anything not pending or locked is your **free balance**.

All of your positions on SYMMIO are treated as **cross margin**. That means your entire allocated balance, across all positions and pending quotes, works together to support your risk. As your unrealized PnL (UPnL) changes, the system leans on these collateral sources in order: first your locked margin, then your free balance, and finally your pending locked amounts. If your total UPnL is still above the lowest allowed threshold, you’re safe. But if losses grow so large that even your pending locked funds are not enough, the account is considered unsafe and liquidation is triggered.

Because the system is cross-margin, **Party A is liquidated as a whole**. All of Party A’s open positions and outstanding quotes are moved into a liquidated state at once — in practical terms, everything is closed or cancelled together.&#x20;

When this 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 as a reward for keeping the system safe. Your CVA and margins are used according to the rules of the system.

### Example

To illustrate this process, let's use an example:

Imagine Alice (Party A) has $1,000 allocated for trading. She's got multiple positions open, all cross-margined together. Let's say she's got:

* $500 locked in active positions
* $200 in 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

Usually, when trading on-chain, every action needs a wallet signature and a network confirmation: to open a trade, close a trade, cancel etc. That’s secure, but it can feel slow and clunky, especially if you’re used to the speed of execution on centralized exchanges. Instant Trading on SYMMIO is a way to keep the same trustless backend while making the trading experience feel almost as fast as a CEX. Instead of signing every single 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 control.

In previous versions, instant trading relied on **delegated access** through the [MultiAccount ](/contract-documentation/symmio-perps-v0.8.5/multiaccount-legacy)contract. You would grant a solver permission to call specific functions (like "send quote" or "close position") on your behalf. The solver could then execute trades for you without needing your wallet signature each time.

This worked, but had a trust trade-off: once you granted a solver access, they technically *could* misuse it. In practice this wasn't a concern because solvers are trusted entities, but if a solver were ever compromised, your funds could be at risk.

The Instant Layer replaces broad delegation with a more secure approach. Instead of giving a solver open-ended permission to act on your behalf, you now have two options:

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

You sign a specific action with exact parameters "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 to the contract in a single transaction. The contract verifies your signature, confirms the parameters match exactly what you signed, and executes.

In this case, the solver can only do exactly what you signed. They can't change the price, the size, or anything else. And each signature can only be used 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 this session key permission to call specific functions on your behalf — i.e. "this key can send quotes and request closes for the next 24 hours." From that point on, all your trading actions use the session key.

This is similar to the old delegation, but with important improvements:

* **You don't need to trust the solver.** The session key is generated locally in your browser — the solver never has 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.** There's a cooldown-based revocation process so the solver gets fair notice.

#### How it feels in practice

From your perspective as a trader, 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.

When you first visit a SYMMIO frontend on v0.8.5, you'll activate "instant action mode" for your account. This is a one-time on-chain transaction. After that, the frontend builder should handle session keys automatically generating a new one each session and cleaning up old ones.

### Batched execution

One of the biggest speed improvements is that the solver can now bundle multiple actions into a single transaction. For example, when you click "Open," the solver can submit your `sendQuote`, their `lockQuote`, and their `openPosition` all at once. Previously doing these actions requirred three separate transactions. The contract processes them in order and automatically passes the quote ID from the first step to the second and third.

This means your position opens in one block instead of three, cutting latency dramatically.


# Withdrawals

When you’re trading on SYMMIO, your funds sit inside the protocol rather than directly in your wallet. You can deposit in instantly and trade as much as you like, the only time there’s a delay is when you want to withdraw back out to your wallet (or to 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. You can think of this like the withdrawal delay on an optimistic rollup.

The reason for this is safety is: If something goes wrong, the cooldown gives the protocol’s monitoring systems time to react. During that window, suspicious activity can be detected, users can be suspended and damage can be contained.&#x20;

For normal traders, this just means:

* You can **trade instantly**.
* You can **deposit instantly**.
* But when you withdraw, there is a **12-hour delay** before the funds land back in your wallet.

The old system had a single withdrawal path with a fixed 12-hour wait, plus a separate bridge mechanism for instant withdrawals. In the latest version, these are unified into a single flexible withdrawal system with multiple options:

### Withdrawal options in 0.8.5 (new)

#### Normal withdrawal

The simplest option. You initiate a withdrawal, wait for the cooldown to pass, and finalize it. Your funds are transferred directly to your wallet on the same chain. No provider involvement, no fees beyond gas.

#### Express withdrawal

If you need your funds now and don't want to wait, an **express provider** can front the money to you immediately. The provider pays you right away, then waits out the cooldown and gets reimbursed by the protocol afterward. From your perspective, the withdrawal is near-instant. The provider typically charges a small fee for this service.

#### Cross-chain withdrawal

SYMMIO operates on multiple chains, and you might want to receive your funds on a different chain than where you're trading. A **virtual provider** handles this: they accept the withdrawal on the source chain and deliver your funds on the destination chain. The actual collateral reconciliation happens behind the scenes.

#### Express cross-chain withdrawal

In this scenario, express provider fronts your funds immediately on the destination chain. This combines 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 possible, the frontend builder should offer it. If not, it should show the estimated wait time. 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 a single withdrawal request.

### Cancelling a withdrawal

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

For a standard withdrawal, cancellation doesn't require an 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 independently. Outside that window you can still cancel immediately, but inside it the cancellation moves to a pending state and requires the provider's approval before it goes through.

For express withdrawals, any cancellation after the provider has accepted requires their approval — there's no window where you can cancel freely, since the provider has already fronted your funds.

{% hint style="info" %}
The legacy bridge withdrawal mechanism from v0.8.4 still works but is being phased out. The new withdrawal 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 earn or lose money over time. That profit or loss is called unrealized PnL. It exists on paper, but it isn’t yet locked in as realized PnL. Separately, you also have an allocated balance: real collateral that can be used to pay for losses, open new positions, or be withdrawn. The protocol requires this allocated balance to remain sufficient at all times, which means you can’t close a losing trade unless you have enough realized collateral to cover the loss. Sometimes a trader is profitable overall but still gets blocked from closing a losing position, because all of those profits are still unrealized inside open positions. Settlement is the mechanism that converts some of those unrealized gains into usable balance.

Let's illustrate settling unrealized profit works with an example:

### How it works

Let's 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 profits/losses across Bob's positions. The contract verifies the signature, checks that everyone stays solvent, and settles. After settlement, $100 from Position 2 and $150 from Position 1 are realized into Bob's allocated balance. Bob now has $250 of spendable collateral, and PerpsHub can close the losing position. Nobody takes on uncollateralized risk, and Bob isn't stuck waiting for the trade to recover.\
\
Position 3 remains unchanged, and PerpsHub can now successfully fill the close request for Position 3, as Bob has sufficient allocated balance ($250) to cover the loss ($250).

Now Bob has $250 of spendable collateral, and PerpsHub can safely close the losing position. Nothing in the system breaks, no one takes on uncollateralized risk, and Bob isn’t trapped waiting for his losing trade to recover.

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

This settlement process has several important protections built in. The updated prices must always lie between the original entry price and the signed current price, neither side of the trade is allowed to end up insolvent as a result of settlement and multiple positions can be settled at once, allowing a solver to quickly free up collateral across many trades if needed. More details on settlement can be found [here](/contract-documentation/symmio-perps-v0.8.4/facets/settlement-facet-0.8.4).


# Funding Rates&#x20;

### Funding Rates in SYMMIO&#x20;

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

If longs are dominating and the perp price is above spot → **longs pay shorts**.\
If shorts are dominating and the perp price is below spot → **shorts pay longs**.

This makes holding the more “advantaged” side a little more expensive, nudging prices back toward the real market value.

#### How it works on SYMMIO

Every symbol on SYMMIO has it's own funding rate, which is applied at the end of periods, called **epochs**. When an epoch ends, positions pay (or receive) funding. On the contracts, this cycle's length is defined as the `fundingRateEpochDuration`. Solvers are responsible for charging funding during the symbol's `fundingRateWindowTime`, which is a short window either side of the epoch 'flip'.

Example Symbol:

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

The older method adjusted the `openedPrice` of your position to simulate the funding charge: if you're long and paying funding, your open price nudges up slightly, giving you a worse entry. No tokens were transferred. This process 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 rather than processing each position individually. When a solver sets a rate, it blends into a running average. When funding is eventually charged, it's deducted directly from your allocated balance rather than adjusting your open price — making it a visible line item rather than a subtle price shift. This approach is significantly more efficient, since the solver no longer needs to touch every open position at each epoch.

{% hint style="info" %}
Solvers are responsible for streaming the rates which they are applying in the next epoch, but they don't necessarily stream the epoch durations. Ultimately they can only charge during the window defined by the smart contracts, so always refer to the smart contracts to clarify epoch durations.
{% endhint %}


# Trading Fees

Refer to the following pages for more information on Trading Fees within SYMMIO:

{% content-ref url="/pages/uP3DUXV6Kye14LQ0mAL2" %}
[Perps - Settlement Costs](/liquidity-provider-documentation/solver-trading-fees/perps-settlement-costs)
{% endcontent-ref %}

{% content-ref url="/pages/T8drk2MmmWuchawE23Bd" %}
[Pair Trading - Settlement costs](/liquidity-provider-documentation/solver-trading-fees/pair-trading-settlement-costs)
{% endcontent-ref %}


# 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>Decentralized Perpetual Trading Platform</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 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/multiaccount) 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()
```


# Interacting with Contracts


# Querying Info from our Contracts

## Querying Info from the SYMMIO Diamond

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

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

This page walks through the most common things a frontend developer reaches for the Inspector to do.

{% 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 also human-readable:

```
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 you've loaded a diamond, the Inspector shows every **facet** registered on it, along with every **function selector** that facet implements. This is the single source of truth for "is function X live on chain Y" — if a selector is missing from this 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` / `pure`) and the Inspector generates 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've got a raw `0x…` blob — from a failed transaction, a log, 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 debug an Instant Layer `SignedOperation` — sign one, paste the `callData` into the decoder, and verify it's calling what you think it's calling before posting to the solver.

### Reading bind state for an account

When wiring up the Instant Layer flow, the most common "why is `sendQuote` reverting" answer 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` returned by 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` / etc.), the bound PartyB, and the affiliate.

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


# Closing a Position On-Chain

## How to Request to Close a Position on SYMM if the Frontend Application is Unavailable

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 using on-chain methods.

### Solution 1: Use the IPFS FE

Use the IPFS frontend to close the position (Website TBA)

### 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 %}


# How to Query Contracts

## The SYMMIO Diamond

SYMMIO's core contracts are implemented with **EIP-2535**, a solution for building modular and upgradeable contracts.

#### **EIP-2535: Diamond Standard Overview** <a href="#eip-2535-diamond-standard-overview" id="eip-2535-diamond-standard-overview"></a>

EIP-2535 introduces the Diamond Standard in smart contract development. This standard centralizes around the concept of a "Diamond" contract, which is constructed from multiple "facets," each responsible for different functionalities. This modular design enhances maintainability by allowing facets to be upgraded or modified independently without affecting the entire contract. Key to this architecture are function selectors, which serve as unique identifiers for each function within the facets. These selectors efficiently direct function calls to the correct facet, facilitating organized and conflict-free code execution. For further reading and technical details, refer to the [official EIP-2535 documentation](https://eips.ethereum.org/EIPS/eip-2535).

When a facet, is invoked via a `delegatecall` from the Diamond, it operates directly on the Diamond’s storage. The facet uses the slot address computed by the library to access or modify the data in the specific storage slot. This operation is transparent to the Diamond itself.

### Finding Facet Contracts

In the SYMM Diamond, the `facets()` function of the `DiamondLoupeFacet` plays a crucial role in inspecting the contract's structure and behaviour. When this function is called on the Diamond, it returns an array of `Facet` structs, each containing the address of a facet along with the specific function selectors associated with that facet.&#x20;

To examine the facets and call functions on them, we'll use a tool called [Louper](https://louper.dev). This platform displays all facets and functions related to a Diamond contract and allows us to query data directly from them. Since functions cannot be called on through the Diamond contract directly through a blockchain explorer, Louper provides an efficient and user-friendly way to interact with the Diamond.

### Using Louper to Read Data

In this example, we'll use Louper to find all the contract symbols available to trade on Thena.

First, paste the address into the tool and select the appropriate chain (BNB):

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

Then, click the **Read** tab, and select the View Facet. This shows all the view functions available to query on the Diamond.

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

Now you can query the symbols available to trade on the Diamond using `getSymbols()` with `start` and `size` as parameters:

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

For more information on the View Facet's functions, please refer to the [ViewFacet ](https://app.gitbook.com/o/-McFr-NnfqafWnoj_Y5M/s/YQhBmTCs9MwhhuPQrV5v/~/changes/474/building-on-symm-io/technical-documentation/contracts-documentation-0.8.2/facets/view-facet)documentation (0.8.2).&#x20;

## Using Subgraphs to Query Data

The Graph is a decentralized protocol for indexing and querying blockchain data. You can also query data on the diamond using a subgraph URL provided by SYMM.  In this example, we'll query the symbol data using the Apollo Client.

**Prerequisites**

* Basic understanding of GraphQL
* A tool for making HTTP requests, such as Postman, cURL, or a GraphQL client like Apollo Client

**Endpoint URL**

First identify a suitable endpoint:

```
https://api.studio.thegraph.com/query/62454/analytics_bnb_8_2/version/latest
```

This endpoint is the analytics endpoint for the SYMMIO Diamond on BNB.

**Example Query**

To fetch data, send a POST request to the endpoint with the GraphQL query:

```graphql
query MyQuery {
  symbols(first: 3) {
    blockNumber
    id
    name
    timestamp
    tradingFee
    updateTimestamp
  }
}
```

**Using cURL**

```sh
shCopy codecurl -X POST \
  -H "Content-Type: application/json" \
  -d '{"query":"query MyQuery { symbols(first: 3) { blockNumber id name timestamp tradingFee updateTimestamp } }"}' \
  https://api.studio.thegraph.com/query/62454/analytics_bnb_8_2/version/latest
```

### **Using Apollo Client in React**

#### Install Apollo Client:

```bash
npm install @apollo/client graphql
```

#### Set up Apollo Client and make the query:

```javascript
import React from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, gql } from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://api.studio.thegraph.com/query/62454/analytics_bnb_8_2/version/latest',
  cache: new InMemoryCache()
});

const GET_SYMBOLS = gql`
  query MyQuery {
    symbols(first: 3) {
      blockNumber
      id
      name
      timestamp
      tradingFee
      updateTimestamp
    }
  }
`;

const DataComponent = () => {
  const { loading, error, data } = useQuery(GET_SYMBOLS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return (
    <div>
      <h2>Symbols</h2>
      {data.symbols.map(symbol => (
        <div key={symbol.id}>
          <p>{symbol.name}</p>
          <p>Block Number: {symbol.blockNumber}</p>
          <p>Timestamp: {new Date(symbol.timestamp * 1000).toLocaleString()}</p>
          <p>Trading Fee: {symbol.tradingFee}</p>
          <p>Update Timestamp: {new Date(symbol.updateTimestamp * 1000).toLocaleString()}</p>
        </div>
      ))}
    </div>
  );
};

const App = () => (
  <ApolloProvider client={client}>
    <DataComponent />
  </ApolloProvider>
);

export default App;
```

## Subgraph URLs by Chain

### Arbitrum

#### [Analytics Subgraph](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/arbitrum_analytics/latest/gn)

[**Events Subgraph**](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/arbitrum_events/latest/gn)

### Mantle

#### [Analytics Subgraph](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/mantle_analytics/latest/gn)

[**Events Subgraph**](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/mantle_events/latest/gn)

### BNB

#### [Analytics Subgraph](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/bnb_analytics/latest/gn)

[**Events Subgraph**](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/bnb_events/latest/gn)

### Base

#### [Analytics Subgraph](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/base_analytics/latest/gn)

[**Events Subgraph**](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/base_events/latest/gn)

### Berachain

#### [Analytics Subgraph](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/bera_analytics/latest/gn)

[**Events Subgraph**](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/bera_events/latest/gn)

### Mode

#### [Analytics Subgraph](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/mode_analytics/latest/gn)

[**Events Subgraph**](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/mode_events/latest/gn)

### Iota

#### [Analytics Subgraph](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/iota_analytics/latest/gn)

[**Events Subgraph**](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/iota_events/latest/gn)

### Polygon

#### [Analytics Subgraph](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/polygon_analytics/latest/gn)

[**Events Subgraph**](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/polygon_events/latest/gn)

### Blast

#### [Analytics Subgraph](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/blast_analytics/latest/gn)

[**Events Subgraph**](https://api.goldsky.com/api/public/project_cm1hfr4527p0f01u85mz499u8/subgraphs/blast_events/latest/gn)


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

In the protocol is no longer a single diamond. It is a small network of diamonds with clearly separated responsibilities, plus an externalized signature verifier and a privileged operator (the ClearingHouse) for the cases the decentralized flow can't handle. This page is a tour of what now 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).** This is the main diamond for facilitrating trades between the trader (partyA) anb the solver (partyB). It's responsible for sending quotes, locking, opening, closing, settling, and liquidating. &#x20;
* **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 collateralslashable, with a two-step request-approval withdrawal flow.
* **Liquidation Insurance Vault.** Receives the excess of liquidator profit above `maxLiquidationProfitPerPosition`.

### Storages

* `DiamondStorage` — contract ownership.
* `GlobalAppStorage` — collateral, fee collector, role mappings, pause states, emergency mode, balance limits etc.
* `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, but 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 have 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 cannot touch another. When a VA's last position closes, its funds are automatically swept back to the parent SubAccount and the address is recycled.&#x20;

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 VA.
* `CUSTOM` — caller-supplied VA addresses; useful for advanced strategies.

### Roles and admin model

The 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/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 three-tier hierarchy: **Owner → Default Admin → Role Admin → Role Holder**.

The active role set in :

| 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 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 — cross-mode PartyB insolvency, 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 tx lands.

### Quote lifecycle

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

PartyA initiates via either of:

* `sendQuote(partyBsWhiteList, symbolId, positionType, orderType, price, quantity, cva, lf, partyAmm, partyBmm, maxFundingRate, deadline, affiliate, upnlAndPriceSig)` — original signature.
* `sendQuoteWithAffiliate(...)`: kept for compatibility.
* `sendQuoteWithAffiliateAndData(..., bytes data)` : Same as `sendQuoteWIthAffiliate()` but with attached arbitrary `bytes` on the quote.&#x20;

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]`. Here, the `SingleUpnlAndPriceSig` Muon is required (unless the 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. Same idea for `fillCloseRequests`. This was added to keep solver response time low under heavy load.

#### Stage 4:  Position closing

PartyA requests closure via `requestToClosePosition(quoteId, closePrice, quantityToClose, orderType, deadline)`, transitioning 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 (with `PARTY_B_MANAGER_ROLE` enabling them) can unilaterally close to reduce risk exposure with no solvency check on themselves. Backed by **pledge collateral** that can be slashed if the 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.

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

In `LATE` / `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 does not. 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 only, the protocol now supports a tiered warning system before hard liquidation. As balance approaches the danger zone, the ClearingHouse 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 — 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 is performed; isolated PartyBs retain the prior capped-payment behavior.

### Funding rate mechanics

In the previous version, funding was charged per quote. PartyB had to process every individual position at each epoch  `O(quotes)`. The current version works by using 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 require 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*, not quotes. For a solver with 5,000 positions across 50 symbols, solvency goes from `O(5000)` to `O(50)`. Aggregate funding debt is tracked with the same shape.

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

### Withdraw System

The Bridge facet's role has been folded into a unified Withdraw System. A withdrawal request is composed of one or more **receiver parts**, each routed through one of three withdrawal patkiohs:

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

Subsequent deallocations don't reset cooldowns of already-initiated withdrawals. `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 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.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)

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

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

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

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

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

Audit: <https://audits.sherlock.xyz/contests/144>


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


# Frontend Builder Introduction

## Introduction

At its core, Symmio is designed with a critical perspective on regulatory restrictions and a thorough commitment to censorship resistance of our core aMFQ technology. Consequently, we do not host any front end. Instead, we focus on providing the crucial technical infrastructure required for executing aMFQ peer-to-peer (p2p) symmetrical trades. Notably, while we provide the technology necessary for these transactions, we do not employ it to establish markets or extend market offerings to users.&#x20;

Traders should use one of our [3rd party frontend applications to trade SYMMIO markets.](https://symm.io/frontends)

### Business-to-Business (B2B)

Symmio adopted a strict business-to-business (B2B) approach in our operations for various reasons. Our infrastructure is exclusively offered to developers and exchanges, enabling them to tap into the potential of decentralized finance without dealing with the complexities of backend development.

We recommend exploring our [Profit Sharing Model](/liquidity-provider-documentation/solver-trading-fees/perps-settlement-costs) for parties interested in launching a Symmio front end and creating their infinite array of markets using the Symmio Tech Stack.&#x20;

### Simply integrate with us

Symmio offers a comprehensive perpetual exchange infrastructure that can be effortlessly integrated into your existing or newly built exchange.&#x20;

The complete functionality found on <https://cloverfield.exchange/trade/BTCUSDT> (Symmio demo frontend) can be easily incorporated into your platform through forking our complete code base by getting a [Frontend Use Grants](/legal-and-brand/terms-of-service-and-licensing/frontend-license/frontend-use-grants) or using our [Broken mention](broken://pages/AEl4ZlpsCsa435YklPXa), streamlining the process for seamless integration.&#x20;

For further information on deploying a frontend application, refer to the [Documentation for Frontend Builders](broken://pages/DfSdaKQFksdhaB9oEeMB)

Symmio effectively circumvents liquidity fragmentation often associated with forking projects and concepts, such as vAMM-based perps (e.g., GMX, Gains) or OrderBook-based (e.g., DyDx). By providing access to an extensive pool of institutionally-backed liquidity providers, known as "Hedgers" in Symmio, the user experience is significantly enhanced through Liquidity aggregation instead of fragmentation.

Please do not hesitate to contact us for further 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 serves as a general outline assuming familiarity with the SYMMIO ecosystem, smart contract interaction, and frontend development.

## **1. Clone or Build the Frontend Application**

The first step is to either clone the official SYMMIO Frontend SDK repository or create your own custom frontend application using the provided SDK and documentation. The SDK simplifies the process of interacting with SYMMIO's APIs and smart contracts. Refer to the [Frontend SDK Setup Guide](/exchange-builder-documentation/frontend-builder-sdk) for detailed instructions on cloning and configuring the SDK or creating your own implementation.

## **2. Deploy a MultiAccount Contract**

To support useful user functionality and track fees, you’ll need to deploy a MultiAccount contract following the standards set by SYMMIO. The MultiAccount contract plays a vital role in managing user interactions with SYMMIO’s smart contracts:

* **User Volume Tracking**: MultiAccount helps track trading volume at a user level, providing insights into user activity and engagement.
* **Sub-Accounts for Trading**: Users can create sub-accounts under their primary account for better organization and management of their trades.
* **Automated Fee Claiming**: Users interact with the `sendQuoteWithAffiliate()` function by passing the MultiAccount address of the frontend. This enables frontend builders to automatically claim fees for trades routed through them, streamlining the process for both users and the platform.

Refer to the [MultiAccount Deployment Guide](/exchange-builder-documentation/frontend-builder-technical-guidance/multiaccount-deployment-guide) for detailed instructions on deploying and configuring the contract.

## **3. Provide Frontend Application URLs for Whitelisting**

Once your frontend application is ready for deployment, you’ll need to share the URL(s) with the team for whitelisting. Provide the production frontend address to ensure it is added to the CORS whitelist. If you have test or staging environments, you can provide their URLs as well, and the team will include them in the whitelist for development and testing.

Whitelisting your frontend is a critical step for enabling features like Instant Execution, where user requests are processed rapidly via delegated access.&#x20;


# Frontend Builder SDK

## SDK - SYMM client

### [An SDK to interact with SYMMIO contracts, hedgers and peripherals.](https://github.com/SYMM-IO/frontend-sdk#an-sdk-to-interact-with-symmio-contracts-hedgers-and-peripherals) <a href="#user-content-an-sdk-to-interact-with-symmio-contracts-hedgers-and-peripherals" id="user-content-an-sdk-to-interact-with-symmio-contracts-hedgers-and-peripherals"></a>

* To enable path completion suggestions in VSCode, first build the project by running yarn build. Then, in the VSCode settings, ensure that "TypeScript > Tsc: Auto Detect" is set to 'on'.
* Be sure not to use the publicProvider from Wagmi, as it causes some errors in the SDK.&#x20;

[Wagmi Website](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. Execute the following steps with Yarn classic (v1).&#x20;
{% endhint %}

{% hint style="info" %}
The **Polygon** network is used internally and may experience intermittent downtime as updates and new deployments occur. For this reason, it’s not recommended for external partner testing. A test environment with a stable test hedger may be made available in the future.
{% endhint %}

**First Execute the following command:**

```
yarn install
```

**Navigate to one of our apps folders. For example the alpha app or the nextjs app:**

```
cd apps/alpha/
```

**To set up your environment:**

1. Locate the .env.example file within the apps/alpha directory.
2. Rename .env.example to .env.
3. Update the fields in the .env file as required. Note that each chain requires a unique

   `HEDGER_URL` and public RPC.

For a detailed list of all deployed contracts and their corresponding Hedger URLs and deployed addresses for testing, please refer to this table:

<table data-full-width="true"><thead><tr><th>Project Name</th><th>Version</th><th>Hedger State</th><th>Chain</th><th>SYMMIO Address</th><th>Collateral Address</th><th>MultiAccount Address</th><th>PartyBAddress</th><th>PartyB API Address</th><th width="137">Main Subgraph</th><th>Analytics Subgraph</th><th>Parties Subgraph</th><th>Funding Rate Subgraph</th><th width="144">Events Subgraph</th><th>Muon</th></tr></thead><tbody><tr><td>Core</td><td>0.8.2</td><td>Active</td><td>Blast</td><td>0x3d17f073cCb9c3764F105550B0BCF9550477D266</td><td>0x4300000000000000000000000000000000000003</td><td>0xd6ee1fd75d11989e57B57AA6Fd75f558fBf02a5e</td><td>0xECbd0788bB5a72f9dFDAc1FFeAAF9B7c2B26E456</td><td>blast-hedger.rasa.capital</td><td><a href="https://api.studio.thegraph.com/query/62454/main_blast_8_2/version/latest">https://api.studio.thegraph.com/query/62454/main_blast_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/analytics_blast_8_2/version/latest">https://api.studio.thegraph.com/query/62454/analytics_blast_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/parties_blast_8_2/version/latest">https://api.studio.thegraph.com/query/62454/parties_blast_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/fundingrate_blast_8_2/version/latest">https://api.studio.thegraph.com/query/62454/fundingrate_blast_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/events_blast_8_2/version/latest">https://api.studio.thegraph.com/query/62454/events_blast_8_2/version/latest</a></td><td><a href="https://muon-oracle3.rasa.capital/v1/">https://muon-oracle3.rasa.capital/v1/</a><a href="https://muon-oracle4.rasa.capital/v1/">https://muon-oracle4.rasa.capital/v1/</a></td></tr><tr><td>IntentX (Blast)</td><td>0.8.2</td><td>Active</td><td>Blast</td><td>0x3d17f073cCb9c3764F105550B0BCF9550477D266</td><td>0x4300000000000000000000000000000000000003</td><td>0x083267D20Dbe6C2b0A83Bd0E601dC2299eD99015</td><td>0xECbd0788bB5a72f9dFDAc1FFeAAF9B7c2B26E456</td><td>blast-hedger.rasa.capital</td><td><a href="https://api.studio.thegraph.com/query/62454/main_blast_8_2/version/latest">https://api.studio.thegraph.com/query/62454/main_blast_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/analytics_blast_8_2/version/latest">https://api.studio.thegraph.com/query/62454/analytics_blast_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/parties_blast_8_2/version/latest">https://api.studio.thegraph.com/query/62454/parties_blast_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/fundingrate_blast_8_2/version/latest">https://api.studio.thegraph.com/query/62454/fundingrate_blast_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/events_blast_8_2/version/latest">https://api.studio.thegraph.com/query/62454/events_blast_8_2/version/latest</a></td><td><a href="https://muon-oracle3.rasa.capital/v1/">https://muon-oracle3.rasa.capital/v1/</a><a href="https://muon-oracle4.rasa.capital/v1/">https://muon-oracle4.rasa.capital/v1/</a></td></tr><tr><td>Thena V3</td><td>0.8.2</td><td>Active</td><td>BNB</td><td>0x9A9F48888600FC9c05f11E03Eab575EBB2Fc2c8f</td><td>0x55d398326f99059ff775485246999027b3197955</td><td>0x650a2D6C263A93cFF5EdD41f836ce832F05A1cF3</td><td>0x9fa01a45E245015fA685F21763e60C60832Ed2D6</td><td></td><td><a href="https://api.studio.thegraph.com/query/62454/main_bnb_8_2/version/latest">https://api.studio.thegraph.com/query/62454/main_bnb_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/main_bnb_8_2/version/latest">https://api.studio.thegraph.com/query/62454/main_bnb_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/parties_bnb_8_2/version/latest">https://api.studio.thegraph.com/query/62454/parties_bnb_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/fundingrate_bnb_8_2/version/latest">https://api.studio.thegraph.com/query/62454/fundingrate_bnb_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/events_bnb_8_2/version/latest">https://api.studio.thegraph.com/query/62454/events_bnb_8_2/version/latest</a></td><td><a href="https://muon-oracle3.rasa.capital/v1/">https://muon-oracle3.rasa.capital/v1/</a><a href="https://muon-oracle4.rasa.capital/v1/">https://muon-oracle4.rasa.capital/v1/</a></td></tr><tr><td>IntentX</td><td>0.8.2</td><td>Active</td><td>Base</td><td>0x91Cf2D8Ed503EC52768999aA6D8DBeA6e52dbe43</td><td>0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913</td><td>0x8Ab178C07184ffD44F0ADfF4eA2ce6cFc33F3b86</td><td>0x9206D9d8F7F1B212A4183827D20De32AF3A23c59</td><td></td><td><a href="https://api.studio.thegraph.com/query/62454/main_base_8_2/version/latest">https://api.studio.thegraph.com/query/62454/main_base_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/main_base_8_2/version/latest">https://api.studio.thegraph.com/query/62454/main_base_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/parties_base_8_2/version/latest">https://api.studio.thegraph.com/query/62454/parties_base_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/fundingrate_base_8_2/version/latest">https://api.studio.thegraph.com/query/62454/fundingrate_base_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/events_base_8_2/version/latest">https://api.studio.thegraph.com/query/62454/events_base_8_2/version/latest</a></td><td><a href="https://muon-oracle2.rasa.capital/v1/">https://muon-oracle2.rasa.capital/v1/</a></td></tr><tr><td>Based</td><td>0.8.2</td><td>Active</td><td>Base</td><td>0x91Cf2D8Ed503EC52768999aA6D8DBeA6e52dbe43</td><td>0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913</td><td>0x1c03B6480a4efC2d4123ba90d7857f0e1878B780</td><td>0x9206D9d8F7F1B212A4183827D20De32AF3A23c59</td><td>base-hedger82.rasa.capital</td><td><a href="https://api.studio.thegraph.com/query/62454/main_base_8_2/version/latest">https://api.studio.thegraph.com/query/62454/main_base_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/main_base_8_2/version/latest">https://api.studio.thegraph.com/query/62454/main_base_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/parties_base_8_2/version/latest">https://api.studio.thegraph.com/query/62454/parties_base_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/fundingrate_base_8_2/version/latest">https://api.studio.thegraph.com/query/62454/fundingrate_base_8_2/version/latest</a></td><td><a href="https://api.studio.thegraph.com/query/62454/events_base_8_2/version/latest">https://api.studio.thegraph.com/query/62454/events_base_8_2/version/latest</a></td><td><a href="https://muon-oracle2.rasa.capital/v1/">https://muon-oracle2.rasa.capital/v1/</a></td></tr><tr><td>CloverField (Polygon)</td><td>0.8.3</td><td>Pending</td><td>Polygon</td><td>0x976c87Cd3eB2DE462Db249cCA711E4C89154537b</td><td>0x50E88C692B137B8a51b6017026Ef414651e0d5ba</td><td>0xffE2C25404525D2D4351D75177B92F18D9DaF4Af</td><td>0x5044238ea045585C704dC2C6387D66d29eD56648</td><td>polygon-hedger-test.rasa.capital</td><td><a href="https://api.thegraph.com/subgraphs/name/symmiograph/symmiomain_polygon_8_2">https://api.thegraph.com/subgraphs/name/symmiograph/symmiomain_polygon_8_2</a></td><td><a href="https://api.thegraph.com/subgraphs/name/symmiograph/symmioanalytics_polygon_8_2">https://api.thegraph.com/subgraphs/name/symmiograph/symmioanalytics_polygon_8_2</a></td><td><a href="https://api.thegraph.com/subgraphs/name/symmiograph/symmioparties_polygon_8_2">https://api.thegraph.com/subgraphs/name/symmiograph/symmioparties_polygon_8_2</a></td><td></td><td></td><td><a href="https://muon-oracle2.rasa.capital/v1/">https://muon-oracle2.rasa.capital/v1/</a><a href="https://muon-oracle3.rasa.capital/v1/">https://muon-oracle3.rasa.capital/v1/</a><a href="https://muon-oracle4.rasa.capital/v1/">https://muon-oracle4.rasa.capital/v1/</a></td></tr></tbody></table>

**Finally, execute the following command:**

```
yarn dev
```

The development server will start at `http://localhost:3000`

**NOTE:** The Alpha and the Next.js application 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, you should select one or multiple chains from the supported chain IDs and add it to the ClientChain variable in the `nextjs/constants/chains.ts` file.

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

**Configuring the Hedger API**

Navigate to the `app/constants/chains/hedgers.ts` and add the chain to the `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` won't change regardless of the chain because frontend applications listen directly to the Binance API for fetching price data.

**Configuring Addresses**

To ensure the frontend interacts properly with the deployed contracts, you'll need to update the `constants/chains/addresses.ts` file with the appropriate contract addresses for each chain. Additionally, make sure to add any necessary contract ABIs to the `/abi` folder.

```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 the balance history or order history of a user we need to determine the appropriate GraphQL subgraph URI based on the network chain ID. Then use the Apollo Client to execute a query to the subgraph. To define these you’ll have to navigate to `packages/core/src/apollo/client` and modify the `balanceHistory.ts` and `orderHistory.ts` files to reflect the new chain. For example:

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

Then add it as a case for the `getBalanceHistoryApolloClient` function and the `getOrderHistoryApolloClient` functions respectively:

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

**Configuring Muon**

Muon is used in our system for verifying the price of an asset and the uPnL of a user. Muon signatures are required by the smart contracts to execute functions related to user positions.

In most scenarios, you will not need to alter the Muon base URLs, however this can be updated from `/constants/chains/misc.ts`.

## State-Component Relationships

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

## MultiAccount

Frontend solutions support creating and switching between sub-accounts via the `MultiAccount` contract.

When a user interacts with the SYMM contracts through a sub-account to send or modify quotes, they'll use the `_call` function on the `MultiAccount` contract with the `account` (the address of the sub-account) and `_callData`, a bytes array. This call is forwarded to the sub-account contract through an `innerCall`, which then calls the function on the diamond.

Users can select the account they desire from a drop-down menu in the header, allowing users to isolate positions across accounts, since all positions on a single sub-account are in **CROSS**. Users are required to provide a name as an input parameter when they create a sub-account. The hook `useActiveAccountAddress` returns the subaccount address, and the `useActiveWagmi` returns the wallet address.

## Querying the Hedger

To optimize user experience, facilitate experimentation, and aid users in identifying the most suitable hedger for their needs, Symmio provides a suite of off-chain information. This information is primarily classified into two categories: APIs and Web-Sockets.

It's important to emphasize that these APIs aim to reduce user request failures. They are often subject to the hedger's policies. For example, if a hedger doesn't mandate a whitelist, then the related APIs become superfluous.

Many of the API endpoints require a `MultiAccount` address as a parameter. This address refers to the address which users will interact with on the frontend for deploying sub-accounts and calling functions related to the opening and closing of positions on the Symmio Diamond Contract.

## Testing Guidelines

### **Testing Environments**

**Preferred Networks for Testing**:

* **Base and Arbitrum (Mainnets)**: These networks are recommended for testing interactions, especially for opening and closing positions, as they provide more stability compared to test networks. Testing here requires a small amount of USDC to verify functionality.
* **Setting Up Environment Variables in `.env`**:

  Add the following environment variables to your `.env` file 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**:

* Currently, for Base and Arbitrum, testing with real capital (small amounts of USDC) is necessary. Polygon supports test tokens but the test hedger isn’t stable enough for reliable frontend testing.

### **2. Setting Up MultiAccount Contracts**

* Frontend builders can use an existing MultiAccount setup (such as **IntentX**) for initial testing of functions like `sendQuote`. This allows for a complete testing cycle without needing a dedicated contract deployment.
* When moving to production, deploy a custom [MultiAccount contract](/exchange-builder-documentation/frontend-builder-technical-guidance/multiaccount-deployment-guide) for proper integration with solvers.

## Hedger API

The Hedger API provides a suite of functionalities for users to help users monitor their positions. Querying the API offers valuable insights into the status of positions, total open interest, notional caps, and the capital requirements needed to initiate positions.

Some useful API endpoints are listed below:

### \[GET] contract-symbols

This endpoint gets the total number of symbols the hedger is offering, and returns important data pertaining to each.

**Returns:**

* **`count`**: The total number of trading symbols available through the hedger.
* **`symbols`**: An array of objects, each representing a specific trading symbol or contract offered by the hedger. The array includes detailed information about each symbol, as outlined in the fields within each object.
  * **`name`**: The name or description of the trading pair (e.g. BTCUSDT).
  * **`symbol`**: The ticker that identifies the former part of the trading pair (e.g. BTC).
  * **`asset`**: The ticker that identifies the latter part of the trading pair (e.g. USDT).
  * **`symbol_id`**: A unique integer identifier for the symbol within the hedger's system.
  * **`price_precision`**: Refers to the number of decimal places to which the price of an asset can be specified.
  * **`quantity_precision`**: Refers to the number of decimal places to which the quantity of the asset being traded can be specified.
  * **`is_valid`**: A boolean indicating whether the symbol is currently valid for trading.
  * **`min_acceptable_quote_value`**: The minimum quote value (of collateral tokens) accepted for a trade with this symbol.
  * **`min_acceptable_portion_lf`**: The minimum portion that is awarded to the liquidator on liquidation.
  * **`trading_fee`**: The fee rate charged for trading this symbol.
  * **`max_leverage`**: The maximum leverage available for trading.
  * **`max_notional_value`**: The maximum notional value of a position.
  * **`rfq_allowed`**: A boolean indicating 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

This endpoint shows the cumulative OI available to hedgers, encompassing all trading symbols. It also provides information on the used capacity.

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

**Returns:**

* **`total_cap`**: The total open interest cap.
* **`used`**: How much is currently in use.

**Output Schema:**

<pre class="language-json"><code class="lang-json"><strong>{ 
</strong><strong>total_cap: float, 
</strong><strong>used: float 
</strong><strong>}
</strong></code></pre>

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

### \[GET] notional\_cap

This endpoint shows the notional cap of a symbol provided by the hedger which represents the value of the outstanding positions that haven't yet been settled for that symbol.

<pre><code><strong>api/notional_cap/${symbolId}/
</strong></code></pre>

**Parameters:**

* **`symbolId`:** The symbol ID of the pair (eg. for `BTCUSDT`, the `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

This endpoint provides the maximum and minimum prices at which `partyA` can place a limit order for a specific trading 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

To help understand the meaning of the error codes returned from querying a position state, this API fetches a list of potential error labels and their associated error codes that may arise while interacting with the hedger system. Leaving the `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

These parameters are important because they are used when opening the quote. This method returns important values related to collateral requirements for a specified pair based on the provided parameters.

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

**Parameters:**

* **`symbol`**: The trading symbol (e.g. BTCUSDT)
* **`leverage`**: The leverage applied to the position. This will affect the calculation of collateral requirements.

**Returns:**

* **`cva`**: Credit Valuation Adjustment. Either `partyA` (the user) or `partyB` (the hedger) can get liquidated and `cva` is the penalty that the liquidated side should pay to the other.
* **`partyAmm`**: Maintenance Margin for `partyA`. The amount that is actually behind the position and is considered in liquidation status.
* **`lf`**: Liquidation Fee. This will be 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**

This endpoint retrieves market information for various whitelisted trading pairs, it requires the deployed `MultiAccount` contract 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 for 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`. It's used mostly for displaying data on the [Market Information](https://cloverfield.exchange/markets) page.

### **\[GET] partyA\_upnl**

This endpoint returns the unrealized profits and losses 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

This API method retrieves balance information for a specified address and multi-account address. It returns detailed financial data for both parties involved, which is crucial for managing and analyzing account balances and associated 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

This API method retrieves funding information for various trading pairs. It returns details about the next funding time and the funding rates for both short and long positions for each specified trading pair.

`api/get_funding_info/`

**Parameters**

This endpoint does not require any parameters to be passed in the URL.

**Returns**

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

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

This endpoint returns information regarding the state of a position, indicating if there has been any internal issue with the quote processing by returning an `action_status`. It is useful for discerning any errors associated with a specific trading position.

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

**Parameters:**

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

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

**Returns:**

* **`create_time`**: Unix timestamp indicating when the position was created.
* **`modify_time`**: Unix timestamp indicating the last modification time of the position.
* **`counterparty_address`**: The blockchain address of the counterparty involved in the position (this will be `partyA`)
* **`filled_amount_close`**: The filled amount for closing the position, or `null` if the position has not been closed.
* **`action_status`**: Indicates the success or failure status of the action, such as `success` for successful processing.
* **`error_code`**: An error code if there was an issue with processing, 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`**: Describes the last action taken on the position.
* **`failure_type`**: Describes the type of failure if the action was not successful, otherwise `null`.
* **`order_type`**: Numeric code representing 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 displaying notifications related to a user's positions. It's applied in the `getNotifications` function in the core package's `src/state/notifications/thunks.ts`\\

## Websockets

You can use the various hedger WebSockets to stream real-time data from the hedger, ensuring that the frontend displays the most up-to-date information. Below are the most commonly used websockets:

### **Funding Rate WebSocket**

`api/ws/funding-rate-ws`

Streams real-time data on the funding rates for various trading symbols.

**Parameters:**

* `string[]` - An array of symbol names for which funding rate updates are requested (e.g., `["BTCUSDT", "ETHUSDT", "XRPUSDT"]`).

**Returns:**

* **`next_funding_time`**: The timestamp for 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 for this implements this Websocket and can be found in the core package at `src/state/hedger/thunks.ts`.

### **Unrealized PnL WebSocket**

`api/ws/upnl-ws`

Streams real-time data on the profit and losses of a user (This may be deprecated soon).

**Parameters:**

* **`string`** Address of the sub-account.

**Returns:**

* **`upnl`**: Unrealized profit and loss for the account's current 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`**: Represents the penalty the liquidated entity must pay to the other party.
* **`lf`**: Liquidation Fee. This will be awarded to the Liquidator that liquidates the position.
* **`party_a_mm`**: Maintenance Margin of `partyA`. The amount that is actually behind the position and is considered in liquidation status.
* **`party_b_mm`**: Maintenance Margin of `partyB`. The amount that is actually behind the position and is considered in liquidation status.
* **`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`

This WebSocket streams real-time data on the state of trading positions. It ensures that the frontend can display the most current information regarding the state of various trading positions.

**Parameters:**

* `address[]`: An array of counterparty addresses for which position state updates are requested.

**Returns:**

* `id`: Identifier for the position state event.
* `create_time`: The timestamp when the position was created.
* `modify_time`: The timestamp 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 ckise.
* `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

## Building with the SYMMIO Architecture

The SYMM platform is built on a robust, modular architecture that leverages advanced concepts like the Diamond Standard for upgradeable contracts, MultiAccount management, and a suite of specialized facets to manage trading operations. This section serves as your comprehensive guide to understanding and working with the core components of SYMMIO. Whether you are creating a frontend or developing an additional utility for your project, this guide provides the foundational knowledge and practical instructions to get you started.

{% hint style="info" %}
To learn how to build a trading bot using SYMMIO, refer to [this guide](/trader-documentation/building-a-trading-bot)
{% endhint %}

### Overview

This section is designed to help developers understand how the SYMM ecosystem is structured and how its various components interact. You will learn about the architectural principles behind SYMM—including modularity, upgradeability, and separation of concerns—and how these design choices enable secure and efficient trading on the platform.

The upcoming subsections will cover the following topics:

* **Diamond Explainer:**\
  Explore the core of the SYMMIO architecture built on the Diamond Standard (EIP‑2535). This subsection will explain how the diamond pattern is used to create a flexible and upgradeable smart contract system, breaking down facets, selectors, and storage management.
* **MultiAccount Explainer:**\
  Understand the MultiAccount design, which allows users to manage sub‑accounts and delegate trading actions. Learn how this system provides both cross‑account and isolated account functionality, and the mechanisms behind access delegation and revocation.
* **Creating an Account:**\
  Walk through the process of creating a new sub‑account using the SYMMIO MultiAccount contract. This section will describe account initialization, role assignment, and the deployment of account contracts.
* **Depositing and Allocating:**\
  Learn how to fund your SYMMIO sub-account by depositing collateral and then allocating those funds for trading. Detailed examples will illustrate how the allocation process works and how it affects your trading capacity.
* **Sending a Quote:**\
  Discover how to initiate a trade by sending a quote. This part covers the core functionality for setting up trade requests—including specifying the trading symbol, price, quantity, and risk parameters—and how these requests are processed by the system.
* **Closing a Quote:**\
  Understand the standard process for closing a position. This section details how a user can request a position close, the various states involved, and the roles of both Party A and Party B during the closing process.
* **Sending a Quote (Instant Open):**\
  This variant of the quote-sending process is designed for scenarios where immediate execution is desired. Learn how the Instant Open mechanism works, its benefits, and its trade-offs compared to the standard quote process.
* **Closing a Quote (Instant Open):**\
  Explore the instant close mechanism that allows positions to be closed immediately. This approach minimizes delays and is designed for users who need to react swiftly to changing market conditions

Each subsection will provide a detailed explanation, function signatures, example usage, and event descriptions to help you build with SYMM confidently. By the end of this section, you will have a clear understanding of how to leverage the SYMM architecture to create, manage, and execute trades in a secure, modular, and upgradeable environment.


# Introduction and Diamond Explainer

A Brief Overview of the Diamond Architecture

SYMMIO's core contracts are organised 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 would not fit inside a single contract under the 24 KB EIP-170 limit. The Diamond pattern lets us split the logic across many smaller facets while keeping a single stable address that users and integrators interact with.

It also gives us per-function upgradeability. New behaviour can be added or fixed without redeploying the whole protocol or changing the address frontends, indexers, and bots are pointing 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 is 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 (also 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/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 analogous set of storage areas for quote state, balances, funding rates, etc.


# Querying Info from the SYMMIO Diamond

## Querying Info from the SYMMIO Diamond

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

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

This page walks through the most common things a frontend developer reaches for the Inspector to do.

{% 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 also human-readable:

```
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 you've loaded a diamond, the Inspector shows every **facet** registered on it, along with every **function selector** that facet implements. This is the single source of truth for "is function X live on chain Y" — if a selector is missing from this 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` / `pure`) and the Inspector generates 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="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 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.

### Decoding calldata

If you've got a raw `0x…` blob — from a failed transaction, a log, 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 debug an Instant Layer `SignedOperation` — sign one, paste the `callData` into the decoder, and verify it's calling what you think it's calling before posting to the solver.

### Reading bind state for an account

When wiring up the Instant Layer flow, the most common "why is `sendQuote` reverting" answer 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` returned by 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` / etc.), the bound PartyB, and the affiliate.

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


# MultiAccount

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

{% hint style="info" %}
This guide has been updated to reflect the Etherscan V1 -> V2 upgrade.
{% 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.)


# Creating an Account and Depositing Funds

In SYMM, managing your trading capital starts with creating a dedicated sub‑account via the [MultiAccount](/exchange-builder-documentation/frontend-builder-technical-guidance/multiaccount) contract. These sub‑accounts allow you to interact with the SYMM system by forwarding your calls through your deployed account contract.

### Step 1: Creating a Sub‑Account

1. **Navigate to the MultiAccount Contract:**
   * Use your preferred blockchain explorer or development tool to interact with the MultiAccount contract.
2. **Call `addAccount()`:**
   * Call `addAccount(string name)` function, passing a name for your sub‑account.
   * **Example Call:**

     ```solidity
     multiAccount.addAccount("TradingAccount");
     ```
3. **Check Transaction Logs:**
   * Once the transaction is mined, look for the `AddAccount` event in the transaction logs. An AddAccount event will be emitted:

     ```
     AddAccount(address owner, address account, string name)
     ```
4. **Query Your Accounts:**
   * You can also use the view function `getAccounts(address user, uint256 start, uint256 size)` on the MultiAccount contract to retrieve an array of your sub‑accounts.
   * **Parameters:**
     * `user`: Your wallet address.
     * `start`: The starting index (for pagination).
     * `size`: The number of accounts to return.
   * This returns a list of Account structs that include both the sub‑account addresses and their assigned names.

### Step 2: Depositing and Allocating Funds

After creating your sub‑account, the next step is to fund it. SYMM uses a two‑step process where you first deposit funds and then allocate those funds for trading.&#x20;

{% hint style="info" %}
Use the `depositAndAllocateForAccount()` function on the **MultiAccount** contract rather than the deposit/allocate functions on the Diamond contract to deposit and allocate to a sub-account.&#x20;
{% endhint %}

1. **Call** [**`depositAndAllocateForAccount`**](/contract-documentation/symmio-perps-v0.8.4/helper-contracts/multiaccount#depositandallocateforaccount)**`()`:**
   * This function deposits collateral into your sub‑account and immediately allocates it for trading.
   * **Function Signature:**

     ```solidity
     function depositAndAllocateForAccount(address account, uint256 amount) external onlyOwner(account, msg.sender) whenNotPaused;
     ```
   * **Parameters:**
     * `account`: The address of your sub‑account.
     * `amount`: The amount of collateral you wish to deposit (in the token's native decimals).
2. **Process:**
   * The function retrieves the collateral token address from the Symmio platform.
   * It then transfers the specified amount from your wallet to the MultiAccount contract.
   * The contract approves the Symmio contract to spend the collateral.
   * It calls the `depositFor(account, amount)` function on the Symmio contract.
   * The amount is converted into 18‑decimals and passed to the `allocate(uint256)` function via an internal call.
   * **Example Call (JavaScript with web3):**

     ```javascript
     const depositAmount = web3.utils.toWei("10", "ether");
     await multiAccountContract.methods
       .depositAndAllocateForAccount(subAccountAddress, depositAmount)
       .send({ from: myWalletAddress });
     ```
3. **Events:**
   * On successful execution, the function emits two events:
     * **DepositForAccount:** Indicates that funds have been deposited into the sub‑account.
     * **AllocateForAccount:** Confirms that the deposited funds have been allocated for trading.

{% hint style="info" %}
The collateral token address can be found by calling `getCollateral()` on the SYMM Diamond's view facet
{% endhint %}


# Sending a Quote

Sending a quote is a core action on the SYMM platform. It allows Party A to signal an intent to trade by submitting a detailed quote request. To ensure that the quote is accepted and ultimately opened by a hedger (Party B), you must follow the steps below and provide accurate parameters. Debugging is difficult with MultiAccount as the transaction could fail for a number of reasons.&#x20;

{% hint style="info" %}
`sendQuoteWithAffiliate()` should be used to send quotes.
{% 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 Party B addresses (hedgers/solvers) that Party A is willing to trade with.\
\&#xNAN;*Example:*

```javascript
const partyBsWhiteList = [solverAddress1, solverAddress2];
```

***

### **`symbolId`**

The identifier for the trading symbol.

{% hint style="info" %}
Use the [`getSymbols()`](/contract-documentation/symmio-perps-v0.8.4/facets/view-facet-0.8.4#getsymbol-9-and-getsymbols) function on the SYMM Diamond to see available symbols, or query a solver’s supported symbols from the [contract-symbols endpoint](/api-endpoints-and-deployments/solver-addresses-and-endpoints/rasa-capital#get-contract-symbols) (RASA).
{% endhint %}

***

### **`positionType`**

Enum indicating the type of position:

```solidity
enum PositionType { LONG, SHORT }
```

Use `0` for LONG positions and `1` for SHORT positions.

***

### **`orderType`**

Enum indicating the order type:

```solidity
enum OrderType { LIMIT, MARKET }
```

Use `0` for LIMIT orders and `1` for MARKET orders.

***

### **`price`**

For **LIMIT** orders:&#x20;

This is the exact price (in 18 decimals) at which you wish to open the position.

For **MARKET** orders:&#x20;

The price a quote is opened at includes the spread that a solver charges. In order for your MARKET quote to be accepted, you should send an price slightly adjusted from the current market price depending on the position type. This is an adjusted price includes a slippage buffer (e.g., 5%).

* **LONG Order:** Increase the price you receive from Muon by 5% (your position opens slightly higher to reflect the hedger's spread)
* **SHORT Order:** Decrease the price you receive from Muon by 5% (your 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 would be 5e18.

### **`cva, lf, partyAmm, partyBmm`**

These represent 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 get liquidated and `cva` is the penalty that the liquidated side should pay to the other.
* **`lf` (Liquidator Fee):** Fee reserved for liquidators.
* **`partyAmm` (Party A Maintenance Margin):** Margin required for Party A.
* **`partyBmm` (Party B Maintenance Margin):** Margin required for Party B.&#x20;

{% hint style="info" %}
Query the solver’s [`get_locked_params`](/api-endpoints-and-deployments/solver-addresses-and-endpoints/rasa-capital#get-get_locked_params) endpoint. Multiply the notional value by the returned percentage (divided by 100) for each parameter. If you sent a MARKET order, make sure that the notional is calculated based on the adjustedPrice you sent (with the slippage included)
{% endhint %}

**The general formula to send these values as parameters are described below:**

**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 based on the price you wish to send to the contract, not the Muon price. It should reflect the hedger's spread.**

**Formula for MARKET orders:**

*notionalValue = (quantity \* adjustedPrice)*&#x20;

**Formula for LIMIT orders:**

*notionalValue = (quantity \* requestedPrice)*&#x20;
{% 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 allowed by Party A, converted to 18 decimals (this is usually 200e18)

{% hint style="info" %}
You can obtain this from the solver's [`contract-symbols`](/api-endpoints-and-deployments/solver-addresses-and-endpoints/rasa-capital#get-contract-symbols) endpoint (RASA)
{% endhint %}

### **`deadline`**

A Unix timestamp marking the expiry of the quote. Ensure it’s set far enough in the future for a solver to act.

### **`affiliate`**

The affiliate address. This is the frontend's MultiAccount contract address.

### **`upnlSig`**

\
This is A `SingleUpnlAndPriceSig` struct that confirms:

* **`reqId`:** A unique request identifier.
* **`timestamp`:** When the signature was generated.
* **`upnl`:** The unrealized profit and loss for Party A.
* **`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 is obtained from the Muon oracle by calling the `uPnl_A_withSymbolPrice` method.&#x20;
{% 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 Example for fetching and formatting the upnlSig for a 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 using `sendQuoteWithAffiliate()`, you must encode the function call with the SYMM Diamond’s ABI before forwarding it through your MultiAccount contract. This ensures that the parameters are formatted correctly and that the Diamond delegates the call to the appropriate facet.

The transaction payload before encoding should be formatted 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 convert the `sendQuoteWithAffiliate` function call into a byte string. For example:

   ```javascript
   const encodedSendQuoteWithAffiliateData = web3.eth.abi.encodeFunctionCall(
     sendQuoteWithAffiliateFunctionAbi,
     sendQuoteWithAffiliateParameters
   );
   ```

   * **sendQuoteWithAffiliateFunctionAbi:** The ABI definition for the `sendQuoteWithAffiliate` function.
   * **sendQuoteParameters:** An array containing all parameters in the order defined by the function signature.
2. **Prepare the Call Data:**

   The MultiAccount contract’s `_call()` method expects an array where:

   * The first element is the sub‑account address.
   * The second element is an array containing your encoded function call.

   ```javascript
   const _callData = [ subAccountAddress, [ encodedSendQuoteWithAffiliateData ] ];
   ```
3. **Send the Transaction:**

   Finally, forward the encoded call through your MultiAccount contract.&#x20;

   ```javascript
   const sendQuoteWithAffiliateReceipt = await multiAccountContract.methods._call(..._callData).send({
     from: account.address,
     gas: adjustedGasLimit.toString(),
     gasPrice: sendQuotePrice.toString()
   });
   ```

By following these steps, you can successfully encode and send a quote with affiliate information using the `sendQuoteWithAffiliate()` function in SYMM.

***

### Finding the Quote ID

Once you send a quote using the `sendQuoteWithAffiliate()` function, the system will emit a **SendQuote** event. This event confirms that your quote has been successfully submitted and provides a unique identifier (the `quoteId`) along with key details of your quote. For example, the event is defined as follows:

```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
);
```

You can use this ID to track and query your quote later, using [getQuote()](/contract-documentation/symmio-perps-v0.8.4/facets/view-facet-0.8.4#getquote).

***


# Closing a Quote

Closing a quote is a critical step for Party A to exit an open position. The process involves sending a close request that specifies which portion of the position to close, at what price, and by when. Similar to sending a quote, you must encode your function call with the SYMM Diamond’s ABI and forward it via your MultiAccount contract.

### 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 that you want to close.

### **`closePrice`**

For **LIMIT** orders:&#x20;

This is the exact price (in 18 decimals) at which you wish to open the position.

For **MARKET** orders:&#x20;

The price a quote is opened at includes the spread that a solver charges. In order for your MARKET quote to be accepted, you should send an price slightly adjusted from the current market price depending on the position type. This is an adjusted price includes a slippage buffer (e.g., 5%).

* **For LIMIT orders:** This is the price at which you wish to close the position, expressed in 18 decimals.\
  **Important:** If you are using a MARKET order (where a slippage buffer is applied), you must adjust the closePrice accordingly.
  * For a LONG position, increase the current market price by your chosen slippage percentage. (e.g. 5%)
  * For a SHORT position, decrease the market price by that percentage. (e.g. 5%)

This adjusted price accounts for the hedger’s spread and ensures that your close request can be accepted.

### **`quantityToClose`**

The amount of the position (in 18‑decimal format) that you want to close.

### **`orderType`**

An enum value indicating the type of closing order:

* `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 this time, [the request will expire](#user-content-fn-1)[^1].

### Parameter Encoding

All function calls must be encoded using the SYMM Diamond’s ABI. For closing a quote, you must encode the parameters for the `requestToClosePosition()` function and pass the resulting calldata to your MultiAccount contract’s `_call()` method. For example:

```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 Diamond’s ABI
const encodedClosePositionData = web3.eth.abi.encodeFunctionCall(
  requestToClosePositionFunctionAbi, // ABI definition for requestToClosePosition
  closePositionParameters
);

// Prepare call data for MultiAccount
const _callData = [accountAddress, [encodedClosePositionData]];

// Estimate gas and send the transaction via the MultiAccount contract
const gasPrice = await web3.eth.getGasPrice();
const gasEstimate = await multiAccountContract.methods._call(..._callData).estimateGas({ from: process.env.WALLET_ADDRESS });
const gasEstimateBigInt = BigInt(gasEstimate);
const bufferPercentage = 0.20; // 20% buffer for gas
const adjustedGasLimit = gasEstimateBigInt + (gasEstimateBigInt * BigInt(20) / BigInt(100));

const transactionReceipt = await multiAccountContract.methods._call(..._callData).send({
  from: process.env.WALLET_ADDRESS,
  gas: adjustedGasLimit.toString(),
  gasPrice: gasPrice.toString()
});

console.log("Transaction Receipt:", transactionReceipt);
```

[^1]: what do we do about expired quotes?


# 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 (Solvers)](/liquidity-provider-documentation/building-a-solver-on-symmio/instant-trading/instant-trading-solvers)
{% 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 is that now the broad  `delegateAccess()` is gone and replaced by a more specific, signature-driven flow on the new **Instant Layer** that lives next to a standalone **Account Layer** diamond.&#x20;

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

### End-to-end Summary

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

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

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

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

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

### 1. Create a sub-account on the Account Layer

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

Code Snippet:

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


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

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

`isolationType` constants from `_common.py`:

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

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

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

***

### 2. Deposit collateral

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

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


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

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

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

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

***

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

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

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

From `steps/02b_bind_partyb.py`:

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

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


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

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

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

***

### 4. Generate a session key and grant delegations

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

1. Generate a fresh session key in-process.
2. Call `grantDelegation` on the Instant Layer to give the session key the trading selectors.
3. Call `grantDelegation` again to give the TPSL bot its own narrower close-selector set.

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

Selectors:

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

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

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

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


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

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

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

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

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

***

### 5. The EIP-712 `SignedOperation` helper

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

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


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


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

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

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

* **`signerAccount.addr` differs by operation.** On open it's the **sub-account** (because `sendQuote` runs from the sub-account, which then creates the VA). On close it's the **virtual account** (because the position lives on the VA and the close acts on it).
* **Salt-only nonces.** `nonce=0` lets multiple operations execute in any order as long as each `salt` is unique. Use sequential nonces only when ordering matters.
* **`maxUses=1`** for standard ops. `maxUses=0` means unlimited until the deadline, useful for flex-field flows.
* **String-encoded numerics on the wire.** The solver's HTTP layer expects `"0"` / `"1"` / decimal strings for `nonce`, `maxUses`, `deadline` even though they're typed as `uint256`. The signed hash uses the integers; the wire format stringifies.

***

### 6. Open a position (instant\_open)

Two `SignedOperation` ops, posted as one batch:

1. `addMarginToNextVA(subAccount, vaIsoType, symbolId, marginWei)` on the **Account Layer** — pre-funds the next predicted VA address with margin transferred from the sub-account.
2. `sendQuoteWithAffiliateAndData(...)` on **Symmio Core** — sends the quote from the new VA, with an empty Muon-sig sentinel (the solver injects the real one) and the `data` field carrying a client-side correlation ID.

Calldata encoders from `steps/_common.py`:

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


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


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

Opening a position:

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


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

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

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

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

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

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

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

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

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

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

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

#### Locked-param formulas

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

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

#### v0.8.5 nuances on the open path

* **Custom quote data.** The `data` field on `sendQuoteWithAffiliateAndData` persists arbitrary bytes on the quote. The reference implementation stuffs a UUID into it via `abi_encode(["(string)"], [(uuid,)])` so the off-chain system can correlate websocket reports back to the trade.
* **Empty Muon sig.** Because the sub-account is bound to PartyB (step 3), the contract skips Muon validation — the solver's empty sentinel passes. Don't try to fill in real Muon data; the solver injects what it needs server-side.
* **Cross-mode solvers** may report `nonce: 0` on PartyB-side signatures (parallel-safe). Doesn't change anything you send.

***

### 7. Close a position (instant\_close)

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

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

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

Code Snippet:

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


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

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

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

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

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

***

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

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

The EIP-712 domain and types:

```python
from _common import CHAIN_ID, INSTANT_LAYER

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

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

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

Build, sign, submit:

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


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

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

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

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

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

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

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

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

{% hint style="info" %}
**NOTE:** Every numeric string in the order should match the symbol's `price_precision` / `quantity_precision`. The COH may reject unbounded decimals with error 407 *"provided values do not meet the required precision."*

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

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


# 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.\
\
\&#xNAN;*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.\
       \&#xNAN;*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 for the normal cooldown period, which is 12 hours. Instead, you select a fee option that matches how fast you want your withdrawal processed and withdraw funds through a bridge. This guide shows how to do this without using a frontend application.

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 to execution.

### Before You Start

You’ll need:

* **Python 3.8** or newer
* A wallet private key (used for signing and paying gas)
* An RPC URL for the chain you are 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 configuration from a .env file.\
You’ll need to use the private key corresponding with the wallet that you wish 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 on Base USDC has 6 decimals.
{% endhint %}

***

### Running the Script

From the project root:

```bash
python instant_withdrawal.py
```

Once started, the script walks through all steps automatically.

***

### What the Script Does (Step by Step)

Below is the exact flow, mapped directly to the Python code.

***

#### 1. Sign in with your wallet (SIWE)

The script first proves wallet ownership using Sign-In With Ethereum and gets an access token.

This gives it 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

Next, the script asks the backend for available 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, they must be freed 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 step must complete on-chain before continuing.

***

#### 4. Transfer funds to the withdrawal bridge (on-chain)

Once funds are free, they are 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

Now that the bridge transfer exists on-chain, the script fetches again 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 automatically 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 this is done, the withdrawal is scheduled and will execute automatically.

## Reference Implementation

[This repository](https://github.com/academy17/instant_withdrawal_reference) contains a python script showcasing the full flow for Instant Withdrawal.&#x20;


# 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
}
```


# Role of a Liquidity Provider (Solver)

On SYMMIO, traders don’t match against a central order book. They express intents, which are requests which say “I want to go long or short this market, here’s my size, my price, and the counterparties I wish to trade with.*”* Those intents are picked up by a network of independent counterparties called [solvers](/getting-started/glossary-of-terms). A solver is the party that decides whether to take the other side of a trade, quote prices, and manage the risk that comes with it.

When you run a solver, you are acting as the [Party B](/getting-started/glossary-of-terms) in the protocol. Your address is whitelisted on the SYMMIO contracts so you are allowed to 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 this 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 are also responsible for collateral management and solvency on their side. Before opening or closing a position, the contracts and [Muon oracle](/api-endpoints-and-deployments/muon-api) verify that both you and the trader have enough margin, and that the prices you’re using 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 profits so that positions can be safely closed. Your risk model is entirely up to you, however it must conform to the protocol's solvency rules enforced on the contracts.

Solvers also play a key role in the user experience. Frontend Applications rely on solver APIs to show things like markets, prices and funding data. When a trader wishes to do things like [instantly open a trade](/trader-documentation/how-trading-works-in-symmio/instant-trading), they delegate certain functions to you so that you can open and close positions without asking them to sign every transaction. When they'd like to set a [take profit or stop loss](/trader-documentation/how-trading-works-in-symmio/using-take-profit-and-stop-loss), your **conditional orders** service will watch the market and submit the close request when their conditions are hit. From the trader’s perspective, the frontend application feels like a fast, seamless 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 Trading Fees

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

## Settlement Costs in Symmio

**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](/liquidity-provider-documentation/solver-trading-fees/perps-settlement-costs)

[Pair Trading - Settlement costs](/liquidity-provider-documentation/solver-trading-fees/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")

## 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% (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

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 or more
* 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 or 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: `37,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>


# 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.4) can be found [here](https://docs.symm.io/protocol-architecture/technical-documentation/contracts-documentation-0.8.2).

## 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](/getting-started/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-solvers). 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 presets 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.


# Building a Solver on SYMMIO


# 1. Intent Life Cycle

The lifecycle of an Intent begins with **PartyA** (traders) initiating a trade request on the SYMMIO platform by specifying key parameters for the desired trade. This request, known as an "Intent," is broadcast on-chain via the platform's core contracts. Once posted, it becomes visible to **PartyB**, the hedgers or market makers, who have the option to review and respond.

Hedgers/Solvers monitor these on-chain requests using tools such as event listeners or subgraphs. They evaluate the parameters of each Intent against their strategy, risk profile, and market conditions. If a hedger decides to proceed, they can claim the Intent and respond by submitting a corresponding transaction on-chain. This transaction indicates their agreement to the proposed terms or includes any modifications they deem necessary, such as an adjusted price or collateral requirement.

## PartyA SendQuote Flow Overview

* Creates a Sub-Account: Through A [MultiAccount](/contract-documentation/version-history/contracts-documentation-0.8.2/multiaccount) contract deployed by a frontend.
* Deposits and Allocates Collateral: To the created sub-account.
* `sendQuote()` with the desired parameters. This requires two things:
  * A Muon Signature is fetched verifying the partyA's ability to open a position, and the price of the asset (this is usually handled by the frontend)
  * The transaction is encoded via the `_call()` function on the MultiAccount contract, which will forward to `sendQuote()` on the Diamond contract.

After being whitelisted as a PartyB by the SYMMIO developers, hedgers can use the Cloverfield interface to allow them to send quotes using the `sendQuote` functionality. This interface enables hedgers to easily verify the functionality of their service and test their strategies and workflows in real time.&#x20;

More documentation on sendQuote is available here ([0.8.2](/contract-documentation/version-history/contracts-documentation-0.8.2/facets/partya-facet), [0.8.3](/contract-documentation/version-history/contracts-documentation-0.8.3/facets/partya-facet))


# 2. Seeing the Intent

To build a robust hedging service in the SYMMIO ecosystem, it's crucial for hedgers (PartyB) to monitor blockchain events triggered by PartyA's intents. This section shows a basic implementation of an event listener using `ethers.js` or `web3.py`.&#x20;

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

### Web3EventPoller

The `Web3EventPoller` class is a high-level utility designed to:

1. **Connect to the Blockchain**: Establish a connection to the blockchain network using `web3.py`.
2. **Load Contract and ABI**: Use the contract's address and ABI to interact with its events.
3. **Monitor Events**: Listen for specific on-chain events such as `SendQuote` or `ForceCancelQuote`.
4. **Handle Events**: Delegate event handling to predefined handler classes (e.g., `SendQuoteHandler`).
5. **Maintain State**: Use an `EventPoller` instance to manage event polling, handle retries, and ensure no events are missed.

```python
from web3 import Web3
from web3.middleware import geth_poa_middleware
import logging
from settings import RPC, ContractAddress, PollInterval, FromBlock
from event_poller import EventPoller
from handlers.send_request_handler import SendQuoteHandler

class Web3EventPoller:
    @staticmethod
    def run():
        # 1. Initialize Web3 and contract
        w3_instance = Web3(Web3.HTTPProvider(RPC))
        w3_instance.middleware_onion.inject(geth_poa_middleware, layer=0)
        contract_abi = [...]  # Loaded from a JSON file
        contract = w3_instance.eth.contract(address=ContractAddress, abi=contract_abi)

        # 2. Create a poller instance
        poller = EventPoller(
            context=...,
            from_block=FromBlock,
            poll_interval=PollInterval
        )

        # 3. Register event + handler
        poller.add_event("SendQuote", SendQuoteHandler().handle)
        # ... add other events as needed ...

        # 4. Start the poller
        poller.start()
        poller.wait()
```

### Event Poller Internals

`EventPoller`: A utility script that tracks from\_block progress (often persisted in something like Redis) and polls in intervals (poll\_interval).

```python
class EventPoller:
    def __init__(self, context, from_block=0, poll_interval=10, block_chunk_size=10000):
        # 1. Initialize context, default block, and interval settings.
        # 2. Create data structures to track events and their states.
        # 3. Set up a thread pool executor for concurrent event polling.
        self.context = context
        self.tracked_events = {}
        self.poll_interval = poll_interval
        self.block_chunk_size = block_chunk_size
        self.stop_signal = False
        self.default_from_block = from_block
        self.futures = {}
        self.event_from_blocks = {}
        self.executor = ThreadPoolExecutor()

    def add_event(self, event_name, callback=None):
        # 1. Register the event name and callback in `tracked_events`.
        # 2. Check Redis for the last processed block for the event.
        # 3. Set `from_block` for the event based on Redis or the default value.
        self.tracked_events[event_name] = callback
        stored_from_block = REDIS_SERVER.get("event_poller_from_block_" + event_name)
        self.event_from_blocks[event_name] = int(
            stored_from_block) if stored_from_block is not None else self.default_from_block

    def remove_event(self, event_name):
        # 1. Remove the event and its block tracking state.
        # 2. Stop the associated polling thread if it's running.
        if event_name in self.tracked_events:
            del self.tracked_events[event_name]
            del self.event_from_blocks[event_name]
            self.stop_event_thread(event_name)

    def _fetch_event(self, event_name, callback, from_block, to_block):
        # 1. Retrieve the event object from the contract.
        # 2. Create a filter for logs between `from_block` and `to_block`.
        # 3. Fetch all logs and update the last processed block in Redis.
        # 4. Invoke the callback for each log if provided.
        event = getattr(self.context.contract.events, event_name)
        event_filter = event.create_filter(fromBlock=from_block, toBlock=to_block)
        events = event_filter.get_all_entries()
        REDIS_SERVER.set_permanent("event_poller_from_block_" + event_name, to_block)
        self.event_from_blocks[event_name] = int(to_block) + 1
        if callback:
            for ev in events:
                callback(self.context, ev)
        return events

    def _run_event(self, event_name, callback):
        # 1. Poll the blockchain to get the current block number.
        # 2. Fetch logs for the event in chunks until the current block.
        # 3. Invoke the callback for each log and sleep for `poll_interval`.
        while not self.stop_signal:
            current_block = self.context.provider.eth.block_number
            from_block = self.event_from_blocks[event_name]
            while from_block < current_block:
                end_block = min(from_block + self.block_chunk_size - 1, current_block)
                self._fetch_event(event_name, callback, from_block, end_block)
                from_block = end_block + 1
            time.sleep(self.poll_interval)

    def start(self):
        # 1. Spawn a thread for each registered event using the executor.
        # 2. Each thread invokes `_run_event` to monitor and process logs.
        for event_name, callback in self.tracked_events.items():
            future = self.executor.submit(self._run_event, event_name, callback)
            self.futures[event_name] = future

    def wait(self):
        # 1. Block until all threads complete their tasks.
        # 2. Ensures the application does not exit prematurely.
        for future in self.futures.values():
            future.result()

    def stop(self):
        # 1. Set `stop_signal` to True to gracefully terminate polling.
        # 2. Used to shut down the poller when the application exits.
        self.stop_signal = True

    def stop_event_thread(self, event_name):
        # 1. Cancel the thread associated with the specified event.
        # 2. Remove the thread from the `futures` dictionary.
        if event_name in self.futures:
            self.futures[event_name].cancel()
            del self.futures[event_name]
```

### Send Quote Handler

The `SendQuoteHandler` processes `SendQuote` events from PartyA. It should validate the event data (e.g., whitelist checks, quote status) and construct and stores a structured payload in Redis for further use by the system.

```python
import datetime
import logging
import simplejson as json
from settings import HedgerAddress
from share import REDIS_SERVER_EVENTS


class SendQuoteHandler:

    def handle(self, context, args):
        # 1. Extract event data and block information
        event_values = args.get('args')
        block = context.provider.eth.get_block(args.get("blockNumber"))

        # 2. Fetch additional data for the symbol and quote
        symbol = context.symbol_fm.map_fields(
            context.view_provider.contract.functions.getSymbol(event_values.get("symbolId")).call()
        )
        quote = context.quote_fm.map_fields(
            context.view_provider.contract.functions.getQuote(event_values.get("quoteId")).call()
        )

        # 3. Check if the quote has already been processed
        quote_status = quote.get("quoteStatus")
        if quote_status == 3:
            return

        # 4. Validate if the hedger is in the whitelist for this quote
        whitelist = [context.provider.to_checksum_address(addr) for addr in event_values.get("partyBsWhiteList")]
        if len(whitelist) > 0 and HedgerAddress not in whitelist:
            return

        # 5. Construct a payload with relevant event and blockchain data
        value = {
            "blockNumber": args.get("blockNumber"),
            "cva": event_values.get("cva"),
            "lf": event_values.get("lf"),
            "partyAmm": event_values.get("partyAmm"),
            "partyBmm": event_values.get("partyBmm"),
            "quantity": event_values.get("quantity"),
            "marketPrice": event_values.get("marketPrice"),
            "requestedOpenPrice": event_values.get("price"),
            "quoteStatus": quote.get("quoteStatus"),
            "quoteId": event_values.get("quoteId"),
            "symbolId": event_values.get("symbolId"),
            "symbol": symbol.get("name"),
            "action": "SendQuote",
            "partyA": context.provider.to_checksum_address(event_values.get("partyA")),
            "positionType": event_values.get("positionType"),
            "orderTypeOpen": event_values.get("orderType"),
            "maxFundingRate": event_values.get("maxFundingRate"),
            "deadline": event_values.get("deadline"),
            "timeStamp": block.get("timestamp"),
            "timestampsSendQuoteTimeStamp": block.get("timestamp"),
            "trHash": args.get("transactionHash").hex(),
            "received_timestamp": datetime.datetime.utcnow().timestamp() // 1000,
            "input_source": 'py_event_listener',
            "type": 'quote'
        }

        # 6. Generate a unique key for the Redis store
        key = f"{event_values.get('quoteId')}-{args.get('transactionHash').hex()}-{block.get('timestamp')}"

        # 7. Log the event for debugging purposes
        logging.info("SendQuote Received: " + key)

        # 8. Store the event data in Redis if the key does not already exist
        REDIS_SERVER_EVENTS.setnx(key, json.dumps(value))
```

**Extract Event Data and Block Information**

```python
event_values = args.get('args')
block = context.provider.eth.get_block(args.get("blockNumber"))
```

* Retrieves the event arguments (`args`) and the blockchain block details (`block`).

**Fetch Additional Data for the Symbol and Quote**

```python
symbol = context.symbol_fm.map_fields(
    context.view_provider.contract.functions.getSymbol(event_values.get("symbolId")).call()
)
quote = context.quote_fm.map_fields(
    context.view_provider.contract.functions.getQuote(event_values.get("quoteId")).call()
)
```

* Fetches detailed information about the symbol and quote using their IDs.
* Maps the raw blockchain data into structured fields for easier access.

**Check If the Quote Is Already Processed**

```python
quote_status = quote.get("quoteStatus")
if quote_status == 3:
    return
```

Checks the status of the quote. If it has already been processed (status 3), the handler exits early.

**Validate PartyB Whitelist**

```python
whitelist = [context.provider.to_checksum_address(addr) for addr in event_values.get("partyBsWhiteList")]
if len(whitelist) > 0 and HedgerAddress not in whitelist:
    return
```

Confirms that the current hedger (PartyB) is authorized to act on this quote. If a whitelist exists and the hedger is not included, the handler skips processing.

**Constructing the Payload**

```python
value = {
    "blockNumber": args.get("blockNumber"),
    ...
    "type": 'quote'
}
```

Creates a structured dictionary containing all relevant event and blockchain data. This includes trade details (e.g., `quoteId`, `price`), contextual metadata (e.g., `blockNumber`), and hedger-specific information (e.g., `partyBmm`).

**Generate a Unique Key for Redis**

```python
key = f"{event_values.get('quoteId')}-{args.get('transactionHash').hex()}-{block.get('timestamp')}"
```

Ensures that each event is uniquely identified to avoid duplication in the Redis store.

**Logging the Event**

```python
logging.info("SendQuote Received: " + key)
```

Logs the event processing for debugging and traceability.

**Store the Event Data in Redis**

```python
REDIS_SERVER_EVENTS.setnx(key, json.dumps(value))
```

Stores the payload in Redis using the unique key. The `setnx` method ensures that the event is only stored if the key does not already exist.


# 3. Hedging Off-Chain

If the Hedger chooses a 1:1 approach, it can place a matching order on its preferred broker (Binance, BYBIT) to remain delta-neutral. Once the broker fill is confirmed, the Hedger proceeds on-chain.

{% hint style="info" %}
Hedging the position opened on-chain elsewhere is entirely optional.
{% endhint %}


# 4. Opening/Closing a Position On-Chain

## Opening a Position

At this stage, the solver calls `openPosition()` or a combined function (`lockAndOpenQuote()`).

* Partial Fills are possible: if Hedger only wants to open half the quantity, the remainder might get reposted as a new Intent.
* The contract does a final solvency check via Muon. If any party is insolvent, the contract disallows opening.

Before a Hedger can `lockQuote()` or `openPosition()`, sufficient collateral must be allocated to PartyB. This allocation ensures that PartyB meets the margin requirements for handling the position. If the allocated balance is insufficient, the contract will reject the lockQuote or openPosition transaction.

### Allocating Collateral for partyB

Below is a short script that shows how a solver would allocate collateral to a partyA. More information can be found [here](/contract-documentation/version-history/contracts-documentation-0.8.2/facets/partyb-facet).

```javascript
async function allocateForPartyB(amount, partyB, partyA) {
  try {
    console.log(`Allocating ${amount} tokens for PartyB with address ${partyA} from ${partyB}...`);

    // Estimate gas for the transaction
    const allocateGasEstimate = await diamondContract.methods.allocateForPartyB(amount, partyA).estimateGas({ from: partyB });
    console.log("Estimated Gas: ", allocateGasEstimate);

    // Fetch current gas price
    const allocateGasPrice = await web3.eth.getGasPrice();
    console.log("Current Gas Price: ", allocateGasPrice);

    // Execute the allocation transaction
    const receipt = await diamondContract.methods.allocateForPartyB(amount, partyA).send({
      from: partyB,
      gas: allocateGasEstimate,
      gasPrice: allocateGasPrice,
    });

    console.log("Allocation successful!");
    return { success: true, receipt: receipt };
  } catch (error) {
    console.error("Error during allocation:", error);
    return { success: false, error: error.toString() };
  }
}
```

### Locking the Quote

Locking the quote ensures that no other Hedger can act on the same quoteId. This step requires a valid Muon signature for the uPnl (`uPnl_B`) of the position. You can read more about this method [here](/api-endpoints-and-deployments/muon-api). The formatted signature and quoteId must be passed to the `lockQuote()` method.

```javascript
async function lockQuote(accountAddress, partyA, quoteId, increaseNonce, muonUrls, chainId, diamondAddress) {
  const lockQuoteClient = LockQuoteClient.createInstance(true);
  const signatureResult = await lockQuoteClient.getMuonSig(accountAddress, partyA, 'symmio', muonUrls, chainId, diamondAddress);

  if (signatureResult.success) {
    const { reqId, timestamp, upnl, gatewaySignature, sigs } = signatureResult.signature;
    const upnlSigFormatted = {
      reqId: web3.utils.hexToBytes(reqId),
      timestamp: timestamp.toString(),
      upnl: upnl.toString(),
      gatewaySignature: web3.utils.hexToBytes(gatewaySignature),
      sigs: {
        signature: sigs.signature.toString(),
        owner: sigs.owner,
        nonce: sigs.nonce,
      },
    };

    const gasEstimate = await diamondContract.methods.lockQuote(quoteId, upnlSigFormatted, increaseNonce).estimateGas({ from: accountAddress });
    const gasPrice = await web3.eth.getGasPrice();

    const receipt = await diamondContract.methods.lockQuote(quoteId, upnlSigFormatted, increaseNonce).send({
      from: accountAddress,
      gas: gasEstimate,
      gasPrice,
    });

    console.log("Lock Quote successful!", receipt);
  } else {
    console.error("Failed to fetch Muon signature:", signatureResult.error);
  }
}
```

### Opening the Position Script

Once the quote is locked, the solver calls `openPosition()` to finalize the transaction. This method performs a solvency check before opening.&#x20;

```javascript
async function openPosition(accountAddress, partyA, quoteId, filledAmount, openedPrice, symbolId, muonUrls, chainId, diamondAddress) {
  const openPositionClient = OpenPositionClient.createInstance(true);
  const signatureResult = await openPositionClient.getPairUpnlAndPriceSig(accountAddress, partyA, symbolId, 'symmio', muonUrls, chainId, diamondAddress);

  if (signatureResult.success) {
    const { reqId, timestamp, uPnlA, uPnlB, price, gatewaySignature, sigs } = signatureResult.signature;
    const upnlSigFormatted = {
      reqId: web3.utils.hexToBytes(reqId),
      timestamp: timestamp.toString(),
      upnlPartyA: uPnlA.toString(),
      upnlPartyB: uPnlB.toString(),
      price: price.toString(),
      gatewaySignature: web3.utils.hexToBytes(gatewaySignature),
      sigs: {
        signature: sigs.signature.toString(),
        owner: sigs.owner,
        nonce: sigs.nonce,
      },
    };

    const gasEstimate = await diamondContract.methods.openPosition(quoteId, filledAmount, openedPrice, upnlSigFormatted).estimateGas({ from: accountAddress });
    const gasPrice = await web3.eth.getGasPrice();

    const receipt = await diamondContract.methods.openPosition(quoteId, filledAmount, openedPrice, upnlSigFormatted).send({
      from: accountAddress,
      gas: gasEstimate,
      gasPrice,
    });

    console.log("Open Position successful!", receipt);
  } else {
    console.error("Failed to fetch Muon signature:", signatureResult.error);
  }
}
```

The Muon [`uPnlWithSymbolPrice` method](/api-endpoints-and-deployments/muon-api#upnlwithsymbolprice) is used for solvency verification. The `positionState` will change to OPENED upon successful execution. If any party is insolvent, the contract will reject the transaction.

### Combined Methods: `lockAndOpenQuote()`

For efficiency, solvers can use combined methods like lockAndOpenQuote(). These methods execute the locking and opening steps in a single transaction.

```solidity
	/**
	 * @notice Locks and opens the specified quote with the provided details and signatures.
	 * @param quoteId The ID of the quote to be locked and opened.
	 * @param filledAmount PartyB has the option to open the position with either the full amount requested by the user or a specific fraction of it
	 * @param openedPrice The price at which the position is opened.
	 * @param upnlSig The Muon signature containing the single UPNL value used to lock the quote.
	 * @param pairUpnlSig The Muon signature containing the pair UPNL and price values used to open the position.
	 */
	function lockAndOpenQuote(
		uint256 quoteId,
		uint256 filledAmount,
		uint256 openedPrice,
		SingleUpnlSig memory upnlSig,
		PairUpnlAndPriceSig memory pairUpnlSig
	) external whenNotPartyBActionsPaused onlyPartyB notLiquidated(quoteId) {}
```

{% hint style="info" %}
Both `uPnlSig` (derived from [uPnL\_B](/api-endpoints-and-deployments/muon-api#get-upnl_b)) and a `pairUpnlSig` (derived from [uPnlWithSymbolPrice](/api-endpoints-and-deployments/muon-api#upnlwithsymbolprice)) are required for this group action
{% endhint %}

Once opened, the position remains active until fully closed or liquidated. PartyA can place limit or market close requests, which the solver can fill. If the solver fails to respond within a certain timeframe the partyA can execute [force closes or force cancels](/contract-documentation/version-history/contracts-documentation-0.8.3/facets/partya-facet).&#x20;

## Closing a Position

PartyA calls `requestToCloseQuote()` with parameters (quantity, price if limit, etc.). The solver can close his corresponding off-chain hedge, then call fillCloseRequest() on-chain.

Closing a position on SYMMIO involves calling the `fillCloseRequest()` function. The solver (PartyB) can close an open position partially or fully, depending on the request type and specified amount.

### **How fillCloseRequest Works**

Parameters:

* `quoteId`: The unique identifier of the quote to be closed.
* `filledAmount`: The amount being closed.
* `closedPrice`: The final price at which the position is being closed.
* `upnlSig`: A Muon signature that ensures solvency and validates the closing action.

`LIMIT` requests can be closed incrementally in multiple steps however `MARKET` requests must be closed in a single transaction.

### **Preconditions for Closing**

* Ensure the Hedger (PartyB) is authorized to act on the quote. If not, contact SYMMIO developers to enable whitelisting.
* A Muon signature validates solvency for both parties before the close action is processed.
* The position must not be in the `LIQUIDATED` state, and the partyA must be solvent after the close.

### Closing the Position Script

```javascript
const Web3 = require('web3');
const OpenPositionClient = require('./OpenPositionClient'); // Assuming OpenPositionClient exists in your setup
const { abi } = require('./DiamondContractABI.json'); // ABI for the diamond contract

async function fillCloseRequest(accountAddress, partyA, partyB, quoteId, filledAmount, closedPrice, symbolId, muonUrls, chainId, diamondAddress) {
  console.log("Filling close request...");

  const closeRequestClient = OpenPositionClient.createInstance(true);
  if (!closeRequestClient) {
    console.error("OpenPositionClient is not enabled.");
    return { success: false, error: "Initialization failed" };
  }

  const web3 = new Web3(process.env.RPC_URL);
  const diamondContract = new web3.eth.Contract(abi, diamondAddress);
  const appName = "symmio";
  const urls = [muonUrls];

  try {
    // Fetch Muon signature
    const signatureResult = await closeRequestClient.getPairUpnlAndPriceSig(accountAddress, partyA, symbolId, appName, urls, chainId, diamondAddress);
    if (!signatureResult.success) {
      throw new Error(signatureResult.error || "Muon signature fetch failed");
    }

    const { reqId, timestamp, uPnlA, uPnlB, price, gatewaySignature, sigs } = signatureResult.signature;
    const upnlSigFormatted = {
      reqId: web3.utils.hexToBytes(reqId),
      timestamp: timestamp.toString(),
      upnlPartyA: uPnlA.toString(),
      upnlPartyB: uPnlB.toString(),
      price: price.toString(),
      gatewaySignature: web3.utils.hexToBytes(gatewaySignature),
      sigs: {
        signature: sigs.signature.toString(),
        owner: sigs.owner,
        nonce: sigs.nonce,
      },
    };

    // Call fillCloseRequest
    const receipt = await diamondContract.methods.fillCloseRequest(
      quoteId,
      filledAmount,
      closedPrice,
      upnlSigFormatted
    ).send({
      from: partyB,
      gas: 500000,
    });

    console.log("Close request successfully filled!");
    return { success: true, receipt };
  } catch (error) {
    console.error("Error filling close request:", error);
    return { success: false, error: error.toString() };
  }
}

module.exports = fillCloseRequest;

```

The script allows **PartyB** to close a trade initiated by **PartyA** using the `fillCloseRequest()` function on the smart contract. The Muon signature is used for solvency validation. The correct Muon method here is also [`uPnlWithSymbolPrice` ](/api-endpoints-and-deployments/muon-api#upnlwithsymbolprice)


# 5. Creating the APIs

Quote Services, Hedger API, Hedger Websockets

As a Solver integrating with SYMMIO, you’ll need to include **REST APIs**, to allow frontends to display data like available symbols, retrieving open interest and notional caps.

## REST APIs

Detailed documentation about REST API endpoint queries and returned data can be found in the following section. It's recommended that solvers follow the same structure in their endpoints to integrate with frontends easily.

The full list of endpoints that solvers can integrate is here, however not all are essential for an implementation. Essential endpoints are listed below:

### [**GET Contract Symbols: /contract-symbols**](/liquidity-provider-documentation/building-a-solver-on-symmio/5.-creating-the-apis/get-contract-symbols)

### [&#xD;**GET Open Interest: /open-interest**](/liquidity-provider-documentation/building-a-solver-on-symmio/5.-creating-the-apis/get-open-interest)

### [&#xD;**GET Notional Cap by Symbol: /notional\_cap/{symbol\_id}**](/liquidity-provider-documentation/building-a-solver-on-symmio/5.-creating-the-apis/get-notional-cap)

### [&#xD;**GET Price Range by Symbol: /price-range/{symbol}**](/liquidity-provider-documentation/building-a-solver-on-symmio/5.-creating-the-apis/get-price-range)

### [&#xD;**GET Locked Parameters for a Symbol: /get\_locked\_params/{symbol}?leverage={leverage}**](/liquidity-provider-documentation/building-a-solver-on-symmio/5.-creating-the-apis/get-get-locked-params)

### [&#xD;**GET Funding Information: /get\_funding\_info**](/liquidity-provider-documentation/building-a-solver-on-symmio/5.-creating-the-apis/get-get-funding-info)

### [**GET Error Codes: /error\_codes**](/liquidity-provider-documentation/building-a-solver-on-symmio/5.-creating-the-apis/get-error-codes)

### [**POST Position Information: /position\_state/\<offset>/\<size>**](/liquidity-provider-documentation/building-a-solver-on-symmio/5.-creating-the-apis/post-position-state)


# GET Contract Symbols

## /contract-symbols

```json
/contract-symbols
```

**Example Query:**

```
https://base-hedger82.rasa.capital/contract-symbols
```

**Example Response:**

```json
   {
      "price_precision": 1,
      "quantity_precision": 3,
      "name": "BTCUSDT",
      "symbol": "BTC",
      "asset": "USDT",
      "symbol_id": 1,
      "is_valid": true,
      "min_acceptable_quote_value": 120,
      "min_acceptable_portion_lf": "0.003000000000000000",
      "trading_fee": "0.000600000000000000",
      "max_leverage": 60,
      "max_notional_value": 2100000,
      "rfq_allowed": true,
      "hedger_fee_open": "0.0006",
      "hedger_fee_close": "0.0006",
      "max_funding_rate": "200",
      "min_notional_value": "100",
      "max_quantity": "1000",
      "lot_size": "0"
    },
    {
      "price_precision": 3,
      "quantity_precision": 1,
      "name": "FILUSDT",
      "symbol": "FIL",
      "asset": "USDT",
      "symbol_id": 55,
      "is_valid": true,
      "min_acceptable_quote_value": 10,
      "min_acceptable_portion_lf": "0.004000000000000000",
      "trading_fee": "0.000800000000000000",
      "max_leverage": 50,
      "max_notional_value": 1750000,
      "rfq_allowed": true,
      "hedger_fee_open": "0.0006",
      "hedger_fee_close": "0.0006",
      "max_funding_rate": "200",
      "min_notional_value": "5",
      "max_quantity": "10000000",
      "lot_size": "0"
    }, //...
```

#### **Data Sources**

Values are derived from two sources:

1. **On-Chain Contract Data**: Immutable parameters fetched directly from the blockchain (via `getSymbol(symbolId)` or`getSymbols(start, size)`).
2. **Hedger-Defined Parameters**: Configurable values set by the market maker/hedger, subject to on-chain constraints.

**Contract-Sourced Values**

* **`symbol_id`**: Unique identifier for the trading pair (e.g., `1` for BTCUSDT). Mapped directly from the contract’s `symbolId`.
* **`name`**: Trading pair name (e.g., `BTCUSDT`).&#x20;
* **`is_valid`**: Indicates if the symbol is active for trading. Mapped from the contract’s `isValid`.
* **`min_acceptable_quote_value`**: Minimum quote value (e.g., $120) required to open a position. Converted from the contract’s fixed-point value (e.g., `120000000000000000000` → `120`).
* **`min_acceptable_portion_lf`**: Minimum portion of the liquidity fee (e.g., `0.003`). Converted from the contract’s fixed-point value (e.g., `3000000000000000` → `0.003`).
* **`trading_fee`**: Fee charged per trade (e.g., `0.0006` or 0.06%). Derived from the contract’s `tradingFee` (e.g., `600000000000000` → `0.0006`).

**Hedger-Defined Values**

* **`max_leverage`**: Maximum leverage allowed (e.g., `60x`). Must be **≤** the contract’s `Leverage` (e.g., `60` ≤ `100`).
* **`max_notional_value`**: Maximum position size (e.g., $2,100,000). Must be **≥** the contract’s `minAcceptableQuoteValue` (e.g., `2100000` ≥ `120`).
* **`max_quantity`**: Maximum tradable quantity (e.g., `1000` BTC). No on-chain constraints.
* **`max_funding_rate`**: Upper limit for funding rate (e.g., `200` ). Set by the hedger.
* **`min_notional_value`**: Minimum position size (e.g., $100). Configured by the hedger.
* **`hedger_fee_open`/`hedger_fee_close`**: Additional fees charged by the hedger (e.g., `0.0006`).
* **`rfq_allowed`**: Whether Request for Quote (RFQ) is enabled for the symbol.

**Derived Values**

* **`symbol`/`asset`**: Base and quote assets (e.g., `BTC` and `USDT`). Parsed from the `name` field (e.g., `BTCUSDT` → `symbol: BTC`, `asset: USDT`).
* **`price_precision`**: Number of decimal places for price (e.g., `1` → $100.1). Determined by the hedger based on market conventions.
* **`quantity_precision`**: Number of decimal places for quantity (e.g., `3` → 0.001 BTC). Derived from `min_acceptable_portion_lf` (e.g., `0.003` implies 3 decimals).
* **`lot_size`**: Always `0` (no minimum lot size enforced).

## Contract Symbols Implementation (Rasa)

### Router

```python
@common_router.get(
    '/contract-symbols', responses={status.HTTP_200_OK: {"model": SymbolsContractResponseSchema}},
    response_model=SymbolsContractResponseSchema
)
async def get_contract_symbols():
    symbols = await get_contract_all_symbols()
    return dict(count=len(symbols), symbols=symbols)
```

### Controller Logic

**Caching**

Uses `@redis_ttl_cache(ttl=300)`, so the *entire* symbol list is stored in Redis for 5 minutes. Subsequent calls within that window skip on-chain and DB work.

**On-Chain Fetching**

```python
symbols = await symmio_contract.functions
                         .getSymbols(start=0, size=500)
                         .call_async()
```

Calls the SYMMIO “diamond” contract’s `getSymbols` method to retrieve up to 500 entries and returns a list of tuples matching the on-chain `Symbol` struct fields.

{% hint style="info" %}
*A size of 500 may be insufficient for future symbols added to the SYMM contracts.*
{% endhint %}

**Filters & Whitelists**

```python
if not symbol[2]:                     # on-chain `isValid == false`
    continue
if symbol[1].upper() not in whitelist:
    continue
```

Drops any symbol with `isValid == false` or if the symbol is not in your hedger’s own whitelist (normalized to uppercase).

**Price Precision and Quantity Precision**

```python
price_precision    = Symbol.get_price_precision(symbol[1])
quantity_precision = Symbol.get_quantity_precision(symbol[1])
```

These methods query the **Postgres** `Symbol` table for the precision values you configured.

**USDT-Pair Restriction**

```python
if symbol[1].endswith("USDT"):
    … include in result …
```

Only includes symbols quoted in USDT.

### **Composing the Response Object**

**The response dict consists of:**

* **On-chain fields**: `symbolId`, `name`, `isValid`, `minAcceptableQuoteValue`, etc.
* **DB/config fields**: `price_precision`, `quantity_precision`, plus hedger parameters (max leverage, fees, notional caps) via `HedgerParameters`.
* Returns a list of dicts, one per symbol.&#x20;

**Full Controller Snippet**

```python
@redis_ttl_cache(ttl=300)
async def get_contract_all_symbols():
    symbols: List[Tuple] = await symmio_contract.functions.getSymbols(start=0, size=500).call_async()
    symbols_list = []
    symbol_whitelist = get_symbol_whitelist()
    for symbol in symbols:
        if not symbol[2]:
            continue
        if symbol[1].upper() not in symbol_whitelist:
            continue
        try:
            price_precision = Symbol.get_price_precision(symbol[1])
            quantity_precision = Symbol.get_quantity_precision(symbol[1])
        except PreConditionException:
            price_precision = None
            quantity_precision = None
        if 'USDT' == symbol[1][-4:]:
            symbols_list.append({
                'name': symbol[1].upper(), 'symbol': symbol[1][:-4], 'asset': 'USDT', 'symbol_id': symbol[0],
                'price_precision': price_precision,
                'quantity_precision': quantity_precision,
                'is_valid': symbol[2],
                'min_acceptable_quote_value': (Decimal(symbol[3]) / Scale) * MinQuoteValueMultiplier,
                'min_acceptable_portion_lf': Decimal(symbol[4]) / Scale,
                'trading_fee': Decimal(symbol[5]) / Scale,
                'max_leverage': int(
                    HedgerParameters.get_by_key(f"quote_boundaries::dynamic::{symbol[1]}::leverage::max")),
                'max_notional_value': HedgerParameters.get_by_key_convert_decimal(
                    f'quote_boundaries::{symbol[1]}::notionalValue::max'),
                'rfq_allowed': symbol[1].upper() not in OpenPositionSymbolBlackList,
                'max_funding_rate': HedgerParameters.get_by_key_convert_decimal('quote::max-funding-rate'),
                'hedger_fee_open': HedgerParameters.get_by_key(
                    f'{HedgerParameterPrefixes.open_order_gap}{symbol[1].upper()}'),
                'hedger_fee_close': HedgerParameters.get_by_key(
                    f'{HedgerParameterPrefixes.close_order_gap}{symbol[1].upper()}')
            })
    return symbols_list
```

For reference, the structure of an on-chain `Symbol` is here:

```solidity
struct Symbol {
    uint256 symbolId;                   // tuple index [0]
    string  name;                       // index [1]
    bool    isValid;                    // index [2]
    uint256 minAcceptableQuoteValue;    // index [3]
    uint256 minAcceptablePortionLF;     // index [4]
    uint256 tradingFee;                 // index [5]
    uint256 maxLeverage;                // index [6]
    uint256 fundingRateEpochDuration;   // index [7]
    uint256 fundingRateWindowTime;      // index [8]
}
```


# GET Open Interest

### /open-interest

```json
/open-interest
```

**Example Query:**

```
https://base-hedger82.rasa.capital/open-interest
```

**Example Response:**

```json
{
  "total_cap": "2438560.667955634347925220",
  "used": "111969.991183710650000000"
}
```

#### **Data Source**

All values are configured and managed exclusively by the hedger. This data reflects the hedger’s internal risk management settings and is **not** derived from on-chain contracts or user positions.

#### **Response Fields**

* **`total_cap`**: The maximum total open interest across all assets (in collateral, e.g., USDT).&#x20;
* **`used`**: The portion of the total capacity currently utilized by open positions across all assets.

## **Open Interest Implementation (Rasa)**

***

### Router

```python
@common_router.get(
    '/open-interest',
    responses={status.HTTP_200_OK: {"model": OpenInterestResponseSchema}},
    response_model=OpenInterestResponseSchema,
    dependencies=[
        Depends(CustomRateLimiter(times=1, seconds=1)),
        Depends(CustomRateLimiter(times=40, minutes=1)),
        Depends(CustomRateLimiter(times=1500, hours=1)),
    ]
)
@with_context_db_session
async def get_open_interest():
    # 1. Compute hedger’s capacity and usage
    total_cap, used = await OpenCapEngine.get_total_cap_and_used_amount_async(from_stats=True)

    # 2. Emit metrics for monitoring
    apm.label(total_cap=total_cap, used=used)

    # 3. Return JSON response
    return {
        "total_cap": total_cap,
        "used": used
    }
```

* **Path**: `GET /open-interest`
* **Rate‐limits**: 1 req/sec, 40 req/min, 1 500 req/hr
* **DB session**: injected via `@with_context_db_session` (in case the capacity engine reads from SQL)

***

### Logic

1. **Call Capacity Engine**

   ```python
   total_cap, used = await OpenCapEngine.get_total_cap_and_used_amount_async(from_stats=True)
   ```

   * Invokes your hedger’s internal risk‐management component to compute:
     * **total\_cap**: how much notional collateral the hedger is willing to allow in aggregate.
     * **used**: how much of that allowance is currently locked up by open positions.
2. **Instrumentation**

   ```python
   apm.label(total_cap=total_cap, used=used)
   ```

   * Sends metrics (via your APM/tracing tool) for observability.
3. **Return Payload**

   ```python
   return {"total_cap": total_cap, "used": used}
   ```

   * FastAPI serializes this to JSON matching `OpenInterestResponseSchema`.

   ```python
   class OpenInterestResponseSchema(BaseModel):
       total_cap: Decimal
       used: Decimal
   ```

***

#### Steps to Implement Your Own Version

1. **Define the Response Schema**

   ```python
   class OpenInterestResponseSchema(BaseModel):
       total_cap: Decimal
       used: Decimal
   ```
2. **Build or Wire Up Your Capacity Engine**
   * Implement `OpenCapEngine.get_total_cap_and_used_amount_async(from_stats: bool) → Tuple[Decimal, Decimal]`.
   * Internally, read from your own data sources:
     * **Configuration table** (max capacities per asset or global).
     * **Positions table** (sum up open positions’ collateral needs).
   * Return `(total_cap, used)`.
3. **Add the FastAPI Route**
   * In your `common_router`, add the `@common_router.get("/open-interest")` handler shown above.
   * Import `OpenInterestResponseSchema` and `OpenCapEngine`.
   * Include your rate‐limiting dependencies and `@with_context_db_session` if your engine needs DB access.
4. **Instrumentation (Optional)**
   * If you have an APM or metrics library, record these values on each call.

***


# GET Notional Cap

### /notional\_cap

```json
/notional_cap/{symbol_id}
```

**Example Query:**

```
https://base-hedger82.rasa.capital/notional_cap/1
```

**Example Response:**

```json
{
  "total_cap": "2344336.83177192369792522000000000",
  "used": "17746.15500000000000000000000000"
}
```

#### **Data Source**

Values are configured and managed exclusively by the hedger. These caps are set per trading pair and reflect the hedger’s risk management strategy for individual assets.

## **Notional Cap Implementation (Rasa)**

### Router

```python
@common_router.get(
    '/notional_cap/{symbol_id}',
    responses={status.HTTP_200_OK: {"model": OpenInterestResponseSchema}},
    response_model=OpenInterestResponseSchema,
    dependencies=[
        Depends(CustomRateLimiter(times=1, seconds=1)),
        Depends(CustomRateLimiter(times=40, minutes=1)),
        Depends(CustomRateLimiter(times=1500, hours=1)),
    ]
)
@with_context_db_session
async def get_notional_cap(symbol_id: int):
    symbol_id_validator(symbol_id)
    total_cap, used = await OpenCapEngine.get_cap_and_used_amount_async(symbol_id, from_stats=True)
    apm.label(total_cap=total_cap, used=used)
    return {"total_cap": total_cap, "used": used}

```

### Logic

* Ensures the provided `symbol_id` exists and is well-formed; raises a 400/422 if not.
* Queries the hedger’s capacity engine for the per-symbol maximum notional cap and current usage.
* Emits metrics for monitoring and alerting.
* FastAPI serializes this to JSON matching `OpenInterestResponseSchema`:

```python
class OpenInterestResponseSchema(BaseModel):
    total_cap: Decimal
    used: Decimal
```

#### Steps to Implement Your Own Version

**Define the Response Schema**

```python
class OpenInterestResponseSchema(BaseModel):
    total_cap: Decimal
    used: Decimal
```

**Implement Symbol Validation**

* Write `symbol_id_validator(symbol_id: int)` that checks against your `Symbol` table or whitelist.

**Build The Capacity Engine**

* In `OpenCapEngine`, implement `get_cap_and_used_amount_async(symbol_id, from_stats=True)` to return `(Decimal total_cap, Decimal used)`, using:
  * **Per-symbol config** table for maximum caps.
  * **Positions** table to sum currently open notional on that symbol.

**Add the FastAPI Route**

* In your `common_router`, add the `GET /notional_cap/{symbol_id}` handler as shown, importing the schema, validator, and engine.
* Attach the same rate-limiting and `@with_context_db_session` decorators.


# GET Price Range

### /price-range

```json
/price-range/{symbol}
```

**Example Query:**

```
https://base-hedger82.rasa.capital/price-range/BTCUSDT
```

**Example Response:**

```json
{
  "min_price": "1113.60",
  "max_price": "754960.6666666666666666666668176588"
}
```

#### **Data Source**

The `min_price` and `max_price` are configured by the hedger but **must align with the price range** of the external exchange where the hedger is actively hedging (e.g., Binance, Bybit).

**`min_price`**: The minimum acceptable order price for the symbol (in quote currency, e.g., USDT). Derived from the external exchange’s current price floor or liquidity constraints.

* Example: `"1113.60"` → Orders below this price are rejected.

**`max_price`**: The maximum acceptable order price for the symbol. Reflects the hedging platform’s upper price limit.

## Price Range Implementation (Rasa)

### Router

```python
@common_router.get(
    '/price-range/{symbol}',
    responses={status.HTTP_200_OK: {"model": SymbolPriceRangeInputSchema}},
    response_model=SymbolPriceRangeInputSchema
)
async def get_symbol_price_range(symbol: symbol_enum):
    # 1. Compute safe order-price bounds from the exchange
    _, _, min_price, max_price = BinanceConditionChecks.get_price_bounds(symbol, from_stats=True)

    # 2. Return JSON matching SymbolPriceRangeInputSchema
    return {
        "min_price": min_price,
        "max_price": max_price
    }

```

* Reads the exchange’s order-book or recent trades (via the Binance API or a cached copy).
* Computes a safe **min\_price** (floor) and **max\_price** (ceiling) for new orders.

#### Steps to Implement Your Own Version

**Define the Response Schema**

```python
class SymbolPriceRangeInputSchema(BaseModel):
    min_price: Decimal
    max_price: Decimal
```

**Build Your Condition-Checks Utility**

* Implement `BinanceConditionChecks.get_price_bounds(symbol, from_stats)` to return a tuple `(_, _, min_price, max_price)`.

**Add the FastAPI Route**

* In `common_router`, register the `GET /price-range/{symbol}` endpoint as shown above.
* Import `symbol_enum` for the path parameter (a class containing all Symbol names), and `SymbolPriceRangeInputSchema` for the response.


# GET Error Codes

### /error\_codes

```json
/error_codes/{error_code}
```

**Paths**

* `GET /error_codes` — returns **all** exposed error codes.
* `GET /error_codes/{error_code}` — returns the **message** for one specific code, or 404 if not found/blacklisted.

**Example Query:**

```
https://base-hedger82.rasa.capital/error_codes
```

See this [section ](/liquidity-provider-documentation/building-a-solver-on-symmio/solver-error-codes)for more information about error codes.

## Error Codes Implementation (Rasa)

```python
@common_router.get(
    '/error_codes',
    responses={status.HTTP_200_OK: {"model": ErrorCodeResponseSchema}},
    response_model=ErrorCodeResponseSchema
)
async def get_all_error_codes():
    return {
        code: msg
        for code, msg in ErrorCodes.items()
        if code not in ErrorCodeBlackList
    }


@common_router.get(
    '/error_codes/{error_code}',
    responses={status.HTTP_200_OK: {"model": ErrorCodeResponseSchema}},
    response_model=ErrorCodeResponseSchema
)
async def get_error_message(error_code: int):
    msg = ErrorCodes.get(error_code)
    if msg is None or error_code in ErrorCodeBlackList:
        raise ErrorCodeResponse(
            status_code=404,
            error=ErrorInfoContainer.error_code_not_found
        )
    return {error_code: msg}

```

### Logic

**Imports**

```python
from share.error_codes import ErrorCodes       # a dict[int, str]
from db_tools.triggers.trigger_utils import ErrorCodeBlackList  # a set[int]
```

* `ErrorCodes` is your master map of all possible error‐code → message entries.
* `ErrorCodeBlackList` lists codes you don’t want to expose via the public API.

**GET /error\_codes**

* Filters out any blacklisted codes, then returns the rest as a plain dict.

**GET /error\_codes/{error\_code}**

* Looks up the message via `ErrorCodes.get(error_code)`.
* If missing or blacklisted, raises an `ErrorCodeResponse(404)`.
* Otherwise returns `{ error_code: message }`.

#### Steps to Implement Your Own Version

**Define the Error Codes**

```python
ErrorCodes = {
  1001: "Insufficient balance",
  1002: "Order timeout",
  # …
}
ErrorCodeBlackList = {9999, 8888}  # codes reserved for internal use
```

**Create the Response Schema**

```python
class ErrorCodeResponseSchema(RootModel[Dict[int, str]]):
    pass
```

**Define an Exception (example)**

```python
class ErrorCodeResponse(HTTPException):
    def __init__(self, status_code: int, error: ErrorInfoContainer):
        super().__init__(status_code=status_code, detail=error.to_dict())
```

**Add FastAPI Routes**

* In your `common_router`, register the two `@common_router.get` endpoints exactly as above.
* Import `ErrorCodes`, `ErrorCodeBlackList`, `ErrorCodeResponse`, and `ErrorCodeResponseSchema`.


# GET Get Locked Params

### /get\_locked\_params

```json
get_locked_params/{symbol}?leverage={leverage}
```

**Example Query:**

```
https://base-hedger82.rasa.capital/get_locked_params/BTCUSDT?leverage=9
```

**Example Response:**

```json
{"cva":"6.00","partyAmm":"91.00","lf":"3.00","leverage":"9.0","partyBmm":"0"}
```

These parameters are solver-defined and based on a `cn` value. You can read more about this calculation [here](/liquidity-provider-documentation/solver-settings/maintenance-margin-cva-calculations).

{% hint style="info" %}
**More information about calculating the maintenance margin can be found** [**here**](/liquidity-provider-documentation/solver-settings/maintenance-margin-cva-calculations)**.**
{% endhint %}

## Get Locked Params Rasa Implementation

### Router

```python
@common_router.get(
    '/get_locked_params/{symbol}',
    responses={status.HTTP_200_OK: {"model": LockedParamsResponseSchema}},
    response_model=LockedParamsResponseSchema
)
@with_context_db_session
async def get_locked_params(
    symbol: symbol_enum,
    leverage: float
):
    dynamic_lock_params = get_dynamic_lock_params(symbol, leverage)
    if dynamic_lock_params is False:
        raise ErrorCodeResponse(
            status_code=400,
            error=ErrorInfoContainer.invalid_leverage
        )

    # Cast all values to strings for JSON consistency
    dynamic_lock_params = {k: str(v) for k, v in dynamic_lock_params.items()}

    # Echo back the requested leverage
    dynamic_lock_params['leverage'] = str(leverage)

    # Append the hedger’s party-B margin-multiplier
    dynamic_lock_params['partyBmm'] = str(
        HedgerParameters.get_by_key_convert_decimal(
            f'quote_boundaries::{symbol}::hedger::mm::percentage'
        )
    )

    return dynamic_lock_params

```

### Logic

**Compute Dynamic Parameters**

```python
dynamic_lock_params = get_dynamic_lock_params(symbol, leverage)
```

Calls the risk-model function, which returns a dict of:

* `cva` (collateral valuation adjustment)
* `partyAmm` (party A margin multiplier)
* `lf` (liquidity fee portion)
* Returns `False` if the requested leverage is outside the valid range.

**Validate Leverage**

```python
if dynamic_lock_params is False:
    raise ErrorCodeResponse(400, ErrorInfoContainer.invalid_leverage)
```

* If the risk-model rejects the leverage, returns a 400 with a structured error.

**Normalize to Strings**

```python
dynamic_lock_params = {k: str(v) for k, v in dynamic_lock_params.items()}
```

* Ensures all numeric values serialize cleanly as JSON strings.

**Augment with Leverage & PartyBmm**

```python
dynamic_lock_params['leverage'] = str(leverage)
dynamic_lock_params['partyBmm'] = str(
    HedgerParameters.get_by_key_convert_decimal(
        f'quote_boundaries::{symbol}::hedger::mm::percentage'
    )
)
```

* Echoes back the input `leverage`.
* Reads your hedger’s configured “party B margin multiplier” from the database/config.

**Return**

* Yields a JSON object:

  ```json
  {
    "cva": "6.00",
    "partyAmm": "91.00",
    "lf": "3.00",
    "leverage": "9.0",
    "partyBmm": "0"
  }
  ```

### Steps to Implement Your Own Version

**Define the Response Schema**

```python
class LockedParamsResponseSchema(BaseModel):
    cva: str
    partyAmm: str
    lf: str
    leverage: str
    partyBmm: str
```

**Write a Risk-Model Function**

* Implement `get_dynamic_lock_params(symbol: str, leverage: float) → dict | False`.
* Should return a dict with `cva`, `partyAmm`, `lf` if valid, or `False` for invalid leverage.
* Logic to base this on can be found [further up](#get_locked_params).

**Configure HedgerParameters**

* Ensure your `HedgerParameters` table or config contains the key\
  `quote_boundaries::{symbol}::hedger::mm::percentage` if you're following this example.

{% hint style="info" %}
Most hedgers will return 0 for the `partyB` parameter as initially the partyB is not required to add a maintenance margin, however they will require allocating a certain amount of maintenance margin to ensure they doesn't face instant liquidation.
{% endhint %}


# GET Get Funding Info

### /get\_funding\_info

```json
/get_funding_info
```

**Example Query:**

```
https://base-hedger82.rasa.capital/get_funding_info
```

**Example Response:**

```json
  "BTCUSDT": {
    "next_funding_time": 1744070400000,
    "next_funding_rate_short": "0.000040455",
    "next_funding_rate_long": "-0.000053940",
    "funding_rate_epoch_duration": 14400
  },
  "FILUSDT": {
    "next_funding_time": 1744070400000,
    "next_funding_rate_short": "0.000045081",
    "next_funding_rate_long": "-0.000060108",
    "funding_rate_epoch_duration": 14400
  },

```

## Get Funding Info Rasa Implementation

### Router

```python
@common_router.get(
    '/get_funding_info',
    response_model=Dict[str, FundingInfoResponseSchema],
    dependencies=[
        Depends(CustomRateLimiter(times=1, seconds=1)),
        Depends(CustomRateLimiter(times=40, minutes=1)),
        Depends(CustomRateLimiter(times=1500, hours=1)),
    ]
)
async def get_funding_info(symbols: List[symbol_enum] = Query(None)):
    # symbols: optional list of tickers; defaults to BaseSymbols if None
    return get_funding_rates(symbols or BaseSymbols)
```

**Output**:

* Dict mapping ticker → `FundingInfoResponseSchema`.

```python
class FundingInfoResponseSchema(BaseModel):
    next_funding_time: int
    next_funding_rate_short: Decimal
    next_funding_rate_long: Decimal
```

### Data Sources

1. **Symbol table (DB)**
   * `Symbol.funding_rate_epoch_duration`
   * `Symbol.funding_rate_window_time`
2. **External exchange (Binance)**
   * `binance_client.futures_mark_price()` → `lastFundingRate`, `nextFundingTime`
3. **Hedger coefficients** (from `settings.py`)
   * `FundingRateHedgerToUserCoefficient`
   * `FundingRateUserToHedgerCoefficient`
4. **Positions table (DB)**
   * Used for filtering and in expressions (`Positions.position_type`, `Positions.opened_price`, etc.)

### Logic&#x20;

In short, the logic can be summarized like this:

```python
GET /get_funding_info
    ↓
get_funding_info(symbols) #Entry point 
    ↓
get_funding_rates(symbols)    
    ├─→ FundingRate.get_symbol_funding_details()
    ├    # read Symbol.is_valid from DB
    ├    # call binance_client.futures_mark_price()
    ├    # return List[SymbolFundingDetail]
    └─→ FundingRate.get_funding_details(..., to_apply=False)
    ├    # 1) build SQL CTE from symbol_funding_details
    ├    # 2) apply hedger’s long/short coefficients
    ├    # 3) compute contract_rate expression
    ├    # 4) group by Positions.party_a_address
    ├    # 5) optional $-amount filtering if to_apply=True
    ├    # 6) session.all_(stmt) → [FundingDetail(...)]
    ↓
map into Dict[str, FundingInfoResponseSchema]
    ↓
return JSON
```

**Fetch Symbol & Market Details**

```python
@staticmethod
def get_symbol_funding_details() -> List[SymbolFundingDetail]:
    # 1) Fetch mark‐price & funding‐time info for all symbols from Binance
    binance_markets: Dict[str, BinanceMarketDetail] = binance_client.futures_mark_price()

    # 2) Record current time (in seconds) to compare against each symbol’s funding window
    start_time = get_now_epoch()

    result: List[SymbolFundingDetail] = []
    # 3) Load every valid symbol row from your local Symbol table
    for symbol in session.scalars_all(
        Symbol.select().where(Symbol.is_valid.is_(true()))
    ):  

        # 4) Read the next funding timestamp (ms→s) from Binance’s data
        binance_next_funding_time = binance_markets[symbol.title].nextFundingTime // 1000
        
        # 5) Only keep symbols whose next funding falls within the on-chain funding window
        if binance_next_funding_time < start_time + symbol.funding_rate_window_time:
            symbol_funding_detail = SymbolFundingDetail(
                symbol_id                = symbol.symbol_id,
                binance_mark_price       = binance_markets[symbol.title].markPrice,
                binance_last_funding_rate= binance_markets[symbol.title].lastFundingRate,
                binance_next_funding_time= binance_next_funding_time,
                funding_rate_window_time = symbol.funding_rate_window_time,
            )

            # 7) Sanity check: confirm on-chain epoch aligns with Binance’s tick
            #    If it doesn’t, emit an alert and skip this symbol
            if binance_next_funding_time % symbol.funding_rate_epoch_duration != 0:
                send_instant_error_message(
                    title='Funding time of contract not compatible with Binance.',
                    amend=asdict(symbol_funding_detail)
                )
                continue

            # 8) Add to the list of symbols we’ll compute funding for
            result.append(symbol_funding_detail)

    # 9) Return all valid SymbolFundingDetail entries
    return result

```

**Coefficient Application Logic**

```python
@staticmethod
def get_funding_details(
    symbol_funding_details: List[SymbolFundingDetail],
    to_apply: bool = True
) -> Optional[List[FundingDetail]]:
    # 1) Build an in-SQL CTE of symbol timing & rates
    symbol_details_expr = values(
        column('symbol_id', Integer),
        column('price',     Numeric(**numeric_args)),
        column('funding_rate', Numeric(**numeric_args)),
        column('funding_window_open_time', BigInteger),
        column('binance_next_funding_time', BigInteger),
        name='funding_details'
    ).data([
        detail.get_data_for_cte()
        for detail in symbol_funding_details
    ])

    # 2) Apply hedger coefficients to raw funding_rate
    funding_rate_expr = symbol_details_expr.c.funding_rate * case(
        # SHORT & positive → hedger pays user (negate HedgerToUserCoef)
        (and_(Positions.position_type == PositionType.SHORT,
              symbol_details_expr.c.funding_rate >= 0),
         -FundingRateHedgerToUserCoefficient),

        # SHORT & negative → user pays hedger (negate UserToHedgerCoef)
        (and_(Positions.position_type == PositionType.SHORT,
              symbol_details_expr.c.funding_rate < 0),
         -FundingRateUserToHedgerCoefficient),

        # LONG & positive → user pays hedger
        (and_(Positions.position_type == PositionType.LONG,
              symbol_details_expr.c.funding_rate >= 0),
         FundingRateUserToHedgerCoefficient),

        # LONG & negative → hedger pays user
        (and_(Positions.position_type == PositionType.LONG,
              symbol_details_expr.c.funding_rate < 0),
         FundingRateHedgerToUserCoefficient)
    )

    # 3) Embed into full “contract rate” formula
    sign = case((Positions.position_type == PositionType.SHORT, -1), else_=1)
    contract_rate = func.round(
        (
            (
                (funding_rate_expr * symbol_details_expr.c.price)
                / (Positions.funding_rate_diff * Positions.opened_price)
                * sign + 1
            ) * Positions.funding_rate_diff - 1
        ) * sign,
        NumberFloatingPoint
    )

    # 4) Build query: group by counterparty (party A)
    stmt = Positions.select(
        Positions.party_a_address,
        func.json_agg(Positions.quote_id),
        func.json_agg(contract_rate)
    ).outerjoin(
        AppliedFundingRate, Positions.quote_id == AppliedFundingRate.quote_id
    ).join(Symbol).select_from(symbol_details_expr).join(
        symbol_details_expr,
        Symbol.symbol_id == symbol_details_expr.c.symbol_id
    ).where(
        AppliedFundingRate.id.is_(None),
        Positions.state.in_([PositionState.OPEN, PositionState.CLOSE_PENDING]),
        Positions.open_time <= symbol_details_expr.c.binance_next_funding_time,
        or_(
            Positions.last_funding_time < symbol_details_expr.c.funding_window_open_time,
            Positions.last_funding_time.is_(None)
        )
    ).group_by(Positions.party_a_address)

    # 5) If “to_apply”, also calculate $-amount and filter tiny ones
    if to_apply:
        funding_amount = (
            (Positions.quantity - Positions.filled_closed_amount)
            * Positions.opened_price * contract_rate
        )
        stmt = stmt.add_columns(
            func.json_agg(
                case(
                    (contract_rate > Positions.max_funding_rate,
                     Positions.max_funding_rate),
                    else_=contract_rate
                )
            ),
            func.sum(funding_amount).label('funding_amount')
        ).where(
            func.abs(funding_amount) > GasWorthyFundingThresholdPerPosition
        ).order_by(desc('funding_amount'))

    # 6) Execute and map to dataclass
    rows = session.all_(stmt)
    return [FundingDetail(*row) for row in rows]

```


# POST Position State

### /position-state

```json
position-state/{start}/{size}
```

**Example Query:**

```
https://base-hedger82.rasa.capital/position-state/{start}/{size}
```

### Overview

This endpoint gives insights into historical lifecycle updates for positions based on a quote ID or counterparty address. Results are sourced from the `Notifications`, `Positions`, and `Symbol` tables, then mapped into the response schema.

***

### Endpoint Specification

**URL**: `https://base-hedger82.rasa.capital/position-state/{start}/{size}`\
**Method**: `POST`

#### Headers

* `Content-Type: application/json`
* Optional: `App-Name:`(client identifier)

**cURL Example (Limit Open — Reserved but not yet Opened)**

```bash
curl -X POST "https://base-hedger82.rasa.capital/position-state/0/10" \
     -H "Content-Type: application/json" \
     -H "App-Name: Cloverfield" \
     -d '{"quote_id": "131388"}'
```

**Response** (HTTP 200):

```json
{
    "count": 1,
    "position_state": [
        {
            "id": "7d0c10a4-1d7e-44b9-8712-77c863bfab55",
            "create_time": 1745970777,
            "modify_time": 1745970777,
            "quote_id": 131388,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "0",
            "avg_price_open": "0",
            "avg_price_close": "0",
            "last_seen_action": "SendQuote",
            "action_status": "seen",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "alert"
        }
    ]
}
```

#### Path Parameters

| Name  | Type    | Description                           |
| ----- | ------- | ------------------------------------- |
| start | integer | 0-based pagination offset             |
| size  | integer | Number of records to return (max 100) |

#### Body Parameters (JSON)

Provide **at least `quote_id` or `address`** of the following to filter results; other fields are optional:

| Field             | Type      | Description                                                                             |
| ----------------- | --------- | --------------------------------------------------------------------------------------- |
| `quote_id`        | string    | Exact quote ID (string). One of `quote_id` or `address` is required.                    |
| `address`         | string    | Counterparty address (Ethereum hex string). One of `quote_id` or `address` is required. |
| `symbols`         | string\[] | Optional filter by trading symbols (e.g., `["ETHUSD"]`).                                |
| `states`          | string\[] | Optional filter by position state enums (e.g., `["alert"]`).                            |
| `create_time_gte` | integer   | Optional: include records with `Notifications.create_time >=` epoch seconds UTC.        |
| `modify_time_gte` | integer   | Optional: include records with `Notifications.modify_time >=` epoch seconds UTC.        |

***

### Request & Response Schemas

**Define Schemas**

Create `PositionsStateRequestSchema` and `PositionStateResponseSchema` + `PositionsStateOutputSchema` in the schema file.

```python
class PositionStateResponseSchema(BaseModel):
    id: UUID
    create_time: int
    modify_time: int
    quote_id: int
    temp_quote_id: Optional[int] = None
    counterparty_address: str
    filled_amount_open: Decimal = 0
    filled_amount_close: Decimal = 0
    avg_price_open: Decimal = 0
    avg_price_close: Decimal = 0
    last_seen_action: str
    action_status: str
    failure_type: Optional[str] = None
    error_code: Optional[int] = None
    order_type: int
    state_type: PositionStateType

class PositionsStateOutputSchema(BaseModel):
    count: int
    position_state: List[PositionStateResponseSchema]
```

***

### Data Sources & Field Mapping

<table><thead><tr><th>Response Field</th><th width="357.800048828125">Source Table.Column(s)</th><th>Notes</th></tr></thead><tbody><tr><td><code>id</code></td><td><code>Notifications.id</code></td><td>Primary key (UUID) -> in some solver implementations this is also the quote ID.</td></tr><tr><td><code>create_time</code></td><td><code>Notifications.create_time</code></td><td>Epoch seconds UTC</td></tr><tr><td><code>modify_time</code></td><td><code>Notifications.modify_time</code></td><td>Epoch seconds UTC</td></tr><tr><td><code>quote_id</code></td><td><code>Notifications.quote_id</code></td><td><code>quote_id</code>.</td></tr><tr><td><code>temp_quote_id</code></td><td><code>Notifications.temp_quote_id</code></td><td>Used for instant trades or <code>null</code>.</td></tr><tr><td><code>counterparty_address</code></td><td><code>Notifications.counterparty_address</code></td><td>partyA address</td></tr><tr><td><code>filled_amount_open</code></td><td><code>Notifications.filled_amount_open</code></td><td>Recorded open fill amount</td></tr><tr><td><code>avg_price_open</code></td><td><code>Notifications.avg_price_open</code></td><td>Recorded open fill price</td></tr><tr><td><code>filled_amount_close</code></td><td><code>Notifications.filled_amount_close</code></td><td>Recorded close fill amount</td></tr><tr><td><code>avg_price_close</code></td><td><code>Notifications.avg_price_close</code></td><td>Recorded close fill price</td></tr><tr><td><code>last_seen_action</code></td><td><code>Notifications.last_seen_action</code></td><td>e.g., <code>SendQuote</code>, <code>FillLimitOrderOpen</code></td></tr><tr><td><code>action_status</code></td><td><code>Notifications.action_status</code></td><td><code>success</code>, <code>seen</code>, etc.</td></tr><tr><td><code>failure_type</code></td><td><code>Notifications.failure_type</code></td><td>If action failed</td></tr><tr><td><code>error_code</code></td><td><code>Notifications.error_code</code></td><td>Numeric code</td></tr><tr><td><code>order_type</code></td><td><code>Notifications.order_type</code></td><td>Limit = 0, Market = 1</td></tr><tr><td><code>state_type</code></td><td><code>Notifications.state_type</code></td><td><code>"alert"</code> or <code>"report"</code></td></tr></tbody></table>

***

### Implementation Details (Annotated)

This section explains how each piece fits together.

#### Router Layer (`routers.py`)

```python
@common_router.post(
    '/position-state/{start}/{size}',
    responses={status.HTTP_200_OK: {"model": PositionsStateOutputSchema}},
    response_model=PositionsStateOutputSchema
)
@with_context_db_session
async def search_position_state(
    request: PositionsStateRequestSchema,
    start: NonNegativeInt = 0,
    size: PositiveInt = 100
):
    # Map incoming HTTP payload to internal search DTO
    new_request = NotificationsRequestSchema()
    new_request.counterparty_address = request.address
    new_request.quote_id = request.quote_id
    new_request.symbols = request.symbols
    new_request.states = request.states
    new_request.timestamp_gte = request.modify_time_gte

    # Call service layer to fetch and map results
    count, data = search_notification_by_counterparty(
        new_request, start, size, map_to_positions_state=True
    )

    # Return raw dict form; FastAPI serializes to schema
    return dict(count=count, position_state=data)
```

### Controller Layer (`controllers.py`)

```python
# Located in controllers.py
# Handles fetching and mapping notifications to position state

def search_notification_by_counterparty(
    request: NotificationsRequestSchema,
    start: int,
    size: int,
    map_to_positions_state: bool = False
):
    # a. Sanitize page size
    size = min(size, 100)

    # b. Handle relative timestamps (e.g., -3600 for last hour)
    if request.timestamp_gte and request.timestamp_gte < 0:
        request.timestamp_gte = get_now_epoch() + request.timestamp_gte

    # c. Build base SQLAlchemy query with necessary joins
    stmt = (
        Notifications.select()
        .outerjoin(
            Positions,
            or_(
                Positions.quote_id == Notifications.quote_id,
                Positions.temp_quote_id == Notifications.temp_quote_id
            )
        )
        .outerjoin(
            Symbol, Positions.symbol_id == Symbol.symbol_id
        )
        .where(Notifications.should_send == true())
    )

    # d. Apply filters based on request fields
    if request.quote_id:
        if is_quote_id_temp(request.quote_id):
            stmt = stmt.where(Notifications.temp_quote_id == request.quote_id)
        else:
            stmt = stmt.where(Notifications.quote_id == request.quote_id)
    if request.counterparty_address:
        stmt = stmt.where(
            Notifications.counterparty_address == request.counterparty_address
        )
    if request.states:
        stmt = stmt.where(Positions.state.in_(request.states))
    if request.symbols:
        stmt = stmt.where(Symbol.title.in_(request.symbols))
    if request.timestamp_gte:
        stmt = stmt.where(Notifications.create_time >= request.timestamp_gte)

    # e. Count and paginate
    count = session.count_star(stmt)
    records = (
        session.scalars_all(
            stmt.order_by(desc(Notifications.create_time))
                .offset(start)
                .limit(size)
        )
    )

    # f. Map to DTOs
    result = []
    for row in records:
        data = row.to_dict()
        if map_to_positions_state:
            result.append(map_notification_to_positions_state(data))
        else:
            result.append(prepare_notification_to_send(data))
    return count, result
```

***

### Example (Full Open Position -> Close Position Flow)

**cURL Query**

```bash
curl -X POST "https://base-hedger82.rasa.capital/position-state/0/10" \
     -H "Content-Type: application/json" \
     -H "App-Name: Cloverfield" \
     -d '{"quote_id": "131391"}'
```

**fResponse** (HTTP 200):

```json
{
    "count": 6,
    "position_state": [
        {
            "id": "000be762-2592-43b0-937f-32a75fa4587a", //last notification
            "create_time": 1745976098,
            "modify_time": 1745976098,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "0",
            "avg_price_open": "0",
            "avg_price_close": "0",
            "last_seen_action": "FillLimitOrderClose",
            "action_status": "success",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "alert"
        },
        {
            "id": "4763fe3d-e3ed-40f9-9821-2b501342579b",
            "create_time": 1745976092,
            "modify_time": 1745976092,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "6.70000000",
            "avg_price_open": "0",
            "avg_price_close": "2.2345",
            "last_seen_action": "RequestToClosePosition",
            "action_status": "success",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "report"
        },
        {
            "id": "c99b51b8-a18e-4271-af05-3ed37f3805b9",
            "create_time": 1745976091,
            "modify_time": 1745976091,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "0",
            "avg_price_open": "0",
            "avg_price_close": "0",
            "last_seen_action": "RequestToClosePosition",
            "action_status": "seen",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "alert"
        },
        {
            "id": "8ff29bbd-6ca6-42cd-973f-20b6f88bfdea",
            "create_time": 1745976010,
            "modify_time": 1745976010,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "0",
            "avg_price_open": "0",
            "avg_price_close": "0",
            "last_seen_action": "FillLimitOrderOpen",
            "action_status": "success",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "alert"
        },
        {
            "id": "98620b7c-c752-42ed-8611-34bde40a4b28",
            "create_time": 1745976004,
            "modify_time": 1745976004,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "6.70000000",
            "filled_amount_close": "0",
            "avg_price_open": "2.2367",
            "avg_price_close": "0",
            "last_seen_action": "SendQuote",
            "action_status": "success",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "report"
        },
        {
            "id": "2f383602-054e-46a2-96e6-b39a7c483c49", //first notification
            "create_time": 1745975999,
            "modify_time": 1745975999,
            "quote_id": 131391,
            "temp_quote_id": null,
            "counterparty_address": "0xEb42F3b1aC3b1552138C7D30E9f4e0eF43229542",
            "filled_amount_open": "0",
            "filled_amount_close": "0",
            "avg_price_open": "0",
            "avg_price_close": "0",
            "last_seen_action": "SendQuote",
            "action_status": "seen",
            "failure_type": null,
            "error_code": 0,
            "order_type": 0,
            "state_type": "alert"
        }
    ]
}
```

#### By Address

```bash
curl -X POST "https://base-hedger82.rasa.capital/position-state/0/50" \
     -H "Content-Type: application/json" \
     -H "App-Name: Cloverfield" \
     -d '{
       "address": "0x20F764F49bf8A2c653942dA29FeD1D7A7BAefD20"
     }'

```

**Response** (200 OK):

```json
{
  "count": 383,
  "position_state": [ /* items */ ]
}
```

***

### Full Lifecycle Flow

Below is the end-to-end sequence of events and how they map to the records you see in the `/position-state` response:

1. **RFQ Observed**
   * **On-Chain Event Poller** listens for `SendQuote` events.
   * Creates a **“alert”** notification (`state_type = "alert"`) with `action_status = "seen"` and zeros in all fill fields.
   * At this point the hedger can lock the quote. The quote is seen but not yet filled.
2. **Hedger Confirmation & Opening of the position**
   * The hedger’s risk engine **executes** the quote on-chain.
   * Updates the `Notifications`table with actual `filled_amount_open` and `avg_price_open` values.
   * Emits a **“report”** notification (`state_type = "report"`) with `action_status = "success"` and real fill data.
3. **On-Chain Fill Event**
   * Almost immediately thereafter, the contract itself emits an `OpenPosition`event when the transaction finally lands on-chain.
   * Poller picks it up and creates another **“alert”** (`state_type = "alert"`) with zeros in fill fields until the DB update completes.
   * The same poller picks up that on-chain event and writes a fresh Notification with
     * `last_seen_action = "FillLimitOrderOpen"`
     * `action_status = "success"`




---

[Next Page](/llms-full.txt/1)

