aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord Bots

Discord Level Bot: XP System and Role Rewards

If you want an active community, rewarding members works: a discord level bot grants users XP for every message, levels them up at certain thresholds and, if you like, hands out roles automatically. Ready-made bots like MEE6 or Arcane do this, but when you build the logic yourself there are no limits — you fully control the XP formula, the anti-spam rules, the rank card design and the reward roles. In this article we'll build a working XP system from scratch with discord.js v14.

How it works: XP and level logic

The core idea is simple: the bot listens to every message on the server and adds a small amount of XP to the member who sent it. When XP reaches a threshold, the member moves up to the next level. Two things deserve attention:

  • Anti-spam (cooldown): So a member can't farm XP by flooding messages, we set a per-user wait time (e.g. 60 seconds). Messages sent before that window passes earn no XP.
  • Level curve: To make each level a bit harder, we compute the XP threshold with a rising formula. In the popular MEE6 formula, the XP needed to go from level x to x+1 is 5 * x² + 50 * x + 100. So level 0→1 needs 100 XP, 1→2 needs 155 XP; the number grows at every step.

Project setup and required intents

First, create a project folder and install the dependencies. To store XP persistently we'll use better-sqlite3, which is light and fast:

npm init -y
npm install discord.js better-sqlite3 dotenv

To read message content, the bot needs the Message Content Intent privilege. Enable it both in the Discord Developer Portal → Bot → Privileged Gateway Intents, and declare the intents you need in code:

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(`Logged in as ${c.user.tag}`);
});

client.login(process.env.TOKEN);

Never hard-code the bot token; put it in a .env file as TOKEN=... and add .env to .gitignore.

Database: storing XP persistently

So progress isn't reset every time the bot restarts, we need to write XP to disk. A simple table with one row per guild and user is enough:

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)
  );
`);

// Fetch a user's record (create it if missing)
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;
}

Storing the guild ID matters too: if the same bot runs on multiple servers, each server's levels stay separate.

Adding XP per message

Now the heart of it: the messageCreate event. Here we make sure bots and system messages earn no XP, apply the cooldown, and check for level-ups:

const cooldowns = new Map(); // userId -> last XP time
const COOLDOWN_MS = 60_000;  // 60 seconds

// XP needed to go from level x to x+1
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;

  // Level up while enough XP has accumulated
  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);
});

Granting 15–25 random XP per message is the classic approach MEE6 also uses; a little randomness instead of a fixed value makes the system harder to game.

Role rewards and the level-up announcement

On a level-up we want two things: post a celebration message to the channel and grant the role tied to that threshold. You can keep role thresholds in a simple object:

const ROLE_REWARDS = {
  5:  'ROLE_ID_APPRENTICE',
  10: 'ROLE_ID_MASTER',
  20: 'ROLE_ID_LEGEND',
};

async function onLevelUp(message, level) {
  await message.channel.send(
    `🎉 ${message.author}, congrats! You're now **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);
    }
  }
}

For roles to work, the bot's own role must sit above the roles it hands out (Server Settings → Roles), and the bot needs the Manage Roles permission. Otherwise roles.add throws a permission error.

The rank card: a rank command

Users want to see their level. The simplest approach is to return a text card via a slash command. For a visual card you can draw a PNG with @napi-rs/canvas and send it with AttachmentBuilder; but an embed is plenty to start:

// /rank command (must be registered beforehand)
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`
  );
}

You must register slash commands with Discord before using them (guild.commands.set or global registration via the REST API). Adding a /leaderboard command that uses ORDER BY level DESC, xp DESC LIMIT 10 to show a ranking encourages friendly competition.

Frequently Asked Questions

My bot sees messages but doesn't add XP — why?

Most likely the Message Content Intent is off. Make sure you enabled that privilege in the Developer Portal and declared GatewayIntentBits.MessageContent in code. The bot also needs permission to view the channel in question.

Can I use MySQL instead of better-sqlite3?

Yes. SQLite is more than enough and zero-setup for a single server and mid-sized communities. If you run a high-traffic bot across many servers, MySQL/PostgreSQL or a Redis cache scales better; the query logic stays the same.

How do I prevent XP abuse (spam)?

A per-user cooldown is the most effective measure. You can also ignore very short messages (e.g. a single emoji), exclude certain channels (bot-commands, spam channel) from XP, and require a minimum account age for suspicious accounts.

Want a custom level and XP system for your community? I can build a bespoke Discord bot complete with anti-spam rules, visual rank cards, role rewards and a leaderboard. Get in touch and let's bring your server to life.

Devamı için