Rizon

Learn

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.

5 min read
  • api
  • developers
  • bots
  • typescript

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 with POST /api/v1/orders, and stream prices and your own fills over wss://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:

WhoHow
You, in a browser"Create key" on the docs page; it's shown once
A script with nobody to approvecurl -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 accountPOST /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" >> .gitignore

The 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 price event carries live. 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, hello has 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/refill puts 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/markets gives 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 with POST /api/v1/orders/batch (cancel and place in one step).
  • Hedge, don't close. GET /api/v1/exposure sums your open positions by expiry and strike, so you can see your payout at every settle price.
  • Watch the tape. The trades channel streams every trade on the exchange when it opens and again when it settles.

Limits worth knowing

LimitValue
REST requests20 a second per key, bursts of up to 40
Orders20 a second per account
WebSockets10 open per key; 20 messages a second
New accounts20 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.

More from Learn

  1. Elon Musk's posts this week: 304 in seven days (Oct 19 – Oct 25, 2026)

    Elon Musk posted 304 times on X over Oct 19 – Oct 25: 43.4 a day, down 16% on the week before. His busiest hours, bursts and quiet stretches.

    3 min read
    • elon musk
    • x
    • posts
    • weekly
  2. Bitcoin's week, second by second: Oct 19 – Oct 25, 2026

    Bitcoin, Oct 19 – Oct 25: median 1-minute move $17, biggest minute −$532, and how often 5-minute windows flipped in their last 30 seconds.

    3 min read
    • bitcoin
    • btc
    • volatility
    • weekly
  3. Trading CPI and FOMC on short horizons: how big is Bitcoin's first minute?

    Across 21 CPI releases (2025–26), Bitcoin's median first-minute move was 0.47%, about 10 times a normal weekday minute. After 14 FOMC statements, 0.27%.

    8 min read
    • btc
    • data
    • macro
    • cpi
    • fomc