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 /configunderlimits.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:
- EURC/USDC bid 1000 @ 1.08 fills 500, consuming 540 USDC.
- Remaining budget is 960 USDC.
- GBPC/USDC bid 500 @ 1.27 costs 635 USDC, so its remaining quantity stays 500. It is not increased to 755.
- 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:
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¶
-
Learn about limit orders and swaps
-
Understand how orders move through states
-
Fee structure for VL and standard orders