Skip to content

Virtual Liquidity

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 typed-data signature against the Sera domain. Deposits that take the permit path use the ERC-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.

The pair labels and calculations below illustrate a hypothetical base/quote orientation. For live orders, use the actual addresses and orientation returned by GET /markets; adjust side, quantity and price accordingly. The submission example below does this explicitly.

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

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.

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

Shared collateral works because the matching engine accounts for consumption against one budget and adjusts siblings as fills occur. Different markets do not by themselves prevent multiple legs from filling. Accounting reserves the maximum single-leg cost and enforces the shared remaining budget.

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

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 ✅
  • Ask from_address: USDC, to_address: GBPC @ 0.79 — fromToken = USDC ✅

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 ❌
  • Ask from_address: GBPC, to_address: USDC @ 1.27 — fromToken = GBPC ❌

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

Load the Market Maker Guide setup and VL helper. The helper previews every leg, signs the normalized message with its shared-group UUID, saves the complete request before submission and returns the full response. Read GET /config → limits.vl_batch before choosing a batch size.

This two-market example spends USDC on both legs using each returned market's orientation. Quantities are in base units and price is quote per base. The values 1000 and 1 illustrate the request flow; replace them with your intended sizes/prices in those units. The budget examples above are hypothetical market orientations, not a deployed registry.

response = requests.get(f"{API}/markets", timeout=10)
response.raise_for_status()
markets = response.json()["markets"]
selected = [m for m in markets if "USDC" in (m["base_symbol"], m["quote_symbol"])][:2]
if len(selected) < 2:
    raise RuntimeError("Two distinct active markets spending USDC are required")
expiry = int(time.time()) + 3600
legs = [{
    "side": "ask" if m["base_symbol"] == "USDC" else "bid",
    "base": {"address": m["base_address"], "decimals": m["base_decimals"]},
    "quote": {"address": m["quote_address"], "decimals": m["quote_decimals"]},
    "amount": Decimal("1000"), "price": Decimal("1"), "expiration": expiry,
} for m in selected]
batch = place_vl_batch(legs)
print(batch)  # Retain outcomes and budget status, not only order_ids.
const marketsResponse = await fetch(`${API}/markets`);
if (!marketsResponse.ok) throw new Error(`markets: ${marketsResponse.status}`);
const selected = (await marketsResponse.json()).markets.filter((m: any) =>
  [m.base_symbol, m.quote_symbol].includes("USDC")).slice(0, 2);
if (selected.length < 2) throw new Error("Two distinct active markets spending USDC are required");
const expiry = Math.floor(Date.now() / 1000) + 3600;
const legs = selected.map((m: any) => ({
  side: (m.base_symbol === "USDC" ? "ask" : "bid") as "ask" | "bid",
  base: { address: m.base_address, decimals: m.base_decimals },
  quote: { address: m.quote_address, decimals: m.quote_decimals },
  amount: "1000", price: "1", expiration: expiry,
}));
const batch = await placeVlBatch(legs);
console.log(batch); // Retain outcomes and budget status, not only order_ids.

Placement Outcomes

Field Interpretation
order_ids Submitted identities; presence alone does not establish resting size or settlement
amendments Inspect quantity_semantics: placement_total_quantity is placement total, remaining_quantity is remaining size; do not treat all actual_amount values as live remaining quantity
cancelled Placement removals and their reasons
fills Immediate matching evidence; remaining is the post-fill remainder, and on-chain settlement must still be checked
vl_group When status=confirmed, max_budget - budget_consumed is a placement-time budget snapshot; later fills can change it

When budget status is pending, budget_consumed is null or the group is absent, do not infer a zero cost or confirmed resting quantity. Empty outcome arrays and legacy missing fields are not proof of a full-size resting order. Reconcile with order/fill reads. Keep the saved request and retry the original body only after reconciliation; never regenerate IDs for an unresolved batch.

Fill & Amendment Flow

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

sequenceDiagram
    participant Sera
    participant Accounting

    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->>Accounting: Update frozen balance

Amendment Example

Starting budget: 1,500 USDC. In these hypothetical base/quote orientations:

  1. EURC/USDC bid 1000 @ 1.08 fills 500, consuming 540 USDC.
  2. Remaining budget is 960 USDC.
  3. GBPC/USDC bid 500 @ 1.27 costs 635 USDC, so its remaining quantity stays 500. It is not increased to 755.
  4. XSGD/USDC bid 2000 @ 0.75 is capped to 1280, assuming this is valid for the market's precision and signed settlement quantum.

Amendments only reduce current remaining quantity when needed. Use raw token units and the signed settlement quantum; do not indiscriminately floor to whole tokens.

Cancelling a VL Batch

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

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