The vike-io/demo-trading-apps monorepo ships five token-price trackers. They split cleanly into two families: three exchange trackers (Binance, Bybit, OKX) that show that venue's live spot + perpetuals book, and two market-data trackers (CoinGecko, CoinMarketCap) that show the global top-100 by market cap. Each runs as a separate slug under one Cloudflare Worker; the code per tracker is one HTML template and a manifest. Everything else is shared.
This is a tour — what each one does, what it's pulling, and the bit of code that makes it interesting.
Exchange trackers — Binance, Bybit, OKX
Same shape across all three: a markets list (top N USDT pairs by 24h quote volume), a spot/perp toggle, and a drill-in trade.html page that pulls candles + order book + recent trades for the selected pair. The only thing that changes between them is the upstream API surface — Binance's api.binance.com, Bybit's api.bybit.com/v5, OKX's www.okx.com/api/v5.
- Binance Tracker — demo · source. Spot + USDT-Perp, with a live funding column in perp mode.
- Bybit Tracker — demo · source. Same idea on Bybit's V5 unified API. Interval enum is numeric (
60,240,D) rather than the Binance-style1h/4h/1d. - OKX Tracker — demo · source. OKX uses dashed symbols (
BTC-USDT,BTC-USDT-SWAP) and a slightly different response envelope ({code, data}).
![]()
The interesting move in the exchange trackers is that they ship a loadMarkets() that uses one endpoint to get every symbol on the host, then sorts client-side. For Binance that's /ticker/24hr returning ~1.5k spot symbols and ~200 perp symbols in a single response:
async function loadMarkets() {
const res = await fetch(`${API_BASE}/${state.mode}/ticker/24hr`);
if (!res.ok) throw new Error(`Binance ${res.status}`);
const all = await res.json();
const usdt = all
.filter(r => r.symbol.endsWith("USDT") && Number(r.quoteVolume) > 0)
.sort((a, b) => Number(b.quoteVolume) - Number(a.quoteVolume))
.slice(0, CONFIG.topNPairs);
state.rows = usdt;
// Perp mode: fetch all funding rates in one shot, then render.
if (state.mode === "perp") {
try {
const fundRes = await fetch(`${API_BASE}/perp/premiumIndex`);
if (fundRes.ok) {
const fund = await fundRes.json();
state.funding = {};
for (const f of fund) state.funding[f.symbol] = f.lastFundingRate;
}
} catch (e) { /* funding is best-effort */ }
}
renderRows();
}
Two HTTP calls in perp mode, one in spot mode — for the entire top-50. No pagination, no per-pair fan-out. The funding fetch is wrapped in a try/catch because if it 429s the markets table should still render. That "render even when one feed is down" pattern shows up in every tracker.
Market-data trackers — CoinGecko, CoinMarketCap
Both wrap CoinGecko's /coins/markets?vs_currency=…&sparkline=true endpoint, which returns the top N coins with a 7-day price array per coin baked into the response. Same upstream, different chrome.
- CoinGecko Tracker — demo · source. The reference layout: top-100, currency switcher (USD/EUR/BTC), search, per-coin detail page on click.
- CoinMarketCap Tracker — demo · source. Same data, CMC-style colors and layout — illustrates that swapping the visual brand on top of one upstream is a templating concern, not a code one.
![]()
The sparklines are SVG generated inline — no charting library:
function sparklineSVG(prices) {
if (!prices || prices.length < 2) return "";
const w = 80, h = 24, pad = 1;
const min = Math.min(...prices);
const max = Math.max(...prices);
const range = max - min || 1;
const stepX = (w - pad * 2) / (prices.length - 1);
const pts = prices.map((p, i) => {
const x = pad + i * stepX;
const y = h - pad - ((p - min) / range) * (h - pad * 2);
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(" ");
const color = prices[prices.length - 1] >= prices[0] ? "var(--green)" : "var(--red)";
return `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">`
+ `<polyline fill="none" stroke="${color}" stroke-width="1.4" points="${pts}"/></svg>`;
}
CoinGecko returns 168 points per coin (7 days × 24 hours). 100 coins × 168 points × one polyline each = 100 tiny SVGs, all drawn in one render pass. No <canvas>, no D3, no observer — the markup is just <svg><polyline></svg> per row.
What the trackers actually share
Each tracker is a separate slug, but they all live in the same Worker and share the same plumbing:
| Concern | Where it lives |
|---|---|
| Request proxying (path → upstream + key headers) | The umbrella Worker, driven by each tracker's manifest.json |
manifest.json schema (slug, name, tagline, upstream, pages, config) |
Same across all 5 trackers |
Template substitution ({{brand_name}}, {{api_base}}, etc.) |
build.py walks each tracker folder, renders templates/*.html into the case's dist/, written to be served by the Worker |
Theme toggle (cg-theme localStorage key, ☀️/🌙 button) |
Same script copied across templates |
| Error banner + retry button | Same script copied across templates |
That's why a brand-new tracker is a single PR: drop a folder under the monorepo root with a manifest.json and a templates/index.html, and the Worker picks it up. The build step doesn't care about its name.
Family at a glance
| Tracker | Family | Upstream | Notable column |
|---|---|---|---|
| Binance | Exchange | api.binance.com, fapi.binance.com |
Funding (perp mode) |
| Bybit | Exchange | api.bybit.com/v5 |
Funding (perp mode) |
| OKX | Exchange | www.okx.com/api/v5 |
Funding (swap mode) |
| CoinGecko | Market data | api.coingecko.com/api/v3 |
7d sparkline |
| CoinMarketCap | Market data | api.coingecko.com/api/v3 |
7d sparkline |
Gotchas
- Symbol conventions differ. Binance uses
BTCUSDT, OKX usesBTC-USDT(spot) andBTC-USDT-SWAP(perp), Bybit usesBTCUSDTfor both but distinguishes viacategory=linear. None of these line up. The perp funding spread case shows what cross-venue normalization looks like in practice. - CoinGecko free tier is rate-limited. ~10–30 req/min on the demo key. The trackers send one request per render, so a tab left open at 30-second polling will brush against that limit. The Worker forwards the
x-cg-demo-api-keyheader fromCOINGECKO_API_KEYif you set it. - Funding intervals vary. Binance + Bybit are on 8-hour cycles, OKX is too for most symbols but a few are hourly. The exchange trackers display the raw
lastFundingRatepercentage without annualizing — annualization is a downstream concern handled by the perp funding spread case. - CoinGecko vs CMC ranking. The CoinMarketCap demo uses CoinGecko's data, so its order won't match CMC's actual website (the two services compute market cap with slightly different exclusion rules). It's a styling demo, not a data-source comparison.
- Sparkline window.
&sparkline=truereturns the last 7 days at hourly resolution. There's no parameter to change the window — for longer charts you'd swap to/coins/{id}/market_chart.
Five trackers, one Worker, one manifest schema. The repo is vike-io/demo-trading-apps — the umbrella build.py and worker.js are at the root, each tracker is a sibling folder.