npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

discord-moderation

v0.1.0

Published

A discord moderation suite for discord.js bots with features like Anti Alt.

Downloads

147

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

npm install discord-moderation
# discord.js v14 is a required peer dependency
npm install discord.js

Node.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 AntiAltResult

All 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 guildMemberAdd handler clean — just call antiAlt.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 fire

Result 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 ago

Built-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, spamWarning with 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: nsfwFlagged with attachment context

License

MIT © 2025