Six systems — client, game, core, anticheat, control plane
I build the wholestack a Minecraftnetwork runs on.
Eighteen, writing code since I was seven. The client players install, the game they play, the
core underneath it, an anticheat with a Rust cloud behind it, and the Go control plane that
deploys all of it — plus a benchmark that measures AI-text detectors for a living.
live state is never stored — status always comes from the agents
Biggest project to date
pvp.surf
A live Minecraft PvP network, and the reason four of the systems below exist. It runs Surf
Client on the player's machine, Arc as the game, Triangle as the core underneath it and
Sniffer watching all of it — each one useful alone, and only worth building because they had
to fit each other.
Surf ClientArcTriangleSniffer
Selected work
Six systems
Control plane · Go
Grove
three binaries · no runtime deps beyond Docker
Go
41.7k
Panel files
104
Agent binary
7MB
A Pterodactyl-style control plane: a panel plus per-node agents that run every game server as
a Docker container. It has since absorbed the whole public web presence — forums, stats,
store, launcher feed — and a commercial hosting business on top.
groveControl plane — SQLite, web UI, REST and WebSocket, and every public vhost answered off one listener by Host header.
branchNode agent — one per VM, driving the local Docker Engine over its socket.
geodnsAuthoritative GeoDNS — replaced the Velocity proxy as the network's shared front door.
saplingA hosting business — ~3.5k lines, its own site and vocabulary, provisioning real servers on the same agent plumbing. Customers never see the panel.
The part I'd point at first
The Docker client is hand-rolled — 419 lines straight over the unix socket, including the
hijacked attach stream that carries console stdin — specifically so branch stays
a ~7MB static binary instead of pulling in the official SDK.
geodns answers each lookup with the closest live backend, so clients connect
directly and the ping in a player's server list is the real server's, not a proxy's. Players
move between backends on 1.20.5+ transfer packets. It fails open, so a dead nexus can't take
the front door down.
Live state is never stored. The panel is the system of record for definitions; status always comes from the agents.
Fail open, never fabricate. An unreachable nexus reports "unknown", never zero — "0 players online" is a claim about the network, and a control-plane blip isn't evidence for it.
Immutability at the deploy boundary. Instances are stamped from templates and rolled onto published versions. You can't hand-edit one into a state you can't reproduce.
Server-side enforcement. Hidden UI is convenience; every route re-checks.
21 of 29 servers up across two nodes. The amber badge on A-PHX01 flags an instance behind its template.
Publishing a version
CI posts bundles to /api/deploy/{template}, and there's an embedded SFTP server
where each template appears as a directory holding its entire effective data tree. Edit the
files, then echo "notes" > PUBLISH — a synthetic file — and your working copy
zips into the next semver through the same pipeline CI uses. A copy seeded before someone
else published refuses to publish as stale.
Permissions, and money
The forums store nothing about who may do what. Categories carry a weight floor, compared
against the player's live rank at the moment they act — never frozen into a session.
The store runs real commerce: Stripe webhooks, gifting, a second currency, and chargeback
handling with configurable punishment — deliberately not applied to refunds you issue
yourself, since refunding isn't evidence of fraud.
Practice core · Java
Arc
the game itself — queues, kits, arenas, ranked, replays
Java
153.6k
Source files
568
Modules
05
A multi-instance practice network: Velocity entry → lobby ⇄ N direct combat servers, every
backend on HazelSpigot. Two combat modes — Legacy (1.8.8 mechanics via engine profiles) and
Modern (vanilla) — with per-mode queues, matchmade centrally by a standalone WebSocket broker.
nexusThe broker — WebSocket hub, matchmaker, provisioner and the seam into Grove. 17.4k lines, standalone.
spigotOne plugin, two roles — role: lobby|game in config. 126.8k lines across UI, minigames, ranked, replay and world handling.
velocityThe entry edge — hands clients to regional games with native transfer packets, which take the proxy out of the gameplay data path entirely.
siegeLoad generator — three modes: observe, isolated (a private nexus booted inside the harness) and fleet, gated behind --i-understand.
Why transfers matter
Once matchmaking is done, 1.20.5+ transfer packets move the player onto the real combat
server, so the proxy is no longer carrying gameplay packets. Combined with geodns handing out
the closest backend, a match runs on a direct connection to the box it's on — which is the
only way the ping a player sees is the ping they actually have.
siege runs from my own machine, never from a fleet node — a generator sharing a
box with the thing it measures competes for exactly the CPU and memory the measurement is
about.
JavaVelocityPaper / SpigotWebSocketMavenMongoDB
arc / duel
A Legacy duel on G-ASH01 at 26ms. The scoreboard names the server the match is actually on.
arc / kit editor
Kit layout for 1.9+ combat — players build their own loadouts, then queue with them.
Anticheat · Java + Rust + React
Sniffer
the servers measure · the cloud decides
Java
52.7k
Rust
15.3k
React / TS
5.3k
An anticheat for modern Minecraft, written from scratch. The whole plugin is one idea:
envelopes. Most prediction anticheats simulate a player as a vector — pick the single
most likely input, run physics forward, compare. Every place reality could have diverged
becomes a fudge constant bolted onto a threshold, and eventually the check is measuring its own
tolerances.
Predicting a set, not a point
A state is an envelope: a per-axis interval of possible positions and velocities. Every
physics operation is lifted to run on envelopes, so uncertainty is carried explicitly instead
of guessed away. Where the reachable set genuinely isn't box-shaped — sprinting or not,
knockback this tick or next — hulling it into one envelope would hand a cheater the union of
worlds they can't occupy at once, so the engine splits into a population of branches
with their own flags instead.
Uncertainty grows only while the client is unobserved and shrinks the moment it reports in. A
violation is then the strongest claim the design can make: no assignment of inputs,
flags or pending knockback puts the player where they say they are. There is no speed
threshold, no fly threshold, no step threshold — they're all the same statement.
engineExpand → simulate → collide → filter → narrow, every tick. Vanilla's post-move tail is affine, so it's stored as scale × realised + offset per axis and inverted exactly.
worldA mirror, not the main thread. Blocks are read from the ChunkData packets the server actually sent clients, so it sees what players were told. Collision shapes come from the running server and cache forever — no hand-maintained shape table to rot.
cloudRust, axum and Postgres with a React panel. One gzipped envelope every 5s carries behaviour windows, detections and punishments; the answer carries model verdicts, other servers' alerts and a directive per online player.
mitigateNerfs, not bans. A ban must be certain, so it's slow. A nerf only has to be probably deserved and quietly reversible, so it's fast.
The rule every effect obeys
Each mitigation must have an innocent explanation the player already believes in. Missing an
edge hit is hit registration. Soft knockback is knockback. A block that doesn't place is a
block that didn't place. Nothing does anything a busy server couldn't do by accident — which
is why none of it can be reported as a bug that reproduces.
Three things keep it unattributable: domains, so what you're caught doing shapes what
stops working; jitter, a per-player-per-session constant so two identical cheaters
measure different numbers; and spacing, because runs are the tell, not the rate — one
missed hit is bad luck, three in a second is a bug report.
Three active models — aim, autoclicker and lagcheat — with validation ROC AUC beside each, and every verdict carrying a calibrated probability rather than a yes or no.
Packet-level observation
The engine runs on netty threads and never touches Bukkit. A rotation-only packet still
constrains the search: the client only omits position when it moved less than its send
threshold, so "no position" is itself an observation — an envelope of radius 0.01415 around
the last position — rather than a tick to skip.
Minecraft client · Java / Fabric
Surf Client
two clients, one launcher, two release feeds
Java
68.3k
Modules
45
MC versions
02
The Fabric client players install — and a second, separate client for 1.8.9. Not a port: 77% of
the modern client's files name a net.minecraft type that doesn't exist on 1.8.9, so
what the two share is a design, not code.
26.2The full client — 45 modules across combat, visuals, HUD, utility and media. A Vulkan-drawn interface that uses no vanilla widgets, animated frame-rate-independently; OptiFine capes, Spotify, proximity voice chat, and account switching without restarting the game.
1.8.9The legacy client — the readouts that matter in a fight: FPS, CPS, ping, keystrokes, armour, potions, crosshair, zoom, freelook, toggle sprint. Same module and HUD model, on Legacy Fabric and Java 8.
launcherArc Launcher — its own isolated Minecraft/Fabric install, Microsoft sign-in, and a Play button that goes straight into the network. Engine in Java, window in Electron.
A build that enforces the rule
The staff edition carries anticheat-test combat modules. Those exist to be tested against
Sniffer on the modern network, so there is nothing on 1.8.9 for them to be tested against —
and :client-legacy:verifyNoRestricted fails the build if any ever appear in that jar.
Two update feeds rather than one with a version field, because the clients release
independently: a shared manifest would make every 1.8.9 release look like a pending update to
every modern install. Installing writes the new jar in place — no launcher release, no
reinstall, no restart of a launcher that's already open.
Every module in one list, drawn without a single vanilla widget. 26 of 48 on, at 518 fps.
arc launcher / home
Build 0.4.69, both versions installed side by side, with the changelog the publish script writes from CHANGELOG.md.
Network core · Java 21
Triangle
ranks · punishments · disguises · network state
Java
28.6k
Artifacts
02
Packets
13
The core every backend and proxy runs: ranks and the ladder, punishments, staff disguises,
network lock and drain, access gating, anticrash. It replaced its predecessor end to end, and
only two artifacts ship — a Velocity plugin and a Paper core.
stateWhole state, never deltas. Cross-proxy state travels complete, and the Redis bus never echoes to the sender — apply locally, then send.
storeMongo is the only persistence. Redis is pub/sub and ephemeral keys, nothing else. Proxies hold no persistent data at all.
ioVirtual threads throughout. Nothing blocks a Netty event loop. Rank ladders and permission snapshots cache in Caffeine, invalidated by whole-state packets.
uiNo separate host. Management is a grant-scoped tab inside the network SPA that Grove already serves — triangle.network, .ranks, .anticrash, .access.
Which way each thing fails
Backends stay authoritative, so proxy fast paths — the ban screen, permission snapshots —
fail open toward the backend check: a proxy that can't reach Mongo must not lock
everyone out. Web and staff enforcement fails closed, because a panel that can't verify
a grant must not act on it. Same system, opposite defaults, and the difference is the cost of
being wrong.
Breeze worked, and it froze permissions into sessions. Triangle decides permission from the
player's live rank at the moment they act, which is what lets the forums, the store and the
staff panel all read one source of truth without any of them caching a copy that can go stale
mid-session.
The proxy also pulls the server inventory itself rather than waiting to be told, and registers
backends dynamically — so a new game server that Grove starts is in the rotation without a
config edit anywhere.
Adversarial ML · Python
Astra
ships as one .exe · nothing to install
Python
11.6k
Transform families
06
Adapter kinds
03
Astra measures how far an AI-text detector degrades under adversarial transformation. You give
it a detector and a labelled corpus; it reports where the detector breaks, how cheaply, and at
what false-positive cost.
The number it exists to produce
robust TPR@1%FPR — worst-case true positive rate across every transformation, at a 1%
false positive rate measured on untransformed human text. A detector that catches 98%
of raw model output and 21% of the same output after one cheap rewrite is a 21% detector.
Astra reports the second number.
transformsSix families — surface, lexical, syntactic, semantic, degradation and hybrid, each costed separately by the strength needed to land it.
adaptersThree ways in — HTTP, in-process Python, or a subprocess. It won't start a run against an unvalidated adapter: three known-good samples go first, and the raw response is shown on failure.
reportRefusals over guesses — no bare accuracy figure anywhere, no confident number over a thin stratum, and it says so outright if your human pool is too small to observe a 1% FPR at all.
A distinction most benchmarks fold away
Surface-level failures are reported separately. A homoglyph swap that defeats a detector is an
input-sanitisation bug in its pipeline, not evidence that its model is fragile — folding the
two together makes every detector look worse than it is and hides the fix. The false-positive
drill-down gets equal billing with the attack results, because that half of detector behaviour
is the half that lands on real people.
A detector at 88.8% untransformed and 21.3% robust. The banner is the part most benchmarks leave out: at a 10% operating point, L2 writers are flagged 4× as often as native writers.
AI and data work
Where the models are, and what keeps them honest
Behavioural modelling
Sniffer's cloud
Six kinds of behaviour window — click, aim, backtrack, trigger, lagrange, scaffold — each with
its own close condition and its own floor below which it's discarded rather than scored. The
cloud fits native gradient boosting (and ROCKET) over them, runs rule analyzers across a
player's whole history, and folds every server's evidence into one decayed, burst-collapsed
score per player.
Verdicts are calibrated probabilities, not labels, and that's the point: 72% sure isn't enough
to remove somebody and is easily enough to stop them winning.
Models live3 · GBM
Val ROC AUC.76 – .86
Labelled windows11,177
Players observed941
Ground truth
Labelling discipline
A model is only as good as its ground truth, and the only ground truth anybody has is a staff
member watching a player and saying what they're doing. A collect session uploads every window
that player produces already labelled, with the named cheats attached, and sessions die with the
server on purpose.
Auto-labelling from the models' own verdicts is off by default — a model that trains on
its own confident mistakes drifts toward them, which is exactly how the aim model came to flag
legitimate players. Collector builds are compiled unable to punish anyone, so gathering data and
acting on it can't be the same binary.
Drift guardno self-training
Collector buildcannot punish
Evaluation
Astra, and measuring detectors
The other side of the same problem: not building a classifier, but establishing what one is
actually worth. Worst-case rather than average-case, a fixed false-positive operating point so
numbers across detectors mean the same thing, and strata reported as insufficient instead of
estimated from too few documents.
The false-positive drill-down is the half I care about most — a detector that's 4× harder on
second-language writers is a fairness result, not a footnote.
Headline metricrobust TPR@1%FPR
Attack families6
Bare accuracy shownnever
Applied LLM work
Jarvis & Echo
Two Python voice assistants, ~12.5k lines together: MCP tool servers, speech in and out, local
models, retrieval over a personal corpus, PDF and chart generation, and a dashboard over the
run history. Where most of my day-to-day LLM and pipeline work has gone outside Astra.
ToolingMCP servers
Modalityspeech + text
Also built
The rest of the shelf
Server fork · Java
Hazel
The Paper-lineage server every Arc backend runs on, carrying its own patch set — 25.6k lines of server-side Java, async pathfinding and entity tracking, and the engine profiles that give Legacy its 1.8.8 combat mechanics.
Network core · Java
Breeze
Triangle's predecessor, 22.6k lines across Bukkit, Bungee and shared implementations. The first core I wrote that had to keep a player's state correct across servers.
Bot framework · Kotlin
Mineral
A headless Minecraft bot framework with an LWJGL bridge and a behaviour layer — 14k lines of Kotlin, used to put synthetic players into a match and watch what falls over.
Desktop · Electron + Python
Sora
A video compressor that wraps ffmpeg in something you'd actually hand to someone — Electron shell, Python pipeline, one packaged executable.
Bot · Node.js
Moon
The network's Discord bot — commands, event handlers and the glue between Discord and everything above.
Plugins · Java
The early shelf
Echo, Polar, Solar, Zoom, Edge, Koi, Glorious, Kahti and a long tail of others — years of Minecraft plugins before any of the above, and where I learned the Bukkit API well enough to stop needing it.
I'm kawed. I started writing code at seven and I'm eighteen now, which means I've been doing this
for longer than I haven't. Most of that time has gone into one place: a Minecraft PvP network,
and everything it needs to exist.
That turned out to be a good way to learn, because a network doesn't let you specialise. The
client is a rendering problem. The practice core is a state-machine problem. Triangle is a
distributed-systems problem where the interesting question is which direction each component
should fail. Sniffer is a physics problem wearing a machine-learning problem's coat. Grove is
infrastructure that grew a storefront. Astra isn't Minecraft at all — it's a measurement problem,
and the hard part is refusing to print numbers you can't stand behind.
The thread through all of it is the same handful of rules: never store live state, never fabricate
a figure you didn't measure, make the deploy boundary immutable, and enforce on the server no
matter how good the UI looks. The anticheat and the benchmark are the same rule twice — say only
what the evidence supports, and be explicit about the uncertainty you're carrying.
age 7First line of code.
↓Years of Minecraft plugins — Echo, Polar, Solar, Zoom, Edge, Koi, Glorious and a long tail of others.
↓Breeze — the first real network core. Ranks, punishments, cross-server state.
↓Arc and Surf Client — the network people play, and the client they install to play it.
↓Grove — stopped deploying by hand. 41.7k lines of Go later, nobody SSHes into anything.
↓Triangle — replaced Breeze end to end, on Java 21 and virtual threads.
↓Sniffer — an anticheat from scratch, and a Rust cloud that learned to decide.
nowAstra, and whatever's next.
Get in touch
Open to work, and to interesting problems
Got something that needs building?
Networks, clients, control planes, models that have to be right — or the awkward glue between them.