Skip to content

Order Lifecycle

Network & Compatibility

Resource Value
API base URL https://api.sera.cx/api/v1
Chain Ethereum mainnet (chainId 1)
Sera contract 0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198
Vault contract 0xC7d4Fd2638e6630C8C61329878676b88A8A24D43
SOR contract 0xa7A0cf7cd6f043fCA23f29d8ae5aae6b46e11c18

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, and many 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.

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:

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}")
const timeRes = await fetch("https://api.sera.cx/api/v1/system/time");
const { timestamp } = await timeRes.json();
console.log("Server time:", timestamp);

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:

tokens = requests.get("https://api.sera.cx/api/v1/tokens").json()["tokens"]

# Find USDC and EURC addresses
usdc = next(t for t in tokens if t["symbol"] == "USDC")
eurc = next(t for t in tokens if t["symbol"] == "EURC")
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: any) => t.symbol === "USDC");
const eurc = tokens.find((t: any) => t.symbol === "EURC");

Step 3: Place a Limit Order

Construct and sign an EIP-712 Order, then submit it. The full signing flow is documented under Authentication and Market Maker Guide → Place a Single Limit Order. Excerpt:

import time, uuid, requests

# Generate a unique order ID
order_id    = str(uuid.uuid4())
executor_id = requests.get("https://api.sera.cx/api/v1/health").json()["executor_id"]
raw         = int(uuid.UUID(order_id))
group_id    = raw >> 16
uuid_int    = str((executor_id << 252) | (raw << 124) | (group_id << 12))

# Construct the order. from_address is the market base token and
# to_address is the market quote token. Side decides which one is spent.
order = {
    "owner_address": wallet_address,
    "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":      order_id,
    "uuid_int":      uuid_int,
    "expiration":    int(time.time()) + 86_400,
}

# Sign with EIP-712 (see Authentication for full signing details)
signature = sign_order(signer, order)

result = requests.post(
    "https://api.sera.cx/api/v1/orders",
    json={**order, "signature": signature},
    timeout=10,
).json()
print("Order placed:", result["order_id"])
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 raw      = BigInt("0x" + orderId.replace(/-/g, ""));
const groupId  = raw >> 16n;
const uuidInt  = ((BigInt(executorId) << 252n) | (raw << 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) + 86_400,
};

// Sign with EIP-712 (see Authentication for full signing details)
const signature = await signOrder(signer, 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:

status = requests.get(
    f"https://api.sera.cx/api/v1/orders/{order_id}",
    headers={"Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET"},
    timeout=10,
).json()
print("Status:", status["status"])
# External statuses: "pending", "matched", "settled", "cancelled", "failed"
print("Filled:", status["filled_amount"])
print("Signed uuid_int:", status["uuid_int"])
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:

balances = requests.get(
    "https://api.sera.cx/api/v1/balances",
    params={"owner_address": wallet_address},
    headers={"Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET"},
    timeout=10,
).json()["balances"]

for bal in balances:
    print(f"{bal['symbol']}: vault_raw={bal['vault_available']}, "
          f"frozen_raw={bal['vault_frozen']}, decimals={bal['decimals']}")
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) {
  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
cancel_signature = sign_cancel_order(signer, wallet_address, int(status["uuid_int"]))

cancel_res = requests.post(
    "https://api.sera.cx/api/v1/orders/cancel",
    json={
        "owner_address": wallet_address,
        "order_id":      order_id,
        "uuid_int":      status["uuid_int"],
        "signature":     cancel_signature,
    },
    timeout=10,
).json()
print("Cancelled:", cancel_res)
// Sign a CancelOrder message
const cancelSignature = await signCancelOrder(signer, walletAddress, BigInt(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

Orders are typically subject to a ~5-minute cancel cooldown after placement; hitting it returns 429. Retry after the cooldown or fetch the order status before resubmitting.

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 use 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. Fields outside the documented response schema are not part of the public API contract.