跳轉至

做市商指南

本指南端到端走完 Sera 的訂單生命週期:身分驗證、向 Vault 充值、下達單筆與多對限價單、監聽成交,以及撤單。本頁引用的端點皆在 Endpoints 下有詳細文件 — 本頁將它們串成可運行的流程。

文中的請求範例指向公開測試網 https://api.sera.cx/api/v1。主網形態相同;僅 EIP-712 的 chainIdverifyingContract 不同。

適用對象

您正在執行一個自動化策略:

  • 在一個或多個穩定幣交易對上掛出限價單(買單和/或賣單)。
  • 在行情變化時需要快速修改或撤單。
  • 希望在多個相關交易對(USDC/EURC、USDC/EURT、…)之間獲得資金效率。

如果您主要是從錢包發起一次性兌換,請改用 Swaps 端點。

前置條件

要求 說明
Ethereum 錢包 持有代幣、簽署 EIP-712 訊息。常見函式庫(ethers、viem、web3py)只需一個 signer 物件。
ETH 作為 Gas 用於 depositwithdraw 交易,以及成交的鏈上結算。
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_intorder_id 的複合 uint256 形式 — 撮合引擎對這個數值簽名,而非 UUID 字串。從 UUID4 一次性計算:

function uuidToInt(uuidStr) {
  return BigInt('0x' + uuidStr.replace(/-/g, ''));
}

每次下單/撤單都必須同時提交 order_id(人類可讀形式)與 uuid_int(數值形式)。

錯誤信封

失敗的 POST /orders 回傳一個具型別的信封:

{ "detail": { "detail": "Order placement failed", "error_code": "INSUFFICIENT_EQUITY" } }

請依 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 的形態(分頁,可按 statussymbolside、範圍篩選)記錄在 訂單列表

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)。
  • 定期刷新 /configlimits.vl_batch.max(以及將來的上限)可能不另行通知地變化。至少每天讀一次。
  • 關注 settlement_summarymatched 不是 終態。已撮合的訂單仍可能出現回滾;在清理狀態前請依 latest_failed_fill_failure_reason(顯示)與 error_code(邏輯)對帳。
  • 撤單冷卻以每個訂單為單位 — 頻繁閃報的 bot 需要規劃庫存,避免反覆撤同一個 order_id

下一步