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 /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Âļ
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
- Sibling 1 (EURC/USDC Bid 1000 @ 1.08) partially fills 500 units â consumes 540 USDC
- Remaining budget: 960 USDC
- Sibling 2 (GBPC/USDC Bid 500 @ 1.27) is amended: new quantity = floor(960 / 1.27) = 755
- 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Âļ
-
Learn about limit orders and swaps
-
Understand how orders move through states
-
Fee structure for VL and standard orders