# Sera Documentation - Full Corpus

> Complete Markdown corpus for English (en). Generated from the Sera MkDocs source.

- Canonical site: https://docs.testnet.sera.cx/
- Language: English (en)
- Index: [https://docs.testnet.sera.cx/llms.txt](https://docs.testnet.sera.cx/llms.txt)

## Document Index
- [Introduction](https://docs.testnet.sera.cx/): Welcome to the Sera documentation
- [Introduction to FX Trading](https://docs.testnet.sera.cx/fx-primer/): A beginner's guide to foreign exchange trading with stablecoins
- [Why Sera](https://docs.testnet.sera.cx/why-sera/): How Sera improves on traditional FX trading
- [Non-Custodial Design](https://docs.testnet.sera.cx/non-custodial/): How Sera keeps your funds secure without ever holding them
- [Compliance](https://docs.testnet.sera.cx/compliance/): How Sera uses on-chain analytics to screen wallets and maintain a safe trading environment
- [Quickstart](https://docs.testnet.sera.cx/quickstart/): Get started with Sera in minutes
- [Core Concepts](https://docs.testnet.sera.cx/concepts/): Understanding key concepts in Sera
- [Supported Currency Pairs](https://docs.testnet.sera.cx/currency-pairs/): Currency pairs available for trading on Sera
- [Order Types](https://docs.testnet.sera.cx/order-types/): Overview of the different order types available on Sera
- [Swap Trading](https://docs.testnet.sera.cx/swaps/): How to execute instant swaps on Sera
- [The Vault](https://docs.testnet.sera.cx/vault/): Understanding Sera's non-custodial Vault smart contract
- [Limit Orders](https://docs.testnet.sera.cx/limit-orders/): How to place and manage limit orders on Sera
- [Virtual Liquidity](https://docs.testnet.sera.cx/virtual-liquidity/): Place orders across multiple trading pairs with a single shared budget
- [Order Lifecycle](https://docs.testnet.sera.cx/order-lifecycle/): Complete walkthrough of an order from placement to settlement
- [Fees & Costs](https://docs.testnet.sera.cx/fees/): Understanding fees on Sera
- [API Overview](https://docs.testnet.sera.cx/api-reference/): Sera REST API reference
- [Authentication](https://docs.testnet.sera.cx/api-reference/authentication/): How to authenticate with the Sera API
- [Market Maker Guide](https://docs.testnet.sera.cx/api-reference/market-maker-guide/): Hands-on tutorial — place an order, cancel an order, and automate quoting against your own rate feed
- [System Endpoints](https://docs.testnet.sera.cx/api-reference/endpoints/system/): System and utility API endpoints
- [Order Endpoints](https://docs.testnet.sera.cx/api-reference/endpoints/orders/): API endpoints for limit orders, Virtual Liquidity, and fills
- [Swap Endpoints](https://docs.testnet.sera.cx/api-reference/endpoints/swaps/): API endpoints for one-shot routed swaps
- [Account Endpoints](https://docs.testnet.sera.cx/api-reference/endpoints/account/): API endpoints for balances, deposits, withdrawals, and transfers
- [Contract Overview](https://docs.testnet.sera.cx/contracts/): Sera smart contract architecture
- [Audit Reports](https://docs.testnet.sera.cx/contracts/audits/): Independent security audits of Sera smart contracts
- [Sera.sol](https://docs.testnet.sera.cx/contracts/sera/): Core settlement contract reference
- [SeraBatcher.sol](https://docs.testnet.sera.cx/contracts/batcher/): Batch execution contract reference
- [SeraSOR.sol](https://docs.testnet.sera.cx/contracts/sor/): Smart Order Router contract reference
- [Vault.sol](https://docs.testnet.sera.cx/contracts/vault/): Asset custody contract reference
- [Roadmap](https://docs.testnet.sera.cx/protocol/roadmap/): Sera's path from swap infrastructure to lending and derivatives
- [Cold Start Problem](https://docs.testnet.sera.cx/protocol/cold-start-problem/): Why on-chain FX liquidity is hard to bootstrap and how Sera plans to solve it
- [Swap](https://docs.testnet.sera.cx/protocol/swap/): The current CLOB execution layer and the planned FCICAMM extension
- [Earn](https://docs.testnet.sera.cx/protocol/earn/): The phase where FX balances become useful beyond execution
- [Raise and Receive (Lend)](https://docs.testnet.sera.cx/protocol/raise-and-receive/): The lending layer that makes balances and positions productive
- [Accelerate (Derivatives)](https://docs.testnet.sera.cx/protocol/accelerate/): The phase where Sera builds risk markets on top of spot and lending
- [FAQ](https://docs.testnet.sera.cx/faq/): Frequently asked questions about Sera

---

# Introduction

- Canonical URL: https://docs.testnet.sera.cx/
- Source path: `docs/en/index.md`
- Description: Welcome to the Sera documentation

# Overview

Sera is a **stablecoin FX exchange** — designed to bring institutional-grade foreign exchange trading to stablecoin markets.

!!! warning "Notice"
    Sera is available on Ethereum Mainnet and Sepolia. Features, APIs, and contract addresses are subject to change without prior notice. Documentation may not always reflect the latest state. For real-time updates, join our [Telegram](https://t.me/seraprotocol) or follow us on [X (Twitter)](https://x.com/seraprotocol).

!!! info "Testnet"
    The testnet is available at [testnet.sera.cx](https://testnet.sera.cx) and its documentation is at [docs.testnet.sera.cx](https://docs.testnet.sera.cx).

## Why Sera?

<div class="grid cards" markdown>

-   :material-shield-check:{ .lg .middle } **Non-Custodial**

    ---

    Your funds live in on-chain smart contracts — never in our custody. Even if our servers go down, you can [withdraw directly on-chain](contracts/sera.md#emergencywithdraw). See [Non-Custodial Design](non-custodial.md).

-   :material-lightning-bolt:{ .lg .middle } **Web2 Speed, Web3 Security**

    ---

    Off-chain order matching delivers sub-second performance comparable to centralized exchanges, while all settlement and custody remains fully on-chain.

-   :material-gas-station-off:{ .lg .middle } **Gasless Swaps**

    ---

    Swap users don't need ETH for gas — fees are automatically factored into the quote. No wallet top-ups, no failed transactions.

-   :material-routes:{ .lg .middle } **Smart Order Routing (SOR)**

    ---

    Multi-leg atomic routing finds the best path for your swap across all available pairs. A GBP → SGD swap can route through USD if it offers better pricing, all in one atomic transaction.

-   :material-layers-triple:{ .lg .middle } **Virtual Liquidity**

    ---

    Place limit orders across multiple currency pairs backed by a single shared budget. Maximize capital efficiency without multiplying collateral requirements. See [Virtual Liquidity](virtual-liquidity.md).

-   :material-swap-horizontal:{ .lg .middle } **Thousands of Currency Pairs**

    ---

    Trade between stablecoins representing USD, EUR, GBP, SGD, JPY, and more — with SOR enabling cross-pair routing, any combination is tradeable. See [Supported Currency Pairs](currency-pairs.md).

</div>

[Launch App](https://testnet.sera.cx/connect){ .md-button .md-button--primary }
[Read the Docs](#how-it-works){ .md-button }

---

## How It Works

Sera connects buyers and sellers of stablecoins using a central limit order book (CLOB) for price discovery and matching. All fund custody and settlement happens on-chain via Ethereum smart contracts.

!!! info "Coming Soon: AMM"
    Sera will introduce a Function-Controlled Invariant Curve AMM (FCICAMM) to complement the CLOB. See the [Roadmap](protocol/roadmap.md) for details.

1. **Trade** — Execute instant [swaps](swaps.md) directly — no deposit required
2. **Deposit** — For [limit orders](limit-orders.md) and [Virtual Liquidity](virtual-liquidity.md), deposit tokens into the [Vault](vault.md) first
3. **Settle** — Matched trades are settled on-chain through the Sera smart contracts
4. **Withdraw** — Move funds from the Vault back to your wallet at any time

Sera's off-chain component handles only order matching — it never has access to your funds. All custody, settlement, and withdrawals are enforced by open-source, verifiable smart contracts. If Sera's API ever becomes unavailable, you can withdraw your funds directly on-chain via the [emergency withdrawal](contracts/sera.md#emergencywithdraw) mechanism.

---

## Security & Custody

Sera is **fully non-custodial**. Your funds are held in [audited](contracts/audits.md), open-source smart contracts on Ethereum — not by Sera. Our off-chain services exist solely to match orders; they never hold, control, or have access to your tokens.

| Component | Role | Holds funds? |
|-----------|------|:------------:|
| **Vault contract** | Stores deposited tokens with per-user ledger balances | Yes (on-chain) |
| **Sera contract** | Settles matched trades and enforces withdrawals | Yes (on-chain) |
| **Off-chain matching** | Matches buy/sell orders (CLOB) | No |
| **API** | Accepts orders and serves market data | No |

**Emergency withdrawal** — If Sera's off-chain services ever become unavailable, you can always recover your funds by calling `emergencyWithdraw()` directly on the Sera smart contract. No reliance on any off-chain service is required. See [Emergency Withdrawal](contracts/sera.md#emergencywithdraw).

---

## For Developers

Sera provides a REST API for programmatic trading and market data access. Smart contracts are open source and have been [independently audited](contracts/audits.md).

- **REST API** — Place orders, get quotes, query balances
- **EIP-712 Signatures** — Cryptographic authorization for all trading operations
- **Open-Source Contracts** — Full transparency on settlement logic

---

## Network Support

| Network | Status |
|---------|--------|
| Ethereum Mainnet | Live |
| Ethereum Sepolia | Testnet |

---

## Next Steps

<div class="grid cards" markdown>

-   :material-school:{ .lg .middle } **[FX Trading Primer](fx-primer.md)**

    ---

    New to foreign exchange? Start here.

-   :material-rocket-launch:{ .lg .middle } **[Quickstart](quickstart.md)**

    ---

    Get started with your first trade on Sera

-   :material-book-open-variant:{ .lg .middle } **[Core Concepts](concepts.md)**

    ---

    Understand order types, lifecycle, and fees

-   :material-api:{ .lg .middle } **[API Reference](api-reference/index.md)**

    ---

    Integrate Sera into your application

</div>

---

# Introduction to FX Trading

- Canonical URL: https://docs.testnet.sera.cx/fx-primer/
- Source path: `docs/en/fx-primer.md`
- Description: A beginner's guide to foreign exchange trading with stablecoins

# Introduction to FX Trading

Foreign exchange (FX or forex) is the global market for trading currencies. It is the largest and most liquid financial market in the world, with [$9.6 trillion in daily volume](https://www.bis.org/press/p250930.htm).

Sera brings FX trading on-chain by enabling you to trade between thousands of **stablecoins** — tokens that are pegged to fiat currencies like USD, EUR, GBP, SGD etc.

## What is a Currency Pair?

A currency pair expresses the price of one currency relative to another. It is written as **BASE/QUOTE**:

- **Base currency** — the currency you are buying or selling
- **Quote currency** — the currency used to price the base

For example, in **EUR/USD = 1.0850**:

- EUR is the base currency
- USD is the quote currency
- 1 EUR costs 1.0850 USD

### Bids and Asks

Every currency pair has two prices:

- **Bid** — the highest price a buyer is willing to pay
- **Ask** — the lowest price a seller is willing to accept

The difference between the bid and ask is called the **spread**. Tighter spreads mean better pricing for traders.

## Why Trade FX with Stablecoins?

Traditional FX trading requires bank accounts, brokers, and is typically restricted to business hours. Stablecoin FX offers several advantages:

- **24/7 Trading** — Markets are always open
- **Global Access** — Trade from anywhere with an Ethereum wallet
- **Transparency** — All settlements are verifiable on-chain
- **Lower Barriers** — No minimum account sizes or complex onboarding
- **Fast Settlement** — Trades settle in seconds, not days

## Common Currency Pairs

Major pairs involve the world's most traded currencies:

| Pair | Name | Description |
|------|------|-------------|
| EUR/USD | Euro / US Dollar | Most traded pair globally |
| GBP/USD | British Pound / US Dollar | Also called "Cable" |
| USD/JPY | US Dollar / Japanese Yen | Major Asian pair |
| AUD/USD | Australian Dollar / US Dollar | Commodity currency pair |
| USD/SGD | US Dollar / Singapore Dollar | Key Southeast Asian pair |

## How Prices Move

FX prices are driven by supply and demand, which in turn are influenced by:

- **Interest Rates** — Central bank rate decisions affect currency values
- **Economic Data** — GDP, employment, and inflation reports
- **Geopolitical Events** — Elections, trade policy, and global events
- **Market Sentiment** — Risk appetite and capital flows

In stablecoin FX, prices track the underlying fiat exchange rates through arbitrage — when a stablecoin pair deviates from the real-world FX rate, traders have an incentive to correct the price.

## Getting Started on Sera

Ready to start trading? Here's the flow:

1. **Connect your wallet** — Visit [testnet.sera.cx](https://testnet.sera.cx/connect)
2. **Deposit stablecoins** — Fund your vault with any supported token
3. **Place an order** — Choose a currency pair and place a limit order or instant swap
4. **Monitor and manage** — Track your orders and withdraw proceeds at any time

For step-by-step instructions, see the [Quickstart Guide](quickstart.md).

## Coming Soon: Derivatives

Sera is building on-chain FX derivatives — bringing instruments like forwards and options to stablecoin markets. If you're interested in learning more or exploring early access, reach out to us at [support@sera.cx](mailto:support@sera.cx).

---

# Why Sera

- Canonical URL: https://docs.testnet.sera.cx/why-sera/
- Source path: `docs/en/why-sera.md`
- Description: How Sera improves on traditional FX trading

# Why Sera

Traditional foreign exchange operates through a fragmented network of banks, brokers, and dealers. While this system moves [$9.6 trillion daily](https://www.bis.org/press/p250930.htm), it comes with significant structural limitations. Sera addresses these by bringing FX trading on-chain.

## Advantages Over Traditional FX

### Transparency

Traditional FX is an over-the-counter (OTC) market — pricing is opaque and spreads vary by counterparty. Sera uses a [central limit order book (CLOB)](concepts.md) for fair price discovery and matching, ensuring consistent pricing for all participants.

### Non-Custodial

In traditional FX, your funds sit with a broker or prime brokerage. If the counterparty fails, your capital is at risk. On Sera, funds are held in [audited smart contracts](contracts/audits.md) on Ethereum — not by any intermediary. You retain full control and can withdraw at any time, even if Sera's servers go offline.

### 24/7 Settlement

Traditional FX settles on a T+1 or T+2 basis through CLS Bank or correspondent banking networks, and only during banking hours. Sera settles trades on-chain in real time, 24 hours a day, 7 days a week. No settlement risk, no weekend gaps.

### No Intermediaries

A traditional FX trade can pass through multiple intermediaries — dealers, prime brokers, custodians, and settlement agents — each adding cost and delay. Sera removes these layers. Trades are matched off-chain and settled directly on-chain between counterparties.

### Permissionless Access

Traditional FX markets have high barriers to entry: minimum account sizes, credit checks, and geographic restrictions. Sera is open to anyone with an Ethereum wallet. No minimum balance, no application process.

### Verifiable

Traditional FX relies on trust — you trust your broker's quoted price, your custodian's balance sheet, and your settlement agent's process. On Sera, all smart contracts are open source. Settlement logic, balances, and trade history are independently verifiable on-chain.

---

## What Makes Sera Unique

Beyond the general advantages of on-chain trading, Sera introduces capabilities that don't exist in traditional FX or other on-chain exchanges.

### Smart Order Routing (SOR)

Sera's [Smart Order Router](contracts/sor.md) finds the best execution path across all available pairs. If you want to swap GBP to SGD but the direct pair has thin liquidity, SOR can atomically route through USD — e.g. GBP → USD → SGD — in a single transaction, at the best available price. All legs settle atomically: either the entire route executes, or none of it does.

### Virtual Liquidity

[Virtual Liquidity](virtual-liquidity.md) lets market makers place orders across multiple trading pairs backed by a single shared budget. Instead of locking separate collateral for EUR/USD, GBP/USD, and SGD/USD, a single USD deposit can back all three. When one order fills, the remaining orders are automatically adjusted. This dramatically improves capital efficiency — a capability that doesn't exist in traditional FX or other on-chain exchanges.

### Direct Non-USD Settlement Pairs

Traditional FX routes almost everything through USD. A EUR → SGD trade typically executes as EUR → USD → SGD, with two spreads, two settlements, and the associated costs. Sera supports direct cross-currency pairs — EUR/SGD, GBP/JPY, AUD/BRL, and [hundreds more](currency-pairs.md) — so you can trade and settle directly without the USD leg, reducing costs and complexity.

### Gasless Swaps

Swap users don't need ETH for gas fees. Costs are factored directly into the quote, so you can trade using only stablecoins — no wallet top-ups, no failed transactions from insufficient gas.

### MEV Is Not an Issue

On Sera, MEV is not a practical trading issue in the way it is on on-chain AMMs. Price discovery and matching happen inside Sera's Web2 CLOB matching engine, not in Ethereum's public mempool. Ethereum is used as the final settlement layer only.

Because orders are matched off-chain rather than exposed as public marketable swaps on-chain, searchers do not get the usual opportunity to front-run or sandwich user flow. For swaps, users still sign exact execution bounds such as `maxInputAmount` and `minOutputAmount`, so settlement either happens within those signed limits or fails.

---

## Summary

| | Traditional FX | Sera |
|---|---|---|
| **Custody** | Broker / prime brokerage | Non-custodial smart contracts |
| **Settlement** | T+1 / T+2, banking hours only | Real-time, 24/7 |
| **Pricing** | Opaque OTC pricing | Consistent CLOB pricing |
| **Intermediaries** | Multiple (dealer, PB, custodian, CLS) | None |
| **Cross-currency** | Routed through USD | Direct pairs available |
| **Capital efficiency** | Separate margin per position | Virtual Liquidity across pairs |
| **Access** | Restricted, high minimums | Permissionless |
| **Verifiability** | Trust-based | On-chain, open source |

---

# Non-Custodial Design

- Canonical URL: https://docs.testnet.sera.cx/non-custodial/
- Source path: `docs/en/non-custodial.md`
- Description: How Sera keeps your funds secure without ever holding them

# Non-Custodial Design

Sera is a **fully non-custodial** exchange. Your funds are always held in on-chain smart contracts on Ethereum — never by Sera. This page explains what that means, how it works, and why it matters.

## What Does Non-Custodial Mean?

On a custodial exchange, you transfer your tokens to the exchange and trust them to keep your funds safe. If the exchange is hacked, goes offline, or acts maliciously, your funds are at risk.

On Sera, this is not the case. Your tokens are held in **[audited](contracts/audits.md), open-source smart contracts** deployed on Ethereum. These contracts enforce all rules — deposits, trading, settlement, and withdrawals — through code that anyone can verify. Sera's team cannot move your funds, freeze them arbitrarily, or prevent you from withdrawing.

## What Sera Controls vs. What It Doesn't

| Component | What it does | Has access to your funds? |
|-----------|-------------|:-------------------------:|
| **Vault contract** | Stores deposited tokens with per-user ledger balances | On-chain custody only |
| **Sera contract** | Settles matched trades and enforces withdrawals | On-chain custody only |
| **Off-chain matching** | Matches buy and sell orders (CLOB) | **No** |
| **API** | Accepts orders and serves market data | **No** |

Sera's off-chain services exist solely to match orders. They never hold, control, or have access to your tokens. All fund movements happen on-chain through the smart contracts.

## How Your Funds Stay Safe

### During Trading

When you deposit tokens into the [Vault](vault.md), they move from your wallet into the Vault smart contract — not to Sera. The contract tracks your balance under your address. When you place a limit order, your collateral is frozen in the contract. When your order fills, settlement is a ledger update inside the vault — no token movement and no gas cost per trade. This lets professional market makers and financial institutions trade continuously after a single deposit, without paying gas on every fill. At every step, the smart contracts enforce the rules.

### During Swaps

Swaps don't require a Vault deposit. The system builds the transaction and you sign it with your wallet — because the parameters are structured as EIP-712 typed data, you can review exactly what you're signing (tokens, amounts, recipient) before approving. Settlement then happens atomically on-chain. Sera's off-chain service only facilitates the quote — it never touches your tokens.

### If Sera Goes Offline

If Sera's API or off-chain services ever become unavailable, you can **always** recover your funds — including frozen balances — by calling `emergencyWithdraw()` directly on the Sera smart contract. This works without any interaction with Sera's servers.

The emergency withdrawal process:

1. Call `emergencyWithdraw(token, amount)` on the Sera contract
2. Wait ~24 hours (~7,200 blocks) — this delay prevents abuse
3. Call `emergencyWithdraw(token, amount)` again to execute

See [Emergency Withdrawal](contracts/sera.md#emergencywithdraw) for the full contract reference.

## What If Sera's Servers Are Hacked?

Even if an attacker gains full control of all of Sera's off-chain infrastructure, **your funds remain safe** as long as you don't sign a malicious transaction.

Here's why: Sera's off-chain systems have **zero access** to your tokens. All fund movements are executed by on-chain smart contracts and require **your cryptographic signature**. An attacker who compromises Sera's servers cannot:

- Withdraw your funds from the Vault
- Modify your balances
- Settle trades you didn't agree to
- Bypass the smart contract rules

The only attack vector would be tricking you into signing a malicious transaction — for example, a swap with manipulated parameters. But because Sera uses **EIP-712 typed data signing**, your wallet displays the exact details (token addresses, amounts, recipient) before you approve. As long as you review what you sign, a compromised backend cannot steal your funds.

| Scenario | Risk to your funds |
|----------|-------------------|
| Sera's API is compromised | None — off-chain services can't move tokens |
| Attacker modifies order matching | None — settlement still requires valid signatures and on-chain rules |
| Attacker sends you a malicious transaction to sign | Only if you sign it without reviewing — your wallet shows full details via EIP-712 |
| Attacker tries to call contracts directly | Fails — contracts require your signature for any fund movement |

This is the core advantage of non-custodial design: the security of your funds depends on the smart contracts and your own signatures — not on the security of Sera's servers.

## Open Source & Auditable

All smart contracts are open source. Anyone can:

- Read the contract code to verify the rules
- Independently [audit](contracts/audits.md) the contracts
- Verify that deployed contracts match the published source code

This transparency is fundamental to the non-custodial guarantee — you don't have to trust Sera, you can verify.

## Audit Reports

Sera's smart contracts have been independently audited. See the dedicated [**Audit Reports**](contracts/audits.md) page for the full list and per-contract source links, or [browse the audit folder directly on GitHub](https://github.com/sera-cx/orderbook-contract-v2/tree/audit/audits).

## Summary

| Question | Answer |
|----------|--------|
| Who holds my funds? | The on-chain Vault smart contract |
| Can Sera access my funds? | No — off-chain services never touch your tokens |
| Can I withdraw anytime? | Yes — via the API, or directly on-chain if the API is down |
| Are the contracts open source? | Yes — fully auditable. See [Audit Reports](contracts/audits.md). |
| What if Sera disappears? | You withdraw directly on-chain via emergency withdrawal |
| What if Sera's servers are hacked? | Your funds are safe — attackers can't move tokens without your signature |

---

# Compliance

- Canonical URL: https://docs.testnet.sera.cx/compliance/
- Source path: `docs/en/compliance.md`
- Description: How Sera uses on-chain analytics to screen wallets and maintain a safe trading environment

# Compliance

Sera is committed to operating a safe and responsible platform. As part of that commitment, we use on-chain analytics to screen wallet addresses and prevent bad actors from accessing our services.

## Wallet Screening

Sera continuously monitors and screens wallet addresses using on-chain analytics. Addresses associated with sanctioned entities, illicit activity, or other high-risk classifications are added to a blocklist and denied access to the platform.

This screening is proactive — wallets are evaluated before and during use of the platform, not only at onboarding. If a wallet is flagged after it has already been using the platform, access is revoked.

## What Access Is Restricted

A blocklisted wallet address cannot:

- Connect to the Sera platform
- Place or cancel orders through the API
- Execute swaps through the Sera interface
- Access account data or trade history via the API

Access restrictions apply at the application layer — the API and front end will refuse requests from restricted addresses.

## Funds Are Never Withheld

Sera is a **fully non-custodial** platform. Wallet screening affects access to Sera's services, but it does not affect your funds.

If your wallet is blocklisted:

- Any tokens held in the [Vault](vault.md) smart contract remain under your control
- You can call `emergencyWithdraw()` directly on the Sera smart contract to retrieve your funds without using Sera's API or interface
- No on-chain mechanism exists to freeze or seize your tokens — that is a fundamental property of the non-custodial design

The distinction is clear: Sera can deny access to its off-chain services, but it cannot touch your funds. Your tokens are held by the smart contract, not by Sera.

## Why We Do This

On-chain analytics help Sera identify and avoid facilitating illicit financial flows. This includes wallets associated with:

- OFAC-sanctioned addresses
- Known mixers or tumbling services
- Addresses linked to hacks, scams, or theft
- Other high-risk classifications identified by our analytics provider

This protects honest users and helps ensure Sera operates responsibly within the evolving regulatory landscape.

## Disputes and Enquiries

If you believe your wallet has been flagged incorrectly, contact us at [support@sera.cx](mailto:support@sera.cx). We review blocklist decisions on a case-by-case basis.

---

# Quickstart

- Canonical URL: https://docs.testnet.sera.cx/quickstart/
- Source path: `docs/en/quickstart.md`
- Description: Get started with Sera in minutes

# Quickstart

!!! info "Network & Compatibility"
    | Resource       | Value                                                       |
    |----------------|-------------------------------------------------------------|
    | API base URL   | `https://api.testnet.sera.cx/api/v1`                        |
    | Chain          | Sepolia (chainId `11155111`)                                |
    | Sera contract  | `0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e`                 |
    | Vault contract | `0x3c7945840bAE0d7e7f3824Ebccef1962629250F0`                 |
    | SOR contract   | `0x83c1368110B640A729f3810De5FBe94b99aa5668`                 |

    **Signing primitives.** Every trading mutation is an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed-data signature against the Sera domain. Deposits that take the permit path use the [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) `Permit` extension — supported by USDC, EURC, EURT, and most modern stablecoins, not all ERC-20s; call `GET /permit/metadata` to check support before signing. API-key management uses an EIP-712 `ManageApiKey` payload.

    **Tested clients.** Python `eth_account >= 0.10` + `requests`; TypeScript `ethers` v6 (`signer.signTypedData`). Browser wallets confirmed working with EIP-712 typed data: **MetaMask**, **Rabby**, **Frame**, **Coinbase Wallet**, **Trust**, **Rainbow**. Safe multisigs work via EIP-1271 (the message is verified on-chain rather than via ecrecover).

    **Address casing.** Read endpoints (`/balances`, `/orders`, `/fills`) treat `owner_address` as case-sensitive — pass the lowercase form. EIP-712 signed payloads accept EIP-55 checksum addresses.

This guide walks you through your first interaction with Sera — from querying available tokens to placing your first swap.

## Prerequisites

- An Ethereum wallet (e.g., MetaMask)
- Testnet ETH for deposits, withdrawals, and limit-order settlement (swap-only flows do not require ETH). Use a [Sepolia faucet](https://sepoliafaucet.com).

## Step 1: Explore Available Tokens

Query the token registry to see what stablecoins are available:

=== "cURL"

    ```bash
    curl https://api.testnet.sera.cx/api/v1/tokens
    ```

=== "Python"

    ```python
    import requests

    response = requests.get("https://api.testnet.sera.cx/api/v1/tokens")
    tokens = response.json()["tokens"]
    print(tokens)
    ```

=== "TypeScript"

    ```typescript
    const response = await fetch("https://api.testnet.sera.cx/api/v1/tokens");
    const { tokens } = await response.json();
    console.log(tokens);
    ```

## Step 2: Get a Swap Quote

Get a quote to swap between two tokens:

=== "cURL"

    ```bash
    curl -X POST https://api.testnet.sera.cx/api/v1/swap/quote \
      -H "Content-Type: application/json" \
      -d '{
        "from_token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
        "to_token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
        "from_amount": "1000000000",
        "owner_address": "0xYOUR_ADDRESS",
        "recipient": "0xYOUR_ADDRESS",
        "expiration": 1735689600,
        "gas_mode": "receive_less"
      }'
    ```

=== "Python"

    ```python
    import time, requests

    quote = requests.post(
        "https://api.testnet.sera.cx/api/v1/swap/quote",
        json={
            "from_token":     "0x965d4b4546716e416e950bc30467d128455d2d0e",  # USDC
            "to_token":       "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",  # EURC
            "from_amount":    "1000000000",                                  # 1000 USDC (6 dec)
            "owner_address":  "0xYOUR_ADDRESS",
            "recipient":      "0xYOUR_ADDRESS",
            "expiration":     int(time.time()) + 3600,
            "gas_mode":       "receive_less",
        },
        timeout=10,
    ).json()
    print(quote)
    ```

=== "TypeScript"

    ```typescript
    const response = await fetch("https://api.testnet.sera.cx/api/v1/swap/quote", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        from_token:    "0x965d4b4546716e416e950bc30467d128455d2d0e",   // USDC
        to_token:      "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",   // EURC
        from_amount:   "1000000000",                                    // 1000 USDC (6 dec)
        owner_address: "0xYOUR_ADDRESS",
        recipient:     "0xYOUR_ADDRESS",
        expiration:    Math.floor(Date.now() / 1000) + 3600,
        gas_mode:      "receive_less",
      }),
    });
    const quote = await response.json();
    console.log(quote);
    ```

The response includes `uuid`, `route_params`, and `quote_breakdown`. Sign only `route_params`; use `quote_breakdown` to display before/after gas amounts.

## Step 3: Sign and Execute the Swap

Sign the `route_params` using EIP-712 typed data signing, then submit:

=== "Python"

    ```python
    from eth_account import Account
    from eth_account.messages import encode_typed_data

    # `domain` and `INTENT_TYPES` are documented under Authentication
    signable  = encode_typed_data(domain, INTENT_TYPES, quote["route_params"])
    signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()

    submit = requests.post(
        "https://api.testnet.sera.cx/api/v1/swap",
        json={"uuid": quote["uuid"], "signature": "0x" + signature.lstrip("0x")},
        timeout=10,
    ).json()
    ```

=== "TypeScript"

    ```typescript
    // `domain` and `INTENT_TYPES` are documented under Authentication
    const signature = await signer.signTypedData(domain, INTENT_TYPES, quote.route_params);

    const result = await fetch("https://api.testnet.sera.cx/api/v1/swap", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ uuid: quote.uuid, signature }),
    });
    ```

Quotes are single-use. If submission fails after the quote is consumed, request a fresh quote instead of retrying the same `uuid`.

For detailed signing instructions, see [Authentication](api-reference/authentication.md).

## Step 4: Check Your Balances

Create an API key to query your balances and order history. The `/balances` endpoint returns **both** your wallet balance (tokens in your Ethereum wallet) and your Vault balance (tokens deposited for limit order trading), along with any frozen amounts locked in open orders.

=== "Python"

    ```python
    balances = requests.get(
        "https://api.testnet.sera.cx/api/v1/balances",
        params={"owner_address": "0xYOUR_ADDRESS"},
        headers={"Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET"},
        timeout=10,
    ).json()["balances"]

    for bal in balances:
        print(f"{bal['symbol']}:")
        print(f"  Wallet:         {bal['wallet_balance']}")
        print(f"  Vault available: {bal['vault_available']}")
        print(f"  Vault frozen:    {bal['vault_frozen']}")
    ```

=== "TypeScript"

    ```typescript
    const response = await fetch(
      "https://api.testnet.sera.cx/api/v1/balances?owner_address=0xYOUR_ADDRESS",
      { headers: { "Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET" } },
    );
    const { balances } = await response.json();

    for (const bal of balances) {
      console.log(`${bal.symbol}:`);
      console.log(`  Wallet:          ${bal.wallet_balance}`);
      console.log(`  Vault available: ${bal.vault_available}`);
      console.log(`  Vault frozen:    ${bal.vault_frozen}`);
    }
    ```

## Using the Web App

You can also trade directly through the Sera web interface:

1. Visit [testnet.sera.cx](https://testnet.sera.cx/connect)
2. Connect your wallet
3. Select a currency pair
4. Place a limit order or execute an instant swap
5. Monitor your orders in the dashboard

## Next Steps

- [Market Maker Guide](api-reference/market-maker-guide.md) — End-to-end walkthrough for programmatic order placement and cancellation
- [Core Concepts](concepts.md) — Understand order types, lifecycle, and fees
- [API Reference](api-reference/index.md) — Full API documentation
- [Order Types](order-types.md) — Learn about limit orders, swaps, and more

---

# Core Concepts

- Canonical URL: https://docs.testnet.sera.cx/concepts/
- Source path: `docs/en/concepts.md`
- Description: Understanding key concepts in Sera

# Core Concepts

This page explains the fundamental concepts you need to understand when trading on Sera.

## Order Types

Sera supports three primary order types:

### Swaps

Instant swaps execute immediately at the best available price. You specify the tokens and amount you want to trade, and Sera finds the best route.

!!! info "Swaps do not require a pre-funded Vault balance"
    Swaps are signed from your wallet and do not require a prior Vault deposit. The signed route can pull the exact input amount from your wallet at execution time, and gas is absorbed into the quote. This makes swaps the simplest way to trade on Sera.

- **Price Protection** — Set a minimum output amount to protect against unfavorable prices
- **Multi-Leg Routing** — Swaps can route through intermediate currencies for better rates

For the full guide, see [Swaps](swaps.md).

### Limit Orders

Limit orders let you specify the exact price at which you want to trade. They sit on the order book until matched or cancelled. Requires a [Vault](vault.md) deposit and ETH for gas.

- **Bid (Buy)** — Specify the maximum price you're willing to pay
- **Ask (Sell)** — Specify the minimum price you're willing to receive

For the full guide, see [Limit Orders](limit-orders.md).

### Virtual Liquidity Batches

Virtual Liquidity (VL) batches let you place limit orders across multiple trading pairs backed by a single shared budget, maximizing capital efficiency for multi-pair strategies.

- **Shared Collateral** — Freeze the max of any single order cost, not the sum
- **Unique Markets** — Each sibling must target a distinct market; inverse pairs such as `XSGD/USDC` and `USDC/XSGD` count as duplicates
- **Automatic Amendment** — When one order fills, siblings are resized to fit remaining budget

For a full guide, see [Virtual Liquidity](virtual-liquidity.md).

## Order Lifecycle

Every order follows a defined lifecycle. The public API surfaces five statuses:

```mermaid
stateDiagram-v2
    [*] --> Pending: Place Order
    Pending --> Pending: Partial Fill (still active)
    Pending --> Matched: All Legs Crossed
    Matched --> Settled: Chain Confirmed
    Matched --> Failed: Settlement Reverted
    Pending --> Cancelled: Cancel
    Pending --> Failed: Rejected or Settlement Failed
```

| Status | Description |
|--------|-------------|
| `pending` | Submitted, resting on the book, or partially filled |
| `matched` | All legs crossed in the matching engine; on-chain settlement is in flight |
| `settled` | Fully filled and on-chain settlement confirmed |
| `cancelled` | Cancelled by the owner before full fill |
| `failed` | Rejected at intake or settlement reverted |

For partially filled orders, `pending` means "still resting", not "unfilled".
Use the filled/remaining amount fields, `settlement_summary`, `/fills`, and
public `settlement_economics.balance_debits` / `balance_credits` to show
execution progress and owner balance movement. Public `gross_*` economics
fields are compatibility fields, not a protocol-spread audit surface.

Proceeds from a `settled` order are credited to your Vault balance automatically — there is no separate "claim" step.

For a complete walkthrough, see [Order Lifecycle](order-lifecycle.md).

## Non-Custodial Architecture

Sera is fully non-custodial. Your funds are held in on-chain smart contracts — Sera's off-chain services handle only order matching and never have access to your tokens. Even if Sera's API goes down, you can always withdraw directly on-chain.

For the full explanation, see [Non-Custodial Design](non-custodial.md).

## Vault & Balances

- **Wallet Balance** — Tokens in your Ethereum wallet
- **Vault Balance** — Tokens deposited into the [Vault](vault.md), available for trading
- **Frozen Balance** — Tokens currently locked in open orders — still in the Vault contract under your address

To trade with limit orders, you first deposit tokens into the [Vault](vault.md). When you place an order, the required tokens are frozen. When an order fills, proceeds are credited to your Vault balance. All fund movements are enforced by the smart contracts.

## Virtual Liquidity

Virtual Liquidity (VL) lets you place orders across multiple trading pairs backed by a single shared budget. Instead of locking capital for each order independently, a VL batch shares one pool of collateral — one fill automatically adjusts the remaining siblings to stay within budget.

- **Capital Efficiency** — Freeze the max of any single order cost, not the sum
- **Multi-Pair Coverage** — Place 2 to 50 sibling orders across distinct markets in one batch. Query `GET /config` → `limits.vl_batch` for the current cap.
- **Unique Markets** — Exact duplicates and inverse pairs are rejected inside the same batch
- **Automatic Amendment** — Siblings are resized or cancelled as the budget is consumed

For a full guide, see [Virtual Liquidity](virtual-liquidity.md).

## Fees & Gas

- **Swap users** — Every cost the swap pays, including gas, is already incorporated into the quote you sign. Choose `receive_less` or `pay_more` gas mode when requesting the quote; you do not need to hold ETH separately.
- **Limit order users** — Gas is paid in real ETH at settlement time.

For more details, see [Fees & Costs](fees.md).

## EIP-712 Signatures

All trading operations on Sera are authorized via **EIP-712 typed data signatures**. This means:

- You sign a structured message with your wallet (e.g., MetaMask)
- The signature authorizes a specific action (place order, cancel, withdraw)
- Your private key never leaves your device
- Each signature is bound to a specific chain and contract, preventing replay attacks

For API users, see [Authentication](api-reference/authentication.md) to learn how to construct and sign these messages.

## Next Steps

<div class="grid cards" markdown>

-   :material-format-list-bulleted:{ .lg .middle } **[Order Types](order-types.md)**

    ---

    Deep dive into limit orders, swaps, and more

-   :material-layers-triple:{ .lg .middle } **[Virtual Liquidity](virtual-liquidity.md)**

    ---

    Multi-pair orders with shared capital

-   :material-api:{ .lg .middle } **[API Reference](api-reference/index.md)**

    ---

    Start building with the Sera API

</div>

---

# Supported Currency Pairs

- Canonical URL: https://docs.testnet.sera.cx/currency-pairs/
- Source path: `docs/en/currency-pairs.md`
- Description: Currency pairs available for trading on Sera

# Supported Currency Pairs

Sera supports stablecoin trading across many fiat currencies. Each currency is represented by one or more stablecoins on Ethereum.

## Available Currencies

The table below reflects the Sepolia testnet token registry. Mainnet and other deployments may carry a different subset — query `GET /tokens` for the live list, which is the authoritative source.

### 🇦🇪 UAE Dirham (AED)

| Ticker | Address |
|--------|---------|
| DRAM | [`0xfba0dcc6e8dbde0db4aa7b7be140a887ea789c0b`](https://sepolia.etherscan.io/address/0xfba0dcc6e8dbde0db4aa7b7be140a887ea789c0b) |
| ZANDAED | [`0xb08bdec72ebc79a56551a8748b789b51bfd0bfe6`](https://sepolia.etherscan.io/address/0xb08bdec72ebc79a56551a8748b789b51bfd0bfe6) |

### 🇦🇷 Argentine Peso (ARS)

| Ticker | Address |
|--------|---------|
| ARGT | [`0x5bc627379b6b961a8c8480bc4c32b98c12a9373b`](https://sepolia.etherscan.io/address/0x5bc627379b6b961a8c8480bc4c32b98c12a9373b) |
| ARSE | [`0x132343ae28cd7b0c417e7cbc2a32020ad2a83e37`](https://sepolia.etherscan.io/address/0x132343ae28cd7b0c417e7cbc2a32020ad2a83e37) |
| ARST | [`0x8e461d96b83e54fe02cf76b792664e3b799e36da`](https://sepolia.etherscan.io/address/0x8e461d96b83e54fe02cf76b792664e3b799e36da) |
| ARZ | [`0xe5eb4d73cdbda876c585ac553e01a12be4a4425f`](https://sepolia.etherscan.io/address/0xe5eb4d73cdbda876c585ac553e01a12be4a4425f) |
| WARS | [`0x79cf2c7ef9edd1d1a8cb92eb6b268187079d2ef6`](https://sepolia.etherscan.io/address/0x79cf2c7ef9edd1d1a8cb92eb6b268187079d2ef6) |

### 🇦🇺 Australian Dollar (AUD)

| Ticker | Address |
|--------|---------|
| AUDD | [`0x9aa9fdf28de6b89251bc7a48290e5e68a1924330`](https://sepolia.etherscan.io/address/0x9aa9fdf28de6b89251bc7a48290e5e68a1924330) |
| AUDF | [`0x051654f1a5489e2d9118087815b07cb2d3dbf797`](https://sepolia.etherscan.io/address/0x051654f1a5489e2d9118087815b07cb2d3dbf797) |
| AUDM | [`0x6c0d9a6ea20ced7596de1c602e73f42421fcc882`](https://sepolia.etherscan.io/address/0x6c0d9a6ea20ced7596de1c602e73f42421fcc882) |
| AUDX | [`0x7ebc3500de704b6ced89d4f4dc6ed9ebd575e833`](https://sepolia.etherscan.io/address/0x7ebc3500de704b6ced89d4f4dc6ed9ebd575e833) |
| EAUD | [`0x7a5dbcb2808015d76fbaafd16281f6381b91adc5`](https://sepolia.etherscan.io/address/0x7a5dbcb2808015d76fbaafd16281f6381b91adc5) |

### 🇧🇴 Bolivian Boliviano (BOB)

| Ticker | Address |
|--------|---------|
| BOLT | [`0xb6bb2fad4820f10026a8af583375858ebb306a9b`](https://sepolia.etherscan.io/address/0xb6bb2fad4820f10026a8af583375858ebb306a9b) |

### 🇧🇷 Brazilian Real (BRL)

| Ticker | Address |
|--------|---------|
| BBRL | [`0xe57d167cc57eb3d56b70eaded72c4fb7b6d0efc3`](https://sepolia.etherscan.io/address/0xe57d167cc57eb3d56b70eaded72c4fb7b6d0efc3) |
| BRAT | [`0x5a3a102f5bd451dddcee790427663b53a761b46e`](https://sepolia.etherscan.io/address/0x5a3a102f5bd451dddcee790427663b53a761b46e) |
| BRL1 | [`0x52026009479ef65833b517ae4b1ba0bde1f9616f`](https://sepolia.etherscan.io/address/0x52026009479ef65833b517ae4b1ba0bde1f9616f) |
| BRLA | [`0xf149a81e1d6206737de2bf1683b31cb7181b3e62`](https://sepolia.etherscan.io/address/0xf149a81e1d6206737de2bf1683b31cb7181b3e62) |
| BRLT | [`0x2f286bb5e68ec9d2b33efb26bcda5c3a81121b45`](https://sepolia.etherscan.io/address/0x2f286bb5e68ec9d2b33efb26bcda5c3a81121b45) |
| BRLV | [`0x74e91488b4af17f5fa411e43b642069f1e22db67`](https://sepolia.etherscan.io/address/0x74e91488b4af17f5fa411e43b642069f1e22db67) |
| BRTH | [`0x03392f020203e409067beb9282822e9e82b73ea5`](https://sepolia.etherscan.io/address/0x03392f020203e409067beb9282822e9e82b73ea5) |
| BRZ | [`0x3ce0ff0c1d7f38de7b7a2a63a1e0d750ff42478b`](https://sepolia.etherscan.io/address/0x3ce0ff0c1d7f38de7b7a2a63a1e0d750ff42478b) |
| WBRL | [`0x7e42fe1ab142be43887af67388516ae68f008343`](https://sepolia.etherscan.io/address/0x7e42fe1ab142be43887af67388516ae68f008343) |

### 🇨🇦 Canadian Dollar (CAD)

| Ticker | Address |
|--------|---------|
| CADC | [`0x67f9068a74b77baa0b8480edf936ffb57423b61c`](https://sepolia.etherscan.io/address/0x67f9068a74b77baa0b8480edf936ffb57423b61c) |
| QCAD | [`0xab7ada4acdb8a6cfb29822f6a6f28fa6555b3f4a`](https://sepolia.etherscan.io/address/0xab7ada4acdb8a6cfb29822f6a6f28fa6555b3f4a) |

### 🌍 CFA Franc (CFA)

| Ticker | Address |
|--------|---------|
| ECFA | [`0x443ed496558f9b50e029dbeeaee8932786eb57e9`](https://sepolia.etherscan.io/address/0x443ed496558f9b50e029dbeeaee8932786eb57e9) |

### 🇨🇭 Swiss Franc (CHF)

| Ticker | Address |
|--------|---------|
| CCHF | [`0xe7b1a4581789ee3156bd0b62ef6d6e5a1d2c7e5a`](https://sepolia.etherscan.io/address/0xe7b1a4581789ee3156bd0b62ef6d6e5a1d2c7e5a) |
| CHFAU | [`0x46b74cb363d602f66a2a010154d131f0343703c3`](https://sepolia.etherscan.io/address/0x46b74cb363d602f66a2a010154d131f0343703c3) |
| DCHF | [`0x145f5ba4a0d097425aa8da6feb7d80f5141f9097`](https://sepolia.etherscan.io/address/0x145f5ba4a0d097425aa8da6feb7d80f5141f9097) |
| VCHF | [`0xe97f32b2ca3d8179419b1c86d173158750804e4f`](https://sepolia.etherscan.io/address/0xe97f32b2ca3d8179419b1c86d173158750804e4f) |

### 🇨🇱 Chilean Peso (CLP)

| Ticker | Address |
|--------|---------|
| CHLT | [`0xb7f36ae290dd1e24add6ee916115179818a5c062`](https://sepolia.etherscan.io/address/0xb7f36ae290dd1e24add6ee916115179818a5c062) |
| WCLP | [`0x49ee116de52c7e9f2b034e2af3e23b7169e85040`](https://sepolia.etherscan.io/address/0x49ee116de52c7e9f2b034e2af3e23b7169e85040) |

### 🇨🇳 Chinese Yuan (Offshore) (CNH)

| Ticker | Address |
|--------|---------|
| AXCNH | [`0x826266db15d0d01fc9dbe6a2d31dca16425ed9f9`](https://sepolia.etherscan.io/address/0x826266db15d0d01fc9dbe6a2d31dca16425ed9f9) |
| CNHT | [`0x9e9f5a02e3613ab9a512a1cbc624f316018ba182`](https://sepolia.etherscan.io/address/0x9e9f5a02e3613ab9a512a1cbc624f316018ba182) |

### 🇨🇴 Colombian Peso (COP)

| Ticker | Address |
|--------|---------|
| COLT | [`0x7eb0d08f56dad77e9e21e8f1925f2efbec7c373b`](https://sepolia.etherscan.io/address/0x7eb0d08f56dad77e9e21e8f1925f2efbec7c373b) |
| COPM | [`0x8711ca3e7dbb1e67445cb7e7c4ddc474476a8be0`](https://sepolia.etherscan.io/address/0x8711ca3e7dbb1e67445cb7e7c4ddc474476a8be0) |
| WCOP | [`0x68e4a4c441fffe36e27d34eceab6105045b42bec`](https://sepolia.etherscan.io/address/0x68e4a4c441fffe36e27d34eceab6105045b42bec) |

### 🇪🇺 Euro (EUR)

| Ticker | Address |
|--------|---------|
| EEUR | [`0xaad15b1f95bf49762ea5765dcb1ce952d4689349`](https://sepolia.etherscan.io/address/0xaad15b1f95bf49762ea5765dcb1ce952d4689349) |
| EUR0 | [`0x6d40b5405f85c70c289af8e70d4afc4c6b3540f3`](https://sepolia.etherscan.io/address/0x6d40b5405f85c70c289af8e70d4afc4c6b3540f3) |
| EURAU | [`0x4484db7340a83b997094a8ab83e3980658d093c1`](https://sepolia.etherscan.io/address/0x4484db7340a83b997094a8ab83e3980658d093c1) |
| EURC | [`0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f`](https://sepolia.etherscan.io/address/0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f) |
| EURCV | [`0xab7ad77232aa632604391f8d407bde132e013613`](https://sepolia.etherscan.io/address/0xab7ad77232aa632604391f8d407bde132e013613) |
| EURD | [`0x45091562af9e86feac8c9748c23383dc6a60a5cc`](https://sepolia.etherscan.io/address/0x45091562af9e86feac8c9748c23383dc6a60a5cc) |
| EURE | [`0x9aad62a9cedc32d633a39b78dba4f0c0742f562c`](https://sepolia.etherscan.io/address/0x9aad62a9cedc32d633a39b78dba4f0c0742f562c) |
| EURI | [`0x708d2261239ac73802ab4342655cff5e5536f9ac`](https://sepolia.etherscan.io/address/0x708d2261239ac73802ab4342655cff5e5536f9ac) |
| EURND | [`0xea77c5920597ecd05f721e514561d88bd1d7f662`](https://sepolia.etherscan.io/address/0xea77c5920597ecd05f721e514561d88bd1d7f662) |
| EUROP | [`0x81322971f7b1e94334627c3e00cb79b72d58d678`](https://sepolia.etherscan.io/address/0x81322971f7b1e94334627c3e00cb79b72d58d678) |
| EUROT | [`0x0add3a58bca76e57b3b59ba379bc25ff1f57d45a`](https://sepolia.etherscan.io/address/0x0add3a58bca76e57b3b59ba379bc25ff1f57d45a) |
| EURQ | [`0x0e57f70c0ccbba293ee2b62a9a76589c8cb4f1dc`](https://sepolia.etherscan.io/address/0x0e57f70c0ccbba293ee2b62a9a76589c8cb4f1dc) |
| EURR | [`0x83946e5b5711e4b23a7f805b9614e3ff01afd2bc`](https://sepolia.etherscan.io/address/0x83946e5b5711e4b23a7f805b9614e3ff01afd2bc) |
| EURS | [`0x08630589d1f94292405a7fb32f6f073a604fc852`](https://sepolia.etherscan.io/address/0x08630589d1f94292405a7fb32f6f073a604fc852) |
| EURT | [`0x02aa41876fe62da9e28fe8a970a5dfa3dfdabf07`](https://sepolia.etherscan.io/address/0x02aa41876fe62da9e28fe8a970a5dfa3dfdabf07) |
| REUR | [`0x6e289df0c97a3fd52d8e1c0eb4af5e672fa22a18`](https://sepolia.etherscan.io/address/0x6e289df0c97a3fd52d8e1c0eb4af5e672fa22a18) |
| TNEUR | [`0x941e7761459f2ef19770a5e150f5027a10cc0f17`](https://sepolia.etherscan.io/address/0x941e7761459f2ef19770a5e150f5027a10cc0f17) |
| VEUR | [`0xfbfc867e1ccd3382b9d4eaf89b9e8a6600d7d4e7`](https://sepolia.etherscan.io/address/0xfbfc867e1ccd3382b9d4eaf89b9e8a6600d7d4e7) |

### 🇬🇧 British Pound (GBP)

| Ticker | Address |
|--------|---------|
| ARYZEEGBP | [`0xfbc4ad8f38509e7ad577b228e00ee5fa186f0e96`](https://sepolia.etherscan.io/address/0xfbc4ad8f38509e7ad577b228e00ee5fa186f0e96) |
| EGBP | [`0x38abe0998504fd6a4bdc8fdbf47541b3ec945730`](https://sepolia.etherscan.io/address/0x38abe0998504fd6a4bdc8fdbf47541b3ec945730) |
| GBPA | [`0x7a3f4d673539b5c0aabd255d561d413ec5902dbb`](https://sepolia.etherscan.io/address/0x7a3f4d673539b5c0aabd255d561d413ec5902dbb) |
| TGBP | [`0x676979d17cd0278100d2502cf89520c872f84eb1`](https://sepolia.etherscan.io/address/0x676979d17cd0278100d2502cf89520c872f84eb1) |
| VGBP | [`0x315586083c500417b03bf50531ba2ceadda56b2a`](https://sepolia.etherscan.io/address/0x315586083c500417b03bf50531ba2ceadda56b2a) |

### 🇭🇰 Hong Kong Dollar (HKD)

| Ticker | Address |
|--------|---------|
| EHKD | [`0xa02a0109d2c620b84c74d6a39110b2bc0daf49e2`](https://sepolia.etherscan.io/address/0xa02a0109d2c620b84c74d6a39110b2bc0daf49e2) |
| HKDR | [`0x3155de7df9f42a22fab0cc934c20d1cad0b86d5b`](https://sepolia.etherscan.io/address/0x3155de7df9f42a22fab0cc934c20d1cad0b86d5b) |

### 🇮🇩 Indonesian Rupiah (IDR)

| Ticker | Address |
|--------|---------|
| IDRT | [`0xf295f26f7fe8a5a8a5c15ca56d8d571f891200dc`](https://sepolia.etherscan.io/address/0xf295f26f7fe8a5a8a5c15ca56d8d571f891200dc) |
| IDRX | [`0x89964830f5f397df2d4cc60b371dad76cb559d42`](https://sepolia.etherscan.io/address/0x89964830f5f397df2d4cc60b371dad76cb559d42) |
| XIDR | [`0x17ef7e1b45aa223ffbf46fef0814ae814d269b1f`](https://sepolia.etherscan.io/address/0x17ef7e1b45aa223ffbf46fef0814ae814d269b1f) |

### 🇮🇳 Indian Rupee (INR)

| Ticker | Address |
|--------|---------|
| ARC | [`0xc560f9895b482b6a2c8782bb63a4785a4e049eec`](https://sepolia.etherscan.io/address/0xc560f9895b482b6a2c8782bb63a4785a4e049eec) |
| TINR | [`0xfe4a6b8ed78b712db5b6171981b57ad981960dce`](https://sepolia.etherscan.io/address/0xfe4a6b8ed78b712db5b6171981b57ad981960dce) |

### 🇯🇵 Japanese Yen (JPY)

| Ticker | Address |
|--------|---------|
| DCJPY | [`0xa4dc7ff6b09212c159b48b53a25e3a3ed00ccb92`](https://sepolia.etherscan.io/address/0xa4dc7ff6b09212c159b48b53a25e3a3ed00ccb92) |
| EJPY | [`0xe0f33914a4fd88862b01e5b10614975ce98c5894`](https://sepolia.etherscan.io/address/0xe0f33914a4fd88862b01e5b10614975ce98c5894) |
| GYEN | [`0x013be8de57f99c1345f629fe4c8dd2a52c2a32a9`](https://sepolia.etherscan.io/address/0x013be8de57f99c1345f629fe4c8dd2a52c2a32a9) |
| JPYC | [`0x0b2dfe45ca948a5f75e358e4000ed0adf1142150`](https://sepolia.etherscan.io/address/0x0b2dfe45ca948a5f75e358e4000ed0adf1142150) |
| JPYR | [`0x9c970d0dd64d1ad3f7d887592bc240ae2817d0a5`](https://sepolia.etherscan.io/address/0x9c970d0dd64d1ad3f7d887592bc240ae2817d0a5) |
| JPYSC | [`0x6ae1d68f5c8471564d5d95a05aa4fb91654e64dc`](https://sepolia.etherscan.io/address/0x6ae1d68f5c8471564d5d95a05aa4fb91654e64dc) |
| JPYT | [`0xd34058cfc29fe38f74457cccf855753e8647f4fc`](https://sepolia.etherscan.io/address/0xd34058cfc29fe38f74457cccf855753e8647f4fc) |

### 🇰🇬 Kyrgyzstani Som (KGS)

| Ticker | Address |
|--------|---------|
| KGST | [`0x824c48e8872959a9c136085d19da7a253581ca42`](https://sepolia.etherscan.io/address/0x824c48e8872959a9c136085d19da7a253581ca42) |

### 🇰🇷 South Korean Won (KRW)

| Ticker | Address |
|--------|---------|
| KRW1 | [`0x0c1efdb7d048dfc02b9435760f2a41c13b591c62`](https://sepolia.etherscan.io/address/0x0c1efdb7d048dfc02b9435760f2a41c13b591c62) |
| KRWIN | [`0xf0ef33b2f297f11eaa7e2230f7e34f4f4e3f516a`](https://sepolia.etherscan.io/address/0xf0ef33b2f297f11eaa7e2230f7e34f4f4e3f516a) |
| KRWO | [`0x42d866e0b39cc320f2d97cfb59685e0e317d4a68`](https://sepolia.etherscan.io/address/0x42d866e0b39cc320f2d97cfb59685e0e317d4a68) |
| KRWQ | [`0xc494f59fe26eff51f53918dff26e3d9b57832dde`](https://sepolia.etherscan.io/address/0xc494f59fe26eff51f53918dff26e3d9b57832dde) |

### 🇲🇳 Mongolian Tögrög (MNT)

| Ticker | Address |
|--------|---------|
| MONT | [`0x6abec8295fa6b5d70032d0b32b1362165bf3b26d`](https://sepolia.etherscan.io/address/0x6abec8295fa6b5d70032d0b32b1362165bf3b26d) |

### 🇲🇽 Mexican Peso (MXN)

| Ticker | Address |
|--------|---------|
| EMXN | [`0x157b721dc5ca94eba01fc14bb8df97c645db61aa`](https://sepolia.etherscan.io/address/0x157b721dc5ca94eba01fc14bb8df97c645db61aa) |
| MEXT | [`0x38ee7acf67b572f50065aa85f7a759fb6a118220`](https://sepolia.etherscan.io/address/0x38ee7acf67b572f50065aa85f7a759fb6a118220) |
| MXNB | [`0x19ef1b6a3b58d162f8fa1f13bf7cf6ac68208335`](https://sepolia.etherscan.io/address/0x19ef1b6a3b58d162f8fa1f13bf7cf6ac68208335) |
| MXNE | [`0x4e30bd8725b1813adffa609e645e145db1ee3d07`](https://sepolia.etherscan.io/address/0x4e30bd8725b1813adffa609e645e145db1ee3d07) |
| MXNT | [`0x4ae448902f0a44022073069cdad627350f00e759`](https://sepolia.etherscan.io/address/0x4ae448902f0a44022073069cdad627350f00e759) |
| WMXN | [`0x6052065f4ba10db6cc5ec95f3aca534a7a376286`](https://sepolia.etherscan.io/address/0x6052065f4ba10db6cc5ec95f3aca534a7a376286) |

### 🇲🇾 Malaysian Ringgit (MYR)

| Ticker | Address |
|--------|---------|
| JMYR | [`0x06ae6920a16bc26eeb3e891f05066aa043023cd1`](https://sepolia.etherscan.io/address/0x06ae6920a16bc26eeb3e891f05066aa043023cd1) |
| MYRT | [`0x66d3b49b587b970b5835751708136adb805f1fe4`](https://sepolia.etherscan.io/address/0x66d3b49b587b970b5835751708136adb805f1fe4) |

### 🇳🇬 Nigerian Naira (NGN)

| Ticker | Address |
|--------|---------|
| CNGN | [`0x33d3c739c9ff714fd3d5c572eadaa1741902723d`](https://sepolia.etherscan.io/address/0x33d3c739c9ff714fd3d5c572eadaa1741902723d) |
| NGNC | [`0xebbfd43cafa24a7ff2885cc0606fc3e7d49f5ce8`](https://sepolia.etherscan.io/address/0xebbfd43cafa24a7ff2885cc0606fc3e7d49f5ce8) |

### 🇳🇿 New Zealand Dollar (NZD)

| Ticker | Address |
|--------|---------|
| ENZD | [`0x1c9c74c46afc6dd1b1745025383bc6f3d1b32314`](https://sepolia.etherscan.io/address/0x1c9c74c46afc6dd1b1745025383bc6f3d1b32314) |
| NZDD | [`0xb814325c3ca7a8d23fca2c7549eecae57a562067`](https://sepolia.etherscan.io/address/0xb814325c3ca7a8d23fca2c7549eecae57a562067) |
| NZDS | [`0x178c34f44866cb626bf50fb61d82f65da3f3e898`](https://sepolia.etherscan.io/address/0x178c34f44866cb626bf50fb61d82f65da3f3e898) |

### 🇵🇪 Peruvian Sol (PEN)

| Ticker | Address |
|--------|---------|
| PERT | [`0x0ea91143d33c8b0c92ade4ac53b68306074721a6`](https://sepolia.etherscan.io/address/0x0ea91143d33c8b0c92ade4ac53b68306074721a6) |
| WPEN | [`0x2975e931c8577bfaf90590f79393a72fb018a3ce`](https://sepolia.etherscan.io/address/0x2975e931c8577bfaf90590f79393a72fb018a3ce) |

### 🇵🇭 Philippine Peso (PHP)

| Ticker | Address |
|--------|---------|
| PHPC | [`0x95ec6e0f3dc06c141e8d2970aa7cd2993bb2c661`](https://sepolia.etherscan.io/address/0x95ec6e0f3dc06c141e8d2970aa7cd2993bb2c661) |
| PHPX | [`0x475affbcee14054989cdef14121f5f355512994b`](https://sepolia.etherscan.io/address/0x475affbcee14054989cdef14121f5f355512994b) |
| PHT | [`0x74a9cbc4d8ccd36c373d4640963a6ccdd38ebbf6`](https://sepolia.etherscan.io/address/0x74a9cbc4d8ccd36c373d4640963a6ccdd38ebbf6) |

### 🇵🇰 Pakistani Rupee (PKR)

| Ticker | Address |
|--------|---------|
| PKRND | [`0x88231bcc3ccd21edf1ed5bcd47c74c01fa79a3b4`](https://sepolia.etherscan.io/address/0x88231bcc3ccd21edf1ed5bcd47c74c01fa79a3b4) |

### 🇵🇾 Paraguayan Guaraní (PYG)

| Ticker | Address |
|--------|---------|
| PRYT | [`0xc4e1f54a26d645662b14441cb08e647a177e1d5f`](https://sepolia.etherscan.io/address/0xc4e1f54a26d645662b14441cb08e647a177e1d5f) |

### 🇸🇬 Singapore Dollar (SGD)

| Ticker | Address |
|--------|---------|
| ESGD | [`0x53e079bb5ce84c34ebd8a6064dc64feb7a20bd34`](https://sepolia.etherscan.io/address/0x53e079bb5ce84c34ebd8a6064dc64feb7a20bd34) |
| TNSGD | [`0x91655fce87ee180dc66ba85cacde453be5256beb`](https://sepolia.etherscan.io/address/0x91655fce87ee180dc66ba85cacde453be5256beb) |
| XSGD | [`0x058e06ca2628165a6cd1e80e4dc82203dc0020aa`](https://sepolia.etherscan.io/address/0x058e06ca2628165a6cd1e80e4dc82203dc0020aa) |

### 🇹🇭 Thai Baht (THB)

| Ticker | Address |
|--------|---------|
| THBK | [`0xcdd003626b02b0bdfaf11b1c9ae37d63d4fb73cb`](https://sepolia.etherscan.io/address/0xcdd003626b02b0bdfaf11b1c9ae37d63d4fb73cb) |
| THBT | [`0x677305e3c91a5e292c7ee2c6ccf63af981a13ca0`](https://sepolia.etherscan.io/address/0x677305e3c91a5e292c7ee2c6ccf63af981a13ca0) |

### 🇹🇷 Turkish Lira (TRY)

| Ticker | Address |
|--------|---------|
| TRYB | [`0x47ec6cb53eb6d4a85a57f33e0fc50d00569956c1`](https://sepolia.etherscan.io/address/0x47ec6cb53eb6d4a85a57f33e0fc50d00569956c1) |
| TRYT | [`0xf3d49275a0a54a00255e6b574ccfab6f3e5ccc2d`](https://sepolia.etherscan.io/address/0xf3d49275a0a54a00255e6b574ccfab6f3e5ccc2d) |

### 🇹🇿 Tanzanian Shilling (TZS)

| Ticker | Address |
|--------|---------|
| NTZS | [`0x32078bc7fb543f296942bbe8127a194d1eb1d4ba`](https://sepolia.etherscan.io/address/0x32078bc7fb543f296942bbe8127a194d1eb1d4ba) |

### 🇺🇸 US Dollar (USD)

| Ticker | Address |
|--------|---------|
| FDUSD | [`0x7903b295f14929a71a835c9ae9fd34eaf1adbf97`](https://sepolia.etherscan.io/address/0x7903b295f14929a71a835c9ae9fd34eaf1adbf97) |
| USDC | [`0x965d4b4546716e416e950bc30467d128455d2d0e`](https://sepolia.etherscan.io/address/0x965d4b4546716e416e950bc30467d128455d2d0e) |
| USDT | [`0x8365421d0e1b316fc6398d21be162992216bf2ad`](https://sepolia.etherscan.io/address/0x8365421d0e1b316fc6398d21be162992216bf2ad) |

### 🇺🇾 Uruguayan Peso (UYU)

| Ticker | Address |
|--------|---------|
| URYT | [`0x9621ba6ef4045014ea8877cb1c12e3f9565a7ac8`](https://sepolia.etherscan.io/address/0x9621ba6ef4045014ea8877cb1c12e3f9565a7ac8) |

### 🇻🇪 Venezuelan Bolívar (VES)

| Ticker | Address |
|--------|---------|
| VENT | [`0xfa55ae506f21812268eeb95e88319da4678516a2`](https://sepolia.etherscan.io/address/0xfa55ae506f21812268eeb95e88319da4678516a2) |

### 🇻🇳 Vietnamese Dong (VND)

| Ticker | Address |
|--------|---------|
| VNDC | [`0xd308f36845110b3b7c6d68e6b8495e56b6b9f3f6`](https://sepolia.etherscan.io/address/0xd308f36845110b3b7c6d68e6b8495e56b6b9f3f6) |

### 🇿🇦 South African Rand (ZAR)

| Ticker | Address |
|--------|---------|
| EZAR | [`0x111df0eed784d46fd546bcdaa6bc05e690c1879c`](https://sepolia.etherscan.io/address/0x111df0eed784d46fd546bcdaa6bc05e690c1879c) |
| ZARP | [`0xaf32e47292ce20c89ccb4aad4ce0d12b52ebdd19`](https://sepolia.etherscan.io/address/0xaf32e47292ce20c89ccb4aad4ce0d12b52ebdd19) |
| ZARSC | [`0x0d00a9bb8b976bf37de6de32802349d9df335759`](https://sepolia.etherscan.io/address/0x0d00a9bb8b976bf37de6de32802349d9df335759) |
| ZARU | [`0xe76bce709110713a94d6456474d0939e2c787ebd`](https://sepolia.etherscan.io/address/0xe76bce709110713a94d6456474d0939e2c787ebd) |

## Trading Pairs

Trading pairs on Sera are identified on-chain by the base and quote **token
contract addresses**. For display, each pair is rendered as
`BASE_SYMBOL/QUOTE_SYMBOL` using the ERC-20 tokens' display tickers — so a
Euro-vs-Dollar market between EURC and USDC shows as `EURC/USDC`, not
`EUR/USD`. Pair orientation is canonicalised server-side (smaller
lowercased address becomes base), and the display string reflects that
canonical orientation.

You can trade any combination of supported stablecoins. Common pairs include:

- **EURC/USDC** — Euro stablecoin vs US Dollar stablecoin
- **GBPA/USDC** — British Pound stablecoin vs US Dollar stablecoin
- **XSGD/USDC** — Singapore Dollar stablecoin vs US Dollar stablecoin
- **GYEN/USDC** — Japanese Yen stablecoin vs US Dollar stablecoin

Exact pair strings depend on which specific stablecoins are registered — a
deployment that lists EURT instead of EURC will render the Euro pair as
`EURT/USDC`. Query `GET /api/v1/markets` for the live list.

## Querying Available Tokens

Use the API to get the current list of supported tokens:

=== "cURL"

    ```bash
    curl https://api.testnet.sera.cx/api/v1/tokens
    ```

=== "Python"

    ```python
    import requests

    response = requests.get("https://api.testnet.sera.cx/api/v1/tokens")
    tokens = response.json()["tokens"]
    print(tokens)
    ```

=== "TypeScript"

    ```typescript
    const response = await fetch("https://api.testnet.sera.cx/api/v1/tokens");
    const { tokens } = await response.json();
    console.log(tokens);
    ```

---

# Order Types

- Canonical URL: https://docs.testnet.sera.cx/order-types/
- Source path: `docs/en/order-types.md`
- Description: Overview of the different order types available on Sera

# Order Types

Sera supports several order types to suit different trading strategies.

## Limit Orders

Limit orders let you specify the exact price at which you want to buy or sell. They sit on the order book until filled, cancelled, or expired.

- Requires **Vault deposit** — collateral is frozen when the order is placed
- Gas paid in **real ETH** at settlement time
- Supports partial fills
- Amend an order by cancelling it and submitting a new signed order

For the full guide, see [Limit Orders](limit-orders.md).

## Instant Swaps

Swaps execute immediately at the best available price. They are Fill-or-Kill — either the full amount executes or the swap is rejected.

- **No pre-funded Vault required** — the signed route can pull the exact input amount from your wallet at execution time
- **No ETH needed for gas** — gas is automatically factored into the swap quote
- Choose gas mode: `receive_less` (deducted from output) or `pay_more` (added to input)

For the full guide, see [Swap Trading](swaps.md).

## Virtual Liquidity Batches

VL batches group 2 to 50 limit orders across **distinct markets** under a single shared budget (active cap returned at runtime by `GET /config` under `limits.vl_batch`). The Vault freezes only the maximum individual order cost (not the sum), and when any sibling fills, the remaining siblings are automatically amended or cancelled to stay within budget. Exact duplicates and inverse pairs count as the same market and are rejected.

For the full guide, see [Virtual Liquidity](virtual-liquidity.md).

## Choosing the Right Order Type

| Scenario | Recommended |
|----------|-------------|
| You want a specific price | [Limit order](limit-orders.md) |
| You want immediate execution | [Swap](swaps.md) |
| You need guaranteed full fill | [Swap](swaps.md) (Fill-or-Kill) |
| You want partial fills | [Limit order](limit-orders.md) |
| You don't want to hold ETH for gas | [Swap](swaps.md) (gas included in quote) |
| You want multi-pair coverage with shared capital | [VL batch](virtual-liquidity.md) |

---

# Swap Trading

- Canonical URL: https://docs.testnet.sera.cx/swaps/
- Source path: `docs/en/swaps.md`
- Description: How to execute instant swaps on Sera

# Swap Trading

!!! info "Network & Compatibility"
    | Resource       | Value                                                       |
    |----------------|-------------------------------------------------------------|
    | API base URL   | `https://api.testnet.sera.cx/api/v1`                        |
    | Chain          | Sepolia (chainId `11155111`)                                |
    | Sera contract  | `0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e`                 |
    | Vault contract | `0x3c7945840bAE0d7e7f3824Ebccef1962629250F0`                 |
    | SOR contract   | `0x83c1368110B640A729f3810De5FBe94b99aa5668`                 |

    **Signing primitives.** Every trading mutation is an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed-data signature against the Sera domain. Deposits that take the permit path use the [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) `Permit` extension — supported by USDC, EURC, EURT, and most modern stablecoins, not all ERC-20s; call `GET /permit/metadata` to check support before signing. API-key management uses an EIP-712 `ManageApiKey` payload.

    **Tested clients.** Python `eth_account >= 0.10` + `requests`; TypeScript `ethers` v6 (`signer.signTypedData`). Browser wallets confirmed working with EIP-712 typed data: **MetaMask**, **Rabby**, **Frame**, **Coinbase Wallet**, **Trust**, **Rainbow**. Safe multisigs work via EIP-1271 (the message is verified on-chain rather than via ecrecover).

    **Address casing.** Read endpoints (`/balances`, `/orders`, `/fills`) treat `owner_address` as case-sensitive — pass the lowercase form. EIP-712 signed payloads accept EIP-55 checksum addresses.

Swaps provide instant execution for traders who want to exchange tokens immediately at the best available price. Unlike limit orders, swaps are Fill-or-Kill — they either execute in full or are rejected.

## Swap Flow

```mermaid
sequenceDiagram
    participant User
    participant API as Sera API
    participant Chain as Ethereum

    User->>API: POST /swap/quote
    API-->>User: Quote (uuid, route_params, quote_breakdown)
    User->>User: Sign route_params (EIP-712)
    User->>API: POST /swap (uuid + signature)
    API->>Chain: Settlement
    Chain-->>API: Confirmation
    API-->>User: Success (trade_id)
```

## Step 1: Request a Quote

=== "Python"

    ```python
    import time, requests

    quote = requests.post(
        "https://api.testnet.sera.cx/api/v1/swap/quote",
        json={
            "from_token":    "0x965d4b...d0e",   # USDC address
            "to_token":      "0xef64d1...40f",    # EURC address
            "from_amount":   "1000000000",      # 1000 USDC (6 decimals)
            "owner_address": "0xYOUR_WALLET",
            "recipient":     "0xYOUR_WALLET",   # may be a different address
            "expiration":    int(time.time()) + 3600,
            "gas_mode":      "receive_less",
        },
        timeout=10,
    ).json()
    # quote["uuid"]           — unique quote identifier
    # quote["route_params"]   — EIP-712 parameters to sign
    # quote["quote_breakdown"] — before/after gas amounts for display
    # quote["fee_breakdown"]   — legacy gas summary (gas_cost_usd, gas_cost_from_token)
    ```

=== "TypeScript"

    ```typescript
    const response = await fetch("https://api.testnet.sera.cx/api/v1/swap/quote", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        from_token:    "0x965d4b...d0e",   // USDC address
        to_token:      "0xef64d1...40f",    // EURC address
        from_amount:   "1000000000",      // 1000 USDC (6 decimals)
        owner_address: "0xYOUR_WALLET",
        recipient:     "0xYOUR_WALLET",   // may be a different address
        expiration:    Math.floor(Date.now() / 1000) + 3600,
        gas_mode:      "receive_less",
      }),
    });

    const quote = await response.json();
    // quote.uuid            — unique quote identifier
    // quote.route_params    — EIP-712 parameters to sign
    // quote.quote_breakdown — before/after gas amounts for display
    // quote.fee_breakdown   — legacy gas summary (gas_cost_usd, gas_cost_from_token)
    ```

!!! warning "Quotes are single-use"
  `POST /swap` atomically consumes the stored quote on the first valid submission. If execution fails after the quote is consumed, request a new quote instead of retrying the same `uuid`.

### Quote Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `from_token` | address | ERC-20 address of the input token |
| `to_token` | address | ERC-20 address of the output token |
| `from_amount` | string | Amount in raw token units (e.g., `"1000000000"` for 1000 USDC) |
| `owner_address` | address | Your wallet address |
| `recipient` | address | Where output tokens are delivered. Can be any address — set to a different wallet to send the swap output to a third party. |
| `expiration` | integer | Unix timestamp deadline |
| `gas_mode` | string | `"receive_less"` or `"pay_more"` |

### Quote Response

The response includes:

- **`uuid`** — A unique identifier for this quote (used when submitting)
- **`route_params`** — The EIP-712 Intent struct fields to sign
- **`quote_breakdown`** — Display-only before/after gas amounts in raw token units
- **`fee_breakdown`** — Legacy gas summary: `gas_cost_usd` and `gas_cost_from_token`
- **`expires_at`** — When the quote expires (quotes are one-time use)

## Step 2: Sign the Quote

Sign the `route_params` using EIP-712 typed data signing with your wallet:

=== "Python"

    ```python
    from eth_account import Account
    from eth_account.messages import encode_typed_data

    DOMAIN = {
        "name": "Sera",
        "version": "1",
        "chainId": 11155111,                                              # Sepolia
        "verifyingContract": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",  # Sera.sol
    }

    INTENT_TYPES = {
        "Intent": [
            {"name": "taker",                "type": "address"},
            {"name": "inputToken",           "type": "address"},
            {"name": "outputToken",          "type": "address"},
            {"name": "maxInputAmount",       "type": "uint256"},
            {"name": "minOutputAmount",      "type": "uint256"},
            {"name": "recipient",            "type": "address"},
            {"name": "initialDepositAmount", "type": "uint256"},
            {"name": "uuid",                 "type": "uint256"},
            {"name": "deadline",             "type": "uint48"},
        ]
    }

    # Sign quote["route_params"] exactly as returned by POST /swap/quote
    signable  = encode_typed_data(DOMAIN, INTENT_TYPES, quote["route_params"])
    signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
    ```

=== "TypeScript"

    ```typescript
    import { Wallet } from "ethers";

    const DOMAIN = {
      name: "Sera",
      version: "1",
      chainId: 11155111,                                                  // Sepolia
      verifyingContract: "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",     // Sera.sol
    };

    const INTENT_TYPES = {
      Intent: [
        { name: "taker",                type: "address" },
        { name: "inputToken",           type: "address" },
        { name: "outputToken",          type: "address" },
        { name: "maxInputAmount",       type: "uint256" },
        { name: "minOutputAmount",      type: "uint256" },
        { name: "recipient",            type: "address" },
        { name: "initialDepositAmount", type: "uint256" },
        { name: "uuid",                 type: "uint256" },
        { name: "deadline",             type: "uint48"  },
      ],
    };

    // Sign quote.route_params exactly as returned by POST /swap/quote
    const signature = await signer.signTypedData(DOMAIN, INTENT_TYPES, quote.route_params);
    ```

## Step 3: Execute the Swap

Submit the signed quote:

=== "Python"

    ```python
    swap = requests.post(
        "https://api.testnet.sera.cx/api/v1/swap",
        json={"uuid": quote["uuid"], "signature": "0x" + signature.lstrip("0x")},
        timeout=10,
    ).json()
    # swap["success"]  — whether the swap was accepted
    # swap["trade_id"] — unique trade identifier for tracking
    ```

=== "TypeScript"

    ```typescript
    const result = await fetch("https://api.testnet.sera.cx/api/v1/swap", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ uuid: quote.uuid, signature }),
    });

    const swap = await result.json();
    // swap.success  — whether the swap was accepted
    // swap.trade_id — unique trade identifier for tracking
    ```

## Gas Modes

Unlike limit orders (where you pay gas in real ETH), swap gas costs are **automatically factored into the quote** by the server. You do not need to hold ETH to execute a swap — the gas is absorbed into the token amounts.

When requesting a quote, you choose how the gas cost is applied:

| Mode | Behavior |
|------|----------|
| `receive_less` | Gas cost is deducted from output. You spend exactly `from_amount`, but receive slightly less. |
| `pay_more` | Gas cost is added to input. You receive the full quoted amount, but spend slightly more. |

The `quote_breakdown` in the quote response shows the before-gas amount, gas amount, and after-gas amount in raw token units. Use it for frontend display. The `fee_breakdown` field remains available as a legacy gas summary in USD and input-token display units.

Your frontend should sign `route_params` exactly as returned. Do not recompute signed amounts from display fields.

## Pricing Adjustments

Some same-currency swaps, such as USDC↔USDT, may be quoted without a cross-currency FX adjustment because both tokens track the same fiat currency. For all swaps, the public quote response exposes the signed amounts and gas display math only.

## Multi-Leg Routing

Sera automatically finds optimal routes for your swap. If a direct pair doesn't exist or a multi-hop path offers better pricing, the swap routes through intermediate currencies transparently.

For example, a **GBP → SGD** swap might execute as:

1. GBP → USD
2. USD → SGD

This happens atomically — either all legs succeed or none do.

## Why MEV Is Not an Issue

Sera swaps are quote-first. You do not broadcast an open-ended market order for public price discovery.

That means users are not exposing an open-ended market order to the public mempool for price discovery. Instead, you request a quote, sign the exact `route_params`, and settlement either executes within the signed bounds or fails.

The signed Intent includes `maxInputAmount`, `minOutputAmount`, a one-time `uuid`, and a `deadline`. Because the trade is not being discovered and repriced in the public mempool, the usual sandwich-attack vector does not exist.

## Error Handling

Every 4xx response from `POST /swap` returns a typed envelope:

```json
{
  "detail": {
    "detail": "Quote was rejected; request a fresh quote",
    "error_code": "QUOTE_STALE"
  }
}
```

Branch on `error_code` for routing logic; the inner `detail` is a human-readable string for display only. The full code list is documented in the [Swap endpoints reference](api-reference/endpoints/swaps.md#error-envelope).

| HTTP Status | Meaning |
|-------------|---------|
| 200 | Swap accepted and processing |
| 400 | Invalid request, signature mismatch, missing permit fields, or non-executable quote |
| 409 | `error_code: "QUOTE_STALE"` — the wallet's pending swap state changed between quote and submit, or the quote can no longer be accepted. The quote is **not** consumed; silently re-quote and re-submit |
| 410 | Quote expired or already consumed (request a new quote) |
| 429 | Rate limit exceeded (wait and retry) |
| 503 | Service temporarily unavailable |

---

# The Vault

- Canonical URL: https://docs.testnet.sera.cx/vault/
- Source path: `docs/en/vault.md`
- Description: Understanding Sera's non-custodial Vault smart contract

# The Vault

The Vault is an on-chain smart contract that holds deposited tokens with per-user ledger balances. It is the foundation of trading on Sera.

## Non-Custodial Design

The Vault is **fully non-custodial**. Your tokens remain in the smart contract under your address — Sera's off-chain services (order matching, API) never hold or control your funds. All deposits, freezes, settlements, and withdrawals are enforced entirely on-chain. Every action requires your EIP-712 signature, which means you can review exactly what you're authorizing (tokens, amounts, recipient) in your wallet before approving.

## Why the Vault Exists

The Vault exists to **guarantee settlement for every participant**. When you trade on Sera, you want certainty that when your order is matched, the other side can actually pay. The Vault provides this guarantee — because both parties have pre-deposited funds, settlement always succeeds.

Without the Vault, tokens would need to be pulled from wallets at settlement time. If the counterparty's wallet no longer had sufficient funds, the trade would fail — leaving you with a matched order that can't settle. By requiring pre-deposited collateral, the Vault ensures that every matched order settles immediately and atomically on-chain. This is what makes Sera's order book reliable for all participants.

!!! note
    Swaps do not require a Vault deposit — tokens are handled at execution time. The Vault is only required for limit orders and Virtual Liquidity batches.

## Balance Types

| Balance | Description |
|---------|-------------|
| **Wallet balance** | Tokens in your Ethereum wallet (not yet deposited) |
| **Vault available** | Tokens in the Vault, ready to be used for new orders |
| **Vault frozen** | Tokens locked in open orders — still in the Vault contract under your address, not held by Sera |
| **Vault total** | Available + frozen |

You can check all balances via `GET /balances`.

## Deposit & Withdraw

**Depositing** moves tokens from your wallet into the Vault contract. The API builds unsigned transactions for you — your frontend signs and broadcasts them. See [Deposit](api-reference/endpoints/account.md#deposit) for the full flow.

**Withdrawing** moves tokens from the Vault back to your wallet using a dual-signature instant withdrawal. See [Withdraw](api-reference/endpoints/account.md#withdraw-co-signature) for details.

## Emergency Withdrawal

If Sera's API ever becomes unavailable, you can always recover your funds — including frozen balances — by calling `emergencyWithdraw()` directly on the Sera smart contract. This is a two-step process with a ~24 hour delay to prevent abuse, but it guarantees you can always access your tokens without relying on any off-chain service.

See [Emergency Withdrawal](contracts/sera.md#emergencywithdraw) for details.

---

# Limit Orders

- Canonical URL: https://docs.testnet.sera.cx/limit-orders/
- Source path: `docs/en/limit-orders.md`
- Description: How to place and manage limit orders on Sera

# Limit Orders

Limit orders let you specify the exact price at which you want to buy or sell. The order sits on the order book until it is filled, cancelled, or expires.

## Prerequisites

Before placing a limit order, you need:

1. **Tokens deposited in the [Vault](vault.md)** — Both you and your counterparty pre-deposit into the Vault, guaranteeing that every matched order settles. See [The Vault](vault.md) to understand why this is needed and how it works.
2. **ETH in your wallet** — Limit order settlement happens on-chain, and gas is paid in real ETH (unlike swaps, where gas is included in the quote).

## Bid (Buy Order)

A bid order specifies:

- **Price** — The maximum price you're willing to pay per unit
- **Amount** — The quantity you want to buy
- **Pair identity** — `from_address` is the market base token (what you want to buy) and `to_address` is the market quote token (what prices the market)

Example: "Buy 1000 XSGD at 0.7450 USDC per XSGD"

## Ask (Sell Order)

An ask order specifies:

- **Price** — The minimum price you're willing to accept per unit
- **Amount** — The quantity you want to sell
- **Pair identity** — `from_address` is still the market base token and `to_address` is still the market quote token

Example: "Sell 500 XSGD at 0.7460 USDC per XSGD"

For every limit order:

- `bid` spends `to_address` to buy `from_address`
- `ask` spends `from_address` to sell into `to_address`
- Use the token contract addresses from `GET /tokens`; human-readable pair labels come from `GET /markets`

## Amount and Price Precision

Each market publishes its accepted order-entry grid in `GET /markets`:

- `quantity_precision` / `amount_step` for `amount`
- `tick_precision` / `price_step` for `price`
- `rounding_mode: "reject_extra_precision"`

Use decimal strings, not floating-point numbers, when building orders. The final
`POST /orders` call rejects extra precision and non-canonical strings before
signature verification. Integrations should call `POST /orders/preview`, show
the returned canonical `amount`, `price`, notional, and raw signed amounts to
the user, then submit the final order with exactly those normalized values.

## How Collateral Works

Collateral is managed entirely by the **on-chain Vault smart contract** — Sera's off-chain services never hold or control your funds.

When you place a limit order:

1. The required collateral is **frozen** in the Vault smart contract on-chain — it remains in the contract under your address, not transferred to Sera
2. When the order is filled, proceeds are **credited** to your Vault balance on-chain via the settlement contract
3. If you cancel, the frozen collateral is **released** back to your available Vault balance

Even while frozen, your tokens stay in the Vault contract and can be recovered via [emergency withdrawal](contracts/sera.md#emergencywithdraw) if Sera's off-chain services become unavailable.

You can check your available and frozen balances via `GET /balances`.

## Gas Costs

Limit orders are settled on-chain when matched. You are responsible for paying gas in **real ETH** — unlike swaps, gas is not abstracted away. Make sure you have ETH in your wallet to cover settlement transaction fees.

!!! note
    On Sepolia testnet, you can get free test ETH from a [faucet](https://sepoliafaucet.com).

## Order Expiration

Orders must include an expiration timestamp — a future Unix timestamp after which the order can no longer be matched.

- `expiration` is required on every signed order
- the API rejects `expiration <= now`
- the API also rejects values beyond 365 days minus a 300-second clock-skew guard from current server time

Use `GET /system/time` when you build the order and leave a small client-side buffer instead of signing right at the limit.

## Cancelling Orders

Orders can be cancelled at any time if they have not been fully filled. Cancelling a partially filled order returns the unfilled portion to your Vault balance.

- **Single cancel** — `POST /orders/cancel` with an EIP-712 CancelOrder signature
- **Cancel all** — `DELETE /orders/cancel-all` (requires API key)

!!! note
    Orders are typically subject to a ~5-minute cancel cooldown after placement; hitting it returns `429`. The policy is server-side and may be relaxed for high-volume accounts.

## API Reference

| Action | Endpoint | Auth |
|--------|----------|------|
| Preview order | `POST /orders/preview` | None |
| Place order | `POST /orders` | EIP-712 signature |
| Cancel order | `POST /orders/cancel` | EIP-712 signature |
| Cancel all | `DELETE /orders/cancel-all` | API Key |
| Get order | `GET /orders/{id}` | API Key |
| List orders | `GET /orders` | API Key |

See [Order Endpoints](api-reference/endpoints/orders.md) for full request/response details.

---

# Virtual Liquidity

- Canonical URL: https://docs.testnet.sera.cx/virtual-liquidity/
- Source path: `docs/en/virtual-liquidity.md`
- Description: Place orders across multiple trading pairs with a single shared budget

# Virtual Liquidity

!!! info "Network & Compatibility"
    | Resource       | Value                                                       |
    |----------------|-------------------------------------------------------------|
    | API base URL   | `https://api.testnet.sera.cx/api/v1`                        |
    | Chain          | Sepolia (chainId `11155111`)                                |
    | Sera contract  | `0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e`                 |
    | Vault contract | `0x3c7945840bAE0d7e7f3824Ebccef1962629250F0`                 |
    | SOR contract   | `0x83c1368110B640A729f3810De5FBe94b99aa5668`                 |

    **Signing primitives.** Every trading mutation is an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed-data signature against the Sera domain. Deposits that take the permit path use the [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) `Permit` extension — supported by USDC, EURC, EURT, and most modern stablecoins, not all ERC-20s; call `GET /permit/metadata` to check support before signing. API-key management uses an EIP-712 `ManageApiKey` payload.

    **Tested clients.** Python `eth_account >= 0.10` + `requests`; TypeScript `ethers` v6 (`signer.signTypedData`). Browser wallets confirmed working with EIP-712 typed data: **MetaMask**, **Rabby**, **Frame**, **Coinbase Wallet**, **Trust**, **Rainbow**. Safe multisigs work via EIP-1271 (the message is verified on-chain rather than via ecrecover).

    **Address casing.** Read endpoints (`/balances`, `/orders`, `/fills`) treat `owner_address` as case-sensitive — pass the lowercase form. EIP-712 signed payloads accept EIP-55 checksum addresses.

Virtual Liquidity (VL) lets you place orders across multiple trading pairs while guaranteeing they collectively never exceed a single shared budget. Instead of locking up capital for each order independently, a VL batch shares one pool of collateral — so you can provide liquidity on many pairs without multiplying your capital requirements.

## Why Virtual Liquidity?

Consider a market maker who wants to bid on **EURC/USDC**, **GBPC/USDC**, and **XSGD/USDC** simultaneously. Without VL, each order requires its own locked collateral — 3 orders means 3x the capital. With VL, a single USDC budget backs all three orders. When one fills, the remaining orders are automatically adjusted to stay within budget.

| Approach | Capital Required | Risk |
|----------|-----------------|------|
| 3 separate orders | Sum of all order costs | Each order locks independently |
| 1 VL batch | Max of any single order cost | Shared budget, automatic adjustment |

## How It Works

A VL batch consists of:

- **One shared budget** — A maximum spend denominated in a single "spent token"
- **Multiple sibling orders** — 2 to 50 limit orders, each targeting a **different market** (active cap returned at runtime by `GET /config` under `limits.vl_batch`)
- **Automatic amendment** — When one sibling fills, the remaining siblings are resized or cancelled to fit the remaining budget

!!! note "Unique market rule"
  VL batches reject both exact duplicates and inverse duplicates. For example, `XSGD/USDC` and `USDC/XSGD` are treated as the same market and cannot appear in the same batch.

```mermaid
flowchart TD
    A[Place VL Batch] --> B[Freeze shared budget]
    B --> C1[Sibling 1: EURC/USDC Bid]
    B --> C2[Sibling 2: GBPC/USDC Bid]
    B --> C3[Sibling 3: XSGD/USDC Bid]
    C1 -->|Fills| D[Deduct from budget]
    D --> E{Budget remaining?}
    E -->|Yes| F[Amend siblings 2 & 3]
    E -->|No| G[Cancel siblings 2 & 3]
```

## Shared Budget & Collateral

The key insight behind VL is that sibling orders are on **different trading pairs**, so at most one can match at any given moment. This means the vault only needs to freeze the **maximum** individual order cost, not the sum.

### Spent Token (fromToken) Rules

All siblings in a VL batch must resolve to the **same `fromToken`** — the ERC-20 token being spent. `fromToken` is derived from `side` plus the pair definition (`from_address` = base token, `to_address` = quote token):

| Side | fromToken (spent) | toToken (received) | Cost Per Fill |
|------|-------------------|--------------------|---------------|
| Bid (Buy) | `to_address` (quote token) | `from_address` (base token) | `quantity x fill_price` |
| Ask (Sell) | `from_address` (base token) | `to_address` (quote token) | `quantity` |

!!! warning "No Auto-Correction"
  The system does **not** auto-flip `side` or swap pair orientation to make `fromToken` match across siblings. `fromToken` is computed from the submitted `side`, `from_address`, and `to_address`, and the entire batch is rejected with `422` if the derived spent tokens do not match.

**Example 1: Same-side batch (all bids spending USDC)**

- Bid `from_address: EURC`, `to_address: USDC` @ 1.08 — fromToken = USDC
- Bid `from_address: GBPC`, `to_address: USDC` @ 1.27 — fromToken = USDC
- Bid `from_address: XSGD`, `to_address: USDC` @ 0.75 — fromToken = USDC

Frozen amount: **1,500 USDC** (the maximum single order cost), not 3,215 USDC.

**Example 2: Mixed-side batch (both spending USDC)**

When USDC appears as quote in one pair but base in another, you can still batch them — as long as the derived `fromToken` resolves to USDC on both:

- Bid `from_address: MYRC`, `to_address: USDC` @ 4.50 — fromToken = **USDC** :white_check_mark:
- Ask `from_address: USDC`, `to_address: GBPC` @ 0.79 — fromToken = **USDC** :white_check_mark:

This is valid because both orders spend USDC.

**Example 3: Invalid mixed-side batch (mismatched fromToken)**

- Ask `from_address: MYRC`, `to_address: USDC` @ 4.50 — fromToken = **MYRC** :x:
- Ask `from_address: GBPC`, `to_address: USDC` @ 1.27 — fromToken = **GBPC** :x:

Even though both markets are quoted in USDC, one sibling spends MYRC and the other spends GBPC. The batch is **rejected with 422**.

## Placing a VL Batch

Submit all sibling orders together via `POST /orders/vl/batch`. Each sibling carries its own signed `order_id` / `uuid_int` / signature — see the [Market Maker Guide](api-reference/market-maker-guide.md#tutorial-4-multi-pair-quoting-with-a-virtual-liquidity-batch) for the full signing flow.

=== "Python"

    ```python
    import time, requests

    expiry = int(time.time()) + 86_400
    batch = requests.post(
        "https://api.testnet.sera.cx/api/v1/orders/vl/batch",
        json={"orders": [
            {
                "owner_address": wallet_address,
                "side": "bid", "amount": "1000.0", "price": "1.08",
                "order_type": "limit",
                "from_address": EURC_ADDRESS, "to_address": USDC_ADDRESS,
                "order_id": eur_order_id, "uuid_int": eur_uuid_int,
                "signature": eur_sig, "expiration": expiry,
            },
            {
                "owner_address": wallet_address,
                "side": "bid", "amount": "500.0", "price": "1.27",
                "order_type": "limit",
                "from_address": GBPC_ADDRESS, "to_address": USDC_ADDRESS,
                "order_id": gbp_order_id, "uuid_int": gbp_uuid_int,
                "signature": gbp_sig, "expiration": expiry,
            },
            {
                "owner_address": wallet_address,
                "side": "bid", "amount": "2000.0", "price": "0.75",
                "order_type": "limit",
                "from_address": XSGD_ADDRESS, "to_address": USDC_ADDRESS,
                "order_id": sgd_order_id, "uuid_int": sgd_uuid_int,
                "signature": sgd_sig, "expiration": expiry,
            },
        ]},
        timeout=10,
    ).json()
    # batch["order_ids"]  — every sibling's order_id (always populated)
    # batch["amendments"] — legs clipped at placement to fit max_budget
    # batch["cancelled"]  — legs dropped because the budget was already exhausted
    # batch["fills"]      — immediate fills that landed at placement time
    # batch["vl_group"]   — authoritative budget snapshot { max_budget, budget_consumed, spent_token }
    ```

=== "TypeScript"

    ```typescript
    const expiry = Math.floor(Date.now() / 1000) + 86_400;
    const response = await fetch("https://api.testnet.sera.cx/api/v1/orders/vl/batch", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        orders: [
          {
            owner_address: walletAddress,
            side: "bid", amount: "1000.0", price: "1.08",
            order_type: "limit",
            from_address: EURC_ADDRESS, to_address: USDC_ADDRESS,
            order_id: eurOrderId, uuid_int: eurUuidInt,
            signature: eurSig, expiration: expiry,
          },
          {
            owner_address: walletAddress,
            side: "bid", amount: "500.0", price: "1.27",
            order_type: "limit",
            from_address: GBPC_ADDRESS, to_address: USDC_ADDRESS,
            order_id: gbpOrderId, uuid_int: gbpUuidInt,
            signature: gbpSig, expiration: expiry,
          },
          {
            owner_address: walletAddress,
            side: "bid", amount: "2000.0", price: "0.75",
            order_type: "limit",
            from_address: XSGD_ADDRESS, to_address: USDC_ADDRESS,
            order_id: sgdOrderId, uuid_int: sgdUuidInt,
            signature: sgdSig, expiration: expiry,
          },
        ],
      }),
    });

    const batch = await response.json();
    // batch.order_ids   — every sibling's order_id (always populated)
    // batch.amendments  — legs clipped at placement to fit max_budget
    // batch.cancelled   — legs dropped because the budget was already exhausted
    // batch.fills       — immediate fills that landed at placement time
    // batch.vl_group    — authoritative budget snapshot { max_budget, budget_consumed, spent_token }
    ```

Each sibling still needs its own valid `expiration`, and the same bounded future rule from standard limit orders applies.

### Placement Outcomes

The response distinguishes four placement outcomes for every leg. They are not mutually exclusive — a single placement can simultaneously rest some legs, clip another, drop a third, and immediately fill a fourth.

| Outcome | Meaning |
|---------|---------|
| **Resting at signed size** | Leg appears in `order_ids` only. Fully on the book at the signed `amount`. |
| **Amended** | Leg appears in both `order_ids` and `amendments`. The engine clipped `original_amount` → `actual_amount` so the batch fits the shared `max_budget`. The leg is live on the book at `actual_amount`. |
| **Cancelled at placement** | Leg appears in both `order_ids` and `cancelled`. The shared budget was already spent by an earlier-priority sibling, so the leg never enters the book. `reason` is `"quota_exceeded"`. |
| **Immediate fill** | Leg appears in `order_ids` and `fills`. The leg crossed at least one resting maker as it was placed. `remaining` is the leg's `left_amount` after the immediate fills; the leg may still be live on the book if `remaining > 0`. A leg crossing multiple makers produces multiple `trades[]` entries. |

`vl_group.max_budget − vl_group.budget_consumed` is the budget that future fills can still draw from. It will already reflect any `fills` returned in the same response.

When every leg rests at its signed size with no immediate fill, `amendments`, `cancelled`, and `fills` are all empty lists.

## Fill & Amendment Flow

When a sibling fills, the system automatically adjusts the rest of the batch:

```mermaid
sequenceDiagram
    participant Sera
    participant Vault

    Sera->>Sera: Sibling 1 fills (500 USD consumed)
    Sera->>Sera: Budget: 1500 → 1000 USD remaining
    alt Budget sufficient
        Sera->>Sera: Amend remaining siblings to fit 1000 USD
    else Budget exhausted
        Sera->>Sera: Cancel remaining siblings
    end
    Sera->>Vault: Update frozen balance
```

### Amendment Example

Starting budget: **1,500 USD**

1. Sibling 1 (EURC/USDC Bid 1000 @ 1.08) partially fills 500 units — consumes **540 USDC**
2. Remaining budget: **960 USDC**
3. Sibling 2 (GBPC/USDC Bid 500 @ 1.27) is amended: new quantity = floor(960 / 1.27) = **755**
4. Sibling 3 (XSGD/USDC Bid 2000 @ 0.75) is amended: new quantity = floor(960 / 0.75) = **1280**

Each sibling is independently capped to fit within the remaining budget.

## Cancelling a VL Batch

Cancel all siblings in a batch with `POST /orders/vl/cancel`:

=== "Python"

    ```python
    cancel_res = requests.post(
        "https://api.testnet.sera.cx/api/v1/orders/vl/cancel",
        json={
            "owner_address": wallet_address,
            "vl_batch_id":   batch["order_ids"][0],   # the batch's primary order ID
            "signature":     cancel_vl_sig,           # EIP-712 CancelVLBatch signature
        },
        timeout=10,
    )
    ```

=== "TypeScript"

    ```typescript
    const cancelRes = await fetch("https://api.testnet.sera.cx/api/v1/orders/vl/cancel", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        owner_address: walletAddress,
        vl_batch_id:   batch.order_ids[0],   // the batch's primary order ID
        signature:     cancelVlSig,          // EIP-712 CancelVLBatch signature
      }),
    });
    ```

Cancellation behavior:

- **Cancel one sibling** (via `POST /orders/cancel`) — The budget is **not** unfrozen (other siblings are still resting)
- **Cancel entire batch** (via `POST /orders/vl/cancel`) — All siblings are cancelled and the remaining budget is unfrozen
- **All siblings fill or cancel** — Once no siblings remain active, any leftover budget is unfrozen

## Validation Rules

VL batches are validated at placement. The following constraints must be met:

| Rule | Reason |
|------|--------|
| All siblings target different markets | Prevents duplicate exposure; inverse pairs count as the same market |
| All siblings share the same spent token | Ensures a single budget can back all orders |
| 2 to 50 siblings per batch | Env-tunable cap; read `limits.vl_batch` from `GET /config` for the active value |
| Each sibling is a valid limit order | Standard order validation applies |

**Valid:**

- Bid EURC/USDC + Bid GBPC/USDC + Bid XSGD/USDC (all fromToken = USDC)
- Ask USDC/GBPC + Bid MYRC/USDC (mixed sides, but both fromToken = USDC)

**Invalid:**

- Bid EURC/USDC + Bid EURC/USDC (duplicate market)
- Bid XSGD/USDC + Ask USDC/XSGD (inverse duplicate market)
- Ask MYRC/USDC + Ask GBPC/USDC (fromToken = MYRC vs GBPC — mismatched, rejected with 422)

## Next Steps

<div class="grid cards" markdown>

-   :material-format-list-bulleted:{ .lg .middle } **[Order Types](order-types.md)**

    ---

    Learn about limit orders and swaps

-   :material-swap-horizontal:{ .lg .middle } **[Order Lifecycle](order-lifecycle.md)**

    ---

    Understand how orders move through states

-   :material-cash:{ .lg .middle } **[Fees & Costs](fees.md)**

    ---

    Fee structure for VL and standard orders

</div>

---

# Order Lifecycle

- Canonical URL: https://docs.testnet.sera.cx/order-lifecycle/
- Source path: `docs/en/order-lifecycle.md`
- Description: Complete walkthrough of an order from placement to settlement

# Order Lifecycle

!!! info "Network & Compatibility"
    | Resource       | Value                                                       |
    |----------------|-------------------------------------------------------------|
    | API base URL   | `https://api.testnet.sera.cx/api/v1`                        |
    | Chain          | Sepolia (chainId `11155111`)                                |
    | Sera contract  | `0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e`                 |
    | Vault contract | `0x3c7945840bAE0d7e7f3824Ebccef1962629250F0`                 |
    | SOR contract   | `0x83c1368110B640A729f3810De5FBe94b99aa5668`                 |

    **Signing primitives.** Every trading mutation is an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed-data signature against the Sera domain. Deposits that take the permit path use the [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) `Permit` extension — supported by USDC, EURC, EURT, and most modern stablecoins, not all ERC-20s; call `GET /permit/metadata` to check support before signing. API-key management uses an EIP-712 `ManageApiKey` payload.

    **Tested clients.** Python `eth_account >= 0.10` + `requests`; TypeScript `ethers` v6 (`signer.signTypedData`). Browser wallets confirmed working with EIP-712 typed data: **MetaMask**, **Rabby**, **Frame**, **Coinbase Wallet**, **Trust**, **Rainbow**. Safe multisigs work via EIP-1271 (the message is verified on-chain rather than via ecrecover).

    **Address casing.** Read endpoints (`/balances`, `/orders`, `/fills`) treat `owner_address` as case-sensitive — pass the lowercase form. EIP-712 signed payloads accept EIP-55 checksum addresses.

This tutorial walks through the complete lifecycle of a limit order on Sera — from placing the order to claiming proceeds.

## Overview

```mermaid
sequenceDiagram
    participant User
    participant API as Sera API
    participant Chain as Ethereum

    User->>API: POST /orders (signed order)
    API-->>User: order_id
    Note over API: Order is matched<br/>when counterparty found
    API->>Chain: Settlement on-chain
    User->>API: GET /orders/{id}
    API-->>User: Status: settled
    Note over User: Proceeds available<br/>in vault balance
```

## Step 1: Check Server Time

Before creating signatures, sync with the server clock to avoid expiration issues:

=== "Python"

    ```python
    import requests

    time_res = requests.get("https://api.testnet.sera.cx/api/v1/system/time")
    server_time = time_res.json()["timestamp"]
    print(f"Server time: {server_time}")
    ```

=== "TypeScript"

    ```typescript
    const timeRes = await fetch("https://api.testnet.sera.cx/api/v1/system/time");
    const { timestamp } = await timeRes.json();
    console.log("Server time:", timestamp);
    ```

  Every signed order requires `expiration`, and the API enforces `now < expiration <= now + 365 days - 300 seconds`. Use server time, not only the browser clock, when you derive that field.

## Step 2: Query Available Tokens

Get the list of supported tokens to find the contract addresses you need:

=== "Python"

    ```python
    tokens = requests.get("https://api.testnet.sera.cx/api/v1/tokens").json()["tokens"]

    # Find USDC and EURC addresses
    usdc = next(t for t in tokens if t["symbol"] == "USDC")
    eurc = next(t for t in tokens if t["symbol"] == "EURC")
    ```

=== "TypeScript"

    ```typescript
    const tokensRes = await fetch("https://api.testnet.sera.cx/api/v1/tokens");
    const { tokens } = await tokensRes.json();

    // Find USDC and EURC addresses
    const usdc = tokens.find((t: any) => t.symbol === "USDC");
    const eurc = tokens.find((t: any) => t.symbol === "EURC");
    ```

## Step 3: Place a Limit Order

Construct the order, preview the canonical EIP-712 payload, sign that previewed payload, then submit it. The full signing flow is documented under [Authentication](api-reference/authentication.md) and [Market Maker Guide → Place a Single Limit Order](api-reference/market-maker-guide.md#tutorial-1-place-a-single-limit-order). Excerpt:

=== "Python"

    ```python
    import time, uuid, requests

    # Generate a unique order ID
    order_id    = str(uuid.uuid4())
    executor_id = requests.get("https://api.testnet.sera.cx/api/v1/health").json()["executor_id"]
    raw         = int(uuid.UUID(order_id))
    group_id    = raw >> 16
    uuid_int    = str((executor_id << 252) | (raw << 124) | (group_id << 12))

    # Construct the order. from_address is the market base token and
    # to_address is the market quote token. Side decides which one is spent.
    order = {
        "owner_address": wallet_address,
        "side":          "bid",                # buy EURC with USDC
        "amount":        "1000",               # 1000 EURC
        "price":         "1.085",              # at 1.085 USDC per EURC
        "order_type":    "limit",
        "from_address":  EURC_ADDRESS,         # base token you want to buy
        "to_address":    USDC_ADDRESS,         # quote token you spend on a bid
        "order_id":      order_id,
        "uuid_int":      uuid_int,
        "expiration":    int(time.time()) + 86_400,
    }

    preview = requests.post(
        "https://api.testnet.sera.cx/api/v1/orders/preview",
        json=order,
        timeout=10,
    ).json()

    # Sign the previewed EIP-712 payload (see Authentication for full details)
    signature = sign_order_payload(signer, preview["eip712_order"], preview["eip712_types"])

    result = requests.post(
        "https://api.testnet.sera.cx/api/v1/orders",
        json={
            **order,
            "amount": preview["normalized_amount"],
            "price": preview["normalized_price"],
            "signature": signature,
        },
        timeout=10,
    ).json()
    print("Order placed:", result["order_id"])
    ```

=== "TypeScript"

    ```typescript
    import { v4 as uuidv4 } from "uuid";

    // Generate a unique order ID
    const orderId  = uuidv4();
    const { executor_id: executorId } = await fetch("https://api.testnet.sera.cx/api/v1/health")
      .then(r => r.json());
    const raw      = BigInt("0x" + orderId.replace(/-/g, ""));
    const groupId  = raw >> 16n;
    const uuidInt  = ((BigInt(executorId) << 252n) | (raw << 124n) | (groupId << 12n)).toString();

    // Construct the order. from_address is the market base token and
    // to_address is the market quote token. Side decides which one is spent.
    const order = {
      owner_address: walletAddress,
      side:          "bid",                  // buy EURC with USDC
      amount:        "1000",                 // 1000 EURC
      price:         "1.085",                // at 1.085 USDC per EURC
      order_type:    "limit",
      from_address:  EURC_ADDRESS,           // base token you want to buy
      to_address:    USDC_ADDRESS,           // quote token you spend on a bid
      order_id:      orderId,
      uuid_int:      uuidInt,
      expiration:    Math.floor(Date.now() / 1000) + 86_400,
    };

    const preview = await fetch("https://api.testnet.sera.cx/api/v1/orders/preview", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(order),
    }).then(r => r.json());

    // Sign the previewed EIP-712 payload (see Authentication for full details)
    const signature = await signOrderPayload(
      signer,
      preview.eip712_order,
      preview.eip712_types,
    );

    const response = await fetch("https://api.testnet.sera.cx/api/v1/orders", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        ...order,
        amount: preview.normalized_amount,
        price: preview.normalized_price,
        signature,
      }),
    });
    const result = await response.json();
    console.log("Order placed:", result.order_id);
    ```

## Step 4: Monitor Order Status

Use your API key to poll the order status:

=== "Python"

    ```python
    status = requests.get(
        f"https://api.testnet.sera.cx/api/v1/orders/{order_id}",
        headers={"Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET"},
        timeout=10,
    ).json()
    print("Status:", status["status"])
    # External statuses: "pending", "matched", "settled", "cancelled", "failed"
    print("Filled:", status["filled_amount"])
    print("Signed uuid_int:", status["uuid_int"])
    ```

=== "TypeScript"

    ```typescript
    const statusRes = await fetch(
      `https://api.testnet.sera.cx/api/v1/orders/${orderId}`,
      { headers: { "Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET" } },
    );
    const status = await statusRes.json();
    console.log("Status:", status.status);
    // External statuses: "pending", "matched", "settled", "cancelled", "failed"
    console.log("Filled:", status.filled_amount);
    console.log("Signed uuid_int:", status.uuid_int);
    ```

## Step 5: Check Your Balances

Once the order settles, proceeds appear in your vault balance:

=== "Python"

    ```python
    balances = requests.get(
        "https://api.testnet.sera.cx/api/v1/balances",
        params={"owner_address": wallet_address},
        headers={"Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET"},
        timeout=10,
    ).json()["balances"]

    for bal in balances:
        print(f"{bal['symbol']}: vault_raw={bal['vault_available']}, "
              f"frozen_raw={bal['vault_frozen']}, decimals={bal['decimals']}")
    ```

=== "TypeScript"

    ```typescript
    const balanceRes = await fetch(
      `https://api.testnet.sera.cx/api/v1/balances?owner_address=${walletAddress}`,
      { headers: { "Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET" } },
    );

    const { balances } = await balanceRes.json();
    for (const bal of balances) {
      console.log(`${bal.symbol}: vault_raw=${bal.vault_available}, ` +
                  `frozen_raw=${bal.vault_frozen}, decimals=${bal.decimals}`);
    }
    ```

`GET /balances` now returns raw uint256 decimal strings. Convert them with each row's `decimals` field before displaying human-readable balances.

## Step 6: Cancel an Order (Optional)

If you want to cancel an unfilled or partially filled order:

=== "Python"

    ```python
    # Sign a CancelOrder message
    cancel_signature = sign_cancel_order(signer, wallet_address, int(status["uuid_int"]))

    cancel_res = requests.post(
        "https://api.testnet.sera.cx/api/v1/orders/cancel",
        json={
            "owner_address": wallet_address,
            "order_id":      order_id,
            "uuid_int":      status["uuid_int"],
            "signature":     cancel_signature,
        },
        timeout=10,
    ).json()
    print("Cancelled:", cancel_res)
    ```

=== "TypeScript"

    ```typescript
    // Sign a CancelOrder message
    const cancelSignature = await signCancelOrder(signer, walletAddress, BigInt(status.uuid_int));

    const cancelRes = await fetch("https://api.testnet.sera.cx/api/v1/orders/cancel", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        owner_address: walletAddress,
        order_id:      orderId,
        uuid_int:      status.uuid_int,
        signature:     cancelSignature,
      }),
    });
    console.log("Cancelled:", await cancelRes.json());
    ```

!!! note
    Orders are typically subject to a ~5-minute cancel cooldown after placement; hitting it returns `429`. The policy is server-side and may be relaxed for high-volume accounts.

## Step 7: Withdraw (Optional)

To move funds from your vault back to your wallet, use the instant-withdraw flow:

1. `POST /withdraw` to obtain the executor co-signature.
2. `POST /withdraw/build` to get the unsigned `executeInstantWithdrawDualSig` transaction.
3. Sign that transaction locally and broadcast it with `POST /withdraw/send`.

If the API is unavailable, you can always fall back to the on-chain `emergencyWithdraw()` flow.

## Virtual Liquidity Orders

VL orders follow the same lifecycle as standard limit orders, but with shared budget management. When a VL sibling fills, the matching engine automatically amends or cancels the remaining siblings to stay within budget.

Key differences:

- **Placement** — Use `POST /orders/vl/batch` instead of `POST /orders`
- **Cancellation** — Use `POST /orders/vl/cancel` to cancel the entire batch, or `POST /orders/cancel` for individual siblings
- **Budget tracking** — The system tracks a shared frozen balance across all siblings; fills on one sibling reduce the budget available to others

See [Virtual Liquidity](virtual-liquidity.md) for the full guide.

## Order States Summary

| State | Meaning | Can Cancel? |
|-------|---------|-------------|
| `pending` | Submitted, resting on the book, or partially filled | Yes |
| `matched` | All legs crossed in the matching engine; on-chain settlement is in flight | No |
| `settled` | Fully filled and chain-confirmed | No |
| `cancelled` | Cancelled before full fill | No |
| `failed` | Rejected or settlement reverted | Usually no; inspect `error` and `error_code` |

`pending` can include a partially filled resting order. For progress displays,
do not rely on `status` alone: read `filled_base_amount`,
`filled_quote_amount`, `remaining_amount`, `settlement_summary`, and `/fills`.
Use `settlement_economics.balance_debits` and `balance_credits` for owner
balance-movement displays. Public `gross_debits` / `gross_credits` remain
compatibility fields and should not be treated as raw execution totals.

---

# Fees & Costs

- Canonical URL: https://docs.testnet.sera.cx/fees/
- Source path: `docs/en/fees.md`
- Description: Understanding fees on Sera

# Fees & Costs

## Swaps

Swap fees are quoted to you upfront. When you request a quote via `POST /swap/quote`, the returned amounts already incorporate every cost the swap will pay, so the value you sign is the value that settles. You do not pay anything outside of what the quote shows, and you do not need to hold ETH for gas — it is already included.

You choose how the cost is applied when you ask for the quote:

| Gas Mode | Description |
|----------|-------------|
| **`receive_less`** | Costs are deducted from your output amount — you spend exactly your input and receive slightly less |
| **`pay_more`** | Costs are added to your input amount — you receive the full quoted output and spend slightly more |

If you want a line-item view, the quote response includes `quote_breakdown` with before-gas amounts, gas amounts, and after-gas amounts in raw token units. The older `fee_breakdown` object remains available as a compact gas summary in USD and input-token display units.

Order and fill reads expose public, user-visible fee totals only.

## Limit Orders

Limit orders are settled on-chain when matched. You are responsible for paying gas in **real ETH** for the settlement transaction. Make sure your wallet has ETH before placing an order — without it, your matched fills cannot settle.

!!! note
    On Sepolia testnet, you can get free test ETH from a [faucet](https://sepoliafaucet.com).

## Withdrawals & Transfers

Deposits, withdrawals, and direct ERC-20 transfers built through the API produce a normal Ethereum transaction that you sign and broadcast yourself, so you pay the standard network gas fee for that transaction. There is no separate Sera charge on top of it.

---

# API Overview

- Canonical URL: https://docs.testnet.sera.cx/api-reference/
- Source path: `docs/en/api-reference/index.md`
- Description: Sera REST API reference

# API Overview

The Sera API is a versioned REST API for trading, balance queries, and transaction-building flows.

## Base URL

```
https://api.testnet.sera.cx/api/v1
```

## Content Type

All request and response bodies use JSON.

## Authentication

| Method | Used For | How It Is Sent |
|--------|----------|----------------|
| **API Key** | Read endpoints and transaction-building helpers | `Authorization: Bearer {api_key}:{api_secret}` |
| **EIP-712 Signature** | Trading mutations, cancellations, withdrawals, API-key management | Signature fields in the request body or query string |

See [Authentication](authentication.md) for the EIP-712 domain, typed-data payloads, and `uuid_int` rules.

## Rate Limits

Public per-client throttling is enforced at the edge proxy/CDN.

Requests authenticated with an API key are additionally rate limited per wallet inside the application:

| Group | Typical Endpoints | Limit |
|-------|-------------------|-------|
| `read` | `GET /orders`, `GET /balances`, `GET /fills` | 10 req/s |
| `trade` | `POST /orders`, `POST /swap` | 5 req/s |
| `cancel` | `POST /orders/cancel`, `DELETE /orders/cancel-all` | 2 req/s |
| `transfer` | Deposit, approve, withdraw, transfer, and tx-broadcast helpers | 2 req/s |

## Endpoints

### System

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/health` | None | Service health, `executor_id`, and signature readiness |
| GET | `/system/time` | None | Server timestamp for signature deadlines |
| GET | `/tokens` | None | Whitelisted token registry |
| GET | `/markets` | None | Active markets with token metadata |
| GET | `/fx/rate` | None | Aggregated FX rate for an ISO currency pair plus 24h delta |
| GET | `/permit/metadata` | API Key | EIP-2612 permit domain, nonce, and allowance probe |
| GET | `/config` | None | Public chain and contract bootstrap config |
| POST | `/verify-signature` | None | Verify an EIP-712 order signature |

### Trading

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/swap/quote` | None | Get a swap quote and signed `route_params` payload |
| POST | `/swap` | EIP-712 | Execute a signed swap quote |
| POST | `/orders/preview` | None | Preview canonical order values and EIP-712 payload |
| POST | `/orders` | EIP-712 | Place a limit order |
| POST | `/orders/cancel` | EIP-712 | Cancel a single order |
| DELETE | `/orders/cancel-all` | API Key | Cancel all open orders |
| POST | `/orders/vl/batch` | EIP-712 | Place a Virtual Liquidity batch |
| POST | `/orders/vl/cancel` | EIP-712 | Cancel a full Virtual Liquidity batch |

### Orders And Fills

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/orders/{order_id}` | API Key | Get one order by ID |
| GET | `/orders` | API Key | List orders for one account |
| GET | `/fills/{order_id}` | API Key | List fills for one order |
| GET | `/fills` | API Key | List fills across all orders |

### Account And Funds

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/balances` | API Key | Wallet + vault balances |
| POST | `/approve` | API Key | Build unsigned ERC-20 `approve(spender, amount)` tx |
| POST | `/deposit` | API Key | Build unsigned `depositFund` or `depositFundWithPermit` tx |
| POST | `/tx/send` | API Key | Broadcast a signed approve/deposit tx |
| POST | `/withdraw` | EIP-712 | Request executor co-signature for instant withdraw |
| POST | `/withdraw/build` | Optional API Key | Build unsigned `executeInstantWithdrawDualSig` tx |
| POST | `/withdraw/send` | Optional API Key | Broadcast a signed withdrawal tx |
| POST | `/transfer` | API Key | Build unsigned ERC-20 `transfer` tx |
| POST | `/transfer/send` | API Key | Broadcast a signed transfer tx |

### API Key Management

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api-keys` | EIP-712 | Create a read-only API key |
| GET | `/api-keys` | EIP-712 | List API keys for one wallet (signed query parameters) |
| POST | `/api-keys/list` | EIP-712 | List API keys for one wallet (signed JSON body) |
| DELETE | `/api-keys` | EIP-712 | Revoke an API key (signed query parameters) |
| POST | `/api-keys/revoke` | EIP-712 | Revoke an API key (signed JSON body) |
| POST | `/api-keys/revoke-all` | EIP-712 | Bulk-revoke every active API key for one wallet in a single signature |
| POST | `/api-keys/self-revoke` | API Key | Revoke the API key currently being used |
| POST | `/api-keys/verify` | None | Verify an API key/secret pair without consuming rate-limit budget |

## Error Responses

Most errors return a JSON body with a `detail` field:

```json
{
  "detail": "Human-readable error message"
}
```

Server-side failures (originally 5xx) are mapped to `503` with a generic body — clients see `{"detail": "Service temporarily unavailable; please retry"}` plus a `Retry-After: 1` header. The specific cause is recorded server-side and is not echoed back to integrators.

### Common Status Codes

| Code | Meaning |
|------|---------|
| 200 | Success |
| 201 | Resource created |
| 400 | Bad request or signature mismatch |
| 401 | Missing or invalid API key |
| 403 | Authenticated wallet does not match requested owner |
| 404 | Resource not found |
| 409 | Conflict, such as replayed signature or self-match rejection |
| 410 | Quote expired, already consumed, or not found |
| 422 | Validation failure |
| 429 | Rate limit or cancel cooldown hit |
| 503 | Service temporarily unavailable; retry after the `Retry-After` header |

## Code Examples

=== "Python"

    ```python
    import requests

    # Public endpoint
    tokens = requests.get("https://api.testnet.sera.cx/api/v1/tokens").json()

    # Authenticated endpoint
    headers = {"Authorization": "Bearer sera_abc123:secret456"}
    orders = requests.get(
        "https://api.testnet.sera.cx/api/v1/orders",
        params={"owner_address": "0x..."},
        headers=headers,
    ).json()
    ```

=== "TypeScript"

    ```typescript
    // Public endpoint (no auth)
    const tokens = await fetch("https://api.testnet.sera.cx/api/v1/tokens")
      .then(r => r.json());

    // Authenticated endpoint (API key)
    const orders = await fetch(
      "https://api.testnet.sera.cx/api/v1/orders?owner_address=0x...",
      { headers: { "Authorization": "Bearer sera_abc123:secret456" } },
    ).then(r => r.json());
    ```

=== "cURL"

    ```bash
    # Public endpoint
    curl https://api.testnet.sera.cx/api/v1/tokens

    # Authenticated endpoint
    curl -H "Authorization: Bearer sera_abc123:secret456" \
      "https://api.testnet.sera.cx/api/v1/orders?owner_address=0x..."
    ```

---

# Authentication

- Canonical URL: https://docs.testnet.sera.cx/api-reference/authentication/
- Source path: `docs/en/api-reference/authentication.md`
- Description: How to authenticate with the Sera API

# Authentication

!!! info "Network & Compatibility"
    | Resource       | Value                                                       |
    |----------------|-------------------------------------------------------------|
    | API base URL   | `https://api.testnet.sera.cx/api/v1`                        |
    | Chain          | Sepolia (chainId `11155111`)                                |
    | Sera contract  | `0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e`                 |
    | Vault contract | `0x3c7945840bAE0d7e7f3824Ebccef1962629250F0`                 |
    | SOR contract   | `0x83c1368110B640A729f3810De5FBe94b99aa5668`                 |

    **Signing primitives.** Every trading mutation is an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed-data signature against the Sera domain. Deposits that take the permit path use the [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) `Permit` extension — supported by USDC, EURC, EURT, and most modern stablecoins, not all ERC-20s; call `GET /permit/metadata` to check support before signing. API-key management uses an EIP-712 `ManageApiKey` payload.

    **Tested clients.** Python `eth_account >= 0.10` + `requests`; TypeScript `ethers` v6 (`signer.signTypedData`). Browser wallets confirmed working with EIP-712 typed data: **MetaMask**, **Rabby**, **Frame**, **Coinbase Wallet**, **Trust**, **Rainbow**. Safe multisigs work via EIP-1271 (the message is verified on-chain rather than via ecrecover).

    **Address casing.** Read endpoints (`/balances`, `/orders`, `/fills`) treat `owner_address` as case-sensitive — pass the lowercase form. EIP-712 signed payloads accept EIP-55 checksum addresses.

Sera uses two authentication modes:

- **API keys** for account reads and transaction-building helpers.
- **EIP-712 signatures** for trading, cancellation, withdrawal, and API-key management.

In practice, the public utility routes are:

- `GET /health`, `GET /system/time`, `GET /tokens`, `GET /markets`, and `GET /config`
- `POST /swap/quote` and `POST /verify-signature`

The API-key protected read and helper routes are:

- `GET /orders`, `GET /orders/{order_id}`, `GET /fills`, `GET /fills/{order_id}`, and `GET /balances`
- `GET /permit/metadata`
- transaction builders such as `POST /approve`, `POST /deposit`, `POST /tx/send`, `POST /transfer`, and `POST /transfer/send`

## Setup

Every code snippet on this page assumes the following imports and constants. Pick a tab and reuse it across the rest of the page.

=== "Python"

    ```python
    import json, time
    import requests
    from eth_account import Account
    from eth_account.messages import encode_typed_data

    API           = "https://api.testnet.sera.cx/api/v1"
    PRIVATE_KEY   = "0x...your wallet key..."
    WALLET        = Account.from_key(PRIVATE_KEY).address

    DOMAIN = {
        "name": "Sera",
        "version": "1",
        "chainId": 11155111,
        "verifyingContract": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
    }
    ```

=== "TypeScript"

    ```typescript
    import { Wallet, TypedDataDomain, ZeroAddress } from "ethers";

    const API         = "https://api.testnet.sera.cx/api/v1";
    const PRIVATE_KEY = "0x...your wallet key...";
    const signer      = new Wallet(PRIVATE_KEY);
    const WALLET      = signer.address;

    const DOMAIN: TypedDataDomain = {
      name: "Sera",
      version: "1",
      chainId: 11155111,
      verifyingContract: "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
    };
    ```

## API Keys

API keys are read-only credentials sent as:

```
Authorization: Bearer {api_key}:{api_secret}
```

Endpoint summary:

| Method | Path | Signed With |
|--------|------|-------------|
| `POST` | `/api-keys` | EIP-712 body payload (`action=create`) |
| `GET` or `POST` | `/api-keys` or `/api-keys/list` | EIP-712 query parameters or body payload (`action=list`) |
| `DELETE` or `POST` | `/api-keys` or `/api-keys/revoke` | EIP-712 query parameters or body payload (`action=revoke_<api_key>`) |
| `POST` | `/api-keys/revoke-all` | EIP-712 body payload (`action=revoke_all`) |
| `POST` | `/api-keys/self-revoke` | Bearer credentials of the key being revoked |
| `POST` | `/api-keys/verify` | None (the key being verified is the body) |

### Create An API Key

API keys are created by signing an EIP-712 `ManageApiKey` message.

=== "Python"

    ```python
    MANAGE_API_KEY_TYPES = {
        "ManageApiKey": [
            {"name": "owner",     "type": "address"},
            {"name": "action",    "type": "string"},
            {"name": "timestamp", "type": "uint256"},
        ]
    }

    timestamp = int(time.time())
    message   = {"owner": WALLET, "action": "create", "timestamp": timestamp}
    signable  = encode_typed_data(DOMAIN, MANAGE_API_KEY_TYPES, message)
    signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()

    r = requests.post(
        f"{API}/api-keys",
        headers={"Content-Type": "application/json"},
        data=json.dumps({
            "owner_address": WALLET,
            "action":        "create",
            "timestamp":     timestamp,
            "signature":     "0x" + signature.lstrip("0x"),
            "label":         "Trading bot",
        }),
        timeout=10,
    )
    api_key, api_secret = (lambda b: (b["api_key"], b["api_secret"]))(r.json())
    ```

=== "TypeScript"

    ```typescript
    const MANAGE_API_KEY_TYPES = {
      ManageApiKey: [
        { name: "owner",     type: "address" },
        { name: "action",    type: "string"  },
        { name: "timestamp", type: "uint256" },
      ],
    };

    const timestamp = Math.floor(Date.now() / 1000);
    const signature = await signer.signTypedData(DOMAIN, MANAGE_API_KEY_TYPES, {
      owner: WALLET, action: "create", timestamp,
    });

    const response = await fetch(`${API}/api-keys`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        owner_address: WALLET,
        action:        "create",
        timestamp,
        signature,
        label:         "Trading bot",
      }),
    });
    const { api_key, api_secret } = await response.json();
    ```

Notes:

- The signed timestamp must be within 5 minutes of server time.
- A wallet can hold up to 10 active API keys.
- `api_secret` is returned only once. Store it securely.

### List API Keys

=== "Python"

    ```python
    timestamp = int(time.time())
    message   = {"owner": WALLET, "action": "list", "timestamp": timestamp}
    signable  = encode_typed_data(DOMAIN, MANAGE_API_KEY_TYPES, message)
    signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()

    keys = requests.get(
        f"{API}/api-keys",
        params={
            "owner_address": WALLET,
            "action":        "list",
            "timestamp":     timestamp,
            "signature":     "0x" + signature.lstrip("0x"),
        },
        timeout=10,
    ).json()
    ```

=== "TypeScript"

    ```typescript
    const timestamp = Math.floor(Date.now() / 1000);
    const signature = await signer.signTypedData(DOMAIN, MANAGE_API_KEY_TYPES, {
      owner: WALLET, action: "list", timestamp,
    });

    const url = new URL(`${API}/api-keys`);
    url.searchParams.set("owner_address", WALLET);
    url.searchParams.set("action",        "list");
    url.searchParams.set("timestamp",     String(timestamp));
    url.searchParams.set("signature",     signature);

    const keys = await fetch(url).then(r => r.json());
    ```

If your client prefers a JSON body over signed query parameters, `POST /api-keys/list` accepts the same `owner_address`, `action`, `timestamp`, and `signature` fields in the request body.

### Revoke An API Key

=== "Python"

    ```python
    api_key_to_revoke = "sera_..."
    action            = f"revoke_{api_key_to_revoke}"
    timestamp         = int(time.time())

    signable = encode_typed_data(
        DOMAIN, MANAGE_API_KEY_TYPES,
        {"owner": WALLET, "action": action, "timestamp": timestamp},
    )
    signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()

    requests.delete(
        f"{API}/api-keys",
        params={
            "owner_address": WALLET,
            "api_key":       api_key_to_revoke,
            "action":        action,
            "timestamp":     timestamp,
            "signature":     "0x" + signature.lstrip("0x"),
        },
        timeout=10,
    )
    ```

=== "TypeScript"

    ```typescript
    const apiKeyToRevoke = "sera_...";
    const action         = `revoke_${apiKeyToRevoke}`;
    const timestamp      = Math.floor(Date.now() / 1000);

    const signature = await signer.signTypedData(DOMAIN, MANAGE_API_KEY_TYPES, {
      owner: WALLET, action, timestamp,
    });

    const url = new URL(`${API}/api-keys`);
    url.searchParams.set("owner_address", WALLET);
    url.searchParams.set("api_key",       apiKeyToRevoke);
    url.searchParams.set("action",        action);
    url.searchParams.set("timestamp",     String(timestamp));
    url.searchParams.set("signature",     signature);

    await fetch(url, { method: "DELETE" });
    ```

If your client prefers a JSON body over signed query parameters, `POST /api-keys/revoke` accepts the same fields plus `api_key` in the request body.

### Revoke All API Keys

Revoke every active API key for a wallet in a single signature. The action is the literal string `revoke_all`. Useful after a suspected leak, device loss, or as part of a wallet-rotation playbook — one wallet popup instead of N.

=== "Python"

    ```python
    timestamp = int(time.time())
    signable  = encode_typed_data(
        DOMAIN, MANAGE_API_KEY_TYPES,
        {"owner": WALLET, "action": "revoke_all", "timestamp": timestamp},
    )
    signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()

    r = requests.post(
        f"{API}/api-keys/revoke-all",
        headers={"Content-Type": "application/json"},
        data=json.dumps({
            "owner_address": WALLET,
            "action":        "revoke_all",
            "timestamp":     timestamp,
            "signature":     "0x" + signature.lstrip("0x"),
        }),
        timeout=10,
    )
    # 200: {"status":"ok","revoked_api_keys":["sera_...","sera_..."],"count":2}
    # 200: {"status":"ok","revoked_api_keys":[],"count":0}            # no active keys
    # 409: signature already used (replay) — re-sign with a fresh timestamp
    ```

=== "TypeScript"

    ```typescript
    const timestamp = Math.floor(Date.now() / 1000);
    const signature = await signer.signTypedData(DOMAIN, MANAGE_API_KEY_TYPES, {
      owner: WALLET, action: "revoke_all", timestamp,
    });

    const res = await fetch(`${API}/api-keys/revoke-all`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        owner_address: WALLET,
        action:        "revoke_all",
        timestamp,
        signature,
      }),
    });
    // 200: { "status": "ok", "revoked_api_keys": ["sera_...", "sera_..."], "count": 2 }
    // 200: { "status": "ok", "revoked_api_keys": [], "count": 0 }   // no active keys
    // 409: signature already used (replay) — re-sign with a fresh timestamp
    ```

### Self-Revoke (Bearer-Authenticated)

Revoke the API key whose credentials you are currently using, without a wallet signature. Authenticate with `Authorization: Bearer {api_key}:{api_secret}`; the body's `api_key` must equal the bearer's `api_key` (you can only revoke the key you are signed in as — to revoke a sibling key, use `DELETE /api-keys` with a wallet signature).

=== "Python"

    ```python
    requests.post(
        f"{API}/api-keys/self-revoke",
        headers={
            "Content-Type":  "application/json",
            "Authorization": f"Bearer {api_key}:{api_secret}",
        },
        data=json.dumps({"api_key": api_key}),
        timeout=10,
    )
    ```

=== "TypeScript"

    ```typescript
    await fetch(`${API}/api-keys/self-revoke`, {
      method: "POST",
      headers: {
        "Content-Type":  "application/json",
        "Authorization": `Bearer ${api_key}:${api_secret}`,
      },
      body: JSON.stringify({ api_key }),
    });
    ```

### Verify An API Key

Confirm that an `api_key`/`api_secret` pair is valid without consuming rate-limit budget or side-effecting any state. Returns the owner address on success.

=== "Python"

    ```python
    r = requests.post(
        f"{API}/api-keys/verify",
        headers={"Content-Type": "application/json"},
        data=json.dumps({"api_key": api_key, "api_secret": api_secret}),
        timeout=10,
    )
    # 200: {"valid": True, "owner_address": "0x..."}
    # 401: {"detail": "Invalid api_key or api_secret"}
    # 503: {"detail": "Service temporarily unavailable; please retry"}
    ```

=== "TypeScript"

    ```typescript
    const res = await fetch(`${API}/api-keys/verify`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ api_key, api_secret }),
    });
    // 200: { "valid": true, "owner_address": "0x..." }
    // 401: { "detail": "Invalid api_key or api_secret" }
    // 503: { "detail": "Service temporarily unavailable; please retry" }
    ```

## EIP-712 Domain

The public API verifies signatures against the Sepolia Sera contract domain (already shown in [Setup](#setup)):

=== "Python"

    ```python
    DOMAIN = {
        "name": "Sera",
        "version": "1",
        "chainId": 11155111,
        "verifyingContract": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
    }
    ```

=== "TypeScript"

    ```typescript
    const DOMAIN = {
      name: "Sera",
      version: "1",
      chainId: 11155111,
      verifyingContract: "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
    };
    ```

Use `GET /config` to fetch the live `chain_id`, `sera_address`, `vault_address`, and `sor_address` for the current deployment instead of hardcoding them.

## Expiration Rules

Signed order payloads require `expiration`, and swap quote requests use the same bounded future timestamp for the signed Intent they return.

- `expiration` must be strictly in the future.
- `expiration` must be no more than 365 days minus the 300-second clock-skew guard from current server time.
- Missing, zero, past, or far-future values are rejected before the request reaches matching or settlement.

Use `GET /system/time` to derive these timestamps and leave a small buffer instead of signing right at the edge.

## UUID Binding For Order Requests

Limit orders now carry two linked identifiers:

- `order_id`: a UUID4 string used as the human-readable order ID.
- `uuid_int`: the decimal uint256 embedded in the signed on-chain `Order.uuid` field.

The API rejects requests where `uuid_int` does not match the composite encoding of `order_id` shown below. Fetch the live `executor_id` from `GET /health`, then generate `uuid_int` using the composite layout below:

```text
[255:252] executor_id | [251:124] full UUID4 bits | [123:12] group_id | [11:0] leg_id
```

### Standalone Limit Orders

For a normal limit order:

- `leg_id = 0`
- `group_id = first 112 bits of order_id`

=== "Python"

    ```python
    import uuid as _uuid

    def uuid_string_to_int(s: str) -> int:
        return int(_uuid.UUID(s))

    def encode_standalone_uuid(order_id: str, executor_id: int) -> str:
        raw   = uuid_string_to_int(order_id)
        group = raw >> 16
        return str((executor_id << 252) | (raw << 124) | (group << 12))
    ```

=== "TypeScript"

    ```typescript
    function uuidStringToBigInt(uuid: string): bigint {
      return BigInt(`0x${uuid.replace(/-/g, "")}`);
    }

    function encodeStandaloneUuid(orderId: string, executorId: number): string {
      const raw   = uuidStringToBigInt(orderId);
      const group = raw >> 16n;
      return ((BigInt(executorId) << 252n) | (raw << 124n) | (group << 12n)).toString();
    }
    ```

Example valid pair:

```json
{
  "order_id": "00000000-0000-4000-8000-000000000001",
  "uuid_int": "6427948336465191935941739505432058208337171677044006212075520"
}
```

### Virtual Liquidity Batches

For VL batches:

- all siblings share the same `group_id`
- `group_id` is derived from **order 0**
- `leg_id` increments `0, 1, 2, ...`

=== "Python"

    ```python
    def encode_vl_uuid(order_id: str, executor_id: int,
                      leg_id: int, group_order_id: str) -> str:
        raw   = uuid_string_to_int(order_id)
        group = uuid_string_to_int(group_order_id) >> 16
        return str((executor_id << 252) | (raw << 124) | (group << 12) | leg_id)
    ```

=== "TypeScript"

    ```typescript
    function encodeVlUuid(
      orderId: string, executorId: number,
      legId: number, groupOrderId: string,
    ): string {
      const raw   = uuidStringToBigInt(orderId);
      const group = uuidStringToBigInt(groupOrderId) >> 16n;
      return ((BigInt(executorId) << 252n) | (raw << 124n) | (group << 12n) | BigInt(legId)).toString();
    }
    ```

## Order Signature

The signed on-chain `Order` struct is:

=== "Python"

    ```python
    ORDER_TYPES = {
        "Order": [
            {"name": "user",                 "type": "address"},
            {"name": "expiration",           "type": "uint48"},
            {"name": "feeBps",               "type": "uint48"},
            {"name": "recipient",            "type": "address"},
            {"name": "fromToken",            "type": "address"},
            {"name": "toToken",              "type": "address"},
            {"name": "fromAmount",           "type": "uint256"},
            {"name": "toAmount",             "type": "uint256"},
            {"name": "initialDepositAmount", "type": "uint256"},
            {"name": "uuid",                 "type": "uint256"},
        ]
    }
    ```

=== "TypeScript"

    ```typescript
    const ORDER_TYPES = {
      Order: [
        { name: "user",                 type: "address" },
        { name: "expiration",           type: "uint48"  },
        { name: "feeBps",               type: "uint48"  },
        { name: "recipient",            type: "address" },
        { name: "fromToken",            type: "address" },
        { name: "toToken",              type: "address" },
        { name: "fromAmount",           type: "uint256" },
        { name: "toAmount",             type: "uint256" },
        { name: "initialDepositAmount", type: "uint256" },
        { name: "uuid",                 type: "uint256" },
      ],
    };
    ```

In `POST /orders` requests, the API uses pair identity fields rather than direct spend/receive fields:

- `from_address` is the market base token address.
- `to_address` is the market quote token address.
- `bid` spends `to_address` to buy `from_address`.
- `ask` spends `from_address` to sell into `to_address`.

Fetch these token addresses from `GET /tokens` and display-oriented pair labels from `GET /markets`.
For production order placement, call `POST /orders/preview` before wallet
signature and sign the returned `eip712_order`. Preview applies the public
market precision grid and returns the exact raw `fromAmount` / `toAmount` values
that the final `POST /orders` submission must match.

Example:

=== "Python"

    ```python
    order_data = {
        "user":                 WALLET,
        "expiration":           int(time.time()) + 86_400,
        "feeBps":                0,
        "recipient":            "0x0000000000000000000000000000000000000000",
        "fromToken":            "0x...",
        "toToken":              "0x...",
        "fromAmount":            1_085_000_000,
        "toAmount":              1_000_000_000,
        "initialDepositAmount":  0,
        "uuid":                  int(uuid_int),
    }
    signable  = encode_typed_data(DOMAIN, ORDER_TYPES, order_data)
    signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
    ```

=== "TypeScript"

    ```typescript
    const orderData = {
      user:                 WALLET,
      expiration:           Math.floor(Date.now() / 1000) + 86_400,
      feeBps:               0,
      recipient:            ZeroAddress,
      fromToken:            "0x...",
      toToken:              "0x...",
      fromAmount:           1_085_000_000n,
      toAmount:             1_000_000_000n,
      initialDepositAmount: 0n,
      uuid:                 BigInt(uuidInt),
    };
    const signature = await signer.signTypedData(DOMAIN, ORDER_TYPES, orderData);
    ```

## Intent Signature For Swaps

`POST /swap/quote` returns `route_params`. Sign them exactly as returned.

=== "Python"

    ```python
    INTENT_TYPES = {
        "Intent": [
            {"name": "taker",                "type": "address"},
            {"name": "inputToken",           "type": "address"},
            {"name": "outputToken",          "type": "address"},
            {"name": "maxInputAmount",       "type": "uint256"},
            {"name": "minOutputAmount",      "type": "uint256"},
            {"name": "recipient",            "type": "address"},
            {"name": "initialDepositAmount", "type": "uint256"},
            {"name": "uuid",                 "type": "uint256"},
            {"name": "deadline",             "type": "uint48"},
        ]
    }
    signable  = encode_typed_data(DOMAIN, INTENT_TYPES, quote["route_params"])
    signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
    ```

=== "TypeScript"

    ```typescript
    const INTENT_TYPES = {
      Intent: [
        { name: "taker",                type: "address" },
        { name: "inputToken",           type: "address" },
        { name: "outputToken",          type: "address" },
        { name: "maxInputAmount",       type: "uint256" },
        { name: "minOutputAmount",      type: "uint256" },
        { name: "recipient",            type: "address" },
        { name: "initialDepositAmount", type: "uint256" },
        { name: "uuid",                 type: "uint256" },
        { name: "deadline",             type: "uint48"  },
      ],
    };
    const signature = await signer.signTypedData(DOMAIN, INTENT_TYPES, quote.route_params);
    ```

## Cancel Signatures

### CancelOrder

`CancelOrder.orderId` is the composite `uuid_int`, not the UUID string.

=== "Python"

    ```python
    CANCEL_ORDER_TYPES = {
        "CancelOrder": [
            {"name": "owner",   "type": "address"},
            {"name": "orderId", "type": "uint256"},
        ]
    }
    signable  = encode_typed_data(
        DOMAIN, CANCEL_ORDER_TYPES,
        {"owner": WALLET, "orderId": int(uuid_int)},
    )
    signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
    ```

=== "TypeScript"

    ```typescript
    const CANCEL_ORDER_TYPES = {
      CancelOrder: [
        { name: "owner",   type: "address" },
        { name: "orderId", type: "uint256" },
      ],
    };
    const signature = await signer.signTypedData(DOMAIN, CANCEL_ORDER_TYPES, {
      owner: WALLET, orderId: BigInt(uuidInt),
    });
    ```

### CancelVLBatch

=== "Python"

    ```python
    CANCEL_VL_BATCH_TYPES = {
        "CancelVLBatch": [
            {"name": "owner",     "type": "address"},
            {"name": "vlBatchId", "type": "string"},
        ]
    }
    ```

=== "TypeScript"

    ```typescript
    const CANCEL_VL_BATCH_TYPES = {
      CancelVLBatch: [
        { name: "owner",     type: "address" },
        { name: "vlBatchId", type: "string"  },
      ],
    };
    ```

## Withdraw Signature

=== "Python"

    ```python
    WITHDRAW_TYPES = {
        "WithdrawIntent": [
            {"name": "user",      "type": "address"},
            {"name": "tokens",    "type": "address[]"},
            {"name": "amounts",   "type": "uint256[]"},
            {"name": "recipient", "type": "address"},
            {"name": "deadline",  "type": "uint256"},
            {"name": "uuid",      "type": "uint256"},
        ]
    }
    ```

=== "TypeScript"

    ```typescript
    const WITHDRAW_TYPES = {
      WithdrawIntent: [
        { name: "user",      type: "address"   },
        { name: "tokens",    type: "address[]" },
        { name: "amounts",   type: "uint256[]" },
        { name: "recipient", type: "address"   },
        { name: "deadline",  type: "uint256"   },
        { name: "uuid",      type: "uint256"   },
      ],
    };
    ```

## Verify A Signature Before Submission

=== "Python"

    ```python
    r = requests.post(
        f"{API}/verify-signature",
        headers={"Content-Type": "application/json"},
        data=json.dumps({
            "owner_address": WALLET,
            "side":          "bid",
            "amount":        "1000",
            "price":         "1.085",
            "from_address":  EURC_ADDRESS,
            "to_address":    USDC_ADDRESS,
            "order_id":      "00000000-0000-4000-8000-000000000001",
            "uuid_int":      "6427948336465191935941739505432058208337171677044006212075520",
            "signature":     signature,
            "expiration":    int(time.time()) + 86_400,
        }),
        timeout=10,
    )
    ```

=== "TypeScript"

    ```typescript
    const response = await fetch(`${API}/verify-signature`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        owner_address: WALLET,
        side:          "bid",
        amount:        "1000",
        price:         "1.085",
        from_address:  EURC_ADDRESS,
        to_address:    USDC_ADDRESS,
        order_id:      "00000000-0000-4000-8000-000000000001",
        uuid_int:      "6427948336465191935941739505432058208337171677044006212075520",
        signature,
        expiration:    Math.floor(Date.now() / 1000) + 86_400,
      }),
    });
    ```

## Clock Synchronization

Use `GET /system/time` for expiration and deadline calculations, `GET /health` for the live `executor_id` used in `uuid_int` generation, and `GET /config` for the live EIP-712 contract addresses.

---

# Market Maker Guide

- Canonical URL: https://docs.testnet.sera.cx/api-reference/market-maker-guide/
- Source path: `docs/en/api-reference/market-maker-guide.md`
- Description: Hands-on tutorial — place an order, cancel an order, and automate quoting against your own rate feed

# Market Maker Guide

!!! info "Network & Compatibility"
    | Resource       | Value                                                       |
    |----------------|-------------------------------------------------------------|
    | API base URL   | `https://api.testnet.sera.cx/api/v1`                        |
    | Chain          | Sepolia (chainId `11155111`)                                |
    | Sera contract  | `0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e`                 |
    | Vault contract | `0x3c7945840bAE0d7e7f3824Ebccef1962629250F0`                 |
    | SOR contract   | `0x83c1368110B640A729f3810De5FBe94b99aa5668`                 |

    **Signing primitives.** Every trading mutation is an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed-data signature against the Sera domain. Deposits that take the permit path use the [ERC-2612](https://eips.ethereum.org/EIPS/eip-2612) `Permit` extension — supported by USDC, EURC, EURT, and most modern stablecoins, not all ERC-20s; call `GET /permit/metadata` to check support before signing. API-key management uses an EIP-712 `ManageApiKey` payload.

    **Tested clients.** Python `eth_account >= 0.10` + `requests`; TypeScript `ethers` v6 (`signer.signTypedData`). Browser wallets confirmed working with EIP-712 typed data: **MetaMask**, **Rabby**, **Frame**, **Coinbase Wallet**, **Trust**, **Rainbow**. Safe multisigs work via EIP-1271 (the message is verified on-chain rather than via ecrecover).

    **Address casing.** Read endpoints (`/balances`, `/orders`, `/fills`) treat `owner_address` as case-sensitive — pass the lowercase form. EIP-712 signed payloads accept EIP-55 checksum addresses.

This is the page you want open while you write the first version of your bot. It walks through three things in order:

1. Place a single limit order manually, end-to-end.
2. Cancel that order.
3. Wrap both into an automated quoting loop driven by your own rate source.

Setup that lives elsewhere — minting an API key, funding the Vault, withdrawing later — is linked under [Next Steps](#next-steps). Come back here once your wallet has a USDC balance on Sepolia and an API key in hand.

Code snippets are tabbed between Python and TypeScript. Pick a tab and stick with it; the two implementations are line-for-line equivalent.

## Setup

=== "Python"

    ```bash
    pip install eth-account requests
    ```

    ```python
    from decimal import Decimal
    import json, time, uuid
    import requests
    from eth_account import Account
    from eth_account.messages import encode_typed_data

    API     = "https://api.testnet.sera.cx/api/v1"
    DOMAIN  = {
        "name": "Sera",
        "version": "1",
        "chainId": 11155111,                                          # Sepolia
        "verifyingContract": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
    }

    PRIVATE_KEY = "0x...your wallet key..."                            # signer
    API_KEY     = "sera_..."                                           # see Authentication
    API_SECRET  = "..."
    OWNER       = Account.from_key(PRIVATE_KEY).address

    AUTH = {"Authorization": f"Bearer {API_KEY}:{API_SECRET}"}
    ```

=== "TypeScript"

    ```bash
    npm i ethers@6
    ```

    ```typescript
    import { Wallet, TypedDataDomain, randomBytes } from "ethers";

    const API     = "https://api.testnet.sera.cx/api/v1";
    const DOMAIN: TypedDataDomain = {
      name: "Sera",
      version: "1",
      chainId: 11155111,                                              // Sepolia
      verifyingContract: "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
    };

    const PRIVATE_KEY = "0x...your wallet key...";                    // signer
    const API_KEY     = "sera_...";                                    // see Authentication
    const API_SECRET  = "...";
    const wallet      = new Wallet(PRIVATE_KEY);
    const OWNER       = wallet.address;

    const AUTH = { "Authorization": `Bearer ${API_KEY}:${API_SECRET}` };
    ```

The EIP-712 `Order` typed-data structure mirrors the on-chain `SeraLib.ORDER_TYPEHASH`. Define it once:

=== "Python"

    ```python
    ORDER_TYPES = {
        "Order": [
            {"name": "user",                 "type": "address"},
            {"name": "expiration",           "type": "uint48"},
            {"name": "feeBps",               "type": "uint48"},
            {"name": "recipient",            "type": "address"},
            {"name": "fromToken",            "type": "address"},
            {"name": "toToken",              "type": "address"},
            {"name": "fromAmount",           "type": "uint256"},
            {"name": "toAmount",             "type": "uint256"},
            {"name": "initialDepositAmount", "type": "uint256"},
            {"name": "uuid",                 "type": "uint256"},
        ]
    }

    CANCEL_ORDER_TYPES = {
        "CancelOrder": [
            {"name": "owner",   "type": "address"},
            {"name": "orderId", "type": "uint256"},
        ]
    }
    ```

=== "TypeScript"

    ```typescript
    const ORDER_TYPES = {
      Order: [
        { name: "user",                 type: "address" },
        { name: "expiration",           type: "uint48"  },
        { name: "feeBps",               type: "uint48"  },
        { name: "recipient",            type: "address" },
        { name: "fromToken",            type: "address" },
        { name: "toToken",              type: "address" },
        { name: "fromAmount",           type: "uint256" },
        { name: "toAmount",             type: "uint256" },
        { name: "initialDepositAmount", type: "uint256" },
        { name: "uuid",                 type: "uint256" },
      ],
    };

    const CANCEL_ORDER_TYPES = {
      CancelOrder: [
        { name: "owner",   type: "address" },
        { name: "orderId", type: "uint256" },
      ],
    };
    ```

## The `uuid_int` Bit Layout

Every order's `uuid` is a packed 256-bit integer:

```text
┌──────────┬───────────────────┬──────────────────┬──────────┐
│[255:252] │     [251:124]     │    [123:12]      │  [11:0]  │
│ Executor │     Order ID      │    Group ID      │  Leg ID  │
│  4 bit   │      128 bit      │     112 bit      │  12 bit  │
│  (0–15)  │   (full UUID4)    │  (UUID4 >> 16)   │ (0–4095) │
└──────────┴───────────────────┴──────────────────┴──────────┘
```

For a standalone order, `group_id = order_id >> 16` and `leg_id = 0`. For a VL batch leg `i`, every leg shares the same `group_id` (derived from leg 0's UUID4) and `leg_id = i`. The server rejects anything else.

`executor_id` is set per deployment. On Sepolia testnet it is `0` unless your deployment publishes otherwise. Read once at startup and reuse — drifting executor IDs invalidate every signed UUID you have outstanding.

=== "Python"

    ```python
    def compose_uuid(order_id: str, executor_id: int = 0,
                     leg_id: int = 0, group_uuid: str | None = None) -> int:
        """Pack a UUID4 string into the on-chain composite uint256."""
        oid    = int(uuid.UUID(order_id))
        gsrc   = int(uuid.UUID(group_uuid)) if group_uuid else oid
        gid    = gsrc >> 16                                            # top 112 of 128
        return (executor_id << 252) | (oid << 124) | (gid << 12) | leg_id

    def new_order_id() -> str:
        return str(uuid.uuid4())
    ```

=== "TypeScript"

    ```typescript
    function composeUuid(
      orderId: string,
      executorId: bigint = 0n,
      legId: bigint = 0n,
      groupUuid: string | null = null,
    ): bigint {
      const oid  = BigInt("0x" + orderId.replace(/-/g, ""));
      const gsrc = groupUuid ? BigInt("0x" + groupUuid.replace(/-/g, "")) : oid;
      const gid  = gsrc >> 16n;                                       // top 112 of 128
      return (executorId << 252n) | (oid << 124n) | (gid << 12n) | legId;
    }

    function newOrderId(): string {
      const b = randomBytes(16);
      b[6] = (b[6] & 0x0f) | 0x40;                                    // version 4
      b[8] = (b[8] & 0x3f) | 0x80;                                    // RFC 4122 variant
      const h = Buffer.from(b).toString("hex");
      return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`;
    }
    ```

## Tutorial 1 — Place a Single Limit Order

You're going to place a bid at `1.085 EURC/USDC` for `1000` EURC. The market base is EURC, the quote is USDC. A `bid` means: spend USDC to receive EURC.

Resolve the token addresses from `GET /tokens` once at startup; for this walkthrough we hard-code them:

=== "Python"

    ```python
    USDC = {"address": "0x965d4b4546716e416e950bc30467d128455d2d0e", "decimals": 6}
    EURC = {"address": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f", "decimals": 6}
    ```

=== "TypeScript"

    ```typescript
    const USDC = { address: "0x965d4b4546716e416e950bc30467d128455d2d0e", decimals: 6 };
    const EURC = { address: "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f", decimals: 6 };
    ```

The wire payload to `POST /orders` uses pair-natural fields (`side`, `amount`, `price`, `from_address`=base, `to_address`=quote). Call `POST /orders/preview` first. Preview applies the public market precision grid and returns the exact EIP-712 `Order` payload to sign.

=== "Python"

    ```python
    def place_order(side: str, base: dict, quote: dict,
                    amount: Decimal, price: Decimal,
                    expiration: int) -> dict:
        order_id = new_order_id()
        uuid_int = compose_uuid(order_id)                              # executor_id=0

        wire = {
            "owner_address": OWNER,
            "side":          side,
            "amount":        str(amount),
            "price":         str(price),
            "order_type":    "limit",
            "from_address":  base["address"],                           # base, NOT spend
            "to_address":    quote["address"],                          # quote, NOT receive
            "order_id":      order_id,
            "uuid_int":      str(uuid_int),
            "expiration":    expiration,
        }

        preview = requests.post(
            f"{API}/orders/preview",
            headers={"Content-Type": "application/json"},
            data=json.dumps(wire),
            timeout=10,
        )
        if preview.status_code != 200:
            body = preview.json()
            raise RuntimeError(f"{body.get('error_code')}: {body.get('detail')}")
        preview_body = preview.json()

        order_struct = preview_body["eip712_order"]
        for key in ("expiration", "feeBps", "fromAmount", "toAmount",
                    "initialDepositAmount", "uuid"):
            order_struct[key] = int(order_struct[key])
        signable = encode_typed_data(DOMAIN, ORDER_TYPES, order_struct)
        signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()

        wire["amount"] = preview_body["normalized_amount"]
        wire["price"] = preview_body["normalized_price"]
        wire["signature"] = "0x" + signature.lstrip("0x")
        r = requests.post(f"{API}/orders", headers={**AUTH, "Content-Type": "application/json"},
                          data=json.dumps(wire), timeout=10)
        if r.status_code != 201:
            envelope = r.json().get("detail", {})
            raise RuntimeError(f"{envelope.get('error_code')}: {envelope.get('detail')}")
        return {"order_id": order_id, "uuid_int": str(uuid_int)}

    placed = place_order("bid", EURC, USDC, Decimal("1000"),
                         Decimal("1.085"), int(time.time()) + 3600)
    print("placed:", placed)
    ```

=== "TypeScript"

    ```typescript
    async function placeOrder(
      side: "bid" | "ask",
      base: { address: string; decimals: number },
      quote: { address: string; decimals: number },
      amount: string,
      price: string,
      expiration: number,
    ) {
      const orderId  = newOrderId();
      const uuidInt  = composeUuid(orderId);                           // executorId=0n

      const wireNoSignature = {
        owner_address: OWNER,
        side, amount, price, order_type: "limit",
        from_address: base.address,                                    // base, NOT spend
        to_address:   quote.address,                                   // quote, NOT receive
        order_id:     orderId,
        uuid_int:     uuidInt.toString(),
        expiration,
      };

      const previewResponse = await fetch(`${API}/orders/preview`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(wireNoSignature),
      });
      const preview = await previewResponse.json();
      if (!previewResponse.ok) {
        throw new Error(`${preview.error_code}: ${preview.detail}`);
      }

      const signature = await wallet.signTypedData(
        DOMAIN,
        preview.eip712_types ?? ORDER_TYPES,
        preview.eip712_order,
      );

      const wire = {
        ...wireNoSignature,
        amount: preview.normalized_amount,
        price: preview.normalized_price,
        signature,
      };
      const r = await fetch(`${API}/orders`, {
        method: "POST",
        headers: { ...AUTH, "Content-Type": "application/json" },
        body: JSON.stringify(wire),
      });
      if (r.status !== 201) {
        const env = (await r.json()).detail ?? {};
        throw new Error(`${env.error_code}: ${env.detail}`);
      }
      return { order_id: orderId, uuid_int: uuidInt.toString() };
    }

    const placed = await placeOrder("bid", EURC, USDC, "1000", "1.085",
                                    Math.floor(Date.now() / 1000) + 3600);
    console.log("placed:", placed);
    ```

Confirm it landed:

```bash
curl -H "$AUTH" "$API/orders?owner_address=$OWNER&status=pending&limit=10"
```

The error envelope returned on `4xx` is documented in [Order Endpoints → Error Envelope](endpoints/orders.md#error-envelope). Branch on `error_code`, not on the human `detail`.

## Tutorial 2 — Cancel That Order

The `CancelOrder` struct only carries `owner` and `orderId` (the composite uint256, **not** the UUID string). You stored both at placement time — use them now.

=== "Python"

    ```python
    def cancel_order(order_id: str, uuid_int: int) -> None:
        signable = encode_typed_data(
            DOMAIN, CANCEL_ORDER_TYPES,
            {"owner": OWNER, "orderId": uuid_int},
        )
        signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
        r = requests.post(
            f"{API}/orders/cancel",
            headers={**AUTH, "Content-Type": "application/json"},
            data=json.dumps({
                "owner_address": OWNER,
                "order_id":      order_id,
                "uuid_int":      str(uuid_int),
                "signature":     "0x" + signature.lstrip("0x"),
            }),
            timeout=10,
        )
        if r.status_code == 429:
            raise RuntimeError("cancel cooldown — wait and retry")
        r.raise_for_status()

    cancel_order(placed["order_id"], int(placed["uuid_int"]))
    ```

=== "TypeScript"

    ```typescript
    async function cancelOrder(orderId: string, uuidInt: bigint): Promise<void> {
      const signature = await wallet.signTypedData(
        DOMAIN, CANCEL_ORDER_TYPES,
        { owner: OWNER, orderId: uuidInt },
      );
      const r = await fetch(`${API}/orders/cancel`, {
        method: "POST",
        headers: { ...AUTH, "Content-Type": "application/json" },
        body: JSON.stringify({
          owner_address: OWNER,
          order_id: orderId,
          uuid_int: uuidInt.toString(),
          signature,
        }),
      });
      if (r.status === 429) throw new Error("cancel cooldown — wait and retry");
      if (!r.ok) throw new Error(`cancel failed: ${r.status}`);
    }

    await cancelOrder(placed.order_id, BigInt(placed.uuid_int));
    ```

Per-order cancel cooldown is 5 minutes — hitting it returns `429`. If you ever lose `uuid_int`, fetch the order: `GET /orders/{order_id}` returns it in the response body.

## Tutorial 3 — Automate Two-Sided Quoting on One Pair

This is the loop:

```text
loop:
    mid     ← get_mid("EURC/USDC")           # your rate source
    if first_iteration or |mid − last_mid| / last_mid > drift_bps:
        cancel(open_bid, open_ask)           # any stale orders
        bid_px  ← mid * (1 − spread_bps/1e4)
        ask_px  ← mid * (1 + spread_bps/1e4)
        open_bid ← place_order("bid", EURC, USDC, qty, bid_px, ...)
        open_ask ← place_order("ask", EURC, USDC, qty, ask_px, ...)
        last_mid ← mid
    sleep(poll_seconds)
```

Three knobs:

- `spread_bps` — half-spread on each side. 5 bps each side = 10 bps round-trip.
- `drift_bps` — only requote when the mid has moved by more than this. Stops you from churning cancels when the rate jitters within a tick.
- `poll_seconds` — how often you wake up. 1–5 s is typical.

### The Rate Stub

You bring the rate source. The integration point is one function:

=== "Python"

    ```python
    def get_mid(pair: str) -> Decimal:
        """Return the current mid price for `pair`.

        Implement against your own data feed — internal pricing service,
        Bloomberg, an aggregator REST API, etc. The output must be a
        Decimal price expressed as quote-per-base (e.g. EURC/USDC = 1.085).
        """
        raise NotImplementedError("Wire up your pricing feed here.")
    ```

=== "TypeScript"

    ```typescript
    async function getMid(pair: string): Promise<number> {
      // Wire up your pricing feed here — internal service, Bloomberg,
      // Refinitiv, an aggregator REST API, etc. Return quote-per-base
      // (e.g. EURC/USDC = 1.085).
      throw new Error("Wire up your pricing feed here.");
    }
    ```

### The Loop

=== "Python"

    ```python
    SPREAD_BPS = Decimal("5")     # 5 bps either side
    DRIFT_BPS  = Decimal("3")     # requote only when mid moves > 3 bps
    POLL_S     = 2.0
    QTY        = Decimal("1000")  # base units per side

    open_bid = open_ask = None
    last_mid = None

    while True:
        try:
            mid = get_mid("EURC/USDC")
            if last_mid is not None and \
               abs(mid - last_mid) / last_mid * 10_000 <= DRIFT_BPS:
                time.sleep(POLL_S); continue

            # Cancel-first, then place — avoids self-match across the requote.
            if open_bid: cancel_order(**open_bid)
            if open_ask: cancel_order(**open_ask)
            open_bid = open_ask = None

            half   = SPREAD_BPS / Decimal(10_000)
            bid_px = (mid * (Decimal(1) - half)).quantize(Decimal("0.0001"))
            ask_px = (mid * (Decimal(1) + half)).quantize(Decimal("0.0001"))
            expiry = int(time.time()) + 3600

            placed_bid = place_order("bid", EURC, USDC, QTY, bid_px, expiry)
            placed_ask = place_order("ask", EURC, USDC, QTY, ask_px, expiry)
            open_bid   = {"order_id": placed_bid["order_id"], "uuid_int": int(placed_bid["uuid_int"])}
            open_ask   = {"order_id": placed_ask["order_id"], "uuid_int": int(placed_ask["uuid_int"])}
            last_mid   = mid
        except RuntimeError as e:
            # Surfaces our typed error_code: INSUFFICIENT_EQUITY, STP_BLOCKED, …
            print("place/cancel error:", e)
        except requests.RequestException as e:
            print("network error:", e)
        time.sleep(POLL_S)
    ```

=== "TypeScript"

    ```typescript
    const SPREAD_BPS = 5;                                              // each side
    const DRIFT_BPS  = 3;
    const POLL_MS    = 2_000;
    const QTY        = "1000";                                         // base units per side

    type Live = { order_id: string; uuid_int: bigint } | null;
    let openBid: Live = null, openAsk: Live = null, lastMid: number | null = null;

    const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));

    while (true) {
      try {
        const mid = await getMid("EURC/USDC");
        if (lastMid !== null &&
            Math.abs(mid - lastMid) / lastMid * 10_000 <= DRIFT_BPS) {
          await sleep(POLL_MS); continue;
        }

        if (openBid) await cancelOrder(openBid.order_id, openBid.uuid_int);
        if (openAsk) await cancelOrder(openAsk.order_id, openAsk.uuid_int);
        openBid = openAsk = null;

        const half   = SPREAD_BPS / 10_000;
        const bidPx  = (mid * (1 - half)).toFixed(4);
        const askPx  = (mid * (1 + half)).toFixed(4);
        const expiry = Math.floor(Date.now() / 1000) + 3600;

        const pb = await placeOrder("bid", EURC, USDC, QTY, bidPx, expiry);
        const pa = await placeOrder("ask", EURC, USDC, QTY, askPx, expiry);
        openBid = { order_id: pb.order_id, uuid_int: BigInt(pb.uuid_int) };
        openAsk = { order_id: pa.order_id, uuid_int: BigInt(pa.uuid_int) };
        lastMid = mid;
      } catch (e) {
        console.error("loop error:", e);
      }
      await sleep(POLL_MS);
    }
    ```

A few behaviours worth knowing before this hits production:

- **Cancel first, then place.** If you place fresh quotes before cancelling stale ones, your new bid can cross your stale ask — the API rejects with `STP_BLOCKED`. The loop above cancels both legs before placing.
- **`INSUFFICIENT_EQUITY` is your friend.** If you see it, you're trying to freeze more vault balance than you have. Reduce `QTY`, fund more, or trim the spread.
- **Cancel cooldown is per `order_id`.** It only matters if you try to cancel the same order twice. The loop above places a new `order_id` every requote, so each cancel hits a different cooldown bucket.
- **Reconcile before clearing local state.** `cancel_order` returning 200 means the matching engine accepted the cancel; chain settlement of any in-flight fill is asynchronous. If you track inventory, watch `settlement_summary` via `GET /orders/{id}` until it terminalises before zeroing your position state.

## Tutorial 4 — Multi-Pair Quoting With a Virtual Liquidity Batch

The single-pair loop above freezes one slot of USDC for the bid and one slot of EURC for the ask. If you want to quote N pairs from one collateral pool, you place a VL batch: every leg shares the same `group_id`, and the matching engine freezes only the largest single-leg cost (not the sum).

Use the same loop skeleton; replace the per-pair `placeOrder` with a batch call:

=== "Python"

    ```python
    def place_vl_batch(legs: list[dict]) -> list[dict]:
        """legs: list of {side, base, quote, amount, price, expiration}."""
        leg_0_uuid = new_order_id()                                    # source of group_id
        signed_legs = []
        for i, leg in enumerate(legs):
            order_id = leg_0_uuid if i == 0 else new_order_id()
            uuid_int = compose_uuid(order_id, executor_id=0,
                                    leg_id=i, group_uuid=leg_0_uuid)

            base, quote = leg["base"], leg["quote"]
            base_raw    = raw(leg["amount"], base["decimals"])
            quote_raw   = raw(leg["amount"] * leg["price"], quote["decimals"])
            if leg["side"] == "bid":
                ft, tt, fa, ta = quote["address"], base["address"], quote_raw, base_raw
            else:
                ft, tt, fa, ta = base["address"], quote["address"], base_raw, quote_raw

            struct = {
                "user": OWNER, "expiration": leg["expiration"],
                "feeBps": 0,
                "recipient": "0x0000000000000000000000000000000000000000",
                "fromToken": ft, "toToken": tt,
                "fromAmount": fa, "toAmount": ta,
                "initialDepositAmount": 0, "uuid": uuid_int,
            }
            signable = encode_typed_data(DOMAIN, ORDER_TYPES, struct)
            sig = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
            signed_legs.append({
                "owner_address": OWNER,
                "side":          leg["side"],
                "amount":        str(leg["amount"]),
                "price":         str(leg["price"]),
                "order_type":    "limit",
                "from_address":  base["address"],
                "to_address":    quote["address"],
                "order_id":      order_id,
                "uuid_int":      str(uuid_int),
                "signature":     "0x" + sig.lstrip("0x"),
                "expiration":    leg["expiration"],
            })

        r = requests.post(
            f"{API}/orders/vl/batch",
            headers={**AUTH, "Content-Type": "application/json"},
            data=json.dumps({"orders": signed_legs}),
            timeout=10,
        )
        r.raise_for_status()
        return r.json()["order_ids"]
    ```

=== "TypeScript"

    ```typescript
    async function placeVlBatch(legs: {
      side: "bid" | "ask";
      base:  { address: string; decimals: number };
      quote: { address: string; decimals: number };
      amount: string;
      price:  string;
      expiration: number;
    }[]): Promise<string[]> {
      const leg0Uuid = newOrderId();                                   // source of group_id
      const signedLegs: any[] = [];
      for (let i = 0; i < legs.length; i++) {
        const leg = legs[i];
        const orderId  = i === 0 ? leg0Uuid : newOrderId();
        const uuidInt  = composeUuid(orderId, 0n, BigInt(i), leg0Uuid);

        const baseRaw  = raw(leg.amount, leg.base.decimals);
        const quoteRaw = raw((Number(leg.amount) * Number(leg.price)).toString(),
                             leg.quote.decimals);
        const isBid    = leg.side === "bid";
        const ft = isBid ? leg.quote.address : leg.base.address;
        const tt = isBid ? leg.base.address  : leg.quote.address;
        const fa = isBid ? quoteRaw          : baseRaw;
        const ta = isBid ? baseRaw           : quoteRaw;

        const struct = {
          user: OWNER, expiration: leg.expiration, feeBps: 0,
          recipient: "0x0000000000000000000000000000000000000000",
          fromToken: ft, toToken: tt, fromAmount: fa, toAmount: ta,
          initialDepositAmount: 0n, uuid: uuidInt,
        };
        const signature = await wallet.signTypedData(DOMAIN, ORDER_TYPES, struct);
        signedLegs.push({
          owner_address: OWNER,
          side: leg.side, amount: leg.amount, price: leg.price,
          order_type: "limit",
          from_address: leg.base.address,
          to_address:   leg.quote.address,
          order_id:     orderId,
          uuid_int:     uuidInt.toString(),
          signature, expiration: leg.expiration,
        });
      }

      const r = await fetch(`${API}/orders/vl/batch`, {
        method: "POST",
        headers: { ...AUTH, "Content-Type": "application/json" },
        body: JSON.stringify({ orders: signedLegs }),
      });
      if (!r.ok) throw new Error(`VL batch failed: ${r.status}`);
      const body = await r.json();
      return body.order_ids as string[];
    }
    ```

Wrap a quoting loop around it: pull a mid for each pair, compute bid+ask per leg, cancel the previous batch via `POST /orders/vl/cancel` (signature over `CancelVLBatch{owner, vlBatchId}`), submit a fresh one. Query `GET /config → limits.vl_batch` once at startup for the current batch-size cap.

## Practical Notes

- **Persist `order_id` and `uuid_int` together.** Every cancel needs both. Keying your local state on `order_id` only and recomputing `uuid_int` from scratch is fine; storing them together is just less error-prone.
- **Idempotency.** Picking `order_id` client-side makes `POST /orders` idempotent on it. Network blip after submit? Re-send the same payload — the server dedupes.
- **Don't hard-code `limits.vl_batch.max`.** Read it from `GET /config` at startup; treat it as the source of truth.
- **Watch `settlement_summary` before clearing inventory state.** A `200` on `POST /orders/cancel` or a `pending → cancelled` transition does not mean every in-flight match has resolved on chain. Poll `GET /orders/{id}` until `settlement_summary.status` terminalises.

## Next Steps

- [Authentication](authentication.md) — minting and revoking API keys.
- [Order Endpoints](endpoints/orders.md) — full request/response reference, every field, every error code.
- [Account Endpoints](endpoints/account.md) — deposit into the Vault, withdraw, query balances.
- [Virtual Liquidity](../virtual-liquidity.md) — the budget model behind VL batches.
- [Order Types](../order-types.md) — limit, FOK, IOC, post-only semantics.

---

# System Endpoints

- Canonical URL: https://docs.testnet.sera.cx/api-reference/endpoints/system/
- Source path: `docs/en/api-reference/endpoints/system.md`
- Description: System and utility API endpoints

# System Endpoints

## Health Check

```
GET /health
```

**Authentication:** None

Returns service health plus the live `executor_id` and whether signature verification is ready.

```json
{
  "status": "healthy",
  "version": "v1",
  "timestamp": "2026-04-15T08:00:00+00:00",
  "executor_id": 0,
  "relayer_executor_id": 0,
  "signature_ready": true
}
```

Notes:

- `status` is `healthy` when signature verification is ready and the locally resolved `executor_id` matches the chain-side probe.
- `status` is `degraded` when signature verification is not yet ready, or when `executor_id` and `relayer_executor_id` disagree (an active mismatch surfaces drift before any order is rejected).
- `relayer_executor_id` may be `null` when the chain-side probe is temporarily unavailable. A `null` value alone is **not** a degradation signal; only an active disagreement with `executor_id` is.
- Use `executor_id` when generating composite `uuid_int` values.
- The route returns `503` if a backing dependency is unavailable.

## Server Time

```
GET /system/time
```

**Authentication:** None

```json
{
  "timestamp": 1713168000
}
```

Use this value to set `expiration` and `deadline` fields for signed payloads.

## Token Registry

```
GET /tokens
```

**Authentication:** None

```json
{
  "tokens": [
    {
      "currency": "USD",
      "symbol": "USDC",
      "address": "0xDcaEcdd8Db64f4316A11917Ad0162DEBD935285b",
      "decimals": 6,
      "min_trade_amount_raw": "8800000",
      "min_trade_amount": "8.800000"
    },
    {
      "currency": "EUR",
      "symbol": "EURC",
      "address": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
      "decimals": 6,
      "min_trade_amount_raw": "0",
      "min_trade_amount": "0"
    }
  ]
}
```

The response shape is an object with a `tokens` array.

`min_trade_amount_raw` mirrors the on-chain `minAmount` stored per token
on the Sera contract — the minimum from-token amount (in raw units) that
a match will accept. A submission whose input falls below this floor is
rejected pre-flight with HTTP 400 and `detail.code = "AMOUNT_BELOW_MIN"`
on both `/swap/quote` and `/orders`. `"0"` means no floor. The decimal
companion `min_trade_amount` is `min_trade_amount_raw / 10^decimals`.

## Market Registry

```
GET /markets
```

**Authentication:** None

Returns the active markets with quote/base metadata.

```json
{
  "markets": [
    {
      "symbol": "EURC/USDC",
      "base_symbol": "EURC",
      "quote_symbol": "USDC",
      "base_address": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
      "quote_address": "0xDcaEcdd8Db64f4316A11917Ad0162DEBD935285b",
      "tick_precision": 4,
      "quantity_precision": 2,
      "amount_step": "0.01",
      "price_step": "0.0001",
      "rounding_mode": "reject_extra_precision",
      "base_decimals": 6,
      "quote_decimals": 6,
      "min_ask_amount_raw": "0",
      "min_ask_amount": "0",
      "min_bid_quote_amount_raw": "8800000",
      "min_bid_quote_amount": "8.800000"
    }
  ]
}
```

`base_address` and `quote_address` are the ERC-20 contract addresses that
uniquely identify the pair. `base_symbol` / `quote_symbol` are the display
tickers and match the `symbol` string. Pair orientation is canonicalised
server-side so that `base_address < quote_address` bytewise (lowercased).

`quantity_precision` / `amount_step` define the order quantity grid.
`tick_precision` / `price_step` define the order price grid. A
`rounding_mode` of `reject_extra_precision` means final `POST /orders`
rejects non-canonical or over-precise `amount` / `price` values instead of
silently rounding them. Use `POST /orders/preview` before signing.

`min_ask_amount_raw` / `min_bid_quote_amount_raw` derive from the per-token
on-chain minimum (see `/tokens` → `min_trade_amount_raw`):

- **ASK floor** is the base-token minimum — price-independent. A sell order
  rejects if `qty * 10^base_decimals < min_ask_amount_raw`.
- **BID floor** is surfaced on the quote side because the effective base
  minimum depends on price: `min_base(P) = min_bid_quote_amount / P`. A
  buy order rejects if `qty * price * 10^quote_decimals < min_bid_quote_amount_raw`.

Decimal companions (`min_ask_amount`, `min_bid_quote_amount`) are the raw
values scaled by the respective token's `decimals` for display.

## FX Rate

```
GET /fx/rate
```

**Authentication:** None

Aggregated FX rate for an ISO currency pair plus the day-over-day delta. The
response includes the actual `as_of` timestamp so the caller can judge
freshness.

### Query Parameters

| Name | Type | Required | Description |
|------|------|----------|-------------|
| `base` | string | yes | ISO currency code (e.g. `USD`). Case-insensitive, alphabetic only. |
| `quote` | string | yes | ISO currency code (e.g. `SGD`). Case-insensitive, alphabetic only. |

### Response

```json
{
  "pair": "USD/SGD",
  "rate": "1.2843",
  "as_of": 1777372800.12,
  "rate_24h_ago": "1.2901",
  "as_of_24h_ago": 1777286400.05,
  "change_pct": "-0.4496"
}
```

- `pair` always reflects the requested orientation (`base/quote`).
- `rate` is the aggregated public FX rate for that pair.
- `as_of` is the unix timestamp of the newest source quote used for the rate.
- `rate_24h_ago`, `as_of_24h_ago`, and `change_pct` are nullable when no
  suitable historic quote is available. The endpoint still returns 200 in
  that case — coverage gaps are not a freshness failure.
- `change_pct` is signed and rendered to 4 decimal places.

### Auto-Inversion

When only the inverse pair is available at the current anchor, the rate is
computed as `1 / inverse_rate` and quantised to 8 decimal places. The historic
anchor is locked to the same direction — orientations are never mixed across
the two anchors.

### Same Currency

`?base=USD&quote=USD` returns `rate=1`, `rate_24h_ago=1`, and `change_pct=0`.

### Status Codes

| Code | Meaning |
|------|---------|
| 200 | Success — `rate` is always present; historic fields may be `null`. |
| 400 | `base`/`quote` missing or non-alphabetic. |
| 503 | No eligible source data for either direction within the public recency window. |

## Permit Metadata

```
GET /permit/metadata
```

**Authentication:** API Key required

Query parameters:

- `token`: ERC-20 token address
- `owner`: wallet that may sign the permit
- `spender`: allowance target, typically the live `sor_address` or `vault_address` from `GET /config`
- `amount` (optional): raw uint256 amount to compare against current allowance

Example response for a supported token:

```json
{
  "token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
  "owner": "0x...",
  "spender": "0x...",
  "current_allowance_raw": "0",
  "required": true,
  "permit_supported": true,
  "nonce": 4,
  "domain": {
    "name": "USD Coin",
    "version": "2",
    "chainId": 11155111,
    "verifyingContract": "0x965d4b4546716e416e950bc30467d128455d2d0e"
  }
}
```

Unsupported tokens return `permit_supported: false` and omit `nonce` / `domain`. The optional `required` flag is only present when you supply `amount`.

## Public Config

```
GET /config
```

**Authentication:** None

Returns the public bootstrap values needed for signing and transaction building.

```json
{
  "chain_id": 11155111,
  "sera_address": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
  "vault_address": "0x3c7945840bAE0d7e7f3824Ebccef1962629250F0",
  "sor_address": "0x83c1368110B640A729f3810De5FBe94b99aa5668",
  "domain_separator": "0x...",
  "eip712_domain": {
    "name": "Sera",
    "version": "1",
    "chainId": 11155111,
    "verifyingContract": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e"
  },
  "limits": {
    "vl_batch": { "min": 2, "max": 50 }
  }
}
```

This endpoint intentionally stays available even during partial startup. Missing values are returned as `null`; clients should retry rather than treating null fields as a hard failure.

`limits` carries public API caps that may vary per deployment. Read the active values at runtime via `GET /config` instead of hardcoding. Today this contains `limits.vl_batch.{min,max}` (VL batch placement size); future caps will live under additional sub-keys.

## Verify Signature

```
POST /verify-signature
```

**Authentication:** None

Useful for testing EIP-712 order signatures before placing an order.

### Request Body

```json
{
  "owner_address": "0x...",
  "side": "bid",
  "amount": "1000",
  "price": "1.085",
  "from_address": "0x...",
  "to_address": "0x...",
  "order_id": "00000000-0000-4000-8000-000000000001",
  "uuid_int": "6427948336465191935941739505432058208337171677044006212075520",
  "signature": "0x...",
  "expiration": 1713254400
}
```

`from_address` is the market base token and `to_address` is the market quote token. `expiration` is required and must satisfy the same bounded future rule as `POST /orders`.

### Response

```json
{
  "valid": true,
  "recovered_address": "0x...",
  "expected_address": "0x...",
  "error": null
}
```

---

# Order Endpoints

- Canonical URL: https://docs.testnet.sera.cx/api-reference/endpoints/orders/
- Source path: `docs/en/api-reference/endpoints/orders.md`
- Description: API endpoints for limit orders, Virtual Liquidity, and fills

# Order Endpoints

## Preview Limit Order

```
POST /orders/preview
```

**Authentication:** None

Use preview before asking a wallet to sign a limit order. The endpoint validates
only public market metadata and the submitted order fields. It returns the
canonical decimal strings and EIP-712 `Order` payload that should be signed.

### Request Body

```json
{
  "owner_address": "0x...",
  "side": "bid",
  "amount": "1000",
  "price": "1.085",
  "order_type": "limit",
  "from_address": "0x...",
  "to_address": "0x...",
  "order_id": "00000000-0000-4000-8000-000000000001",
  "uuid_int": "6427948336465191935941739505432058208337171677044006212075520",
  "expiration": 1713254400
}
```

`from_address` is the market base token and `to_address` is the market quote
token. `side` controls which token is spent in the signed `Order`: a `bid`
spends quote, and an `ask` spends base.

### Response

```json
{
  "ok": true,
  "symbol": "EURC/USDC",
  "side": "bid",
  "normalized_amount": "1000",
  "normalized_price": "1.085",
  "amount_step": "0.01",
  "price_step": "0.0001",
  "rounding_mode": "reject_extra_precision",
  "canonicalization_required": false,
  "from_token": "0x...",
  "to_token": "0x...",
  "from_amount": "1085000000",
  "to_amount": "1000000000",
  "notional": "1085.000000",
  "eip712_order": {
    "user": "0x...",
    "expiration": "1713254400",
    "feeBps": "0",
    "recipient": "0x0000000000000000000000000000000000000000",
    "fromToken": "0x...",
    "toToken": "0x...",
    "fromAmount": "1085000000",
    "toAmount": "1000000000",
    "initialDepositAmount": "0",
    "uuid": "6427948336465191935941739505432058208337171677044006212075520"
  },
  "eip712_types": {
    "Order": [
      { "name": "user", "type": "address" }
    ]
  }
}
```

The `eip712_types.Order` array contains the full `Order` type; the example is
abbreviated for readability. Sign `eip712_order` exactly as returned, then send
the final `POST /orders` request with the normalized `amount` and `price` plus
the wallet signature.

Preview does not check balances, available liquidity, account eligibility, or
other final submission checks. Those checks happen on final submission.

## Place Limit Order

```
POST /orders
```

**Authentication:** EIP-712 Order signature

### Request Body

```json
{
  "owner_address": "0x...",
  "side": "bid",
  "amount": "1000",
  "price": "1.085",
  "order_type": "limit",
  "from_address": "0x...",
  "to_address": "0x...",
  "order_id": "00000000-0000-4000-8000-000000000001",
  "uuid_int": "6427948336465191935941739505432058208337171677044006212075520",
  "signature": "0x...",
  "expiration": 1713254400
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `owner_address` | address | Yes | Wallet address of the order owner |
| `side` | string | Yes | `bid` or `ask` |
| `amount` | string | Yes | Canonical quantity string on the market `amount_step` grid |
| `price` | string | Yes | Canonical price string on the market `price_step` grid |
| `order_type` | string | Yes | Use `limit` |
| `from_address` | address | Yes | ERC-20 address of the market base token |
| `to_address` | address | Yes | ERC-20 address of the market quote token (must differ from `from_address`) |
| `order_id` | UUID string | Yes | Human-readable UUID4 order ID |
| `uuid_int` | decimal string | Yes | Composite uint256 bound to `order_id` |
| `signature` | hex string | Yes | EIP-712 Order signature |
| `expiration` | integer | Yes | Unix timestamp deadline; must satisfy `now < expiration <= now + 365 days - 300 seconds` |

`client_id` and `fee_bps` are not accepted by the live public API.

`from_address` and `to_address` identify the market, not the immediate spend direction. For a `bid`, you buy `from_address` using `to_address`; for an `ask`, you sell `from_address` for `to_address`. Resolve these addresses from `GET /tokens`.

Decimal strings must use plain fixed-point syntax: no signs, exponents,
separators, commas, underscores, or whitespace. Final order placement rejects
non-canonical strings and non-zero extra precision before signature
verification. Call `POST /orders/preview` first and submit the normalized
`amount` and `price` exactly.

### Response

```json
{
  "order_id": "00000000-0000-4000-8000-000000000001"
}
```

## Cancel Order

```
POST /orders/cancel
```

**Authentication:** EIP-712 CancelOrder signature

### Request Body

```json
{
  "owner_address": "0x...",
  "order_id": "00000000-0000-4000-8000-000000000001",
  "uuid_int": "6427948336465191935941739505432058208337171677044006212075520",
  "signature": "0x..."
}
```

`uuid_int` is required because the signed `CancelOrder.orderId` field is the composite uint256, not the UUID string.

### Response

```json
{
  "status": "ok"
}
```

### Notes

- Orders are subject to a cancel cooldown (default 5 minutes).
- Hitting the cooldown returns `429`.
- If you lost `uuid_int`, fetch the order first; it is returned in order status responses.

## Cancel All Orders

```
DELETE /orders/cancel-all
```

**Authentication:** API Key required

### Query Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `owner_address` | address | Yes | Must match the authenticated API-key owner |

### Response

```json
{
  "cancelled": ["order-id-1"],
  "failed": [],
  "skipped_cooldown": ["order-id-2"],
  "total": 1
}
```

`total` is the number of orders successfully cancelled.

## Get Order

```
GET /orders/{order_id}
```

**Authentication:** API Key required

### Response

```json
{
  "trade_id": "00000000-0000-4000-8000-000000000001",
  "owner_address": "0x...",
  "status": "pending",
  "order_type": "limit",
  "symbol": "EURC/USDC",
  "side": "bid",
  "base_symbol": "EURC",
  "quote_symbol": "USDC",
  "base_token": "0x...",
  "quote_token": "0x...",
  "price": "1.085",
  "amount": "1000.0",
  "remaining_amount": "600.0",
  "filled_base_amount": "400.0",
  "filled_quote_amount": "434.0",
  "notional": "1085.0",
  "remaining_notional": "651.0",
  "from_token": "0x...",
  "to_token": "0x...",
  "from_amount": "1085000000",
  "filled_amount": "434000000",
  "to_amount": "1000000000",
  "created_at": "2026-04-15T08:00:00+00:00",
  "updated_at": "2026-04-15T08:01:00+00:00",
  "expiration": "1713254400",
  "error": null,
  "error_code": null,
  "uuid_int": "6427948336465191935941739505432058208337171677044006212075520",
  "vl_batch_id": null,
  "settlement_summary": {
    "status": "settled",
    "total_fill_count": 1,
    "pending_fill_count": 0,
    "settled_fill_count": 1,
    "failed_fill_count": 0,
    "reverted_fill_count": 0,
    "latest_settlement_id": 42,
    "latest_tx_hash": "0x...",
    "latest_parent_status": "confirmed",
    "latest_fill_settlement_status": "settled",
    "latest_failed_fill_failure_reason": null
  },
  "settlement_economics": {
    "perspective_order_id": "00000000-0000-4000-8000-000000000001",
    "gross_debits": [
      { "token": "USDC", "token_address": "0x...", "amount": "434.0", "amount_raw": "434000000" }
    ],
    "gross_credits": [
      { "token": "EURC", "token_address": "0x...", "amount": "400.0", "amount_raw": "400000000" }
    ],
    "balance_debits": [
      { "token": "USDC", "token_address": "0x...", "amount": "434.0", "amount_raw": "434000000" }
    ],
    "balance_credits": [
      { "token": "EURC", "token_address": "0x...", "amount": "400.0", "amount_raw": "400000000" }
    ],
    "fees_paid": []
  }
}
```

### Response Notes

- `order_type` is `limit` for limit orders and `vl/cancel` flows, and `swap` for orders created through `POST /swap`.
- `expiration` echoes the signed order deadline as a string when present.
- `error_code` is a typed terminal failure code. See the [error envelope table](#error-envelope) below for the full public set. Any unrecognised internal failure collapses to `TRANSIENT_SETTLEMENT_FAILURE`.
- `vl_batch_id` is set when the order belongs to a Virtual Liquidity batch.
- `settlement_summary` aggregates per-fill settlement progress for the order. `latest_failed_fill_failure_reason` is a curated, display-oriented revert name for the most recent failed fill (anything outside the public allowlist is returned as `null`).
- `settlement_economics` is the public owner-perspective view of known fills. It contains:
    - `gross_debits` / `gross_credits` — compatibility fields for owner-visible public totals. Do not treat them as raw execution totals or use them to reconstruct values outside the documented response schema.
    - `balance_debits` / `balance_credits` — the preferred wallet/vault movement totals for reconciliation and user-facing balance displays.
    - `fees_paid` — explicit user-visible fees such as gas, with `type` and token attribution.

  Use `balance_debits` / `balance_credits` for wallet or vault movement displays. `fees_paid` is a line-item explanation of explicit user-visible fees; do not subtract it again from `balance_*` totals unless your product intentionally presents a separate cost breakdown.
- `filled_amount`, `filled_base_amount`, and `filled_quote_amount` are owner-visible progress values when settlement economics are available. Raw source amount fields such as `from_amount` are emitted from signed/raw accounting values when available, not recomputed from decimal display price.

External order statuses are:

- `pending` — submitted, resting on the book, or partially filled; the order can still receive more fills or be cancelled
- `matched` — every leg has crossed in the matching engine, but the on-chain settlement is still in flight; the match will settle (or surface a revert reason via `settlement_summary`) and will not return to `pending`
- `settled` — chain-confirmed terminal success
- `failed` — chain-confirmed revert, or pre-broadcast failure / rejection
- `cancelled` — terminal cancel

### Error Envelope

`POST /orders` returns a typed envelope on every 4xx failure (including the self-match 409). The wire shape is:

```json
{
  "detail": {
    "detail": "Order placement failed",
    "error_code": "INSUFFICIENT_EQUITY"
  }
}
```

Branch on `error_code`; the human `detail` is for display only.

| `error_code` | When it surfaces | Client action |
|--------------|------------------|---------------|
| `ALLOWANCE_INSUFFICIENT` | ERC-20 allowance to the spender insufficient at transfer time, including invalid/expired permit signatures | Re-permit or re-approve and retry |
| `INTENT_DEADLINE_EXPIRED` | Signed order's deadline passed before the tx landed | Replace the order with a fresh deadline |
| `SLIPPAGE_EXCEEDED` | The matching engine killed an IOC/FOK because no crossing liquidity exists at the signed price | Widen slippage and re-submit |
| `NO_LIQUIDITY` | Route has no executable depth at the quoted price | Reduce size or try a different pair |
| `QUOTE_STALE` | Quote/plan snapshot or signed deadline expired before submission | Submit a fresh order |
| `AMOUNT_BELOW_MIN` | Amount is below the pair's configured minimum | Increase amount |
| `INVALID_PRECISION` | `amount` or `price` has non-zero digits beyond the market grid | Use the previewed normalized value and retry |
| `INVALID_DECIMAL_FORMAT` | `amount` or `price` is not a canonical fixed-point positive decimal string | Reformat as the normalized fixed-point string and retry |
| `STP_BLOCKED` | Self-trade prevention: order would have matched the caller's own resting order on the opposite side | Cancel the resting order or revise the side |
| `INSUFFICIENT_EQUITY` | Caller's available equity for the spent token is below the order's frozen-amount requirement | Deposit more or cancel orders that have it tied up |
| `PAIR_INACTIVE` | Trading pair is not currently active | Use `GET /markets` to find a tradeable pair |
| `TRANSIENT_SETTLEMENT_FAILURE` | Catch-all for non-actionable infrastructure failures and counterparty-state changes; also the safe default for any unknown internal code | Retry; if it persists, escalate operationally |

`GET /orders/{id}.error_code` is drawn from the same set; `error` carries the display-oriented revert reason of the most recent reverted fill (free-form text — use `error_code` for logic).

## List Orders

```
GET /orders
```

**Authentication:** API Key required

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `owner_address` | address | Required | Must match the authenticated owner |
| `limit` | integer | 50 | Max 500 |
| `offset` | integer | 0 | Pagination offset |
| `status` | string | All | Filter by external status: `pending`, `matched`, `settled`, `cancelled`, `failed`. |
| `type` | string | All | `swap` or `limit` |
| `symbol` | string | All | Pair label search, for example `EURC/USDC` |
| `side` | string | All | `bid` or `ask` |
| `from_token` | string | All | Filter by resolved from-token address |
| `to_token` | string | All | Filter by resolved to-token address |
| `base_token` | string | All | Filter by base-token address |
| `quote_token` | string | All | Filter by quote-token address |
| `base_currency` | string | All | Legacy query-param name that filters on `base_symbol`, for example `EURC` |
| `quote_currency` | string | All | Legacy query-param name that filters on `quote_symbol`, for example `USDC` |
| `trade_ids` | string | All | Comma-separated trade ID list |
| `search` | string | All | Free-text search across trade fields |
| `has_error` | boolean | All | `true` returns only errored trades |
| `min_price` / `max_price` | string | All | Price range filters |
| `min_amount` / `max_amount` | string | All | Amount range filters |
| `min_remaining_amount` / `max_remaining_amount` | string | All | Remaining amount range filters |
| `min_filled_amount` / `max_filled_amount` | string | All | Filled amount range filters |
| `min_notional` / `max_notional` | string | All | Notional range filters |
| `created_after` / `created_before` | string | All | ISO8601 or timestamp-compatible created-at bounds |
| `updated_after` / `updated_before` | string | All | ISO8601 or timestamp-compatible updated-at bounds |
| `sort_by` | string | `created_at` | Sort field |
| `order` | string | `desc` | Sort direction (`asc` or `desc`) |

Filters are applied across the full matching trade set before pagination, so `total` reflects all filtered matches and you can paginate against it directly.

### Response

Each entry in `trades` has the same schema as the [Get Order](#get-order) response, including `base_token` / `quote_token`, full numeric breakdown (`remaining_amount`, `filled_base_amount`, `filled_quote_amount`, `notional`, `remaining_notional`), `vl_batch_id`, `settlement_summary`, and public-safe `settlement_economics`. `total` reflects the count after filtering, not the current page size.

```json
{
  "trades": [
    {
      "trade_id": "00000000-0000-4000-8000-000000000001",
      "owner_address": "0x...",
      "status": "pending",
      "order_type": "limit",
      "symbol": "EURC/USDC",
      "side": "bid",
      "base_symbol": "EURC",
      "quote_symbol": "USDC",
      "base_token": "0x...",
      "quote_token": "0x...",
      "price": "1.085",
      "amount": "1000.0",
      "remaining_amount": "1000.0",
      "filled_base_amount": "0",
      "filled_quote_amount": "0",
      "notional": "1085.0",
      "remaining_notional": "1085.0",
      "from_token": "0x...",
      "to_token": "0x...",
      "from_amount": "1085000000",
      "filled_amount": "0",
      "to_amount": "1000000000",
      "created_at": "2026-04-15T08:00:00+00:00",
      "updated_at": "2026-04-15T08:00:00+00:00",
      "expiration": "1713254400",
      "error": null,
      "error_code": null,
      "uuid_int": "6427948336465191935941739505432058208337171677044006212075520",
      "vl_batch_id": null,
      "settlement_summary": { "status": "pending", "total_fill_count": 0, "pending_fill_count": 0, "settled_fill_count": 0, "failed_fill_count": 0, "reverted_fill_count": 0, "latest_settlement_id": null, "latest_tx_hash": null, "latest_parent_status": null, "latest_fill_settlement_status": null, "latest_failed_fill_failure_reason": null },
      "settlement_economics": {
        "perspective_order_id": "00000000-0000-4000-8000-000000000001",
        "gross_debits": [],
        "gross_credits": [],
        "balance_debits": [],
        "balance_credits": [],
        "fees_paid": []
      }
    }
  ],
  "total": 1
}
```

## Place VL Batch

```
POST /orders/vl/batch
```

**Authentication:** EIP-712 Order signature for every sibling order

### Request Body

```json
{
  "orders": [
    {
      "owner_address": "0x...",
      "side": "bid",
      "amount": "100.0",
      "price": "0.75",
      "order_type": "limit",
      "from_address": "0x...",
      "to_address": "0x...",
      "order_id": "00000000-0000-4000-8000-000000000010",
      "uuid_int": "6427948336465191935942058520151046588146668590738473494773760",
      "signature": "0x...",
      "expiration": 1713254400
    },
    {
      "owner_address": "0x...",
      "side": "bid",
      "amount": "100.0",
      "price": "0.80",
      "order_type": "limit",
      "from_address": "0x...",
      "to_address": "0x...",
      "order_id": "00000000-0000-4000-8000-000000000011",
      "uuid_int": "6427948336465191935942079787798979146800635051651437980286977",
      "signature": "0x...",
      "expiration": 1713254400
    }
  ]
}
```

### VL Validation Rules

| Rule | Description |
|------|-------------|
| Batch size | 2 to 50 orders (active cap returned at runtime by `GET /config` under `limits.vl_batch`) |
| Same owner | All siblings must share one `owner_address` |
| Same spent token | All siblings must resolve to the same `fromToken` |
| Unique markets | Exact duplicates and inverse pairs are rejected |
| Shared group | All `uuid_int` values must share the same VL `group_id` |
| Sequential legs | `leg_id` values must be `0, 1, 2, ...` in array order |

Each sibling uses the same request semantics as `POST /orders`: `from_address` is the market base token, `to_address` is the quote token, and `expiration` is required.

### Response

```json
{
  "order_ids": [
    "00000000-0000-4000-8000-000000000010",
    "00000000-0000-4000-8000-000000000011"
  ],
  "amendments": [
    {
      "order_id": "00000000-0000-4000-8000-000000000011",
      "original_amount": "100.0",
      "actual_amount": "62.5",
      "reason": "budget_clip"
    }
  ],
  "cancelled": [],
  "fills": [
    {
      "order_id": "00000000-0000-4000-8000-000000000010",
      "trades": [
        { "quantity": "25", "price": "0.40", "counterparty_order_id": "0x...", "ts": 1713254400 }
      ],
      "remaining": "75"
    }
  ],
  "vl_group": {
    "primary_id": "00000000-0000-4000-8000-000000000010",
    "max_budget": "80",
    "budget_consumed": "10",
    "spent_token": "USDC"
  }
}
```

| Field | Description |
|-------|-------------|
| `order_ids` | Every leg's `order_id` in submission order. Always populated. |
| `amendments` | Legs the matching engine clipped at placement so they fit the shared `max_budget`. The user signed for `original_amount` but the order rests at `actual_amount`. `reason` is `"budget_clip"`. |
| `cancelled` | Legs the matching engine dropped because the budget was already exhausted by an earlier-priority sibling. `reason` is `"quota_exceeded"`. |
| `fills` | Immediate fills that landed at placement time (a leg crossing multiple makers produces multiple `trades[]` entries). This is **success**, not rejection — `remaining` is the leg's `left_amount` after the immediate fills. |
| `vl_group` | Authoritative budget snapshot: `max_budget − budget_consumed` is what future fills can still draw from. `spent_token` is the shared `fromToken` symbol. |

`amendments`, `cancelled`, and `fills` are empty lists when every leg rests at its signed size with no immediate fill. Older clients ignoring these fields keep their existing `order_ids`-only contract.

## Cancel VL Batch

```
POST /orders/vl/cancel
```

**Authentication:** EIP-712 CancelVLBatch signature

### Request Body

```json
{
  "owner_address": "0x...",
  "vl_batch_id": "00000000-0000-4000-8000-000000000010",
  "signature": "0x..."
}
```

### Response

```json
{
  "status": "ok"
}
```

`status` is the cancellation result and is typically `ok`.

## Get Fills For One Order

```
GET /fills/{order_id}
```

**Authentication:** API Key required

### Query Parameters

| Parameter | Type | Default |
|-----------|------|---------|
| `limit` | integer | 100 |
| `offset` | integer | 0 |

### Response

```json
{
  "items": [
    {
      "maker_order_id": "maker-order-id",
      "taker_order_id": "taker-order-id",
      "quantity": "100.0",
      "price": "0.75",
      "settlement_status": "pending",
      "tx_hash": null,
      "timestamp": "2026-04-15T08:00:00+00:00",
      "failure_reason": null,
      "settlement_economics": {
        "perspective_order_id": "taker-order-id",
        "gross_debits": [
          { "token": "USDC", "token_address": "0x...", "amount": "75.0", "amount_raw": "75000000" }
        ],
        "gross_credits": [
          { "token": "EURC", "token_address": "0x...", "amount": "100.0", "amount_raw": "100000000" }
        ],
        "balance_debits": [
          { "token": "USDC", "token_address": "0x...", "amount": "75.0", "amount_raw": "75000000" }
        ],
        "balance_credits": [
          { "token": "EURC", "token_address": "0x...", "amount": "100.0", "amount_raw": "100000000" }
        ],
        "fees_paid": []
      }
    }
  ]
}
```

Per-fill `settlement_status` values:

- `pending` — settlement transaction has not been broadcast yet (`tx_hash` is `null`)
- `confirming` — settlement transaction broadcast and awaiting block confirmations (`tx_hash` is populated; safe to watch on chain)
- `settled` — chain-confirmed terminal success
- `failed` — settlement-level failure
- `reverted` — per-leg revert inside the on-chain batcher; the parent settlement transaction may still have succeeded

`failure_reason`, when present, is one of a fixed allowlist of decoded contract revert names. Anything outside that allowlist is returned as `null` so the field shape stays a closed set.

Fill-level `settlement_economics` uses the same public-safe owner perspective as orders. Fill `price` is the public owner-effective price when accounting provides it. Fills may include both `maker_order_id` and `taker_order_id` as references. `tx_hash` is safe to expose because it is the on-chain settlement transaction reference.

## List Fills Across Orders

```
GET /fills
```

**Authentication:** API Key required

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `owner_address` | address | Required | Must match the authenticated owner |
| `order_status` | string | All | `pending`, `matched`, `settled`, `cancelled`, `failed` |
| `settlement_status` | string | All | `pending`, `confirming`, `settled`, `failed`, `reverted` |
| `limit` | integer | 100 | Max 500 |
| `offset` | integer | 0 | Pagination offset |

### Response

```json
{
  "items": [
    {
      "maker_order_id": "maker-order-id",
      "taker_order_id": "taker-order-id",
      "quantity": "100.0",
      "price": "0.75",
      "settlement_status": "pending",
      "tx_hash": null,
      "timestamp": "2026-04-15T08:00:00+00:00",
      "failure_reason": null,
      "settlement_economics": {
        "perspective_order_id": "taker-order-id",
        "gross_debits": [],
        "gross_credits": [],
        "balance_debits": [],
        "balance_credits": [],
        "fees_paid": []
      }
    }
  ]
}
```

---

# Swap Endpoints

- Canonical URL: https://docs.testnet.sera.cx/api-reference/endpoints/swaps/
- Source path: `docs/en/api-reference/endpoints/swaps.md`
- Description: API endpoints for one-shot routed swaps

# Swap Endpoints

## Request Swap Quote

```
POST /swap/quote
```

**Authentication:** None

### Request Body

```json
{
  "from_token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
  "to_token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
  "from_amount": "1000000",
  "owner_address": "0x...",
  "recipient": "0x...",
  "expiration": 1713254400,
  "gas_mode": "receive_less"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `from_token` | address | Yes | ERC-20 input token address |
| `to_token` | address | Yes | ERC-20 output token address |
| `from_amount` | string | Yes | Raw input amount as a positive uint256 string |
| `owner_address` | address | Yes | Wallet that will sign and fund the swap |
| `recipient` | address | Yes | Final token recipient. May differ from `owner_address` — set to any address to deliver the output to a third-party wallet. The signed Intent locks this value, and the contract enforces that the final leg pays exactly this address. |
| `expiration` | integer | Yes | Unix timestamp used as the Intent deadline; must satisfy `now < expiration <= now + 365 days - 300 seconds` |
| `gas_mode` | string | No | `receive_less` or `pay_more`; defaults to `receive_less` |

`from_token` and `to_token` must differ.

If `from_amount` is below the token floor from `GET /tokens`, the endpoint returns HTTP 400 with structured `detail.code = "AMOUNT_BELOW_MIN"` and the minimum raw / decimal amount that must be met.

### Response

```json
{
  "uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
  "route_params": {
    "taker": "0x...",
    "inputToken": "0x965d4b4546716e416e950bc30467d128455d2d0e",
    "outputToken": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
    "maxInputAmount": "1000000",
    "minOutputAmount": "803450",
    "recipient": "0x...",
    "initialDepositAmount": "1000000",
    "uuid": "6431994229952211760403847975151123456789012345678901234567",
    "deadline": 1713254400
  },
  "fee_breakdown": {
    "gas_cost_usd": "0.12",
    "gas_cost_from_token": "0.120000"
  },
  "quote_breakdown": {
    "gas_mode": "receive_less",
    "input_token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
    "output_token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
    "input_amount_before_gas": "1000000",
    "input_amount_after_gas": "1000000",
    "output_amount_before_gas": "923450",
    "output_amount_after_gas": "803450",
    "gas_cost_input_token": "120000",
    "gas_cost_output_token": "120000",
    "gas_cost_usd": "0.12"
  },
  "expires_at": 1713250830,
  "permit": {
    "permit_supported": true,
    "permit_required": true,
    "token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
    "spender": "0x...",
    "owner": "0x...",
    "value_raw": "1000000",
    "current_allowance_raw": "1000000",
    "nonce": 4,
    "suggested_deadline": 1713254400,
    "domain": {
      "name": "USD Coin",
      "version": "2",
      "chainId": 11155111,
      "verifyingContract": "0x965d4b4546716e416e950bc30467d128455d2d0e"
    },
    "eip712": {
      "domain": {
        "name": "USD Coin", "version": "2",
        "chainId": 11155111,
        "verifyingContract": "0x965d4b4546716e416e950bc30467d128455d2d0e"
      },
      "primaryType": "Permit",
      "types": {
        "Permit": [
          { "name": "owner", "type": "address" },
          { "name": "spender", "type": "address" },
          { "name": "value", "type": "uint256" },
          { "name": "nonce", "type": "uint256" },
          { "name": "deadline", "type": "uint256" }
        ]
      },
      "message": {
        "owner": "0x...",
        "spender": "0x...",
        "value": "1000000",
        "nonce": 4,
        "deadline": 1713254400
      }
    }
  }
}
```

### Response Notes

- `uuid` is the quote record ID used in `POST /swap`.
- `route_params.uuid` is the composite uint256 bound into the signed Intent.
- `expires_at` is short-lived. The current implementation stores quotes for roughly 30 seconds.
- `quote_breakdown` is for display and audit math only. All token amount fields are raw integer strings in token base units. For `receive_less`, `route_params.minOutputAmount` equals `output_amount_after_gas`; for `pay_more`, `route_params.maxInputAmount` equals `input_amount_after_gas`.
- `fee_breakdown` is the legacy gas summary with USD and input-token display values. Use `quote_breakdown` when the UI needs before/after gas amounts or output-token gas.
- Neither breakdown exposes fields outside the documented public quote schema.
- `permit` is non-null on **every wallet-deposit swap quote** (anything where `route_params.initialDepositAmount > 0`), even when the wallet already holds a standing allowance covering the spend. The server always re-signs per quote so back-to-back swaps cannot race the same allowance to zero. The `eip712` sub-object is shaped exactly as `signTypedData(domain, types, message)` expects — pass it straight to the wallet without assembling the struct. `permit.nonce` is the wallet's expected next EIP-2612 nonce *after* any swap from the same wallet already accepted by the server but not yet mined, so two back-to-back swaps queue in order for submission. `current_allowance_raw` is informational only and does not control whether a permit is required. Only equity-only swaps (`initialDepositAmount = 0`) leave `permit` as `null`.
- `permit_supported = false` indicates the input token does not implement EIP-2612 — fall back to `POST /approve` (no `eip712` block in this branch).

### Gas Modes

| Mode | Behavior |
|------|----------|
| `receive_less` | Preserve spend and let gas reduce output |
| `pay_more` | Preserve output target by increasing total input budget |

!!! note
  A quote is consumed only when a fully valid signed submission arrives. Signature mismatches, missing required permit fields, and `minOutputAmount = 0` all return `400` without burning the quote, so a benign error doesn't force you to re-quote. If another valid caller wins the race first, you receive `410`.

!!! note
    Some quotes intentionally return `route_params.minOutputAmount = "0"` when there is no executable route at the requested size. Those quotes are informational only and `POST /swap` will reject them.

!!! note
  If `permit.permit_supported` is `false`, fall back to `POST /approve` with the live `sor_address` from `GET /config`, sign the returned tx locally, broadcast it via `POST /tx/send`, and then submit `POST /swap`.

## Request Multiple Swap Quotes (Batched)

```
POST /swap/quote/batch
```

**Authentication:** None

Request up to **50 swap quotes in a single round-trip**. Useful for market-makers and traders that need to price many pairs at once — collapses repeated `/swap/quote` calls into one HTTP request.

### Request Body

```json
{
  "quotes": [
    {
      "from_token": "0x965d4b4546716e416e950bc30467d128455d2d0e",
      "to_token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
      "from_amount": "1000000",
      "owner_address": "0x...",
      "recipient": "0x...",
      "expiration": 1713254400,
      "gas_mode": "receive_less"
    }
  ]
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `quotes` | array | Yes | 1–50 quote requests; each item has the same shape as `POST /swap/quote`'s body |

Per-item field validation is identical to `POST /swap/quote`. A request that fails validation on any item (invalid address, self-trade, bad `gas_mode`, etc.) returns `422`. Per-item quote failures are surfaced as a per-item `error` envelope instead — the request itself returns `200`.

### Response

```json
{
  "items": [
    {
      "ok": true,
      "quote": {
        "uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
        "route_params": { "...": "..." },
        "fee_breakdown": { "gas_cost_usd": "0.12", "gas_cost_from_token": "0.120000" },
        "quote_breakdown": { "gas_mode": "receive_less", "...": "..." },
        "expires_at": 1713250830,
        "permit": null
      },
      "error": null
    },
    {
      "ok": false,
      "quote": null,
      "error": {
        "rejectionCategory": "no_liquidity",
        "message": "No liquidity"
      }
    }
  ]
}
```

`items[i]` corresponds positionally to `quotes[i]` — same order, same length. Each item is either a successful quote (`ok: true`) or a typed error envelope (`ok: false`).

### Error Envelope

`rejectionCategory` is a string. When the underlying error carries a typed code (the same `code` field used by `POST /swap`'s typed error envelope — e.g., `AMOUNT_BELOW_MIN`, `NO_LIQUIDITY`, `SLIPPAGE_EXCEEDED`, `INTENT_DEADLINE_EXPIRED`, `QUOTE_STALE`), it is **forwarded verbatim** so the same client switch can handle both endpoints. Batch-runtime concerns surface their own categories:

| `rejectionCategory` | When |
|---|---|
| *Public typed code* (e.g. `AMOUNT_BELOW_MIN`, `NO_LIQUIDITY`, `SLIPPAGE_EXCEEDED`) | Forwarded from the pricing engine; same vocabulary as `POST /swap`'s typed `error_code`. See the [Swap error envelope](#error-envelope) above for the action table. |
| `no_liquidity` | Pricing returned no liquidity in the legacy string form (lowercase variant; treat the same as the typed `NO_LIQUIDITY`) |
| `invalid_quote` | Other 4xx from the pricing engine that didn't carry a typed code |
| `upstream_unavailable` | Quote service temporarily unavailable |
| `quote_timeout` | A quote item exceeded the public response budget |
| `internal_error` | Unhandled server error |

### Operational Notes

- **Cap:** 50 quotes per request. Larger batches return `422` from request validation. Submit further pairs in subsequent calls.
- **Partial success:** each item is independent. A slow or unavailable pair can return its own error while siblings still succeed.
- **Pricing snapshot:** quotes are computed independently per pair — there is no cross-pair coherent snapshot. If you need atomic pricing across pairs, this endpoint is not the right tool.
- **No batch idempotency:** every successful item mints a fresh quote `uuid`. Retrying a batch produces new quote UUIDs.
- Quote UUIDs returned per item are independent — submit each via `POST /swap` with its own signed Intent, just as you would for `POST /swap/quote` results.

## Execute Swap

```
POST /swap
```

**Authentication:** EIP-712 Intent signature carried in the request body

### Request Body

```json
{
  "uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
  "signature": "0x...",
  "permit_signature": "0x...",
  "permit_deadline": 1713254400
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `uuid` | string | Yes | Quote ID returned by `POST /swap/quote` |
| `signature` | hex string | Yes | Signature over `quote.route_params` |
| `permit_signature` | hex string | Yes when `permit != null` | EIP-2612 permit signature obtained by passing `quote.permit.eip712.{domain, types, message}` to `wallet.signTypedData(...)`. Both standard 65-byte and compact 64-byte hex encodings are accepted. Omit only on equity-only swaps where `quote.permit == null` |
| `permit_deadline` | integer | Yes when `permit != null` | The `deadline` value signed into the permit (= `quote.permit.eip712.message.deadline`). Required alongside `permit_signature` |

### Response

```json
{
  "success": true,
  "trade_id": "85c92fcb-21b9-43ba-bb36-01d7b21eaa8d",
  "status": "pending",
  "fee_breakdown": {
    "gas_cost_usd": "0.12",
    "gas_cost_from_token": "0.120000"
  }
}
```

`trade_id` is the order ID you can later inspect through `GET /orders/{order_id}` or filter through `GET /orders?type=swap`.

`fee_breakdown` is optional and may be omitted on some failure or degraded-response paths.

### Intent Type To Sign

```javascript
const types = {
  Intent: [
    { name: 'taker', type: 'address' },
    { name: 'inputToken', type: 'address' },
    { name: 'outputToken', type: 'address' },
    { name: 'maxInputAmount', type: 'uint256' },
    { name: 'minOutputAmount', type: 'uint256' },
    { name: 'recipient', type: 'address' },
    { name: 'initialDepositAmount', type: 'uint256' },
    { name: 'uuid', type: 'uint256' },
    { name: 'deadline', type: 'uint48' }
  ]
};
```

Sign `quote.route_params` exactly as returned. Do not regenerate or normalize fields client-side.

### Error Envelope

`POST /swap` returns a typed envelope on every 4xx failure. The wire shape is:

```json
{
  "detail": {
    "detail": "Swap execution failed",
    "error_code": "NO_LIQUIDITY"
  }
}
```

The inner `detail` is a curated, human-readable string. The `error_code` carries the categorical signal — clients should branch on `error_code` and ignore the human string for routing logic.

| `error_code` | When it surfaces | Client action |
|--------------|------------------|---------------|
| `ALLOWANCE_INSUFFICIENT` | ERC-20 allowance to the spender is insufficient at transfer time, or the permit signature is invalid/expired | Re-permit or re-approve and retry |
| `INTENT_DEADLINE_EXPIRED` | Signed Intent's deadline passed before the tx landed | Request a fresh quote |
| `SLIPPAGE_EXCEEDED` | The matching engine rejected the swap because no crossing liquidity exists at the signed price | Widen slippage and re-quote |
| `NO_LIQUIDITY` | Route has no executable depth at the quoted price; widening slippage will not help | Reduce size or try a different pair |
| `QUOTE_STALE` | Quote snapshot or signed deadline expired before submission | Request a fresh quote |
| `AMOUNT_BELOW_MIN` | Amount is below the pair's configured minimum | Increase amount |
| `STP_BLOCKED` | Self-trade prevention — the swap would cross the caller's own resting order | Cancel the resting order or revise the side |
| `TRANSIENT_SETTLEMENT_FAILURE` | Catch-all for non-actionable infrastructure failures and counterparty-state changes | Retry; if it persists, escalate operationally |

### Error Cases

| Status | Cause |
|--------|-------|
| `400` | Invalid request, signature mismatch, missing required permit fields, or non-executable quote (`minOutputAmount = 0`) |
| `409` | Typed envelope with `error_code = "QUOTE_STALE"` — the wallet's pending swap state advanced between the quote and this submit, or the quote can no longer be accepted. The quote is **not** consumed; clients should silently re-quote and re-submit. |
| `410` | Quote expired, not found, or already consumed |
| `429` | Wallet rate limit exceeded |
| `503` | Service temporarily unavailable; retry after the `Retry-After` header |

---

# Account Endpoints

- Canonical URL: https://docs.testnet.sera.cx/api-reference/endpoints/account/
- Source path: `docs/en/api-reference/endpoints/account.md`
- Description: API endpoints for balances, deposits, withdrawals, and transfers

# Account Endpoints

## Get Balances

```
GET /balances
```

**Authentication:** API Key required

### Query Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `owner_address` | address | Yes | Must match the authenticated API-key owner |
| `include_zero` | boolean | No | Defaults to `false`. When `false`, tokens whose `total` is zero are omitted to keep the response bounded; set `true` to include every whitelisted token. |

### Response

```json
{
  "owner_address": "0x...",
  "balances": [
    {
      "token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
      "symbol": "EURC",
      "decimals": 6,
      "wallet_balance": "1250000000",
      "vault_available": "400000000",
      "vault_frozen": "100000000",
      "vault_total": "500000000",
      "total": "1750000000"
    }
  ],
  "updated_at": "2026-04-15T09:12:34.567890+00:00",
  "wallet_balance_available": true
}
```

| Field | Type | Description |
|-------|------|-------------|
| `token` | address | ERC-20 contract address |
| `symbol` | string | Token ticker |
| `decimals` | integer | Token precision; use to convert the raw strings to human-readable values |
| `wallet_balance` | string | User's wallet balance (raw uint256 decimal string) |
| `vault_available` | string | Vault balance available for trading (raw uint256 decimal string) |
| `vault_frozen` | string | Vault balance locked in open orders (raw uint256 decimal string) |
| `vault_total` | string | `vault_available + vault_frozen` |
| `total` | string | `wallet_balance + vault_total` |
| `wallet_balance_available` | boolean | `false` if the wallet RPC lookup failed |

All balance fields are raw uint256 decimal strings. Use each row's `decimals` field to convert them to human-readable values. Vault balances are authoritative; wallet balances are best-effort.

### Response Notes

- `wallet_balance_available = false` means the wallet RPC lookup failed; the vault-side numbers are still authoritative, and the wallet-side fields are returned as `0`.
- `vault_available + vault_frozen = vault_total`.
- `wallet_balance + vault_total = total`.

### Example

=== "Python"

    ```python
    import requests

    balances = requests.get(
        "https://api.testnet.sera.cx/api/v1/balances",
        params={"owner_address": "0x..."},
        headers={"Authorization": "Bearer sera_key:secret"},
        timeout=10,
    ).json()["balances"]

    for bal in balances:
        print(f"{bal['symbol']}: available={bal['vault_available']}, frozen={bal['vault_frozen']}")
    ```

=== "TypeScript"

    ```typescript
    const response = await fetch(
      "https://api.testnet.sera.cx/api/v1/balances?owner_address=0x...",
      { headers: { "Authorization": "Bearer sera_key:secret" } },
    );
    const { balances } = await response.json();

    for (const bal of balances) {
      console.log(`${bal.symbol}: available=${bal.vault_available}, frozen=${bal.vault_frozen}`);
    }
    ```

## Deposit Helpers { #deposit }

Fetch the live `vault_address` and `sor_address` from `GET /config`. Deposits now use three helpers:

1. `POST /approve` to build an ERC-20 allowance tx when you are not using permit.
2. `POST /deposit` to build `depositFund(...)` or `depositFundWithPermit(...)`.
3. `POST /tx/send` to broadcast the signed approve or deposit tx.

If the token supports EIP-2612, you can skip a separate approve by passing `permit_signature` and `permit_deadline` to `POST /deposit`. For client-side permit discovery, use `GET /permit/metadata`.

### Build Deposit Transaction

```
POST /deposit
```

**Authentication:** API Key required

#### Request Body

```json
{
  "token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
  "owner": "0x...",
  "amount": "1000000",
  "permit_signature": "0x...",
  "permit_deadline": 1713254400,
  "permit_amount": "1000000"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `token` | address | Yes | ERC-20 token address to deposit |
| `owner` | address | Yes | Depositor address; must match the authenticated API-key owner |
| `amount` | string | Yes | Raw uint256 token amount |
| `permit_signature` | hex string | No | EIP-2612 permit signature for inline allowance grant; both standard 65-byte and compact 64-byte hex encodings are accepted |
| `permit_deadline` | integer | No | Deadline signed into the permit |
| `permit_amount` | string | No | Raw uint256 permit allowance; defaults to `amount` when omitted |

When permit fields are omitted, the builder targets `depositFund(token, owner, amount)`. When permit fields are present, it targets `depositFundWithPermit(...)` instead.

#### Response

```json
{
  "tx": {
    "to": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
    "data": "0x...",
    "value": "0x0",
    "chainId": "0xaa36a7",
    "nonce": "0x2b",
    "gas": "0x13880",
    "type": "0x2",
    "maxFeePerGas": "0x...",
    "maxPriorityFeePerGas": "0x..."
  }
}
```

### Build Approve Transaction

```
POST /approve
```

**Authentication:** API Key required

#### Request Body

```json
{
  "token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
  "owner": "0x...",
  "spender": "0x3c7945840bAE0d7e7f3824Ebccef1962629250F0",
  "amount": "1000000"
}
```

`spender` must be the live Vault or SOR address from `GET /config`; other targets are rejected.

#### Response

```json
{
  "tx": {
    "to": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
    "data": "0x...",
    "value": "0x0",
    "chainId": "0xaa36a7",
    "nonce": "0x2a",
    "gas": "0xc350",
    "type": "0x2",
    "maxFeePerGas": "0x...",
    "maxPriorityFeePerGas": "0x..."
  }
}
```

### Broadcast Signed Approve Or Deposit

```
POST /tx/send
```

**Authentication:** API Key required

#### Request Body

```json
{
  "raw_tx": "0x..."
}
```

#### Response

```json
{
  "tx_hash": "0x..."
}
```

`POST /tx/send` only accepts signed approve / deposit calls that match an allowed selector-to-target pairing.

For `approve`, the encoded spender must also be the live Vault or SOR address from `GET /config`. If you submit an EIP-7702 type-4 transaction, every authorization delegate must be on the deployment's allowlist or the request is rejected.

## Withdraw Co-Signature

Instant withdrawal is a dual-signature flow: the user signs `WithdrawIntent`, the executor co-signs it, and the user then signs the final transaction.

### 1. Request Executor Co-Signature

```
POST /withdraw
```

**Authentication:** Optional. If an API key is supplied, `intent.user` must match the authenticated owner.

#### Request Body

```json
{
  "intent": {
    "user": "0x...",
    "tokens": ["0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f"],
    "amounts": ["1000000"],
    "recipient": "0x...",
    "deadline": "1713254400",
    "uuid": "123456789"
  },
  "user_signature": "0x..."
}
```

| Field | Type | Description |
|-------|------|-------------|
| `intent.user` | address | Withdrawing user's wallet |
| `intent.tokens` | address[] | Token addresses to withdraw (1-20) |
| `intent.amounts` | string[] | Per-token amounts (raw uint256 strings) |
| `intent.recipient` | address | Destination wallet for the withdrawn tokens |
| `intent.deadline` | string | Unix timestamp deadline (uint256 as string). Must be in the future and at most 365 days minus the 300-second clock-skew guard. |
| `intent.uuid` | string | Unique replay-protection identifier (uint256 as string) |
| `user_signature` | string | EIP-712 WithdrawIntent signature |

`tokens` and `amounts` must have the same length, with 1 to 20 entries. Each `amount` must be a positive uint256 string.

#### Response

```json
{
  "success": true,
  "executor_address": "0xDa6e605DB8c3221f4B3706c1da9C4E28195045f5",
  "executor_signature": "0x...",
  "error": null
}
```

### 2. Build Withdrawal Transaction

```
POST /withdraw/build
```

**Authentication:** Optional. If an API key is supplied, `intent.user` must match the authenticated owner.

#### Request Body

```json
{
  "intent": {
    "user": "0x...",
    "tokens": ["0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f"],
    "amounts": ["1000000"],
    "recipient": "0x...",
    "deadline": "1713254400",
    "uuid": "123456789"
  },
  "user_signature": "0x...",
  "executor": "0xDa6e605DB8c3221f4B3706c1da9C4E28195045f5",
  "executor_signature": "0x..."
}
```

| Field | Type | Description |
|-------|------|-------------|
| `intent` | object | Same WithdrawIntent as step 1 |
| `user_signature` | string | Your EIP-712 signature |
| `executor` | address | Co-signing executor address from step 1's response |
| `executor_signature` | string | Executor co-signature returned by step 1 |

#### Response

```json
{
  "tx": {
    "to": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
    "data": "0x...",
    "value": "0x0",
    "chainId": "0xaa36a7",
    "nonce": "0x2c",
    "gas": "0x249f0",
    "type": "0x2",
    "maxFeePerGas": "0x...",
    "maxPriorityFeePerGas": "0x..."
  }
}
```

### 3. Broadcast Signed Withdrawal

```
POST /withdraw/send
```

**Authentication:** Optional

#### Request Body

```json
{
  "raw_tx": "0x..."
}
```

#### Response

```json
{
  "tx_hash": "0x..."
}
```

`POST /withdraw/send` only accepts signed `executeInstantWithdrawDualSig(...)` calls targeting the live Sera contract from `GET /config`. If you submit an EIP-7702 type-4 transaction, every authorization delegate must be on the deployment's allowlist or the request is rejected.

## Emergency Withdrawal

If the API is unavailable, users can still withdraw directly on-chain through Sera's two-step flow:

1. Call `emergencyWithdraw(token, amount)` on the Sera contract to initiate the withdrawal request.
2. Wait ~24 hours (~7,200 blocks).
3. Call `emergencyWithdraw(token, amount)` again with the same parameters to execute.

See the [Sera.sol contract reference](../../contracts/sera.md#emergencywithdraw) for details.

## ERC-20 Transfer

The API can also build and broadcast direct ERC-20 transfers for whitelisted tokens.

### Build Transfer

```
POST /transfer
```

**Authentication:** API Key required

#### Request Body

```json
{
  "token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
  "to": "0x...",
  "amount": "1000000",
  "from_address": "0x..."
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `token` | address | Yes | ERC-20 token contract address (must be in the token registry) |
| `to` | address | Yes | Recipient address |
| `amount` | string | Yes | Amount in raw token units |
| `from_address` | address | Yes | Sender wallet address |

#### Response

```json
{
  "tx": {
    "to": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
    "data": "0x...",
    "value": "0x0",
    "chainId": "0xaa36a7",
    "nonce": "0x2d",
    "gas": "0xea60",
    "type": "0x2",
    "maxFeePerGas": "0x...",
    "maxPriorityFeePerGas": "0x..."
  }
}
```

### Broadcast Transfer

```
POST /transfer/send
```

**Authentication:** API Key required

#### Request Body

```json
{
  "raw_tx": "0x..."
}
```

#### Response

```json
{
  "tx_hash": "0x..."
}
```

---

# Contract Overview

- Canonical URL: https://docs.testnet.sera.cx/contracts/
- Source path: `docs/en/contracts/index.md`
- Description: Sera smart contract architecture

# Smart Contract Overview

Sera's settlement layer is an open-source Ethereum contract system composed of a custody vault, the core orderbook, a smart order router, and a batching wrapper.

## Contract Addresses

### Ethereum Mainnet

| Contract | Address |
|----------|---------|
| **Vault** | [`0xC7d4Fd2638e6630C8C61329878676b88A8A24D43`](https://etherscan.io/address/0xC7d4Fd2638e6630C8C61329878676b88A8A24D43) |
| **Sera** | [`0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198`](https://etherscan.io/address/0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198) |
| **SeraSOR** | [`0xa7A0cf7cd6f043fCA23f29d8ae5aae6b46e11c18`](https://etherscan.io/address/0xa7A0cf7cd6f043fCA23f29d8ae5aae6b46e11c18) |
| **SeraBatcher** | [`0x1f4b366f4145A92978df4bEeb6BdE71bC652F034`](https://etherscan.io/address/0x1f4b366f4145A92978df4bEeb6BdE71bC652F034) |

### Sepolia Testnet

| Contract | Address |
|----------|---------|
| **Vault** | `0x3c7945840bAE0d7e7f3824Ebccef1962629250F0` |
| **Sera** | `0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e` |
| **SeraSOR** | `0x83c1368110B640A729f3810De5FBe94b99aa5668` |
| **SeraBatcher** | `0x29F99C5dc36D555933700BE3dffEa6e721a27f0a` |

Addresses can drift between deployments. Fetch live values from `GET /config` instead of hardcoding.

## Source Code

All contracts are open source on GitHub and have been [independently audited](audits.md). Links below point to the audited revision.

| Contract | Source |
|----------|--------|
| **Sera.sol** | [`src/Sera.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/Sera.sol) |
| **SeraSOR.sol** | [`src/SeraSOR.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/SeraSOR.sol) |
| **SeraBatcher.sol** | [`src/SeraBatcher.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/SeraBatcher.sol) |
| **Vault.sol** | [`src/Vault.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/Vault.sol) |

## Architecture

```mermaid
flowchart TD
    User["User / API"] --> Sera["Sera.sol<br/><i>Core settlement, signatures, withdrawals</i>"]
    User --> SOR["SeraSOR.sol<br/><i>Multi-leg routed swaps</i>"]
    SOR --> Sera
    Batcher["SeraBatcher.sol<br/><i>Batch execution wrapper</i>"] --> Sera
    Batcher --> SOR
    Sera --> Vault["Vault.sol<br/><i>Custody and ledger balances</i>"]
```

## Contract Roles

- `Vault.TRADER_ROLE` is granted to Sera so only the matching engine can move tracked balances.
- `Sera.EXECUTOR_ROLE` is granted to the off-chain executor, SeraSOR, and SeraBatcher.
- `Sera.PAUSER_ROLE` and `DEFAULT_ADMIN_ROLE` stay with protocol administration.
- All admin roles are held by a Timelock contract, which is in turn owned by a multisig.
- `Sera.trustedRouter` is set to the active SeraSOR deployment.

## EIP-712 Domain

Orders, routed intents, and withdrawals are signed under the Sera domain:

```javascript
const domain = {
  name: 'Sera',
  version: '1',
  chainId: 11155111,
  verifyingContract: '0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e'
};
```

## Contracts

### Sera.sol

Core settlement contract for matching, deposits, replay protection, and withdrawals.

- `matchOrders()` settles a signed maker/taker pair.
- `depositFund()` and `depositFundWithPermit()` fund vault balances through Sera.
- `executeInstantWithdrawDualSig()` executes user-plus-executor withdrawals.
- `emergencyWithdraw()` preserves on-chain recovery if the API stack is unavailable.

[View Sera.sol Reference →](sera.md)

### SeraSOR.sol

Smart Order Router for one-shot multi-leg swaps.

- `executeIntent()` consumes a signed routed intent.
- Intermediate route balances stay transient inside the transaction instead of touching the vault.

[View SeraSOR.sol Reference →](sor.md)

### SeraBatcher.sol

Executor wrapper for best-effort and atomic batching.

- `batchMatchOrders()` continues on failure.
- `batchMatchOrdersAtomic()` reverts the whole batch on any failure.
- `batchMatchMixed()` combines atomic batches, individual matches, and routed intents.

[View SeraBatcher.sol Reference →](batcher.md)

### Vault.sol

Custody and ledger contract.

- `deposit()` and `withdraw()` move ERC-20 balances in and out.
- `transferLedger()` settles matched trades without moving physical tokens.
- `balanceOf()` exposes per-user tracked balances.

[View Vault.sol Reference →](vault.md)

## Settlement Flow

```mermaid
sequenceDiagram
    participant User
    participant Sera as Sera.sol
    participant Vault as Vault.sol

    User->>Sera: depositFund(token, owner, amount)
    Sera->>Vault: deposit(owner, token, amount)

    Sera->>Vault: transferLedger(fromUser, toUser, token, amount)
    Note over Vault: Matching updates ledger balances only

    User->>Sera: executeInstantWithdrawDualSig(intent, userSig, executorSig)
    Sera->>Vault: withdraw(user, token, amount, recipient)
    Vault-->>User: ERC-20 transfer
```

## Security Features

- **Non-custodial** — every action requires the user's signature; the protocol cannot move funds without authorization.
- **Emergency withdrawal** — users can withdraw directly on-chain even if the API is down (subject to a ~24h delay).
- **Reentrancy protection** — sensitive functions use transient reentrancy guards.
- **Pausable** — admin emergency-pause mechanism.
- **Role-based access control** — separate executor, admin, and pauser roles.

## Next Steps

<div class="grid cards" markdown>

-   :material-file-code:{ .lg .middle } **[Sera.sol](sera.md)**

    ---

    Core settlement contract reference

-   :material-routes:{ .lg .middle } **[SeraSOR.sol](sor.md)**

    ---

    Smart Order Router reference

-   :material-layers:{ .lg .middle } **[SeraBatcher.sol](batcher.md)**

    ---

    Batch execution reference

-   :material-safe:{ .lg .middle } **[Vault.sol](vault.md)**

    ---

    Asset custody reference

</div>

---

# Audit Reports

- Canonical URL: https://docs.testnet.sera.cx/contracts/audits/
- Source path: `docs/en/contracts/audits.md`
- Description: Independent security audits of Sera smart contracts

# Audit Reports

Sera's smart contracts have been independently audited. Full reports — findings, severity classifications, and remediations applied — are published in the contracts repository alongside the audited revision.

[View Audit Reports on GitHub →](https://github.com/sera-cx/orderbook-contract-v2/tree/audit/audits){ .md-button .md-button--primary }

## Audited Contracts

The audited revision of every deployed contract lives on the `audit` branch of [sera-cx/orderbook-contract-v2](https://github.com/sera-cx/orderbook-contract-v2). Source links below point directly at the audited code.

| Contract | Source | Description |
|----------|--------|-------------|
| **Sera.sol** | [`src/Sera.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/Sera.sol) | Core settlement, signatures, withdrawals |
| **SeraSOR.sol** | [`src/SeraSOR.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/SeraSOR.sol) | Smart Order Router for multi-leg routes |
| **SeraBatcher.sol** | [`src/SeraBatcher.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/SeraBatcher.sol) | Batch execution wrapper |
| **Vault.sol** | [`src/Vault.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/Vault.sol) | Asset custody and ledger balances |

## Verifying the Deployment

The on-chain addresses listed on the [Contract Overview](index.md) correspond to compiled artifacts from the audited source. You can independently verify the deployment by reading the on-chain bytecode and comparing against the build output for the `audit` branch.

## Why Audits Matter

Sera is fully non-custodial — your funds live in these contracts, not in a centralized custodian. Independent audits give you a second pair of eyes on the code that enforces deposits, matching, settlement, and withdrawals. See [Non-Custodial Design](../non-custodial.md) for the broader security model.

---

# Sera.sol

- Canonical URL: https://docs.testnet.sera.cx/contracts/sera/
- Source path: `docs/en/contracts/sera.md`
- Description: Core settlement contract reference

# Sera.sol

Sera is the core settlement contract for deposits, matching, replay protection, and withdrawals.

**Mainnet:** [`0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198`](https://etherscan.io/address/0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198)
**Sepolia:** `0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e`
**Source:** [`src/Sera.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/Sera.sol)

## Constants

```solidity
string public constant NAME = "Sera";
string public constant VERSION = "1";
uint32 public constant WITHDRAW_DELAY_BLOCKS = 7200;
uint32 public constant WITHDRAW_EXPIRATION_BLOCKS = 14400;
uint256 public constant MAX_EXPIRATION = 365 days;
uint256 constant BPS_DENOMINATOR = 100_000_000_000_000;
```

Note that `BPS_DENOMINATOR` is `100_000_000_000_000`, not `10_000` — the contract uses a higher-precision denominator inside `uint48`, so any percentage value stored against it must be scaled accordingly.

## Data Structures

### Order

```solidity
struct Order {
    address user;
    uint48 expiration;
    uint48 feeBps;
    address recipient;
    address fromToken;
    address toToken;
    uint256 fromAmount;
    uint256 toAmount;
    uint256 initialDepositAmount;
    uint256 uuid;
}
```

- `recipient = address(0)` means proceeds stay in the user's vault ledger.
- `initialDepositAmount` is used by routed swaps and can be zero for normal resting orders.
- `uuid` is the on-chain composite uint256, not the human-readable UUID string used by clients.

### MatchData

```solidity
struct MatchData {
    Order order0;          // First order
    bytes signature0;      // EIP-712 signature for order0
    uint256 matchAmount0;  // Amount of order0.fromToken to fill
    Order order1;          // Second order
    bytes signature1;      // EIP-712 signature for order1
    uint256 matchAmount1;  // Amount of order1.fromToken to fill
}
```

### WithdrawIntent

```solidity
struct WithdrawIntent {
    address user;          // Withdrawing user
    address[] tokens;      // Tokens to withdraw (1-20)
    uint256[] amounts;     // Amounts per token
    address recipient;     // Withdrawal destination
    uint256 deadline;      // Signature deadline
    uint256 uuid;          // Replay protection
}
```

## Deposit Functions

### depositFund

Deposit tokens from your wallet into the vault.

```solidity
function depositFund(address _token, address _owner, uint256 _value) public
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `_token` | address | ERC-20 token to deposit |
| `_owner` | address | Must be `msg.sender` |
| `_value` | uint256 | Amount to deposit |

**Requirements:**

- Caller must be `_owner`
- Token must be whitelisted
- Contract must not be paused

### depositFundWithPermit

Deposit with ERC-2612 permit (approve + deposit in one transaction).

```solidity
function depositFundWithPermit(
    address _token,
    address _owner,
    uint256 _permitAmount,
    uint256 _depositAmount,
    uint256 _deadline,
    bytes calldata _sig
) public
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `_token` | address | ERC-20 token |
| `_owner` | address | Depositor (must be `msg.sender`) |
| `_permitAmount` | uint256 | Amount in the permit signature |
| `_depositAmount` | uint256 | Actual deposit amount (must be ≤ `_permitAmount`) |
| `_deadline` | uint256 | Permit deadline |
| `_sig` | bytes | ERC-2612 permit signature (64 or 65 bytes) |

## Withdrawal Functions

### emergencyWithdraw

Two-step delayed withdrawal for when the API is unavailable.

```solidity
function emergencyWithdraw(address token, uint256 amount) public
```

**Flow:**

1. **First call** — Initiates a withdrawal request, recording the block number
2. **Wait** — At least 7,200 blocks (~24 hours) must pass
3. **Second call** — Executes the withdrawal with the same parameters

**Requirements:**

- Amount must be > 0
- Execution must happen within 14,400 blocks (~48 hours) of the request
- Execution amount must match the request amount

**Events:**

```solidity
event WithdrawRequested(address indexed user, address indexed token, uint256 amount, uint256 indexed requestBlock);
event Withdraw(address indexed token, address indexed to, uint256 amount);
```

!!! note
    Frozen users **can** use emergency withdrawal. This ensures funds are always recoverable.

### executeInstantWithdrawDualSig

Instant withdrawal with both user and executor signatures.

```solidity
function executeInstantWithdrawDualSig(
    WithdrawIntent calldata intent,
    bytes calldata userSignature,
    address executor,
    bytes calldata executorSignature
) external
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `intent` | WithdrawIntent | Withdrawal parameters |
| `userSignature` | bytes | User's EIP-712 WithdrawIntent signature |
| `executor` | address | Co-signing executor; must hold `EXECUTOR_ROLE` |
| `executorSignature` | bytes | Executor's EIP-712 WithdrawIntent signature |

**Requirements:**

- Deadline not expired
- UUID not previously used
- 1-20 tokens per withdrawal
- `executor` must have `EXECUTOR_ROLE`
- Both signatures must be valid (EOA or ERC-1271)

Supports multi-token withdrawal in a single transaction. Replay protection is enforced per user via the signed `uuid`.

**Events:**

```solidity
event InstantWithdraw(address indexed user, uint256 indexed uuid, address indexed token, uint256 amount, address recipient);
```

## Order Matching

### matchOrders

Settle a matched order pair. Called by authorized executors.

```solidity
function matchOrders(MatchData calldata _match, uint256 deadline) external
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `_match` | MatchData | Both orders with signatures and fill amounts |
| `deadline` | uint256 | Unix timestamp; transaction reverts if expired |

**Validation:**

- Token symmetry: `order0.fromToken == order1.toToken` and vice versa
- No self-matching (different order hashes)
- No same-token matching
- Both orders not expired
- Both signatures valid
- Sufficient vault balances
- Fill amounts within order limits
- `feeBps <= BPS_DENOMINATOR`
- Token must be whitelisted and meet its minimum order amount on first fill

**Events:**

```solidity
event OrderMatched(
    bytes32 indexed orderHash0, address indexed user0, address token0, uint256 amount0, uint256 protocolTake0,
    bytes32 indexed orderHash1, address user1, address token1, uint256 amount1, uint256 protocolTake1
);
event OrderFullyFilled(bytes32 indexed orderHash, address indexed user);
```

## State Variables

```solidity
mapping(bytes32 => uint256) public filledAmount;
mapping(address => mapping(uint256 => bool)) public isUuidExecuted;
mapping(address => mapping(uint256 => bool)) public isIntentUuidUsed;
mapping(address => mapping(address => WithdrawRequest)) public withdrawRequests;
```

`isIntentUuidUsed` is the shared replay registry that SeraSOR consumes through `consumeIntentUuid(...)`.

## EIP-712 Type Hashes

```solidity
// Order signing
bytes32 constant ORDER_TYPEHASH = keccak256(
    "Order(address user,uint48 expiration,uint48 feeBps,address recipient,address fromToken,address toToken,uint256 fromAmount,uint256 toAmount,uint256 initialDepositAmount,uint256 uuid)"
);

// Withdrawal signing
bytes32 constant WITHDRAW_INTENT_TYPEHASH = keccak256(
    "WithdrawIntent(address user,address[] tokens,uint256[] amounts,address recipient,uint256 deadline,uint256 uuid)"
);
```

### SOR Digest Helper

Sera also exposes `getIntentDigest(...)`, which returns the EIP-712 digest used by SeraSOR for routed swap intents.

## Admin Functions

| Function | Role | Description |
|----------|------|-------------|
| `setTreasury(address)` | Admin | Set the treasury address |
| `setSlippageShares(...)` | Admin | Configure on-chain slippage-share parameters |
| `batchModifyWhitelistedTokens(...)` | Admin | Whitelist/unwhitelist tokens |
| `setTrustedRouter(address)` | Admin | Set SeraSOR address |
| `rescueToken(address, address)` | Admin | Rescue accidentally sent tokens |
| `pause()` / `unpause()` | Pauser | Emergency pause/unpause |

## Errors

| Error | Cause |
|-------|-------|
| `UserFrozen` | User account is frozen |
| `AmountBelowMinimum` | Order below token's minimum amount |
| `WithdrawNotReady` | Withdrawal delay period not elapsed |
| `WithdrawExpired` | Withdrawal request expired |
| `IntentExpired` | Signature deadline passed |
| `UuidAlreadyUsed` | UUID replay detected |
| `TokenMismatch` | Order token pair doesn't match |
| `SelfMatch` | Attempting to match order against itself |
| `OrderExpired` | Order expiration timestamp passed |
| `OrderFilledAmountExceeded` | Fill would exceed remaining order amount |
| `InvalidSignature` | Signature verification failed |
| `InsufficientVaultBalance` | User doesn't have enough deposited |

---

# SeraBatcher.sol

- Canonical URL: https://docs.testnet.sera.cx/contracts/batcher/
- Source path: `docs/en/contracts/batcher.md`
- Description: Batch execution contract reference

# SeraBatcher.sol

SeraBatcher is the executor wrapper for batched matching and batched SOR execution.

**Mainnet:** [`0x1f4b366f4145A92978df4bEeb6BdE71bC652F034`](https://etherscan.io/address/0x1f4b366f4145A92978df4bEeb6BdE71bC652F034)
**Sepolia:** `0x29F99C5dc36D555933700BE3dffEa6e721a27f0a`
**Source:** [`src/SeraBatcher.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/SeraBatcher.sol)

## Constants

```solidity
uint256 public constant MAX_BATCH_SIZE = 20;
uint256 public constant MAX_INTENT_SIZE = 10;
uint256 public constant VERSION = 2;
```

## Data Structures

### AtomicBatch

```solidity
struct AtomicBatch {
    MatchData[] matches;  // All-or-nothing order pairs
}
```

### IntentExecution

```solidity
struct IntentExecution {
    MatchData[] matches;           // Route legs
    bytes intentSignature;         // Taker's SOR signature
    IntentParams intent;           // SOR parameters
    uint8 uniqueTokenCount;        // Transient hash table size hint
    uint256 permitDeadline;        // ERC-2612 permit deadline
    bytes permitSignature;         // ERC-2612 permit signature
}
```

## Functions

### batchMatchOrders

Best-effort batch matching. Individual failures don't stop subsequent matches.

```solidity
function batchMatchOrders(
    MatchData[] calldata _matches,
    uint256 deadline
) external returns (uint256 failedMask)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `_matches` | MatchData[] | Array of order pairs (max 20) |
| `deadline` | uint256 | Match expiration timestamp |

**Returns:** `failedMask` — Bitmask where bit `i` = 1 if match `i` failed.

**Events:**

```solidity
event MatchFailed(bytes32 indexed orderHash0, bytes32 indexed orderHash1, bytes reason, uint256 indexed batchIndex);
event BatchExecuted(uint256 attempted, uint256 failedMask);
```

---

### batchMatchOrdersAtomic

All-or-nothing batch matching. If any match fails, the entire transaction reverts.

```solidity
function batchMatchOrdersAtomic(
    MatchData[] calldata _matches,
    uint256 deadline
) external
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `_matches` | MatchData[] | Array of order pairs (max 20) |
| `deadline` | uint256 | Match expiration timestamp |

Use this when matches are dependent on each other and partial execution would be incorrect.

**Events:**

```solidity
event AtomicBatchExecuted(uint256 matchCount);
```

---

### batchMatchMixed

Execute atomic batches, independent matches, and SOR intents in one transaction.

```solidity
function batchMatchMixed(
    AtomicBatch[] calldata _atomicBatches,
    MatchData[] calldata _singleMatches,
    IntentExecution[] calldata _intents,
    uint256 deadline
) external returns (uint256 failedMask)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `_atomicBatches` | AtomicBatch[] | All-or-nothing sub-batches (max 20) |
| `_singleMatches` | MatchData[] | Independent order pairs (max 20, best-effort) |
| `_intents` | IntentExecution[] | SOR executions (max 10, best-effort) |
| `deadline` | uint256 | Match expiration timestamp |

**Returns:** `failedMask` — Bitmask covering all three groups sequentially.

**Execution order:**

1. Atomic batches (each independently atomic, isolated via try-catch)
2. Single matches (best-effort, continue on failure)
3. SOR intents (each independently atomic, continue on failure)

The mixed batch `failedMask` is sequential across all three sections: atomic batches first, then single matches, then routed intents.

**Events:**

```solidity
event AtomicBatchFailed(uint256 batchIndex, bytes reason);
event IntentFailed(uint256 indexed intentIndex, bytes reason);
event MatchFailed(bytes32 indexed orderHash0, bytes32 indexed orderHash1, bytes reason, uint256 indexed batchIndex);
event BatchExecuted(uint256 attempted, uint256 failedMask);
```

## Errors

| Error | Cause |
|-------|-------|
| `TooManyOrders` | More than 20 order pairs |
| `TooManyBatches` | More than 20 atomic batches |
| `TooManyIntents` | More than 10 SOR intents |
| `InvalidSORAddress` | SOR contract not configured |

---

# SeraSOR.sol

- Canonical URL: https://docs.testnet.sera.cx/contracts/sor/
- Source path: `docs/en/contracts/sor.md`
- Description: Smart Order Router contract reference

# SeraSOR.sol

SeraSOR executes multi-leg atomic routes. The taker signs a single routed intent and the executor chooses the exact route at execution time.

**Mainnet:** [`0xa7A0cf7cd6f043fCA23f29d8ae5aae6b46e11c18`](https://etherscan.io/address/0xa7A0cf7cd6f043fCA23f29d8ae5aae6b46e11c18)
**Sepolia:** `0x83c1368110B640A729f3810De5FBe94b99aa5668`
**Source:** [`src/SeraSOR.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/SeraSOR.sol)

## Constants

```solidity
uint256 public constant MAX_ROUTE_LEGS = 20;
```

## Data Structures

### IntentParams

```solidity
struct IntentParams {
    address taker;                 // wallet that signs and funds the route
    address inputToken;            // Initial wallet funding token
    address outputToken;           // Final output token
    uint256 maxInputAmount;        // Max total spend across all legs
    uint256 minOutputAmount;       // Min total output
    address recipient;             // Final output recipient
    uint256 initialDepositAmount;  // Amount to pull from wallet
    uint256 uuid;                  // Replay protection
    uint48 deadline;               // Signature deadline
}
```

The `taker` field is part of the signed payload. Older docs that omitted it are incorrect for the live contracts and public API.

## Functions

### executeIntent

Execute a multi-leg atomic route based on a taker's signed Intent.

```solidity
function executeIntent(
    MatchData[] calldata matches,
    bytes calldata intentSignature,
    IntentParams calldata intent,
    uint8 uniqueTokenCount,
    uint256 permitDeadline,
    bytes calldata permitSignature
) external
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `matches` | MatchData[] | Array of order match pairs (one per leg) |
| `intentSignature` | bytes | Taker's EIP-712 Intent signature |
| `intent` | IntentParams | Signed routing parameters |
| `uniqueTokenCount` | uint8 | Optimization hint for transient balance tracking |
| `permitDeadline` | uint256 | ERC-2612 permit deadline (0 if not using permit) |
| `permitSignature` | bytes | ERC-2612 permit signature (empty if not using permit) |

**Flow:**

1. Validate the Intent signature and check UUID hasn't been used
2. Pull `initialDepositAmount` from taker's wallet (with optional permit)
3. For each leg:
    - Match taker order against a maker order via `Sera.settleRoutedLeg()`
    - Hold intermediate outputs in transient balance
4. Verify envelope constraints:
    - Total input ≤ `maxInputAmount`
    - Total output ≥ `minOutputAmount`
    - All intermediate balances consumed (no dust)

**Example route: GBP → SGD via USD**

```
Leg 1: GBP → USD (taker sells GBP, receives USD)
Leg 2: USD → SGD (taker sells USD, receives SGD)
```

The USD is held transiently between legs and never touches the taker's vault.

**Requirements:**

- ≥1 and ≤20 legs
- Deadline not expired
- UUID not previously used
- All legs belong to the same taker
- Final leg delivers to the signed recipient
- All transient balances fully consumed

**Events:**

```solidity
event IntentMatched(bytes32 indexed intentHash, address indexed taker, uint256 legCount);
event IntentLegMatched(bytes32 indexed intentHash, uint256 indexed legIndex, bytes32 takerOrderHash, bytes32 makerOrderHash);
```

## EIP-712 Type Hash

```solidity
bytes32 constant INTENT_TYPEHASH = keccak256(
    "Intent(address taker,address inputToken,address outputToken,uint256 maxInputAmount,uint256 minOutputAmount,address recipient,uint256 initialDepositAmount,uint256 uuid,uint48 deadline)"
);
```

## State Variables

Replay protection is centralized in Sera. SeraSOR calls `sera.consumeIntentUuid(taker, uuid)` instead of keeping an isolated router-local registry.

## Errors

| Error | Cause |
|-------|-------|
| `EmptyRoute` | No legs provided |
| `TooManyLegs` | More than 20 legs |
| `InvalidRoute` | Route validation failed (token mismatch, wrong recipient, etc.) |
| `TransientBalanceNotZero` | Intermediate tokens not fully consumed |
| `InsufficientOutput` | Final output below `minOutputAmount` |
| `ExcessiveInput` | Total input exceeds `maxInputAmount` |

Replay protection is centralized in Sera: `executeIntent` calls `sera.consumeIntentUuid(taker, uuid)`, which reverts with `UuidAlreadyUsed` if the UUID was already consumed.

---

# Vault.sol

- Canonical URL: https://docs.testnet.sera.cx/contracts/vault/
- Source path: `docs/en/contracts/vault.md`
- Description: Asset custody contract reference

# Vault.sol

The vault contract holds all user funds with per-user ledger tracking. It separates asset custody from trading logic.

## Why the Vault Exists

The vault enables **gas-free continuous trading**. Without it, every order fill would require an on-chain token transfer, meaning traders pay gas on every trade. With the vault, users deposit once and their balance is tracked in an internal ledger. Fills are settled via `transferLedger` — a ledger update, not a token movement — so traders can keep placing and filling orders without additional gas costs.

This is critical for **professional market makers and financial institutions** who place hundreds or thousands of orders per day. Without the vault, each fill would cost gas, making active market-making on L1 economically unviable. The vault lets them deposit collateral once, then trade continuously at the speed of the off-chain matching engine while only paying gas on deposit and withdrawal.

**Mainnet:** [`0xC7d4Fd2638e6630C8C61329878676b88A8A24D43`](https://etherscan.io/address/0xC7d4Fd2638e6630C8C61329878676b88A8A24D43)
**Sepolia:** `0x3c7945840bAE0d7e7f3824Ebccef1962629250F0`
**Source:** [`src/Vault.sol`](https://github.com/sera-cx/orderbook-contract-v2/blob/audit/src/Vault.sol)

## Functions

### deposit

Pull tokens from a user's wallet into the vault.

```solidity
function deposit(address user, address token, uint256 amount) external
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `user` | address | Beneficiary account |
| `token` | address | ERC-20 token address |
| `amount` | uint256 | Amount to deposit |

**Requirements:**

- Caller must have `TRADER_ROLE`
- User must not be blacklisted
- Amount must be > 0
- User must have approved the vault to spend the token

On Sepolia, the active `TRADER_ROLE` holder is the Sera contract.

**Events:**

```solidity
event Deposited(address indexed token, address indexed user, uint256 amount);
```

---

### creditLedger

Credit a user's vault balance. The caller must have already transferred tokens to the vault in the same transaction.

```solidity
function creditLedger(address user, address token, uint256 expectedAmount) external
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `user` | address | Beneficiary account |
| `token` | address | ERC-20 token address |
| `expectedAmount` | uint256 | Amount to credit |

---

### withdraw

Debit a user's vault balance and transfer tokens to a recipient.

```solidity
function withdraw(address user, address token, uint256 amount, address to) external
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `user` | address | Account being debited |
| `token` | address | ERC-20 token address |
| `amount` | uint256 | Amount to withdraw |
| `to` | address | Token recipient |

**Requirements:**

- `to` must not be the zero address
- Amount must be > 0
- User balance must be sufficient

**Events:**

```solidity
event Withdrawn(address indexed token, address indexed user, uint256 amount);
```

---

### transferLedger

Internal ledger transfer between two users. No ERC-20 token movement occurs.

```solidity
function transferLedger(address fromUser, address toUser, address token, uint256 amount) external
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `fromUser` | address | Source account |
| `toUser` | address | Destination account |
| `token` | address | ERC-20 token address |
| `amount` | uint256 | Amount to transfer |

This is used internally during order settlement. The total vault TVL remains constant.

**Events:**

```solidity
event Withdrawn(address indexed token, address indexed user, uint256 amount); // fromUser
event Deposited(address indexed token, address indexed user, uint256 amount); // toUser
```

Both events are emitted to keep standard subgraph indexers in sync with the ledger move, even though no ERC-20 transfer actually happens.

---

### balanceOf

Query a user's vault balance for a specific token.

```solidity
function balanceOf(address token, address user) external view returns (uint256)
```

Query the total physical ERC-20 balance held in the vault:

```solidity
function balanceOf(address token) external view returns (uint256)
```

---

### setBlacklisted

Blacklist or unblacklist a user account.

```solidity
function setBlacklisted(address user, bool _isBlacklisted) external
```

Blacklisted users cannot deposit or receive credits, but **can still withdraw** their existing balance.

**Events:**

```solidity
event Blacklisted(address indexed user, bool isBlacklisted);
```

---

### isBlacklisted

Check if a user is blacklisted.

```solidity
function isBlacklisted(address user) external view returns (bool)
```

---

### rescueToken

Rescue tokens that were accidentally sent directly to the vault (not through deposit).

```solidity
function rescueToken(address token, address to, uint256 amount) external
```

Can only rescue surplus tokens (physical balance minus tracked balance). User-tracked funds cannot be rescued.

## Errors

| Error | Cause |
|-------|-------|
| `BlacklistedUser` | User is blacklisted (deposits/credits blocked) |
| `ZeroAmount` | Amount is 0 |
| `ZeroAddress` | Recipient or user is the zero address |
| `InsufficientBalance` | User doesn't have enough balance |
| `CannotRescueTrackedFunds` | Attempted to rescue tracked user funds |
| `InsufficientSurplus` | Rescue amount exceeds surplus |

---

# Roadmap

- Canonical URL: https://docs.testnet.sera.cx/protocol/roadmap/
- Source path: `docs/en/protocol/roadmap.md`
- Description: Sera's path from swap infrastructure to lending and derivatives

# Roadmap

Sera is being built in four deliberate motions: **Swap**, **Earn**, **Raise and Receive**, and **Accelerate**. The order matters. Spot execution creates price discovery, price discovery makes balances useful, useful balances become collateral, and collateralized positions become the substrate for derivatives.

!!! success "Current Focus"
    Sera is in the **Swap** phase today. Spot execution and settlement come first because every later phase depends on reliable corridor liquidity.

!!! info "Current Deployment"
    The live Swap stack today is CLOB execution plus on-chain settlement. FCICAMM and ERC-1155 position NFTs remain planned extensions rather than deployed contracts.

## Roadmap at a Glance

<div class="roadmap-timeline" markdown>

=== "2026 Q1: Swap"

    💸 **Swap / Settle (APIs)** · *Launch*
    Focused corridors, mainnet rails, and stablecoin partners.

=== "2026 Q2: Earn"

    📈 **Earn / Spend (FX / Pay / Cards)** · *PMF*
    Build retention, trust, and real payment flow around FX balances.

=== "2026 Q3: Raise and Receive"

    🏦 **Lend** · *Capital Efficiency*
    Expand corridor by corridor with credit and reusable collateral.

=== "2027+: Accelerate"

    🚀 **Derivatives** · *Risk Markets*
    Scale the ecosystem with hedging, leverage, and structured exposure.

</div>

---

Use the pages in this section to go deeper into the liquidity problem, the Swap architecture, and the later phases that build on top of spot execution.

- [Cold Start Problem](cold-start-problem.md) explains the target liquidity model and why Sera plans to pair a CLOB with FCICAMM.
- [Swap](swap.md) covers the current CLOB stack plus the planned FCICAMM and ERC-1155 positions NFT primitive.
- [Earn](earn.md), [Raise and Receive (Lend)](raise-and-receive.md), and [Accelerate (Derivatives)](accelerate.md) cover the later phases at a higher level.

!!! warning "Subject to Change"
    Sera is in active development. Timeline, features, and specifications are subject to change without notice. For real-time updates, follow our [Telegram](https://t.me/seraprotocol) or [X (Twitter)](https://x.com/seraprotocol).

---

# Cold Start Problem

- Canonical URL: https://docs.testnet.sera.cx/protocol/cold-start-problem/
- Source path: `docs/en/protocol/cold-start-problem.md`
- Description: Why on-chain FX liquidity is hard to bootstrap and how Sera plans to solve it

# The Cold Start Problem

New exchanges face the same circular problem: traders want liquidity before they trade, and liquidity providers want order flow before they commit capital. In on-chain FX, that loop is even harder to break because liquidity is fragmented by corridor. EUR/USD, USD/SGD, GBP/USD, and more exotic pairs each need their own dependable depth profile.

This means Sera cannot rely on one mechanism to do everything. The protocol needs a way to launch corridors with immediate executable liquidity, while still creating room for competitive market makers to tighten spreads over time.

## Why It Is Harder in FX

Spot crypto venues can sometimes get away with bootstrapping around a small number of flagship pairs. FX is different. Each corridor has its own natural users, treasury flows, volatility pattern, and market-maker appetite. A venue can look healthy on one pair and still be functionally illiquid on the next ten.

That is why Sera treats liquidity as a layered system rather than a single pool problem. The goal is not just to show TVL. The goal is to make real size executable, corridor by corridor, while letting spreads improve as organic flow arrives.

## Sera's Liquidity Model

!!! info "Current Deployment"
    The live protocol currently relies on the CLOB for executable liquidity and on-chain settlement. The FCICAMM layer described below is the planned follow-on liquidity design from the roadmap, not a contract that is live in the current deployed stack.

Sera's target liquidity model combines two complementary sources of liquidity:

| Layer | Source | Spread Profile | Primary Role |
|-------|--------|----------------|--------------|
| **CLOB** | Active market makers | Tightest available | Price discovery and competitive inside markets |
| **FCICAMM** | Passive indexed liquidity | Wider, oracle-anchored | Backstop depth, new corridor bootstrap, and large-size absorption |

The CLOB is where active participants compete. It creates the best available inside price and keeps spreads honest. In the planned full model, FCICAMM sits behind that competitive layer as structured passive inventory, making sure Sera still has executable liquidity when a corridor is new, market makers are thin, or order sizes exceed the immediately available book.

## Price Spectrum

```mermaid
graph TD
    subgraph "Liquidity Spectrum"
    direction LR

    P1["FCICAMM Buy Wall<br/>(Oracle Quote)"]
    P2["CLOB Bids<br/>(Active MMs)"]
    Mid(("Mid Market"))
    P3["CLOB Asks<br/>(Active MMs)"]
    P4["FCICAMM Sell Wall<br/>(Oracle Quote)"]

    P1 --- P2 --- Mid --- P3 --- P4
    end

    style P1 fill:#b7f7d1,stroke:#0f172a,stroke-width:1.5px,color:#000
    style P2 fill:#defce8,stroke:#0f172a,stroke-width:1px,color:#000
    style Mid fill:#ffffff,stroke:#0f172a,stroke-width:3px,color:#000
    style P3 fill:#defce8,stroke:#0f172a,stroke-width:1px,color:#000
    style P4 fill:#b7f7d1,stroke:#0f172a,stroke-width:1.5px,color:#000
```

In normal conditions, traders should execute against the CLOB first because that is where competition lives. In the target design, FCICAMM becomes more important at the edges: new corridors, larger tickets, or periods when active liquidity temporarily thins out.

## How Liquidity Can Build Over Time

1. **FCICAMM rollout:** Once introduced, FCICAMM can provide reliable backstop liquidity so a corridor is tradable from the start.
2. **Market-maker entry:** Professional makers quote tighter inside spreads on the CLOB once they see executable flow.
3. **Volume concentration:** More users route into the corridor because pricing improves and fills become more predictable.
4. **Depth compounding:** The CLOB handles the competitive inside market while FCICAMM can continue to absorb excess size and support continuity.

This is how Sera plans to address the cold start problem without depending only on incentives or hoping passive liquidity magically discovers price on its own.

## Liquidity Depth Example

<div style="background: var(--md-code-bg-color); border: 1px solid var(--md-default-fg-color--lightest); border-radius: 8px; padding: 24px; margin: 1.5em 0; text-align: center;">
    <div style="font-weight: 700; font-size: 1.1em; margin-bottom: 20px; color: var(--md-default-fg-color);">Liquidity Depth vs. Price</div>
    <svg viewBox="0 0 500 250" xmlns="http://www.w3.org/2000/svg" style="width: 100%; max-width: 1000px; height: auto; overflow: visible;">
        <line x1="50" y1="200" x2="450" y2="200" style="stroke: var(--md-default-fg-color); stroke-width: 1; opacity: 0.3;" />

        <g>
            <title>Price: 0.98, Depth: 80</title>
            <rect x="70" y="80" width="50" height="120" rx="4" style="fill: #22C55E;" />
            <text x="95" y="70" text-anchor="middle" style="font-size: 14px; font-weight: bold; fill: var(--md-default-fg-color); opacity: 0.7;">80</text>
            <text x="95" y="225" text-anchor="middle" style="font-size: 14px; fill: var(--md-default-fg-color);">0.98</text>
        </g>

        <g>
            <title>Price: 0.99, Depth: 30</title>
            <rect x="150" y="155" width="50" height="45" rx="4" style="fill: #22C55E;" />
            <text x="175" y="145" text-anchor="middle" style="font-size: 14px; font-weight: bold; fill: var(--md-default-fg-color); opacity: 0.7;">30</text>
            <text x="175" y="225" text-anchor="middle" style="font-size: 14px; fill: var(--md-default-fg-color);">0.99</text>
        </g>

        <g>
            <rect x="230" y="198" width="50" height="2" style="fill: var(--md-default-fg-color); opacity: 0.2;" />
            <text x="255" y="225" text-anchor="middle" style="font-size: 14px; fill: var(--md-default-fg-color);">1.00</text>
        </g>

        <g>
            <title>Price: 1.01, Depth: 30</title>
            <rect x="310" y="155" width="50" height="45" rx="4" style="fill: #22C55E;" />
            <text x="335" y="145" text-anchor="middle" style="font-size: 14px; font-weight: bold; fill: var(--md-default-fg-color); opacity: 0.7;">30</text>
            <text x="335" y="225" text-anchor="middle" style="font-size: 14px; fill: var(--md-default-fg-color);">1.01</text>
        </g>

        <g>
            <title>Price: 1.02, Depth: 80</title>
            <rect x="390" y="80" width="50" height="120" rx="4" style="fill: #22C55E;" />
            <text x="415" y="70" text-anchor="middle" style="font-size: 14px; font-weight: bold; fill: var(--md-default-fg-color); opacity: 0.7;">80</text>
            <text x="415" y="225" text-anchor="middle" style="font-size: 14px; fill: var(--md-default-fg-color);">1.02</text>
        </g>

        <text x="250" y="248" text-anchor="middle" style="font-size: 14px; font-style: italic; fill: var(--md-default-fg-color); opacity: 0.6;">Price (Low → High)</text>
    </svg>
</div>

!!! note
    The deeper outer bars represent the planned FCICAMM liquidity layer anchored to oracle pricing, while the inner bars represent active orderbook liquidity competing closer to the mid-market price.

---

# Swap

- Canonical URL: https://docs.testnet.sera.cx/protocol/swap/
- Source path: `docs/en/protocol/swap.md`
- Description: The current CLOB execution layer and the planned FCICAMM extension

# Swap

Swap is the first phase because Sera needs a reliable execution layer before it can build anything else. The protocol starts with a Web2 CLOB matching engine for active price discovery: market makers quote with web2 speed, users sign EIP-712 instructions, and Ethereum is used as the final settlement layer. That structure gives Sera tighter spreads, better routing, and more dependable corridor execution than asking passive liquidity alone to discover price.

!!! info "Current Deployment"
	The live Swap phase today is CLOB-first with on-chain settlement. FCICAMM and ERC-1155 position NFTs remain roadmap items and are not part of the contracts currently deployed in `orderbook-contract-v2`.

The CLOB is where active liquidity competes. It is the place for professional market makers, tighter inside prices, and fast updates as conditions change. This is the layer that establishes the tradable reference price for each corridor.

FCICAMM is planned to complement that orderbook layer rather than replace it. The orderbook handles competitive inside markets, while the future FCICAMM layer is intended to provide structured passive inventory and deterministic corridor liquidity behind it. That combination is the target design for supporting both active market making and more durable backstop liquidity in the same system.

In the planned FCICAMM extension, each position would create a **positions NFT under ERC-1155**. That matters because the protocol is not just storing liquidity internally; it is turning each position into a standardized on-chain object. Those ERC-1155 positions are intended to become the basis for later derivatives, since they make the underlying inventory, payoff profile, and ownership legible and programmable.

---

# Earn

- Canonical URL: https://docs.testnet.sera.cx/protocol/earn/
- Source path: `docs/en/protocol/earn.md`
- Description: The phase where FX balances become useful beyond execution

# Earn

Earn is the phase where Sera moves from pure execution into retention and everyday utility. Once users can swap efficiently, the next step is giving them a reason to keep balances inside the ecosystem through savings, treasury workflows, remittance, payments, and eventually spend rails such as cards.

This phase matters because persistent balances are more valuable than transient order flow. Earn turns Sera from a venue people visit only to trade into a place where capital can sit, circulate, and support repeated financial activity.

---

# Raise and Receive (Lend)

- Canonical URL: https://docs.testnet.sera.cx/protocol/raise-and-receive/
- Source path: `docs/en/protocol/raise-and-receive.md`
- Description: The lending layer that makes balances and positions productive

# Raise and Receive (Lend)

Raise and Receive is the lending phase. Instead of leaving spot balances and liquidity positions idle, Sera can let users borrow against them or fund corridor-specific credit demand. That increases capital efficiency and makes the existing liquidity base useful beyond immediate trading.

It is also the bridge between payments and capital markets. Once balances and positions can support lending, Sera expands from FX conversion into a broader financial stack where users can both deploy capital and receive it.

---

# Accelerate (Derivatives)

- Canonical URL: https://docs.testnet.sera.cx/protocol/accelerate/
- Source path: `docs/en/protocol/accelerate.md`
- Description: The phase where Sera builds risk markets on top of spot and lending

# Accelerate (Derivatives)

Accelerate is the phase where Sera layers derivatives on top of the spot and lending foundation. By this point, the protocol has executable spot prices, sticky balances, and tokenized positions that can serve as reference assets for structured risk products.

That opens the door to forwards, options, and other corridor-specific derivatives. At that stage, Sera is no longer only solving FX settlement; it is providing a fuller market for pricing, funding, and hedging currency exposure.

---

# FAQ

- Canonical URL: https://docs.testnet.sera.cx/faq/
- Source path: `docs/en/faq.md`
- Description: Frequently asked questions about Sera

# Frequently Asked Questions

## General

### What is Sera?

Sera is a stablecoin FX exchange that enables trading between stablecoins representing different fiat currencies (USD, EUR, GBP, SGD, JPY, and more). All settlement happens on Ethereum via open-source smart contracts.

### What currencies are supported?

Sera supports many fiat currencies through various stablecoin issuers. See [Supported Currency Pairs](currency-pairs.md) for the full list.

### Is Sera custodial?

No. Sera is fully non-custodial. Your funds are held in on-chain smart contracts (the Vault) and can only be moved with your cryptographic signature. Sera's off-chain services handle only order matching — they never have access to your tokens. You can withdraw at any time, even if Sera's API is unavailable, using the on-chain [emergency withdrawal](contracts/sera.md#emergencywithdraw) mechanism. See [Non-Custodial Design](non-custodial.md) for the full explanation.

## Trading

### What order types are available?

Sera supports **limit orders** (specify your price), **instant swaps** (fill-or-kill at the best available price), and **Virtual Liquidity batches** (multi-pair orders with shared collateral). See [Order Types](order-types.md) and [Virtual Liquidity](virtual-liquidity.md) for details.

### What is Virtual Liquidity?

Virtual Liquidity (VL) lets you place 2 to 50 limit orders across distinct markets backed by a single shared budget. Query `GET /config` (under `limits.vl_batch`) for the current cap. Instead of locking collateral for each order independently, a VL batch freezes only the maximum of any single order's cost. When one sibling fills, the others are automatically resized to fit the remaining budget. Exact duplicates and inverse pairs count as the same market and are rejected. See [Virtual Liquidity](virtual-liquidity.md).

### How do instant swaps work?

Instant swaps use **Smart Order Routing (SOR)** to find the best price across all available liquidity. The SOR engine routes through intermediate currencies when a direct path isn't optimal — for example, a JPY→GBP swap might route through USD if that produces a better rate.

1. Request a quote via the API — the SOR engine finds the optimal route
2. Sign the quote parameters with your wallet
3. Submit the signature to execute

Swaps are atomic — they either execute in full or are rejected entirely. See [Swap Trading](swaps.md).

### What are the fees?

Swap fees are incorporated into the quote you sign — what you see is what you pay, with gas already included. Limit order users pay Ethereum gas in real ETH at settlement time. See [Fees & Costs](fees.md).

### Can I cancel an order?

Yes. Open and partially filled orders can be cancelled using `POST /orders/cancel` with an EIP-712 CancelOrder signature. Orders are typically subject to a ~5-minute cancel cooldown after placement; hitting it returns `429`. The policy is server-side and may be relaxed for high-volume accounts.

### What happens to partially filled orders when cancelled?

The unfilled portion is returned to your vault balance. Any proceeds from the filled portion remain available in your vault.

## API

### Do I need an API key?

- **Public endpoints** (health, tokens, swap quotes) — No API key needed
- **Trading endpoints** (place order, swap) — No API key needed, but requires EIP-712 signatures
- **Read endpoints** (balances, order history) — API key required

See [Authentication](api-reference/authentication.md) for how to create API keys.

### What are the rate limits?

Public throttling is enforced at the edge proxy/CDN. Requests authenticated with an API key are additionally rate limited per wallet inside the application:

- `read`: 10 requests/second
- `trade`: 5 requests/second
- `cancel`: 2 requests/second
- `transfer`: 2 requests/second

### Is there a WebSocket API?

Not currently for public users. Use polling with the REST API for order status updates.

## Security

### How are funds secured?

Funds are held in the [Vault.sol](contracts/vault.md) smart contract with per-user ledger balances. The Vault is non-custodial — Sera's off-chain services (order matching, API) never hold or control your funds. All trading operations require EIP-712 signatures from your wallet, and all settlement happens on-chain. The smart contracts are open source and have been [independently audited](contracts/audits.md). If Sera's off-chain services ever go down, you can withdraw your funds directly on-chain via [emergency withdrawal](contracts/sera.md#emergencywithdraw).

### What if the API goes down?

You can always withdraw directly through the smart contract using the **emergency withdrawal** mechanism:

1. Call `emergencyWithdraw(token, amount)` on the Sera contract
2. Wait ~24 hours (7,200 blocks)
3. Call `emergencyWithdraw(token, amount)` again to execute

See [Emergency Withdrawal](contracts/sera.md#emergencywithdraw) for details.

### Can my account be frozen?

In rare cases (e.g., compliance requirements), accounts may be frozen. Frozen accounts **can still withdraw** their funds but cannot place new trades.

## Support

### How do I get help?

- Email: [support@sera.cx](mailto:support@sera.cx)
- Telegram: [t.me/seraprotocol](https://t.me/seraprotocol)
- X (Twitter): [@seraprotocol](https://x.com/seraprotocol)
