Market Maker GuideÂļ
This guide walks a market maker through Sera's order lifecycle end-to-end: authenticating, funding the Vault, placing single and multi-pair limit orders, monitoring fills, and cancelling. The endpoints referenced here are documented in detail under Endpoints â this page strings them together into a working flow.
Throughout, request snippets target the public testnet at https://api.sera.cx/api/v1. Mainnet uses the same shape; only the EIP-712 chainId and verifyingContract differ.
Who This Is ForÂļ
You are running an automated strategy that:
- Places resting limit orders (bids and/or asks) on one or more stablecoin pairs.
- Needs to amend or cancel quickly when the market moves.
- Wants capital efficiency across multiple correlated pairs (USDC/EURC, USDC/EURT, âĻ).
If your flow is mostly one-shot swaps from a wallet, you want the Swaps endpoints instead.
PrerequisitesÂļ
| Requirement | Notes |
|---|---|
| Ethereum wallet | Holds tokens, signs EIP-712 messages. A signer object suffices for off-the-shelf libraries (ethers, viem, web3py). |
| ETH for gas | Needed for deposit and withdraw transactions, and for on-chain settlement of fills. |
| API key + secret | Read-only credentials for queries and helpers â created by signing a ManageApiKey message. See Authentication. |
| Token registry snapshot | Resolve from_token / to_token addresses from GET /tokens at startup and refresh periodically. |
| Active limits | Read GET /config once per process; do not hard-code caps. |
Step 1 â BootstrapÂļ
// 1. Resolve protocol caps and the active token registry once at startup.
const [{ limits }, { tokens }] = await Promise.all([
fetch('https://api.sera.cx/api/v1/config').then(r => r.json()),
fetch('https://api.sera.cx/api/v1/tokens').then(r => r.json()),
]);
const vlBatchMin = limits.vl_batch.min;
const vlBatchMax = limits.vl_batch.max; // see GET /config â limits.vl_batch
const tokenByAddress = new Map(tokens.map(t => [t.address.toLowerCase(), t]));
limits.vl_batch.{min,max} defines the legal size of a Virtual Liquidity batch. Always read the cap from GET /config rather than baking a number into the bot.
Step 2 â Mint an API KeyÂļ
API keys are read-only credentials for queries; trading mutations still require an EIP-712 signature per order. Create one with a signed ManageApiKey payload:
const domain = {
name: 'Sera',
version: '1',
chainId: 11155111, // Sepolia; use 1 on mainnet
verifyingContract: '0xd0fc92d8eF9bE26D7288fCa1D6458f675e72B83a',
};
const types = {
ManageApiKey: [
{ name: 'owner', type: 'address' },
{ name: 'action', type: 'string' },
{ name: 'timestamp', type: 'uint256' },
],
};
const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(domain, types, {
owner: walletAddress,
action: 'create',
timestamp,
});
const { api_key, api_secret } = await fetch(
'https://api.sera.cx/api/v1/api-keys',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
owner_address: walletAddress,
action: 'create',
timestamp,
signature,
label: 'Market-maker bot',
}),
},
).then(r => r.json());
// Persist api_secret immediately â it is returned exactly once.
const authHeader = `Bearer ${api_key}:${api_secret}`;
See Authentication for list/revoke flows and the 5-minute timestamp window.
Step 3 â Fund the VaultÂļ
Limit orders settle against your Vault balance, not your wallet. Deposit the spent token (the from_token of every order) before placing:
// Ask the API to build a deposit transaction.
const tx = await fetch('https://api.sera.cx/api/v1/deposit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': authHeader,
},
body: JSON.stringify({
owner_address: walletAddress,
token: usdcAddress,
amount: '1000000000', // raw units (USDC: 6 decimals â 1,000 USDC)
}),
}).then(r => r.json());
// Sign and broadcast the returned transaction with your wallet/signer.
const sent = await signer.sendTransaction(tx);
await sent.wait();
Confirm the balance landed before placing orders:
const { balances } = await fetch(
`https://api.sera.cx/api/v1/balances?owner_address=${walletAddress}`,
{ headers: { 'Authorization': authHeader } },
).then(r => r.json());
for (const b of balances) {
console.log(`${b.symbol}: vault=${b.vault_available}, frozen=${b.vault_frozen}`);
}
vault_frozen is the portion locked behind resting orders. Plan capacity off vault_available.
Step 4 â Place a Single Limit OrderÂļ
Every order is a signed EIP-712 Order message bound to a uuid_int (a uint256 derived from your client-side order_id). The recipe:
import { v4 as uuidv4 } from 'uuid';
import { uuidToInt } from './uuid-utils'; // see "uuid_int" below
const orderId = uuidv4();
const uuidInt = uuidToInt(orderId); // 256-bit numeric form
const expiration = Math.floor(Date.now() / 1000) + 3600;
const orderTypes = {
Order: [
{ name: 'owner', type: 'address' },
{ name: 'fromToken', type: 'address' },
{ name: 'toToken', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'price', type: 'uint256' },
{ name: 'side', type: 'uint8' }, // 0 = bid, 1 = ask
{ name: 'orderId', type: 'uint256' },
{ name: 'expiration', type: 'uint256' },
],
};
const message = {
owner: walletAddress,
fromToken: baseTokenAddress, // market base (e.g. EURC)
toToken: quoteTokenAddress, // market quote (e.g. USDC)
amount: parseUnits('1000', baseDecimals), // base-side quantity
price: parseUnits('1.085', priceScale), // quote-per-base, scaled
side: 0, // bid
orderId: uuidInt,
expiration,
};
const signature = await signer.signTypedData(domain, orderTypes, message);
const response = await fetch('https://api.sera.cx/api/v1/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': authHeader,
},
body: JSON.stringify({
owner_address: walletAddress,
side: 'bid',
amount: '1000.0',
price: '1.085',
order_type: 'limit',
from_address: baseTokenAddress,
to_address: quoteTokenAddress,
order_id: orderId,
uuid_int: uuidInt.toString(),
signature,
expiration,
}),
});
if (!response.ok) {
const { detail } = await response.json();
// Branch on detail.error_code â see the error table below.
throw new Error(`${detail.error_code}: ${detail.detail}`);
}
const { order_id } = await response.json();
A successful response confirms the order is in the matching engine. Resting orders show up in GET /orders, and any immediate cross is reflected via the settlement_summary of subsequent fetches. Full request/response fields are in Order Endpoints â Place Limit Order.
uuid_intÂļ
uuid_int is the composite uint256 form of order_id â the matching engine signs against this, not the UUID string. Compute it once from the UUID4:
Both order_id (the human form) and uuid_int (the numeric form) must accompany every place/cancel request.
Error EnvelopeÂļ
Failed POST /orders calls return a typed envelope:
Branch on error_code, not on the human detail. The full table is documented under Order Endpoints â Error Envelope.
Step 5 â Place a Multi-Pair Batch (Virtual Liquidity)Âļ
A Virtual Liquidity (VL) batch resting on N distinct markets freezes only the largest single-leg cost, not the sum. Useful for quoting EURC/USDC and EURT/USDC simultaneously without doubling your collateral.
Each sibling is signed exactly like a standalone POST /orders request, plus a shared VL group_id and a sequential leg_id baked into the uuid_int. The API then submits them atomically:
const groupId = uuidv4();
const siblings = pairs.map((pair, idx) => ({
owner_address: walletAddress,
side: 'bid',
amount: '1000.0',
price: pair.bid,
order_type: 'limit',
from_address: pair.base,
to_address: pair.quote,
order_id: uuidv4(),
uuid_int: composeVlUuidInt(groupId, idx).toString(),
signature: signSibling(...), // per-leg EIP-712 Order signature
expiration: Math.floor(Date.now() / 1000) + 3600,
}));
const result = await fetch('https://api.sera.cx/api/v1/orders/vl/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': authHeader },
body: JSON.stringify({ orders: siblings }),
}).then(r => r.json());
Validation rules to mind (all enforced server-side; see Place VL Batch):
| Rule | Notes |
|---|---|
| Batch size | 2 to 50 siblings. Query GET /config â limits.vl_batch for the active cap. |
| Same owner | One owner_address across every leg. |
| Same spent token | All siblings resolve to the same fromToken. |
| Unique markets | Duplicates and inverse pairs are rejected. |
| Shared group | Every leg's uuid_int must encode the same group_id. |
| Sequential legs | leg_id values must be 0, 1, 2, âĻ in submission order. |
The response distinguishes three states per leg â they are all success, not rejection:
order_idsâ every leg actually placed, in submission order.amendmentsâ legs the engine clipped down so the batch fits the shared budget (the user signed fororiginal_amount; the order rests atactual_amount,reason: "budget_clip").cancelledâ legs dropped because the budget was exhausted by an earlier-priority sibling (reason: "quota_exceeded").fillsâ legs that crossed immediately at placement (one entry per leg; a leg crossing N makers produces Ntrades[]entries).vl_groupâ{ max_budget, budget_consumed, spent_token }. Future fills draw frommax_budget â budget_consumed.
When the whole batch rests cleanly, amendments / cancelled / fills are empty and order_ids matches what you submitted 1:1.
Step 6 â Monitor Open Orders and FillsÂļ
Polling cadence: most market-maker bots reconcile every 1â5 seconds. The shape of GET /orders (paginated, filterable on status, symbol, side, ranges) is documented under List Orders.
const { trades, total } = await fetch(
`https://api.sera.cx/api/v1/orders?owner_address=${walletAddress}&status=pending&limit=500`,
{ headers: { 'Authorization': authHeader } },
).then(r => r.json());
The order statuses you care about:
pendingâ resting on the book, possibly partially filled.matchedâ engine has crossed every leg; awaiting on-chain settlement.settledâ chain-confirmed terminal success.failedâ chain-confirmed revert, or a pre-broadcast failure.cancelledâ terminal cancel.
For per-fill detail, GET /fills/{order_id} returns the matched trades, including settlement status and counterparty order IDs.
Step 7 â CancelÂļ
One order:
const cancelTypes = {
CancelOrder: [
{ name: 'owner', type: 'address' },
{ name: 'orderId', type: 'uint256' },
],
};
const signature = await signer.signTypedData(domain, cancelTypes, {
owner: walletAddress,
orderId: uuidInt, // composite uint256, not the UUID string
});
await fetch('https://api.sera.cx/api/v1/orders/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': authHeader },
body: JSON.stringify({
owner_address: walletAddress,
order_id: orderId,
uuid_int: uuidInt.toString(),
signature,
}),
});
Hitting the cancel cooldown (default 5 minutes per order) returns 429 â back off, do not retry tight. If you lost uuid_int, fetch the order first; it is returned in every order status response.
A whole VL batch in one shot:
await fetch('https://api.sera.cx/api/v1/orders/vl/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': authHeader },
body: JSON.stringify({
owner_address: walletAddress,
vl_batch_id: primaryOrderId, // any leg's id of the batch
signature: vlCancelSig,
}),
});
Everything you have resting:
const url = `https://api.sera.cx/api/v1/orders/cancel-all?owner_address=${walletAddress}`;
const result = await fetch(url, {
method: 'DELETE',
headers: { 'Authorization': authHeader },
}).then(r => r.json());
console.log(result.cancelled.length, 'cancelled,', result.skipped_cooldown.length, 'on cooldown');
cancel-all is the bulk panic button â it iterates server-side and respects the same per-order cooldown.
Step 8 â WithdrawÂļ
When you want to pull collateral back to your wallet, sign an InstantWithdraw and submit. The full payload is documented under Account Endpoints â Withdraw (co-signature).
Best PracticesÂļ
- Idempotency â pick
order_id(UUID4) on the client side and treat the API call as idempotent on it. Retrying a request with the sameorder_idafter a network blip is safe. - Keep
uuid_intindexed â every cancel and most reconciliation requires it. Store it alongsideorder_idat placement time. - Pre-flight balance checks â call
GET /balancesand confirmvault_available âĨ frozen_amount(order)before submitting; the API will reject otherwise (INSUFFICIENT_EQUITY). - Refresh
/configperiodically âlimits.vl_batch.max(and future caps) may change without notice. Re-read at least daily. - Watch the
settlement_summaryâmatchedis not terminal. A matched order can still surface a revert; reconcile againstlatest_failed_fill_failure_reason(display) anderror_code(logic) before clearing state. - Cancel cooldown is per order â bots that flicker quotes need to size their inventory so they are not constantly cancelling the same
order_id.
Next StepsÂļ
- Order Endpoints â full reference for every
/ordersroute, fields, and error codes. - Authentication â API-key management and EIP-712 signing details.
- Virtual Liquidity â conceptual primer on the VL budget model.
- Order Types â limit, FOK, IOC, and post-only semantics.