Skip to main content

Quickstart (Python / JavaScript)

Working code for the first hour against the Spot API: sign a request, read your account, place and cancel a resting order, watch the book, and check how your fills settled on Canton. Every snippet below is lifted from the open-source starter repository, which also contains a complete two-sided quoter and a reconciliation job:

Starter repository: Rocky-DEX/Rocky-API-Maker-Bot — Python package rocky_spot and a zero-dependency TypeScript client, Apache-2.0 licensed. Clone it, copy .env.example, and run examples/quickstart.

Prerequisites: an API key (HMAC or ED25519, see Authentication) — create one on the API Keys page of the web app after connecting your wallet, or via /v1/api-keys; Python 3.10+ with requests (pynacl for Ed25519) or Node 22+ (no dependencies).

1. Sign a request​

The gateway verifies the exact bytes before the last &signature=. Build the query string once, sign it, append the signature, send it untouched — for every method, including POST /api/v3/order. Nothing goes in the body.

import hashlib, hmac, time, urllib.parse, requests

BASE, KEY, SECRET = "https://api.rocky.exchange", "your_key", "your_secret"

def signed_query(params: dict) -> str:
params = {k: v for k, v in params.items() if v is not None}
params["timestamp"] = int(time.time() * 1000)
payload = urllib.parse.urlencode(params)
sig = hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
return f"{payload}&signature={sig}"

def signed(method: str, path: str, params: dict = {}):
r = requests.request(method, f"{BASE}{path}?{signed_query(params)}", headers={"X-MBX-APIKEY": KEY})
body = r.json()
if r.status_code >= 400:
raise RuntimeError(f"{r.status_code} {body}") # {"code": -2011, "msg": "..."}
return body
import { createHmac } from "node:crypto";

const BASE = "https://api.rocky.exchange", KEY = "your_key", SECRET = "your_secret";

function signedQuery(params: Record<string, string | number | undefined>): string {
const q = new URLSearchParams();
for (const [k, v] of Object.entries(params)) if (v !== undefined) q.append(k, String(v));
q.append("timestamp", String(Date.now()));
const payload = q.toString();
const sig = createHmac("sha256", SECRET).update(payload).digest("hex");
return `${payload}&signature=${sig}`;
}

async function signed<T>(method: string, path: string, params: Record<string, string | number | undefined> = {}): Promise<T> {
const r = await fetch(`${BASE}${path}?${signedQuery(params)}`, { method, headers: { "X-MBX-APIKEY": KEY } });
const body = await r.json();
if (!r.ok) throw new Error(`${r.status} ${JSON.stringify(body)}`);
return body as T;
}

Ed25519 keys sign the same payload and send the signature base64 instead of hex; see the reference vector. The starter's signing module handles both and self-tests against the published vectors.

2. Account and segment​

acct = signed("GET", "/api/v3/account")
print(acct["settlement"]) # {"segment": "wallet", "enrolled": true, "frozen": false, "party": "..."}
print({b["asset"]: b["free"] for b in acct["balances"] if b["free"] != "0"})
const acct = await signed<{ canTrade: boolean; settlement: { segment: string; frozen: boolean }; balances: { asset: string; free: string }[] }>("GET", "/api/v3/account");
console.log(acct.settlement, acct.balances.filter((b) => b.free !== "0"));

settlement.segment is wallet for a party-bound account on the on-chain segment: balances mirror your Canton wallet, and canTrade turns false if the mirror sync freezes the account. See Account Information.

3. Filters, then a resting order​

Read tickSize, stepSize, minQty, minNotional from exchangeInfo and quantize before sending; a price off the tick or a notional below the minimum is rejected (-1100 / -1013).

from decimal import Decimal, ROUND_DOWN, ROUND_UP

info = requests.get(f"{BASE}/api/v3/exchangeInfo").json()
sym = next(s for s in info["symbols"] if s["symbol"] == "CBTC-USDCB")
f = {k: v for flt in sym["filters"] for k, v in flt.items() if k != "filterType"}
tick, step, min_notional = Decimal(f["tickSize"]), Decimal(f["stepSize"]), Decimal(f["minNotional"])

book = requests.get(f"{BASE}/api/v3/depth", params={"symbol": "CBTC-USDCB", "limit": 5}).json()
best_bid = Decimal(book["bids"][0][0])

price = (best_bid * Decimal("0.8") / tick).quantize(Decimal(1), rounding=ROUND_DOWN) * tick # 20% below: rests
qty = (min_notional * Decimal("1.05") / price / step).quantize(Decimal(1), rounding=ROUND_UP) * step

ack = signed("POST", "/api/v3/order", {"symbol": "CBTC-USDCB", "side": "BUY", "type": "LIMIT",
"quantity": str(qty), "price": str(price), "newClientOrderId": "qs-1"})
print(ack["orderId"], ack["status"]) # NEW; the ack never carries fills
const info = await (await fetch(`${BASE}/api/v3/exchangeInfo`)).json();
const sym = info.symbols.find((s: any) => s.symbol === "CBTC-USDCB");
const f: Record<string, string> = Object.assign({}, ...sym.filters.map(({ filterType, ...rest }: any) => rest));
const tick = Number(f.tickSize), step = Number(f.stepSize), dp = (s: string) => (s.split(".")[1] ?? "").replace(/0+$/, "").length;

const book = await (await fetch(`${BASE}/api/v3/depth?symbol=CBTC-USDCB&limit=5`)).json();
const price = (Math.floor((Number(book.bids[0][0]) * 0.8) / tick) * tick).toFixed(dp(f.tickSize));
const qty = (Math.ceil((Number(f.minNotional) * 1.05) / Number(price) / step) * step).toFixed(dp(f.stepSize));

const ack = await signed<{ orderId: string; status: string }>("POST", "/api/v3/order",
{ symbol: "CBTC-USDCB", side: "BUY", type: "LIMIT", quantity: qty, price, newClientOrderId: "qs-1" });
console.log(ack.orderId, ack.status);

newClientOrderId makes a retried placement idempotent for 15 minutes; it is not stored on the order.

4. Cancel one, cancel all​

signed("DELETE", "/api/v3/order", {"symbol": "CBTC-USDCB", "orderId": ack["orderId"]}) # -2011 if already gone
cancelled = signed("DELETE", "/api/v3/openOrders", {"symbol": "CBTC-USDCB"}) # kill switch: weight 1, not order-counted
print(len(cancelled), "orders cancelled")
await signed("DELETE", "/api/v3/order", { symbol: "CBTC-USDCB", orderId: ack.orderId });
const cancelled = await signed<unknown[]>("DELETE", "/api/v3/openOrders", { symbol: "CBTC-USDCB" });
console.log(cancelled.length, "orders cancelled");

Wire DELETE /api/v3/openOrders to every failure path of your strategy (feed loss, exceptions, SIGTERM). See Cancel All Orders.

5. Fills and on-chain settlement​

for t in signed("GET", "/api/v3/myTrades", {"symbol": "CBTC-USDCB", "limit": 20}):
print(t["orderId"], t["qty"], t["price"], t["settlementStatus"], t["cantonUpdateId"])

summary = signed("GET", "/api/v3/settlements/summary")["total"]
print(summary["counts"], "unsettled quote qty:", summary["unsettledQuoteQty"])
for row in signed("GET", "/api/v3/settlements", {"status": "failed", "limit": 50}):
print("FAILED", row["tradeId"], row["lastError"]) # operations are alerted; do not re-quote this inventory
for (const t of await signed<any[]>("GET", "/api/v3/myTrades", { symbol: "CBTC-USDCB", limit: 20 }))
console.log(t.orderId, t.qty, t.price, t.settlementStatus, t.cantonUpdateId);

const { total } = await signed<{ total: { counts: Record<string, number>; unsettledQuoteQty: string } }>("GET", "/api/v3/settlements/summary");
console.log(total.counts, "unsettled quote qty:", total.unsettledQuoteQty);

orderId on each fill maps it back to the quote it hit. A fill normally goes pending → processing → confirmed within seconds; confirmed carries the cantonUpdateId you can verify on Canton. Each fill on the wallet segment is a DvP your validator confirms and pays traffic for — see External Market Maker Guide and Settlement Ledger.

6. Market data over WebSocket​

import asyncio, json, websockets

async def main():
url = "wss://api.rocky.exchange/stream?streams=cbtc-usdcb@depth5/cbtc-usdcb@trade"
async with websockets.connect(url, ping_interval=None) as ws: # gateway pings every 20 s
async for raw in ws:
msg = json.loads(raw)
if msg["stream"].endswith("@depth5"):
bids, asks = msg["data"]["bids"], msg["data"]["asks"]
print("mid", (float(bids[0][0]) + float(asks[0][0])) / 2)
else:
print("trade", msg["data"]["p"], msg["data"]["q"], "maker-sold" if msg["data"]["m"] else "maker-bought")

asyncio.run(main())
const ws = new WebSocket("wss://api.rocky.exchange/stream?streams=cbtc-usdcb@depth5/cbtc-usdcb@trade");
ws.onmessage = (ev) => {
const { stream, data } = JSON.parse(String(ev.data));
if (stream.endsWith("@depth5")) console.log("mid", (Number(data.bids[0][0]) + Number(data.asks[0][0])) / 2);
else console.log("trade", data.p, data.q, data.m ? "maker-sold" : "maker-bought");
};
ws.onclose = () => console.log("reconnect here");

Streams are public; there is no user-data stream, so poll openOrders (weight 1) and myTrades / settlements (weight 10) for your own state. See WebSocket Streams.

7. Rate-limit headers​

Every signed response carries x-mbx-used-weight-1m and x-mbx-order-count-1m. Read them and back off before the gateway answers 429 -1003; the starter clients expose them as properties after each call. External market makers are on the ext-mm tier: 2400 weight/min, 100 orders/10 s, 600 orders/min; cancels are free. See Rate limits.