Twitch
Judge chat as it happens, then time out, delete, warn, reply or clip.
Jev Events reads chat as each message arrives, asks Jev your questions about it, and runs what you pick on the answers. Choose below, and the code and the steps to run it fill in.
With your own Twitch, on your computer. The quickest way to try it.
What to watch
Chat. Live chat, read as the signed-in account. Actions need it to be the channel's owner or a moderator.
What to ask
Jev answers every question about each chat message, in one request.
What to do
Each rule runs when an answer is likely enough. Leave the rules out to print every answer.
Times the chatter out. Moderators, VIPs and the broadcaster are never touched.
Deletes the message from chat.
The code
import { monitor, recipes } from "jev-events";import { twitch } from "@jev-events/twitch";const mods = monitor({ source: twitch.chat(), questions: { hateful: recipes.chat.hateful, spam: recipes.chat.spam, }, // Native actions only log what they would do. Set dryRun: false once the answers look right. dryRun: true,}) .on("hateful", { min: 0.9 }, twitch.timeout({ seconds: 600 })) .on("spam", { min: 0.85 }, twitch.deleteMessage());await mods.start();console.log("Watching chat. Stop with Ctrl-C.");Run it
- 1
Make a folder for it, and install Jev Events and
tsx, which runs TypeScript. Jev Events needs Node.js 22 or newer.mkdir my-monitor && cd my-monitornpm init -y && npm pkg set type=modulenpm i jev-events @jev-events/twitch tsx - 2
Sign in with your Twitch account. The first time, the command walks you through registering your own Twitch app.
npx jev-events auth twitch - 3
Put your TypeSafe API key in
.env.echo "TYPESAFE_API_KEY=<your key>" >> .env - 4
Save the code as
monitor.ts, and run it.npx tsx --env-file=.env monitor.ts - 5
Native actions log a
[dry-run] would …line instead of running. When those look right, setdryRun: false.
Try it without code
Reading a public chat needs no Twitch account. Pick any live channel and watch what Jev says about each message:
npx jev-events watch twitch:some_live_channelTo read your own channel as your account, which is what actions need, install the integration and sign in once:
npm i jev-events @jev-events/twitch
npx jev-events auth twitch
npx jev-events watch twitchBy default it asks what kind of message each one is and whether it's hateful. Ask your own with --ask "spoiler=Does this reveal the story?". See the CLI.
The first time, auth twitch walks you through registering your own Twitch app, then shows a code to approve on Twitch. It asks for every scope below; pass --scopes to ask for fewer.
The source
twitch.chat() emits each new chat message in the signed-in account's own channel. In a web app, every streamer who connects has their own channel read, as their own account.
| Option | Default | What it does |
|---|---|---|
channel | The account's own channel | Reads another channel instead, such as "somechannel" |
ignore | !commands and well-known bots | What to skip before judging: { commands: false } judges commands too, and bots takes your own list of logins |
twitch.chat("somechannel") is short for twitch.chat({ channel: "somechannel" }). To act in someone else's channel, the account has to be a moderator there: the broadcaster types /mod <your account> in chat.
Chat comes over a WebSocket that stays open, so in a web app it runs in a worker that stays up, not in a serverless function. The builder writes that worker.ts for you. To read a channel without signing in, use twitchChat(channel) from jev-events/public.
Actions
| Action | What it does | Scope |
|---|---|---|
twitch.timeout({ seconds?, reason? }) | Times the chatter out, 600 seconds by default | moderator:manage:banned_users |
twitch.ban({ reason? }) | Bans the chatter. Prefer a timeout unless you're sure | moderator:manage:banned_users |
twitch.deleteMessage() | Deletes the message | moderator:manage:chat_messages |
twitch.warn({ reason? }) | Sends a warning the chatter must acknowledge before chatting again | moderator:manage:warnings |
twitch.reply(text) | Replies to the message in chat | user:write:chat |
twitch.say(text) | Says something in chat | user:write:chat |
twitch.clip() | Clips the live stream. Pair it with burst() so a hype moment makes one clip, not fifty | clips:edit |
Reading chat needs user:read:chat. reason and text can be strings or functions of the event, such as (e) => `@${e.item.author.name} thanks!` . The default reason shows moderators why it acted, for example jev-events: kind:hateful (93%). Actions are dry-run until you pass dryRun: false to monitor().
Chat messages
Handlers receive a TwitchChatItem:
| Field | Type | Notes |
|---|---|---|
text | string | The message, with emotes as their names |
author | { id, login, name, roles } | roles holds broadcaster, moderator, vip, subscriber or staff |
channel | string | The channel's login |
firstMessage | boolean | The chatter's first message in this channel |
reply | { author, text } | The message this one replies to, if any |
bits | number | Bits cheered with the message |
Jev sees the text, the author's name and roles, firstMessage and the reply, plus the three messages before it. See what Jev sees.
Good to know
- Protected users. Actions never run on the broadcaster, moderators, VIPs or Twitch staff.
- Fresh or not at all. Messages that waited more than 10 seconds to be judged are dropped instead of acted on late.
- Three connections. Twitch allows 3 chat connections per account and app, so one account can be read by at most 3 monitors or
watchruns at a time. - A server with one account of your own.
twitch.fromEnv()builds the connection fromTWITCH_CLIENT_IDandTWITCH_ACCESS_TOKEN, plusTWITCH_REFRESH_TOKENandTWITCH_CLIENT_SECRETwhen you have them:await mods.start({ connections: [twitch.fromEnv()] }). Renewed tokens can't be written back to your environment. A Confidential app's refresh token keeps working, but a Public app's is spent after the first renewal, so use a Confidential app here.