Case study · live & operating

Generators SMP

A DonutSMP-style economy Minecraft server — I designed the gameplay, wrote the custom server software, built the monetization model, and run the live infrastructure. It’s a small business and a systems-engineering project wearing the same hat.

Paper → Folia Java 21 SQLite / WAL systemd · RCON economy design

The market gap

Most Minecraft servers monetize badly. Vanilla SMPs and minigame servers sell cosmetics and soft perks — nice-to-haves bolted onto gameplay that doesn’t really want you to spend. A small genre solved this: the economy SMP, with DonutSMP as the benchmark. There, money isn’t a side-shop — it’s woven into the core loop, and the result is a category that monetizes a Minecraft server like a free-to-play game.

GenSMP is built to occupy that proven model at early-DonutSMP scale (low hundreds of concurrent players). The thesis is simple: the same loop that makes the game compelling — grind passive income, climb tiers, defend your wealth, show it off — is also the loop that sells. The job is to design that loop honestly and let the monetization fall out of it.

The flywheel

Subscribe
Claim crate keys
Open lootbox-style drops
Rare wins broadcast server-wide
Status & FOMO
↺ pulls the next player in

The engine is the crate — a randomized, lootbox-style draw. A subscription’s recurring value is the keys it grants; the rank itself only adds cosmetics and minor convenience. A free loop feeds the same prestige engine — vote or refer a friend → earn keys → unbox → rare pulls broadcast to everyone — so free players power the population and the social proof. Critically, the store sells draws and vanity, never raw power: the game stays fair, which is what keeps the free majority playing. The cost side stays lean — a cheap Hetzner box bridges today’s scale, escalating to a dedicated OVH machine only once player numbers justify it.

Monetization, by design

Three subscription tiers, sold through a Tebex store that auto-grants the permission group on purchase. The deliberate choice: a subscription buys you a crate to open and some cosmetic flair — never an edge in the game itself. Extra home slots and a few seconds off a teleport warmup don’t move the needle on fairness; the lootbox draw and the chat tag are what people actually pay for.

TierPriceCrateHomesWarmupTag
Free Voting* 1 10s
Gen+ $5 Gen+ 3 7s [+]
Gen++ $10 Gen++ 5 5s [++]
Gen Ultra $15 Ultra 7 3s [U]

* Free players still earn Voting-crate keys by voting. Each paid tier claims its crate on a recurring cooldown — that repeat draw is the subscription’s real value.

Crates — the product

The actual thing you’re buying: a randomized, lootbox-style draw. Four tiers (Voting → Gen+ → Gen++ → Ultra); subscribers claim theirs on a recurring cooldown, free players earn Voting keys by voting. Rare pulls broadcast to everyone with a sound — the draw is private, the win is public, and that’s what drives the next subscription.

Referrals

Invite milestones (5 / 10 / 25 / 50) pay out escalating crate keys, and the top referrer wears a green [Friendly] tag. Organic growth, no ad spend.

Prestige tags

The #1 player on each metric gets a colored chat tag — [CEO] (richest gens), [Mogged] (balance), [Slayer] (kills), [DeGen] (playtime). Status you can only earn by playing — or paying to play faster.

Sinks & market

A player auction house and tiered shops drain currency back out, so money keeps its value. /sell is the floor price; selling through a high-tier generator pays multiples more — another reason to climb.

Engagement, borrowed from mobile

The monetization only works if people keep coming back — so the core loop is paced with mechanics lifted straight from free-to-play mobile and idle games. A generator doesn’t earn forever: it fills a capped store over a few hours and then idles. Sitting full means leaving money on the table, so the optimal play is to return on a schedule and empty it.

CAP
placed & empty filling ~6–8h full → idle until you return

That single rule — a ceiling plus a refill timer — is the same trick behind energy systems and crop timers in mobile games. It sets a natural session rhythm (log in roughly twice a day to keep everything productive), and it doubles as a balance lever: nobody banks runaway wealth by going AFK for a week.

Storage caps

↔ energy / resource caps. A full generator stops earning, so the cap itself is the nudge to come back — wasted production is the cost of not returning.

Fill timers (~6–8h)

↔ harvest timers (Hay Day, FarmVille). The refill window paces the day into a couple of natural check-in sessions.

Daily votes & crate cooldowns

↔ daily-login rewards & streaks. Voting refreshes every 24h for keys; subscriber crates recharge on a cooldown — both pay you to return on schedule.

None of this is incidental. Caps and timers turn a passive-income game into a scheduled habit, and daily-active players are the substrate everything else runs on: more sessions → more votes and social proof → more crate openings → more reasons to subscribe. The retention loop is what makes the monetization loop work.

Server architecture

Today it runs as a single tuned box — enough for launch. The scaling target is a proxy-fronted, multi-server topology built around Folia, the multithreaded Minecraft server. The whole architecture is shaped by one hard constraint, explained below.

Today — one box
Hetzner CCX13 2 dedicated vCPU · 8 GB · NVMe · Ubuntu 26.04
Paper 1.21.1JDK 215G heapsystemd

Runs as a systemd service (auto-restart on crash/boot), console driven over local-only RCON, firewalled to the game + vote ports. Realistic ceiling: ~20–45 concurrent. A deliberate cheap bridge.

At scale — proxy + Folia + helper
Players
Velocity proxy queue · failover · single entry point
Folia main the SMP world · OVH 9950X / 128 GB · regionized threads
Hub / lobby helper spawn, queue, routing
SQLite (WAL) · MySQL-ready

Why Folia — the one constraint

Standard Paper ticks each world on a single thread. Add 15 more CPU cores and the world still advances on one of them — so a busy SMP hits a wall around 100–200 players no matter the hardware. Folia breaks the world into regions that each tick on their own thread, so a 16-core box finally does 16 cores of work. That’s why the target hardware (OVH 9950X) and the software (Folia) are a single decision: the cores only pay off if the tick parallelizes.

P0

Make the plugin Folia-ready

Phase 0

Migrate ~30 scheduler call-sites from the legacy Bukkit scheduler to Folia’s region / entity / async schedulers. These APIs exist on Paper too, so the code runs today and is Folia-ready. The hard part: the one global generator-tick loop has to become per-region.

P1

Bigger box + a proxy

Phase 1

Move to the OVH 9950X, put a Velocity proxy out front for queueing and failover (the proxy secret is already stubbed in config). Still Paper, but more headroom — ~150 concurrent.

P2

Flip to Folia

Phase 2

Swap Paper for Folia (now a jar swap, not a rewrite) and replace the few Folia-incompatible plugins. Regionized ticking finally uses all 16 cores → several-hundred concurrent.

The leverage is in Phase 0: doing the scheduler migration now, while the plugin still runs on Paper, turns the eventual Folia switch into a jar swap instead of a rewrite. Front-loading the hard part is the whole strategy.

The backend

Every generator, sale, raid and listing is persisted. The data layer is small but was hardened by a real outage — and it’s where the most interesting performance work lives.

The incident that shaped it

Early on, the database ran on a single connection with no write-ahead log. Under about three players every subsystem funnelled through that one connection and the server started throwing Connection is not available, request timed out after 30000ms. The fix is the current design: SQLite in WAL mode (concurrent reads alongside a writer), a HikariCP pool of 4, a 5-second busy timeout, and batched async writes so the main game thread never blocks on disk. A MySQL backend is a config toggle for when it’s needed.

What’s stored

generatorsEvery placed generator — type, owner, location, tier, stack, stored, lifetime earnings, raid timers.
player_statsPer-player lifetime earnings per generator type — feeds the leaderboards.
auction_listingsPlayer market: serialized item blob, price, 48h expiry, atomic status.
auction_collectionReturn bin for expired / cancelled / overflow auction items.
raid_logImmutable audit of every successful raid (raider, target, loot).
referralsOne-referrer-per-player mapping (enforced by primary key).
key_claimsCrate-key cooldown tracking (3-day claim window per donor tier).
pending_*_rewardsOffline queues — votes, keys, referral rewards delivered on next join.
The core table
CREATE TABLE generators (
  id              TEXT PRIMARY KEY,   -- generator instance UUID
  type            TEXT NOT NULL,      -- dirt … netherite
  owner           TEXT NOT NULL,
  world, x, y, z,                     -- block location
  tier            INTEGER,            -- 1–10
  stack           INTEGER,            -- 1–16
  stored          INTEGER,            -- items waiting to be claimed
  lifetime        DOUBLE,             -- $ earned since placement
  protected_until BIGINT,            -- raid grace window
  post_raid_cd    BIGINT
);
-- indexes by (world,x,y,z), owner, type → O(1) lookups
-- writes are batched every 30s via flushDirty() → one commit, not N

The performance win — ticking only what matters

A passive-income server can accumulate tens of thousands of placed generators. The naïve loop ticks all of them every second — and it did, until it didn’t scale. Now generators are indexed by chunk, and each tick only touches the chunks that are actually loaded:

Before
O(all placed)

Walk every generator ever placed, every second — and clone a location object for each. A cliff at ~5k gens.

After
O(loaded chunks)

A world → chunk → generators index. Tick only loaded chunks. 10k+ generators run with no main-thread stall.

Same idea throughout: O(1) lookups by id and location, an auction purchase that claims a row atomically (UPDATE … WHERE status = 0) so a crash can’t double-spend, and a 30-second batch flush that collapses thousands of tiny writes into one transaction. Capacity knobs (max-players, simulation-distance) are tuned against spark profiles, not guesses.

The game underneath

All of the above exists to serve one loop:

Generators10 types, dirt → netherite. Tiers 1–10, stackable ×16, capped storage. Passive income that scales with investment.
RaidsBreak into someone else’s generator and skim half its stored value as cash. Wealth has to be defended.
Auction houseA real player-to-player market — list, buy, expire-to-bin — with atomic, offline-safe settlement.
Crates & shopsAnimated crate openings, NPC-run tiered shops, and currency sinks that keep the economy honest.
LeaderboardsLive hub holograms and chat tags rank the top earners, fighters and socialites. Status is the endgame.
Quality of lifeRandom teleport, homes, proximity voice chat, a sidebar HUD — the table stakes that keep players around.

Why I built it

I wanted to ship a real product — one with users, money, and consequences when it breaks — and own every layer of it. GenSMP is that: I designed the economy, wrote the ~dozen interlocking systems behind it, set the prices, and I’m the one who gets paged when the database locks up at peak.

The part I find most useful to have done is the monetization design — and the constraint I set on it. The store sells crate draws and cosmetics, never power, because the moment paying wins you the game you lose the free majority who make it worth playing. So the work was making a fairness-neutral model still convert: why the lootbox pull has to feel good, why the rare win has to be public, where currency sinks go so the economy doesn’t inflate. That’s incentive design as a system — the same reasoning a business problem needs, with a feedback loop fast enough to watch in real time.

And the engineering kept me honest. The database outage at three players, the generator loop that fell off a cliff at scale, the decision to do the hard Folia migration now rather than during a fire later — each one was a lesson in finding the real constraint and building around it instead of around the symptom.

It’s the clearest example I have of working end to end: strategy, system design, implementation, and operations — held together by one person who has to make them all agree.