Build a Bitcoin trading bot in 10 minutes with the Rizon API
A working TypeScript bot for Rizon's 15-second Bitcoin market. Get a key in one call, read the book, place an order, stream prices and settlements.
Short answer: Get a key with one call (
curl -X POST https://rizon.markets/api/v1/accounts), read the markets and books over REST without a key, place orders withPOST /api/v1/orders, and stream prices and your own fills overwss://rizon.markets/api/v1/ws. The bot below is about 70 lines of TypeScript: it watches Bitcoin, buys $10 of up or down on the 15-second market after a sharp move, and logs each result as it settles. It's paper money, so the real exchange is your sandbox.
Rizon's API is the same one its own market maker uses: REST for requests, a WebSocket for streams, JSON both ways. Market data needs no key. Everything here runs with Bun, which reads .env by itself and lets a WebSocket send an Authorization header.
1. Get a key
Three ways, depending on who's asking:
| Who | How |
|---|---|
| You, in a browser | "Create key" on the docs page; it's shown once |
| A script with nobody to approve | curl -X POST https://rizon.markets/api/v1/accounts: a new account with $1,000 of paper and a trade key, in one call |
| An AI agent on your account | POST /api/v1/connect, then you approve one link; the agent collects the key |
mkdir rizon-bot && cd rizon-bot
curl -s -X POST https://rizon.markets/api/v1/accounts \
-H "Content-Type: application/json" -d '{"name": "momentum_bot"}'
# {"key":"rz_…","account":{"id":"…","name":"momentum_bot","balance":1000,…}}
echo "RIZON_API_KEY=rz_…" > .env # paste your key
echo ".env" >> .gitignoreThe key is shown once and only its hash is stored, so keep it in .env and out of git. Callsigns are 3 to 16 letters, digits, _ or -; leave name out and you get a TRADER-####.
2. Read the markets and the book
A market is an underlying and a horizon: BTC-15s, BTC-1m, BTC-5m. Each has one continuous book whose price is the strike, like a futures book: up is buying (bids), down is selling (asks). A market up order fills at the best ask, a market down order at the best bid. Every contract pays 2×: you pay your size and get twice it back if Bitcoin finishes on your side of the strike.
// book.ts: the top of each BTC book, as distances from spot. Run: bun book.ts
const API = `${process.env.RIZON_URL ?? 'https://rizon.markets'}/api/v1`;
const { markets } = await (await fetch(`${API}/markets`)).json();
for (const m of markets.filter((m: any) => m.underlying === 'BTC-USD')) {
const book = await (await fetch(`${API}/books/${m.id}`)).json();
const bid = book.bids[0];
const ask = book.asks[0];
console.log(
`${m.id.padEnd(8)} spot ${m.spot.toFixed(2)} ` +
`bid ${bid?.strike ?? '-'} ($${bid?.size ?? 0}) ask ${ask?.strike ?? '-'} ($${ask?.size ?? 0}) ` +
`up at the ask needs BTC up more than $${ask ? (ask.strike - m.spot).toFixed(2) : '-'} in ${m.horizon}s; ` +
`one sigma over that horizon is $${m.sigma.toFixed(2)}`,
);
}It prints one line per market: the spot price, the best bid and ask strikes with their sizes, how far Bitcoin has to rise for up at the ask to win, and sigma, the market's realised one-sigma move over its horizon. That last pair is the most useful number in short-horizon trading: a strike $1 away is nothing over 5 minutes and a real distance over 15 seconds.
3. Place an order
// order.ts: buy $10 of up on the 1-minute market, at the best ask. Run: bun order.ts
const API = `${process.env.RIZON_URL ?? 'https://rizon.markets'}/api/v1`;
const res = await fetch(`${API}/orders`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.RIZON_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ market: 'BTC-1m', side: 'up', size: 10 }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error}`);
for (const p of body.positions) {
console.log(`${p.side} $${p.size} at strike ${p.strike}, BTC was ${p.spot}, expires ${new Date(p.expiresAt).toISOString()}`);
}The reply has the order, the positions it opened (a large market order can open several, at several strikes) and any of your own resting orders it canceled on the way. Each position expires on a whole second about one horizon after the fill: read expiresAt, don't compute it. A position can't be closed; it runs to expiry, and you hedge it with a later one.
Add "strike" to make it a limit order, and "tif": "post" to make sure it only ever rests (a maker quote). Give an order your own clientId, and if a request fails in transit, send it again: a repeat is refused with 409 duplicate clientId and the original order's id, instead of becoming a second order.
4. The bot: stream, decide, trade
The WebSocket sends {type, data} messages. With a key you get hello (your account, open orders and positions), then the channels you asked for, plus your own order, position and account events whatever the channels.
// bot.ts: a small momentum bot for Rizon's 15-second Bitcoin market. Run: bun bot.ts
// When BTC has moved more than TRIGGER_USD in the last 10 seconds, it buys $10 of up (or down) on BTC-15s,
// and logs each position as it settles. An example of the API's moving parts, not a strategy.
const BASE = process.env.RIZON_URL ?? 'https://rizon.markets';
const API = `${BASE}/api/v1`;
const WS = `${BASE.replace(/^http/, 'ws')}/api/v1/ws?channels=price:BTC-USD`;
const KEY = process.env.RIZON_API_KEY;
if (!KEY) throw new Error('Set RIZON_API_KEY in .env first');
const headers = { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' };
const MARKET = 'BTC-15s';
const SIZE = 10; // paper dollars per order
const LOOKBACK_MS = 10_000;
const TRIGGER_USD = 15;
const MAX_OPEN = 3; // open positions at once
const ticks: { t: number; price: number }[] = [];
const open = new Set<string>(); // ids of open positions
let busy = false;
async function api(path: string, init: RequestInit = {}) {
const res = await fetch(`${API}${path}`, { ...init, headers });
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${res.status} ${body.error ?? res.statusText}`);
return body;
}
async function buy(side: 'up' | 'down') {
busy = true;
try {
const { positions } = await api('/orders', { method: 'POST', body: JSON.stringify({ market: MARKET, side, size: SIZE }) });
for (const p of positions) {
open.add(p.id);
console.log(`${side} $${p.size} at strike ${p.strike} (spot ${p.spot}), expires ${new Date(p.expiresAt).toISOString()}`);
}
} catch (err) {
console.log(`order refused: ${(err as Error).message}`); // e.g. 409 no liquidity, 409 insufficient balance
} finally {
busy = false;
}
}
function onPrice(t: number, price: number) {
ticks.push({ t, price });
while (ticks[0].t < t - LOOKBACK_MS) ticks.shift();
const move = price - ticks[0].price;
if (busy || open.size >= MAX_OPEN || Math.abs(move) < TRIGGER_USD) return;
ticks.length = 0; // one order per move
void buy(move > 0 ? 'up' : 'down');
}
function connect(delay = 1000) {
const ws = new WebSocket(WS, { headers });
ws.onopen = () => (delay = 1000);
ws.onmessage = (e) => {
const { type, data } = JSON.parse(String(e.data));
if (type === 'hello') {
for (const p of data.positions) open.add(p.id);
console.log(`connected as ${data.account.name}: $${data.account.balance} free, ${open.size} open`);
} else if (type === 'price' && data.underlying === 'BTC-USD') {
if (data.live) onPrice(data.t, data.price); // live is false while the feed is simulated: sit out
} else if (type === 'position' && data.result) {
open.delete(data.id);
console.log(`${data.result}: ${data.side} at ${data.strike}, BTC settled at ${data.settlePrice}, pnl ${data.pnl}`);
} else if (type === 'account' && data.balance < 10) {
api('/me/refill', { method: 'POST' }).then(() => console.log('refilled to $1,000'), () => {});
}
};
ws.onclose = () => {
console.log(`disconnected, retrying in ${delay / 1000}s`);
setTimeout(() => connect(Math.min(delay * 2, 30_000)), delay);
};
}
connect();Run it with bun bot.ts. It prints a line when it connects, one per order (side, strike, spot, expiry) and one per settlement (win, loss or push, the settle price and the PnL). Leave it running and it takes a rank on the leaderboard (GET /api/v1/leaderboard, or R in the app) once its first position settles.
What each part is doing:
- Sitting out simulated prices. Every
priceevent carrieslive. If Rizon's feeds (Coinbase, then Kraken) are both down, the exchange runs a simulated price and contracts still settle on it; a bot that only trades live prices skips those stretches. - State from
hello. Events aren't replayed after a disconnect. On reconnect,hellohas your open positions, so the bot rebuilds its count from there. - Backoff. A deploy drops connections for a few seconds; the bot retries after 1, 2, 4… seconds, up to 30.
- Refills. Once the balance is under $10,
POST /me/refillputs it back to $1,000.
5. Make it yours
- Size the trigger with
sigma. A fixed $15 trigger means different things at 14:00 and 21:00 UTC.GET /api/v1/marketsgives each market's realised sigma; trigger on a multiple of it instead. - Be the maker. Rest limit orders with
"tif": "post"at strikes either side of spot, and re-quote withPOST /api/v1/orders/batch(cancel and place in one step). - Hedge, don't close.
GET /api/v1/exposuresums your open positions by expiry and strike, so you can see your payout at every settle price. - Watch the tape. The
tradeschannel streams every trade on the exchange when it opens and again when it settles.
Limits worth knowing
| Limit | Value |
|---|---|
| REST requests | 20 a second per key, bursts of up to 40 |
| Orders | 20 a second per account |
| WebSockets | 10 open per key; 20 messages a second |
| New accounts | 20 an hour per address |
| Order size | $1 to $100,000, at most 2 decimals |
A 429 carries Retry-After. Trading refusals are 409s whose error a bot can branch on, such as no liquidity or insufficient balance. The full reference is at /docs.
For AI agents
Rizon publishes its docs in the formats agents read: /CLAUDE.md (also /AGENTS.md) is Rizon in one file to drop into a bot's project, /llms.txt is the index, /docs.md the full reference and /prompts.md a set of copy-paste prompts (a data collector, a market maker, a momentum bot, a copy trader, a dashboard, an analyst). An agent working for you asks for a key with POST /api/v1/connect and you approve it in your browser, so its trades show up on your own chart. Chat messages sent through the API carry api: true, so people can tell a script from someone typing.
FAQ
Is the Rizon API free?
Yes. It's a paper exchange: every account starts with $1,000 of practice money, there are no fees, and the real exchange is the sandbox.
Do I need to sign up to trade from code?
No. POST /api/v1/accounts returns a new account and a trade key in one call, with no email. To trade on the account you use in your browser, create a key on the docs page instead.
Which languages can I use?
Anything that speaks HTTP and WebSockets. The docs include curl, Python and TypeScript examples; this tutorial uses TypeScript on Bun because Bun's WebSocket can send an Authorization header.
Can an AI agent trade on Rizon?
Yes. Point it at /CLAUDE.md: it explains the markets and the connect flow, so the agent can ask for a key on your account and trade through the same API.
How often can my bot place orders?
Up to 20 orders a second per account, and 20 REST requests a second per key, in bursts of up to 40.