API Reference / Market Maker API
Endpoints for programmatic market making: batch place and cancel, cancel by market, cancel all across markets, heartbeat protection, and separate rate-limit buckets for placing and cancelling. Credential: a pxk_ API key.
| Base URL | http://127.0.0.1:3100 |
|---|---|
| Path prefix | /v1/px |
| Authentication | Authorization: Bearer pxk_<keyId>.<secret> — created under Settings → API keys; a session JWT is accepted too. |
| Scopes | read → GET · place → place · cancel → every cancel endpoint and the heartbeat (a heartbeat's effect is a cancel). Funding actions (deposit / borrow) are session-only; API keys get 403. |
| Amounts & time | Always E6 fixed-point as decimal strings (quantityE6, limitPriceE6, expireAtMs). Never floating point. |
| Error body | { "code", "message", "requestId" }; quote the requestId when contacting us. |
These values and the limits the server enforces come from one shared constant (@fathom/exchange-sdk).
The Quickstart on the overview page walks the retail order loop. The market-making loop is a different one: get a key → confirm your tier → register the heartbeat → quote both sides in a batch → replace quotes → hit the kill switch before you stop. Below is the minimal runnable skeleton of that loop — set two environment variables and it runs.
#!/usr/bin/env python3
"""做市闭环最小骨架。运行前:export PX_BASE_URL=... PX_API_KEY=pxk_..."""
import os, time, requests
BASE = os.environ["PX_BASE_URL"]
H = {"X-PX-Key": os.environ["PX_API_KEY"], "Content-Type": "application/json"}
MARKET = "px-btc-120k"
def call(method, path, body=None):
r = requests.request(method, BASE + path, headers=H, json=body, timeout=5)
if r.status_code == 429: # 桶内代币不足:整批被拒,退避后重发整批
time.sleep(1.0)
return None
r.raise_for_status()
return r.json()
# 1. 登记心跳。超过窗口未续期,服务端撤掉你名下全部挂单(cancel-on-disconnect)
call("POST", "/v1/px/heartbeats")
open_ids = []
try:
while True:
# 2. 撤掉上一轮的报价。单市场撤单比逐张撤便宜,也避免撤单频率打满
if open_ids:
call("DELETE", "/v1/px/orders/by-market", {"marketId": MARKET})
open_ids = []
# 3. 双边批量报价。逐张成败,results 按请求顺序返回
res = call("POST", "/v1/px/orders/batch", {"orders": [
{"marketId": MARKET, "outcome": "A", "side": "BUY", "orderType": "LIMIT",
"limitPriceE6": "520000", "quantityE6": "100000000", "tif": "GTC",
"clientOrderId": f"bid-{int(time.time())}"},
{"marketId": MARKET, "outcome": "A", "side": "SELL", "orderType": "LIMIT",
"limitPriceE6": "540000", "quantityE6": "100000000", "tif": "GTC",
"clientOrderId": f"ask-{int(time.time())}"},
]})
if res:
open_ids = [r["order"]["orderId"] for r in res["results"] if r["ok"]]
for r in res["results"]:
if not r["ok"]:
print("rejected:", r["code"], r["message"])
# 4. 续心跳,频率取窗口的一半以内,给网络抖动留余量
call("POST", "/v1/px/heartbeats")
time.sleep(5)
finally:
# 5. 收工:kill switch 撤掉全部挂单,再注销心跳登记(注销本身不触发全撤)
call("DELETE", "/v1/px/orders/all")
call("DELETE", "/v1/px/heartbeats")
Deliberately absent: reconnect backoff, book subscription and exposure checks. All three are coupled to your strategy, and shipping a generic implementation here would only mislead.
Each endpoint has its own page with behaviour rules, authorization, and request and response examples.
| Endpoint | Path | Scope | Summary |
|---|---|---|---|
| Batch place | POST /v1/px/orders/batch | place | Submit several orders in one request. |
| Batch cancel | DELETE /v1/px/orders | cancel | Cancel orders by id. |
| Cancel by market | DELETE /v1/px/orders/by-market | cancel | Cancel all of your open orders in one market. |
| Cancel all (kill switch) | DELETE /v1/px/orders/all | cancel | Cancel all of your open orders across all markets. |
| Heartbeat (cancel-on-disconnect) | POST /v1/px/heartbeats | cancel | Send periodic heartbeats. If one is not received in time, the server cancels all of your open orders. |
| Deregister heartbeat | DELETE /v1/px/heartbeats | cancel | Remove the heartbeat registration. Open orders are left in place. |
| Place one order | POST /v1/px/orders | place | Submit one order. |
| Cancel one order | DELETE /v1/px/orders/:orderId | cancel | Cancel one order. |
| My open orders | GET /v1/px/orders | read | Return all of your open orders. |
GET /v1/px/markets/:marketId.GET /v1/px/orders first, then consume new events.POST /v1/px/orders/batch, at most 15 orders per batch.DELETE /v1/px/orders/all, then DELETE /v1/px/heartbeats.| Status | code | When |
|---|---|---|
| 400 | BATCH_TOO_LARGE | Batch exceeds the size cap |
| 400 | INVALID_PRICE | Price is not a multiple of the market tick |
| 400 | INVALID_QUANTITY | Quantity below the minimum or malformed |
| 400 | INSUFFICIENT_BALANCE | Available balance cannot cover the reservation |
| 400 | MARKET_NOT_OPEN | Market is not open or has ended |
| 404 | ORDER_NOT_FOUND | Order does not exist or is not yours (indistinguishable) |
| 409 | ORDER_NOT_LIVE | Order already in a terminal state |
| 409 | HEARTBEAT_ID_MISMATCH | Heartbeat ID mismatch; the response carries the ID the server expected |
| 429 | RATE_LIMITED | Bucket exhausted; Retry-After gives the wait in seconds |
| 503 | MARKET_DEGRADED | Mirrored book is stale; the order is refused rather than filled at a stale price |
| 401 | API_KEY_INVALID / API_KEY_REVOKED | Key invalid or revoked |
| 403 | FORBIDDEN | Key scope insufficient (e.g. placing with a read-only key) |
Inside a batch place, per-order rejections use the same codes in results[i].code; per-id cancel reasons appear in notCanceled.