A Hypixel-style Bazaar for Paper: a commodity market with algorithmic dynamic pricing, built for the eco suite.
Where RoyalAuctions handles one-off items, the Bazaar handles the things players trade constantly — cobblestone, wheat, mob drops, enchanted materials. Different problem, different UI.
Part of a suite with RoyalBank, RoyalAuctions and EconGuard.
- Why not an order book
- The pricing model
- Requirements
- Install
- Commands & permissions
- Items and pricing config
- EcoShop anchoring
- Menus
- Storage
- Integrations
- Building
- Gotchas
Real Hypixel's bazaar is an order book — players place buy and sell orders and trade against each other. That needs liquidity. Hypixel has tens of thousands of concurrent players; your server almost certainly doesn't. On a normal server an order book is a ghost town: nobody to trade against, so nothing fills, and the feature is dead on arrival.
RoyalBazaar instead makes the server the counterparty, always, with infinite depth. There is always a price to buy at and a price to sell at, whether 200 players are online or 2. The prices still move with supply and demand — that's what the engine below is for.
Each item has a mid price. Buying pushes it up, selling pushes it down, along an exponential curve:
mid' = mid × e^(±q / E)
The crucial part: cost and proceeds are the integral along that curve, not quantity × price:
buy cost = E × mid × (e^( q/E) − 1) × (1 + spread/2)
sell proceeds = E × mid × (1 − e^(−q/E)) × (1 − spread/2)
Which gives three properties that matter:
- Small trades feel normal. When
q ≪ E,e^(q/E) ≈ 1 + q/E, so cost ≈q × price. Linear, unsurprising. - Whales price themselves out. Dumping 100k of an item craters the price on yourself — each marginal unit sells for less than the last. You cannot drain the market at a flat price.
- No arbitrage loop exists. A round trip loses to the spread and to your own market impact: you buy (pushing the price up against yourself), then sell (pushing it back down). This isn't a rule patched on top to stop exploiters — the maths makes it impossible.
elasticity (E) is the knob that matters most per item: high = stable staple (cobblestone),
low = volatile rare (netherite).
Mean reversion pulls each price back toward its base_price every tick, in log space, so a panic
sell recovers over time instead of staying crashed:
mid = clamp( mid × e^((ln(base) − ln(mid)) × reversion_rate), floor, ceiling )
Floor and ceiling caps are a hard backstop — clamped after every trade and every tick, so no formula edge case can send a price to 0 or ∞.
base = 100, E = 50,000, spread = 5%:
| Trade | Cost | New mid |
|---|---|---|
| Buy 64 | ~$6,565 | 100.13 — barely moves ✅ normal play |
| Buy 100,000 | ~$32.7M | 739 — the curve punishes the whale ✅ |
| Server | Paper 1.21+ (tested on Paper 26.2 / Java 25) |
| Required | Vault and an economy provider (EssentialsX, CMI, an EcoBits currency with vault: true, …) |
| Optional | eco (for ecoitems: items), EcoShop (for price anchoring), EconGuard, PlaceholderAPI |
Runs without the eco suite — you just lose custom items and EcoShop anchoring.
- Drop
RoyalBazaar.jarintoplugins/. - Start the server. Configs generate on first run.
/bz.
Base command /bazaar, aliases /bz, /rbazaar.
| Command | |
|---|---|
/bz |
Open the bazaar |
/bz price <item> |
Show an item's live buy / sell / mid |
/bz reload |
Admin. Reload config, categories, menus |
| Node | Default |
|---|---|
royalbazaar.use |
true |
royalbazaar.admin |
op |
The bundled categories/ are vanilla-only — 338 items across farming, mining, combat, woods &
fishes and oddities — so a fresh install works on any server without EcoItems. oddities.yml carries
a commented EcoItems example showing base_price: auto and the NPC anchors; uncomment it only if
that plugin is installed.
One file per category in categories/. The file name is the category id.
# categories/mining.yml
name: "&bMining"
icon: "minecraft:iron_pickaxe"
slot: 13 # where this category sits in the icon grid
defaults: # every item inherits these unless it overrides them
spread: 0.05 # 5% gap between buy and sell — your margin + inflation sink
elasticity: 40000 # units to move the price by a factor of e (higher = more stable)
reversion_rate: 0.02 # fraction pulled back toward base each tick
floor_pct: 0.4 # price never below 40% of base
ceiling_pct: 3.0 # price never above 300% of base
items:
cobblestone:
item: "minecraft:cobblestone"
base_price: 2.0
elasticity: 100000 # staple — very stable
diamond:
item: "minecraft:diamond"
base_price: 100.0
elasticity: 20000 # rarer — more volatile
ceiling_pct: 5.0
enchanted_cobblestone:
item: "ecoitems:enchanted_cobblestone" # note: ecoitems: is PLURAL
base_price: 320.0Adding an item is one block. Removing it is deleting the block. The defaults: block is what stops
300 items from repeating the same five tuning lines.
Buy price is mid × (1 + spread/2), sell price is mid × (1 − spread/2). Every round trip through
the bazaar destroys a little money. Without a spread the bazaar leaks value and gets arbitraged.
Items and groups flow into the grid in config order. Give one a row/column (both 1-indexed) — or
a raw slot: — to pin it in place; everything unpinned fills in around it:
wheat:
item: "minecraft:wheat"
base_price: 3.0
row: 2
column: 3Pinned entries hold their square on the first page. Where the grid sits at all is the mask pattern in the menu file, so the whole layout is config.
If you run EcoShop, you have a problem: two places that price the same item. If both float independently, players just arbitrage between them — which is strictly worse than having one market.
The rule: one dynamic market per item. Keep EcoShop's dynamic pricing off so it stays a fixed NPC bracket, and let the Bazaar be the single floating market. Then anchor to it:
iron_block:
item: "minecraft:iron_block"
base_price: auto # anchor = EcoShop's buy value (the item's canonical price)
npc_floor: true # floor = EcoShop's sell value (the NPC never undercuts the bazaar)
npc_ceiling: true # ceiling = EcoShop's buy valueRoyalBazaar reads plugins/EcoShop/categories/*.yml directly — read-only, no API coupling. EcoShop
brackets each item with a buy.value (what a player pays the NPC — the natural ceiling) and a
sell.value (what the NPC pays the player — the natural floor). The bazaar floats between them.
Each option falls back to its *_pct default if EcoShop is absent or doesn't list the item, so
mixing anchored and explicit items in one category is fine.
Tuning note: anchoring
base_priceto EcoShop's buy value puts the item at the top of its range, and withnpc_ceiling: truethe mid can then only fall, never rise. If you want headroom, either dropnpc_ceilingor anchor the base nearer the midpoint of EcoShop's buy/sell.
/bazaar opens a category directly — there is no separate landing menu. A category rail down the
left of the category and group menus moves between categories, drawn from each category's own icon
and name, so adding a category adds a rail entry with no menu editing:
category-rail:
enabled: true
column: 1
rows: [1, 2, 3, 4, 5]
glint-selected: trueWhich category /bazaar opens is default-category in config.yml; leave it empty and the first
configured category is used.
The menus are:
| File | Purpose |
|---|---|
bazaar_category.yml |
A category's grid — group icons, or products for a flat category |
bazaar_group.yml |
One group's products |
bazaar_product.yml |
A single item: live stats, plus Buy Instantly / Sell Instantly |
bazaar_buy.yml |
Quantity picker — 1, a stack, fill inventory, or a custom amount typed on a sign |
All in the EcoMenus dialect — mask/pattern, slots: with location: {row, column},
inline item specs, left-click: effect lists, %percent% placeholders.
One extension: a content: region. The 0 slots in the mask are auto-populated with the
category's items and paged, so adding an item never means touching a menu file:
mask:
items:
- black_stained_glass_pane
pattern:
- "111111111"
- "100000001" # 0 = live product slots, auto-filled and paged
- "111111111"
content:
template:
item: "%rbazaar_item%"
name: "%rbazaar_item_display%"
lore:
- "&7Buy: &a$%rbazaar_buy_price%"
- "&7Sell: &e$%rbazaar_sell_price%"
- "&7Trend: %rbazaar_trend% &7(%rbazaar_change_24h% 24h)"
left-click:
- id: rbazaar_open_product
args:
item: "%rbazaar_item%"Effect ids: rbazaar_open_product rbazaar_open_buy rbazaar_buy rbazaar_sell
rbazaar_sell_all rbazaar_search rbazaar_buy_prompt rbazaar_sell_prompt
rbazaar_buy_amount_prompt rbazaar_back rbazaar_next_page rbazaar_prev_page open_menu
close / close_inventory play_sound send_message
rbazaar_buy / rbazaar_sell take an amount: arg — a number, all, or fill (buy as many as
fit in the inventory, capped by what the player can afford).
rbazaar_sell_all scopes itself to how deep the player has navigated. Anywhere above a group it
sells everything the bazaar trades, because a player standing in the bazaar wants one button that
empties their bags; inside a group it narrows to that group's items. A scope: all arg forces the
wide behaviour even inside a group.
rbazaar_back works the parent out from the open menu rather than naming a target, so the same
button behaves correctly however the player arrived.
Placeholders: %rbazaar_item% %rbazaar_item_display% %rbazaar_buy_price%
%rbazaar_sell_price% %rbazaar_buy_cost_1% %rbazaar_buy_cost_64% %rbazaar_sell_value_1%
%rbazaar_sell_value_64% %rbazaar_sell_value_all% %rbazaar_held_amount% %rbazaar_trend%
%rbazaar_change_24h% %rbazaar_volume_24h% %rbazaar_spread_pct%
%rbazaar_category% %rbazaar_category_id% %rbazaar_group% %rbazaar_group_name%
storage:
type: SQLITE # or MYSQL for a network
sqlite-file: bazaar.dbTables: rb_state (current prices), rb_history (snapshots for graphs / 24h change),
rb_transactions (audit log).
Prices are authoritative in memory, flushed write-behind on a timer and on shutdown — a trade never touches disk on the hot path. Trades run on the main thread, so the price mutation, balance move and inventory change are effectively atomic: no partial trades, no race between two buyers.
engine:
tick-interval-seconds: 60 # mean reversion + stat roll
history-interval-seconds: 300 # price snapshot for graphs
flush-interval-seconds: 30 # write-behind flush- eco — items are eco lookup ids (
ecoitems:enchanted_cobblestone). eco resolves and matches them, including custom-item NBT, so selling a custom item back works correctly. Vanilla ids use Bukkit directly. - EcoShop — price anchoring (see above). Read-only file parse.
- EconGuard — a pre-trade veto seam exists: every trade is offered to EconGuard before any money or items move (rate limits, circuit breakers, wash-trade heuristics), plus a post-trade audit feed. Currently permissive — the real API call is a TODO.
- PlaceholderAPI —
%royalbazaar_buy_<itemId>%,%royalbazaar_sell_<itemId>%,%royalbazaar_mid_<itemId>%. Item ids keep their namespace.
mvn -DskipTests package # -> target/RoyalBazaar.jarJava 21, Maven. Versioning is year.week.revision (e.g. 2026.28.0), matching the eco suite.
- Paper 26.2 requires Java 25 to run (the plugin targets Java 21 bytecode).
- EcoItems' namespace is
ecoitems:(plural), notecoitem:. base_price: autoneeds EcoShop. If EcoShop doesn't list the item, the item is skipped with a warning — it does not silently get a made-up price.- The plugin waits for a Vault economy provider instead of disabling when one isn't registered at enable time. Vault being a hard dependency says nothing about the economy plugin, which can register later — disabling would kill the plugin purely because of plugin load order.
gui/bazaar_main.yml (the old category hub) has been removed. /bazaar now opens a category and
the rail navigates between them. If you customised that file it is no longer read and can be deleted;
any button still pointing at menu: bazaar_main lands on the default category rather than erroring,
so nothing needs editing first.
Category icons now come from each category's own slot, icon and name rather than being written
into a menu file, so a category added after an upgrade appears on its own.
- "Sell All" has no confirmation. One click dumps your entire stack at market. Worth gating.
- The EconGuard veto is a permissive stub. The seam is in place; the call isn't.