Skip to content

Order Lifecycle

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.

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.testnet.sera.cx/api/v1/system/time")
server_time = time_res.json()["timestamp"]
print(f"Server time: {server_time}")
const timeRes = await fetch("https://api.testnet.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.testnet.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.testnet.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 the order, preview the canonical EIP-712 payload, sign that previewed payload, 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.testnet.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",               # 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,
}

preview = requests.post(
    "https://api.testnet.sera.cx/api/v1/orders/preview",
    json=order,
    timeout=10,
).json()

# Sign the previewed EIP-712 payload (see Authentication for full details)
signature = sign_order_payload(signer, preview["eip712_order"], preview["eip712_types"])

result = requests.post(
    "https://api.testnet.sera.cx/api/v1/orders",
    json={
        **order,
        "amount": preview["normalized_amount"],
        "price": preview["normalized_price"],
        "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.testnet.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",                 // 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,
};

const preview = await fetch("https://api.testnet.sera.cx/api/v1/orders/preview", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(order),
}).then(r => r.json());

// Sign the previewed EIP-712 payload (see Authentication for full details)
const signature = await signOrderPayload(
  signer,
  preview.eip712_order,
  preview.eip712_types,
);

const response = await fetch("https://api.testnet.sera.cx/api/v1/orders", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    ...order,
    amount: preview.normalized_amount,
    price: preview.normalized_price,
    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.testnet.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.testnet.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.testnet.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.testnet.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.testnet.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.testnet.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. The policy is server-side and may be relaxed for high-volume accounts.

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.balance_debits and balance_credits for owner balance-movement displays. Public gross_debits / gross_credits remain compatibility fields and should not be treated as raw execution totals.