discord-moderation
v0.1.0
Published
A discord moderation suite for discord.js bots with features like Anti Alt.
Downloads
147
Maintainers
Readme
discord-moderation
A modular, TypeScript-first moderation toolkit for Discord.js bots. v0.1.0 ships
AntiAltClient— a recommendation-only alt-account detector. It analyses incoming members and returns a scored risk result. You decide what to do with it.
Table of Contents
- Installation
- Quick Start
- How It Works
- Configuration
- Event System
- Result Shape
- Built-in Detectors
- Utility: buildEmbed
- Full Bot Example
- Roadmap — ModerationSuite
Installation
npm install discord-moderation
# discord.js v14 is a required peer dependency
npm install discord.jsNode.js ≥ 18 required (uses Unicode property escapes in regex).
Quick Start
import { Client, GatewayIntentBits, Events } from "discord.js";
import { AntiAltClient } from "discord-moderation";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers, // ← privileged intent, enable in Developer Portal
],
});
const antiAlt = new AntiAltClient();
client.on(Events.GuildMemberAdd, async (member) => {
const result = await antiAlt.run(member);
console.log(result.score); // 0–100
console.log(result.risk); // 'low' | 'medium' | 'high'
console.log(result.recommendedAction); // 'none' | 'warn' | 'kick' | 'softban' | 'ban'
console.log(result.shouldAct); // true when score ≥ shouldAct threshold
if (result.risk === "high") {
await member.kick("Potential alt account");
}
});
client.login("YOUR_TOKEN");How It Works
guildMemberAdd
│
▼
antiAlt.run(member)
│
├── UsernameDetector ──┐
├── AvatarDetector ──┼── concurrent Promise.allSettled()
└── AccountAgeDetector ──┘
│
weighted average
│
0–100 score
│
┌──────────┼──────────┐
low medium high
│ │ │
emit events emit events emit events
'result' 'result' 'result'
'lowRisk' 'mediumRisk' 'highRisk'
│
return AntiAltResultAll detectors run concurrently. If a detector throws, it returns a safe fallback (score 0) rather than crashing the analysis.
Configuration
All config is optional. Pass nothing and safe defaults are applied.
const antiAlt = new AntiAltClient({
threshold: { high: 65, shouldAct: 60 },
actions: { high: "ban", medium: "kick" },
detectors: {
accountAge: { weight: 0.6 },
username: { enabled: false },
},
});Thresholds
Controls the score boundaries for each risk level (0–100 scale).
| Option | Default | Description |
| --------------------- | ------- | ------------------------------------------------ |
| threshold.low | 20 | Minimum score for 'low' risk |
| threshold.medium | 45 | Minimum score for 'medium' risk |
| threshold.high | 70 | Minimum score for 'high' risk |
| threshold.shouldAct | 65 | Score at which result.shouldAct becomes true |
// Stricter server — lower the high threshold
const antiAlt = new AntiAltClient({
threshold: { high: 55, shouldAct: 50 },
});Actions
Maps each risk level to a recommendedAction string in the result. These are suggestions only.
| Risk | Default Action | Available Values |
| -------- | -------------- | ---------------------------------- |
| low | 'none' | 'none' 'warn' 'kick' 'ban' |
| medium | 'warn' | same |
| high | 'kick' | same |
// Stricter server — ban instead of kick on high risk
const antiAlt = new AntiAltClient({
actions: { high: "ban", medium: "kick" },
});Detectors
Each built-in detector accepts enabled and weight overrides.
| Detector | Default Weight | Description |
| ------------ | -------------- | ----------------------------------------------- |
| username | 0.3 | Scam words, alt terms, digit suffix, short name |
| avatar | 0.2 | No custom avatar, known default hash |
| accountAge | 0.5 | Tiered score based on account creation date |
// Emphasise account age, disable username check
const antiAlt = new AntiAltClient({
detectors: {
accountAge: { weight: 0.7 },
avatar: { weight: 0.3 },
username: { enabled: false },
},
});Weight normalisation: Weights don't need to sum to 1. The scorer divides by the total weight of all enabled detectors automatically.
Event System
AntiAltClient extends Node's EventEmitter. All events are fully typed.
// 'result' fires for EVERY member — ideal for audit logging
antiAlt.on("result", (member, result) => {
db.insertAuditLog({ userId: member.id, score: result.score });
});
// Risk-level events fire exclusively — only one per run
antiAlt.on("lowRisk", (member, result) => {
/* score < 45 */
});
antiAlt.on("mediumRisk", (member, result) => {
/* score 45–69 */
});
antiAlt.on("highRisk", (member, result) => {
/* score ≥ 70 */
});Tip: It is better to keep your
guildMemberAddhandler clean — just callantiAlt.run(member)and let listeners handle the rest.
// guildMemberAdd — just a trigger
client.on(Events.GuildMemberAdd, (member) => antiAlt.run(member));
// Reactions defined anywhere in your codebase
antiAlt.on("highRisk", async (member, result) => {
await logChannel.send({ embeds: [buildEmbed(member, result)] });
await member.kick("Alt account detected");
});Removing listeners
const handler = (member, result) => {
/* ... */
};
antiAlt.on("highRisk", handler);
// later...
antiAlt.off("highRisk", handler);
// or once:
antiAlt.once("highRisk", handler); // auto-removes after first fireResult Shape: AntiAltResult
interface AntiAltResult {
score: number; // 0–100 weighted risk score
risk: "low" | "medium" | "high";
shouldAct: boolean; // true when score ≥ threshold.shouldAct
recommendedAction: "none" | "warn" | "kick" | "softban" | "ban";
factors: DetectorResult[]; // per-detector breakdown
metadata: {
accountAge: number; // ms since account creation
username: string;
avatar: {
default: boolean; // true = no custom avatar
hash?: string;
};
joinedAt: Date;
timestamp: Date; // when analysis ran
};
}
interface DetectorResult {
detector: string; // 'username' | 'avatar' | 'accountAge'
enabled: boolean;
rawScore: number; // 0–100, this detector's own score
contribution: number; // weighted points added to final score
reason: string; // human-readable explanation
details?: Record<string, unknown>; // detector-specific extras
}Reading factors
const result = await antiAlt.run(member);
for (const factor of result.factors) {
console.log(`${factor.detector}: ${factor.rawScore} → ${factor.reason}`);
}
// Example output:
// username: 65 → scam domain word detected: "n1tr0", long digit suffix
// avatar: 50 → no custom avatar set
// accountAge: 80 → account created 8 hours agoBuilt-in Detectors
Username Detector
Default weight: 0.3
Checks the username against known scam patterns and structural signals.
| Check | Score | Notes |
| ------------------------------- | ----- | ----------------------------------------------------------------------------- |
| High-confidence scam word | +35 | nitro, vbucks, discord impersonation, onlyfans — leetspeak aware |
| Moderate-confidence alt term | +20 | alt, smurf, burner, account, scam, nsfw — word-boundary protected |
| Long digit suffix (4+ digits) | +30 | e.g. user8821 — mass-registration pattern |
| Very short username (≤ 3 chars) | +15 | Unusually short |
Scam pattern examples: n1tr0, N!TR0, v-bucks, v b*cks, d1sc0rd, 0nlyfans
Alt term examples: my_alt ✅ matches · exalt_gamer ❌ blocked by word boundary
Avatar Detector
Default weight: 0.2
| Check | Score | Notes |
| ---------------------------------- | ----- | ----------------------------------------------------- |
| No custom avatar (Discord default) | +50 | user.avatar === null |
| Known Discord legacy avatar hash | +20 | Old default avatar hashes still seen on some accounts |
Account Age Detector
Default weight: 0.5
| Account Age | Score | | ----------- | ----- | | < 1 hour | 100 | | < 1 day | 80 | | < 7 days | 60 | | < 30 days | 40 | | < 90 days | 20 | | ≥ 90 days | 0 |
Utility: buildEmbed
import { buildEmbed } from "discord-moderation";
antiAlt.on("result", async (member, result) => {
const embed = buildEmbed(member, result);
await logChannel.send({ embeds: [embed] });
});Returns a pre-built EmbedBuilder showing the score, risk level, per-detector breakdown, and recommended action. Colour-coded by risk (green / yellow / red).
Full Bot Example
import { Client, GatewayIntentBits, Events } from "discord.js";
import { AntiAltClient, buildEmbed } from "discord-moderation";
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
});
const antiAlt = new AntiAltClient({
threshold: { high: 65, shouldAct: 60 },
actions: { high: "ban", medium: "kick" },
detectors: {
accountAge: { weight: 0.6 },
username: { weight: 0.25 },
avatar: { weight: 0.15 },
},
});
// Audit every join — fires regardless of risk
antiAlt.on("result", (member, result) => {
console.log(`[${result.risk}] ${member.user.username} — ${result.score}/100`);
});
// Send embed for medium risk
antiAlt.on("mediumRisk", async (member, result) => {
const channel = client.channels.cache.get("LOG_CHANNEL_ID");
if (channel?.isTextBased()) {
await channel.send({ embeds: [buildEmbed(member, result)] });
}
});
// Kick + alert on high risk
antiAlt.on("highRisk", async (member, result) => {
const channel = client.channels.cache.get("LOG_CHANNEL_ID");
if (channel?.isTextBased()) {
await channel.send({
content: `🚨 High-risk member flagged: <@${member.id}>`,
embeds: [buildEmbed(member, result)],
});
}
if (result.shouldAct) {
await member.kick("Potential alt account — automated action");
}
});
// guildMemberAdd is just a trigger
client.on(Events.GuildMemberAdd, (member) => antiAlt.run(member));
client.once(Events.ClientReady, (c) => console.log(`Online as ${c.user.tag}`));
client.login(process.env.DISCORD_TOKEN);Roadmap
discord-moderation is the foundation of ModerationSuite — a growing collection of independent, composable modules for Discord.js bots. Each module follows the same principle: analyse and recommend, never act unilaterally.
v0.2.0 — Anti-Alt improvements
- [ ] More username signals: display name vs. username divergence, Unicode homoglyph detection
- [ ] Profile age cross-referenced with join velocity (multiple joins from similar accounts in a short window)
- [ ] Configurable scam pattern list — let developers append their own regex terms
v0.3.0 — AntiRaidClient
Detect coordinated join waves before they cause damage.
- Sliding-window join rate tracking (e.g. 10 joins in 30 seconds)
- Pattern matching across simultaneous joiners (similar usernames, account ages, avatars)
- Events:
raidDetected,raidWarning,raidCleared - Configurable sensitivity and cooldown
v0.4.0 — AntiSpamClient
Message-level spam detection.
- Per-user message rate limiting with configurable burst tolerance
- Duplicate / near-duplicate message detection (Levenshtein similarity)
- Mention spam (mass @everyone / role / user pings)
- Events:
spamDetected,spamWarningwith per-message context
v0.5.0 — AntiNsfwClient
Attachment and link screening.
- External image link analysis (heuristic URL scoring)
- Known NSFW domain blocklist
- Optional integration hooks for external image classification APIs (pluggable, no hard dependency)
- Events:
nsfwFlaggedwith attachment context
License
MIT © 2025
