Skip to content

Market Maker GuideÂļ

Network & Compatibility

Resource Value
API base URL https://api.sera.cx/api/v1
Chain Ethereum mainnet (chainId 1)
Sera contract 0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198
Vault contract 0xC7d4Fd2638e6630C8C61329878676b88A8A24D43
SOR contract 0xa7A0cf7cd6f043fCA23f29d8ae5aae6b46e11c18

Signing primitives. Every trading mutation is an EIP-712 typed-data signature against the Sera domain. Deposits that take the permit path use the ERC-2612 Permit extension — supported by USDC, EURC, and many modern stablecoins, not all ERC-20s; call GET /permit/metadata to check support before signing. API-key management uses an EIP-712 ManageApiKey payload.

Tested clients. Python eth_account >= 0.10 + requests; TypeScript ethers v6 (signer.signTypedData). Browser wallets confirmed working with EIP-712 typed data: MetaMask, Rabby, Frame, Coinbase Wallet, Trust, Rainbow. Safe multisigs work via EIP-1271 (the message is verified on-chain rather than via ecrecover).

Address casing. Read endpoints (/balances, /orders, /fills) treat owner_address as case-sensitive — pass the lowercase form. EIP-712 signed payloads accept EIP-55 checksum addresses.

This is the page you want open while you write the first version of your bot. It walks through three things in order:

  1. Place a single limit order manually, end-to-end.
  2. Cancel that order.
  3. Wrap both into an automated quoting loop driven by your own rate source.

Setup that lives elsewhere — minting an API key, funding the Vault, withdrawing later — is linked under Next Steps. Come back here once your wallet has a USDC balance on Ethereum mainnet and an API key in hand.

Code snippets are tabbed between Python and TypeScript. Pick a tab and stick with it; the two implementations are line-for-line equivalent.

SetupÂļ

pip install eth-account requests
from decimal import Decimal
import json, time, uuid
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data

API     = "https://api.sera.cx/api/v1"
DOMAIN  = {
    "name": "Sera",
    "version": "1",
    "chainId": 1,                                                  # Mainnet
    "verifyingContract": "0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198",
}

PRIVATE_KEY = "0x...your wallet key..."                            # signer
API_KEY     = "sera_..."                                           # see Authentication
API_SECRET  = "..."
OWNER       = Account.from_key(PRIVATE_KEY).address

AUTH = {"Authorization": f"Bearer {API_KEY}:{API_SECRET}"}
npm i ethers@6
import { Wallet, TypedDataDomain, randomBytes } from "ethers";

const API     = "https://api.sera.cx/api/v1";
const DOMAIN: TypedDataDomain = {
  name: "Sera",
  version: "1",
  chainId: 1,                                                      // Mainnet
  verifyingContract: "0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198",
};

const PRIVATE_KEY = "0x...your wallet key...";                    // signer
const API_KEY     = "sera_...";                                    // see Authentication
const API_SECRET  = "...";
const wallet      = new Wallet(PRIVATE_KEY);
const OWNER       = wallet.address;

const AUTH = { "Authorization": `Bearer ${API_KEY}:${API_SECRET}` };

The EIP-712 Order typed-data structure mirrors the on-chain SeraLib.ORDER_TYPEHASH. Define it once:

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" },
  ],
};

The uuid_int Bit LayoutÂļ

Every order's uuid is a packed 256-bit integer:

┌──────────â”Ŧ───────────────────â”Ŧ──────────────────â”Ŧ──────────┐
│[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) │
└──────────┴───────────────────┴──────────────────┴──────────┘

For a standalone order, group_id = order_id >> 16 and leg_id = 0. For a VL batch leg i, every leg shares the same group_id (derived from leg 0's UUID4) and leg_id = i. The server rejects anything else.

executor_id is set per deployment. On Ethereum mainnet it is 0 unless your deployment publishes otherwise. Read once at startup and reuse — drifting executor IDs invalidate every signed UUID you have outstanding.

def compose_uuid(order_id: str, executor_id: int = 0,
                 leg_id: int = 0, group_uuid: str | None = None) -> int:
    """Pack a UUID4 string into the on-chain composite uint256."""
    oid    = int(uuid.UUID(order_id))
    gsrc   = int(uuid.UUID(group_uuid)) if group_uuid else oid
    gid    = gsrc >> 16                                            # top 112 of 128
    return (executor_id << 252) | (oid << 124) | (gid << 12) | leg_id

def new_order_id() -> str:
    return str(uuid.uuid4())
function composeUuid(
  orderId: string,
  executorId: bigint = 0n,
  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;                                       // top 112 of 128
  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)}`;
}

Tutorial 1 — Place a Single Limit OrderÂļ

You're going to place a bid at 1.085 EURC/USDC for 1000 EURC. The market base is EURC, the quote is USDC. A bid means: spend USDC to receive EURC.

Resolve the token addresses from GET /tokens once at startup; for this walkthrough we hard-code them:

USDC = {"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "decimals": 6}
EURC = {"address": "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c", "decimals": 6}
const USDC = { address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", decimals: 6 };
const EURC = { address: "0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c", decimals: 6 };

The wire payload to POST /orders uses pair-natural fields (side, amount, price, from_address=base, to_address=quote). The EIP-712 message is the on-chain Order struct, where fromToken is whatever you spend (USDC on a bid, EURC on an ask) and amounts are raw. Build both, sign, submit:

def raw(amount: Decimal, decimals: int) -> int:
    return int(amount * (Decimal(10) ** decimals))

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)                              # executor_id=0

    # Pair-natural raws
    base_raw  = raw(amount, base["decimals"])
    quote_raw = raw(amount * price, quote["decimals"])

    # EIP-712 struct: fromToken is what the signer SPENDS.
    if side == "bid":
        from_token, to_token = quote["address"], base["address"]
        from_amount, to_amount = quote_raw, base_raw
    else:
        from_token, to_token = base["address"], quote["address"]
        from_amount, to_amount = base_raw, quote_raw

    order_struct = {
        "user":                 OWNER,
        "expiration":           expiration,
        "feeBps":                0,
        "recipient":            "0x0000000000000000000000000000000000000000",
        "fromToken":            from_token,
        "toToken":              to_token,
        "fromAmount":           from_amount,
        "toAmount":             to_amount,
        "initialDepositAmount":  0,
        "uuid":                 uuid_int,
    }
    signable = encode_typed_data(DOMAIN, ORDER_TYPES, order_struct)
    signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()

    wire = {
        "owner_address": OWNER,
        "side":          side,
        "amount":        str(amount),
        "price":         str(price),
        "order_type":    "limit",
        "from_address":  base["address"],                           # base, NOT spend
        "to_address":    quote["address"],                          # quote, NOT receive
        "order_id":      order_id,
        "uuid_int":      str(uuid_int),
        "signature":     "0x" + signature.lstrip("0x"),
        "expiration":    expiration,
    }
    r = requests.post(f"{API}/orders", headers={**AUTH, "Content-Type": "application/json"},
                      data=json.dumps(wire), timeout=10)
    if r.status_code != 201:
        envelope = r.json().get("detail", {})
        raise RuntimeError(f"{envelope.get('error_code')}: {envelope.get('detail')}")
    return {"order_id": order_id, "uuid_int": str(uuid_int)}

placed = place_order("bid", EURC, USDC, Decimal("1000"),
                     Decimal("1.085"), int(time.time()) + 3600)
print("placed:", placed)
function raw(amount: string, decimals: number): bigint {
  const [w, f = ""] = amount.split(".");
  const padded = (w + f.padEnd(decimals, "0")).slice(0, w.length + decimals);
  return BigInt(padded || "0");
}

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);                           // executorId=0n

  const baseRaw  = raw(amount, base.decimals);
  const quoteRaw = raw((Number(amount) * Number(price)).toString(), quote.decimals);

  const isBid    = side === "bid";
  const fromToken   = isBid ? quote.address : base.address;
  const toToken     = isBid ? base.address  : quote.address;
  const fromAmount  = isBid ? quoteRaw      : baseRaw;
  const toAmount    = isBid ? baseRaw       : quoteRaw;

  const orderStruct = {
    user: OWNER, expiration, feeBps: 0,
    recipient: "0x0000000000000000000000000000000000000000",
    fromToken, toToken, fromAmount, toAmount,
    initialDepositAmount: 0n, uuid: uuidInt,
  };
  const signature = await wallet.signTypedData(DOMAIN, ORDER_TYPES, orderStruct);

  const wire = {
    owner_address: OWNER,
    side, amount, price, order_type: "limit",
    from_address: base.address,                                    // base, NOT spend
    to_address:   quote.address,                                   // quote, NOT receive
    order_id:     orderId,
    uuid_int:     uuidInt.toString(),
    signature,
    expiration,
  };
  const r = await fetch(`${API}/orders`, {
    method: "POST",
    headers: { ...AUTH, "Content-Type": "application/json" },
    body: JSON.stringify(wire),
  });
  if (r.status !== 201) {
    const env = (await r.json()).detail ?? {};
    throw new Error(`${env.error_code}: ${env.detail}`);
  }
  return { order_id: orderId, uuid_int: uuidInt.toString() };
}

const placed = await placeOrder("bid", EURC, USDC, "1000", "1.085",
                                Math.floor(Date.now() / 1000) + 3600);
console.log("placed:", placed);

Confirm it landed:

curl -H "$AUTH" "$API/orders?owner_address=$OWNER&status=pending&limit=10"

The error envelope returned on 4xx is documented in Order Endpoints → Error Envelope. Branch on error_code, not on the human detail.

Tutorial 2 — Cancel That OrderÂļ

The CancelOrder struct only carries owner and orderId (the composite uint256, not the UUID string). You stored both at placement time — use them now.

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.lstrip("0x"),
        }),
        timeout=10,
    )
    if r.status_code == 429:
        raise RuntimeError("cancel cooldown — wait and retry")
    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("cancel cooldown — wait and retry");
  if (!r.ok) throw new Error(`cancel failed: ${r.status}`);
}

await cancelOrder(placed.order_id, BigInt(placed.uuid_int));

Per-order cancel cooldown is 5 minutes — hitting it returns 429. If you ever lose uuid_int, fetch the order: GET /orders/{order_id} returns it in the response body.

Tutorial 3 — Automate Two-Sided Quoting on One PairÂļ

This is the loop:

loop:
    mid     ← get_mid("EURC/USDC")           # your rate source
    if first_iteration or |mid − last_mid| / last_mid > drift_bps:
        cancel(open_bid, open_ask)           # any stale orders
        bid_px  ← mid * (1 − spread_bps/1e4)
        ask_px  ← mid * (1 + spread_bps/1e4)
        open_bid ← place_order("bid", EURC, USDC, qty, bid_px, ...)
        open_ask ← place_order("ask", EURC, USDC, qty, ask_px, ...)
        last_mid ← mid
    sleep(poll_seconds)

Three knobs:

  • spread_bps — half-spread on each side. 5 bps each side = 10 bps round-trip.
  • drift_bps — only requote when the mid has moved by more than this. Stops you from churning cancels when the rate jitters within a tick.
  • poll_seconds — how often you wake up. 1–5 s is typical.

The Rate StubÂļ

You bring the rate source. The integration point is one function:

def get_mid(pair: str) -> Decimal:
    """Return the current mid price for `pair`.

    Implement against your own data feed — internal pricing service,
    Bloomberg, an aggregator REST API, etc. The output must be a
    Decimal price expressed as quote-per-base (e.g. EURC/USDC = 1.085).
    """
    raise NotImplementedError("Wire up your pricing feed here.")
async function getMid(pair: string): Promise<number> {
  // Wire up your pricing feed here — internal service, Bloomberg,
  // Refinitiv, an aggregator REST API, etc. Return quote-per-base
  // (e.g. EURC/USDC = 1.085).
  throw new Error("Wire up your pricing feed here.");
}

The LoopÂļ

SPREAD_BPS = Decimal("5")     # 5 bps either side
DRIFT_BPS  = Decimal("3")     # requote only when mid moves > 3 bps
POLL_S     = 2.0
QTY        = Decimal("1000")  # base units per side

open_bid = open_ask = None
last_mid = None

while True:
    try:
        mid = get_mid("EURC/USDC")
        if last_mid is not None and \
           abs(mid - last_mid) / last_mid * 10_000 <= DRIFT_BPS:
            time.sleep(POLL_S); continue

        # Cancel-first, then place — avoids self-match across the requote.
        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", EURC, USDC, QTY, bid_px, expiry)
        placed_ask = place_order("ask", EURC, USDC, QTY, ask_px, expiry)
        open_bid   = {"order_id": placed_bid["order_id"], "uuid_int": int(placed_bid["uuid_int"])}
        open_ask   = {"order_id": placed_ask["order_id"], "uuid_int": int(placed_ask["uuid_int"])}
        last_mid   = mid
    except RuntimeError as e:
        # Surfaces our typed error_code: INSUFFICIENT_EQUITY, STP_BLOCKED, â€Ļ
        print("place/cancel error:", e)
    except requests.RequestException as e:
        print("network error:", e)
    time.sleep(POLL_S)
const SPREAD_BPS = 5;                                              // each side
const DRIFT_BPS  = 3;
const POLL_MS    = 2_000;
const QTY        = "1000";                                         // base units per side

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("EURC/USDC");
    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", EURC, USDC, QTY, bidPx, expiry);
    const pa = await placeOrder("ask", EURC, USDC, QTY, askPx, expiry);
    openBid = { order_id: pb.order_id, uuid_int: BigInt(pb.uuid_int) };
    openAsk = { order_id: pa.order_id, uuid_int: BigInt(pa.uuid_int) };
    lastMid = mid;
  } catch (e) {
    console.error("loop error:", e);
  }
  await sleep(POLL_MS);
}

A few behaviours worth knowing before this hits production:

  • Cancel first, then place. If you place fresh quotes before cancelling stale ones, your new bid can cross your stale ask — the API rejects with STP_BLOCKED. The loop above cancels both legs before placing.
  • INSUFFICIENT_EQUITY is your friend. If you see it, you're trying to freeze more vault balance than you have. Reduce QTY, fund more, or trim the spread.
  • Cancel cooldown is per order_id. It only matters if you try to cancel the same order twice. The loop above places a new order_id every requote, so each cancel hits a different cooldown bucket.
  • Reconcile before clearing local state. cancel_order returning 200 means the matching engine accepted the cancel; chain settlement of any in-flight fill is asynchronous. If you track inventory, watch settlement_summary via GET /orders/{id} until it terminalises before zeroing your position state.

Tutorial 4 — Multi-Pair Quoting With a Virtual Liquidity BatchÂļ

The single-pair loop above freezes one slot of USDC for the bid and one slot of EURC for the ask. If you want to quote N pairs from one collateral pool, you place a VL batch: every leg shares the same group_id, and the matching engine freezes only the largest single-leg cost (not the sum).

Use the same loop skeleton; replace the per-pair placeOrder with a batch call:

def place_vl_batch(legs: list[dict]) -> list[dict]:
    """legs: list of {side, base, quote, amount, price, expiration}."""
    leg_0_uuid = new_order_id()                                    # source of 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=0,
                                leg_id=i, group_uuid=leg_0_uuid)

        base, quote = leg["base"], leg["quote"]
        base_raw    = raw(leg["amount"], base["decimals"])
        quote_raw   = raw(leg["amount"] * leg["price"], quote["decimals"])
        if leg["side"] == "bid":
            ft, tt, fa, ta = quote["address"], base["address"], quote_raw, base_raw
        else:
            ft, tt, fa, ta = base["address"], quote["address"], base_raw, quote_raw

        struct = {
            "user": OWNER, "expiration": leg["expiration"],
            "feeBps": 0,
            "recipient": "0x0000000000000000000000000000000000000000",
            "fromToken": ft, "toToken": tt,
            "fromAmount": fa, "toAmount": ta,
            "initialDepositAmount": 0, "uuid": uuid_int,
        }
        signable = encode_typed_data(DOMAIN, ORDER_TYPES, struct)
        sig = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
        signed_legs.append({
            "owner_address": OWNER,
            "side":          leg["side"],
            "amount":        str(leg["amount"]),
            "price":         str(leg["price"]),
            "order_type":    "limit",
            "from_address":  base["address"],
            "to_address":    quote["address"],
            "order_id":      order_id,
            "uuid_int":      str(uuid_int),
            "signature":     "0x" + sig.lstrip("0x"),
            "expiration":    leg["expiration"],
        })

    r = requests.post(
        f"{API}/orders/vl/batch",
        headers={**AUTH, "Content-Type": "application/json"},
        data=json.dumps({"orders": signed_legs}),
        timeout=10,
    )
    r.raise_for_status()
    return r.json()["order_ids"]
async function placeVlBatch(legs: {
  side: "bid" | "ask";
  base:  { address: string; decimals: number };
  quote: { address: string; decimals: number };
  amount: string;
  price:  string;
  expiration: number;
}[]): Promise<string[]> {
  const leg0Uuid = newOrderId();                                   // source of 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, 0n, BigInt(i), leg0Uuid);

    const baseRaw  = raw(leg.amount, leg.base.decimals);
    const quoteRaw = raw((Number(leg.amount) * Number(leg.price)).toString(),
                         leg.quote.decimals);
    const isBid    = leg.side === "bid";
    const ft = isBid ? leg.quote.address : leg.base.address;
    const tt = isBid ? leg.base.address  : leg.quote.address;
    const fa = isBid ? quoteRaw          : baseRaw;
    const ta = isBid ? baseRaw           : quoteRaw;

    const struct = {
      user: OWNER, expiration: leg.expiration, feeBps: 0,
      recipient: "0x0000000000000000000000000000000000000000",
      fromToken: ft, toToken: tt, fromAmount: fa, toAmount: ta,
      initialDepositAmount: 0n, uuid: uuidInt,
    };
    const signature = await wallet.signTypedData(DOMAIN, ORDER_TYPES, struct);
    signedLegs.push({
      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:     uuidInt.toString(),
      signature, expiration: leg.expiration,
    });
  }

  const r = await fetch(`${API}/orders/vl/batch`, {
    method: "POST",
    headers: { ...AUTH, "Content-Type": "application/json" },
    body: JSON.stringify({ orders: signedLegs }),
  });
  if (!r.ok) throw new Error(`VL batch failed: ${r.status}`);
  const body = await r.json();
  return body.order_ids as string[];
}

Wrap a quoting loop around it: pull a mid for each pair, compute bid+ask per leg, cancel the previous batch via POST /orders/vl/cancel (signature over CancelVLBatch{owner, vlBatchId}), submit a fresh one. Query GET /config → limits.vl_batch once at startup for the current batch-size cap.

Practical NotesÂļ

  • Persist order_id and uuid_int together. Every cancel needs both. Keying your local state on order_id only and recomputing uuid_int from scratch is fine; storing them together is just less error-prone.
  • Idempotency. Picking order_id client-side makes POST /orders idempotent on it. Network blip after submit? Re-send the same payload — the server dedupes.
  • Don't hard-code limits.vl_batch.max. Read it from GET /config at startup; treat it as the source of truth.
  • Watch settlement_summary before clearing inventory state. A 200 on POST /orders/cancel or a pending → cancelled transition does not mean every in-flight match has resolved on chain. Poll GET /orders/{id} until settlement_summary.status terminalises.

Next StepsÂļ