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.
wss://api.perp.com/ws/privateMarket data (public WS):
wss://api.perp.com/ws/marketREST 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.
| Credential | Format | Use |
|---|---|---|
| API key | perp_live_<48 hex> (or perp_test_) | Public client identifier sent on every request. |
| API secret | 32 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.
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:
| Header | Value |
|---|---|
X-API-Key | The API key string |
X-Timestamp | Unix time in milliseconds |
X-Signature | HMAC-SHA256 over the pre-image (hex) |
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.
{
"action": "AUTHENTICATE",
"id": "1",
"params": {
"apiKey": "perp_live_xxx",
"timestamp": 1773738000000,
"signature": "…"
}
}
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:
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
| Field | Type | Required | Notes |
|---|---|---|---|
clientOrderId | string | Yes | Client-assigned, scoped to the order within a request. |
marketId | string | Yes | e.g. BTC-UP-100000. |
side | enum | Yes | BUY / SELL. |
type | enum | Yes | See order types. |
price | decimal | Conditional | Required for LIMIT/POST_ONLY; omit for MARKET. |
size | decimal | Yes | Order quantity. |
timeInForce | enum | No | GTC (default), IOC, FOK. |
reduceOnly | bool | No | Order may only reduce an open position. |
signer / nonce / orderSignature | — | Create flow | EIP-712 order signature fields. |
Trading commands
All commands share an envelope. The root id is required on writes and is the idempotency key.
{ "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.
| Mode | Allowed | Rejected |
|---|---|---|
ACTIVE | All commands | — |
CANCEL_ONLY | Cancels and reads | Create 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.
| Endpoint | WS command | Returns |
|---|---|---|
GET /api/v1/mm/orders | GET_OPEN_ORDERS | Open orders. |
GET /api/v1/mm/fills | — | Trade fills. |
GET /api/v1/mm/positions | GET_POSITIONS | Positions with entry price and uPnL. |
GET /api/v1/mm/account | GET_BALANCE | Available and locked balance. |
GET /api/v1/mm/limits | GET_LIMITS | Risk 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.
- Apply the snapshot as initial state.
- Apply deltas in strict sequence order.
- On a sequence gap (e.g.
100, 101, 105), reconnect and request a fresh snapshot. - 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.
| Category | Commands | Sustained | Burst |
|---|---|---|---|
| CREATE_BULK | CREATE_BULK_ORDERS | 100/s | 200 |
| CANCEL | CANCEL_BULK_ORDERS, CANCEL_REPLACE_BULK | 500/s | 1000 |
| CANCEL_ALL | CANCEL_ALL_ORDERS | 10/s | 20 |
| READ | GET_* / subscriptions | 1000/s | 2000 |
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
- Reconnect the transport.
- Re-authenticate (
AUTHENTICATE). - Re-subscribe to private streams.
- Fetch open orders and positions over REST to resync state.
- Re-arm the dead-man switch.
- Send
PINGpromptly 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>.
| Code | Meaning |
|---|---|
MM_1001_INVALID_API_KEY | API key not found. |
MM_1002_KEY_REVOKED | Key revoked. |
MM_1005_INVALID_SIGNATURE | HMAC mismatch. |
MM_1006_SIGNATURE_EXPIRED | Timestamp outside ±5s drift. |
MM_1007_DUPLICATE_REQUEST | Request ID already processed. |
MM_1008_NOT_AUTHENTICATED | Command before successful auth. |
MM_2002_INSUFFICIENT_MARGIN | Not enough margin for the order. |
MM_2003_INVALID_PRICE | Price off tick or out of range. |
MM_2004_INVALID_SIZE | Size below min or above max. |
MM_2008_POST_ONLY_REJECTED | Post-only would have crossed. |
MM_2009_REDUCE_ONLY_REJECTED | Reduce-only would increase the position. |
MM_2010_INVALID_ORDER_SIGNATURE | Bad EIP-712 order signature. |
MM_2011_WALLET_MISMATCH | signer not authorized for the key. |
MM_2013_NONCE_REUSED | Order nonce already used. |
MM_3001_TRADING_MODE_RESTRICTED | Command forbidden in current mode. |
MM_4001_RATE_LIMIT | Rate limit exceeded. |
Order-signature errors (MM_2010–MM_2013) apply only to signed create/replace flows; pure cancels are validated by request HMAC alone.