Bir Discord ekonomi botu, sunucuna oyunlaştırma katmanı ekleyerek üyelerin sohbette aktif kalmasını sağlar: sanal bir para birimi kazanırlar, günlük ödüllerini toplarlar ve biriktirdikleri parayla mağazadan rol ya da öğe satın alırlar. Bu yazıda discord.js ile sıfırdan bakiye, günlük ödül ve mağaza komutlarını içeren çalışır bir ekonomi sistemi kuracağız. Komutları modern slash command yapısıyla, verileri ise kalıcı bir veritabanında tutacağız.
Gereksinimler ve proje kurulumu
Başlamak için Node.js 18 veya üzeri, bir kod editörü ve Discord Developer Portal üzerinden oluşturulmuş bir bot uygulaması gerekiyor. Portal'da bota Message Content niyeti zorunlu değil çünkü slash komutları kullanacağız, ancak botu sunucuna eklerken applications.commands ve bot kapsamlarını seçmelisin.
Yeni bir klasör açıp bağımlılıkları kuralım:
npm init -y
npm install discord.js better-sqlite3 dotenv
Token ve uygulama kimliğini koda gömmek yerine bir .env dosyasında tutuyoruz:
DISCORD_TOKEN=botunuzun_tokeni
CLIENT_ID=uygulama_kimligi
GUILD_ID=test_sunucu_kimligi
Veritabanı şeması
Ekonomi verisinin kalıcı olması şart; bot her yeniden başladığında bakiyeler sıfırlanmamalı. Kurulumu basit olduğu için better-sqlite3 kullanıyoruz. Tek bir tablo işimizi görür:
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);
}
Burada last_daily alanı, günlük ödülün ne zaman alındığını Unix zaman damgası olarak saklar. Para hareketlerini tek bir addBalance fonksiyonundan geçirmek, ileride loglama veya işlem güvenliği eklemek istediğinde işini çok kolaylaştırır.
Komutları tanımlamak ve kaydetmek
discord.js'in son sürümlerinde slash komutları Discord API'sine ayrıca kaydedilmelidir. Geliştirme sırasında tek bir sunucuya kaydetmek anında güncellenir; global kayıt ise yayılması bir saate kadar sürebilir. Komutları kaydeden ayrı bir deploy-commands.js betiği yazıyoruz:
const { REST, Routes, SlashCommandBuilder } = require('discord.js');
require('dotenv').config();
const commands = [
new SlashCommandBuilder().setName('balance').setDescription('Bakiyeni gösterir'),
new SlashCommandBuilder().setName('daily').setDescription('Günlük ödülünü al'),
new SlashCommandBuilder().setName('shop').setDescription('Mağazayı listeler'),
new SlashCommandBuilder()
.setName('buy')
.setDescription('Mağazadan öğe satın al')
.addStringOption(o =>
o.setName('item').setDescription('Öğe anahtarı').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('Komutlar kaydedildi.'));
Bakiye ve günlük ödül mantığı
Şimdi ana bot dosyasında etkileşimleri işliyoruz. Günlük ödülde kritik nokta, kullanıcının ödülü 24 saatte bir alabilmesini sağlayan bekleme süresi (cooldown) kontrolüdür. Bunu last_daily ile karşılaştırarak yapıyoruz:
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 saat (ms)
client.on('interactionCreate', async (i) => {
if (!i.isChatInputCommand()) return;
const user = getUser(i.user.id);
if (i.commandName === 'balance') {
await i.reply(`💰 Bakiyen: **${user.balance}** jeton.`);
}
if (i.commandName === 'daily') {
const now = Date.now();
if (user.last_daily && now - user.last_daily < DAILY_COOLDOWN) {
const kalan = user.last_daily + DAILY_COOLDOWN - now;
const saat = Math.ceil(kalan / (60 * 60 * 1000));
return i.reply(`⏳ Bir sonraki ödül için ~${saat} saat bekle.`);
}
addBalance(i.user.id, DAILY_AMOUNT);
db.prepare('UPDATE users SET last_daily = ? WHERE user_id = ?').run(now, i.user.id);
await i.reply(`🎁 ${DAILY_AMOUNT} jeton kazandın!`);
}
});
client.login(process.env.DISCORD_TOKEN);
Zaman damgalarını milisaniye cinsinden saklayıp karşılaştırmak, zaman dilimi sorunlarını tamamen ortadan kaldırır. "Gece yarısı sıfırlanan" bir sistem yerine "son alımdan 24 saat sonra" mantığı hem daha adil hem de uygulaması daha sağlamdır.
Mağaza ve satın alma akışı
Mağaza için öğeleri kodun içinde bir nesne olarak tanımlamak yeterli; küçük sunucularda ayrı tablo zorunlu değildir. Her öğenin bir anahtarı, adı, fiyatı ve isteğe bağlı olarak vereceği bir Discord rolünün kimliği olur:
const SHOP = {
vip: { name: 'VIP Rolü', price: 1000, roleId: 'ROL_KIMLIGI' },
renk: { name: 'Renkli İsim', price: 500, roleId: 'ROL_KIMLIGI_2' },
};
Satın alma komutunda önce bakiyeyi kontrol eder, yeterliyse parayı düşer ve rolü veririz. Bakiye kontrolü ile düşme işlemini tek seferde yapmak, çift tıklama gibi yarış durumlarında negatif bakiye oluşmasını engeller:
if (i.commandName === 'shop') {
const liste = Object.entries(SHOP)
.map(([k, v]) => `• \`${k}\` — ${v.name}: ${v.price} jeton`)
.join('\n');
return i.reply(`🛒 **Mağaza**\n${liste}`);
}
if (i.commandName === 'buy') {
const key = i.options.getString('item');
const item = SHOP[key];
if (!item) return i.reply('❌ Böyle bir öğe yok.');
if (user.balance < item.price) return i.reply('❌ Yeterli jetonun yok.');
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}** satın alındı!`);
}
Rol verme işleminin çalışması için botun rolünün, verdiği rolden daha yukarıda olması ve botun "Rolleri Yönet" yetkisine sahip olması gerekir. Aksi halde Discord izin hatası döner; .catch() ile bu durumu sessizce ele alıp kullanıcıyı çökmeden bilgilendirmek iyi bir alışkanlıktır.
Güvenlik ve geliştirme önerileri
Temel sistem hazır; üretime taşımadan önce birkaç noktaya dikkat etmek gerekir. Mesaj başına para veren bir sistem eklersen, spam'i önlemek için kullanıcı başına kısa bir cooldown koy. Bakiyeleri her zaman sunucu tarafında doğrula, asla istemciden gelen değere güvenme. Botu 7/24 ayakta tutmak için ise küçük bir VPS üzerinde pm2 gibi bir süreç yöneticisiyle çalıştırmak en sağlam çözümdür.
Sık Sorulan Sorular
Veritabanı olarak MySQL kullanabilir miyim?
Evet. SQLite kurulum kolaylığı için ideal, ancak birden çok sunucuda çalışan büyük bir bot için MySQL veya PostgreSQL daha uygundur. Sorgu mantığı aynı kalır; yalnızca veritabanı sürücüsünü ve bağlantıyı değiştirirsin.
Günlük ödül neden zaman damgasıyla kontrol ediliyor?
Çünkü "her gece sıfırlanan" yaklaşımlar zaman dilimi farklarından etkilenir ve kötüye kullanıma açıktır. Son alım zamanını saklayıp 24 saat geçip geçmediğine bakmak hem adil hem de zaman diliminden bağımsız çalışır.
Satın alınan rol botu kapatınca kayboluyor mu?
Hayır. Rol Discord tarafında üyeye atanır ve kalıcıdır; bot kapansa bile rol durur. Yalnızca jeton bakiyesi veritabanında tutulduğu için botun veritabanını kaybetmemeye dikkat etmelisin.
Sunucun için profesyonel, ölçeklenebilir bir ekonomi botu mu istiyorsun? Özel komutlar, sezon sistemleri ve panel entegrasyonuyla ihtiyacına özel bir bot geliştirebilirim. Projeni konuşmak için benimle iletişime geç.