layahost

Discord moderation bot

A moderation bot
that reads the room.

Keyword filters miss “go jump off a bridge” and flag “this boss fight is killing me”. This guide builds a small Discord bot that sends each message to an AI moderation API, deletes the clear cases, sends the borderline ones to your moderators, and costs about $1.50 per 100,000 messages.

What the bot does

  1. Reads every new and edited message in your server (it needs the Message Content intent).
  2. Asks layahost’s moderation template whether the message is toxic or safe. The answer comes back with a probability in about a tenth of a second.
  3. Above 0.9 it deletes the message and times the author out for ten minutes. Between 0.6 and 0.9 it leaves the message up and posts it to a private #mod-log channel for a human to decide.

Discord’s built-in AutoMod is still worth keeping for invite links, mention spam and your own word lists. The bot adds what word lists can’t do: understanding insults, harassment and threats written in ordinary words.

1. Create the bot

  1. In the Discord Developer Portal, create an application, open Bot, copy the token, and switch on Message Content Intent.
  2. Under OAuth2 → URL Generator pick the bot scope and the permissions View Channels, Send Messages, Manage Messages and Moderate Members (for timeouts). Open the generated URL and add the bot to your server.
  3. Create a #mod-log channel that only moderators and the bot can see, and copy its ID (right click → Copy Channel ID, with Developer Mode on).
  4. Create a layahost account, verify your email ($1 of free credit, no card) and create an API key.

2. The code

Node.js 18 or newer, one dependency:

terminal
npm init -y && npm pkg set type=module && npm install discord.js
bot.js
import { Client, Events, GatewayIntentBits } from "discord.js";

const DELETE_AT = 0.9; // delete + timeout above this
const REVIEW_AT = 0.6; // send to #mod-log above this

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

// Ask layahost: is this message toxic? Returns the probability, or null on any error.
async function toxicity(text) {
  try {
    const res = await fetch("https://layahost.com/v1/decide", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.LAYAHOST_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ text, template: "moderation" }),
      signal: AbortSignal.timeout(3000),
    });
    if (!res.ok) return null;
    const answer = await res.json(); // { decision: "toxic", probabilities: { toxic: 0.93, safe: 0.07 }, ... }
    return answer.probabilities.toxic;
  } catch {
    return null; // fail open: never block chat because the check is down
  }
}

async function moderate(message) {
  if (message.author?.bot || !message.inGuild() || !message.content?.trim()) return;

  const p = await toxicity(message.content);
  if (p === null || p < REVIEW_AT) return;

  const where = `${message.author} in ${message.channel}`;
  const quiet = { allowedMentions: { parse: [] } }; // never ping anyone from a quoted message

  try {
    const log = await client.channels.fetch(process.env.MOD_LOG_CHANNEL_ID);
    if (p >= DELETE_AT) {
      await message.delete().catch(() => {});
      await message.member?.timeout(10 * 60 * 1000, "Toxic message").catch(() => {});
      const quoted = message.content.slice(0, 1500).split("\n").map((line) => `> ${line}`).join("\n");
      await log.send({ content: `Removed (${p.toFixed(2)}) from ${where}:\n${quoted}`, ...quiet });
    } else {
      await log.send({ content: `Please check (${p.toFixed(2)}) ${where}: ${message.url}`, ...quiet });
    }
  } catch (err) {
    // Wrong channel ID or missing permission: log it, keep the bot running.
    console.error("Moderation action failed:", err.message);
  }
}

client.on(Events.MessageCreate, moderate);
client.on(Events.MessageUpdate, (_before, after) => moderate(after)); // edits are checked too

client.once(Events.ClientReady, (c) => console.log(`Moderating as ${c.user.tag}`));
client.login(process.env.DISCORD_TOKEN);
terminal
DISCORD_TOKEN=… LAYAHOST_API_KEY=lh_… MOD_LOG_CHANNEL_ID=… node bot.js

Run it anywhere Node runs: a small VPS, a Raspberry Pi, Railway or Fly.io. The bot keeps one connection open to Discord, so serverless functions are a poor fit.

3. Tune it on your server’s messages

Every community has its own tone: a gaming server swears more than a support forum. Before you let the bot delete anything, set DELETE_AT to 1.1 (never) and let it only log for a few days. Then export a few hundred real messages, label them toxic or safe, and upload them as a test set in the layahost console. A run gives you the accuracy on your own messages and the probability threshold that separates the two best. If a hosted model such as clef-flash does better on your messages, add "model": "clef-flash" to the request; the comparison is one click in the same test set. Hosted models are billed by input tokens from prepaid credit, and their text is processed by Cloudflare rather than on our EU servers.

More than toxicity

What it costs

With the default Laya model, one message checked (or edited) is one decision: $15 per million on pay-as-you-go, less on a monthly plan. A server with 3,000 messages a day uses about 90,000 decisions a month, roughly $1.35. The $1 welcome credit covers the first 66.7K messages. See pricing.

Good to know

FAQ

Is this an official Discord bot?

No. It is your own bot, running on your own machine, calling the layahost API. Discord is a trademark of Discord Inc.; layahost is not affiliated with it.

Will it slow down my chat?

No. Messages are posted as usual; the bot checks them a moment later and removes the bad ones, typically within half a second.

Can I use Python instead?

Yes. The API is plain HTTPS; with discord.py, call the same endpoint from your on_message handler. Code examples show the request in Python.