79 commands and 131 subcommands — moderation, economy, a casino, levelling, music, tickets, giveaways
and games — plus an optional web dashboard. Every command works as /ban and as
t?ban, because underneath it is one command, not two copies.
Quick start · Features · Screenshots · Dashboard · Docs · Support
Caution
Never share or commit your .env file or any of its values. It holds your bot token and your MongoDB
password — anyone who gets them controls your bot and your data. .gitignore already covers .env*. If a
token ever reaches somewhere public, reset it immediately in the Developer Portal.
📑 Table of contents
- ✨ What's new in v2
- 🧩 Features
- 📸 See it in Discord
- 🏗️ How it fits together
- 💻 Compatibility
- 🚀 Quick start
- 📖 Full setup guide
- 🐳 Running it in Docker
- ⌨️ Slash and prefix
- 🗂️ Command categories
- 🛠️ Adding your own command
- 🖥️ The web dashboard
- 📜 Scripts
- ❓ FAQ
- 🩺 Troubleshooting
- 📚 Documentation
- 🤝 Contributing
- 💛 Support
- ⚖️ License
Tip
⭐ If Testify is useful to you, or you have borrowed any of its code, please leave a star. It genuinely helps, and it tells us the project is worth continuing.
v2 is a complete rewrite of the original JavaScript bot in TypeScript — not a port with types bolted on. The whole thing was rebuilt around the problems the old codebase actually had.
| v1 (JavaScript) | v2 (TypeScript) | |
|---|---|---|
| Slash + prefix | Two separate implementations of each command, kept in sync by hand | One command serves both. 46 duplicated pairs became one file each |
| Types | None | strict, plus noUncheckedIndexedAccess and exactOptionalPropertyTypes |
| Config | Bot IDs and log channels hardcoded, so a fresh clone logged into someone else's server | Everything through a validated .env — no IDs committed to the repo |
| Start-up | Command registration raced the login | Each step awaited in order; a malformed file names itself and stops the boot |
| Errors | Stack traces could reach chat | One error boundary — users get a plain apology, you get the full context |
| Money | Read-modify-save, so balances could duplicate under load | Atomic database updates |
| Tests | A handful | Nearly 6,000 tests, with an enforced 80% coverage floor |
| Setup | Manual, including patching a file inside node_modules |
npm run setup, and you are running |
| Web | None | An optional dashboard served from the bot itself |
Everything the old bot did is still here, apart from the integrations that needed paid or personal API keys (Spotify, Valorant, Instagram and the AI commands).
|
|
|
|
|
|
Every picture the bot sends is drawn on the fly with its bundled fonts, so it looks the same on every host. These
come from the bot's own renderers — npm run docs:images redraws them with invented members.
One command object serves both surfaces. A slash command and a prefix command arrive by different roads, pass the same gates and run the same code; the dashboard is a third surface onto the same domain logic.
| Operating system | Support | Notes |
|---|---|---|
| Windows 10 / 11 | ✅ Full | cross-env sets the environment variables, so the scripts work in cmd and PowerShell |
| macOS | ✅ Full | Apple Silicon and Intel |
| Linux (Ubuntu / Debian) | ✅ Full | What CI runs on |
| Linux (Fedora / Arch) | ✅ Full | |
| Linux (Alpine) | ✅ Full | Needs apk add --no-cache python3 make g++ for the native modules |
| Raspberry Pi | ✅ Full | Use a 64-bit OS — the prebuilt canvas binaries are arm64 only |
| Version | Support | Notes |
|---|---|---|
| 22.x and older | ❌ Not supported | npm install warns with EBADENGINE |
| 24.11+ (LTS) | ✅ Recommended | What .nvmrc pins and what CI tests against |
| 25.x | ✅ Supported | Works, but not what CI runs |
Important
Testify requires Node 24.11 or newer. If you are stuck on an older version, use
nvm — the repo ships a .nvmrc, so nvm use picks the right version
automatically.
Every push runs the typecheck, lint, formatting, the full test suite, a coverage gate and a real build — then
verifies the compiled dist/ actually starts. A nightly workflow re-runs the tests and scans dependencies for
newly published vulnerabilities with npm audit and Snyk.
Already have Node 24 and a MongoDB connection string? You are five commands away.
git clone https://github.com/Kkkermit/Testify.git
cd Testify
npm ci # installs exactly what the lockfile says
npm run setup -- --dev # asks for your token, client ID, owner ID and database URL
npm run dev # starts the bot, restarting whenever you save a fileThat is the whole setup. npm run setup -- --dev writes .env.development, the file npm run dev reads, so
there is nothing to hand-edit and nothing to paste in the wrong place. Each answer is checked as you type it.
Running it for real rather than developing it? Use npm run setup (which writes .env), then npm run build
and npm start. No token or database yet? The next section walks through both from scratch.
1. Install the tools
- Node.js 24.11 or newer — check yours with
node -v - Git
- A code editor — VS Code is a good default
2. Create your bot and get a token
- Go to the Discord Developer Portal and sign in.
- Click New Application, name it, and confirm.
- Open the Bot tab on the left.
- Under Privileged Gateway Intents, turn on all three, then Save Changes. Message Content is not optional — without it the bot cannot read any prefix command.
- Click Reset Token and copy it. This is the one value you must never share.
3. Invite the bot to your server
- Open OAuth2 → URL Generator.
- Under Scopes, tick
botandapplications.commands. - Under Bot Permissions, tick Administrator while you are getting started.
- Copy the URL at the bottom, open it, pick your server, and authorise.
4. Get a database
Testify keeps everything in MongoDB. The free tier is plenty.
- Sign up at MongoDB Atlas.
- Create a free M0 cluster.
- Under Database Access, create a database user and save the password.
- Under Network Access, click Add IP Address and choose Allow access from anywhere.
- Back on Database, click Connect → Drivers and copy the connection string.
- Replace
<password>in it with your database user's password.
5. Fill in your settings
npm run setup -- --dev # a development bot: writes .env.development, which `npm run dev` reads
npm run setup # the bot people invite: writes .env, which `npm start` readsIt asks for each value, checks it as you type, and writes the file for you. Required fields are marked and it
will not let you skip them. Prefer doing it by hand? Copy .env.development.example to .env.development (or
.env.example to .env) and fill it in.
| Variable | Required | What it is |
|---|---|---|
DISCORD_TOKEN |
✅ | The token from step 2 |
DISCORD_CLIENT_ID |
✅ | General Information → Application ID in the Developer Portal |
DISCORD_OWNER_IDS |
✅ | Your Discord user ID. Comma-separate for several owners |
MONGODB_URI |
✅ | The connection string from step 4 |
DISCORD_DEV_GUILD_ID |
— | A test server ID. Commands register there only, so a half-built one stays off every other server |
LOG_LEVEL |
— | trace, debug, info (default), warn, error or fatal |
CHANNEL_ERROR_LOG |
— | Where command failures are reported, each with a reference the user is also given |
CHANNEL_GUILD_LOG |
— | Where server joins and leaves are reported |
CHANNEL_DM_LOG |
— | Where DMs to the bot are logged, with a button to reply as the bot |
CHANNEL_SLASH_COMMAND_LOG |
— | Where every slash command is logged: which one, by whom, where, and whether it worked |
CHANNEL_PREFIX_COMMAND_LOG |
— | The same for every prefix command |
CHANNEL_BUG_REPORT_LOG |
— | Where /bug-report lands |
CHANNEL_SUGGESTION_LOG |
— | Where /suggest lands |
CHANNEL_EVAL_LOG |
— | Every /eval run with its code, and anyone else trying an owner command. Else the error channel |
SUPPORT_AI_API_KEY |
— | Lets /ask use Claude to pick the right help article. Without it, search alone answers |
Leave any optional value blank and that feature simply stays off. Nothing breaks. The music and dashboard variables are covered in their own sections below.
Getting an ID: enable Developer Mode in Discord (Settings → Advanced), then right-click any user, server or channel and choose Copy ID.
6. Run it
npm run devThe terminal shows each step of start-up as it finishes — settings, database, modules, commands, Discord — and then the bot's name in block capitals with its servers, members and commands. Save any file and the bot restarts itself.
If something is wrong, start-up stops at that step and says what to change, in a box rather than a stack trace:
a value missing from .env, a token Discord rejected, the privileged intents left off, or a database it cannot
reach. Set NO_COLOR=1 for plain output, or FORCE_COLOR=1 to keep colour through a pipe.
For production:
npm run build
npm start7. Music (usually nothing to do)
Music needs two binaries, and npm install normally supplies both — ffmpeg-static and youtube-dl-exec are
optional dependencies, so they install themselves on a normal machine and are skipped rather than
failing the install when a network cannot reach them.
If one is missing, /music status says which, and:
npm run music:setupfetches yt-dlp into bin/. Testify looks in MUSIC_YTDLP_PATH / MUSIC_FFMPEG_PATH, then your PATH, then the
npm package, then bin/ — and for yt-dlp the newest copy that runs wins, wherever it lives, because a stale
extractor is the commonest way music breaks. Set the variables only to force a specific build.
FFmpeg is optional. YouTube and most of SoundCloud already serve Opus, which Discord takes as-is, so the usual track is never transcoded. FFmpeg is what the volume control and seeking use; without it the volume stays at 100% and the few tracks that are not Opus are refused by name rather than played as silence.
Spotify links cannot be played by anything — the audio is DRM-protected. Search for the track by name instead.
"Sign in to confirm you're not a bot" means YouTube is refusing requests from the host, which is common on
cloud hosts. Update yt-dlp with npm run music:setup, and if it keeps happening, set /play to SoundCloud only in
the owner console's control tab.
Tip
Use two bot applications — one for development, one for production. npm run setup -- --dev writes
.env.development, which npm run dev reads instead of .env. That way testing can never touch your live
bot or its database.
If you would rather not install Node at all, the repository ships a Dockerfile and a docker-compose.yml
that bring up the bot and a MongoDB together:
cp .env.example .env # fill it in, or run `npm run setup`
docker compose up -d
docker compose logs -f botMONGODB_URI defaults to the database in the compose file, so a fresh clone needs nothing else set up. Data
lives in a named volume and survives a restart. FFmpeg and yt-dlp are installed by the image, so music works with
nothing to configure — on Railway too.
docs/hosting.md is the full guide — the dashboard behind a reverse proxy, what is in the
image and what is deliberately left out, which hosts can and cannot run a gateway bot, and a troubleshooting
table.
Every command works both ways, from a single implementation:
/ban user:@someone reason:spamming
t?ban @someone spamming
The default prefix is t?, it matches in any case (so a phone's T?help works), and prefix commands are on by
default. Server admins can change either:
| Command | What it does |
|---|---|
/prefix show |
Show the current prefix |
/prefix set <prefix> |
Change it |
/prefix enable |
Turn prefix commands on |
/prefix disable |
Turn them off, leaving slash commands only |
There are 90 prefix aliases — t?bal, t?lb, t?np, t?gamble and the rest — listed beside each command
in docs/commands.md.
A command that answers privately on a slash command cannot do that in a channel, since an ordinary message cannot be ephemeral. The prefix version replies in the channel instead and deletes both messages after 20 seconds, with a countdown saying so.
Run /help in Discord for the browsable version, or see docs/commands.md for the full
generated list.
| Category | Top-level | What is in it |
|---|---|---|
| 💰 Economy | 20 | Balance, work, daily, rob, heist, shop, pets, lottery, leaderboards |
| 🛡️ Moderation | 17 | Ban, kick, mute, warn, softban, lock, clear, roles, slowmode |
| 📚 Info | 12 | User, server and role info, avatars, profiles, ping, help, /ask |
| ⚙️ Settings | 10 | Automod, audit logging, auto roles, counting, welcome, verification, prefix |
| 👑 Owner | 5 | Eval, blacklist, guild list, DM, flush logs |
| 👥 Community | 3 | Memes, translation, Minecraft lookups, advice, wiki |
| 🎮 Fun | 2 | ASCII art, fake tweets, hack, IQ, nitro, Oogway quotes |
| 📈 Levelling | 2 | Rank cards and the levelling settings |
| 💬 Feedback | 2 | Suggestions and bug reports |
| 🎵 Music | 2 | Play by link or search, queue, loop, shuffle, skip, volume, DJ roles |
| 🎰 Casino | 1 | Roulette, blackjack, slots, hi-lo, coinflip, dice, stats, bet limits |
| 🎯 Games | 1 | Guess the number, Pokémon, fast type, RPS, 8ball |
| 🎁 Giveaways | 1 | Start, end, reroll, delete |
| 🎫 Tickets | 1 | Setup, status, disable |
Some categories look small but hold a lot: /casino, /game, /fun and /lookup group many subcommands
under one parent, which is how the bot stays under Discord's hard limit of 100 top-level commands.
Create one file. The loader finds it, /help lists it, and it works as a slash and a prefix command
straight away.
// src/commands/fun/coinflip.command.ts
import { defineCommand } from "@core/command";
import { reply, successEmbed } from "@lib/discord";
export default defineCommand({
name: "coinflip",
description: "Flips a coin.",
category: "fun",
aliases: ["flip", "cf"],
async run(interaction) {
const side = Math.random() < 0.5 ? "Heads" : "Tails";
await reply(interaction, { embeds: [successEmbed(`🪙 ${side}!`)] });
},
});Restart, and you have /coinflip, t?coinflip, t?flip and t?cf. There is no registry to update and
nothing to import by hand.
Important
Three rules worth knowing
- The
.command.tssuffix is what the loader looks for. A file without it is silently never loaded — so a test enforces the naming. categorymust be a key fromsrc/config/categories.ts. A typo is a compile error, not a runtime surprise.- Build embeds with
embed()from@lib/discord. The linter blocks barenew EmbedBuilder(), so every embed gets consistent colours and footers for free.
Options, subcommands, buttons and the 100-command limit are covered in
docs/contributing.md.
Testify ships an optional web dashboard with a screen for nearly everything the bot does, signed in with Discord and served from inside the bot process — no second service to deploy and no API key to manage.
Note
It is off by default, and a bot-only install needs none of this. Leave DASHBOARD_ENABLED unset and
nothing below applies.
It follows your device's light or dark setting out of the box, comes in twelve accent colours, and speaks English, Spanish, German, French, Italian and Russian — picked from the browser's own language and changeable from the Appearance screen. All of it is per browser: nothing you choose there affects anybody else.
Server overview — what the bot is doing here, and what it is missing
Four settings. npm run setup handles all of them: it asks for the client secret and the base URL, sets
DASHBOARD_ENABLED=true, generates the session secret, and prints the redirect URL to paste into Discord.
| Variable | What it is |
|---|---|
DASHBOARD_ENABLED |
true to serve it at all |
DISCORD_CLIENT_SECRET |
From the same Discord application page as your token — OAuth2 tab |
DASHBOARD_BASE_URL |
Where people open it, e.g. http://localhost:5174 |
DASHBOARD_SESSION_SECRET |
npm run secret -- --write writes one into your .env |
The redirect URL goes on that same OAuth2 tab: your base URL with /api/auth/callback on the end. npm run setup prints the exact string, and so does the sign-in screen if you get it wrong.
npm run dev:all # the bot and the dashboard togetherThe page is on :5174, the API on :3000, and Vite proxies between them so your browser only ever talks to one
origin. In production npm run build && npm start serves the built page from the bot's own port.
Important
Enabling the dashboard without the other three settings fails at start-up naming all three at once, rather than starting half-configured. If the port is already in use the bot keeps running without the dashboard and the log says which port to change.
Signing in with Discord gets you the servers you already have Manage Server in, and nothing else. Every
request re-checks that against Discord live, so losing the permission locks you out on your next click rather
than at your next sign-in — and an action asks for what its command asks for, so kicking somebody from the web
needs Kick Members just as /kick does. The owner console is DISCORD_OWNER_IDS and nothing else; to everybody
else those routes answer 404, so they cannot even be found.
The defaults are the safe ones and they assume you are on your own machine:
DASHBOARD_BINDis127.0.0.1. Changing it to0.0.0.0puts an admin panel on the internet. Put a reverse proxy with HTTPS in front of it first.DASHBOARD_TRUST_PROXYisfalse. Turn it on only when a proxy you control setsx-forwarded-for— trusting that header without one lets anyone forge their rate-limit bucket.- Use
https://inDASHBOARD_BASE_URLonce you have a certificate. That is what turns on HSTS and theSecureflag on cookies. - Rotating
DASHBOARD_SESSION_SECRETsigns everybody out. That is the whole procedure after a leak.
The security model is in docs/security.md, and the practical detail for working on the
dashboard is in docs/dashboard/guide.md.
| Command | What it does |
|---|---|
npm run dev |
Runs the bot from source, restarting whenever you save |
npm run dev:all |
Runs the bot and the web dashboard together |
npm run build |
Compiles the bot and the dashboard |
npm start |
Runs the compiled bot |
npm run setup |
Interactive .env generator (add -- --dev for .env.development) |
npm run check |
Typecheck, lint, format check and tests — everything CI runs |
npm test |
Runs the test suite |
npm run test:coverage |
Runs the tests with the 80% coverage gate |
npm run lint / lint:fix |
Lints, optionally fixing what it can |
npm run format |
Formats everything with Prettier |
npm run commit |
Guided commit message in the project's format |
npm run docs:commands |
Regenerates docs/commands.md from the real commands |
npm run docs:images |
Redraws the card images in this README from the bot's renderers |
npm run music:setup |
Fetches yt-dlp into bin/, and reports whether FFmpeg is there |
npm run secret |
Generates a dashboard session secret (-- --write puts it in .env) |
npm run commands:clear |
Removes every registered slash command from Discord |
npm run db:wipe |
Asks development or production, then which collections to empty |
npm run db:wipe:dev |
The same, straight to the database in .env.development |
npm run audit |
Checks dependencies for known vulnerabilities |
Does any of this cost money?
No. Discord bots are free, and MongoDB Atlas has a free tier that is far more than enough. You only start paying if you outgrow the free database, or want to host the bot somewhere other than your own machine.
Do I need to know TypeScript to use this?
No. To run the bot you never touch the code at all. To add a command, basic JavaScript is enough — the types
mostly help by telling you something is wrong before you start the bot rather than after. Copy an existing
file in src/commands/ and change it.
My slash commands are not showing up in Discord.
Almost always one of three things:
- Your Discord client is showing a cached list. Registration is immediate — global commands do not take an hour to roll out — but the client caches what it was last told. Reload it with Ctrl+R (Cmd+R on macOS) and they appear.
- The bot was invited without
applications.commands. Re-invite it with bothbotandapplications.commandsticked in the URL Generator. - Start-up failed before publishing. Commands are published before login, so check the console.
If Discord is showing commands that no longer exist, run npm run commands:clear and start the bot again.
Prefix commands are not working.
Check Message Content Intent is enabled in the Developer Portal. Without it Discord sends the bot empty
message content, so it cannot see any prefix at all — the bot warns about this on start-up if it notices.
Then check /prefix show, and that prefix commands have not been switched off with /prefix disable.
Can I use only slash commands?
Yes — run /prefix disable in each server. Nothing else changes.
Do I really need all three privileged intents?
Message Content is required — prefix commands, automod, levelling and counting all read messages. Server Members is needed for welcome messages, auto roles and member counters. Presence is the one you can most comfortably leave off.
Can I switch off commands I do not want?
Yes, two ways. From the dashboard's Commands screen you can switch any command off for one server, or the bot
owner can switch it off everywhere — no code involved. Or delete a command file and it stops existing; there is no
registry to update and no imports to clean up. To drop a whole category, delete its folder under
src/commands/ and its entry in src/config/categories.ts.
What is the difference between npm run dev and npm start?
npm run dev runs from TypeScript source, restarts when you save, and reads .env.development if you have
one — so it can drive a separate test bot. npm start runs the compiled dist/ build against .env, which is
what you use in production. Run npm run build first.
Where should I host it?
Anywhere that runs a long-lived process — a VPS, a Raspberry Pi, Railway, Fly.io, Render, or a machine at home.
Serverless hosts such as Vercel, Netlify and Cloudflare Workers cannot run it: a Discord bot holds a gateway
connection open for its whole life. The bot needs no inbound ports unless you turn the dashboard on. Use
something like pm2, systemd or Docker's restart: unless-stopped so it comes back after a crash.
How do I update to a newer version?
git pull
npm ci
npm run buildYour .env and your database are untouched.
I accidentally leaked my token. What now?
Reset it straight away: Developer Portal → your application → Bot → Reset Token, then update your .env.
If a MongoDB password leaked, change it under Atlas → Database Access → Edit. Deleting the commit is not
enough — treat anything ever pushed to GitHub as public forever.
Can I use this for my own bot, or rename it?
Yes, with two conditions. It is licensed under Apache 2.0 with the Commons Clause, so you can use it, change it, rebrand it and run it for your own servers, but:
- Keep the credit. The
NOTICEfile names the original author, and it has to stay with any copy or fork you share. - Do not sell it. You may not charge for the bot, or for hosting, support or a service whose value comes mainly from it.
Rename it in the Discord Developer Portal and every surface follows — the bot reads its own name from Discord.
The colours and links live in src/config/theme.ts.
| Symptom | Fix |
|---|---|
Your .env file needs attention |
The message lists exactly which values are wrong. Optional ones can be left blank |
querySrv ECONNREFUSED |
Your DNS cannot resolve the Atlas address. Try another network, or use the non-SRV connection string |
MongoServerError: bad auth |
Wrong database password, or <password> was left in the connection string |
Maximum number of application commands reached |
You are over Discord's limit of 100. Group commands under a shared parent — the error explains how |
Used disallowed intents |
Turn the privileged intents on in the Developer Portal |
EBADENGINE during install |
Your Node is older than 24.11. Run nvm use |
| Cards render with no text on them | You are in a container without fonts — use this repository's Dockerfile |
Sign in to confirm you're not a bot |
YouTube is refusing the host. Update yt-dlp, or set /play to SoundCloud only |
Still stuck? Ask in Discord or open an issue.
Everything written down lives in docs/, which has an index pointing at the right file for
what you are doing.
| Document | For |
|---|---|
📋 docs/commands.md |
Every command, subcommand and alias, generated from code |
🐳 docs/hosting.md |
Docker, compose, Railway, and which hosts cannot work |
🤝 docs/contributing.md |
Branches, commits and what CI checks |
🔒 docs/security.md |
Reporting a vulnerability, and how data is handled |
🖥️ docs/dashboard/guide.md |
Working on the web dashboard |
🧭 AGENTS.md |
The conventions every change here follows |
Contributions are very welcome, including from first-timers. Work goes on a testify/<type>-<nn> branch cut from
develop, and pull requests go into develop; a release pull request takes develop to main, which is what
runs.
npm run check # typecheck, lint, format and tests — run this before pushing
npm run commit # guided commit message in the project's formatdocs/contributing.md has the full guide. New logic needs a test — the suite has an 80%
coverage floor and the pre-push hook enforces it.
Thank you to everybody who has contributed to Testify.
Join us on Discord for help, questions, or just to say hello. If Testify has been useful to you, a coffee keeps it going.
Copyright 2026 Kkermit (Kkermit on Discord, Kkkermit on GitHub).
Released under the Apache License 2.0 with the Commons Clause License
Condition v1.0. You may use, modify and share it, but you may not sell it, and the attribution in
NOTICE must be kept in every copy and derived work.





















