Under the Hood
Architecture
One bot process runs everything on the community side and serves a private REST API. The KSP add-on and this website are both clients of that API, and neither shares any code with the bot.
One process, two servers
The entry point starts the Discord bot and a uvicorn-hosted FastAPI server as concurrent asyncio tasks in the same process. The bot handles Discord interactions, the API handles requests from the game and from the website, and they share the same in-memory data layer rather than talking to each other over a socket.
bot.py
|-- discord.py client slash commands, buttons, embeds, DMs
|-- uvicorn / FastAPI /api/v1/... the KSP add-on
| /api/v1/web/... this website
\-- data layer Firestore, buffered in memory
Next.js site
\-- server-side proxies never calls Firestore directlyThe website reaches the API through its own server-side routes rather than from the browser, so a session token is never handed to page JavaScript. The add-on’s browser interface does the same thing for the same reason, one layer further down.
Where the data lives
Firestore, with a deliberate split between what belongs to a player and what belongs to a server.
users/{user_id}top level, guild independent | XP, balance, level, message count, unlocked levels, rescue count, language preference. The wallet is global: a player carries one balance between every server the bot is in. |
guilds/{guild_id}/...per server | Server configuration, corporations, weekly missions and selections, tickets. Things that genuinely belong to one community rather than to a player. |
contracts/global | One collection, because a contract can run between players in different servers. The originating server is stored on the document for channel routing, not used as a key. |
marketplace/global | Listings, with votes and reports in their own collections beside it. |
mission_classifications/cache | AI classification results, so a set of missions is classified at most once. |
User records are mirrored in an in-memory dictionary. Writes are buffered there and flushed to Firestore on a five minute timer rather than on every change, because XP from chat would otherwise be one Firestore write per message.
Synchronous storage, asynchronous handlers
The firebase-admin SDK is synchronous while Discord and API handlers are async. Writes are wrapped so they can be awaited; simple reads stay synchronous.
user = store.get_user(guild_id, user_id) # synchronous read
await store.add_balance(guild_id, user_id, 250) # awaited writeThe guild_id that is not a key
store.get_user still takes a guild id, because hundreds of call sites pass one, but it is ignored when locating the record. The wallet moved to the top level and the signature stayed put.
Spending controls
Gemini and Firebase both cost money, and a runaway loop is a bill rather than an outage. The cost guard tracks the month’s spend from three sources and applies a ladder rather than a wall.
In-process countersinstant, approximate | Every Firestore and Storage operation is counted as it happens. Instant is the only property a brake needs, and it cannot be exactly right: a file fetched from a signed URL never passes through the process. |
Cloud Monitoringaccurate, minutes behind | Sees the egress the counters miss, plus anything that was not the bot at all. Adopted as a baseline that the fast counters then add to, with the gap reported rather than hidden. |
Billing exportexact, hours behind | Actual billed dollars, net of free-tier credits. Display only, never a trigger, because a brake fed by a source that lands a few times a day would let a runaway spend for a whole cycle first. |
The ladder runs normal, warn, degraded and frozen. Degraded refuses new uploads while reads, downloads and everything else keep working, so the bot stays usable. Freezing arms exactly one final flush, because flushing the memory buffer is itself a write and a stop that refuses it would convert “we stopped spending” into “we lost everyone’s last few minutes of XP”.
When the AI budget is spent, every AI call site behaves exactly as if no API key were configured. See AI Integration.
Cogs
Every bot feature is a discord.py cog. Shared state goes through the store, the settings module and the localisation module rather than between cogs, though a few cogs do import each other where one genuinely owns a helper another needs. New work should prefer the shared modules.
The mimic system
An administrator can act as another user for testing. Rather than threading a fake identity through every handler, the bot patches three internal discord.py dispatch points so the swapped identity is applied before any handler runs.
Code that needs the genuine caller, such as a permission check, reads it back explicitly. Anything that does not is by definition happy to see the mimicked user, which is the whole point.