Skip to content

Virtual LiquidityÂļ

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

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Âļ

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

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Âļ

Submit all sibling orders together via POST /orders/vl/batch:

const response = await fetch('https://api.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, must match signed value
        uuid_int: eurUuidInt,         // Composite uint256 derived from executor_id + UUID bits
        signature: eurSig,            // EIP-712 Order signature
        expiration: Math.floor(Date.now() / 1000) + 86400
      },
      {
        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: Math.floor(Date.now() / 1000) + 86400
      },
      {
        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: Math.floor(Date.now() / 1000) + 86400
      }
    ]
  })
});

const batch = await response.json();
// batch.order_ids   — Every sibling's order_id (always populated)
// batch.amendments  — Legs the engine clipped at placement to fit max_budget
// batch.cancelled   — Legs dropped because the budget was exhausted before they were reached
// 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:

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:

const cancelRes = await fetch('https://api.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Âļ