Skip to content
Mustafa Tarhan

Aiko, a Community Platform for Discord

Moderation, a web dashboard with its own permission model, encrypted storage, an RPG economy and an AI assistant with a hard budget.

Production system2026 – todayActive · in production
Role
Sole developer and operator: requirements, product decisions, testing with the community and operations.
Modules
45
Tests
304, all passing
Deployments
Staging and production
Data at rest
AES-256 encrypted
Stack
Python, asyncio, discord.py, aiohttp, SQLite / SQLCipher, FTS5, Gemini API, faster-whisper, Pillow, JavaScript, systemd, Caddy

What it is

Aiko started as a moderation bot for one community and kept growing. Today it covers moderation and case management, tickets, reports, logging, automod and raid protection; levels, role automation, suggestions and rendered quote cards; a themed RPG with hunting, trading, world bosses and team events; and a support assistant that answers questions about the bot with Google's Gemini, using only knowledge that staff have approved.

It's one codebase deployed twice. System runs on my test server and is where every change lands first. Aiko is production. They're separate Discord applications with their own configuration, secrets and encrypted databases.

Why build it

Big Discord communities usually run a handful of third-party bots: one for moderation, one for tickets, one for levels, one for verification. Each has its own idea of permissions, and the moderators end up with broad Discord rights just so the bots work. The community wanted one tool the moderation team fully controls. Configurable without code, with access based on the team's own ranks rather than raw Discord permissions, and with clear rules for what happens to people's data.

Ground rules

  • Moderators act through the bot and its permissions. What they're allowed to do comes from team levels set in the dashboard, with overrides per command and per module.
  • Every setting can be changed in the dashboard, and which settings someone can see depends on their team level.
  • All state is stored per server, so one process can serve several servers without mixing anything up.
  • Message content is kept for 30 days at most. The database and ticket transcripts are encrypted, and members can export or delete their data.
  • A restart or a deploy must never lose a running bet, an open trade or a timed ban.
  • It has to run on one small VPS at a predictable cost, LLM usage included.

One process, many servers

  1. Entry points
    • Discord gatewayslash commands, components, events
    • Web dashboardaiohttp and plain JS, Discord OAuth2
    • Public pagescommands, wiki, privacy, transcripts
  2. Request context
    • Guild contextContextVar set per gateway event
    • Permission rulescommand, module, action
    • Module gateenabled per server
  3. Modules
    • Moderationcases, reports, tickets, automod, protection
    • Communitylevels, roles, feeds, starboard, quotes
    • Gameeconomy, hunting, bosses, raids, events
    • AI supportGemini behind local retrieval
  4. Stores
    • Per-server storessettings, cases, economy and more
    • Settings cataloguegenerates the dashboard forms
    • User-data registrycovers export and deletion
  5. Storage and ops
    • SQLite with SQLCipherAES-256 at rest
    • Encrypted backupsdaily, pulled off-site
    • systemd and CaddyHTTPS, restarts
Taken from the bot's package structure. Hostnames, ports and IDs are left out on purpose.

Before discord.py hands an event to the bot, a small wrapper reads its guild_id and sets a ContextVar. asyncio tasks inherit the context, so bot.config and every store resolve to the right server without a server ID being passed through hundreds of functions. Background loops and the dashboard pick a server explicitly with use_guild(...). Only whitelisted servers are served, and slash commands are synced per server with only the modules that server has turned on.

Settings are data. A catalogue of typed fields describes every setting, and the dashboard builds its forms from it, so adding a setting is one entry in one file. Changes are written to the moderation log.

Runtime
Python, asyncio, discord.py 2, aiohttp
Data
SQLite with SQLCipher (AES-256), FTS5 full-text search, Fernet-encrypted backups
Web
Dashboard and public pages in plain JavaScript, HTML and CSS; Discord OAuth2 login
AI and media
Gemini (google-genai), faster-whisper, Pillow
Operations
Debian VPS, systemd services and timers, Caddy
Quality
pytest (304 tests), deploy script that refuses to run on failing tests

What's in it

  1. 1.

    Moderation

    Numbered cases for warns, timeouts, kicks and bans, with editable reasons. Actions taken directly in Discord are picked up from the audit log. Timed bans are stored and survive restarts, and warn thresholds can escalate to a timeout, kick or ban.

  2. 2.

    Tickets with anonymous staff

    Staff can answer anonymously: they talk in a private thread and the bot relays their replies, mapping messages both ways and mirroring typing indicators. Transcripts are rendered to HTML, stored encrypted and only decrypted when someone opens them.

  3. 3.

    Protection

    Lockdown, raid detection with an alert card (pause invites, kick or ban from buttons), a honeypot channel and an optional minimum account age.

  4. 4.

    Voice moderation

    In one monitored voice channel the bot decrypts Discord's end-to-end encrypted voice (DAVE), transcribes it locally with faster-whisper and checks it against the automod word list. Transcripts are deleted after seven days.

  5. 5.

    Game

    A themed RPG with creatures, ranks and classes, progression, equipment, a bank market, trading through an escrow table, world bosses, scheduled raids and team events. I tuned the balance by simulating fights, and a test checks that the dashboard's JavaScript copy of the formulas gives the same results as the Python engine.

  6. 6.

    Rendered images

    Quote cards and card-game tables are drawn on the server with Pillow, including emoji and animated GIF cards for animated stickers.

The support assistant

The assistant answers questions about the bot and, for questions about the story, only from an archive of the author's own answers. It's not a general chatbot, and most questions never reach the model:

  1. 1.

    Work out who's asking

    Owner, admin, staff or member, decided from IDs and team levels, never from role names. Checked again on every question, so a removed role takes effect straight away.

  2. 2.

    Approved FAQ

    Exact match on the normalised question. No model call.

  3. 3.

    Local search

    SQLite FTS5 with Porter stemming, aliases and typo correction. Visibility, server and team-level filters are part of the SQL query, not applied afterwards.

  4. 4.

    Cache, then Gemini

    Only then is Gemini called, with the allowed knowledge and a few retrieved excerpts. If that knowledge doesn't cover the question, the bot says it doesn't know.

Where an answer may appear depends on who can see the channel. The bot answers in the channel only from knowledge everyone there may see; if it needs staff knowledge, the answer goes to the asker by DM. Cost has a ceiling: limits per member, per server and overall, plus daily and monthly budgets, each checked and booked in one BEGIN IMMEDIATE transaction so two requests at the same moment can't overspend.

What had to hold

  • Keeping servers apart. Going from one server to several meant every store, loop and listener had to know which server it was working for. The context variable kept that change contained, and disabled modules stay completely silent on a server.
  • Money that can't be spent twice. Bets and trades are booked atomically, so a double click doesn't spend coins twice. Open stakes and escrow entries are refunded on restart or when a module is unloaded.
  • Privacy that tests enforce. Every table that stores a user ID must be registered for export and deletion, and a test fails if a new one isn't. Message content is purged after 30 days at most.
  • Boring deploys. Every change runs on the staging bot first. The deploy script runs the tests, asks for confirmation, takes an encrypted backup of the production database, copies code only (never secrets, configuration or data) and restarts the service.

Trade-offs I accepted

Encrypted SQLite instead of a database server

Why
One small VPS, simple backups, no database listening on the network.
Cost
Writes are serialised, so long transactions have to be avoided.

Plain JavaScript for the dashboard

Why
No build step on the server and a small attack surface. Anything that comes from Discord is inserted as text, never as HTML.
Cost
More hand-written DOM code than with a framework.

A context variable for the current server

Why
Every store knows its server without rewriting hundreds of call sites.
Cost
Implicit state. Code that runs outside an event has to set it, and there are tests for that.

The model only sees what retrieval allows

Why
Predictable cost, no made-up answers, and permissions are applied before the model sees anything.
Cost
Someone has to curate and approve the knowledge in the dashboard.

Today and next

Aiko runs in production on a large community server, and System gets every change first. The test suite has 304 passing tests. The support assistant is built and tested on the staging server.

  • Rolling out multi-server mode in production, starting with an appeals server linked to the main community.
  • Turning on the support assistant in production once staging has run long enough.
  • Discord's review of privileged intents for the production app.
No screenshots for now: the dashboard and the Discord views show real member names and IDs. Cleaned-up captures from the staging server will follow.