# Rizon API

Trade Rizon from code: live Bitcoin prices, the books, every trade on the exchange and your own account, over REST and a WebSocket. It's paper money: every account starts with $1,000 and can refill, so there's no sandbox. The real exchange is safe to build against.

| | |
|---|---|
| REST | `https://rizon.markets/api/v1` |
| WebSocket | `wss://rizon.markets/api/v1/ws` |
| Auth | `Authorization: Bearer rz_…` (market data needs no key) |
| Format | JSON. Money in paper USD, times in milliseconds since 1970 (server clock), strikes on each market's grid |
| For AI agents | [CLAUDE.md](https://rizon.markets/CLAUDE.md) · [llms.txt](https://rizon.markets/llms.txt) · [this page as Markdown](https://rizon.markets/docs.md) |

## Quickstart

```sh
# Market data needs no key
curl https://rizon.markets/api/v1/markets

# A key: make one at https://rizon.markets/docs#keys, or have a script make an account and its key
curl -X POST https://rizon.markets/api/v1/accounts
export RIZON_API_KEY=rz_…

# Buy up, $10, on the 15-second market
curl -X POST https://rizon.markets/api/v1/orders \
  -H "Authorization: Bearer $RIZON_API_KEY" -H "Content-Type: application/json" \
  -d '{"market": "BTC-15s", "side": "up", "size": 10}'
```

The same in Python and TypeScript:

```python
import os, requests

API = "https://rizon.markets/api/v1"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['RIZON_API_KEY']}"

print(s.get(f"{API}/markets").json()["markets"][0])
r = s.post(f"{API}/orders", json={"market": "BTC-15s", "side": "up", "size": 10})
print(r.status_code, r.json())
```

```ts
const API = 'https://rizon.markets/api/v1';
const headers = { Authorization: `Bearer ${process.env.RIZON_API_KEY}`, 'Content-Type': 'application/json' };

const { markets } = await (await fetch(`${API}/markets`)).json();
const res = await fetch(`${API}/orders`, { method: 'POST', headers, body: JSON.stringify({ market: 'BTC-15s', side: 'up', size: 10 }) });
console.log(res.status, await res.json());
```

## Markets and contracts

A market is an underlying and a horizon: `BTC-15s`, `BTC-1m` and `BTC-5m` are Bitcoin over 15 seconds, 1 minute and 5 minutes. `GET /markets` lists the markets that are open. Each has one continuous book, like a futures book whose price is the strike: `up` is buy, `down` is sell.

Orders carry no expiry. Each fill is a trade on the public tape, and a position for each trader on it: their side, at the resting order's strike. It expires on a whole second about one horizon after the fill. Read `expiresAt` rather than computing it: an order from the app can move it by a second. The expiry moves on every second, so a position can't be closed. It runs to expiry, and you hedge it with a later one. An up and a down at the same strike and expiry are flat.

| | |
|---|---|
| `up` | wins if the underlying at expiry is above the strike |
| `down` | wins if it's below the strike |
| Equal | a push: both sides get their size back |
| Payout | you pay your size; a win pays 2 × size, a loss pays 0. No fees |
| Size | $1 to $100,000 per order, at most 2 decimals |

| Underlying | Strikes (`strikeStep`, `strikeOffset`) | Read from | Settlement |
|---|---|---|---|
| `BTC-USD`: the Bitcoin price, in dollars | whole dollars (1, 0) | Coinbase, with Kraken as backup. If both are down the exchange runs a simulated price and `live` is false; contracts still trade and settle on it | the last price at or before the expiry, within about 50 ms of it. With no price in the 5 s before, a push (`settlePrice: null`) |

| | Up order (buy) | Down order (sell) |
|---|---|---|
| Book side | bid | ask |
| A limit at strike K trades at | any strike ≤ K | any strike ≥ K |
| Best | highest strike | lowest strike |
| A market order fills at | the best ask, then up the book | the best bid, then down the book |

A large market order can open several positions at several strikes. An order that would trade with your own resting order cancels that resting order instead (refunding it) and carries on down the book; the reply lists what it canceled.

There's no house. Every trade has an account on each side, and every account that has traded is on the leaderboard, people and bots alike. One of the bots is Rizon's market maker, which quotes every book so there's always something to trade. It's an ordinary account trading through this API, with the same limits as yours. On BTC its best bid and ask are usually 2 strikes apart at 15 s, 4 at 1 m and 9 at 5 m, wider while volatility runs high, centred on spot, with ten levels of $1,000 a side.

It re-quotes when the price moves, with one batch: its quotes that moved are canceled and replaced. Its new quotes trade with any resting order they cross, at that order's strike, so a resting order near the top of the book tends to fill right after the price moves through it. It hears prices on the same stream as everyone (every 50 ms; books every 100 ms), but it runs next to the exchange, so it hears them, and re-quotes, a little sooner than a client over the internet. Its replacement quotes join the back of each level's queue, so your order already resting at one of its strikes is ahead of it for other traders' market orders.

## Keys

| Way | For | How |
|---|---|---|
| In the browser | you | Create key, on [the docs page](https://rizon.markets/docs#keys). It's shown once |
| Connect | an agent, on your account | `POST /connect`, then you approve one link |
| A new account | a script with nobody to approve | `POST /accounts`: an account and its key in one call |

Keys have a scope. `trade` (the default) trades, cancels and chats as you; `read` can only GET and stream. Your keys and callsign stay with you: on your own account only the browser manages them, so revoking a key you gave an agent really cuts it off. On an account an agent made with `POST /accounts`, which has no browser, its trade keys manage both. Send the key as `Authorization: Bearer rz_…`; only a browser's WebSocket, which can't set headers, passes `?key=rz_…` instead. A key works until you revoke it.

### Connect an agent

The agent asks; you approve in a browser where you trade on Rizon; the agent collects the key.

```sh
curl -X POST https://rizon.markets/api/v1/connect \
  -H "Content-Type: application/json" -d '{"name": "btc-mm", "scope": "trade"}'
```

```json
{
  "code": "KX4P-9QHT",
  "url": "https://rizon.markets/connect?code=KX4P-9QHT",
  "poll": "https://rizon.markets/api/v1/connect/cr_8f…?wait=25",
  "expiresAt": 1761600600000
}
```

Show the user `url` and `code`. Then GET `poll` (that's `GET /connect/:id?wait=25`) until `status` isn't `pending`: each call waits up to 25 seconds for an answer.

| `status` | Means |
|---|---|
| `pending` | not answered yet: poll again |
| `approved` | `key` and `account` are in this response. The same poll gets it again for a minute, in case an answer is lost |
| `claimed` | the key was handed over and that minute has passed: if you lost it, start again |
| `denied` | the user said no |
| `expired` | 10 minutes passed: start again |

`name` (1-40 characters, default "my agent") becomes the key's name in the user's list; `scope` defaults to `trade`. 30 requests per hour per address. The approval page itself uses `GET /connect/codes/:code` (what's being asked) and `POST /connect/codes/:code` with `{"approve": true | false}`, which returns `{status}`: approving takes a browser signed in to Rizon, denying only the code.

### A new account

```sh
curl -X POST https://rizon.markets/api/v1/accounts \
  -H "Content-Type: application/json" -d '{"name": "my_bot", "keyName": "v1"}'
```

Returns `201 {"key": "rz_…", "account": {…}}`: a new account with $1,000 paper and a trade key that manages it. Both fields are optional: `name` is a callsign (3-16 letters, digits, `_` or `-`; without one it's `TRADER-####`), `keyName` names the key (1-40 characters). 20 per hour per address.

### Managing keys

| Method | Path | Does |
|---|---|---|
| `GET` | `/keys` | `{keys}`: your live keys, without their secrets |
| `POST` | `/keys` | `{"name"?, "scope"?}`: a new key, returned once with its secret in `key` |
| `DELETE` | `/keys/:id` | Revokes it at once and closes its WebSockets. Returns the key |

These need the browser, or a trade key on an account made with `POST /accounts`. An account holds up to 20 live keys.

## REST

Every path below is under `https://rizon.markets/api/v1`. Single objects come back as themselves, and lists in a field named for what they hold. Fields and parameters an endpoint doesn't take are refused, so a typo gets a 400 instead of being ignored. Lists page with `limit` (1-200, default 50) and `cursor`: pass back `next` from the previous page; it's null on the last.

### Market data

No key needed.

| Method | Path | Returns |
|---|---|---|
| `GET` | `/` | Links, the rules and every endpoint |
| `GET` | `/markets` | `{t, markets}`: each market's spot, feed state and top of book |
| `GET` | `/books/:market` | The book: up to 30 levels a side |
| `GET` | `/prices/:underlying?from=&to=&step=` | `{from, to, step, prices: [[t, price], …]}` for an underlying (`BTC-USD`): the last value in each `step` ms (250 to 60,000; default 250). Default the last 10 minutes; the server keeps 20 |
| `GET` | `/trades?market=&limit=&cursor=` | `{trades, next}`: every trade on the exchange, newest first |
| `GET` | `/trades/:id` | One trade |
| `GET` | `/leaderboard?limit=` | `{traders}`: ranked by realised PnL (`limit` 1-100, default 20) |
| `GET` | `/traders/:id` | One trader's record |
| `GET` | `/chat?limit=` | `{messages}`: the latest chat messages, oldest first (`limit` 1-50, default 50) |

`GET /markets`:

```json
{
  "t": 1761600000123,
  "markets": [
    { "id": "BTC-15s", "underlying": "BTC-USD", "horizon": 15, "strikeStep": 1, "strikeOffset": 0, "spot": 114233.2, "live": true, "bid": 114232, "ask": 114234, "bidSize": 1000, "askSize": 1000, "sigma": 35.45 },
    { "id": "BTC-1m", "underlying": "BTC-USD", "horizon": 60, "strikeStep": 1, "strikeOffset": 0, "spot": 114233.2, "live": true, "bid": 114231, "ask": 114235, "bidSize": 1000, "askSize": 1000, "sigma": 70.9 },
    { "id": "BTC-5m", "underlying": "BTC-USD", "horizon": 300, "strikeStep": 1, "strikeOffset": 0, "spot": 114233.2, "live": true, "bid": 114229, "ask": 114238, "bidSize": 1000, "askSize": 1000, "sigma": 158.55 }
  ]
}
```

`horizon` is in seconds. Strikes are `strikeOffset` plus a whole number of `strikeStep`s. `bid` is the best up order (a market down order fills there), `ask` the best down order (a market up order fills there). `sigma` is realised volatility: the underlying's one-sigma move over the horizon, from its recent ticks, in its own units. Nobody quotes it.

`GET /books/BTC-15s`:

```json
{
  "market": "BTC-15s",
  "t": 1761600000123,
  "bids": [{ "strike": 114232, "size": 1000 }, { "strike": 114230, "size": 1000 }],
  "asks": [{ "strike": 114234, "size": 1000 }, { "strike": 114236, "size": 1000 }]
}
```

A trade: `up` and `down` are who is on each side, `taker` is the side whose order arrived and traded (the other was resting; when the market maker re-quotes through a resting order, it's the taker), and `outcome` is the side that won, or `push`, once it settles. A side is `null` only on trades from before 2026-10-28, when the exchange made its own market.

```json
{
  "id": "Xq3vT9aLm2Pk",
  "market": "BTC-15s",
  "strike": 114234,
  "size": 100,
  "spot": 114233.2,
  "openedAt": 1761600000105,
  "expiresAt": 1761600016000,
  "up": { "id": "a1B2c3D4e5F6", "name": "TRADER-4821" },
  "down": { "id": "Mk7p2Qa9Lr4X", "name": "TRADER-7215" },
  "taker": "up",
  "settlePrice": 114240.02,
  "outcome": "up"
}
```

### Your account

Any key, except to rename.

| Method | Path | Returns |
|---|---|---|
| `GET` | `/me` | Your account |
| `GET` | `/orders?status=open\|all&market=&limit=&cursor=` | `{orders, next}`: open ones by default, newest first. Canceled orders that never filled are kept for a day |
| `GET` | `/orders/:id` | One of your orders |
| `GET` | `/positions?status=open\|settled\|all&market=&limit=&cursor=` | `{positions, next}`: open ones by default, latest expiry first |
| `GET` | `/positions/:id` | One of your positions: your side of the trade with the same id |
| `GET` | `/exposure` | `{exposure}`: your open positions summed by expiry and strike |
| `PATCH` | `/me` | `{"name"}`: a new callsign (3-16 letters, digits, `_` or `-`, unique). Your account. Only the account's owner: the browser, or a trade key on an account made with `POST /accounts` |

`GET /me`: your trader record, plus where your paper is. `balance` is free to trade, `inOrders` is held by your resting orders, `inPositions` is in open positions at cost. `pnl` is realised, and `rank` is null until your first position settles.

```json
{
  "id": "a1B2c3D4e5F6",
  "name": "TRADER-4821",
  "avatar": null,
  "rank": 41,
  "pnl": 0.5,
  "wins": 6,
  "losses": 5,
  "pushes": 1,
  "streak": 2,
  "bestStreak": 3,
  "balance": 870.5,
  "inOrders": 100,
  "inPositions": 30,
  "equity": 1000.5
}
```

`GET /exposure`: one entry per underlying and second at which you hold open positions, soonest first, with your open size on each side at each strike. The underlying's value at that second settles them all, whichever of its markets they came from. At a settle price S, an up at strike k pays 2 × size if S > k, and a down pays 2 × size if S < k, so each entry gives your payout at every price. To keep exposure live, track your positions by id instead: `hello` (or `GET /positions`) has your open ones, and each `position` event updates one.

```json
{
  "exposure": [
    { "underlying": "BTC-USD", "expiresAt": 1761600016000, "strikes": [{ "strike": 114232, "up": 0, "down": 50 }, { "strike": 114234, "up": 120, "down": 0 }] },
    { "underlying": "BTC-USD", "expiresAt": 1761600061000, "strikes": [{ "strike": 114235, "up": 0, "down": 40 }] }
  ]
}
```

### Trading

Trade key.

| Method | Path | Does |
|---|---|---|
| `POST` | `/orders` | Places an order. `201 {order, positions, canceled}` |
| `POST` | `/orders/batch` | `{"cancel"?: [ids], "orders"?: […]}`: cancels first (up to 100 ids; any not resting are skipped), then places up to 20 orders in turn, all in one step: nothing else trades in between. `{canceled, results}`: each result is what `POST /orders` would return, plus its `status`, such as `{"status": 201, "order", "positions", "canceled"}` or `{"status": 409, "error": "post-only order would trade"}` |
| `DELETE` | `/orders/:id` | Cancels one. The order |
| `DELETE` | `/orders?all=true&market=&side=` | Cancels every resting order, or one market's and/or side's, including any you placed in the app. `{canceled}`. Without `all=true` it's refused, so leaving out an id never cancels everything |
| `POST` | `/me/refill` | Back to $1,000 once your balance is below $10. Your account |

#### Place an order

| Field | | |
|---|---|---|
| `market` | required | `BTC-15s`, `BTC-1m` or `BTC-5m` |
| `side` | required | `up` or `down` |
| `size` | required | 1 to 100,000, at most 2 decimals. Taken from your balance when you place it |
| `strike` | optional | Makes it a limit order at this strike, on the market's grid (whole dollars on BTC). Leave it out for a market order |
| `tif` | optional | Limit orders only. `gtc` (the default) rests until it fills or you cancel it; `ioc` takes what it can at the strike or better, then cancels the rest; `post` only ever rests: if it would trade on arrival (against anyone, your own orders included), it's refused before it touches the book, so it cancels nothing. It isn't kept on the order |
| `clientId` | optional | Your own id for the order, unique per account while the order is kept (a canceled order that never filled, for a day; any other, for good): 1-64 of `A-Z a-z 0-9 _ - . :` |

```sh
# market: fills now at the best strikes available, or fails with 409 no liquidity
-d '{"market": "BTC-15s", "side": "up", "size": 25}'

# limit: rests in the book at 114230 until it fills or you cancel it
-d '{"market": "BTC-1m", "side": "up", "size": 100, "strike": 114230}'

# a maker quote that never takes
-d '{"market": "BTC-15s", "side": "down", "size": 200, "strike": 114236, "tif": "post", "clientId": "ask-1"}'
```

```json
{
  "order": {
    "id": "Qm7rL2xV9cNa",
    "clientId": null,
    "market": "BTC-15s",
    "side": "up",
    "strike": null,
    "size": 25,
    "filled": 25,
    "status": "filled",
    "createdAt": 1761600000105
  },
  "positions": [
    {
      "id": "Xq3vT9aLm2Pk",
      "orderId": "Qm7rL2xV9cNa",
      "market": "BTC-15s",
      "side": "up",
      "strike": 114234,
      "size": 25,
      "spot": 114233.2,
      "openedAt": 1761600000105,
      "expiresAt": 1761600016000,
      "settlePrice": null,
      "result": null,
      "pnl": null
    }
  ],
  "canceled": []
}
```

`positions` are what the order opened right away; `canceled` lists your own resting orders it met and canceled on the way. An order's `strike` is null for a market order, and while it's open, `size − filled` rests in the book. A market order that couldn't fill all of its size ends `canceled` with the rest refunded. One that filled nothing is refused with `409 no liquidity` and isn't recorded, and an `ioc` order that filled nothing is refused with `409 no liquidity at that strike`. A refusal also lists in `canceled` any of your resting orders it canceled. A resting order fills later at its own strike, and you hear about it on the WebSocket or by listing positions. If a request fails in transit, send it again with the same `clientId`: a `409 duplicate clientId` carries the `orderId` it already belongs to.

To re-quote, send one batch: the ids of your current quotes in `cancel` and the new ones in `orders`.

### Chat

Trade key.

| Method | Path | Does |
|---|---|---|
| `POST` | `/chat` | `{"text", "share"?, "feedback"?}`: up to 240 characters to the global chat. `share` is one of your settled positions' ids; `feedback: true` sends it as product feedback (up to 1,000 characters). `201` with the message |
| `PUT` | `/chat/:id/reactions/:emoji` | Puts your reaction on one of the last 50 messages: 👍 🔥 😂 🚀 💀 👀 (URL-encoded in the path). The message as it now stands |
| `DELETE` | `/chat/:id/reactions/:emoji` | Takes it off. Both are safe to repeat |

Messages sent through the API carry `api: true`, so people can tell a script from someone typing.

## WebSocket

`wss://rizon.markets/api/v1/ws`. Without a key you get the public channels; with one (an Authorization header, or `?key=rz_…` from a browser) you also get your own orders, positions and account, and can trade over the socket.

Pick channels with `?channels=`, comma-separated: `books` is every book and `all` everything. Leave it out for all of them. With a key, your own `order`, `position` and `account` events come whatever the channels.

| Channel | Events |
|---|---|
| `price:BTC-USD` | `price`: BTC's on every change, up to 20 a second |
| `book:<market>`, like `book:BTC-15s` | `book`: one market's book (up to 30 levels a side) on every change, up to 10 a second each |
| `trades` | `trade` when a trade opens, and again when it settles |
| `leaderboard` | `leaderboard`, whenever it changes |
| `chat` | `chat` for each new message, and again when its reactions change |

Every message from the server is `{"type", "data"}`. Events are named for what they carry; `trade` and `chat` are upserts, so keep the latest by id. On connect you get `hello`, then the current state of each subscribed price, book and leaderboard, so nothing waits for the next change.

| Event | `data` |
|---|---|
| `hello` | `{t, account, orders, positions, markets, channels}`: `account` is null without a key; `orders` and `positions` are your open ones, in the order REST lists them |
| `price` | `{underlying, t, price, live}` |
| `book` | A book |
| `trade` | A trade |
| `leaderboard` | A list of traders |
| `chat` | A chat message |
| `order` · `position` · `account` | One of yours, whenever it changes (needs a key) |

You send requests, named for what they do. Each may carry a `reqId` (a number, or a string of up to 128 characters), and gets one reply: `{"type": "ack" | "error", "reqId", "status", "data"}`, with the status and body its REST call would return.

| Request | Fields | Same as |
|---|---|---|
| `subscribe` · `unsubscribe` | `channels: [...]` | Acked with `{channels}`: everything you now get |
| `place` | the order's fields (trade key) | `POST /orders` |
| `cancel` | `orderId`; or `all: true`, with `market`? and `side`? to narrow it | `DELETE /orders/:id`, `DELETE /orders?all=true` |
| `batch` | `cancel`?, `orders`? | `POST /orders/batch` |
| `ping` | | Acked with `{t}`: the server time |

```json
{ "type": "place", "reqId": 1, "market": "BTC-15s", "side": "up", "size": 10 }
{ "type": "order", "data": { "id": "Qm7rL2xV9cNa", "status": "filled", … } }
{ "type": "position", "data": { "id": "Xq3vT9aLm2Pk", "strike": 114234, … } }
{ "type": "account", "data": { "balance": 990, … } }
{ "type": "ack", "reqId": 1, "status": 201, "data": { "order": { … }, "positions": [ … ], "canceled": [] } }
```

An order's `order`, `position` and `account` events arrive before its reply. For an order placed over REST they're sent before the response, but on another connection, so either can arrive first. Events aren't replayed after a disconnect: `hello` on reconnect has your open orders and positions, and `GET /positions?status=settled` has what settled meanwhile.

A Python bot, with the stream and REST together:

```python
import asyncio, json, os, httpx, websockets  # websockets 14+

KEY = os.environ["RIZON_API_KEY"]
AUTH = {"Authorization": f"Bearer {KEY}"}
API = "https://rizon.markets/api/v1"
WS = "wss://rizon.markets/api/v1/ws?channels=price:BTC-USD,book:BTC-15s"

async def main():
    async with httpx.AsyncClient(base_url=API, headers=AUTH) as rest:
        delay = 1
        while True:
            try:
                async with websockets.connect(WS, additional_headers=AUTH) as ws:
                    delay = 1
                    async for raw in ws:
                        m = json.loads(raw)
                        kind, data = m["type"], m.get("data")
                        if kind == "hello":  # your open orders and positions: rebuild state here
                            print("open positions:", len(data["positions"]))
                        elif kind == "price" and not data["live"]:
                            continue  # a simulated price: sit out
                        elif kind == "position" and data["result"]:
                            print("settled:", data["result"], data["pnl"])
                        # decide, then act over REST, or send {"type": "place", ...} on the socket:
                        # r = await rest.post("/orders", json={"market": "BTC-15s", "side": "up", "size": 10})
            except (OSError, websockets.exceptions.WebSocketException) as e:  # refused, dropped or closed
                print(f"disconnected ({e}), retrying in {delay}s")
                await asyncio.sleep(delay)
                delay = min(delay * 2, 30)

asyncio.run(main())
```

In a browser:

```js
const ws = new WebSocket(`wss://rizon.markets/api/v1/ws?channels=price:BTC-USD,book:BTC-15s&key=${key}`);
ws.onmessage = (e) => {
  const { type, data } = JSON.parse(e.data);
  if (type === 'price') console.log(new Date(data.t).toISOString(), data.price, data.live ? '' : '(simulated)');
  if (type === 'book') console.log(data.market, data.bids[0]?.strike, data.asks[0]?.strike);
};
```

## Errors and limits

Errors are `{"error": "…"}` with a status, and the message says what to fix.

| Status | Means |
|---|---|
| `400` | Malformed: a field or parameter is missing, out of range, or not one the endpoint takes |
| `401` | No key, or a key that isn't valid (revoked?) |
| `403` | The key's scope doesn't allow it (`this key is read-only`), or it's for the account's owner |
| `404` | No such endpoint, market or object (yours only, for orders and positions) |
| `405` | The path exists but not with this method: see the `Allow` header |
| `409` | Valid, but not now. See below |
| `410` | A connect code that expired |
| `429` | Too fast: wait `Retry-After` seconds (also `retryAfter` in the body, and on WebSocket errors) |
| `503` | `no price yet` (just after a restart), or too many connect requests at once |

A trading 409's `error` is one of these, so a bot can branch on it:

| `error` | Also carries |
|---|---|
| `insufficient balance` | |
| `no liquidity` | `canceled`, if it canceled any of your orders |
| `no liquidity at that strike` | `canceled`, likewise |
| `post-only order would trade` | |
| `duplicate clientId` | `orderId` |
| `order is already filled` · `order is already canceled` | `order` |

The other 409s are `refill is available once your balance is below 10`, `name taken`, a full key list, and a connect code that was already answered.

A restart (a deploy) drops connections for a few seconds, and a request in that window can fail with a `502` and no JSON. Resting orders and open positions survive it, and positions that expire meanwhile settle once it's back, on the real price at their expiry.

| Limit | |
|---|---|
| REST requests | 20 a second per key (or per address without one), in bursts of up to 40. Each WebSocket connect counts as one; a bad key counts against its address |
| Orders | 20 a second per account, batches and the WebSocket included. Cancels don't count |
| Batch | 20 orders |
| WebSockets | 10 open per key (or per address without one); 20 messages a second from each key or address, in bursts of up to 40 |
| Chat | 1 message per 800 ms and 5 per 10 s; 20 reactions per 10 s |
| New accounts | 20 per hour per address |
| Connect requests | 30 per hour per address |
| Keys | 20 live per account |

## Objects

```ts
type Side = 'up' | 'down';

interface Market {                                    // an underlying and a horizon, one continuous book
  id: string; underlying: string; horizon: number;    // 'BTC-15s', 'BTC-USD', 15 (seconds)
  strikeStep: number; strikeOffset: number;           // strikes: strikeOffset + n × strikeStep
  spot: number | null; live: boolean;                 // live: false while the value is simulated or stale
  bid: number | null; ask: number | null; bidSize: number; askSize: number;
  sigma: number;                                      // realised 1-sigma move over the horizon, in the underlying's units
}
interface Book { market: string; t: number; bids: Level[]; asks: Level[] }   // best first
interface Level { strike: number; size: number }

interface Order {
  id: string; clientId: string | null; market: string; side: Side;
  strike: number | null;                              // null: a market order
  size: number; filled: number;                       // while open, size - filled rests
  status: 'open' | 'filled' | 'canceled'; createdAt: number;
}
interface Placed { order: Order; positions: Position[]; canceled: Order[] }   // POST /orders

interface Position {                                  // your side of one trade, with the trade's id
  id: string; orderId: string | null; market: string; side: Side; strike: number; size: number;
  spot: number;                                       // the underlying at the fill
  openedAt: number; expiresAt: number;                // expiresAt: a whole second. Read it, don't compute it
  settlePrice: number | null;
  result: 'win' | 'loss' | 'push' | null;             // null while open
  pnl: number | null;                                 // +size, -size or 0
}

interface Exposure { underlying: string; expiresAt: number; strikes: { strike: number; up: number; down: number }[] }

interface Trade {                                     // a fill on the public tape
  id: string; market: string; strike: number; size: number; spot: number;
  openedAt: number; expiresAt: number;
  up: Party | null; down: Party | null;               // who's on each side (null only on trades before 2026-10-28)
  taker: Side | null;                                 // the side whose order arrived (null only on old trades)
  settlePrice: number | null; outcome: Side | 'push' | null;   // null while open
}
interface Party { id: string; name: string }

interface Trader {
  id: string; name: string; avatar: string | null; rank: number | null;
  pnl: number; wins: number; losses: number; pushes: number; streak: number; bestStreak: number;
}
interface Account extends Trader { balance: number; inOrders: number; inPositions: number; equity: number }

interface ChatMessage {
  id: string; from: Party; text: string; createdAt: number;
  share?: Position; feedback?: true; api?: true;
  reactions?: { emoji: string; by: Party[] }[];
}

interface KeyInfo { id: string; name: string; scope: 'trade' | 'read'; hint: string; createdAt: number; lastUsedAt: number | null }
```

Avatar URLs are paths on `https://rizon.markets`.
