Skip to main content
You are watching a Twitch stream, and the chat moves faster than you can read it. We sort each message as it arrives: questions are highlighted, spam, insults and unrequested gameplay tips are hidden, and spoilers are blurred. A keyword list could catch spam links, but “go left, there’s a site of grace behind the tree” has no bad words in it, and neither does a plot twist. Send each message to Jev in its own request, with the stream title and game. Ask three questions about it: what kind of message it is, whether it is toxic (insulting, hateful, or harassing), and whether it spoils the game. A few plain rules then turn those answers into what the viewer sees. Guardrails for LLMs uses the same pattern to screen messages going into and out of an LLM. This demo shows a Jev-powered Chrome extension filtering a real, live Twitch chat:
Here we run 29 chat messages, written for an Elden Ring stream, through the filter. It hides seven, blurs two, and highlights six as questions. Five chat messages go to Jev at once, each in its own request with the stream's game. The category, toxic, and spoiler answers come back, and decide() shows, highlights, hides, or blurs each message in the viewer's chat Five chat messages go to Jev at once, each in its own request with the stream's game. The category, toxic, and spoiler answers come back, and decide() shows, highlights, hides, or blurs each message in the viewer's chat We build two functions: classify() asks the three questions about one message, and decide() turns the answers into show, highlight, dim, hide, or blur. When you change a rule, such as hiding spam, you rerun only decide(), with no new request to Jev.

Setup

Create a new file named twitch_chat_filter.py. As you work through this cookbook, copy each Python code block into that file, in order. For a live run, set TYPESAFE_API_KEY in your environment. Once you have added all the blocks, run this command from the file’s directory:
Every API response is saved in json_cache.json beside the script. Download it there to replay the published numbers instead of calling the API. In a notebook, put the cache in the notebook’s working directory instead. To run everything live again, move the cache aside and set your key. Do the same when changing TYPESAFE_MODEL, because the cache key below includes the state and questions, not the model. Numbers below came from jev-1.14.0 on 2026-10-29.

Load a stretch of chat

We wrote the messages ourselves so that every kind of message turns up at least twice. Two of them are emote-only lines (emotes are small chat images on Twitch, typed as words like PogChamp). Whether a message is a spoiler or backseat advice (gameplay tips the streamer did not ask for) depends on which game is being played, so the stream title and game go into every request.

Write the questions

One request asks three questions about the same state: the stream plus one message.
  • The category question is a Choice over five kinds of message. The label it picks decides what happens to the message: show, highlight, dim, or hide.
  • The toxic question is a Noul. If its probability reaches the threshold TOXIC_AT, the message is hidden, whatever its category.
  • The spoiler question is a Noul. If its probability reaches the threshold SPOILER_AT, the message is blurred, unless it is already hidden.
The category describes the message’s main purpose. Repeated emotes and cheering count as hype; unsolicited promotion counts as spam. A link alone is not spam. conversation is the fallback when none of the other categories fits.

Classify one message

Each message becomes one request: we build the state from the message and send it with the three questions. The cache key is the state and the questions written out as JSON, and each answer is saved under that key. Editing a question changes that key, so the next run sends a new request instead of replaying the old answer.
The backseat advice comes back as backseat at 1.00, with low toxic and spoiler. The Night of Black Knives message favors conversation over backseat, 0.67 to 0.32, and the spoiler question catches it. The last message reads like an insult, but toxic stays low. With “love you tho” on the end, it fits the “light banter” description in the question.

Decide what the viewer sees

Deciding what the viewer sees needs no new request. We read only the answers and apply three checks in order:
  1. A message at or above TOXIC_AT is hidden.
  2. A message whose category has the rule hide is hidden.
  3. A message at or above SPOILER_AT is blurred.
Any message left gets the rule for its category. On the sample messages, every toxic probability is either at least 0.97 or at most 0.38, and every spoiler probability either at least 0.95 or at most 0.38. Any threshold above 0.38 and up to 0.95 gives the same result here. Your own channel’s chat is likely to have probabilities inside that range, so check both thresholds against it before using them live.

Filter the whole chat

The three messages above get cached, leaving 26 new requests on a first live run. No message depends on another, so the requests run in parallel, and pool.map returns the answers in chat order. The chat prints below as the viewer sees it. A hidden message shows only the reason it was hidden, a blurred one shows only its author, and highlighted questions are marked with >>. The label on the right of each line is the category answer.
To run the filter on your own chat, replace STREAM and CHAT. For several channels, pass each channel’s stream to classify(). If you hit a rate limit, lower max_workers and run again; completed answers are already cached. In a live integration, store the platform’s message ID, or a reference to the message’s page element, with each request. Use it to update the right message when its answers arrive.

Change a rule without a new request

Because the answers only describe the message, we can change the rules and reuse the 29 results from the step above. Here we hide hype (cheering and emote-only lines) and dim conversation.

Before you use it on a live channel

  • Each message waits for its answer. Requests in this cookbook took a median of about 0.1 seconds, so a new line appears in chat before its answer comes back. Either hide each new line until its answer arrives, or accept that a toxic message is on screen for that long.
  • One request at a time can’t keep up with a busy chat. Run several requests at once, as in Filter the whole chat.
  • At the time this cookbook was published, Jev was trained mainly on English. It accepted other languages but answered them less accurately, so expect more misses on a non-English channel.
  • A winning category can still be uncertain. Here we always apply the winning label’s rule. Before using it live, choose how to handle messages whose top two category probabilities are close to each other, and Noul values near 0.5. You could flag those messages for review or leave them visible rather than hide them automatically. Test that policy on your own channel’s chat.
  • The spoiler check has no record of the streamer’s progress. It uses the game title and the model’s knowledge of the game to flag likely plot revelations. It cannot tell whether this viewer or the streamer already knows them. To filter only unseen plot points, supply the current progress and relevant plot information, then adapt the question to use that context.

Open it in the playground

The link below opens the playground with the state for the backseat message and the three questions filled in. Run the call live there, or edit the questions and run it again.
Open in the TypeSafe playground →