Build a CS2 Discord score bot.
This bot posts a live CS2 scoreboard to a Discord channel and keeps editing the same message as the match goes: /follow picks a live match, one shared poll of GET /cs2/matches/live updates every followed scoreboard, and the final score is written when the match ends. About 90 lines of discord.js.
- discord.js v14
- One shared poll
- Edits one message, no spam
{ "success": true, "data": [ { "id": "cs2-match-2398974", "eventName": "ROG JOURNEY Autumn 2026", "bestOf": 3, "team1Name": "Ninjas in Pyjamas", "team2Name": "Eternal Fire", "score": { "team1": 2, "team2": 0 } }, { "id": "cs2-match-2398896", "eventName": "FOX Legacy Season 1", "bestOf": 3, "team1Name": "SAW", "team2Name": "Lazer Cats", "score": { "team1": 2, "team2": 0 } } ... ]}Step 1: What do I need before I start?
Node.js 18 or newer, a Discord application with a bot user (discord.com/developers), the bot invited to your server with the applications.commands and bot scopes, and a CS2 API key. Put the three secrets in environment variables.
npm init -y && npm i discord.js
export DISCORD_TOKEN=... # Bot tab, Reset Token
export DISCORD_APP_ID=... # General Information, Application ID
export CITO_API_KEY=cito_... # citoapi.com/signupStep 2: What is the bot's code?
One file. It registers /follow and /results, keeps a map of followed match IDs to Discord messages, and runs one poll loop that edits every followed message. When a match leaves the live list, the bot fetches its final score once and stops following it.
import { Client, GatewayIntentBits, MessageFlags, REST, Routes, SlashCommandBuilder } from "discord.js"
const API = "https://api.citoapi.com/api/v1/cs2"
const H = { "x-api-key": process.env.CITO_API_KEY }
const POLL_MS = 60_000 // one request a minute for every followed match together
const followed = new Map() // matchId -> Discord message
const get = async (path) => (await (await fetch(API + path, { headers: H })).json()).data
const board = (m, final = false) =>
[
`**${m.team1Name} ${m.team1Score} - ${m.team2Score} ${m.team2Name}** (Bo${m.bestOf}) ${final ? "FINAL" : "LIVE"}`,
final ? "" : `${m.currentMap ?? "?"}: ${m.currentMapScore?.team1 ?? 0}-${m.currentMapScore?.team2 ?? 0}, round ${m.currentRound ?? "?"}`,
m.eventName ?? "",
].filter(Boolean).join("\n")
const commands = [
new SlashCommandBuilder().setName("follow").setDescription("Post a self-updating scoreboard for a live CS2 match")
.addStringOption((o) => o.setName("team").setDescription("Team name, e.g. Vitality").setRequired(true)),
new SlashCommandBuilder().setName("results").setDescription("Latest finished CS2 matches"),
].map((c) => c.toJSON())
await new REST().setToken(process.env.DISCORD_TOKEN)
.put(Routes.applicationCommands(process.env.DISCORD_APP_ID), { body: commands })
const client = new Client({ intents: [GatewayIntentBits.Guilds] })
client.on("interactionCreate", async (i) => {
if (!i.isChatInputCommand()) return
if (i.commandName === "results") {
const rows = await get("/matches/results?limit=5")
return i.reply(rows.map((m) => `${m.team1Name} ${m.team1Score}-${m.team2Score} ${m.team2Name} · ${m.eventName}`).join("\n"))
}
const team = i.options.getString("team").toLowerCase()
const match = (await get("/matches/live")).find((m) => `${m.team1Name} ${m.team2Name}`.toLowerCase().includes(team))
if (!match) return i.reply({ content: `No live CS2 match for "${team}" right now.`, flags: MessageFlags.Ephemeral })
await i.reply(board(match))
followed.set(match.matchId, await i.fetchReply())
})
async function poll() {
if (followed.size) {
const live = new Map((await get("/matches/live")).map((m) => [m.matchId, m]))
for (const [id, message] of followed) {
if (live.has(id)) {
await message.edit(board(live.get(id)))
} else {
await message.edit(board(await get(`/matches/${id}`), true)) // finished: final score, stop following
followed.delete(id)
}
}
}
setTimeout(() => poll().catch(console.error), POLL_MS)
}
client.once("ready", () => {
console.log("Logged in as", client.user.tag)
poll()
})
client.login(process.env.DISCORD_TOKEN)Step 3: How do I run it?
Run node score-bot.mjs, type /follow Vitality in a channel while Vitality is playing, and the bot posts the scoreboard and edits it every minute. /results lists the last five finished matches.
node score-bot.mjsHow many API requests does the bot use?
One request per poll, however many matches it follows, plus one per command and one per finished match. Polling every 60 seconds is about 43,200 requests a month if it runs non-stop, which fits the Pro plan (250,000); the free plan's 500 a month is for building and testing.
| Poll interval | Requests / month (always on) | Plan |
|---|---|---|
| 60 s | 43,200 | Pro |
| 30 s | 86,400 | Pro |
| 15 s | 172,800 | Pro |
| Only while a match is followed | Far fewer | Starter or Pro, depending on traffic |
CS2 live data refreshes every 15 seconds at the median, so polling faster than about 15 seconds returns the same score twice. Free and Starter see live data on a 60-second delay.
Can the bot get scores pushed instead of polling?
Yes. On Pro and Scale, register a webhook for cs2.score.updated and cs2.match.completed and update the message when the POST arrives; the webhook tutorial has the signature check. The SSE stream at /cs2/live/stream works on every plan too.
Frequently asked questions
Something missing? Email support@citoapi.com.
How do I make a CS2 live score Discord bot?
Use discord.js with a /follow command that posts a scoreboard message, then poll GET /cs2/matches/live once a minute and edit that message with the new score. The full code is above.
Will the bot spam the channel with messages?
No. It edits the one scoreboard message it posted, and writes the final score into it when the match ends.
Can I run the CS2 score bot on the free plan?
For testing, yes. Running it all day needs more than the free 500 requests a month; polling once a minute non-stop is about 43,200 a month, which fits Pro.
Related
- CS2 Discord bot APILive score and result commands for a Discord bot.
- CS2 live score APISeries, map and round score for every match in progress.
- CS2 match alert webhooksGet a signed POST when a round, map or match ends.
- WebSocket disconnects mid-matchHeartbeats, reconnect with backoff, and state back on resubscribe.
- CS2 API in Node.jsFetch CS2 data from JavaScript and TypeScript.
- CS2 API pricingFree, Starter, Pro and Scale: requests, live delay and history per plan.
Build CS2 live score apps
Free is for building and testing. Paid plans add commercial use, real-time live data and the full archive.