做市商指南¶
本指南端到端走完 Sera 的訂單生命週期:身分驗證、向 Vault 充值、下達單筆與多對限價單、監聽成交,以及撤單。本頁引用的端點皆在 Endpoints 下有詳細文件 — 本頁將它們串成可運行的流程。
文中的請求範例指向公開測試網 https://api.sera.cx/api/v1。主網形態相同;僅 EIP-712 的 chainId 與 verifyingContract 不同。
適用對象¶
您正在執行一個自動化策略:
- 在一個或多個穩定幣交易對上掛出限價單(買單和/或賣單)。
- 在行情變化時需要快速修改或撤單。
- 希望在多個相關交易對(USDC/EURC、USDC/EURT、…)之間獲得資金效率。
如果您主要是從錢包發起一次性兌換,請改用 Swaps 端點。
前置條件¶
| 要求 | 說明 |
|---|---|
| Ethereum 錢包 | 持有代幣、簽署 EIP-712 訊息。常見函式庫(ethers、viem、web3py)只需一個 signer 物件。 |
| ETH 作為 Gas | 用於 deposit、withdraw 交易,以及成交的鏈上結算。 |
| API key 與 secret | 用於查詢與輔助介面的唯讀憑證 — 透過簽署 ManageApiKey 訊息建立。詳見 身分驗證。 |
| 代幣登錄快照 | 啟動時從 GET /tokens 解析 from_token / to_token,並定期刷新。 |
| 目前限制 | 行程啟動時讀取一次 GET /config;不要硬編碼上限。 |
步驟 1 — 啟動準備¶
// 1. 啟動時一次性解析協議上限與可用代幣登錄。
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; // 參見 GET /config → limits.vl_batch
const tokenByAddress = new Map(tokens.map(t => [t.address.toLowerCase(), t]));
limits.vl_batch.{min,max} 定義一個 Virtual Liquidity 批次的合法大小。請一律從 GET /config 讀取上限,而不要在 bot 中寫死數值。
步驟 2 — 建立 API Key¶
API key 為查詢用的唯讀憑證;交易型變更仍需對每筆訂單進行 EIP-712 簽名。透過簽署 ManageApiKey 建立:
const domain = {
name: 'Sera',
version: '1',
chainId: 11155111, // Sepolia;主網使用 1
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: '做市商機器人',
}),
},
).then(r => r.json());
// 立即持久化 api_secret — 只回傳一次。
const authHeader = `Bearer ${api_key}:${api_secret}`;
列表/撤銷流程與 5 分鐘時間戳視窗請參閱 身分驗證。
步驟 3 — 向 Vault 充值¶
限價單從您的 Vault 餘額(而非錢包餘額)結算。在掛單前先將花費代幣(每筆訂單的 from_token)充入 Vault:
// 請 API 構建一筆充值交易。
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', // 原始單位(USDC:6 位小數 → 1,000 USDC)
}),
}).then(r => r.json());
// 用您的錢包/signer 簽名並廣播回傳的交易。
const sent = await signer.sendTransaction(tx);
await sent.wait();
下單前確認餘額已入帳:
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 是已被掛單鎖定的部分。容量規劃以 vault_available 為準。
步驟 4 — 下達單筆限價單¶
每筆訂單都是一個綁定到 uuid_int(從客戶端 order_id 衍生的 uint256)的已簽名 EIP-712 Order 訊息。流程:
import { v4 as uuidv4 } from 'uuid';
import { uuidToInt } from './uuid-utils'; // 見下方 "uuid_int"
const orderId = uuidv4();
const uuidInt = uuidToInt(orderId); // 256 位數值形式
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, // 市場基礎幣(如 EURC)
toToken: quoteTokenAddress, // 市場計價幣(如 USDC)
amount: parseUnits('1000', baseDecimals), // 基礎幣側數量
price: parseUnits('1.085', priceScale), // 每單位基礎幣的計價幣數量
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();
// 在 detail.error_code 上分支 — 見下方錯誤表。
throw new Error(`${detail.error_code}: ${detail.detail}`);
}
const { order_id } = await response.json();
成功回應表示訂單已進入撮合引擎。在後續 GET /orders 中可看到掛單,任何即時成交反映在 settlement_summary 中。完整欄位定義位於 訂單端點 — 下達限價單。
uuid_int¶
uuid_int 是 order_id 的複合 uint256 形式 — 撮合引擎對這個數值簽名,而非 UUID 字串。從 UUID4 一次性計算:
每次下單/撤單都必須同時提交 order_id(人類可讀形式)與 uuid_int(數值形式)。
錯誤信封¶
失敗的 POST /orders 回傳一個具型別的信封:
請依 error_code 分支,不要依人類 detail。完整表格位於 訂單端點 — 錯誤信封。
步驟 5 — 下達多對批次(虛擬流動性)¶
虛擬流動性(VL)批次同時掛出 N 個不同市場,但只凍結最大單腿成本,而非總和。例如可以同時掛 EURC/USDC 與 EURT/USDC 而無需倍增擔保品。
每個子訂單與獨立的 POST /orders 請求簽法一致,加上共享的 VL group_id 與一個嵌入 uuid_int 的順序 leg_id。API 會原子地提交它們:
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(...), // 每個 leg 的 EIP-712 Order 簽名
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());
需要留意的驗證規則(全部由伺服器端強制執行;見 下達 VL 批次):
| 規則 | 說明 |
|---|---|
| 批次大小 | 2 到 50 個子訂單。請查詢 GET /config → limits.vl_batch 取得當前上限。 |
| 同一所有者 | 每個 leg 共用同一 owner_address。 |
| 同一花費代幣 | 所有子訂單都解析到同一 fromToken。 |
| 唯一市場 | 重複或反向交易對會被拒絕。 |
| 共享分組 | 每個 leg 的 uuid_int 必須編碼同一 group_id。 |
| 順序 leg | leg_id 必須按提交順序為 0, 1, 2, …。 |
回應區分三種 per-leg 狀態 — 它們全是 成功,非拒絕:
order_ids— 實際下單的每個 leg,按提交順序。amendments— 引擎為配合共享預算而縮減的 legs(使用者對original_amount簽名,但訂單以actual_amount掛出,reason: "budget_clip")。cancelled— 因預算被更高優先級的兄弟訂單耗盡而被丟棄的 legs(reason: "quota_exceeded")。fills— 下單時立即成交的 legs(每個 leg 一筆;一個 leg 穿過 N 個掛單會產生 N 筆trades[])。vl_group—{ max_budget, budget_consumed, spent_token }。後續成交從max_budget − budget_consumed扣減。
當整批乾淨掛出時,amendments / cancelled / fills 皆為空,order_ids 與您提交的一一對應。
步驟 6 — 監控掛單與成交¶
輪詢節奏:多數做市商 bot 每 1–5 秒對帳一次。GET /orders 的形態(分頁,可按 status、symbol、side、範圍篩選)記錄在 訂單列表。
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());
需要關心的訂單狀態:
pending— 掛在訂單簿上,可能已部分成交。matched— 引擎已撮合每個 leg;等待鏈上結算。settled— 鏈上確認的終態成功。failed— 鏈上確認的回滾,或預廣播失敗。cancelled— 終態撤單。
對每筆成交的細節,GET /fills/{order_id} 回傳已撮合的 trades,包括結算狀態與對手方訂單 ID。
步驟 7 — 撤單¶
單筆撤單:
const cancelTypes = {
CancelOrder: [
{ name: 'owner', type: 'address' },
{ name: 'orderId', type: 'uint256' },
],
};
const signature = await signer.signTypedData(domain, cancelTypes, {
owner: walletAddress,
orderId: uuidInt, // 複合 uint256,非 UUID 字串
});
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,
}),
});
碰到撤單冷卻(每筆訂單預設 5 分鐘)會回傳 429 — 請退避,不要密集重試。若遺失 uuid_int,先取回訂單;它會出現在每個訂單狀態回應中。
一次性撤銷整個 VL 批次:
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, // 批次中任一 leg 的 id
signature: vlCancelSig,
}),
});
全部掛單一併撤銷:
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, '已撤單,', result.skipped_cooldown.length, '冷卻中');
cancel-all 是批量緊急按鈕 — 伺服器端迴圈執行,遵守同樣的每訂單冷卻。
步驟 8 — 提款¶
要把擔保品提回錢包,請簽署 InstantWithdraw 並提交。完整 payload 記錄於 帳戶端點 — 雙簽提款。
最佳實踐¶
- 冪等性 — 在客戶端選擇
order_id(UUID4),並將 API 呼叫視為對它冪等。網路抖動後以同一個order_id重試是安全的。 - 保持
uuid_int的索引 — 撤單與大多數對帳都需要它。下單時連同order_id一併儲存。 - 預先做餘額檢查 — 提交前呼叫
GET /balances,確認vault_available ≥ frozen_amount(order);否則 API 會拒絕(INSUFFICIENT_EQUITY)。 - 定期刷新
/config—limits.vl_batch.max(以及將來的上限)可能不另行通知地變化。至少每天讀一次。 - 關注
settlement_summary—matched不是 終態。已撮合的訂單仍可能出現回滾;在清理狀態前請依latest_failed_fill_failure_reason(顯示)與error_code(邏輯)對帳。 - 撤單冷卻以每個訂單為單位 — 頻繁閃報的 bot 需要規劃庫存,避免反覆撤同一個
order_id。