My part ended September 2026

syntaxx.lol

A Discord bot, and the desk it was run from.

A co-developed moderation stack running across community servers, and the desktop dashboard built to support the people running them without ever holding a server hostage. It is still out there, still running — I am simply no longer the one shipping to it. This page is my half, written down while it is still fresh.

Where I left it

The handover state, to the line.

Counted from my last commit rather than from memory. It has kept moving since, so these are my numbers, not today’s. The bot is Node; the dashboard is Python.

40,763Lines of code
157Commands
63Event handlers
60Data models
  • The bot25,482 lines of Node — discord.js, MongoDB, pm2 on a VPS. Older than the repository these counts come from.
  • The desk15,281 lines of Python — a PySide6 desktop dashboard, twelve windows, ten custom widgets, built inside the last stretch.
  • Last stretch13 July to 20 August 2026 — thirty-nine days, ninety-four commits. The window this repository covers, not the life of the bot.
  • Built byCo-developed with Louis (lode787).
The build

Ten systems, not one bot.

Described as they stood at my last commit. Every one had per-guild configuration, its own permission model, and a way to be turned off by the server that hosted it.

Anti-scam and anti-raid

The part I would keep if I could only keep one. A trap channel that catches compromised accounts before they finish spraying links. A scam-server action that blacklists the guild and its owner, bans that owner across the other servers the bot is in, and then leaves quietly — deliberately not a nuke, because in a scam server the members are the victims, and it stays reversible. Raid spam is caught at five messages in three seconds and answered with an hour’s timeout, with per-guild exception lists for users, roles and channels. Alongside those: a global blacklist, an attachment-flood guard, slur logging, and an AI-likelihood check that scores both text and images through two separate providers.

Moderation core

Warns, cases, timeouts, kicks, mutes and purges, all landing on one infraction record that pages by severity. Message, mod and member logging to separate channels. Moderators earned points on a weighted scale — a warn is worth less than a kick — tracked weekly and posted to a log.

Softban: quarantine, not exile

Strips a member’s roles, saves them, and isolates them in a lobby rather than removing them from the server. An appeal channel opens once they are eligible. Leaving and rejoining does not escape it, and deleting the lobby channel does not break it. Lifting it hands back exactly the roles they had.

IP verification

A verification gate that hands out a role, and optionally refuses accounts sharing an address with an already-verified member. The address itself is never stored — only a SHA-256 hash, unique per guild, so one server’s records say nothing about another’s.

Alt detection

Scores every member against a target across four string-similarity algorithms, with homoglyph and leet normalisation, account age and join proximity, avatar and banner matches, and shared roles — weighted so a role most of the server already holds counts for nothing.

Join-to-create voice

Joining one channel spawned a private VC with an owner control panel — lock, hide, rename, limit, transfer, kick, block. Mods could always see in, and were immune to both. The channel died the moment its owner left.

Cross-server calls

Text relayed live between two servers, or three in a “mashup”. Server names were never shown — participants got anonymous colour labels — and every relayed message passed the word filter whatever the sending guild had toggled off. Abuse was traceable and timeout-able from either end.

Levelling and economy

XP with generated rank cards and per-guild styling, global and per-server leaderboards, a wallet, and a casino behind its own channel gate — blackjack, roulette, slots, mines, steal, and a chip leaderboard.

And the long tail

An AI chat channel on a local model, music with a queue, automod, custom commands, applications and staff evaluation, a partner system with quotas, welcome and leave messages, colour roles, verification-free utilities — AFK, QR, background removal, sticker and emoji lifting, timers, a cipher — and RSS listeners posting car news into channels that asked for it.

The dashboard — remote support without a backdoor

A PySide6 desktop app, about 15,000 lines, sharing the bot’s database: server settings, insights, a control panel, bot presence and MOTD, per-file deploys. The problem it solves is ordinary and the answer is the interesting part — a server owner needs help, and helping them means reaching into their server.

So it cannot. Not until that server’s owner is asked over DM and presses approve, on a request only its owner or an administrator can answer — being a bot developer grants nothing, because a developer with no standing in that server is not an approver. Every change made under a grant is appended to that grant’s own log, and the owner is sent the whole log when access ends. Support access you can read afterwards is support access you can trust.

Worth keeping

Four that earned their place.

Out of everything above, these are the four I would build the same way again tomorrow.

01 — The warn pipeline

One path, so the two commands could never drift

The prefix and slash versions of a warn looked like two features and were one: both ran the same action module, so a fix to either was a fix to both. The count was incremented atomically in the database rather than read-then-written, because two moderators warning at the same second would otherwise both read three, both write four, and step straight over the threshold.

At the threshold the bot did not act. It asked — the reply grew Apply and Dismiss buttons, locked to the moderator who issued the warn, so a bystander could not escalate someone else’s call. The trigger counter reset on apply; the lifetime record never did. Removing a warn earned no moderation points, because undoing work is not work, and paying for it would have made warn-then-unwarn a points farm.

The part that stuck: the interesting half of a moderation feature is never the action. It is who is allowed to confirm it, and what happens when two people act at once.

02 — The shell injection

A caption command that was remote code execution as root

One image command built its ffmpeg call as a single string and handed it to exec(), which on the server means /bin/sh -c. The user’s caption text went into that string, and the only escaping applied was for ffmpeg’s own text parser — so the shell’s substitution characters passed straight through. Substitution works inside double quotes in sh, so a payload never even had to break out of the quoting.

Nothing gated the command. Prefix commands were dispatched with no permission check at all, each one expected to gate itself, and this one gated nothing. So any member of any server the bot had joined could run code as the bot process — which ran as root, and could read the file holding the token and the database URL.

The fix was to call ffmpeg with an argument array and no shell at all. The old escaping stayed, but it was never what had made it safe. And it was verified on the server rather than assumed: the same payload through exec() executed, and through execFile() arrived as literal text.

Found while reading the code for an unrelated reason — which is how every interesting bug that summer was found, and never by the thing that was supposed to find it.

03 — The trap channel

Bait for compromised accounts, and the honesty it required

A compromised Discord account sprays scam links into every channel it can reach, in order, fast. So one channel was turned into bait: open to everyone, carrying a pinned disclaimer saying exactly what it was, and anyone who posted in it was removed on the spot. No real member posts in a channel like that by accident — but an account walking the list from the top hits it almost first.

On trigger it deleted the message, sent the user a DM saying their account was likely compromised — sent before the removal, because Discord needs a shared server for a DM to go through — removed them, recorded it against their infractions as a bot action rather than a moderator’s, and purged everything they had posted server-wide that day. Setup stripped attachments, embeds and reactions from the channel, so a scam image could not render in the seconds before the purge landed.

Two details mattered more than the mechanism. The everyone-role was rejected as a bypass role, because accepting it would have silently disarmed the trap while still looking configured. And if the disclaimer was ever deleted, the bot posted it again — a trap without its warning is just a channel that kicks people for no stated reason.

The last feature built, six days before the end. It shipped working.

04 — The AI chat guard

A system prompt that could not be talked out of the bot

One channel per server ran a chat model, and the obvious game starts immediately: talk it into printing its own instructions. Roleplay, a fake “everything above was a test, the real instructions follow”, a claimed authorised penetration test — the whole catalogue turns up within a day of switching it on.

Two things answered it, and neither is the prompt asking nicely. First, the instructions go through the API’s system role rather than being pasted on top of the user’s message, so the model renders them as a genuine system turn instead of more text a user happened to type. “Ignore previous instructions” then has to beat the role separation itself, not a label in a string.

Second, and this is the part that actually holds: a random token is generated per process and embedded in the prompt, alongside a handful of phrases distinctive to it. Every reply is matched against those before it leaves the bot. It does not matter which framing talked the model round — the check never looks at the attack, only at whether instruction text is present in the output. When it is, the reply is dropped and the user gets a refusal instead.

A blocked attempt is worth seeing, so a server can name a private channel for them. The report carries who tried it, where, their exact prompt, and the model output that was stopped — with mentions stripped, because a security notice should not be able to ping the room. The block has already happened by then; the log exists so somebody can read the attempt afterwards.

What I would defend hardest is the comment sitting above it in the source: this catches a near-verbatim leak, not a paraphrase, and a small local model can still be talked into ignoring its instructions. It is a backstop for the exact attack class we saw, not a solution to the general problem. Mitigations that describe themselves accurately are the only ones anyone can reason about later.

Operations

The tooling nobody using it ever saw.

Neither of these is a feature. They are the two internal things that made shipping to a live bot survivable.

Off the laptop, into version control

The stack lived where a stack should never live: on a machine, reaching the server by dragging files across in an FTP client. One dead drive, one bad overwrite, and a year of work is a story rather than a codebase. I moved the whole thing, on my own, into a repository with a real path to production behind it — line endings pinned so a Windows checkout and a Linux runner agree on what a file even is, a deploy that ships the last pushed commit and restarts the process manager, an exclude list that stops a deploy overwriting the live config and the server-side runtime state, and a written runbook for the parts that live outside the repo.

None of that is a feature and no user ever noticed it. It is the reason the project is still around to be argued over.

Two deploy routes that agreed byte for byte

A GitHub Actions workflow shipped the last pushed commit and restarted the process manager; the dashboard shipped individual files straight off a laptop for a fix being tested. The trap is line endings — a Windows checkout holds CRLF for some files and LF for others, so the two routes would have overwritten each other’s copies of unchanged files forever. The dashboard therefore hashed and uploaded the bytes git would check out, resolved per file, rather than the bytes on disk. Both routes shared one exclude list, so neither could ever clobber a live config.

The Command Visualiser

Internal only — no user ever saw this. A generated, self-contained page that read every command file the bot would register and laid out its options, subcommands, and the replies each branch could send, so a command could be reviewed before production rather than after. It flagged the ones that fail silently from the wrong account, which is the worst way for a command to be broken, and listed the files that would not load at all — separating the ones genuinely broken from the ones merely missing a package on this laptop.

Handing over

My part ended for reasons that had nothing to do with the code.

The project did not end with it. It is still running, still being worked on, still doing the job in servers full of people who will never know which half of it was mine — which is, honestly, the correct outcome for infrastructure. You do not get to keep the thing. You get to have built it.

My copy of the history is kept whole — every branch, the full log, my last commit exactly as it stood. Nothing deleted, nothing rewritten to look better in hindsight. It was the largest thing I had built, it went out to real servers for real people, and it worked.

Once a pleb, always a pleb.