Last updated: September 2026

Rage bait detection with Jev

The model is the easy part. The hard part is defining rage bait tightly enough that a probability means something, and here is the definition that works: a post built so that the cheapest available response is an angry reply. Three conditions have to hold at once. The claim is stated at maximum provocation rather than maximum accuracy, a group is named or clearly implied as the target, and the post leaves no way to agree, only to object.

Encode that as a Noul, add a separate Score for how hard the post is trying, and Jev returns both in one parallel pass for an estimated 1.8 cents per 1,000 posts at TypeSafe's published input price. This page has the rubric, the thresholds, the false positives you should expect, and the reason none of it is theoretical: you can run the sibling engagement-bait rule yourself in the free post scorer.

Define it before you score it

Jev reads instructions literally, and TypeSafe documents indirection as degrading answer quality, so "is this rage bait" on its own is close to useless as a question. Everyone has a different threshold for what counts, and the model will pick one of them at random. What makes the rule scoreable is naming the three conditions and then, more importantly, naming everything that looks like bait and is not.

What it is
The tell
Verdict
A strong opinion
It carries an argument, a reason, or a piece of evidence.
Not bait. You can disagree with the reasoning.
A genuine complaint
There is a specific subject and a specific grievance.
Not bait. Someone is annoyed about a real thing.
An uncomfortable fact
Accuracy is the point. The provocation is a side effect.
Not bait. Flagging this is the worst failure mode.
Satire or an obvious joke
The exaggeration is the payload, and the reader is in on it.
Not bait, and the hardest case for any model.
Engagement bait
It asks directly. Like if you agree. Comment your city.
A different rule. Score it separately.
Rage bait
Maximum provocation, an out-group, and no way to agree.
Bait. All three at once.

Two of those rows become rules and the rest become the false criteria. Which primitive answers which shape of question is covered on Choice, Score and Noul.

The row that matters most is the uncomfortable fact. A filter that hides accurate, badly-received information is worse than no filter, because the reader never learns it was hidden. That is why the false half of the criteria object does the real work in the request below, and why it is longer than the true half.

The other row worth separating out is engagement bait. Engagement bait asks. Rage bait never asks, because asking would break the effect. They are two rules, not one, and a single post can score high on both.

The rubric, as a real request

Two Nouls and one Score, one round trip, pinned to jev-1.13.0 so that calibrated thresholds do not move when the jev-latest alias does.

request.json
{
  "state": {
    "post": "Nobody who uses a framework has ever written real software. Prove me wrong.",
    "platform": "X (Twitter)"
  },
  "model": "jev-1.13.0",
  "questions": {
    "rage_bait": {
      "type": "noul",
      "instructions": "The post is built so that the cheapest available response is an angry reply. All three must hold: the claim is stated at maximum provocation rather than maximum accuracy, a group is named or clearly implied as the target, and the post leaves the reader no way to agree, only to object.",
      "criteria": {
        "true": "A sweeping dismissal of an identifiable group, an invitation to argue in place of an argument, a claim written to be indefensible on purpose.",
        "false": "A strong opinion that carries a reason. A specific complaint about a specific thing. An uncomfortable fact where accuracy is the point. Satire the reader is in on."
      }
    },
    "engagement_bait": {
      "type": "noul",
      "instructions": "The post explicitly asks the reader for likes, reposts, replies, follows, or bookmarks."
    },
    "provocation": {
      "type": "score",
      "instructions": "How hard this post is working to provoke a reaction rather than to be understood",
      "criteria": [
        "Not at all. Written to be understood.",
        "Mildly pointed. A sharp opinion, plainly stated.",
        "Deliberately provocative. Phrasing chosen for heat.",
        "Maximally provocative. The heat is the entire content."
      ]
    }
  }
}
bash
curl -X POST https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json

An illustrative response, in the documented shape. The values are written to show the fields, not captured from a call:

response.json
{
  "model": "jev-1.13.0",
  "answers": {
    "rage_bait": { "type": "noul", "noul": 0.934 },
    "engagement_bait": { "type": "noul", "noul": 0.071 },
    "provocation": {
      "type": "score",
      "score": 2.83,
      "legend": {
        "0": "Not at all. Written to be understood.",
        "1": "Mildly pointed. A sharp opinion, plainly stated.",
        "2": "Deliberately provocative. Phrasing chosen for heat.",
        "3": "Maximally provocative. The heat is the entire content."
      },
      "probabilities": { "0": 0.00, "1": 0.04, "2": 0.09, "3": 0.87 },
      "confidence": 0.861
    }
  },
  "usage": { "input_tokens": 417, "output_tokens": 71 }
}

Note the two different answer shapes. Each Noul returns only its noul float, with no confidence field. The Score returns its legend, the full probability distribution and a confidence number. That means the Score is the only answer on this page that can gate itself, which is exactly how the filter below uses it.

Do not read the Score as a magnitude

A Score like the 2.83 above is a probability-weighted mean of level indices, and TypeSafe is explicit that the values are not linearly interpolatable in meaning. 1.5 does not mean halfway between level 1 and level 2. Use it to rank and to threshold. Do not multiply it by anything and call the result intensity.

Thresholds, and why this filter is quiet

TypeSafe declines to publish universal numbers, and their guidance is to start conservative and adjust against your own data. For rage bait specifically the reason to be conservative is structural: the category is genuinely contested, so your false positive rate will be higher than it would be for spam or a credential grab, no matter how good the rubric is.

Band
Action
Why
0.88 and above, with provocation at level 2 or higher
Hide, with the reason and the number on the label
Two independent answers agreeing is a much stronger signal than one high float.
0.60 to 0.88
Collapse behind one click
Cheap to be wrong. The reader opens it if the label looks unfair.
Below 0.60
Show normally
Most sharp opinions land here, and they should.
Anywhere near 0.50
Treat as unknown
A Noul has no confidence field, so the band around 0.5 is the uncertainty.
filterPost.ts
// Two cut points, and the gentler one does most of the work. Rage bait is a
// contested category, so this filter should be quieter than a spam filter.
const RAGE_BAIT_HIDE = 0.88;
const RAGE_BAIT_DEMOTE = 0.60;

interface Answers {
  rage_bait: { noul: number };
  engagement_bait: { noul: number };
  provocation: { score: number; confidence: number };
}

export type FeedAction =
  | { action: 'hide'; reason: string }
  | { action: 'collapse'; reason: string }
  | { action: 'show' };

export function filterPost(a: Answers): FeedAction {
  const p = a.rage_bait.noul;

  // The provocation Score is the only answer here that carries a confidence
  // number, so it is the only one that can gate itself. Below 0.5 the
  // distribution was flat enough that the level is not worth acting on.
  const provocative = a.provocation.confidence >= 0.5 && a.provocation.score >= 2;

  if (p >= RAGE_BAIT_HIDE && provocative) {
    return { action: 'hide', reason: `rage bait ${Math.round(p * 100)}%` };
  }
  if (p >= RAGE_BAIT_DEMOTE) {
    return { action: 'collapse', reason: `rage bait ${Math.round(p * 100)}%` };
  }
  if (a.engagement_bait.noul >= 0.85) {
    return { action: 'collapse', reason: 'asks for engagement' };
  }
  return { action: 'show' };
}

The hide path requires two answers to agree, not one to be high. That is the whole trick, and it costs nothing, because both answers came back in the same request. Full method for fitting the numbers to your own feed is on setting a Jev confidence threshold.

This is not theoretical

The free post scorer at /tools/will-it-go-viral sends seven questions to Jev on every draft: one Score for how far the post will travel against five rubric levels, three Scores for hook, clarity and specificity, and three Nouls for AI slop, engagement bait and toxicity. It is free and needs no signup and no API key.

The engagement-bait Noul behind that tool reads: "This post explicitly asks the reader for likes, reposts, replies, follows, or bookmarks." That is the sibling rule to the one on this page, running against real drafts today. Rage bait is a harder judgment than engagement bait, but nothing about the request shape or the cost structure changes.

Worth stating plainly, since the tool returns a probability distribution TypeSafe describes as calibrated, which we have not independently verified: Jev cannot return a value outside the schema you declared, so a Noul is always a float between 0 and 1. It can still be wrong about the post. "Cannot hallucinate" is a claim about schema conformance, not factual accuracy. TypeSafe's launch post says the 0% figure "is not empirical", and its CEO acknowledged on Hacker News that a schema-valid answer can be factually incorrect. On a contested category like this one, that gap is the whole reason to demote rather than delete.

Other people are building the same filter without writing it down. The Sweep extension ships rage bait as one of seven preset rules across X, Reddit, LinkedIn and Hacker News, sends one Noul per rule, and blurs matches with a label showing the rule and the probability. Its reported cost with three rules is about $0.02 per 1,000 posts, which is the same arithmetic as this page from a different direction.

Filter a niche feed down to what is worth replying to

A rage bait score is most useful on the way in, not on the way out. Two OpenTweet surfaces are the obvious place for it.

  • Inspiration. A niche feed you pull ideas from. A post engineered for anger is the worst possible thing to repurpose, because the format does not survive being rewritten in your own voice. Score on the way in, keep what is left.
  • Replies. Replying to bait is how a good account spends a week arguing. Collapse the high scorers with the reason visible, and reply to the rest.

Jev decides. An LLM writes the reply, because Jev generates no text at all. OpenTweet publishes through one endpoint, /api/v1/posts:

bash
curl -X POST https://opentweet.io/api/v1/posts \
  -H "Authorization: Bearer ot_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Boring take: frameworks are fine. Here is the one measurement that changed my mind.",
    "publish_now": true,
    "platforms": ["x", "bluesky", "linkedin"]
  }'

7-day free trial. Cancel anytime.

Frequently asked questions

What is rage bait, precisely?

A post built so that the cheapest available response is an angry reply. Three things have to be true at once: the claim is stated at maximum provocation rather than maximum accuracy, an out-group is named or clearly implied as the target, and the post leaves no way to agree, only to object. A strong opinion has an argument. A complaint has a grievance. Rage bait has a reply rate.

Can Jev detect rage bait?

Yes, as a text judgment. Declare it as a Noul with true and false criteria that name the boundary cases, and Jev returns a probability between 0 and 1 in one parallel pass. It cannot see the quote-post pile-on, the reply count or the image, so the language is all it judges.

Is rage bait the same as engagement bait?

No, and conflating them is the most common modelling mistake here. Engagement bait asks directly: like if you agree, repost this, comment your city. Rage bait never asks, because asking would break the effect. They need separate rules with separate thresholds, and a post can be both.

What threshold should hide a rage bait post?

Somewhere above 0.85 if you are going to hide anything, and a lower cut around 0.6 for demoting or collapsing. Pick both from your own labeled feed. The category is genuinely contested, so a rage bait filter has a structurally higher false positive rate than a spam filter and it should act more gently.

Is this rubric running anywhere I can try?

The adjacent rule is. The free post scorer at /tools/will-it-go-viral sends seven questions to Jev on every draft, and one of them is an engagement-bait Noul: whether the post explicitly asks the reader for likes, reposts, replies, follows or bookmarks. Rage bait is a different rule than that one, but the request shape is identical, and the tool is free and needs no signup.

What does it cost to score a feed for rage bait?

Two Nouls and one Score over a single post is roughly 420 input tokens, so an estimated 1.8 cents per 1,000 posts at $0.042 per 1M input tokens with output free. The rule text is most of that, and you re-send it on every call, so the ruleset size rather than the post length is what sets your bill.

Score the feed. Post the boring take.

One API and one calendar for X, Bluesky and LinkedIn, from $11.99 a month. Bring your own judgment layer.

7-day free trial. Cancel anytime.