Building a Discord Bot
Real-time match notifications and player lookups
In this guide, we'll build a Discord bot that monitors live Fortnite matches and posts updates to a channel. The bot will also respond to slash commands for player lookups.
What you'll learn:
- • Setting up a Discord bot with discord.js
- • Polling Cito API for live matches
- • Using webhooks for real-time updates
- • Creating slash commands for player lookups
Prerequisites
- Node.js 18+ installed
- A Discord account and server
- A Cito API key (get one free)
Step 1: Project Setup
mkdir fortnite-bot && cd fortnite-bot
npm init -y
npm install discord.js @citoapi/sdk dotenvCreate a .env file with your credentials:
.env
DISCORD_TOKEN=your_discord_bot_token
CITO_API_KEY=sk_live_your_key_here
CHANNEL_ID=your_channel_idStep 2: Basic Bot Structure
index.js
require('dotenv').config();
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');
const { CitoAPI } = require('@citoapi/sdk');
const client = new Client({
intents: [GatewayIntentBits.Guilds]
});
const cito = new CitoAPI(process.env.CITO_API_KEY);
client.once('ready', () => {
console.log(`Logged in as ${client.user.tag}`);
startMatchMonitor();
});
client.login(process.env.DISCORD_TOKEN);Step 3: Match Monitor (Polling)
Poll for live matches every 30 seconds and post updates:
const seenMatches = new Set();
async function startMatchMonitor() {
const channel = client.channels.cache.get(process.env.CHANNEL_ID);
setInterval(async () => {
try {
const { data: matches } = await cito.fortnite.matches.list({
status: 'live',
region: 'NA-EAST'
});
for (const match of matches) {
if (!seenMatches.has(match.id)) {
seenMatches.add(match.id);
const embed = new EmbedBuilder()
.setColor(0x00E5CC)
.setTitle(`🔴 LIVE: ${match.tournament}`)
.setDescription(`${match.players.length} players competing`)
.addFields(
{ name: 'Region', value: match.region, inline: true },
{ name: 'Game', value: `${match.current_game}/${match.total_games}`, inline: true }
)
.setTimestamp();
await channel.send({ embeds: [embed] });
}
}
} catch (error) {
console.error('Error fetching matches:', error);
}
}, 30000); // 30 seconds
}Step 4: Using Webhooks (Recommended)
Instead of polling, use webhooks for instant notifications:
server.js
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
// Verify signature
const signature = req.headers['x-cito-signature'];
const expectedSig = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expectedSig) {
return res.status(401).send('Invalid signature');
}
const { event, data } = req.body;
const channel = client.channels.cache.get(process.env.CHANNEL_ID);
if (event === 'match.started') {
const embed = new EmbedBuilder()
.setColor(0x00E5CC)
.setTitle(`🔴 Match Started: ${data.tournament}`)
.setDescription(`${data.players_count} players`)
.setTimestamp();
channel.send({ embeds: [embed] });
}
res.sendStatus(200);
});
app.listen(3000, () => console.log('Webhook server running'));Step 5: Slash Commands
Add player lookup commands:
const { SlashCommandBuilder } = require('discord.js');
const commands = [
new SlashCommandBuilder()
.setName('player')
.setDescription('Look up a Fortnite player')
.addStringOption(option =>
option.setName('username')
.setDescription('Player username')
.setRequired(true)
)
];
// Register commands
client.once('ready', async () => {
await client.application.commands.set(commands);
});
// Handle interactions
client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === 'player') {
await interaction.deferReply();
const username = interaction.options.getString('username');
try {
const { data: player } = await cito.fortnite.players.get(username);
const embed = new EmbedBuilder()
.setColor(0x00E5CC)
.setTitle(player.username)
.addFields(
{ name: 'Wins', value: player.stats.career.wins.toString(), inline: true },
{ name: 'K/D', value: player.stats.career.kd_ratio.toFixed(2), inline: true },
{ name: 'Earnings', value: `$${player.stats.competitive.earnings.toLocaleString()}`, inline: true }
);
await interaction.editReply({ embeds: [embed] });
} catch (error) {
await interaction.editReply(`Player "${username}" not found.`);
}
}
});