Build a League of Legends Esports Discord Bot in 10 Minutes

Create a Discord bot that shows live LCS/LEC/LCK scores, player stats, and team rosters using the Cito API. Full code included.

Author:
Cito Team

Introduction

Discord is where LoL fans live. Whether it's your friend group watching LCS together, your fantasy league server, or a community of 10,000 fans — having a bot that pulls live match data, player stats, and standings makes your server way more engaging.

In this tutorial, you'll build a Discord bot that can:

  • Show live LCS, LEC, and LCK match scores
  • Look up any pro player's stats
  • Display current league standings
  • Show team rosters and head-to-head records
  • Alert your server when matches go live

And it takes about 10 minutes.

Try the API First

Before building the bot, test the data you'll be working with:

bash
curl -H "x-api-key: cito_live_your_key" \  https://api.citoapi.com/api/v1/lol/live

Free key: Get your key for 500 calls/month in production.

Prerequisites

You need:

Project Setup

bash
mkdir lol-discord-botcd lol-discord-botnpm init -ynpm install discord.js axios dotenv

Create .env:

env
DISCORD_TOKEN=your_discord_bot_tokenCITO_API_KEY=your_cito_api_key

The Bot: Core Setup

JavaScript
// index.jsconst { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');const axios = require('axios');require('dotenv').config(); const client = new Client({  intents: [    GatewayIntentBits.Guilds,    GatewayIntentBits.GuildMessages,    GatewayIntentBits.MessageContent,  ],}); const api = axios.create({  baseURL: 'https://api.citoapi.com/api/v1/lol',  headers: { 'x-api-key': process.env.CITO_API_KEY },}); client.on('ready', () => {  console.log(`Logged in as ${client.user.tag}`);});

Command: Live Matches

Show what's currently being played across all leagues:

JavaScript
client.on('messageCreate', async (message) => {  if (message.author.bot) return;   // !lol live  if (message.content === '!lol live') {    try {      const { data } = await api.get('/live');       if (!data.data || data.data.length === 0) {        message.reply('No live matches right now. Check back during broadcast hours!');        return;      }       const embed = new EmbedBuilder()        .setTitle('Live LoL Esports Matches')        .setColor(0x00E5CC)        .setTimestamp();       data.data.forEach(match => {        embed.addFields({          name: `[${match.league}] ${match.team1.name} vs ${match.team2.name}`,          value: `Score: **${match.team1.score} - ${match.team2.score}** | Game ${match.currentGame} | ${match.status}`,        });      });       message.reply({ embeds: [embed] });    } catch (err) {      message.reply('Error fetching live matches. Try again in a moment.');    }  }});

Command: Player Stats

Look up any pro player by name:

JavaScript
// !lol player fakerif (message.content.startsWith('!lol player ')) {  const playerId = message.content.slice(12).trim().toLowerCase();   try {    const { data } = await api.get(`/players/${playerId}/stats`);    const p = data.data;     const embed = new EmbedBuilder()      .setTitle(`${p.name} - ${p.team}`)      .setDescription(`**${p.role}** | ${p.nationality}`)      .setColor(0x00E5CC)      .addFields(        { name: 'KDA', value: p.kda?.toString() || 'N/A', inline: true },        { name: 'CS/min', value: p.csPerMin?.toString() || 'N/A', inline: true },        { name: 'DPM', value: p.damagePerMin?.toString() || 'N/A', inline: true },        { name: 'Win Rate', value: `${p.winRate || 'N/A'}%`, inline: true },        { name: 'Games', value: p.gamesPlayed?.toString() || 'N/A', inline: true },        { name: 'Vision', value: p.visionScore?.toString() || 'N/A', inline: true },      )      .setTimestamp();     message.reply({ embeds: [embed] });  } catch (err) {    message.reply(`Player "${playerId}" not found. Try the exact pro name (e.g., faker, chovy, caps).`);  }}

Command: League Standings

Show current standings for any league:

JavaScript
// !lol standings lcsif (message.content.startsWith('!lol standings ')) {  const leagueId = message.content.slice(15).trim().toLowerCase();   try {    const { data } = await api.get(`/leagues/${leagueId}/standings`);     const embed = new EmbedBuilder()      .setTitle(`${leagueId.toUpperCase()} Standings`)      .setColor(0x00E5CC)      .setTimestamp();     let description = '';    data.data.forEach((team, i) => {      const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;      description += `${medal} **${team.name}** — ${team.wins}W ${team.losses}L\n`;    });     embed.setDescription(description);    message.reply({ embeds: [embed] });  } catch (err) {    message.reply('League not found. Try: lcs, lec, lck, lpl');  }}

Command: Team Roster

Show a team's current lineup:

JavaScript
// !lol roster cloud9if (message.content.startsWith('!lol roster ')) {  const slug = message.content.slice(12).trim().toLowerCase();   try {    const { data } = await api.get(`/teams/${slug}/roster`);     const embed = new EmbedBuilder()      .setTitle(`${data.data.teamName} — Current Roster`)      .setColor(0x00E5CC);     data.data.players.forEach(player => {      embed.addFields({        name: player.role.toUpperCase(),        value: `**${player.name}** (${player.realName})`,        inline: true,      });    });     message.reply({ embeds: [embed] });  } catch (err) {    message.reply(`Team "${slug}" not found. Use the team slug (e.g., cloud9, t1, fnatic).`);  }}

Command: Head-to-Head

Compare two teams' records:

JavaScript
// !lol h2h t1 gengif (message.content.startsWith('!lol h2h ')) {  const parts = message.content.slice(9).trim().split(' ');  if (parts.length !== 2) {    message.reply('Usage: !lol h2h team1 team2');    return;  }   try {    const { data } = await api.get(`/teams/${parts[0]}/h2h/${parts[1]}`);    const h2h = data.data;     const embed = new EmbedBuilder()      .setTitle(`${h2h.team1.name} vs ${h2h.team2.name}`)      .setDescription(`All-time record: **${h2h.team1.wins}** - **${h2h.team2.wins}**`)      .setColor(0x00E5CC)      .addFields(        { name: 'Last 5 Matches', value: h2h.recentResults || 'N/A' },      );     message.reply({ embeds: [embed] });  } catch (err) {    message.reply('Could not find head-to-head data for those teams.');  }}

Command: Today's Schedule

JavaScript
// !lol todayif (message.content === '!lol today') {  try {    const { data } = await api.get('/schedule/today');     if (!data.data || data.data.length === 0) {      message.reply('No matches scheduled for today.');      return;    }     const embed = new EmbedBuilder()      .setTitle("Today's LoL Esports Matches")      .setColor(0x00E5CC)      .setTimestamp();     data.data.forEach(match => {      embed.addFields({        name: `[${match.league}] ${match.time}`,        value: `**${match.team1}** vs **${match.team2}**`,      });    });     message.reply({ embeds: [embed] });  } catch (err) {    message.reply('Error fetching schedule.');  }}

Adding a Help Command

JavaScript
if (message.content === '!lol help') {  const embed = new EmbedBuilder()    .setTitle('LoL Esports Bot Commands')    .setColor(0x00E5CC)    .setDescription([      '**!lol live** — Show live matches',      '**!lol today** — Today\'s schedule',      '**!lol standings <league>** — League standings (lcs, lec, lck, lpl)',      '**!lol player <name>** — Player stats (faker, caps, chovy)',      '**!lol roster <team>** — Team roster (cloud9, t1, fnatic)',      '**!lol h2h <team1> <team2>** — Head-to-head record',    ].join('\n'));   message.reply({ embeds: [embed] });}

Start the Bot

Add the login call and run it:

JavaScript
client.login(process.env.DISCORD_TOKEN);

bash
node index.js

Your bot should come online. Type !lol help in any channel it has access to.

Deploying for 24/7 Uptime

For production, host on:

  • Railway — Free tier, easy deploy from GitHub
  • Fly.io — Free tier, globally distributed
  • Any VPS — $5/month on DigitalOcean or Hetzner

The bot uses minimal resources (< 50MB RAM).

Adding Caching

Reduce API calls by caching frequently requested data:

JavaScript
const cache = new Map(); async function cachedGet(path, ttl = 60000) {  const cached = cache.get(path);  if (cached && Date.now() - cached.time < ttl) return cached.data;   const { data } = await api.get(path);  cache.set(path, { data, time: Date.now() });  return data;} // Use it: standings don't change often, cache 5 minconst standings = await cachedGet('/leagues/lcs/standings', 300000);

Next Steps

  • Slash commands: Upgrade from ! prefix to Discord's native slash commands for better UX
  • Match alerts: Use webhooks to notify your server when matches start
  • Leaderboard tracking: Weekly KDA/stats leaderboards for your fantasy league
  • Champion drafts: Show live pick/ban data during matches

Get Started

The full source code above is everything you need. Copy it, plug in your keys, and you have a working LoL esports bot.

  • Get your free API key — 500 calls/month, no credit card
  • Create a Discord bot at discord.dev
  • Copy the code above and run node index.js
  • Questions? Join our Discord and we'll help you get set up.

    ---

    Related reading:

    Start building with esports data today

    Free tier available. Same API shape across Call of Duty, Fortnite, LoL, Dota 2, and more.