Guides/Building a Discord Bot

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 dotenv

Create 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_id

Step 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.`);
    }
  }
});