A Node.js Discord bot that uses OCR and image hashing to analyze attached images and images shown in Discord link previews. When an image contains a withdrawal keyword and a payout keyword, the bot:
- Deletes the message.
- Applies a timeout to the author.
- Sends a report to the moderation channel.
Invite the bot to your server:
The OCR result must contain both of these keyword groups:
- Withdrawal group:
Withdrawal. - Payout group:
Success,Succeeded,Successful,Successfully, orUSDTin uppercase.
The keywords:
- Are case-insensitive.
- Can appear in any order.
- Can appear on different lines or far apart in the image.
- Must appear as complete words.
The OCR also checks the domains listed in
blocked-domains.json. A match in that list is
sufficient to detect the image at every paranoia level, including low. At
that level the bot runs only the low-cost OCR pass for this check. The same
list is also used for blocked domains in regular message text, with or without
a URL protocol.
You can tune the detection sensitivity per server with /setup paranoia:
low- exact visual hash match or a domain from the blocked domain list.medium- visual hash match or OCR text containingWithdrawal,Succeeded, andUSDT.high- visual hash match or OCR text containingWithdrawaland eitherSucceededorUSDT.
The default paranoia level is high.
The blocked domain list is copied into the Docker image. Rebuild and redeploy the image after changing it.
The bot includes an anti-raid protection that detects when a user sends the same message in multiple text channels within less than one minute. When a raid is detected, the bot:
- Deletes the repeated messages in the affected channels.
- Applies the configured timeout to the user.
- Sends an alert to the moderation channel, including the deleted message.
Anti-raid is enabled by default at the high level. Configure it per server
with /setup anti-raid, for example:
/setup anti-raid enabled:true level:high
Spam message protection is enabled by default. A matching spam message is
deleted and its author is timed out. Known spammer IDs can be added to
spam-users.json; messages from those users are handled the same way even if
their message does not match a configured spam phrase.
Available levels are:
high- triggers when the same message is sent in 3 or more channels.medium- triggers when the same message is sent in 4 or more channels.low- triggers only when the same message is sent in every text channel in the server.
The bot can also block text messages or embed descriptions that contain an
entry from the list in spam-messages.json. Matching ignores capitalization,
accents, and repeated spaces. Matching messages are deleted, the author is
timed out, and a moderation alert is sent when a moderation channel is
configured.
Spam protection is enabled by default. Server administrators can toggle it with:
/setup spam messages enabled:false
/setup spam messages enabled:true
Add or remove entries directly in the messages array in spam-messages.json,
one message or phrase per JSON array entry. Multi-line messages can be pasted
with their line breaks as-is; the bot accepts and normalizes those line breaks
when it loads the file. The descriptionPatterns entries are regular
expressions for promotional descriptions. They are ignored when a
descriptionExclusions expression detects a negation such as no NSFW or
NSFW not allowed. Its status is also shown by /setup status.
Use enabled:false to disable the protection. The anti-raid configuration is
stored separately for each server and is shown by /setup status.
The bot blocks these links by default. Administrators can toggle the protection per server:
/setup blocked-link enabled:false
/setup blocked-link enabled:true
https://surveybuilder.io/c/capture/MHNKQThUS3A
https://www.upwork.com/freelancers/~011c10fca307bf0e02?mp_source=share
https://agentrouter.org/register?aff=QaiK
También se pueden bloquear dominios completos, incluidos sus subdominios. El
listado global está en blocked-domains.json. Incluye
zangi.com y los dominios maliciosos detectados por OCR en imágenes.
La detección normaliza enlaces ofuscados como https\://example[.]com, tanto
para los dominios bloqueados como para los enlaces de invitación de Discord.
Moderators can report a repeated message by replying to it with:
`!spamreport`
The bot applies a timeout and removes matching messages from the user across the server's accessible text channels.
Administrators and moderators can view statistics for the current server or combined statistics across all servers where the bot is installed:
/spam analytics
/spam global
The bot stores a global aggregate and separate counters for each server ID.
Both include detections, manual spam reports, and feedback votes. They do not
store user, message, image, URL, or OCR information. Historical global counters
collected before per-server statistics were introduced remain in the global
totals and cannot be attributed to an individual server.
SEND_FEEDBACK=false disables both collection and display of these statistics.
The bot can resolve Discord invitation links and compare the destination server ID with a blocklist. This protection is enabled by default. When a message contains an invitation to a blocked server, the bot deletes the message, applies the configured timeout to the author, and sends a moderation alert. A new invite link to the same server is still blocked because matching uses the destination server ID, not the invite code.
The same check is also applied to invitation links recognized by OCR inside an
image. For example, an image containing discord.gg/example is parsed, the
invite is resolved through Discord, and its destination server ID is compared
with the same global and per-server blocklists. The moderation alert includes
the recognized text, invite code, and resolved server ID.
The global blocklist is malicious-servers.json at the repository root. Add one Discord server ID per JSON array entry:
[
"123456789012345678",
"987654321098765432"
]The file is copied into the Docker image. Rebuild/redeploy the image after
changing it. The /setup malicious-servers add command can also add a local
entry for the current server without modifying the repository file.
Manage it with:
/setup malicious-servers protection enabled:true
/setup malicious-servers add server-id:123456789012345678
/setup malicious-servers remove server-id:123456789012345678
/setup malicious-servers list
The enabled/disabled state and local entries are stored separately for each
server and are shown in /setup status. Expired, deleted, or otherwise
unavailable invites cannot be resolved and are left untouched.
The bot can also inspect the public metadata returned for an invitation's
destination server. It compares the server name, description, tags, and
features against the keywords in
nsfw-server-keywords.json, including terms such as
NSFW, +18, 18+, 🔞, squirt, sex, porn, xxx, adult, hentai,
and onlyfans. It also blocks Discord servers marked as age-restricted by
Discord, even when the invite response does not include readable server text.
Matching is case-insensitive and ignores accents.
This protection is enabled by default and can be disabled per server with:
/setup nsfw-servers protection enabled:false
/setup nsfw-servers protection enabled:true
The keyword file is copied into the Docker image, so rebuild/redeploy the image after changing it. If the invitation has expired, is unavailable, or does not expose a server name, the NSFW-name check is skipped.
Server admins can also:
- Set a custom timeout with
/setup timeout. - Exclude roles from detection with
/setup excluded-role add,/setup excluded-role remove, and/setup excluded-role list. - Toggle whether server administrators are excluded with
/setup excluded-administrators enableand/setup excluded-administrators disable. - Enable, disable, or change the sensitivity of anti-raid protection with
/setup anti-raid.
For example, an image containing Withdrawal near the top and Succeeded
near the bottom is considered a match.
The bot scans:
- Images uploaded directly as Discord attachments.
- Images and thumbnails displayed in Discord embeds generated from links.
- Images and embeds contained in forwarded message snapshots.
- Every image in a multi-image message.
- Images sent inside threads, including recently created threads owned by the sender.
Each image is evaluated independently. If any single image contains a matching withdrawal keyword and payout keyword, the entire outer message is deleted and the user who sent or forwarded it is timed out when Discord allows it.
If the match is an OCR, visual-hash, or known source-channel match and the message is from the author of a thread created within the last 10 minutes, the entire thread is deleted along with the offending message.
You can also add a public easter egg response by dropping meme images into
easter-egg photos/. The bot builds hash signatures from that folder and, when
one of those images is detected, replies in the channel with Jajaja, piqué.
in Spanish or Hahaha I got it 😜 in English instead of moderating the
message.
Discord may generate a link preview shortly after the original message is created. The bot handles both new-message and message-update events so those delayed previews are scanned as well. A plain link that Discord does not convert into an image embed is not downloaded automatically.
- Node.js 20 or newer.
When generating the bot invitation in the Discord Developer Portal, select:
bot— adds the bot account to the server.applications.commands— installs the/setupslash command. Discord includes this scope by default when thebotscope is selected, but it should remain enabled.
Grant the bot these permissions:
| Permission | Where it is required | Purpose |
|---|---|---|
| View Channels | Monitored channels and the moderation channel | Receives new messages and accesses the configured moderation channel. |
| Manage Messages | Monitored channels | Deletes messages containing a matching image or raids. |
| Moderate Members | Server-wide | Applies the configured timeout to the message author. |
| Send Messages | Moderation channel | Sends moderation alerts. |
| Embed Links | Moderation channel | Sends the formatted moderation report embed. |
The combined permission integer for these five permissions is
1099511655424. It can be entered in an OAuth2 bot invitation as the
permissions value.
The bot does not require Administrator, Ban Members, or Kick Members. Granting Administrator is not recommended.
The bot's highest role must be above the roles of users it needs to time out. Discord does not allow the bot to time out:
- The server owner.
- Members with the Administrator permission.
- Members whose highest role is equal to or above the bot's highest role.
If the bot cannot apply a timeout because of role hierarchy or permissions, it still attempts to delete the message and records the timeout failure in the moderation alert.
By default, messages from server administrators and the server owner are ignored
completely. Use /setup excluded-administrators disable to scan them too. When
administrator exclusion is disabled, Discord may still prevent the timeout, but
the bot can still delete matching messages when it has permission.
Members with an excluded role are also ignored completely. These exclusions apply to image moderation, anti-raid protection, and spam message protection.
The person running /setup moderation-channel, /setup paranoia, or
/setup status must have the Manage Server permission. The bot itself
does not need Manage Server.
In the Discord Developer Portal, open the application, go to Bot > Privileged Gateway Intents, and enable Message Content Intent. Discord considers attachments part of message content; without this intent, the bot receives an empty attachment collection.
pnpm install
cp .env.example .envSet DISCORD_TOKEN in .env.
pnpm startThe first OCR run may download the English language data and take longer. The worker is reused for subsequent images.
Server administrators with Manage Server can configure the bot with:
/setup moderation-channelto choose where alerts are sent./setup paranoiato set the per-server detection sensitivity./setup timeoutto set the per-server timeout./setup excluded-role ...to manage ignored roles./setup excluded-administrators ...to include or exclude server administrators./setup anti-raidto enable or configure anti-raid protection./setup bot-detection enabled:trueto include messages sent by bots in detection and moderation. This is disabled by default./setup statusto review the current configuration.
Store the meme images you want the bot to recognize in easter-egg photos/.
Run pnpm build:easter-egg-photos to regenerate
generated/easter-egg-photo-manifest.json.
The included image uses node:22-alpine, installs production dependencies
only, and runs the bot as the non-root node user.
Create the environment file before starting the container:
cp .env.example .envSet DISCORD_TOKEN in .env, then build and start the bot:
docker compose up -d --buildView its logs:
docker compose logs -f botStop the bot:
docker compose downThe Compose configuration creates two named volumes:
bot-datastores the per-server moderation channel configuration.bot-dataalso stores the anonymous aggregate analytics counters whenSEND_FEEDBACKis enabled.ocr-cachestores the downloaded Tesseract English language data.
Both volumes survive container recreation and image upgrades. Running
docker compose down -v deletes them, including the saved moderation channel
configuration.
No ports need to be exposed or published. The bot connects outward to the Discord Gateway over HTTPS and WebSocket connections. It does not run an HTTP server or accept inbound network traffic.
The relevant Compose configuration intentionally contains no ports section:
services:
bot:
build: .
restart: unless-stopped
env_file:
- .env
volumes:
- bot-data:/app/data
- ocr-cache:/home/node/.cache/ppu-paddle-ocrThe workflow in .github/workflows/deploy.yml runs on every push to main
and can also be started manually.
It performs these steps:
- Installs dependencies and runs the test suite.
- Builds the Docker image.
- Publishes
latestand commit-specific tags to GitHub Container Registry. - Optionally connects to a server over SSH, pulls the exact commit image, and restarts the bot with Docker Compose.
The published image name is:
ghcr.io/dh-555/spam-guard-bot
Install Docker Engine and the Docker Compose plugin on the destination server. Create the deployment directory and its environment file once:
sudo mkdir -p /opt/d5-spam-guard-bot
sudo chown "$USER":"$USER" /opt/d5-spam-guard-bot
cd /opt/d5-spam-guard-bot
nano .envThe server-side .env must contain at least:
DISCORD_TOKEN=your_real_bot_tokenGitHub Actions deliberately does not overwrite this file.
The SSH user must be able to run docker and docker compose without an
interactive password prompt. No inbound application ports are required; only
SSH access is needed for deployment.
Create these variables under Settings > Secrets and variables > Actions:
| Variable | Location | Required | Example |
|---|---|---|---|
ENABLE_DEPLOY |
Repository variable | Yes | true |
DEPLOY_PATH |
Repository or production environment variable |
No | /opt/d5-spam-guard-bot |
DEPLOY_PORT |
Repository or production environment variable |
No | 22 |
If ENABLE_DEPLOY is not exactly true, the workflow still tests and
publishes the image but skips the SSH deployment.
Create these secrets:
| Secret | Purpose |
|---|---|
DEPLOY_HOST |
Server hostname or IP address. |
DEPLOY_USER |
SSH username. |
DEPLOY_SSH_KEY |
Private SSH key used only for deployment. |
GHCR_USERNAME |
GitHub username used by the server to pull the image. |
GHCR_PULL_TOKEN |
Personal access token (classic) with read:packages. |
The corresponding public SSH key must be added to
~/.ssh/authorized_keys for DEPLOY_USER.
For a private GHCR package, GHCR_PULL_TOKEN needs permission to read
packages. If the package is made public, server-side registry authentication
can be removed from the workflow.
The docs/ directory contains a static legal site with:
- Privacy Policy:
docs/privacy.html - Terms of Service:
docs/terms.html - Legal landing page:
docs/index.html
To publish it with GitHub Pages:
- Open the repository on GitHub.
- Go to Settings > Pages.
- Under Build and deployment, select Deploy from a branch.
- Select the
mainbranch and the/docsfolder. - Save the configuration.
The expected URLs are:
https://dh-555.github.io/Anti-Mr-Scam-bot/
https://dh-555.github.io/Anti-Mr-Scam-bot/privacy.html
https://dh-555.github.io/Anti-Mr-Scam-bot/terms.html
The included policies are project-specific templates, not legal advice. The person or organization operating the Bot should review them for the applicable jurisdiction and deployment practices.
After starting the bot, a server administrator can use Discord's native slash command interface:
/setup moderation-channel channel:#moderation
Discord displays a channel picker for the channel option. The selection is
saved separately for each server and persists across restarts in
data/settings.json.
Use /setup status to view the currently configured moderation channel. The
commands require the Manage Server permission and their responses are only
visible to the administrator who runs them.
Messages sent by bots are ignored by default. To include them in this server's
detection and moderation flow, use /setup bot-detection enabled:true; matching
messages are handled like any other detected message and deleted. Use
/setup bot-detection enabled:false to disable it again.
If a moderation channel has not been configured yet, the bot still scans and
moderates matching images. In that case, it deletes the message, applies the
timeout when possible, and posts a short notice in the same channel telling
admins to configure /setup moderation-channel for full alerts and details.
| Variable | Required | Default |
|---|---|---|
DISCORD_TOKEN |
Yes | — |
TIMEOUT_MINUTES |
No | 1440 (24 hours) |
MAX_IMAGE_SIZE_MB |
No | 8 |
MAX_IMAGE_PIXELS |
No | 16000000 |
IMAGE_DOWNLOAD_TIMEOUT_MS |
No | 15000 |
SEND_FEEDBACK |
No | true |
VISUAL_REFERENCE_MANIFEST_PATH |
No | generated/visual-reference-manifest.json |
VISUAL_MATCH_THRESHOLD |
No | 6 |
Set SEND_FEEDBACK=false to disable sending detection feedback and manual spam
reports to the central feedback channel. Moderation alerts in each server and
the moderation actions themselves continue to work normally.
OCR uses the PP-OCRv6 Small model through ppu-paddle-ocr and caches its ONNX
models under /home/node/.cache/ppu-paddle-ocr in Docker. The compose files
persist that directory in the ocr-cache volume so models are downloaded only
on the first run.
Reference images live in the repository under visual-references/. The build
step hashes them with a perceptual hash and writes the manifest to
generated/visual-reference-manifest.json. At runtime, the bot reads only that
manifest. Lower thresholds are stricter; 0 means exact hash equality.
To regenerate the manifest locally:
pnpm build:visual-referencesWith the current reference folder, startup logs should include:
[Visual matching] Loaded 19 reference hash(es).
Discord limits timeouts to a maximum of 28 days.
pnpm test