Wil je een actieve community, dan werkt het belonen van leden: een discord level bot geeft gebruikers XP voor elk bericht, laat ze bij bepaalde drempels in level stijgen en deelt desgewenst automatisch rollen uit. Kant-en-klare bots zoals MEE6 of Arcane doen dit, maar als je de logica zelf bouwt zijn er geen grenzen: je bepaalt volledig de XP-formule, de anti-spamregels, het ontwerp van de rangkaart en de beloningsrollen. In dit artikel bouwen we vanaf nul een werkend XP-systeem met discord.js v14.
Hoe werkt het: XP- en levellogica
Het basisidee is eenvoudig: de bot luistert naar elk bericht op de server en voegt een kleine hoeveelheid XP toe aan het lid dat het stuurde. Wanneer de XP een drempel bereikt, gaat het lid naar het volgende level. Twee zaken verdienen aandacht:
- Anti-spam (cooldown): Om te voorkomen dat een lid XP verzamelt door berichten te spammen, stellen we een wachttijd per gebruiker in (bijv. 60 seconden). Berichten die voor het einde van dat venster worden verstuurd, leveren geen XP op.
- Levelcurve: Om elk level iets moeilijker te maken, berekenen we de XP-drempel met een oplopende formule. In de populaire MEE6-formule is de XP die nodig is om van level x naar x+1 te gaan
5 * x² + 50 * x + 100. Level 0→1 vergt dus 100 XP, 1→2 vergt 155 XP; het getal groeit bij elke stap.
Projectopzet en vereiste intents
Maak eerst een projectmap aan en installeer de afhankelijkheden. Om XP duurzaam op te slaan gebruiken we better-sqlite3, dat licht en snel is:
npm init -y
npm install discord.js better-sqlite3 dotenv
Om de berichtinhoud te lezen heeft de bot het Message Content Intent-privilege nodig. Schakel dit zowel in de Discord Developer Portal → Bot → Privileged Gateway Intents in, als dat je de gewenste intents in de code declareert:
import { Client, GatewayIntentBits, Events } from 'discord.js';
import 'dotenv/config';
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
client.once(Events.ClientReady, (c) => {
console.log(`Ingelogd als ${c.user.tag}`);
});
client.login(process.env.TOKEN);
Codeer het bot-token nooit hard in de code; zet het in een .env-bestand als TOKEN=... en voeg .env toe aan .gitignore.
Database: XP duurzaam opslaan
Zodat de voortgang niet bij elke herstart van de bot wordt gewist, moeten we XP naar schijf schrijven. Een eenvoudige tabel met één rij per server (guild) en gebruiker volstaat:
import Database from 'better-sqlite3';
const db = new Database('levels.db');
db.exec(`
CREATE TABLE IF NOT EXISTS levels (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
xp INTEGER NOT NULL DEFAULT 0,
level INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (guild_id, user_id)
);
`);
// Haal het record van een gebruiker op (maak het aan indien afwezig)
function getUser(guildId, userId) {
let row = db.prepare(
'SELECT * FROM levels WHERE guild_id = ? AND user_id = ?'
).get(guildId, userId);
if (!row) {
db.prepare(
'INSERT INTO levels (guild_id, user_id) VALUES (?, ?)'
).run(guildId, userId);
row = { guild_id: guildId, user_id: userId, xp: 0, level: 0 };
}
return row;
}
Ook het opslaan van de guild-ID is belangrijk: draait dezelfde bot op meerdere servers, dan blijven de levels van elke server gescheiden.
XP per bericht toevoegen
Nu de kern: het messageCreate-event. Hier zorgen we dat bots en systeemberichten geen XP krijgen, passen we de cooldown toe en controleren we op level-ups:
const cooldowns = new Map(); // userId -> laatste XP-tijdstip
const COOLDOWN_MS = 60_000; // 60 seconden
// XP nodig om van level x naar x+1 te gaan
const xpForNext = (level) => 5 * level ** 2 + 50 * level + 100;
client.on(Events.MessageCreate, async (message) => {
if (message.author.bot || !message.guild) return;
const key = `${message.guild.id}-${message.author.id}`;
const now = Date.now();
if (cooldowns.has(key) && now - cooldowns.get(key) < COOLDOWN_MS) return;
cooldowns.set(key, now);
const user = getUser(message.guild.id, message.author.id);
const gain = 15 + Math.floor(Math.random() * 11); // 15–25 XP
let { xp, level } = user;
xp += gain;
// Stijg in level zolang er genoeg XP is verzameld
while (xp >= xpForNext(level)) {
xp -= xpForNext(level);
level += 1;
onLevelUp(message, level);
}
db.prepare(
'UPDATE levels SET xp = ?, level = ? WHERE guild_id = ? AND user_id = ?'
).run(xp, level, message.guild.id, message.author.id);
});
Per bericht 15 tot 25 willekeurige XP geven is de klassieke aanpak die ook MEE6 gebruikt; een beetje toeval in plaats van een vaste waarde maakt het systeem lastiger te misbruiken.
Rolbeloningen en de level-up-melding
Bij een level-up willen we twee dingen: een felicitatiebericht in het kanaal plaatsen en de rol toekennen die bij die drempel hoort. Je kunt de roldrempels in een eenvoudig object bewaren:
const ROLE_REWARDS = {
5: 'ROL_ID_LEERLING',
10: 'ROL_ID_MEESTER',
20: 'ROL_ID_LEGENDE',
};
async function onLevelUp(message, level) {
await message.channel.send(
`🎉 ${message.author}, gefeliciteerd! Je bent nu **level ${level}**.`
);
const roleId = ROLE_REWARDS[level];
if (roleId) {
const role = message.guild.roles.cache.get(roleId);
if (role) {
await message.member.roles.add(role).catch(console.error);
}
}
}
Wil je dat rollen werken, dan moet de rol van de bot boven de rollen staan die hij uitdeelt (Serverinstellingen → Rollen), en heeft de bot de permissie Manage Roles nodig. Anders geeft roles.add een permissiefout.
De rangkaart: een rank-commando
Gebruikers willen hun level zien. De eenvoudigste aanpak is een tekstkaart teruggeven via een slash-commando. Voor een visuele kaart kun je een PNG tekenen met @napi-rs/canvas en die versturen met AttachmentBuilder; maar om te beginnen volstaat een embed:
// /rank-commando (moet vooraf geregistreerd zijn)
if (interaction.commandName === 'rank') {
const u = getUser(interaction.guild.id, interaction.user.id);
const need = 5 * u.level ** 2 + 50 * u.level + 100;
await interaction.reply(
`**${interaction.user.username}** — Level ${u.level} | ` +
`${u.xp} / ${need} XP`
);
}
Je moet slash-commando's bij Discord registreren voordat je ze gebruikt (guild.commands.set of globale registratie via de REST API). Een /leaderboard-commando met ORDER BY level DESC, xp DESC LIMIT 10 dat een ranglijst toont, stimuleert gezonde competitie.
Veelgestelde vragen
Mijn bot ziet berichten maar voegt geen XP toe, waarom?
Hoogstwaarschijnlijk staat het Message Content Intent uit. Zorg dat je dit privilege hebt ingeschakeld in de Developer Portal en GatewayIntentBits.MessageContent in de code hebt gedeclareerd. De bot moet ook toestemming hebben om het betreffende kanaal te zien.
Kan ik MySQL gebruiken in plaats van better-sqlite3?
Ja. SQLite is ruim voldoende en vereist geen configuratie voor één server en middelgrote communities. Draai je een drukbezochte bot op veel servers, dan schaalt MySQL/PostgreSQL of een Redis-cache beter; de querylogica blijft hetzelfde.
Hoe voorkom ik XP-misbruik (spam)?
Een cooldown per gebruiker is de doeltreffendste maatregel. Je kunt ook erg korte berichten negeren (bijv. één emoji), bepaalde kanalen (bot-commando's, spamkanaal) uitsluiten van XP en een minimumleeftijd voor verdachte accounts eisen.
Wil je een maatwerk level- en XP-systeem voor je community? Ik kan een bot op maat bouwen met anti-spamregels, visuele rangkaarten, rolbeloningen en een ranglijst. Neem contact op en laten we je server tot leven brengen.