Welcome to API Academy

Module 02 · Foundations · ~45 min

Trader control panel.

By the end of this module, you’ll be watching your own trading panel, positions, P&L, and a kill switch, on your phone, before the bot that feeds it exists. The screen you’ll trust your money with.

Build the trader’s surface before the bot that fills it. A one-page HTML panel rendering positions, fills, P&L, and a kill switch, running on hand-written seed data so you see your “first trade” on your phone before any API code exists. Every later module wires its output into this panel; the dashboard is what makes the bot yours.

Quick answer

How do you build a control panel for a trading bot?

Build it before the bot: a one-page HTML panel (vanilla JS plus Chart.js from a CDN) over a thin backend that reads positions.json and fills.ndjson off disk and serves them as JSON, all running on hand-written seed data. The page polls four read endpoints (/api/positions, /api/orders/open, /api/fills/recent, /api/pnl/series) every 2 seconds and renders four panels: positions, open orders, recent fills, and a P&L line chart. A manual cancel button and a file-flag kill switch give you an override surface, and a token locks the whole thing down. Total surface area is about 100 lines of server code and about 200 lines of HTML/JS, no framework, no build step. Every later module pours real bot output into the same two files, so by Module 05 your first real order lands in a panel you already trust.

Verified 2026-06-10 against the backend (8c172d4d) and the SDK releases; the previously flagged endpoint corrections are applied below. The /api/* routes are this panel’s own server, not Limitless.

Section 01

Why panel, then bot.

The classic order, build the bot, then bolt monitoring on at the end, produces a bot you can’t see and a panel you don’t trust. Reversing it gives you the opposite: a panel you trust because you wrote it on something you can verify (seed data), and a bot you watch from the day it places its first real order. The bot becomes a thing you watch, not a thing you debug after the fact.

The panel is the contract. Positions, open orders, recent fills, a P&L curve, and a kill switch, rendered side by side, refreshed every few seconds, behind a single bookmarkable URL. The bot you build over the next sixteen modules has exactly one obligation to this surface: keep positions.json and fills.ndjson truthful. Everything else, how the page lays out, what colors mean, where the cancel button lives, is decided here, today, on data you wrote by hand.

You will use this panel every day for the rest of API Academy. It is the first thing you open at 7am, the last thing you check at 11pm, the surface you bookmark on your phone. Build it now, on seed data, while the stakes are zero. By Module 05 you’ll place your first real order and watch it appear in the panel you already trust.

See the book

Positions, open orders, recent fills, P&L, rendered side by side, refreshed every few seconds. Replaces three terminals and a Discord scroll-back before the first one ever exists.

Read the past

A scrollable fills list with timestamps and reason codes. “What did the bot do at 3am” takes one scroll, not a grep. Today the answers come from your seed file.

Override safely

One button per open order, plus a global kill switch. The contract is set today, scoped, expiring, logged, so the bot you write later inherits a safe override surface instead of growing one bolted-on.

Section 02

The architecture.

Three pieces, no framework. A backend that reads positions.json and fills.ndjson off disk and exposes them as JSON over four endpoints. A single-page HTML served from the same backend that polls those endpoints (or subscribes via websocket later) and renders four panels. An auth layer, URL token or HTTP Basic, so only you can hit it.

Total surface area: about 100 lines of Python or TypeScript on the server, about 200 lines of HTML/JS in the page. No bundler, no build step, no React. Same posture as the rest of this academy.

The diagram on the right is the whole system. If your sketch on a napkin looks like more than this, you’re overbuilding.

   browser (your phone, your laptop)
        |
        |  HTTPS  +  ?key=…  or  Basic auth
        v
   +-----------------------------+
   |  control-panel backend      |
   |  (FastAPI / Hono / Go)      |
   |                             |
   |  /              -> index.html
   |  /api/positions -> JSON
   |  /api/orders/open -> JSON
   |  /api/fills/recent -> JSON
   |  /api/pnl/series -> JSON
   |  /api/state -> { killed }
   |  /api/orders/{id}/cancel POST
   |  /api/kill POST             |
   +--------------+--------------+
                  |
                  |  reads + appends
                  v
        $ACADEMY_DATA_DIR/
            positions.json     (today: seed)
            fills.ndjson       (today: seed)
            audit.ndjson       (writes back)
            kill_switch.flag
              

Section 03

Drop in seed data.

The whole point of building the panel first is that it works now, on data you wrote by hand, before any bot exists. Drop these two files into $ACADEMY_DATA_DIR on the Railway volume you provisioned in Module 01. The backend will read them; the panel will render them.

positions.json, current book

[
  {"market":"btc-up-may-2026","side":"yes","size":42.0,"avgPx":0.6125,"upnl":1.34},
  {"market":"eth-up-may-2026","side":"yes","size":18.5,"avgPx":0.4108,"upnl":-0.62},
  {"market":"trump-2028-nominee","side":"no","size":12.0,"avgPx":0.7401,"upnl":0.27},
  {"market":"fed-cut-jun","side":"yes","size":9.25,"avgPx":0.5503,"upnl":0.04},
  {"market":"sp500-up-week","side":"no","size":6.0,"avgPx":0.4870,"upnl":-0.15},
  {"market":"oil-above-80","side":"yes","size":15.0,"avgPx":0.3350,"upnl":0.83}
]

fills.ndjson, recent fills, one event per line

{"ts":"2026-04-30T22:14:08Z","market":"btc-up-may-2026","side":"yes","price":0.6125,"size":12.0,"source":"seed"}
{"ts":"2026-04-30T22:41:55Z","market":"eth-up-may-2026","side":"yes","price":0.4108,"size":18.5,"source":"seed"}
{"ts":"2026-05-01T01:09:12Z","market":"trump-2028-nominee","side":"no","price":0.7401,"size":12.0,"source":"seed"}
{"ts":"2026-05-01T03:32:44Z","market":"fed-cut-jun","side":"yes","price":0.5503,"size":9.25,"source":"seed"}
{"ts":"2026-05-01T07:50:01Z","market":"sp500-up-week","side":"no","price":0.4870,"size":6.0,"source":"seed"}
{"ts":"2026-05-01T09:18:33Z","market":"oil-above-80","side":"yes","price":0.3350,"size":15.0,"source":"seed"}
{"ts":"2026-05-01T10:02:17Z","market":"btc-up-may-2026","side":"yes","price":0.6210,"size":30.0,"source":"seed"}
{"ts":"2026-05-01T11:44:09Z","market":"eth-up-may-2026","side":"yes","price":0.4090,"size":-4.0,"source":"manual_cancel"}

Eight rows: six initial buys, one add, one manual cancel. The panel renders the first four columns as a fills table; the last row demonstrates that cancel events live in the same stream.

Get the seed files onto your Railway volume

# From your project root, with the snippets above saved as positions.json and fills.ndjson:
railway run "mkdir -p /app/data && cat > /app/data/positions.json" < positions.json
railway run "cat > /app/data/fills.ndjson" < fills.ndjson

# Verify
railway run "wc -l /app/data/fills.ndjson"
railway run "head -1 /app/data/positions.json"

Section 04

Backend: the four endpoints.

Every panel on the page maps to exactly one endpoint. The backend is a thin wrapper around two files on disk, positions.json and fills.ndjson, with no SDK calls today. When the bot exists, it writes those same two files; the panel never needs to know whether a human or a bot wrote them.

GET /api/positions
GET /api/orders/open
GET /api/fills/recent?limit=50
GET /api/pnl/series?range=24h

Tabs on the right show the same backend in TypeScript (Express), Python (FastAPI), and Go (net/http). Pick whichever language you plan to write your bot in, the panel runs in the same process so by Module 18, your auth, your SDK client, and your logger are already there.

// Module 02, Control panel backend (Express + Limitless SDK).
// Mounts four read-only JSON endpoints on the same process as your (future) bot.
//
// $ npm install express @limitless-exchange/sdk

import express from 'express';
import { HttpClient, PortfolioFetcher } from '@limitless-exchange/sdk';
import fs from 'fs';

const http = new HttpClient({ apiKey: process.env.LIMITLESS_API_KEY });
const portfolio = new PortfolioFetcher(http);

const app = express();

app.get('/api/positions', async (_req, res) => {
  const p = await portfolio.getPositions();
  res.json(p.clob.map(pos => ({
    market: pos.market.title,
    side:   pos.side,
    size:   pos.size,
    avgPx:  pos.averagePrice,
    upnl:   pos.unrealisedPnl,
  })));
});

app.get('/api/orders/open', async (_req, res) => {
  // GET /portfolio/orders: all CLOB orders across markets; statuses=LIVE keeps only resting ones.
  const { orders } = await http.get('/portfolio/orders?statuses=LIVE');
  res.json(orders.map(o => ({
    id: o.id, marketId: o.marketId, side: o.side,
    price: o.price, size: o.remainingSize, placedAt: o.createdAt,
  })));
});

app.get('/api/fills/recent', async (req, res) => {
  const limit = Math.min(Number(req.query.limit ?? 50), 200);
  // GET /portfolio/history: paginated trade history (AMM + CLOB fills).
  const hist = await portfolio.getUserHistory(undefined, limit);
  res.json(hist.data.map(f => ({
    ts: f.blockTimestamp, market: f.market?.title, side: f.strategy,
    price: f.outcomeTokenPrice, size: f.outcomeTokenAmount,
  })));
});

app.get('/api/pnl/series', (req, res) => {
  // Replay your NDJSON log into a {ts, equity}[] series.
  const range = String(req.query.range ?? '24h');
  const since = Date.now() - parseRange(range);
  const lines = fs.readFileSync('audit.ndjson', 'utf8').trim().split('\n');
  const series = lines
    .map(l => JSON.parse(l))
    .filter(e => e.kind === 'equity' && e.ts >= since)
    .map(e => ({ ts: e.ts, equity: e.equity }));
  res.json(series);
});

function parseRange(s: string): number {
  const m = /^(\d+)([hd])$/.exec(s);
  if (!m) return 24 * 3_600_000;
  return Number(m[1]) * (m[2] === 'h' ? 3_600_000 : 86_400_000);
}

app.listen(8082, () => console.log('panel on :8082'));

How to run this

  1. For today, swap the SDK calls for fs.readFile on positions.json and fills.ndjson from your data dir. The shape stays identical, the bot just hasn’t shipped yet. Module 06 swaps the file reads for SDK calls without changing the JSON contract.
  2. Save as panel.ts, then npx tsx panel.ts. curl localhost:8082/api/positions should return JSON.
  3. Don’t expose port 8082 to the public yet, auth comes in Section 08.

Section 05

Frontend: render the book.

One index.html file, served from the same backend. Vanilla JS, no React, no bundler, no Tailwind on this surface either if you can help it. Four panels: a positions table, an open-orders list, a recent-fills list, and a P&L line chart drawn with Chart.js from a CDN. One setInterval at 2 seconds re-fetches everything; render functions diff into the DOM. The whole file fits on one screen.

<!-- Module 02, Control panel frontend.
     Drop next to your backend; serve as GET / from the same process. -->
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Bot panel</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
  <style>
    body { font: 14px/1.5 system-ui, sans-serif; margin: 24px; background: #09090b; color: #e4e4e7; }
    h2 { font: 600 11px/1 ui-monospace, monospace; letter-spacing: .18em; text-transform: uppercase; color: #a1a1aa; margin: 24px 0 8px; }
    .grid { display: grid; gap: 24px; grid-template-columns: 1fr 1fr; }
    table { width: 100%; border-collapse: collapse; font: 12px/1.4 ui-monospace, monospace; }
    th, td { padding: 6px 8px; text-align: left; border-bottom: 1px solid #27272a; }
    th { color: #71717a; font-weight: 500; }
    canvas { background: #18181b; border: 1px solid #27272a; border-radius: 8px; padding: 8px; }
  </style>
</head>
<body>
  <h1 style="font: 700 22px/1 system-ui;">Bot panel</h1>
  <div class="grid">
    <div><h2>Positions</h2><table id="pos"></table></div>
    <div><h2>Open orders</h2><table id="ord"></table></div>
    <div style="grid-column: 1/-1"><h2>P&amp;L · last 24h</h2><canvas id="pnl" height="120"></canvas></div>
    <div style="grid-column: 1/-1"><h2>Recent fills</h2><table id="fil"></table></div>
  </div>

  <script>
    const $  = (id) => document.getElementById(id);
    const fmt = (n, d=2) => (Number(n) || 0).toFixed(d);
    const time = (ts) => new Date(ts).toLocaleTimeString();

    const TPL = {
      pos: (rows) => row('Market | Side | Size | Avg | uPnL', rows.map(r =>
        `${r.market} | ${r.side} | ${fmt(r.size)} | ${fmt(r.avgPx, 4)} | ${fmt(r.upnl)}`)),
      ord: (rows) => row('Market | Side | Px | Size | Action', rows.map(r =>
        `${r.market} | ${r.side} | ${fmt(r.price, 4)} | ${fmt(r.size)} | <button data-cancel="${r.id}">Cancel</button>`)),
      fil: (rows) => row('Time | Market | Side | Px | Size | Source', rows.map(r =>
        `${time(r.ts)} | ${r.market} | ${r.side} | ${fmt(r.price, 4)} | ${fmt(r.size)} | ${r.source}`)),
    };

    function row(header, lines) {
      const head = '<tr>' + header.split('|').map(h => `<th>${h.trim()}</th>`).join('') + '</tr>';
      const body = lines.map(l => '<tr>' + l.split('|').map(c => `<td>${c.trim()}</td>`).join('') + '</tr>').join('');
      return head + body;
    }

    const chart = new Chart($('pnl').getContext('2d'), {
      type: 'line',
      data: { datasets: [{ label: 'equity', data: [], borderColor: '#c3ff00', tension: 0.15, pointRadius: 0 }] },
      options: { animation: false, parsing: false, scales: { x: { type: 'linear' }, y: { ticks: { color: '#a1a1aa' } } }, plugins: { legend: { display: false } } },
    });

    async function tick() {
      const [pos, ord, fil, pnl] = await Promise.all([
        fetch('/api/positions').then(r => r.json()),
        fetch('/api/orders/open').then(r => r.json()),
        fetch('/api/fills/recent?limit=50').then(r => r.json()),
        fetch('/api/pnl/series?range=24h').then(r => r.json()),
      ]);
      $('pos').innerHTML = TPL.pos(pos);
      $('ord').innerHTML = TPL.ord(ord);
      $('fil').innerHTML = TPL.fil(fil);
      chart.data.datasets[0].data = pnl.map(p => ({ x: p.ts, y: p.equity }));
      chart.update();
    }

    tick();
    setInterval(tick, 2000);
  </script>
</body>
</html>

How to run this

  1. Save the snippet above as index.html next to your backend file.
  2. In your backend, add one route that serves it: app.use(express.static('.')), FastAPI’s app.mount('/', StaticFiles(directory='.', html=True)), or Go’s http.Handle("/", http.FileServer(http.Dir("."))).
  3. Open http://localhost:8082/. Four panels render against your seed data. The chart fills in once you’ve seeded at least two equity rows (or after the bot writes them, later).
  4. Cancel buttons are wired in Section 07; the click handler is intentionally missing here so you read the safety section first.

Section 06

Real-time updates with websockets.

Polling at 2 seconds is fine for a book of ten positions and a few fills per minute. The day your strategy starts firing fills faster than you can refresh, switch to a push channel: the backend opens a websocket, your bot publishes fill and order events as they land, and the page subscribes and pushes them straight into the same render functions.

The pattern is event-fan-out, not request/response, you keep the four GET endpoints for the initial snapshot when the page loads, then live-update from there. Keep a 30 s reconnect on the client; the network will drop. Today the websocket has no producer; the scaffolding is here so the bot can plug in by Module 07.

// Module 02, Websocket fan-out (server) + listener (client).
//
// $ npm install ws

import { WebSocketServer, WebSocket } from 'ws';

const wss = new WebSocketServer({ port: 8083 });
const clients = new Set<WebSocket>();

wss.on('connection', (ws) => {
  clients.add(ws);
  ws.on('close', () => clients.delete(ws));
});

// Call from anywhere in your bot when an event lands.
export function publish(kind: 'fill' | 'order', payload: unknown): void {
  const msg = JSON.stringify({ kind, payload, ts: Date.now() });
  for (const ws of clients) {
    if (ws.readyState === WebSocket.OPEN) ws.send(msg);
  }
}

// ---- client (drop into the <script> in index.html) ----
//
// const ws = new WebSocket(`ws://${location.hostname}:8083/stream`);
// ws.onmessage = (e) => {
//   const evt = JSON.parse(e.data);
//   if (evt.kind === 'fill')  prependFill(evt.payload);
//   if (evt.kind === 'order') refreshOrders();
// };
// ws.onclose = () => setTimeout(() => location.reload(), 30_000);

Section 07

Manual override: cancel and kill switch.

One button per open order. Clicking it pops a confirm dialog, POSTs to /api/orders/{id}/cancel, and writes a manual_cancel row into the audit log. Idempotent on the backend, if the order is already gone, return success silently. Today there’s no real bot listening, so the cancel just appends a request line to audit.ndjson; the safety contract still applies. Same shape for the kill switch: one POST flips a file flag, the panel reads it back on the next tick.

// Module 02, Manual cancel (backend endpoint + frontend handler).

import express from 'express';
import fs from 'fs';
import { HttpClient, OrderClient } from '@limitless-exchange/sdk';

const http   = new HttpClient({ apiKey: process.env.LIMITLESS_API_KEY });
const orders = new OrderClient(http);

const app = express();
app.use(express.json());

app.post('/api/orders/:id/cancel', async (req, res) => {
  const id = req.params.id;
  try {
    await orders.cancel(id);
  } catch (err: any) {
    // Already cancelled / filled, treat as success (idempotent).
    if (err?.status !== 404) throw err;
  }
  fs.appendFileSync('audit.ndjson', JSON.stringify({
    kind:   'manual_cancel',
    ts:     Date.now(),
    id,
    actor:  req.ip,
  }) + '\n');
  res.json({ ok: true, id });
});

// ---- client (add inside the index.html script) ----
//
// document.addEventListener('click', async (e) => {
//   const id = e.target?.dataset?.cancel;
//   if (!id) return;
//   if (!confirm(`Cancel order ${id}?`)) return;
//   const r = await fetch(`/api/orders/${id}/cancel`, { method: 'POST' });
//   if (!r.ok) alert('Cancel failed');
//   tick();  // re-fetch all panels
// });

A cancel button is order-mutation surface. Treat it like a kill switch with a UI, even today, when no bot is listening.

Always wrap the click in a confirm(), always log the action, never expose this endpoint without auth (Section 08), and never let the bot “auto-replace” an order you just manually cancelled, treat manual_cancel events in your strategy loop as a hands-off signal for that market until you say otherwise. Three required properties: scoped, expiring, logged.

Section 08

The P&L chart.

Twenty lines of Chart.js setup, one line series, no candles, no annotations. The lesson is “you can see your edge or lack of it in two seconds.” If your equity curve looks like noise, your strategy probably is. Today the curve renders against a sample 24-hour series you seed; by Module 13 (PnL Analysis) the same chart is showing real numbers without one line of frontend changing.

<!-- Module 02, Minimal Chart.js setup for the equity curve. -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<canvas id="pnl" height="120"></canvas>

<script>
  const ctx = document.getElementById('pnl').getContext('2d');

  const chart = new Chart(ctx, {
    type: 'line',
    data: {
      datasets: [{
        label:       'equity',
        data:        [],
        borderColor: '#c3ff00',
        borderWidth: 1.5,
        tension:     0.15,
        pointRadius: 0,
      }],
    },
    options: {
      animation: false,
      parsing:   false,                       // expects {x, y} numerics
      scales: {
        x: { type: 'linear', ticks: { color: '#a1a1aa' } },
        y: { ticks: { color: '#a1a1aa' } },
      },
      plugins: { legend: { display: false } },
    },
  });

  async function refreshPnl() {
    const series = await fetch('/api/pnl/series?range=24h').then(r => r.json());
    chart.data.datasets[0].data = series.map(p => ({ x: p.ts, y: p.equity }));
    chart.update();
  }

  refreshPnl();
  setInterval(refreshPnl, 5000);
</script>

Section 09

Auth: protect the panel.

This panel can cancel orders and trip the kill switch. Anything more lax than “you can’t hit it without a secret” is a foot-gun pointed at your future book. Two practical patterns: a URL token for solo use (you bookmark /?key=… and that’s it), or HTTP Basic if you share access with one or two collaborators. Both shown below. Pick one. Never run without one, even today, when the panel is just rendering seed data.

// Module 02, Two auth options for the panel. Pick one.

import express from 'express';
import auth from 'basic-auth';

const app = express();

const PANEL_TOKEN = process.env.PANEL_TOKEN ?? '';
const PANEL_USER  = process.env.PANEL_USER  ?? '';
const PANEL_PASS  = process.env.PANEL_PASS  ?? '';

// ---- Option A: URL token. /?key=… or X-Panel-Key header. ----
function urlToken(req: express.Request, res: express.Response, next: express.NextFunction) {
  const supplied = req.query.key ?? req.header('x-panel-key');
  if (!PANEL_TOKEN || supplied !== PANEL_TOKEN) {
    return res.status(401).send('unauthorized');
  }
  next();
}

// ---- Option B: HTTP Basic. ----
function httpBasic(req: express.Request, res: express.Response, next: express.NextFunction) {
  const creds = auth(req);
  if (!creds || creds.name !== PANEL_USER || creds.pass !== PANEL_PASS) {
    res.set('WWW-Authenticate', 'Basic realm="bot panel"');
    return res.status(401).send('unauthorized');
  }
  next();
}

// Mount one of them BEFORE every /api and /stream route.
app.use(urlToken);          // or: app.use(httpBasic)

// Then your routes…
// app.get('/api/positions', …);

Never commit the token, never run without auth, terminate TLS in front of the panel.

Generate PANEL_TOKEN with openssl rand -hex 32, store it in your .env, and put a Caddy / Nginx / Cloudflare-Tunnel HTTPS layer in front of port 8082. Plain HTTP plus a token in the URL is fine on your home wifi; on the public internet it’s a credential leak waiting to happen.

Section 10

Sibling lesson.

Two early-foundational modules, same shape

This panel is the deterministic-bot half of a deliberate pair. Agents Academy Module 02, Your Dashboard is the LLM-agent half: same architecture (small backend + single-page HTML, seed data on disk, token-gated, kill switch as a file flag), different surface. The control panel surfaces positions / fills / P&L for a deterministic strategy; the dashboard surfaces reasoning / tool-use / iterations for an agent loop.

Both are early-foundational modules, built before the bot or agent that fills them. If you’re going to write an LLM-driven trader instead of (or alongside) the deterministic one in this curriculum, the Agents Academy module is the right operator surface for that brain. The two cross-link from here on.

Common questions

Trader control panel: what people ask

Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.

  1. What endpoints does a bot control panel need?
    Four reads plus the override surface: GET /api/positions, /api/orders/open, /api/fills/recent?limit=50, and /api/pnl/series?range=24h feed the four panels, while POST /api/orders/{id}/cancel and a kill-switch POST handle overrides, with /api/state reporting the killed flag. These /api/* routes belong to the panel’s own server, not Limitless; real exchange calls like GET /portfolio/orders?statuses=LIVE replace the file reads later without changing the JSON contract.
  2. How do you secure a self-hosted trading panel?
    Two practical patterns: a URL token (bookmark /?key=… or send an X-Panel-Key header) for solo use, or HTTP Basic for one or two collaborators. Generate PANEL_TOKEN with openssl rand -hex 32, keep it in env vars, and terminate TLS in front of port 8082 with Caddy, Nginx, or a Cloudflare Tunnel. Never run without auth, even on seed data: the panel can cancel orders and trip the kill switch.
  3. What data files does the control panel read and write?
    The single source of truth is the file system on the Railway volume: positions.json (current book) and fills.ndjson (one fill event per line) under $ACADEMY_DATA_DIR. The panel reads those two and writes its own actions back to audit.ndjson; the kill switch is a file flag (kill_switch.flag). The bot’s only obligation to this surface is keeping those files truthful.
  4. How should a manual cancel button work?
    Like a kill switch with a UI: the click pops a confirm() dialog, POSTs to /api/orders/{id}/cancel, and writes a manual_cancel row into the audit log. The backend is idempotent, an already-gone order returns success silently. The safety contract has three required properties, scoped, expiring, logged, and your strategy loop should treat manual_cancel events as a hands-off signal for that market until you say otherwise.
  5. When should the panel switch from polling to websockets?
    Polling at 2 seconds is fine for a book of ten positions and a few fills per minute. Switch to a push channel the day your strategy fires fills faster than you can refresh: the bot publishes fill and order events over a websocket and the page pushes them into the same render functions. Keep the four GET endpoints for the initial snapshot, and keep a 30 s reconnect on the client.

Section 11

Module checklist.

Eight checks before you call this panel done. Completing all eight unlocks the Continue to Module 03 button below.

Module 02 complete

Yours to watch.

The shift from “I’m debugging a black box” to “I’m watching my book on my phone” happens here. When the bot starts trading, you’ll see fills land in this panel and you’ll have a kill switch within reach, not buried three menus deep on a desktop.

Concretely, one panel alive on hand-written data. The bot you build over the next sixteen modules pours real positions and fills into the same files this panel already renders.

01

A panel built on seed data is a panel you trust before the bot is even alive. Every later module fills it in instead of bolting on.

02

The single source of truth is the file system, positions.json and fills.ndjson. The panel is a read-mostly view that also writes its own actions back.

03

Manual cancel and kill switch are precision instruments with three required properties, scoped, expiring, logged. Anything missing one of those is just a system-prompt edit waiting to bite you.

Next up: Module 03 is the API 101 crash course, auth, keys, wallet signing, your first GET request. Modules 04-06 build the API basics. Module 05 (Orders) places your first real order, which appears in the panel you built today. That’s the moment.

Complete the checklist above to unlock