Build a Discord Moderation Bot
Auto-scan URLs in Discord messages and warn or delete malicious links using the ScamVerify™ API.
This tutorial walks you through building a Discord bot that automatically scans every URL posted in your server, warns members about suspicious links, and auto-deletes confirmed scam URLs. By the end, you will have a working moderation bot that protects your community in real time.
Prerequisites
- Node.js 18 or later
- A ScamVerify™ API key (get one at scamverify.ai/settings/api)
- A Discord server where you have admin permissions
- Basic familiarity with discord.js
What You Will Build
A Discord bot that:
- Monitors all messages for URLs
- Calls the ScamVerify™ URL API to check each link
- Sends a warning embed for medium and high-risk links
- Auto-deletes confirmed scam links and notifies the user
- Logs all actions to a moderation channel
Create the Discord bot
Go to the Discord Developer Portal and click New Application. Name it "ScamVerify Guard".
Under Bot, click Add Bot and copy the bot token. Enable these Privileged Gateway Intents:
- Message Content Intent (required to read message text)
Under OAuth2 > URL Generator, select the bot scope and these permissions:
- Send Messages
- Manage Messages (for auto-deleting scam links)
- Embed Links
- Read Message History
Copy the generated invite URL and add the bot to your server.
Set up the project
mkdir discord-scambot && cd discord-scambot
npm init -y
npm install discord.js dotenvCreate a .env file:
SCAMVERIFY_API_KEY=sv_live_your_key_here
DISCORD_BOT_TOKEN=your-discord-bot-token
MOD_CHANNEL_ID=your-moderation-channel-idTo get the moderation channel ID, enable Developer Mode in Discord settings, then right-click the channel and select Copy Channel ID.
Create the ScamVerify™ client
// scamverify.js
class ScamVerifyClient {
constructor(apiKey) {
if (!apiKey || !apiKey.startsWith('sv_')) {
throw new Error('Invalid API key. Keys must start with sv_live_ or sv_test_');
}
this.apiKey = apiKey;
this.baseUrl = 'https://scamverify.ai/api/v1';
}
async lookupUrl(url) {
const response = await fetch(`${this.baseUrl}/url/lookup`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url }),
});
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || '60';
throw new Error(`Rate limited. Retry after ${retryAfter}s`);
}
if (!response.ok) {
throw new Error(`URL lookup failed: ${response.status}`);
}
return response.json();
}
}
module.exports = { ScamVerifyClient };Build the embed formatter
Create rich embeds that display scan results with color-coded severity.
// embeds.js
const { EmbedBuilder } = require('discord.js');
const VERDICT_COLORS = {
safe: 0x36a64f,
low_risk: 0x2196f3,
medium_risk: 0xff9800,
high_risk: 0xf44336,
scam: 0xd32f2f,
};
const VERDICT_ICONS = {
safe: '\u2705',
low_risk: '\ud83d\udfe2',
medium_risk: '\u26a0\ufe0f',
high_risk: '\ud83d\udea8',
scam: '\ud83d\udeab',
};
function createWarningEmbed(url, result) {
const icon = VERDICT_ICONS[result.verdict] || '\u2753';
const color = VERDICT_COLORS[result.verdict] || 0x808080;
const verdictLabel = result.verdict.replace('_', ' ').toUpperCase();
return new EmbedBuilder()
.setColor(color)
.setTitle(`${icon} URL Scan Result`)
.addFields(
{ name: 'URL', value: `\`${url}\``, inline: false },
{ name: 'Verdict', value: verdictLabel, inline: true },
{ name: 'Risk Score', value: `${result.risk_score}/100`, inline: true },
{ name: 'Confidence', value: `${Math.round(result.confidence * 100)}%`, inline: true },
)
.setDescription(result.explanation)
.setFooter({ text: 'Powered by ScamVerify\u2122' })
.setTimestamp();
}
function createDeletionEmbed(url, result, author) {
return new EmbedBuilder()
.setColor(0xd32f2f)
.setTitle('\ud83d\udeab Scam Link Removed')
.addFields(
{ name: 'URL', value: `\`${url}\``, inline: false },
{ name: 'Risk Score', value: `${result.risk_score}/100`, inline: true },
{ name: 'Posted By', value: `<@${author.id}>`, inline: true },
)
.setDescription(result.explanation)
.setFooter({ text: 'Powered by ScamVerify\u2122' })
.setTimestamp();
}
function createModLogEmbed(url, result, message) {
return new EmbedBuilder()
.setColor(VERDICT_COLORS[result.verdict] || 0x808080)
.setTitle('URL Scan Log')
.addFields(
{ name: 'URL', value: `\`${url}\``, inline: false },
{ name: 'Verdict', value: result.verdict.replace('_', ' ').toUpperCase(), inline: true },
{ name: 'Risk Score', value: `${result.risk_score}/100`, inline: true },
{ name: 'Channel', value: `<#${message.channel.id}>`, inline: true },
{ name: 'Author', value: `<@${message.author.id}>`, inline: true },
)
.setTimestamp();
}
module.exports = { createWarningEmbed, createDeletionEmbed, createModLogEmbed };Build the bot
Create the main bot file that listens for messages, scans URLs, and takes action based on the verdict.
// bot.js
require('dotenv').config();
const { Client, GatewayIntentBits, Events } = require('discord.js');
const { ScamVerifyClient } = require('./scamverify');
const {
createWarningEmbed,
createDeletionEmbed,
createModLogEmbed,
} = require('./embeds');
const scamverify = new ScamVerifyClient(process.env.SCAMVERIFY_API_KEY);
const MOD_CHANNEL_ID = process.env.MOD_CHANNEL_ID;
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
// Extract URLs from message text
function extractUrls(text) {
const urlRegex = /https?:\/\/[^\s<>]+/gi;
return [...new Set(text.match(urlRegex) || [])];
}
client.on(Events.ClientReady, () => {
console.log(`ScamVerify Guard is online as ${client.user.tag}`);
});
client.on(Events.MessageCreate, async (message) => {
// Ignore bots and DMs
if (message.author.bot) return;
if (!message.guild) return;
const urls = extractUrls(message.content);
if (urls.length === 0) return;
for (const url of urls) {
try {
const result = await scamverify.lookupUrl(url);
// Log every scan to the moderation channel
const modChannel = await client.channels.fetch(MOD_CHANNEL_ID);
if (modChannel) {
await modChannel.send({ embeds: [createModLogEmbed(url, result, message)] });
}
// Take action based on verdict
if (result.verdict === 'scam' || result.verdict === 'high_risk') {
// Delete the message
try {
await message.delete();
} catch (deleteErr) {
console.error('Could not delete message:', deleteErr.message);
}
// Notify the channel
const deletionEmbed = createDeletionEmbed(url, result, message.author);
await message.channel.send({ embeds: [deletionEmbed] });
// DM the user
try {
await message.author.send({
content: `Your message in **${message.guild.name}** was removed because it contained a link flagged as **${result.verdict.replace('_', ' ')}** by ScamVerify\u2122.\n\nURL: \`${url}\`\nRisk Score: ${result.risk_score}/100\n\nIf you believe this is a mistake, contact a server moderator.`,
});
} catch (dmErr) {
// User may have DMs disabled
console.log('Could not DM user:', dmErr.message);
}
} else if (result.verdict === 'medium_risk') {
// Warn but do not delete
const warningEmbed = createWarningEmbed(url, result);
await message.reply({ embeds: [warningEmbed] });
}
// safe and low_risk: no action (logged to mod channel only)
} catch (err) {
console.error(`Error scanning ${url}:`, err.message);
}
}
});
client.login(process.env.DISCORD_BOT_TOKEN);Run and test
Start the bot:
node bot.jsYou should see ScamVerify Guard is online as ScamVerify Guard#1234 in the console. Post a URL in any channel where the bot is present to test it.
Try these test cases:
- A known safe URL like
https://google.com(should trigger no visible action) - A suspicious URL (should show a warning embed for medium_risk)
- The bot should log every scan to your moderation channel regardless of verdict
The bot processes URLs sequentially. If someone posts a message with many URLs, there may be a short delay. For high-traffic servers, consider adding a queue to handle scans in the background.
Add a slash command for on-demand checks
Register a /scanurl slash command so users can manually check any URL.
// Add this to bot.js after the client.on(Events.ClientReady) block
const { SlashCommandBuilder } = require('discord.js');
client.on(Events.ClientReady, async () => {
const command = new SlashCommandBuilder()
.setName('scanurl')
.setDescription('Check a URL for scam or malware risk')
.addStringOption(option =>
option.setName('url').setDescription('The URL to scan').setRequired(true)
);
await client.application.commands.create(command);
console.log('Slash command /scanurl registered');
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName !== 'scanurl') return;
const url = interaction.options.getString('url');
await interaction.deferReply();
try {
const result = await scamverify.lookupUrl(url);
const embed = createWarningEmbed(url, result);
await interaction.editReply({ embeds: [embed] });
} catch (err) {
await interaction.editReply({ content: `Scan failed: ${err.message}` });
}
});Next Steps
- Add phone number scanning using
extractPhoneNumbersfrom the Slack bot tutorial - Use the batch endpoint to scan multiple URLs at once
- Add a
/scanphoneslash command for phone number lookups - Set up auto-ban for repeat offenders who post multiple scam links