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:
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:
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:
POST /withdrawto obtain the executor co-signature.POST /withdraw/buildto get the unsignedexecuteInstantWithdrawDualSigtransaction.- 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/batchinstead ofPOST /orders - Cancellation — Use
POST /orders/vl/cancelto cancel the entire batch, orPOST /orders/cancelfor 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.