Un bot d'économie Discord ajoute une couche de gamification à votre serveur qui maintient les membres actifs : ils gagnent une monnaie virtuelle, récupèrent des récompenses quotidiennes et dépensent leurs économies dans une boutique pour acheter des rôles ou des objets. Dans ce guide, nous allons construire de zéro un système d'économie fonctionnel avec discord.js, couvrant les commandes de solde, de récompense quotidienne et de boutique. Nous utiliserons les slash commands modernes et stockerons les données dans une base de données persistante.
Prérequis et mise en place du projet
Pour commencer, il vous faut Node.js 18 ou plus récent, un éditeur de code et une application bot créée dans le Discord Developer Portal. L'intent Message Content n'est pas nécessaire ici car nous utilisons des slash commands, mais en invitant le bot sur votre serveur vous devez sélectionner les scopes applications.commands et bot.
Créez un nouveau dossier et installez les dépendances :
npm init -y
npm install discord.js better-sqlite3 dotenv
Plutôt que d'écrire le token et l'identifiant de l'application en dur, nous les conservons dans un fichier .env :
DISCORD_TOKEN=votre_token_bot
CLIENT_ID=id_application
GUILD_ID=id_serveur_test
Schéma de la base de données
Les données d'économie doivent être persistantes ; les soldes ne doivent pas être réinitialisés à chaque redémarrage du bot. Nous utilisons better-sqlite3 car sa mise en place est triviale. Une seule table suffit :
const Database = require('better-sqlite3');
const db = new Database('economy.db');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
user_id TEXT PRIMARY KEY,
balance INTEGER NOT NULL DEFAULT 0,
last_daily INTEGER
);
`);
function getUser(id) {
let row = db.prepare('SELECT * FROM users WHERE user_id = ?').get(id);
if (!row) {
db.prepare('INSERT INTO users (user_id) VALUES (?)').run(id);
row = { user_id: id, balance: 0, last_daily: null };
}
return row;
}
function addBalance(id, amount) {
getUser(id);
db.prepare('UPDATE users SET balance = balance + ? WHERE user_id = ?').run(amount, id);
}
Le champ last_daily stocke, sous forme d'horodatage Unix, la dernière fois que la récompense quotidienne a été réclamée. Faire passer chaque mouvement de monnaie par une unique fonction addBalance facilite grandement l'ajout ultérieur de journalisation ou de sécurité transactionnelle.
Définir et enregistrer les commandes
Dans les versions récentes de discord.js, les slash commands doivent être enregistrées séparément auprès de l'API Discord. En développement, l'enregistrement sur un seul serveur est instantané ; l'enregistrement global peut prendre jusqu'à une heure pour se propager. Nous écrivons un script deploy-commands.js distinct :
const { REST, Routes, SlashCommandBuilder } = require('discord.js');
require('dotenv').config();
const commands = [
new SlashCommandBuilder().setName('balance').setDescription('Affiche ton solde'),
new SlashCommandBuilder().setName('daily').setDescription('Récupère ta récompense quotidienne'),
new SlashCommandBuilder().setName('shop').setDescription('Liste la boutique'),
new SlashCommandBuilder()
.setName('buy')
.setDescription('Acheter un objet dans la boutique')
.addStringOption(o =>
o.setName('item').setDescription("Clé de l'objet").setRequired(true)),
].map(c => c.toJSON());
const rest = new REST().setToken(process.env.DISCORD_TOKEN);
rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
{ body: commands }
).then(() => console.log('Commandes enregistrées.'));
Logique du solde et de la récompense quotidienne
Nous gérons maintenant les interactions dans le fichier principal du bot. Le point critique de la récompense quotidienne est le contrôle de délai (cooldown) qui limite l'utilisateur à une réclamation toutes les 24 heures. Nous le faisons en comparant avec last_daily :
const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const DAILY_AMOUNT = 250;
const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; // 24 heures (ms)
client.on('interactionCreate', async (i) => {
if (!i.isChatInputCommand()) return;
const user = getUser(i.user.id);
if (i.commandName === 'balance') {
await i.reply(`💰 Ton solde : **${user.balance}** jetons.`);
}
if (i.commandName === 'daily') {
const now = Date.now();
if (user.last_daily && now - user.last_daily < DAILY_COOLDOWN) {
const restant = user.last_daily + DAILY_COOLDOWN - now;
const heures = Math.ceil(restant / (60 * 60 * 1000));
return i.reply(`⏳ Attends ~${heures}h pour ta prochaine récompense.`);
}
addBalance(i.user.id, DAILY_AMOUNT);
db.prepare('UPDATE users SET last_daily = ? WHERE user_id = ?').run(now, i.user.id);
await i.reply(`🎁 Tu as gagné ${DAILY_AMOUNT} jetons !`);
}
});
client.login(process.env.DISCORD_TOKEN);
Stocker et comparer les horodatages en millisecondes élimine totalement les problèmes de fuseau horaire. Une règle « 24 heures depuis la dernière réclamation » est à la fois plus juste et plus robuste à implémenter qu'un système « réinitialisé à minuit ».
Boutique et flux d'achat
Pour la boutique, définir les objets sous forme d'objet dans le code suffit ; une table séparée n'est pas nécessaire sur les petits serveurs. Chaque objet a une clé, un nom, un prix et, éventuellement, l'ID d'un rôle Discord qu'il accorde :
const SHOP = {
vip: { name: 'Rôle VIP', price: 1000, roleId: 'ID_ROLE' },
couleur: { name: 'Nom coloré', price: 500, roleId: 'ID_ROLE_2' },
};
Dans la commande d'achat, nous vérifions d'abord le solde, déduisons le coût s'il est suffisant et accordons le rôle. Effectuer la vérification du solde et la déduction en une seule fois évite un solde négatif lors de situations de concurrence comme les doubles clics :
if (i.commandName === 'shop') {
const liste = Object.entries(SHOP)
.map(([k, v]) => `• \`${k}\` — ${v.name} : ${v.price} jetons`)
.join('\n');
return i.reply(`🛒 **Boutique**\n${liste}`);
}
if (i.commandName === 'buy') {
const key = i.options.getString('item');
const item = SHOP[key];
if (!item) return i.reply('❌ Aucun objet de ce type.');
if (user.balance < item.price) return i.reply('❌ Pas assez de jetons.');
addBalance(i.user.id, -item.price);
if (item.roleId) {
const member = await i.guild.members.fetch(i.user.id);
await member.roles.add(item.roleId).catch(() => null);
}
await i.reply(`✅ **${item.name}** acheté !`);
}
Pour que l'attribution du rôle fonctionne, le rôle du bot doit être plus haut dans la liste que le rôle qu'il accorde, et le bot a besoin de la permission « Gérer les rôles ». Sinon Discord renvoie une erreur de permission ; la gérer proprement avec .catch() pour informer l'utilisateur sans faire planter le bot est une bonne habitude.
Sécurité et pistes d'amélioration
Le système de base est prêt, mais quelques points méritent attention avant la mise en production. Si vous ajoutez un système qui récompense la monnaie par message, mettez un court cooldown par utilisateur pour éviter le spam. Validez toujours les soldes côté serveur et ne faites jamais confiance à une valeur venant du client. Pour maintenir le bot en ligne 24/7, l'approche la plus fiable est de l'exécuter sur un petit VPS avec un gestionnaire de processus comme pm2.
Questions fréquentes
Puis-je utiliser MySQL comme base de données ?
Oui. SQLite est idéal pour sa simplicité de mise en place, mais pour un gros bot fonctionnant sur plusieurs serveurs, MySQL ou PostgreSQL convient mieux. La logique des requêtes reste la même ; vous changez seulement le pilote et la connexion de la base.
Pourquoi la récompense quotidienne est-elle vérifiée avec un horodatage ?
Parce que les approches « réinitialisées chaque minuit » sont affectées par les décalages de fuseau horaire et ouvertes aux abus. Stocker l'heure de la dernière réclamation et vérifier si 24 heures se sont écoulées est juste et indépendant du fuseau horaire.
Un rôle acheté disparaît-il quand le bot est hors ligne ?
Non. Le rôle est attribué au membre du côté de Discord et il est permanent ; il reste même si le bot tombe. Seul le solde de jetons réside dans la base de données, alors veillez à ne pas la perdre.
Vous voulez un bot d'économie professionnel et évolutif pour votre serveur ? Je peux développer un bot sur mesure adapté à vos besoins, avec des commandes personnalisées, des saisons et une intégration de panneau. Pour discuter de votre projet, contactez-moi.