做市商指南¶
网络与兼容性
| 资源 | 值 |
|---|---|
| API 基础 URL | https://api.testnet.sera.cx/api/v1 |
| 链 | Sepolia(chainId 11155111) |
| Sera 合约 | 0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e |
| Vault 合约 | 0x3c7945840bAE0d7e7f3824Ebccef1962629250F0 |
| SOR 合约 | 0x83c1368110B640A729f3810De5FBe94b99aa5668 |
签名原语。 所有交易型变更都是针对 Sera 域的 EIP-712 类型化数据签名。走 permit 路径的充值使用 ERC-2612 Permit 扩展 — USDC、EURC、EURT 以及多数现代稳定币支持,但并非所有 ERC-20 都支持;签名前请调用 GET /permit/metadata 检查。API key 管理使用 EIP-712 ManageApiKey 载荷。
已验证客户端。 Python eth_account >= 0.10 + requests;TypeScript ethers v6(signer.signTypedData)。已验证支持 EIP-712 类型化数据的浏览器钱包:MetaMask、Rabby、Frame、Coinbase Wallet、Trust、Rainbow。Safe 多签通过 EIP-1271 支持(消息在链上验证,而非通过 ecrecover)。
地址大小写。 读取类端点(/balances、/orders、/fills)将 owner_address 视为大小写敏感 — 请传入小写形式。EIP-712 签名载荷接受 EIP-55 校验和地址。
这是你写 bot 第一版时希望开着的页面。按顺序讲三件事:
- 端到端手动下达一笔限价单。
- 撤销刚才那笔单。
- 把两者包装成一个由你自己的报价源驱动的自动挂单循环。
下限价单前,先创建 API Key,并向 Vault 充值对应方向支出的代币:bid 支出市场 quote,ask 支出 base。方向以 GET /markets 为准;仅有钱包 USDC 余额并不足够。参见账户充值,报价前检查 vault_spendable。
需要测试代币?
注册表中的任意稳定币(USDC、EURC、GBPA、XSGD …)都可以从 Sepolia 上的 Sera 测试网水龙头免费铸造:0x4FAc3BB8B77547E2Da7ed903baDBeD2f46cBe65a。完整的可铸造代币清单见 支持的货币对。
代码片段在 Python 与 TypeScript 之间切换。挑一种就一直用 — 两份实现是逐行等价的。
准备¶
from decimal import Decimal
import json, time, uuid
import requests
from pathlib import Path
from eth_account import Account
from eth_account.messages import encode_typed_data
API = "https://api.testnet.sera.cx/api/v1"
DOMAIN = {
"name": "Sera",
"version": "1",
"chainId": 11155111, # Sepolia
"verifyingContract": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
}
PRIVATE_KEY = "0x...你的钱包私钥..." # 签名者
API_KEY = "sera_..." # 见 身份验证
API_SECRET = "..."
OWNER = Account.from_key(PRIVATE_KEY).address
AUTH = {"Authorization": f"Bearer {API_KEY}:{API_SECRET}"}
import { Wallet, TypedDataDomain, randomBytes } from "ethers";
import { mkdirSync, writeFileSync } from "node:fs";
const API = "https://api.testnet.sera.cx/api/v1";
const DOMAIN: TypedDataDomain = {
name: "Sera",
version: "1",
chainId: 11155111, // Sepolia
verifyingContract: "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
};
const PRIVATE_KEY = "0x...你的钱包私钥..."; // 签名者
const API_KEY = "sera_..."; // 见 身份验证
const API_SECRET = "...";
const wallet = new Wallet(PRIVATE_KEY);
const OWNER = wallet.address;
const AUTH = { "Authorization": `Bearer ${API_KEY}:${API_SECRET}` };
EIP-712 的 Order 类型数据结构与链上的 SeraLib.ORDER_TYPEHASH 一一对应。定义一次:
ORDER_TYPES = {
"Order": [
{"name": "user", "type": "address"},
{"name": "expiration", "type": "uint48"},
{"name": "feeBps", "type": "uint48"},
{"name": "recipient", "type": "address"},
{"name": "fromToken", "type": "address"},
{"name": "toToken", "type": "address"},
{"name": "fromAmount", "type": "uint256"},
{"name": "toAmount", "type": "uint256"},
{"name": "initialDepositAmount", "type": "uint256"},
{"name": "uuid", "type": "uint256"},
]
}
CANCEL_ORDER_TYPES = {
"CancelOrder": [
{"name": "owner", "type": "address"},
{"name": "orderId", "type": "uint256"},
]
}
const ORDER_TYPES = {
Order: [
{ name: "user", type: "address" },
{ name: "expiration", type: "uint48" },
{ name: "feeBps", type: "uint48" },
{ name: "recipient", type: "address" },
{ name: "fromToken", type: "address" },
{ name: "toToken", type: "address" },
{ name: "fromAmount", type: "uint256" },
{ name: "toAmount", type: "uint256" },
{ name: "initialDepositAmount", type: "uint256" },
{ name: "uuid", type: "uint256" },
],
};
const CANCEL_ORDER_TYPES = {
CancelOrder: [
{ name: "owner", type: "address" },
{ name: "orderId", type: "uint256" },
],
};
uuid_int 位布局¶
每笔订单的 uuid 是一个打包后的 256 位整数:
┌──────────┬───────────────────┬──────────────────┬──────────┐
│[255:252] │ [251:124] │ [123:12] │ [11:0] │
│ Executor │ Order ID │ Group ID │ Leg ID │
│ 4 bit │ 128 bit │ 112 bit │ 12 bit │
│ (0–15) │ (full UUID4) │ (UUID4 >> 16) │ (0–4095) │
└──────────┴───────────────────┴──────────────────┴──────────┘
独立单笔订单:group_id = order_id >> 16,leg_id = 0。VL 批次第 i 个 leg:所有 leg 共享同一个 group_id(取自 leg 0 的 UUID4),leg_id = i。任何其他取值都会被服务端拒绝。
executor_id 按部署设定。Sepolia 测试网默认是 0,除非你的部署另行公布。启动时读取一次后复用 — executor_id 漂移会让所有已签名但未结算的 UUID 失效。
health_response = requests.get(f"{API}/health", timeout=10)
health_response.raise_for_status()
EXECUTOR_ID = int(health_response.json()["executor_id"])
def compose_uuid(order_id: str, executor_id: int = EXECUTOR_ID,
leg_id: int = 0, group_uuid: str | None = None) -> int:
"""把 UUID4 字符串打包成链上复合 uint256。"""
oid = int(uuid.UUID(order_id))
gsrc = int(uuid.UUID(group_uuid)) if group_uuid else oid
gid = gsrc >> 16 # 128 位中的最高 112 位
return (executor_id << 252) | (oid << 124) | (gid << 12) | leg_id
def new_order_id() -> str:
return str(uuid.uuid4())
const healthResponse = await fetch(`${API}/health`);
if (!healthResponse.ok) throw new Error(`health: ${healthResponse.status}`);
const EXECUTOR_ID = BigInt((await healthResponse.json()).executor_id);
function composeUuid(
orderId: string,
executorId: bigint = EXECUTOR_ID,
legId: bigint = 0n,
groupUuid: string | null = null,
): bigint {
const oid = BigInt("0x" + orderId.replace(/-/g, ""));
const gsrc = groupUuid ? BigInt("0x" + groupUuid.replace(/-/g, "")) : oid;
const gid = gsrc >> 16n; // 128 位中的最高 112 位
return (executorId << 252n) | (oid << 124n) | (gid << 12n) | legId;
}
function newOrderId(): string {
const b = randomBytes(16);
b[6] = (b[6] & 0x0f) | 0x40; // version 4
b[8] = (b[8] & 0x3f) | 0x80; // RFC 4122 variant
const h = Buffer.from(b).toString("hex");
return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`;
}
教程 1 — 手动下达一笔限价单¶
从 GET /markets 选择活跃的 USDC/EURC 市场,并保持返回的 base/quote 方向。bid 使用报价代币买入基础代币。示例价格 1 仅用于演示,单位是 quote/base;请按该方向选择价格。反向交易对也会改变 side、数量和价格的含义。
response = requests.get(f"{API}/markets", timeout=10)
response.raise_for_status()
MARKET = next(m for m in response.json()["markets"]
if {m["base_symbol"], m["quote_symbol"]} == {"USDC", "EURC"})
BASE = {"address": MARKET["base_address"], "decimals": MARKET["base_decimals"]}
QUOTE = {"address": MARKET["quote_address"], "decimals": MARKET["quote_decimals"]}
const marketsResponse = await fetch(`${API}/markets`);
if (!marketsResponse.ok) throw new Error(`markets: ${marketsResponse.status}`);
const MARKET = (await marketsResponse.json()).markets.find((m: any) =>
[m.base_symbol, m.quote_symbol].sort().join("/") === "EURC/USDC");
if (!MARKET) throw new Error("No active USDC/EURC market");
const BASE = { address: MARKET.base_address, decimals: MARKET.base_decimals };
const QUOTE = { address: MARKET.quote_address, decimals: MARKET.quote_decimals };
发往 POST /orders 的请求使用市场基础/报价字段。bid 支出 quote,ask 支出 base;EIP-712 Order 的 fromToken 则表示实际支出代币。以下通过预览获取规范数量和签名字段。
def place_order(side: str, base: dict, quote: dict,
amount: Decimal, price: Decimal,
expiration: int) -> dict:
order_id = new_order_id()
uuid_int = compose_uuid(order_id) # deployment executor ID
preview_request = {
"owner_address": OWNER, "side": side,
"amount": format(amount, "f"), "price": format(price, "f"),
"order_type": "limit", "from_address": base["address"],
"to_address": quote["address"], "order_id": order_id,
"uuid_int": str(uuid_int), "expiration": expiration,
}
response = requests.post(f"{API}/orders/preview", json=preview_request, timeout=10)
response.raise_for_status()
preview = response.json()
signable = encode_typed_data(DOMAIN, preview["eip712_types"], preview["eip712_order"])
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
wire = {
"owner_address": OWNER,
"side": side,
"amount": preview["normalized_amount"],
"price": preview["normalized_price"],
"order_type": "limit",
"from_address": base["address"], # base,不是 spend
"to_address": quote["address"], # quote,不是 receive
"order_id": order_id,
"uuid_int": str(uuid_int),
"signature": "0x" + signature.removeprefix("0x"),
"expiration": expiration,
}
journal = Path("order-requests")
journal.mkdir(mode=0o700, exist_ok=True)
saved_path = journal / f"{order_id}.json"
with saved_path.open("x", encoding="utf-8") as saved:
json.dump(wire, saved)
print(f"Saved order request: {saved_path}")
r = requests.post(f"{API}/orders", headers={**AUTH, "Content-Type": "application/json"},
data=json.dumps(wire), timeout=10)
if r.status_code != 201:
detail = r.json().get("detail")
raise RuntimeError(f"HTTP {r.status_code}, order {order_id}: {detail}")
return {"order_id": order_id, "uuid_int": str(uuid_int)}
placed = place_order("bid", BASE, QUOTE, Decimal("1000"),
Decimal("1"), int(time.time()) + 3600)
print("placed:", placed)
async function placeOrder(
side: "bid" | "ask",
base: { address: string; decimals: number },
quote: { address: string; decimals: number },
amount: string,
price: string,
expiration: number,
) {
const orderId = newOrderId();
const uuidInt = composeUuid(orderId); // deployment executor ID
const previewResponse = await fetch(`${API}/orders/preview`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
owner_address: OWNER, side, amount, price, order_type: "limit",
from_address: base.address, to_address: quote.address,
order_id: orderId, uuid_int: uuidInt.toString(), expiration,
}),
});
if (!previewResponse.ok) throw new Error(JSON.stringify(await previewResponse.json()));
const preview = await previewResponse.json();
const signature = await wallet.signTypedData(DOMAIN, preview.eip712_types, preview.eip712_order);
const wire = {
owner_address: OWNER,
side, amount: preview.normalized_amount, price: preview.normalized_price, order_type: "limit",
from_address: base.address, // base,不是 spend
to_address: quote.address, // quote,不是 receive
order_id: orderId,
uuid_int: uuidInt.toString(),
signature,
expiration,
};
mkdirSync("order-requests", { recursive: true, mode: 0o700 });
const savedPath = `order-requests/${orderId}.json`;
writeFileSync(savedPath, JSON.stringify(wire), { flag: "wx", mode: 0o600 });
console.log(`Saved order request: ${savedPath}`);
const r = await fetch(`${API}/orders`, {
method: "POST",
headers: { ...AUTH, "Content-Type": "application/json" },
body: JSON.stringify(wire),
});
if (r.status !== 201) {
const detail = (await r.json()).detail;
throw new Error(`HTTP ${r.status}, order ${orderId}: ${JSON.stringify(detail)}`);
}
return { order_id: orderId, uuid_int: uuidInt.toString() };
}
const placed = await placeOrder("bid", BASE, QUOTE, "1000", "1",
Math.floor(Date.now() / 1000) + 3600);
console.log("placed:", placed);
确认订单已挂出:
4xx 返回的错误信封在 订单端点 → 错误信封 中有说明。请基于 error_code 分支,而不要基于人类 detail。
教程 2 — 撤销刚才那笔单¶
CancelOrder 结构只携带 owner 与 orderId(复合 uint256,不是 UUID 字符串)。下单时你已经把两者都存起来了 — 现在用上它。
def cancel_order(order_id: str, uuid_int: int) -> None:
signable = encode_typed_data(
DOMAIN, CANCEL_ORDER_TYPES,
{"owner": OWNER, "orderId": uuid_int},
)
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
r = requests.post(
f"{API}/orders/cancel",
headers={**AUTH, "Content-Type": "application/json"},
data=json.dumps({
"owner_address": OWNER,
"order_id": order_id,
"uuid_int": str(uuid_int),
"signature": "0x" + signature.removeprefix("0x"),
}),
timeout=10,
)
if r.status_code == 429:
raise RuntimeError("撤单冷却中 — 稍后重试")
r.raise_for_status()
cancel_order(placed["order_id"], int(placed["uuid_int"]))
async function cancelOrder(orderId: string, uuidInt: bigint): Promise<void> {
const signature = await wallet.signTypedData(
DOMAIN, CANCEL_ORDER_TYPES,
{ owner: OWNER, orderId: uuidInt },
);
const r = await fetch(`${API}/orders/cancel`, {
method: "POST",
headers: { ...AUTH, "Content-Type": "application/json" },
body: JSON.stringify({
owner_address: OWNER,
order_id: orderId,
uuid_int: uuidInt.toString(),
signature,
}),
});
if (r.status === 429) throw new Error("撤单冷却中 — 稍后重试");
if (!r.ok) throw new Error(`撤单失败: ${r.status}`);
}
await cancelOrder(placed.order_id, BigInt(placed.uuid_int));
每订单撤单冷却 5 分钟 — 命中冷却会返回 429。如果丢失了 uuid_int,调用 GET /orders/{order_id} 即可在响应体中取回。
教程 3 — 单交易对两面自动报价¶
这是循环:
循环:
mid ← get_mid(MARKET["symbol"]) # 你的报价源
if 首次迭代 or |mid − last_mid| / last_mid > drift_bps:
cancel(open_bid, open_ask) # 任何已过期的订单
bid_px ← mid * (1 − spread_bps/1e4)
ask_px ← mid * (1 + spread_bps/1e4)
open_bid ← place_order("bid", BASE, QUOTE, qty, bid_px, ...)
open_ask ← place_order("ask", BASE, QUOTE, qty, ask_px, ...)
last_mid ← mid
sleep(poll_seconds)
三个可调旋钮:
spread_bps— 单侧半价差。每侧 5 bps = 来回 10 bps。drift_bps— 只在 mid 移动超过该阈值时重新挂单。避免在小幅抖动时反复撤换。poll_seconds— 唤醒间隔。1–5 秒比较常见。
报价源 stub¶
报价源由你自己提供。集成点就一个函数:
循环本体¶
SPREAD_BPS = Decimal("5") # 单侧 5 bps
DRIFT_BPS = Decimal("3") # 仅当 mid 移动 > 3 bps 才重新挂单
POLL_S = 2.0
QTY = Decimal("1000") # 每侧的 base 数量
open_bid = open_ask = None
last_mid = None
while True:
try:
mid = get_mid(MARKET["symbol"])
if last_mid is not None and \
abs(mid - last_mid) / last_mid * 10_000 <= DRIFT_BPS:
time.sleep(POLL_S); continue
# 先撤再挂 — 避免跨周期自成交。
if open_bid: cancel_order(**open_bid)
if open_ask: cancel_order(**open_ask)
open_bid = open_ask = None
half = SPREAD_BPS / Decimal(10_000)
bid_px = (mid * (Decimal(1) - half)).quantize(Decimal("0.0001"))
ask_px = (mid * (Decimal(1) + half)).quantize(Decimal("0.0001"))
expiry = int(time.time()) + 3600
placed_bid = place_order("bid", BASE, QUOTE, QTY, bid_px, expiry)
open_bid = {"order_id": placed_bid["order_id"], "uuid_int": int(placed_bid["uuid_int"])}
placed_ask = place_order("ask", BASE, QUOTE, QTY, ask_px, expiry)
open_ask = {"order_id": placed_ask["order_id"], "uuid_int": int(placed_ask["uuid_int"])}
last_mid = mid
except RuntimeError as e:
# HTTP 拒绝,包含业务错误码或字符串详情。
print("place/cancel error; reconcile saved orders before restarting:", e)
break
except requests.RequestException as e:
print("network error; reconcile saved orders before restarting:", e)
break
time.sleep(POLL_S)
const SPREAD_BPS = 5; // 单侧
const DRIFT_BPS = 3;
const POLL_MS = 2_000;
const QTY = "1000"; // 每侧的 base 数量
type Live = { order_id: string; uuid_int: bigint } | null;
let openBid: Live = null, openAsk: Live = null, lastMid: number | null = null;
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
while (true) {
try {
const mid = await getMid(MARKET.symbol);
if (lastMid !== null &&
Math.abs(mid - lastMid) / lastMid * 10_000 <= DRIFT_BPS) {
await sleep(POLL_MS); continue;
}
if (openBid) await cancelOrder(openBid.order_id, openBid.uuid_int);
if (openAsk) await cancelOrder(openAsk.order_id, openAsk.uuid_int);
openBid = openAsk = null;
const half = SPREAD_BPS / 10_000;
const bidPx = (mid * (1 - half)).toFixed(4);
const askPx = (mid * (1 + half)).toFixed(4);
const expiry = Math.floor(Date.now() / 1000) + 3600;
const pb = await placeOrder("bid", BASE, QUOTE, QTY, bidPx, expiry);
openBid = { order_id: pb.order_id, uuid_int: BigInt(pb.uuid_int) };
const pa = await placeOrder("ask", BASE, QUOTE, QTY, askPx, expiry);
openAsk = { order_id: pa.order_id, uuid_int: BigInt(pa.uuid_int) };
lastMid = mid;
} catch (e) {
console.error("loop paused; reconcile saved orders before restarting:", e);
break;
}
await sleep(POLL_MS);
}
进入生产前,有几条行为值得清楚:
- 先撤再挂。 如果在撤掉旧单之前就挂新单,你的新 bid 可能会和旧的 ask 撞上 — API 会以
STP_BLOCKED拒绝。上面的循环先撤再挂。 INSUFFICIENT_EQUITY是好事。 看到它说明你冻结的 vault 余额超过了可用。减小QTY、加充值、或收紧价差。- 撤单冷却从下单时开始。 新 ID 不构成豁免。请处理 429 并等待冷却结束,当前实现没有按账户豁免。
- 清状态前先对账。
cancel_order返回 200 只表示撮合引擎接受了取消;任何进行中成交的链上结算是异步的。如果你跟踪库存,要轮询GET /orders/{id}的settlement_summary直到它进入终态,再清掉本地仓位。
恢复后再启动循环¶
单笔下单辅助函数使用本地 order-requests/ 目录(TypeScript 版本运行于 Node.js),在发送订单前保存请求文件,不保存 API 凭据或私钥。保持目录私有,完成对账前保留文件。每一侧成功后立即记录;两次调用不是原子操作。
下单或撤单报错时循环停止。重新启动前,读取已保存的 order_id、uuid_int 和签名请求,携带所有者 API Key 查询 GET /orders/{order_id}。未知提交后的一次 404 不能证明订单未被接受,应等待数据传播并完成对账,再决定是否重发原请求。DUPLICATE_ORDER_ID 要求查询已有订单,不是生成新 ID。所有不确定请求及剩余挂单完成对账后才能恢复;不要清空本地状态直接重跑循环。该教程不实现自动重启恢复。
教程 4 — 用 VL 批次做多交易对报价¶
单交易对循环在链下记账中为 bid 预留报价代币、为 ask 预留基础代币。VL 批次的各腿在不同市场支出同一代币,并共享 group_id;初始预留为最大单腿成本,不是各腿总和。
骨架不变;把单 leg 的 placeOrder 换成批量调用:
def place_vl_batch(legs: list[dict]) -> dict:
"""legs: 每个为 {side, base, quote, amount, price, expiration}。"""
leg_0_uuid = new_order_id() # group_id 的来源
signed_legs = []
for i, leg in enumerate(legs):
order_id = leg_0_uuid if i == 0 else new_order_id()
uuid_int = compose_uuid(order_id, executor_id=EXECUTOR_ID,
leg_id=i, group_uuid=leg_0_uuid)
base, quote = leg["base"], leg["quote"]
response = requests.post(f"{API}/orders/preview", json={
"owner_address": OWNER, "side": leg["side"],
"amount": format(leg["amount"], "f"), "price": format(leg["price"], "f"),
"order_type": "limit", "from_address": base["address"],
"to_address": quote["address"], "order_id": order_id,
"uuid_int": str(compose_uuid(order_id)), "expiration": leg["expiration"],
}, timeout=10)
response.raise_for_status()
preview = response.json()
order_struct = {**preview["eip712_order"], "uuid": str(uuid_int)}
signable = encode_typed_data(DOMAIN, preview["eip712_types"], order_struct)
sig = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
signed_legs.append({
"owner_address": OWNER,
"side": leg["side"],
"amount": preview["normalized_amount"],
"price": preview["normalized_price"],
"order_type": "limit",
"from_address": base["address"],
"to_address": quote["address"],
"order_id": order_id,
"uuid_int": str(uuid_int),
"signature": "0x" + sig.removeprefix("0x"),
"expiration": leg["expiration"],
})
wire = {"orders": signed_legs}
journal = Path("vl-requests")
journal.mkdir(mode=0o700, exist_ok=True)
saved_path = journal / f"{leg_0_uuid}.json"
with saved_path.open("x", encoding="utf-8") as saved:
json.dump(wire, saved)
print(f"Saved VL request: {saved_path}")
r = requests.post(
f"{API}/orders/vl/batch",
headers={**AUTH, "Content-Type": "application/json"},
data=json.dumps(wire),
timeout=10,
)
r.raise_for_status()
return r.json()
async function placeVlBatch(legs: {
side: "bid" | "ask";
base: { address: string; decimals: number };
quote: { address: string; decimals: number };
amount: string;
price: string;
expiration: number;
}[]): Promise<{ order_ids: string[]; [key: string]: unknown }> {
const leg0Uuid = newOrderId(); // group_id 的来源
const signedLegs: any[] = [];
for (let i = 0; i < legs.length; i++) {
const leg = legs[i];
const orderId = i === 0 ? leg0Uuid : newOrderId();
const uuidInt = composeUuid(orderId, EXECUTOR_ID, BigInt(i), leg0Uuid);
const previewResponse = await fetch(`${API}/orders/preview`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
owner_address: OWNER, side: leg.side, amount: leg.amount, price: leg.price,
order_type: "limit", from_address: leg.base.address, to_address: leg.quote.address,
order_id: orderId, uuid_int: composeUuid(orderId).toString(), expiration: leg.expiration,
}),
});
if (!previewResponse.ok) throw new Error(JSON.stringify(await previewResponse.json()));
const preview = await previewResponse.json();
const orderStruct = { ...preview.eip712_order, uuid: uuidInt.toString() };
const signature = await wallet.signTypedData(DOMAIN, preview.eip712_types, orderStruct);
signedLegs.push({
owner_address: OWNER,
side: leg.side, amount: preview.normalized_amount, price: preview.normalized_price,
order_type: "limit",
from_address: leg.base.address,
to_address: leg.quote.address,
order_id: orderId,
uuid_int: uuidInt.toString(),
signature, expiration: leg.expiration,
});
}
const wire = { orders: signedLegs };
mkdirSync("vl-requests", { recursive: true, mode: 0o700 });
const savedPath = `vl-requests/${leg0Uuid}.json`;
writeFileSync(savedPath, JSON.stringify(wire), { flag: "wx", mode: 0o600 });
console.log(`Saved VL request: ${savedPath}`);
const r = await fetch(`${API}/orders/vl/batch`, {
method: "POST",
headers: { ...AUTH, "Content-Type": "application/json" },
body: JSON.stringify(wire),
});
if (!r.ok) throw new Error(`VL batch failed: ${r.status}`);
const body = await r.json();
return body;
}
每个不同市场选择支出共享抵押代币的方向,按该市场返回的基础/报价方向定价。使用 POST /orders/vl/cancel 取消旧组,核对订单与结算结果后再提交替代批次。通过 GET /config → limits.vl_batch 读取活跃上限。
VL 请求在发送前保存到 vl-requests/。超时或出错后停止循环,使用原始 ID 逐腿核对保存的请求;不要再次调用生成函数来重试。核对后如需重发,使用保存的原始 body。完整响应包含下单结果和可能待确认的预算证据,按VL 结果规则处理。核对完成前请妥善保管这些私有文件。
实践要点¶
- 保存每个已签名身份。 保留
order_id、uuid_int、VL group/leg 身份和完整已签名请求。不能仅由某条腿的 order_id 重建其 VL uuid_int;本地身份丢失时先查询原订单。 - 保留提交身份。 发送前持久化订单 ID 与完整签名请求。超时后先查询该 ID;重复提交可能返回
DUPLICATE_ORDER_ID,不会保证再次返回成功。不要用新 ID 替换结果尚未确定的订单。 - 不要把
limits.vl_batch.max写死。 启动时从GET /config读,并把它当成唯一可信源。 - 清库存前先看
settlement_summary。 取消订单返回 200、或状态pending → cancelled,都不代表所有进行中的撮合在链上都落地了。轮询GET /orders/{id}直到settlement_summary.status进入终态。
下一步¶
- 身份验证 — 创建和撤销 API key。
- 订单端点 — 完整的请求/响应参考、每个字段、每个错误代码。
- 账户端点 — 向 Vault 充值、提款、查询余额。
- 虚拟流动性 — VL 批次背后的预算模型。
- 订单类型 — limit、FOK、IOC、post-only 的语义。
示例使用订单预览取得规范数量与签名字段。预览不预留资金,也不跳过最终订单/VL 校验。请从 /config、/health 读取当前签名域与 executor ID,示例部署常量不是永久标识。
对 VL 各腿,/orders/preview 只接受独立订单 UUID(leg_id=0,group 来自该订单 ID)。示例先用此临时 UUID 预览,再在签名前将返回 Order 的 uuid 替换为实际共享 group 的 VL UUID。提交时使用规范金额/价格和已签名的 VL UUID。