Docs
v1.0
DOCS / §05

Market Maker API

A high-performance, signed interface for connecting trading bots to the matching engine: a WebSocket trading channel, a public market-data channel, a REST read API, bulk orders, atomic cancel-replace, and a dead-man switch.

Endpoints Trading (private WS): wss://api.perp.com/ws/private
Market data (public WS): wss://api.perp.com/ws/market
REST read API: https://api.perp.com/api/v1/mm/*

API keys

Each market maker is issued a key pair. The API key is a public identifier; the API secret signs every request. The secret is shown exactly once at creation and cannot be retrieved afterward.

CredentialFormatUse
API keyperp_live_<48 hex> (or perp_test_)Public client identifier sent on every request.
API secret32 random bytes, hex (64 chars)HMAC-SHA256 signing key. Never transmitted after creation.

Secrets are stored encrypted with AES-256-GCM under a KMS-managed key (AWS KMS or HashiCorp Vault); the master key never leaves the KMS and the plaintext secret exists only in gateway memory for the lifetime of a signature check. Rotate keys at least every 90 days or after any incident via POST /api/v1/mm/api-keys/rotate, running old and new in parallel during cutover before revoking the old key.

Lost secret There is no recovery. Revoke the key and create a new one.

Authentication

Every signed request carries an HMAC-SHA256 signature over a canonical pre-image, lowercase hex. The server's clock is authoritative; use an NTP-synced clock. Allowed timestamp drift is ±5 seconds.

REST

Send three headers and sign the canonical pre-image:

HeaderValue
X-API-KeyThe API key string
X-TimestampUnix time in milliseconds
X-SignatureHMAC-SHA256 over the pre-image (hex)
REST pre-image
timestamp + "\n" + apiKey + "\n" + METHOD + "\n" + path + "\n" + sha256(body)

For requests with no body, use the SHA-256 of the empty string (e3b0c442…b855) for the body component.

Private WebSocket

The private connection follows CONNECTED → AUTHENTICATING → AUTHENTICATED → SUBSCRIBED. Until AUTHENTICATE succeeds, private commands are rejected with MM_1008_NOT_AUTHENTICATED.

AUTHENTICATE
{
  "action": "AUTHENTICATE",
  "id": "1",
  "params": {
    "apiKey": "perp_live_xxx",
    "timestamp": 1773738000000,
    "signature": "…"
  }
}
Pre-image — auth
timestamp + "\n" + apiKey + "\n" + "AUTHENTICATE" + "\n" + "/ws/private"

Trading command signature

Write commands (CREATE_BULK_ORDERS, CANCEL_BULK_ORDERS, CANCEL_REPLACE_BULK, SET_TRADING_MODE, CONFIGURE_DEAD_MAN_SWITCH) sign the full command envelope:

Pre-image — write command
timestamp + "\n" + apiKey + "\n" + action + "\n" + path + "\n" + sha256(body)

Order signature (EIP-712)

This transport HMAC is separate from the per-order blockchain signature. Commands that create executable intents — every order in CREATE_BULK_ORDERS and the create leg of CANCEL_REPLACE_BULK — must also carry an EIP-712 signature over the canonical order, signed by the trading wallet:

  • signer — the EVM wallet authorized for this API key.
  • nonce — a wallet-scoped anti-replay nonce.
  • orderSignature — the EIP-712 signature used during on-chain settlement of matched trades.

Cancels are off-chain only and need no order-level signature — request-level HMAC is sufficient.

Order object

FieldTypeRequiredNotes
clientOrderIdstringYesClient-assigned, scoped to the order within a request.
marketIdstringYese.g. BTC-UP-100000.
sideenumYesBUY / SELL.
typeenumYesSee order types.
pricedecimalConditionalRequired for LIMIT/POST_ONLY; omit for MARKET.
sizedecimalYesOrder quantity.
timeInForceenumNoGTC (default), IOC, FOK.
reduceOnlyboolNoOrder may only reduce an open position.
signer / nonce / orderSignatureCreate flowEIP-712 order signature fields.

Trading commands

All commands share an envelope. The root id is required on writes and is the idempotency key.

Envelope
{ "action": "COMMAND", "id": "req-id", "timestamp": , "signature": "…", "params": {} }

CREATE_BULK_ORDERS

Up to 50 orders, 20 distinct markets, 1 MB per request. Returns per-item accepted / rejected results.

{
  "action": "CREATE_BULK_ORDERS",
  "id": "request-123",
  "timestamp": 1773738000000,
  "signature": "…",
  "params": { "orders": [
    {
      "clientOrderId": "mm-001", "marketId": "BTC-UP-100000",
      "side": "BUY", "type": "LIMIT", "price": 65000, "size": 10,
      "signer": "0xAbCd…01", "nonce": 42, "orderSignature": "0xabc123…"
    }
  ] }
}

CANCEL_BULK_ORDERS

Cancel by ID. Off-chain only — request-level HMAC, no order signature.

{ "action": "CANCEL_BULK_ORDERS", "id": "request-124",
  "timestamp": , "signature": "…",
  "params": { "orderIds": ["123", "456"] } }

CANCEL_REPLACE_BULK

The only way to reprice or resize. Atomic in the matching engine: if any create leg fails, the whole operation rolls back and the cancelled orders are restored. Cancel legs are off-chain; create legs carry order signatures. Queue priority resets because the replacement is a new order.

{
  "action": "CANCEL_REPLACE_BULK", "id": "request-125",
  "timestamp": , "signature": "…",
  "params": { "atomic": true, "operations": [
    { "type": "CANCEL", "orderId": "123" },
    { "type": "CREATE", "order": {
        "clientOrderId": "mm-002", "marketId": "BTC-UP-100000",
        "price": 65100, "size": 5,
        "signer": "0xAbCd…01", "nonce": 43, "orderSignature": "0xdef456…"
    } }
  ] }
}

CANCEL_ALL_ORDERS / CANCEL_ALL_BY_MARKET

Drain the whole book or a single market. Off-chain only.

Risk controls

Trading mode

SET_TRADING_MODE changes account state without revoking keys or cancelling orders. It never affects positions — liquidation and funding continue regardless.

ModeAllowedRejected
ACTIVEAll commands
CANCEL_ONLYCancels and readsCreate and cancel-replace (MM_3001_TRADING_MODE_RESTRICTED)

Use CANCEL_ONLY to safely drain the book when a pricing model looks wrong: existing orders stay live, new creates are rejected, and the bot cancels down gradually.

Dead-man switch

The DMS is gateway-enforced emergency protection, distinct from trading mode: the bot sets the mode, but the gateway auto-cancels all orders on connection loss. Configure a timeout; 0 disables it.

{ "action": "CONFIGURE_DEAD_MAN_SWITCH", "id": "request-129",
  "timestamp": , "signature": "…",
  "params": { "timeoutMs": 3000 } }

Heartbeat with PING (server replies PONG) every ~1000 ms; the gateway times out at 3000 ms. The DMS maintains a Redis key per account, refreshed on each heartbeat; a worker monitors expired keys and triggers CANCEL_ALL_ORDERS. After a DMS event the configuration is preserved — re-authenticate, re-subscribe, and send PING promptly to re-arm.

Read API

REST read endpoints require the auth headers above; list responses paginate with cursor + limit. WebSocket remains the channel for real-time streaming.

EndpointWS commandReturns
GET /api/v1/mm/ordersGET_OPEN_ORDERSOpen orders.
GET /api/v1/mm/fillsTrade fills.
GET /api/v1/mm/positionsGET_POSITIONSPositions with entry price and uPnL.
GET /api/v1/mm/accountGET_BALANCEAvailable and locked balance.
GET /api/v1/mm/limitsGET_LIMITSRisk tier, max order size, max leverage, per-request caps.

Market data & private streams

The public market-data WebSocket needs no authentication. Subscribe to a market's book, trades, or mark price.

Order book sync

On SUBSCRIBE_BOOK the server sends a BOOK_SNAPSHOT then incremental BOOK_DELTA messages. Sequences are per-market; the snapshot resets the baseline.

  1. Apply the snapshot as initial state.
  2. Apply deltas in strict sequence order.
  3. On a sequence gap (e.g. 100, 101, 105), reconnect and request a fresh snapshot.
  4. Validate each delta with its checksum; a mismatch forces an immediate resync.

Private streams

On wss://api.perp.com/ws/private: user.orders, user.positions, user.balance, user.fills, user.margin, user.risk, user.liquidations. Order events are NEW, PARTIAL_FILL, FILLED, CANCELLED, REJECTED. Each channel carries its own strictly increasing sequence; on a gap, resync via the REST read API.

Idempotency & rate limits

All writes require a root id (request ID) and are fully idempotent. The gateway stores processed request IDs for 24 hours; replaying the same id returns the cached response without re-executing. clientOrderId is scoped to an order within a request; the request id is scoped to the whole bulk request.

CategoryCommandsSustainedBurst
CREATE_BULKCREATE_BULK_ORDERS100/s200
CANCELCANCEL_BULK_ORDERS, CANCEL_REPLACE_BULK500/s1000
CANCEL_ALLCANCEL_ALL_ORDERS10/s20
READGET_* / subscriptions1000/s2000

Limits are enforced at the API-key, account, and global-engine levels. Over the limit returns 429 with MM_4001_RATE_LIMIT and a retryAfterMs.

Reconnect runbook

  1. Reconnect the transport.
  2. Re-authenticate (AUTHENTICATE).
  3. Re-subscribe to private streams.
  4. Fetch open orders and positions over REST to resync state.
  5. Re-arm the dead-man switch.
  6. Send PING promptly to start the heartbeat window, then resume your loop.

Backoff: start at 100 ms, double per attempt, cap at 30 s, add 0–20% jitter.

Error reference

Format: MM_<domain><code>_<NAME>.

CodeMeaning
MM_1001_INVALID_API_KEYAPI key not found.
MM_1002_KEY_REVOKEDKey revoked.
MM_1005_INVALID_SIGNATUREHMAC mismatch.
MM_1006_SIGNATURE_EXPIREDTimestamp outside ±5s drift.
MM_1007_DUPLICATE_REQUESTRequest ID already processed.
MM_1008_NOT_AUTHENTICATEDCommand before successful auth.
MM_2002_INSUFFICIENT_MARGINNot enough margin for the order.
MM_2003_INVALID_PRICEPrice off tick or out of range.
MM_2004_INVALID_SIZESize below min or above max.
MM_2008_POST_ONLY_REJECTEDPost-only would have crossed.
MM_2009_REDUCE_ONLY_REJECTEDReduce-only would increase the position.
MM_2010_INVALID_ORDER_SIGNATUREBad EIP-712 order signature.
MM_2011_WALLET_MISMATCHsigner not authorized for the key.
MM_2013_NONCE_REUSEDOrder nonce already used.
MM_3001_TRADING_MODE_RESTRICTEDCommand forbidden in current mode.
MM_4001_RATE_LIMITRate limit exceeded.

Order-signature errors (MM_2010MM_2013) apply only to signed create/replace flows; pure cancels are validated by request HMAC alone.

Security TLS 1.3 only. Never log the API secret, HMAC signatures, or pre-images. Max 3 private and 5 market-data WebSocket connections per key.