Devlog #1: the slack bot that got out of hand
this started because our workspace needed a fun bot. not serious. just something to roast people, hold a little economy, maybe play akinator.
that was months ago. FuSBot now has a dungeon system, a voidmaze, an arena, a lab, and an animal collection i still don’t fully understand. i do not know how this happened.
the economy
the first thing i built was horsenncy. the bot’s currency. earn it, spend it, gamble it, give it to people who are losing. the name is a joke i can’t explain and also can’t undo. nobody has asked what it means. everyone probably assumed it made sense to someone.
the economy runs on state.json. giant nested dictionary, not a real architecture decision, it just started that way and everything got built around it. now it’s the spine of the whole bot and also a file that gets auto-committed to the repo every six hours by github actions.
sometimes the commit history is just:
Update bot state
Update bot state
Update bot state
very normal software engineering.
the roasting
/fus_roast and the bot insults someone with an llm.
original version took @username as plain text. fine until you’re in an 18k-user workspace, the bot paginates through the entire member list, hits a rate limit on page 91, and the roast fires at the sender instead of the target.
i found out because someone said “bro you got roasted.” i was the one who ran the command.
now it uses a slack modal user picker. popup opens, you search for your target, slack handles the uid lookup, bot gets the id directly. no pagination. no rate limits. no self-roasting.
the roast hits multiple llm providers in parallel, faucet, groq, gemini, github models, openai, scores results for spiciness and returns the winner. fast mode takes whatever comes back first. overkill for an insult generator. i am at peace with this.
the typing animation
the bot called conversations.typing in a loop while waiting on llm responses. never worked. slack requires channels:write for that, which is way too much permission for a roast bot.
new approach: post ... immediately when the bot starts thinking, delete it when the real reply is ready. not a typing indicator. just looks like one. nobody noticed.
akinator
/fus_aki starts a real akinator game in slack with yes/no/idk/probably buttons.
akinator.py broke when akinator changed their backend. switched to the akinator package, which also broke, KeyError: ‘akitude’ because the api response dropped that field.
fix: monkey-patch. __handler is name-mangled but python lets you replace _AsyncClient__handler from outside. swapped data[“akitude”] for data.get(“akitude”, self.akitude). one line. done. this took longer to explain than it took to fix.
the brain
the bot has a system called the brain. it reads channel messages and builds a lightweight model of what topics come up and who’s active. uses that context when responding to mentions. if a channel is mostly about games it leans into that. if a user keeps mentioning chess, it remembers.
personalization system or mild surveillance depending on how you frame it. the bot is simply learning. there is a distinction.
how it runs
FuSBot runs on github actions. starts on push, runs up to six hours, commits state back to the repo on exit. cron trigger every six hours as keepalive. cancel-in-progress: true so pushing a fix auto-restarts the bot. not how you deploy a slack bot. works anyway.
what’s next
- more quest types
- roast presets
- figuring out what the dungeon is actually supposed to be
- not letting state.json get unreadable
the bot got way bigger than planned. someone lost 50,000 horsenncy in a dungeon run last week and immediately asked when the next update was.
that felt like a good sign.
thanks for reading :)