A Bun + TypeScript bot for Discord that uses AI to automatically moderate messages and remove spam or scam content from your server. It is built with Effect 4 (release candidate, pinned exactly): configuration, the moderation model and logging are services, model responses are schema-decoded rather than cast, and the moderation decision logic is a pure module (src/moderation.ts) covered by tests.
- Automatically monitors Discord channels for spam and scam messages
- Scores every message with Cloudflare Workers AI's Clef model and extracts typed evidence (links, invites, handles, promised amounts) from flagged messages with Clairic
- Considers previous channel messages for context when moderating
- Can be configured to monitor specific channels or the entire server
- Allows excluding users with specific roles from moderation
- Automatically times out users who post spam/scam content (configurable duration)
- Sends detailed notifications to a designated channel for moderation actions
- Provides detailed logging of moderation activities
- Runs as a systemd service on Ubuntu for 24/7 operation
- Bun 1.2 or higher
- Discord Bot Token (with appropriate permissions)
- Cloudflare account ID and an API token allowed to run Workers AI models
- Ubuntu server (for systemd service setup)
-
Clone this repository:
git clone https://github.com/EkuboProtocol/discord-mod-bot.git cd discord-mod-bot -
Install dependencies:
bun install
-
Create a
.envfile based on the example:cp .env.example .env nano .env # Edit with your actual credentials -
Configure your environment variables in the
.envfile:DISCORD_TOKEN=your_discord_bot_token DISCORD_SERVER_ID=your_server_id CLOUDFLARE_ACCOUNT_ID=your_cloudflare_account_id CLOUDFLARE_API_TOKEN=your_workers_ai_token # Add more configuration as needed
Start the bot directly with Bun:
bun run src/index.tsTypeScript is executed as-is; there is no build step. To run the checks the way CI does:
bun run format:check
bun run lint
bun run typecheck
bun testBefore connecting to Discord, the bot asks Clef about one benign synthetic message
using the configured account and token and the normal request. Startup requires a
response that decodes within 30 seconds and scores the message as clean
(p(spam) < 0.5). An API error, timeout, success: false, a response that does not
decode, or a benign message scored as spam fails startup with exit code 1. This
check never posts a Discord message or takes a moderation action. It incurs one
normal API request per startup. Errors processing real messages still fail open.
After Discord emits clientReady, startup fetches the server's roles, channels,
active threads, and the bot member to check effective permissions (including channel
overwrites). It verifies bans, timeouts when enabled, moderation notices and context,
report embeds, and the report button's purge access across regular text channels.
Invalid configured channels or missing permissions fail startup with an actionable
error before moderation handlers are enabled. This check is read-only.
Role hierarchy limitations are logged for non-excluded roles at or above the bot. The server owner and administrators retain Discord's moderation protections; startup cannot guarantee actions against every member. Channel/role changes after startup and threads inaccessible to the bot still require operational attention.
The bot supports the following command line arguments:
Options:
-t, --token Discord bot token [string]
-s, --server Discord server ID [string]
-c, --channels Comma-separated list of moderated channel IDs
(default: all) [string]
-e, --excluded-roles Comma-separated list of role IDs to exclude from
moderation [string]
-x, --excluded-channels Comma-separated list of channel IDs to exclude from
moderation [string]
-w, --welcome-channel Channel ID of the welcome channel to exclude from
moderation [string]
-n, --notification-channel Channel ID to send notifications to [string]
--context-messages Number of previous messages for context
[string] [default: "5"]
-l, --log-level Log level (error, warn, info, debug)
[string] [default: "info"]
--cloudflare-account-id Cloudflare account ID that runs the Workers AI
moderation models [string]
--cloudflare-api-token Cloudflare API token allowed to run Workers AI
models [string]
-h, --help Show help [boolean]
Example:
bun run src/index.ts --token=your_token --server=your_server_id --channels=channel1,channel2 --excluded-roles=role1,role2-
Copy the service file to systemd:
sudo cp discord-mod-bot.service /etc/systemd/system/
-
Edit the service file to match your environment:
sudo nano /etc/systemd/system/discord-mod-bot.service
-
Reload systemd, enable and start the service:
sudo systemctl daemon-reload sudo systemctl enable discord-mod-bot sudo systemctl start discord-mod-bot -
Check the status:
sudo systemctl status discord-mod-bot
The repository includes a ready-to-use App Platform spec at .do/app.yaml that deploys the bot as a worker service.
- Install and authenticate
doctl:doctl auth init
- Review
.do/app.yamland update the GitHub repo/branch or worker sizing if needed. - Create the app:
doctl apps create --spec .do/app.yaml
The live spec is the source of truth, not this file.
deploy_on_pushonly rebuilds the source; it does not re-read.do/app.yaml. Changing the build or run command here has no effect until you rundoctl apps update(below), so the two can silently drift apart. To edit the live spec safely, start fromdoctl apps spec get <APP_ID>— it round-trips the encrypted secret values, whereas applying this file as-is would clear them.
- After the app is created, add the required environment variables (Discord token, server ID, Cloudflare account ID and token, etc.) under Settings → Environment Variables or by updating the spec and running:
doctl apps update <APP_ID> --spec .do/app.yaml
The spec lists the variables the worker expects:
DISCORD_TOKEN(secret) – Discord bot tokenDISCORD_SERVER_ID– ID of the Discord server to moderateCLOUDFLARE_ACCOUNT_ID– Cloudflare account that runs the Workers AI modelsCLOUDFLARE_API_TOKEN(secret) – Cloudflare API token allowed to run Workers AI modelsLOG_LEVEL– Logging verbosity (error,warn,info,debug)CONTEXT_MESSAGE_COUNT– Number of prior messages to send for contextTIMEOUT_DURATION– Timeout duration in minutes (set0to disable)MODERATED_CHANNELS,EXCLUDED_CHANNELS,EXCLUDED_ROLES,WELCOME_CHANNEL_ID,NOTIFICATION_CHANNEL_ID,IGNORED_PHRASES– Optional fine-tuning values; leave blank to use defaults
Remember to keep secret values (Discord and Cloudflare tokens) stored as Secret scope variables in App Platform so they are not committed to the repository.
The bot can be configured using environment variables or command line arguments. A command line argument takes precedence over the matching environment variable, which in turn takes precedence over the built-in default. Every setting has exactly one environment variable name and, where useful, one flag that sets the same thing; --help lists both.
Note:
LOG_LEVELandCONTEXT_MESSAGE_COUNTpreviously had no effect, because the corresponding flags always carried a default value and so always won. They are honoured now.
If a required setting is missing, the bot names it at startup along with both ways to supply it, rather than reporting a bare stack trace.
DISCORD_TOKEN: Your Discord bot tokenDISCORD_SERVER_ID: The ID of your Discord serverMODERATED_CHANNELS: Comma-separated list of channel IDs to moderate (leave empty to moderate all)EXCLUDED_CHANNELS: Comma-separated list of channel IDs to exclude from moderationWELCOME_CHANNEL_ID: Channel ID of the welcome channel to exclude from moderation (automated messages)EXCLUDED_ROLES: Comma-separated list of role IDs to exclude from moderationTIMEOUT_DURATION: Duration in minutes to timeout users when their message is deleted (default: 5, set to 0 to disable)CLOUDFLARE_ACCOUNT_ID: Cloudflare account that runs the Workers AI modelsCLOUDFLARE_API_TOKEN: Cloudflare API token allowed to run Workers AI modelsIGNORED_PHRASES: Comma-separated list of phrases to ignore (never sent to the moderation model)NOTIFICATION_CHANNEL_ID: Channel ID to send detailed moderation notificationsCONTEXT_MESSAGE_COUNT: Number of previous messages to include for context (default: 5)LOG_LEVEL: Logging level (error, warn, info, debug)
Logs are stored in the logs directory:
logs/combined.log: Contains all log messageslogs/error.log: Contains only error messages
Each moderated message, with the previous CONTEXT_MESSAGE_COUNT messages for
context, goes to Clef
with two questions: the probability that it is spam or a scam, and its severity
(high, medium, low or none). The thresholds are constants in
src/moderation.ts, changed by a release:
p(spam) < 0.8: no action.highwithp(high) ≥ 0.6: ban (timeout if the ban fails).highbelow that degrades tomedium.medium: delete and time out forTIMEOUT_DURATION.low, ornonedespitep(spam) ≥ 0.8: delete only.
Only a message that will be acted on is sent to Clairic, which extracts typed evidence: Discord invites, links, off-platform handles, promised amounts, profit cuts, impersonated parties and requests to DM. A field is used only when Clairic returns a value and its confidence clears a floor; invites and links are also found by deterministic checks that undo common obfuscation.
The reason is a template, e.g. High-severity spam/scam (p=0.97): promises $5,000; asks for 20% of profits; directs to Telegram. The public notice never
repeats a link, invite code or handle; the moderator report shows the typed
evidence and the model's scores.
If Workers AI fails, times out, or returns something that does not decode, the message is treated as clean. If Clairic alone fails, the action still happens, with less evidence.
The severity criteria cover:
- Messages containing suspicious links or asking users to open tickets
- Messages asking users to DM for support instead of using public channels
- Generic job-seeking messages that appear to be copy-pasted
- Messages asking who to contact for unspecified business/partnerships
- Messages promising rewards, giveaways, or airdrops that seem suspicious
- Impersonation of staff or team members
- Phishing attempts asking for personal information or wallet addresses
There are several ways to integrate this bot with Discord:
This is the most common method where you create a bot application in the Discord Developer Portal and add it to your server. This approach:
- Requires a separate bot user in your server
- Needs specific permissions to read/delete messages
- Is easy to set up through the Discord Developer Portal
For more advanced integrations with fewer permissions, you could modify this code to use:
- Discord Webhooks: For sending notifications without a full bot
- Discord Interactions: For slash commands (would require additional development)
While technically possible to use a user account token instead of a bot token, this approach:
- Violates Discord's Terms of Service
- Can result in account termination
- Is not supported by this code
If you already have a bot for your server, you could integrate this moderation code as a module in your existing bot framework to avoid adding another bot user to your server.
- Bot not starting: Check the logs for error messages
- Bot not detecting messages: Make sure the bot has the required permissions in Discord
- Workers AI errors: Verify the account ID and that the token can run Workers AI models
- Bot not running after system restart: Make sure the systemd service is enabled
- Context not being considered: Ensure the bot has permissions to read message history
MIT