A Discord bot for Twitch notifications watches a list of channels and posts in your server when one goes live, usually as an embed with the stream title, the game and a role ping. For one streamer and one announcement channel, a free bot does this well: Streamcord's free plan covers five Twitch channels per server, and MEE6's Twitch alerts come with its Premium plan (as of September 2026).
A custom bot earns its price when the list is a roster rather than a person: a league's teams, a creator network, casters across several games. Then the rules matter. Which channel does each alert go to, who gets pinged and when, and what stops a stream that drops and reconnects from pinging everyone twice? That's what I build, on your own Discord application.
What the bot does
For your members
- A player on your roster starts streaming.
- Within about a minute, an embed appears in the channel for the game they're playing, with the title, category, a fresh thumbnail and a link.
- Members who opted into the match-day role get pinged on match days. The rest of the week the alert posts quietly.
- If the stream drops and comes back 10 minutes later, nothing is posted again.
For your admins
- The roster is the source. Streamers come from your team database or Google Sheet, so a new signing is watched from the next sync.
- Routing by your rules. By Twitch category, by team, by division or by platform, including a staff channel for streams in the wrong category.
- Cooldowns and ping schedules per channel, set with slash commands instead of a config file.
- Extras: a live role for members who are streaming, and Twitch schedules copied into Discord events.
I built a simpler version for Rivals League in 2026. It watches one channel, the league's own. A scheduled function checks Twitch every minute, and when the stream goes live it posts to each game's Discord channel (CS2, League of Legends and VALORANT) through a webhook. Each post uses that game's colors and names the division that plays that day. Every channel has its own six-hour cooldown, so a stream that drops and comes back doesn't ping everyone twice.
What it connects to
Twitch: polling or EventSub
There are two ways to learn that a Twitch stream started, and I pick per project.
| Polling Get Streams | EventSub stream.online |
|
|---|---|---|
| How | Ask Twitch every minute which channels are live | Twitch calls your server, or pushes over a WebSocket, when a stream starts |
| Delay | Up to your polling interval | Sent when the stream starts |
| Limits | 100 channels per request; one request a minute costs 1 point from a per-minute bucket that Twitch's example headers show at 800 | Webhooks: app access token, HTTPS on port 443, reply within a few seconds. Each subscription for a streamer who hasn't authorized your app costs 1 against your limit (10,000 in Twitch's example responses) |
| Catches | Needs saved state to spot the offline-to-live change | The event names the broadcaster, stream type and start time but not the title or game, so the bot still calls Get Streams. Delivery is at least once, so duplicates must be skipped |
WebSockets look simpler because they need no public URL, but they require a user access token and allow a total cost of 10, which means about 10 streamers who haven't authorized your app. Missed events aren't replayed after a disconnect either. For a roster I use webhooks, or conduits, which take an app token and deliver over webhooks or WebSockets, and keep a slow poll as a safety net. For a short list on serverless hosting, polling every minute is simpler and costs nothing extra.
YouTube: uploads are easy, live streams are not
YouTube pushes a notification through Google's PubSubHubbub (WebSub) hub when a channel uploads a video or edits a title or description. Subscriptions are leases that must be renewed, and pushes can go missing: on September 2, 2026, Streamcord's status page reported YouTube alerts not being sent for some streamers, elevated errors at Google's hub and subscriptions that failed to renew. I pair push with a cheap fallback check.
A push only says a video appeared. The bot then calls videos.list (1 quota unit) and reads liveBroadcastContent, which is upcoming or live for broadcasts, and actualStartTime, which is empty until the stream begins. Searching for live streams doesn't scale: since June 1, 2026, search.list has its own default quota of 100 calls a day, about one check every 15 minutes for a single channel.
Kick
Kick now has an official API with webhooks. Its livestream.status.updated event reports is_live, the title and the start time, an app access token can subscribe for any broadcaster, and each payload is signed so the bot can check it against Kick's public key.
The Discord side
Alerts can post through channel webhooks, which need no bot connection at all, or through the bot. Two details decide whether a role ping works. Webhooks parse only user mentions by default, so the payload must list the role in allowed_mentions. The role must also be mentionable, or the sender needs Mention Everyone.
The core of a Discord bot for Twitch notifications
This file is the whole loop for Twitch: one request for up to 100 channels, an offline-to-live check with a per-streamer cooldown, and a post to the webhook for that game. It needs no library.
// live-check.js: run checkOnce() every minute from a cron job or setInterval.
// Node 18+ (built-in fetch). TWITCH_APP_TOKEN is an app access token from
// Twitch's client credentials flow.
const COOLDOWN_MS = 6 * 60 * 60 * 1000; // six hours between alerts per streamer
// Game (Twitch category name) -> Discord webhook and the role to ping.
const ROUTES = {
VALORANT: { webhook: process.env.WEBHOOK_VALORANT, role: '111111111111111111' },
'League of Legends': { webhook: process.env.WEBHOOK_LOL, role: '222222222222222222' },
};
// One request covers up to 100 channels. Logins in lowercase, as Twitch returns them.
async function getLiveStreams(logins) {
const params = new URLSearchParams({ first: '100' }); // the default page size is 20
for (const login of logins.slice(0, 100)) params.append('user_login', login);
const res = await fetch(`https://api.twitch.tv/helix/streams?${params}`, {
headers: {
'Client-Id': process.env.TWITCH_CLIENT_ID,
Authorization: `Bearer ${process.env.TWITCH_APP_TOKEN}`,
},
});
if (!res.ok) throw new Error(`Twitch returned ${res.status}`);
const { data } = await res.json();
return data.filter((stream) => stream.type === 'live');
}
// Streams that went from offline to live and are off cooldown.
// Keep `state` in a database so a restart doesn't re-announce everyone.
function justWentLive(streams, logins, state, now = Date.now()) {
const live = new Map(streams.map((stream) => [stream.user_login, stream]));
const fresh = [];
for (const login of logins) {
const prev = state.get(login) ?? { live: false, lastAlert: 0 };
const stream = live.get(login);
if (stream && !prev.live && now - prev.lastAlert >= COOLDOWN_MS) {
fresh.push(stream);
prev.lastAlert = now;
}
prev.live = Boolean(stream);
state.set(login, prev);
}
return fresh;
}
async function announce(stream) {
const route = ROUTES[stream.game_name];
if (!route) return false; // not a game this server announces
// wait=true makes Discord report a failed post instead of dropping it silently.
const res = await fetch(`${route.webhook}?wait=true`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: `<@&${route.role}> ${stream.user_name} is live`,
allowed_mentions: { roles: [route.role] }, // webhooks don't ping roles unless told to
embeds: [{
title: stream.title || `${stream.user_name} is live`,
url: `https://twitch.tv/${stream.user_login}`,
color: 0x9146ff,
// Discord caches images by URL, so add a timestamp to get a fresh frame.
image: { url: `${stream.thumbnail_url.replace('{width}x{height}', '640x360')}?t=${Date.now()}` },
timestamp: stream.started_at,
}],
}),
});
if (!res.ok) throw new Error(`Discord returned ${res.status}`);
return true;
}
const state = new Map();
async function checkOnce(logins) {
const streams = await getLiveStreams(logins);
for (const stream of justWentLive(streams, logins, state)) {
// One broken webhook shouldn't stop the other alerts.
await announce(stream).catch((err) => console.error(`${stream.user_login}: ${err.message}`));
}
}
module.exports = { checkOnce, getLiveStreams, justWentLive, announce };I tested it on Node 24.15.0 against stand-ins for Twitch and Discord that follow the documented rules: a first go-live posts once, a stream still running doesn't repost, a reconnect inside six hours stays quiet, a game with no route posts nothing, 25 streamers going live at once all get announced, one broken webhook doesn't block the others, and more than 100 logins are trimmed to one valid request. A live build adds a database for state, retries for failed posts, a token refresh and a second request per 100 extra streamers. You can preview the embed in the webhook sender and embed builder before wiring it up.
Free bot, builder or custom?
Prices and limits are from each vendor's own pages as of September 2026.
| Option | Price | What you get | Pick it when |
|---|---|---|---|
| Discord's Twitch integration | Free | Syncs Twitch subscriber roles for Partners and Affiliates; no go-live posts | You want sub-only channels |
| Streamcord, free | $0 | 5 Twitch, 5 YouTube and 5 Kick channels per server, custom message and embed color, a live role | One streamer or a small team |
| Streamcord Pro | $2.99/mo | 250 Twitch, 10 YouTube, 10 Kick; game and title filters, cooldowns, custom bot name and avatar, schedules | A bigger Twitch list with simple filters |
| Pingcord | Free tier; Premium price not listed publicly | Free: one integration per service tracking up to 3 channels. Premium: unlimited channels and filter rules | You also want TikTok, Reddit or Facebook alerts |
| MEE6 Premium | $11.99/mo, $49.99/yr after the first year, $89.99 lifetime (list prices) | Up to 300 Twitch, 100 YouTube and 300 Kick accounts; its limits page says YouTube livestreams aren't supported | You already pay for MEE6 |
| Zapier | Free: 100 tasks a month, checks every 15 minutes; Professional from $19.99/mo billed annually | A "New Live Stream by Streamer" trigger into a Discord message | A few alerts where a delay is fine |
| Custom bot | From $590, plus optional Care from $49/mo | Your roster, routing, ping rules and data, on your own bot | Alerts depend on your data or rules |
Free is enough when you announce one streamer, or a handful, into one channel with a default message. Don't pay me to rebuild Streamcord.
Custom makes sense when the watch list lives in your own roster or database, when routing depends on team or division rather than a title keyword, when pings follow a match schedule, when you need YouTube live detection across many channels, or when alerts are one job in a bot you already run.
What a custom stream alert bot costs
A stream notification bot usually lands in the Starter tier: from $590, delivered in 1 to 2 weeks. That covers one platform with routing, cooldowns, role pings and up to about five slash commands, plus the source code, setup docs, 30 days of bug fixes and the first month of Care.
It moves to Community, from $1,490, when it syncs a roster from your database or sheet, covers several platforms, adds a live role and schedule sync, or is one of up to three systems in the same bot. A web dashboard where teams manage their own streamers, or one bot serving many servers, is Platform work from $3,500. Tiers are on the pricing page, and the cost estimator gives a quick range.
What moves a quote:
- Up: YouTube live detection for many channels, EventSub with a public endpoint, several platforms, multi-server setups.
- Down: Twitch only, a fixed list, alerts through webhooks with no bot user.
A polling notifier can run as a scheduled function with nothing to keep online, as the 24/7 hosting guide explains. Care is $49 a month if you'd rather I host and watch it.
How the build works
- Brief. Send the brief: platforms, where the roster lives, channels and ping rules. I reply within one business day.
- Spec. We agree on routes, cooldowns and ping schedules, which become the acceptance checklist. Unsure? The Bot Blueprint is $249, credited if you order within 30 days.
- Build and test in a private server, with test streams and a dry-run mode that posts without pinging.
- Launch on your own Discord application and API keys. You pay 50% to book the slot and 50% on acceptance.
- Aftercare. Thirty days of fixes, then Care if you want it.
For leagues, alerts usually sit next to registration and team channels: see esports and league bots.
Technical notes
- Intents. A webhook-only notifier never connects to the gateway. A bot with slash commands needs only Guilds. A live role reads members' Streaming activity, which requires the privileged Presence intent, and Discord's Streaming activity only accepts Twitch and YouTube URLs. Apps seen by fewer than 10,000 users switch it on in the Developer Portal; above that, Discord reviews it.
- Permissions. Send Messages and Embed Links in alert channels, Manage Webhooks if the bot creates its own webhooks, Mention Everyone only for roles that aren't mentionable, Manage Roles for a live role, and Create Events for schedule sync. The permissions calculator builds the invite link.
- Data. The watch list, live state and an alert log sit in a database you own, platform credentials stay in your accounts, and every incoming webhook is signature-checked.