做市商指南¶
本指南从端到端走完 Sera 的订单生命周期:身份验证、向 Vault 充值、下达单笔与多对限价单、监听成交,以及撤单。本页引用的所有端点都在 Endpoints 下有详细文档 — 本页将它们串联成可运行的流程。
文中的请求示例指向公开测试网 https://api.sera.cx/api/v1。主网形态相同;仅 EIP-712 的 chainId 与 verifyingContract 不同。
适用对象¶
您正在运行一个自动化策略:
- 在一个或多个稳定币交易对上挂出限价单(买单和/或卖单)。
- 在行情变化时需要快速修改或撤单。
- 希望在多个相关交易对(USDC/EURC、USDC/EURT、…)之间获得资金效率。
如果您主要是从钱包发起一次性兑换,请改用 Swaps 端点。
前置条件¶
| 要求 | 说明 |
|---|---|
| 以太坊钱包 | 持有代币、签署 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。