A Discord bot that plays music, rolls RPG dice and answers for your server. The entire brand — name, colors, logo — comes out of a single file.
Swap the brand · Commands · Run it · Under the hood · License
A Discord bot in Node.js, built on discord.js v14 and Lavalink v4, that
does three things: plays music (YouTube, SoundCloud, Spotify), rolls RPG dice in
tabletop notation (2d6+3, advantage, initiative) and reports latency and
server state.
What sets it apart from any other music bot is that it was built to be
handed to different clients. Changing the visual identity doesn't mean
hunting for strings inside commands: it's one file, or not even that — just
variables in .env.
The same installation serves different brands. No fork per client.
# .env — and that's all
BRAND_NAME=ACME Bot
BRAND_URL=https://acme.com
BRAND_COLOR=#B8860Bwithout .env with the .env above
────────────────── ──────────────────
NerdBot ACME Bot
#7C3AED (NerdResolve violet) #B8860B (ACME gold)
What changes: the color and footer of every embed, the /help and
/status titles, the /roll header and the identifier sent to the GIF API.
BRAND is the identity and carries most of the replies. The other three are
semantic — they keep their meaning under any brand, which is why they rarely
change from client to client.
| Token | Default | Where it shows up |
|---|---|---|
BRAND_COLOR |
#7C3AED |
Ordinary replies: queue, help, rolls, initiative |
BRAND_COLOR_SUCCESS |
#22C55E |
Now playing, session ended, /status |
BRAND_COLOR_WARNING |
#F59E0B |
Warnings: no active session, nothing queued |
BRAND_COLOR_ERROR |
#EF4444 |
Command and playback failures |
An invalid color doesn't take the bot down: it warns in the log and falls back to the default.
Replace the two files, keeping the names — or point somewhere else with
BRAND_LOGO and BRAND_BANNER.
| File | What for |
|---|---|
assets/logo.webp |
The mark, used as the embed thumbnail |
assets/banner.webp |
The wide artwork in this README |
The vector sources live in docs/brand/. npm run check fails
if a referenced file doesn't exist.
Rather edit than configure?
src/config/brand.jsis the first file a fork opens: it names and colors the entire product, and carries the step-by-step at the top. Once you've swapped it,grep -ri nerdbot src/should only find that file.
Every one of them exists as a slash command (/play) and as a prefix command
(!play).
| Command | What it does |
|---|---|
/play |
Plays or queues a track from YouTube, SoundCloud or Spotify |
/pause · /resume |
Pauses and resumes |
/skip |
Skips the current track |
/stop |
Stops and clears the queue |
/queue |
Shows the queue, paginated 10 at a time |
/disconnect |
Leaves the voice channel |
| Command | What it does |
|---|---|
/roll |
Rolls in tabletop notation: 2d6+3, 4d6kh3, d20 |
/initiative |
Opens an initiative session, or rolls yours |
/end |
Ends the session and shows the turn order |
Crits and critical failures get a GIF, if GIF_API_KEY is configured.
| Command | What it does |
|---|---|
/ping |
Latency |
/status |
Uptime, memory and state |
/help |
Lists everything, assembled from the loaded commands |
You need your own Discord application — it's free, at discord.com/developers.
git clone https://github.com/nerdresolve/NerdBot.git
cd NerdBot
npm install
cp .env.example .env # fill in DISCORD_TOKEN, DISCORD_CLIENT_ID, DISCORD_GUILD_IDLavalink comes up in a container, with no Java to install and no .jar to
download.
docker compose up -d # Lavalink on 127.0.0.1:2333
npm run deploy # registers the commands on your server
npm startOr in one go, which also installs dependencies and waits for Lavalink to become healthy:
./start.sh # Linux and macOS
start.bat # WindowsDownload Lavalink v4 and
the YouTube plugin. On Windows, start.bat looks in C:\Lavalink; point it
elsewhere with set LAVALINK_HOME=D:\Lavalink. On any system:
java -jar Lavalink.jarLavalink listens only on 127.0.0.1 under Compose: it should never be exposed
to the internet.
npm run checkThree gates, all offline and token-free: syntax across all of src/, the brand
(valid colors, images present) and a sweep that fails the build if there's a
credential in a tracked file. It's the same command CI runs.
src/
config/
brand.js the brand: name, colors, files. What a fork edits
constants.js dice and queue limits; re-exports the brand colors
environment.js reads and validates .env, reporting what stayed off
commands/ one file per command, loaded by scanning the folder
events/ Discord handlers: interaction, message, voice, error
handlers/ discover and register commands and events
music/ Lavalink, the queue and Spotify link resolution
dice/ initiative session and roll presentation
utils/ embeds, logging, dice parser, GIFs
lavalink/ application.yml, mounted into the container
docs/brand/ the brand vectors
A new command is a new file in src/commands/: the commandHandler scans the
folder and registers whatever it finds, and /help builds itself from what was
loaded. There's no central list to keep updated.
| Decision | Why |
|---|---|
| The brand in one file, overridden by the environment | The file serves the fork; the environment serves operations. With both layers, the same image serves different clients without a rebuild — and without anyone editing a command to change a color. |
| Semantic colors, not decorative ones | SUCCESS/WARNING/ERROR mean the same thing under any brand. Only BRAND changes per client, so rebranding means supplying one color, not four. |
| Lavalink via Docker | The previous version required Java and a .jar in C:\Lavalink: it didn't run outside Windows. Compose pins the version and the plugin, and binds the port to 127.0.0.1. |
Secret sweep in check |
The repository is public. A check that only runs during human review fails the first time someone is in a hurry; this one fails CI. |
| The GIF key is masked in the log | Klipy's API takes the key in the URL path. Without the masking, a network failure would print it in full to the log. |
No .env ever enters the repository; .env.example is the template, with every
variable empty and commented.
| Group | Required | Without it |
|---|---|---|
DISCORD_* |
Yes | The bot doesn't start |
BRAND_* |
No | Uses the NerdResolve identity |
LAVALINK_* |
For music | The music commands stay off, everything else works |
SPOTIFY_* |
No | Spotify links don't resolve; YouTube and SoundCloud carry on |
GIF_API_KEY |
No | /roll replies without a GIF |
On startup, the bot says in the log what stayed off and why.
- No automated tests.
npm run checkvalidates syntax, brand and secrets, but there's no unit test for the dice parser — which is the part with real rules (4d6kh3, advantage, exploding dice) and the one that would most deserve one. - The queue lives in memory. Restarting the bot empties the queue and ends any open initiative sessions.
- A single Lavalink node. There's no failover: if the node goes down, the
music stops until
healthCheckreconnects. start.batdoesn't download Lavalink. It checks and warns; anyone not using Docker downloads the.jarand the plugin by hand.
MIT. See LICENSE. Use it, modify it, redistribute it, including commercially — including with your brand in place of ours, which is exactly what the project was built to allow.
The NerdResolve identity (name, glasses, violet) belongs to NerdResolve: it ships as the default so the bot has a face when cloned, not so it can be used as if it were yours. Swap it out — it's one command.
NerdBot, built by NerdResolve