Skip to content

Order Lifecycle

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

Overview

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:

const timeRes = await fetch('https://api.sera.cx/api/v1/system/time');
const { timestamp } = await timeRes.json();
console.log('Server time:', timestamp);
import requests

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

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:

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

// Find USDC and EURC addresses
const usdc = tokens.find(t => t.symbol === 'USDC');
const eurc = tokens.find(t => t.symbol === 'EURC');

Step 3: Place a Limit Order

Construct and sign an EIP-712 Order, then submit it:

import { ethers } from 'ethers';
import { v4 as uuidv4 } from 'uuid';

// Generate a unique order ID
const orderId = uuidv4();
const { executor_id: executorId } = await fetch('https://api.sera.cx/api/v1/health')
  .then(r => r.json());
const rawOrderId = BigInt(`0x${orderId.replace(/-/g, '')}`);
const groupId = rawOrderId >> 16n;
const uuidInt = ((BigInt(executorId) << 252n) | (rawOrderId << 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.0',           // 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) + 86400,  // 24 hours
};

// Sign with EIP-712 (see Authentication docs for full signing details)
const signature = await signOrder(signer, order);

// Submit the order
const response = await fetch('https://api.sera.cx/api/v1/orders', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ ...order, 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:

const statusRes = await fetch(
  `https://api.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:

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

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

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

All users are subject to a 5-minute cooldown — orders must be at least 5 minutes old before they can be cancelled.

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 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.gross_debits and gross_credits for owner balance-movement displays. The public API does not expose protocol spread capture or counterparty transfer splits.