
Security News
/Research
Fake Corepack Site Distributes Infostealer and Proxyware to Developers
A fake corepack.org site is impersonating the Node.js tool and delivers an infostealer and proxyware to developers who download it.
youtube-chat-next
Advanced tools
Read any public YouTube live chat with no API key, no OAuth, no Google Cloud project. Just a video, channel, or handle. Unofficial continuation of youtube-chat by LinaTsukusu, now maintained for modern YouTube.
Original repository: https://github.com/LinaTsukusu/youtube-chat
This fork: https://github.com/LucasSantana-Dev/youtube-chat-next
License: MIT (original copyright preserved)
The reason to reach for this library is that it needs zero credentials:
liveChatMessages.list against a daily quota; this has none.import { LiveChat } from "youtube-chat-next"
// This is the whole setup. No keys, no config, no accounts.
const chat = new LiveChat({ handle: "@LofiGirl" })
chat.on("chat", (item) => console.log(item.author.name))
await chat.start()
That capability is the load-bearing design goal of this fork. It is preserved deliberately, and the daily drift canary exists to catch the day YouTube changes something that would break it.
Needing no key is exactly because this is not an official, sanctioned path, see below.
This library scrapes YouTube's private InnerTube endpoint (/youtubei/v1/live_chat/get_live_chat) by extracting YouTube's own public web API key and a continuation token from the watch page. You never provide the key; it is read from the page for you. It is not the official YouTube Data API. It is not affiliated with, endorsed by, or supported by YouTube or Google.
liveChatMessages.list is compliant and stable. Real tradeoff: it requires a Google Cloud project, an API key, and OAuth, is capped by a daily quota, and can realistically only read chat for streams you own or moderate, not arbitrary public streams. This library needs none of that and reads any public stream, but carries the risks above. Pick the official API when compliance and stability matter more than zero-setup and open access; pick this when they don't.error listener. Node's EventEmitter throws if an error event fires with no listener, which will crash your process.RateLimitError and its Retry-After, lower your request volume (a larger interval), and stop after bounded retries rather than hammering through the limit.Verified against live YouTube on 2026-07-15:
Respects YouTube's polling rate. YouTube's response includes timeoutMs (typically 10000ms) saying how often to poll. Upstream polled every 1000ms, ignoring this, about 10× more requests than YouTube invites. Now interval is a floor; the library honours max(interval, timeoutMs). Measured: 71 messages delivered, 4 requests instead of ~33.
No longer mutates the global axios. Upstream set axios.defaults.headers.common["Accept-Encoding"] = "utf-8", changing the singleton for your entire application. Now uses a private axios instance.
Detects stream end. When a stream ends, continuationContents disappears from YouTube's response. Upstream crashed with TypeError, swallowed it, and retried forever. Now emits an end event cleanly.
No overlapping requests. Replaced setInterval with a self-scheduling fetch loop. If a request takes longer than the polling interval, upstream would fire overlapping requests. Now each fetch waits for the previous one to complete.
Typed errors. Four new error classes let you distinguish "YouTube changed the page shape" from "rate limited" from "stream finished":
ScrapeError, parsing failed (YouTube likely changed the HTML/JSON structure)NotLiveError, channel is not currently liveRateLimitError, 429 or 403 from YouTube (back off and retry)ParseError, response is malformedBackoff with jitter on rate limits. On 429/403, honours the Retry-After header, adds random jitter, and gives up after 5 consecutive failures instead of looping forever.
Locale pinned via Accept-Language. YouTube localises the watch page by IP address, so parsing previously depended on where your process ran. Now forces Accept-Language: en-US so parsing is stable.
axios upgraded 1.2.0 → 1.18.1. Axios 1.2.0 did not properly decompress YouTube's gzip/brotli responses. Upgrade is required for correctness.
Live contract test + daily CI canary. The test suite runs on frozen 2022 YouTube fixtures (cannot detect drift). Now npm run test:live runs against live YouTube daily to catch regressions early.
npm install youtube-chat-next
or
yarn add youtube-chat-next
JavaScript
const { LiveChat } = require("youtube-chat-next")
TypeScript
import { LiveChat } from "youtube-chat-next"
// Option A: Pass channelId (recommended)
// The library will fetch the current liveId automatically.
const liveChat = new LiveChat({ channelId: "CHANNEL_ID_HERE" })
// Option B: Pass liveId directly
// Use this if you already know the live stream ID.
const liveChat = new LiveChat({ liveId: "LIVE_ID_HERE" })
// Option C: Pass a YouTube handle (starts with @)
const liveChat = new LiveChat({ handle: "@channel_name" })
// Optional: Set the polling interval (milliseconds)
// Default is 1000. YouTube typically suggests 10000.
// The library will honour whichever is greater.
const liveChat = new LiveChat(
{ channelId: "CHANNEL_ID" },
5000 // interval in milliseconds
)
// Optional: Choose which chat view to read.
// "top" (default), YouTube's "Top chat": an algorithmically FILTERED subset.
// "live", "Live chat": every message.
// The default is "top" to match YouTube's own default; pass "live" for the full stream.
const liveChat = new LiveChat({ channelId: "CHANNEL_ID" }, 1000, "live")
Note: by default this library reads "Top chat", exactly as YouTube's watch page does, a filtered subset, not every message. This was verified by measurement, not assumed. Pass
"live"as the third argument to read the complete unfiltered chat.
// Fires when the fetch loop starts successfully.
// liveId: string
liveChat.on("start", (liveId) => {
console.log("Live chat started for stream:", liveId)
})
// Fires when a message arrives.
// chat: ChatItem (see Types section below)
liveChat.on("chat", (chatItem) => {
// message is a MessageItem[], each item is either a text run or an emoji.
const text = chatItem.message.map((m) => ("text" in m ? m.text : m.emojiText)).join("")
console.log(`${chatItem.author.name}: ${text}`)
})
// Fires when the stream ends or an unrecoverable error occurs.
// reason: string | undefined
liveChat.on("end", (reason) => {
console.log("Stream ended:", reason)
})
// Fires on errors. REQUIRED, never omit this.
// Distinguishing errors helps you respond correctly:
// - ScrapeError/ParseError: YouTube changed; alert and stop.
// - RateLimitError: Back off and retry.
// - NotLiveError: No stream currently live.
liveChat.on("error", (err) => {
if (err instanceof RateLimitError) {
console.error("Rate limited. Backing off...")
} else if (err instanceof ScrapeError) {
console.error("YouTube structure changed. Stopping.")
} else {
console.error("Error:", err)
}
})
// start() resolves to true if the stream is live and fetching,
// or false if setup failed (check the emitted error).
const ok = await liveChat.start()
if (!ok) {
console.log("Failed to start. See error event for details.")
}
liveChat.stop("Stream ended by caller")
A single message from the live chat.
interface ChatItem {
id: string // Unique message ID
author: {
name: string
thumbnail?: ImageItem // User's avatar
channelId: string
badge?: { // e.g. "Moderator", "New member"
thumbnail: ImageItem
label: string
}
}
message: MessageItem[] // Array of text + emoji
superchat?: { // Paid message (YouTube Super Chat)
amount: string // e.g. "US$1.00"
color: string // Hex color
sticker?: ImageItem // Animated sticker
}
isMembership: boolean // Is channel member?
isVerified: boolean // Verified account?
isOwner: boolean // Stream owner?
isModerator: boolean // Moderator?
timestamp: Date // Message time
}
A single text or emoji element within a message.
type MessageItem = { text: string } | EmojiItem
A reference to an image (thumbnail, emoji, sticker).
interface ImageItem {
url: string // Full URL
alt: string // Alt text / emoji name
}
An emoji or custom emote.
interface EmojiItem extends ImageItem {
emojiText: string // Emoji as text (e.g., "😀") or custom name
isCustomEmoji: boolean // Is this a channel custom emote?
}
import { LiveChat, ScrapeError, NotLiveError, RateLimitError, ParseError } from "youtube-chat-next"
// All inherit from Error and include:
// - message: string
// - name: "ScrapeError" | "NotLiveError" | "RateLimitError" | "ParseError"
import { LiveChat } from "youtube-chat-next"
const liveChat = new LiveChat({ channelId: "UCxxxxxxxxxxxxxx" })
liveChat.on("start", (liveId) => {
console.log("Listening to:", liveId)
})
liveChat.on("chat", (chatItem) => {
console.log(`[${chatItem.author.name}] ${chatItem.message
.map(m => ("text" in m ? m.text : m.emojiText))
.join("")}`)
})
liveChat.on("end", (reason) => {
console.log("Stream ended:", reason)
})
liveChat.on("error", (err) => {
console.error("Error:", err.message)
})
await liveChat.start()
// Listen until stop() is called
import { LiveChat, RateLimitError, ScrapeError } from "youtube-chat-next"
import fs from "fs"
const log = fs.createWriteStream("chat.log", { flags: "a" })
const liveChat = new LiveChat({ liveId: "KHRj-j02a1w" })
liveChat.on("chat", (chatItem) => {
const line = `${chatItem.timestamp.toISOString()} | ${chatItem.author.name}: ${
chatItem.message.map(m => ("text" in m ? m.text : m.emojiText)).join("")
}\n`
log.write(line)
})
liveChat.on("error", (err) => {
if (err instanceof RateLimitError) {
// Rate limited, back off and retry
console.warn("Rate limited. Waiting before retry...")
setTimeout(() => liveChat.start(), 30000)
} else if (err instanceof ScrapeError) {
// YouTube changed page structure, needs a code update
log.write(`CRITICAL: ${err.message}\n`)
} else {
log.write(`ERROR: ${String(err)}\n`)
}
})
await liveChat.start()
This fork exists because the original is unmaintained. It preserves the spirit and API of the original while fixing real-world issues discovered over 3+ years of use.
MIT. See LICENSE for details. Original copyright by LinaTsukusu, modifications by contributors.
FAQs
Fetch YouTube Live chat. Maintained fork of youtube-chat.
We found that youtube-chat-next demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
/Research
A fake corepack.org site is impersonating the Node.js tool and delivers an infostealer and proxyware to developers who download it.

Research
/Security News
A large-scale campaign abused GitHub Actions in compromised repositories to exploit CVE-2026-41940 in cPanel and WHM and steal server credentials.

Security News
Five frontier LLMs generated the same nonexistent package names, leaving 53 available for potential slopsquatting across PyPI and npm.