Skip to content

About

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

BulbaStats

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.


What's in it

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.

Quick start

npm install
npm run dev

Open 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-out

CI 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.

How it works

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/summary digests 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.

Deploying

Deploys to Vercel as a standard Next.js app — no environment variables, no database. Two settings are non-default and worth understanding:

  • vercel.json pins the region to lhr1 (London). Every page proxies to webstore.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 .uk domain — if it is actually hosted elsewhere, change this to the nearest region and the cold-cache pages get proportionally faster.

  • maxDuration = 60 on /market, /orders, /players, /house and /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; /recipes skips 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 hourly TTL.frozen backstop 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_TOKEN count — which is exactly what the capture does. Bot activity is second class elsewhere (a GITHUB_TOKEN push cannot trigger another workflow), so assume it does not count.

    To make it durable, add a repository secret DATA_PUSH_TOKEN holding a personal access token with write access to contents — a fine-grained token with Contents: Read and write on this repository. Avoid the classic repo scope: it grants write access to every repository the account can push to, and actions/checkout persists it into the runner's .git/config by default. Both workflows use it when present and fall back to GITHUB_TOKEN when not. With it set:

    • snapshot commits are attributed to your account rather than the bot;
    • keepalive.yml pushes 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:

    1. 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.

    2. 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 Content once 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.errors lists 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 are null rather than 0.

  • The data branch 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.json on main disables the branch by name, and the job writes a vercel.json onto the data branch 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.

Documentation

  • 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 /about page. 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

License

See LICENSE.md.

About

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages