Jev Events

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.

A chat message it would read
viewer_42
is anyone else getting no sound?? been like this for 2 min

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

monitor.ts
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. 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. 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. 3

    Put your TypeSafe API key in .env.

    echo "TYPESAFE_API_KEY=<your key>" >> .env
  4. 4

    Save the code as monitor.ts, and run it.

    npx tsx --env-file=.env monitor.ts
  5. 5

    Native actions log a [dry-run] would … line instead of running. When those look right, set dryRun: 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_channel

To 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 twitch

By 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.

OptionDefaultWhat it does
channelThe account's own channelReads another channel instead, such as "somechannel"
ignore!commands and well-known botsWhat 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

ActionWhat it doesScope
twitch.timeout({ seconds?, reason? })Times the chatter out, 600 seconds by defaultmoderator:manage:banned_users
twitch.ban({ reason? })Bans the chatter. Prefer a timeout unless you're suremoderator:manage:banned_users
twitch.deleteMessage()Deletes the messagemoderator:manage:chat_messages
twitch.warn({ reason? })Sends a warning the chatter must acknowledge before chatting againmoderator:manage:warnings
twitch.reply(text)Replies to the message in chatuser:write:chat
twitch.say(text)Says something in chatuser:write:chat
twitch.clip()Clips the live stream. Pair it with burst() so a hype moment makes one clip, not fiftyclips: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:

FieldTypeNotes
textstringThe message, with emotes as their names
author{ id, login, name, roles }roles holds broadcaster, moderator, vip, subscriber or staff
channelstringThe channel's login
firstMessagebooleanThe chatter's first message in this channel
reply{ author, text }The message this one replies to, if any
bitsnumberBits 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 watch runs at a time.
  • A server with one account of your own. twitch.fromEnv() builds the connection from TWITCH_CLIENT_ID and TWITCH_ACCESS_TOKEN, plus TWITCH_REFRESH_TOKEN and TWITCH_CLIENT_SECRET when 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.

On this page