Skip to content
kawed.
Six systems — client, game, core, anticheat, control plane

I build the whole stack a Minecraft network 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.

Writing code since
age 7
Lines in the six below
0k
Languages shipped
0
What runs, and how big it is
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.
GoDocker Engine APISQLite WebSocketSFTPDNS GeoIPStripeMongoDB
grove / dashboard
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 rolesrole: 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 / Spigot WebSocketMavenMongoDB
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.

JavaRustaxum sqlx / PostgresReactTypeScript NettyGradient boostingROCKET
sniffer / cloud
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.

JavaFabricLegacy Fabric VulkanLWJGLGradle ElectronMicrosoft OAuth
surfclient / modules
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.

Java 21Virtual threadsVelocity PaperMongoDBRedis CaffeineNetty
Why it replaced Breeze

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.

PythonNLPAdversarial ML CalibrationWebView2PyInstaller
astra / overview
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.

Stack

Counted from the repos, not from memory
Java303,367
Go41,731
JavaScript27,672
Rust15,332
Python14,554
Kotlin13,989
CSS12,492
TypeScript5,272
DockerPostgresMongoDB RedisSQLiteaxum ReactNettyWebSocket DNS / GeoIPStripeMaven GradleCargoElectron FabricVulkanLWJGL Woodpecker CIGitHub Actions

About

Eleven years in

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.

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.