A deep, comprehensive statistics viewer for BulbaStore — the Minecraft item exchange where players trade items on a real order book priced in diamonds.
BulbaStore publishes a rich API but no aggregate statistics. BulbaStats is the missing analytics layer: it reads every public endpoint, cross-joins them, and derives what the API doesn't compute — per-item liquidity, per-player P&L, market microstructure, treasury flows, and market-wide activity — behind a dark, data-dense trading-desk UI.
Read-only. No API key, no auth, no writes. Every number comes from public endpoints.
| Section | What you get |
|---|---|
| Overview | Market-wide volume, fees, breadth and concentration; live trade ticker |
| Market | All 184 listings — mid, spread, depth, turnover, sparklines; sortable and filterable |
| Item | Candlesticks, order-book depth curve and ladder, VWAP, volatility, slippage curve, participants, fills |
| Recipes | Buy it or build it — crafting, smelting and enchanting costs priced against the real book, with an optimal anvil order; what each enchantment is worth alone |
| Supply | What enters the exchange and what leaves it — deposits, withdrawals, net float per item, and the goods that arrived and never traded |
| Players | Leaderboards by volume, fees, maker share, inventory value, open-order capital; the funnel from registering to trading |
| Player | Realized P&L by cost basis, holdings at mid, open orders, counterparty graph, activity |
| Trades | Full trade explorer — filter by item, player, venue, mechanism, side |
| Orders | Resting-order analytics: depth ownership by distance from mid, a catalog-wide slippage matrix, the book with the house stripped out, fill and cancel rates, time-to-fill |
| House | The market maker's position, the inventory it has absorbed, how fast it re-prices, and who operates it |
| Treasury | Pool balances, daily fee revenue by source, distribution history, stock ownership |
| Compare | Two to four listings side by side; the selection lives in the URL so it can be shared |
| Insights | Activity heatmaps, price clustering, liquidity coverage, an interactive trading-network graph, and who operates the house |
Several views offer a toggle that reframes the data rather than filtering it: prices per single / stack / shulker (stack size is a per-item property), daily volume split by venue or by taker side, book depth narrowed to within 25 / 10 / 5% of mid, supply measured in units or in diamonds, and any account hidden from the network graph — hiding the house market maker shows which traders have actually found each other.
Statistics are computed over the complete dataset, not a sample — the market opened 2026-07-12, so full history still fits in a cached server-side aggregation.
Everything is served from a short server-side cache, so a figure can be a little behind the market. Refresh in the header discards every cached read and refetches the page you are on — the thing to press after you have just traded and want to see it land.
npm install
npm run devOpen http://localhost:3000. No environment variables and no database are required; the upstream API is public.
To point at a different upstream, set BULBA_API_BASE (defaults to
https://webstore.bulbastore.uk/upstream/api/v1).
npm run build # production build
npm run start # serve the production build
npm run lint # eslint
npm run snapshot # capture one market snapshot into ./.snapshot-outCI runs the same script hourly with --out data-branch; npm run snapshot is what you'd run locally to inspect
the captured shape. Add --no-depth to skip the price levels, or
--dry-run to fetch and report without writing. --budget-ms caps the whole
capture's wall clock (default 15 minutes, floor 1 minute); past it the script
stops fetching and writes what it already has rather than being killed with
nothing to show.
Server Components fetch from the upstream API and run the aggregation in
lib/analytics/; Client Components handle sorting, filtering, chart hover, and
the Socket.IO live feed. Results are cached with Next.js revalidate tiers
(5 s for live book data upward), so a page view usually costs the upstream API
nothing. Every cached read also carries a shared tag, which is what Refresh
expires.
The two expensive reads are not paced primarily by the clock, because a timer cannot tell time passed from something changed — and the order book measurably sits still for hours. Instead:
- Transaction history is split at an anchor. Everything below a rounded-off
id is a fixed window of the past and stays cached; everything above it is
fetched forward with
after, which is a page or two. Rebuilding history takes about three days at current rates. - The order crawls are content-addressed. One request to
/orders/summarydigests the whole resting book; that digest keys the crawl's cache. An unchanged book costs one request instead of 47.
Prerendering every route with all caches expired: 424 upstream requests and
34.4 MB before this, 80 requests and 1.74 MB after — 81% fewer requests and
95% less data, with no change to any figure on the site. (Measured before
/recipes moved to the grouped book, which adds one request and ~5.7 MB to the
"after" figure.) The split crawl was
checked against the plain one row by row: same rows, same order, no gaps and no
duplicates. See SPEC.md §1.3.
Charts are hand-rolled SVG — no charting dependency.
One thing is recorded rather than derived. Upstream's own order-book history
endpoints only reach back 90 days and don't cover balances, so spread and
depth beyond that window, and balances over time at any age, cannot be
recovered after the fact. A GitHub Action runs npm run snapshot hourly and commits the
result to a separate data branch — roughly 25 KiB per snapshot covering
every book, the treasury, and every bank balance, plus a compact per-day series
the site reads back in one request per day.
Spread and depth over time, and the sparklines on the two book-structure tiles,
come from that branch and nowhere else. Until the workflow has run they simply
aren't there: the panel says so and the tiles carry no trend, which is the
normal state of a fresh deployment. Once it has, the spread and depth charts
give every hour of the last 30 days a slot, so an hour the capture missed shows
as an empty bar instead of being closed up. Set BULBA_DATA_BASE to read a fork or a
local mirror — an http(s) origin either way, since the reader fetches over the
network and a file:// path is rejected outright (npx serve over a checkout
of the branch is enough). See SPEC.md §1.5.
Deploys to Vercel as a standard Next.js app — no environment variables, no database. Two settings are non-default and worth understanding:
-
vercel.jsonpins the region tolhr1(London). Every page proxies towebstore.bulbastore.uk, and a cold order crawl is ~47 sequential requests, so round-trip time dominates rather than compute. The region is inferred from the upstream's.ukdomain — if it is actually hosted elsewhere, change this to the nearest region and the cold-cache pages get proportionally faster. -
maxDuration = 60on/market,/orders,/players,/houseand/recipes. The first four depend on that crawl, which takes ~10 s and would be killed by the default serverless timeout on a cold cache;/recipesskips the crawl but pulls the whole price-level book in one multi-megabyte request, which is no quicker. 60 s is the Hobby-tier ceiling, so it is safe on any plan. Warm requests return from cache immediately — and stay warm across a revalidation now that the crawl is keyed by the book's content rather than by a timer, so paying that cold cost twice in a row takes a change in the book or the hourlyTTL.frozenbackstop expiring. -
The capture will switch itself off unless you stop it. GitHub disables scheduled workflows in a public repository "when no repository activity has occurred in 60 days". It never defines repository activity, and says nothing about whether commits pushed with the built-in
GITHUB_TOKENcount — which is exactly what the capture does. Bot activity is second class elsewhere (aGITHUB_TOKENpush cannot trigger another workflow), so assume it does not count.To make it durable, add a repository secret
DATA_PUSH_TOKENholding a personal access token with write access to contents — a fine-grained token with Contents: Read and write on this repository. Avoid the classicreposcope: it grants write access to every repository the account can push to, andactions/checkoutpersists it into the runner's.git/configby default. Both workflows use it when present and fall back toGITHUB_TOKENwhen not. With it set:- snapshot commits are attributed to your account rather than the bot;
keepalive.ymlpushes an empty commit to the default branch on the 1st and 15th of each month — two resets inside every 60-day window — carrying[skip ci]so Vercel ignores it.
Give the token the longest expiry available, or a classic token with no expiry. A silently expired token is the one failure mode that looks exactly like everything working until the capture stops.
If it does get disabled anyway, GitHub emails the repository owner and re-enabling is one click in the Actions tab; the capture resumes with a gap rather than losing what it already has.
-
GitHub's schedule alone no longer runs the capture hourly. Since GitHub's Actions incidents on 26 August 2026 this repository's scheduled runs are created hours late or not at all — two to eight captures a day instead of 24 (others report the same). Manual dispatch is unaffected, so the dependable trigger is an external scheduler that starts the workflow every hour:
-
Create a fine-grained personal access token with access to this repository only and the single permission Actions: Read and write. It can start workflows but cannot push code. Keep it separate from
DATA_PUSH_TOKEN: this one is stored outside GitHub. -
At any external cron service (cron-job.org is free), schedule this request for every hour — with your fork's owner and name, if you run one:
POST https://api.github.com/repos/TypicalHog/bulba-stats/actions/workflows/snapshot.yml/dispatches Authorization: Bearer <token> Accept: application/vnd.github+json X-GitHub-Api-Version: 2022-11-28 {"ref":"main"}GitHub answers
204 No Contentonce the run is queued.
The workflow's own schedule stays as a fallback. When both fire in the same hour the runs queue one behind the other, both captures are kept, and the charts use the later one. Give this token a long expiry too: when it lapses the capture quietly drops back to the degraded schedule.
-
-
A red snapshot run may still have committed. If any endpoint fails every retry — or the capture itself crashes part way through — it writes what it got, the job pushes it, and then the run is failed on purpose. A partial hour is worth keeping — it is the only record of that moment there will ever be — but a green check over a capture that lost every book is worse than a red one, because nobody inspects a passing job. The job log lists what failed in full; the snapshot's
meta.errorslists the endpoint failures (a write-phase failure, such as an unreadable series file, can't be in it — that file is serialised before the write), and the series columns affected arenullrather than0. -
The
databranch must never deploy. The snapshot job pushes to it hourly, and each push would otherwise trigger a build. Deployment is disabled from both ends —vercel.jsononmaindisables the branch by name, and the job writes avercel.jsononto thedatabranch that opts it out directly, since Vercel evaluates the config from the commit being pushed. If stray builds appear anyway, set an Ignored Build Step in the project's Git settings.
Vercel Analytics and
Speed Insights are mounted in the root
layout. Both packages always inject a <script> tag client-side, on every route.
It only resolves into a working collector on Vercel's platform: in dev it
requests Vercel's public debug endpoint (va.vercel-scripts.com), and in a
self-hosted build it requests a same-origin /_vercel/... path that 404s
harmlessly. Neither collects personal data or sets cookies.
- SPEC.md — full specification: endpoints consumed, measured data volumes, caching tiers, every derived statistic, site map, design system, and architecture.
- Methodology and caveats — see SPEC.md §4
and the
/aboutpage. Derived numbers like P&L and net worth rest on stated assumptions, and the site says so wherever they appear. - Upstream API — https://webstore.bulbastore.uk/docs/api
See LICENSE.md.