Skip to content
BulbaStats

About BulbaStats

BulbaStats is an independent, read-only analytics view of BulbaStore, the Minecraft item exchange. It reads the same public API anyone can call, cross-joins the endpoints, and derives the statistics the API doesn't compute itself. It has no API key, places no orders, and holds no database.

Methodology & caveats

Realized P&L

Computed by weighted-average cost basis over observable market trades. A buy raises the average, a sell realizes the difference against it, and the 4% taker fee is charged into cost or deducted from proceeds. Items obtained in-world — mined, crafted, gifted — never appear as a purchase, so selling them realizes their whole price as profit. Every player page counts those units separately and says so. It measures trading, not wealth creation.

Inventory value

Holdings are valued at the current mid of their variant. Items with no quoted mid are excluded rather than valued at zero, and the count of excluded items is shown, because “we don't know” and “worth nothing” are different claims. On a thin book a mid can move a long way on one order, so treat these as indications.

The house market maker

The BulbaStore account holds roughly 92% of resting orders and is a counterparty to most trades. Leaving it in a ranking drowns out every human trader, so leaderboards exclude it by default and order-flow statistics report it separately. Its cancel rate reflects requoting to track price, not failed trades.

What counts as the house

The exchange operates through named bank accounts — market_maker, bot_supply, bulba_revenue, bulba_reserve and bulba_stock_pool — and more than one account has access to them. Every resting order names the bank it was posted from, so order statistics attribute house liquidity by bank, exactly. Trades carry no bank, so trade statistics fall back to the operating account. The same person can therefore be house in one table and human in another; that is what the data supports, not a judgement. Membership is shown on Insights.

Break-even move

Buying means paying the ask and the 4% fee; selling later means receiving the bid less the fee. A round trip therefore starts under water by the spread plus two fees, and the break-even column is how far mid must rise to make that back. It assumes both legs cross the spread as a taker — resting a limit order instead avoids paying the spread, and patient traders should read it as a ceiling.

Both sides are counted

One trade produces a taker leg and one maker leg per resting order matched. Per-player volume counts a player's own legs, so summing volume across players exceeds market volume — that's the intended reading of “how much did this account trade”, not a double-count of the market.

Time windows

Windowed figures like “last 7 days” are anchored to the most recent trade in the dataset, not the wall clock. Aggregates are computed over a cached crawl, so a wall-clock window would make the same data yield different numbers as the cache ages. Order age and book staleness do use real time, since those are genuinely live quantities.

Volatility & price change

Volatility is the standard deviation of log returns between consecutive candle closes, deliberately not annualized. Price change returns nothing at all unless a candle old enough to compare against exists, rather than silently comparing to the oldest bucket available and labelling it “24h”.

Where the taker fee goes

The 4% taker fee is debited from the taker and credited to the treasury's revenue bank, not destroyed. It accumulates in the house pools and is paid back out to stock holders and the reserve on the distribution schedule, so market-wide currency totals are unchanged by it — the diamonds change hands rather than leaving.

Slippage curves

Simulated locally against the book as it stands, ignoring the taker fee. A real order also moves the price it is being measured against, so the curve is a floor on cost, not a quote.

Net flow between traders

Direction of currency, not profit. A pair's net flow is the diamonds one account received from the other minus what it paid back; the receiver handed over goods worth it. Taker fees are excluded because they go to the treasury rather than the counterparty, so the figure is strictly what moved between the two accounts.

Stack and shulker prices

Minecraft stack size is a property of the item — 64 for most blocks, 16 for eggs and pearls, 1 for tools — so a per-stack price is a per-row multiplier, not a constant. A shulker box is 27 stacks. Only unit prices scale with the toggle: spread stays a ratio and volume stays a total.

Resting value near mid

An order parked far from mid will not trade, so total resting value flatters a book. The median resting order sits a long way out, and only a small fraction of all resting value falls within 5% of mid — the distance bands on the orders page separate depth you could realistically hit from a market maker's ladder.

Trends, and where they’re missing

Stat tiles built from the trade record carry a sparkline and a change against the prior period. The book-structure tiles — two-sided books, median spread — draw on the hourly capture instead: upstream’s own book-history endpoints only reach back 90 days, so the capture is what carries a trend older than that, plus the per-player and treasury history those endpoints do not cover. They stay blank until enough snapshots exist to make a line meaningful, and a capture that could not reach the depth endpoint records null rather than zero, so those points are dropped instead of drawing a collapse that never happened. Changes in a share are given in percentage points, because a share moving from 40% to 43% has risen three points, not 3%.

Niche variants

Items flagged niche upstream — odd enchant combinations that rarely trade — are hidden by default, with a toggle to reveal them.

makerMid's provenance

The upstream makerMid field is a reference price of unknown origin. Docs imply a mid over maker orders, but it matches none of house best mid, quantity-weighted mid, house microprice, or trade VWAP for most listings that carry it, and several unrelated items share the exact same value — pointing at a configured number rather than a live computation. Whether it moves over time isn't established. Shown beside mid, never used as a quote.

Self-crosses

A handful of trades have the same account as both taker and one of the makers — filling against its own resting order. They stay in every total: they are real fills that moved real inventory, and dropping them would silently disagree with the upstream's own volume figures. Nothing on the site implies intent.

The account roster is a floor

There is no players index upstream. Accounts are discovered from the trade record, from anyone who has moved funds, and transitively through shared-bank membership — an account that has done none of those three is unreachable and uncounted.

How fresh the numbers are

Every figure is read server-side and cached, so a page view usually costs the upstream API nothing. The window depends on what it costs to fetch: 5s for the order book, 20s for listings and candles, 90s for trade history and profiles. The 9,400-row order crawl is not on a timer at all — it is keyed to a one-request digest of the book, so it is re-read when the book actually moves rather than every 300s regardless. A number can therefore sit behind the market by up to its tier while the page is being actively read — a page nobody has opened in a while only regenerates on the next visit, so the first view after a quiet spell can be older than that. This is the wrong trade exactly once — when you have just traded and want to see it. The Refresh control in the header discards every cached read and refetches the page you are on.

Data sources

https://webstore.bulbastore.uk/upstream/api/v1

Endpoints read

All public, all unauthenticated

EndpointUsed forCache
GET /listingsItem catalog20s
GET /orderbookQuotes for every listing in one call5s
GET /orderbook/:idDepth, ladder, participants5s
GET /orderbook/:id/viewListing + book + fills in one trip5s
GET /orderbook/:id/candlesPrice history20s
GET /transactions?view=tradesComplete trade history90s
GET /transactions?view=fillsBank movements90s
GET /ordersResting and closed orderskeyed to book digest (300s unpinned, 3600s pinned)
GET /players/:usernameProfile, banks, balances90s
GET /treasury*Pools, revenue, distributions90s
GET /commandsBot command reference900s
GET /docs/:slugThis reference900s
WS /api/wsLive trade tapelive

Documented but not deployed

Verified 404 against the live host — no feature depends on them

  • GET /health
  • GET /ledger, /ledger/balance/…, /ledger/audit/…
  • GET /banks/:id

The client treats these as optional: a missing endpoint removes a section rather than failing a page.

Live but undocumented

Found in the official web client, verified unauthenticated

  • GET /treasury
  • GET /treasury/revenue
  • GET /treasury/distributions
  • GET /lending/orders, /lending/loans

These power the Treasury section. Being undocumented, they may change or disappear without notice — that section degrades to a notice if they do.

Efficiency

GET /transactions?view=trades returns one row per taker action with every resting order it matched in a makers[] array. Checked against the fills view, its 3,674 maker legs matched 3,674 isMaker: true rows with zero gaps — so the complete trade record costs a two-page crawl rather than a twenty-page one, and every per-player and per-item statistic on this site is built from it.

Sparklines come from actual fill prices rather than per-listing candle requests, saving roughly 118 requests per market page view. Depth ownership uses one resting-order crawl instead of 118 individual book fetches. The read tier allows 300 requests a minute per IP and cached reads don't count against it; crawls are capped, fan-out is bounded, and everything is cached server-side, so a page view usually costs the upstream API nothing at all. That budget is shared with everything else using the proxy, which is why the tiers stay conservative rather than spending the headroom.

Upstream reference

Loading…