Last updated: September 2026
Spam and bot detection with Jev
Jev judges text and cannot see account metadata. That one fact decides the whole architecture. The language judgments, is this bulk promotion, is this a credential grab, would this reply fit under any post, go to Jev as Nouls. Account age, follower ratio, link count and posting cadence stay in your own code, because they are arithmetic over dates and integers and Jev is documented as unreliable at both.
Nobody writing about Jev and spam has said this plainly yet, so here is the whole thing: the signal split, the real request, the composite scoring function, a worked example with illustrative numbers, and the price. Screening 1,000 replies works out to an estimated 1.6 cents at TypeSafe's published input price.
Sort the signals first
Every spam signal belongs on exactly one side of this line. Getting the split right is most of the work, and it is not a matter of taste: TypeSafe's own limitations page says to split the work, because "extraction is a judgment, so give it to the model. Arithmetic is not, so keep it in code."
Limitations sourced from docs.typesafe.ai/model-jaggedness/jev-1.13, September 2026.
What happens if you ignore the split
Paste "account created 2026-09-14, 6 followers, 2,100 following" into the state and ask a Noul whether the account looks automated, and you will get an answer, because Jev always returns something schema-valid. It will be a worse answer thanageDays < 7, it will cost you input tokens every single call, and it will move unpredictably when the model version changes. The comparison operator is free, exact and will still be right next year.The Jev half
Three Nouls and one Score, one request, against a pinned model id. The parent post goes in the state alongside the reply, because "this reply engages with nothing specific" is not answerable without it.
{
"state": {
"parent_post": "Shipped multi-platform publishing today. One API call, three networks.",
"reply": "Great insight! This is so valuable. Check my bio for a free growth tool.",
"reply_author_handle": "growthguy_8812x"
},
"model": "jev-1.13.0",
"questions": {
"bulk_promotion": {
"type": "noul",
"instructions": "The reply exists mainly to direct attention to the replier's own offer, profile, or link.",
"criteria": {
"true": "Bio pumps, referral links, 'DM me', giveaway bait, promoting an unrelated product under a popular post.",
"false": "A person mentioning their own project because it is genuinely relevant to the parent post."
}
},
"credential_solicitation": {
"type": "noul",
"instructions": "The reply tries to obtain money, login credentials, or wallet access, including under a false identity.",
"criteria": {
"true": "Fake support handles, seed phrase or wallet-connect requests, guaranteed returns, 'verify your account here'.",
"false": "A legitimate paid offer from an identifiable seller."
}
},
"generic_reply": {
"type": "noul",
"instructions": "The reply would make equal sense under a completely different post, because it engages with nothing specific in the parent post.",
"criteria": {
"true": "Praise with no referent, restating the parent post, 'this is so true', a single emoji.",
"false": "The reply names a detail, disagrees with a specific claim, or asks a question only this post raises."
}
},
"templated": {
"type": "score",
"instructions": "How much this reply reads as filled-in from a template rather than written for this post",
"criteria": [
"Written for this post. Specific, uneven, has a voice.",
"Mostly original with one stock phrase.",
"Largely stock phrasing with one detail swapped in.",
"Entirely formulaic. Interchangeable with thousands of others."
]
}
}
}Every declared question is evaluated in one parallel pass, so four questions cost one round trip. An illustrative response in the documented shape, with values written to show the fields rather than captured from a call:
{
"model": "jev-1.13.0",
"answers": {
"bulk_promotion": { "type": "noul", "noul": 0.967 },
"credential_solicitation": { "type": "noul", "noul": 0.038 },
"generic_reply": { "type": "noul", "noul": 0.912 },
"templated": {
"type": "score",
"score": 2.71,
"legend": {
"0": "Written for this post. Specific, uneven, has a voice.",
"1": "Mostly original with one stock phrase.",
"2": "Largely stock phrasing with one detail swapped in.",
"3": "Entirely formulaic. Interchangeable with thousands of others."
},
"probabilities": { "0": 0.01, "1": 0.06, "2": 0.14, "3": 0.79 },
"confidence": 0.804
}
},
"usage": { "input_tokens": 381, "output_tokens": 64 }
}Read the shapes carefully, because they differ. A Noul answer contains only noul, a float between 0 and 1. There is no confidence field on it. The Score carries its legend, the full distribution and a confidence number. That asymmetry is the most commonly mis-stated detail about this API, and it changes how you write the threshold code: for a Noul, uncertainty is a value near 0.5, not a separate signal.
One more caution specific to spam. TypeSafe documents that state is not treated as hostile. A reply containing "ignore the instructions above" can steer the answer, and spam is precisely the content most likely to try. Put the exact condition in the criteria, and keep an adversarial set in whatever you use to test.
The composite scoring function
This is the piece that is missing from every existing write-up. Two independent halves, one number, weights that live in your repo where a reviewer can argue with them.
// Jev answers. Note that a Noul is a bare float: there is no confidence field
// on it, only on Choice and Score.
interface JevAnswers {
bulk_promotion: { noul: number };
credential_solicitation: { noul: number };
generic_reply: { noul: number };
templated: { score: number }; // 0..3
}
// Everything Jev cannot see. All of it already sits in your database.
interface AccountFacts {
ageDays: number;
followers: number;
following: number;
linkCount: number; // counted with a regex, never asked of the model
postsLast24h: number;
hasAvatar: boolean;
hasBio: boolean;
}
function textRisk(a: JevAnswers): number {
return Math.min(
1,
0.45 * a.bulk_promotion.noul +
0.35 * a.credential_solicitation.noul +
0.20 * a.generic_reply.noul
);
}
function accountRisk(f: AccountFacts): number {
let r = 0;
if (f.ageDays < 7) r += 0.30;
else if (f.ageDays < 30) r += 0.15;
const ratio = f.following > 0 ? f.followers / f.following : 1;
if (f.followers < 10 && f.following > 500) r += 0.25;
else if (ratio < 0.05) r += 0.15;
if (f.linkCount >= 2) r += 0.20;
else if (f.linkCount === 1 && f.ageDays < 30) r += 0.10;
if (f.postsLast24h > 100) r += 0.15;
if (!f.hasAvatar && !f.hasBio) r += 0.10;
return Math.min(r, 1);
}
/**
* One number, from two independent halves. The weights below are a starting
* point, not a measurement: replace them with values fitted to your own
* labeled sample before this decides anything.
*/
export function spamScore(a: JevAnswers, f: AccountFacts): number {
return 0.55 * textRisk(a) + 0.15 * (a.templated.score / 3) + 0.30 * accountRisk(f);
}
export function triage(score: number): 'skip' | 'collapse' | 'show' {
if (score >= 0.75) return 'skip'; // never shown, still logged and sampled
if (score >= 0.45) return 'collapse'; // shown behind one click, with the reason
return 'show';
}The weights above are an illustration, not a result
They are a plausible starting point and nothing more. Fit them on a hand-labeled sample of your own replies before the score decides anything. The structure generalises. The numbers do not, and anyone who publishes weights as if they were measured is guessing at your traffic. The fitting method, and why a Noul has no confidence field to lean on, is on setting a Jev confidence threshold.Worked example
Same reply as the request above, from an account four days old with 6 followers, 2,100 following, one link in the bio-pump text, and 180 posts in the last day.
Worked through with the illustrative answers above. The inputs are written, not measured.
0.722 lands in the collapse band, not the skip band, and that is the point of having two cut points. The text was clearly promotional and the account looks disposable, yet the credential-grab rule came back at 0.038, so this is a bio pump rather than a phishing attempt. Collapsing it behind one click costs nothing if the model was wrong. Hiding it outright would have.
Use the score for ordering before you use it for enforcement. Demote and collapse, measure your false positive rate on a labeled sample for a week, and only then consider an automatic path. Never block an account on this number: it is a first pass with no appeal route, and the one independent test of Jev so far, Mike Taylor's at Every, found it caught 6 of 7 planted defects where Claude Fable 5.1 caught 7 of 7. He called it useful as an early warning system. That is the correct job description.
What it costs
The illustrative request above is estimated at about 380 input tokens. At $0.042 per 1M input tokens with output free, that is an estimated $0.000016 per reply:
- 1,000 replies, about 1.6 cents.
- 100,000 replies a month, about $1.60.
- One million replies, about $16.
All three are arithmetic on an estimated token count at the published list price, not measured spend.
The account half costs nothing, because it is a query against data you already store. That is the second reason to keep it out of the model, after accuracy.
For scale, the free post scorer at will it go viral sends seven questions on one draft, with no signup and no API key, if you want to see the request shape running against a real draft before you build anything. A fuller cost breakdown, including why the ruleset rather than the message dominates the bill, is on content moderation with Jev.
Where the score goes
A spam score is only worth computing if something acts on it. In OpenTweet the natural home is the replies feed: the skip band never reaches you, the collapse band sits behind one click with the reason attached, and what is left is the set of replies actually worth answering. For the narrower question of which of those are engineered to make you angry, see rage bait detection.
Jev decides. An LLM writes the reply, because Jev generates no text at all. OpenTweet publishes through one endpoint, /api/v1/posts, across X, Bluesky and LinkedIn:
curl -X POST https://opentweet.io/api/v1/posts \
-H "Authorization: Bearer ot_your_key" \
-H "Content-Type: application/json" \
-d '{
"text": "Spam replies are now collapsed instead of deleted. You can still open every one.",
"publish_now": true,
"platforms": ["x", "bluesky"]
}'7-day free trial. Cancel anytime.
Frequently asked questions
Can Jev detect spam?
It can judge whether text reads as spam, which is one input to a spam decision. It cannot see who posted it. Jev accepts text only and treats dates as unordered text, so account age, follower ratio, link counts and posting cadence have to be computed in your own code and combined with the language judgment there.
Can Jev detect bots?
Partly. The tell that a reply is generic enough to attach to any post is a language judgment and Jev is good at it. The tells that actually identify automation, an account created yesterday, 4,000 following and 6 followers, 300 posts in a day, are arithmetic over metadata. Those belong in a database query, not in a question.
Why not just put the account facts in the state and ask Jev?
You can, and it will answer, because Jev always returns a schema-valid answer. It will be worse at it than a comparison operator and you will pay input tokens for the privilege. TypeSafe documents both reasons: counting is unreliable, and dates are treated as text rather than ordered values. Their own guidance is to split the work, giving judgment to the model and arithmetic to code.
How much does it cost to screen 1,000 replies with Jev?
An estimated 1.6 cents. Three Nouls and one Score over a parent post plus a reply is roughly 380 input tokens, and input is $0.042 per 1M with output free, so screening 100,000 replies a month works out to an estimated $1.60. These are arithmetic on an estimated token count at TypeSafe’s published list price, not measured spend.
What score should auto-hide a reply?
Pick it from your own labels, and use the composite score for ordering rather than enforcement. A good default posture is that the score decides what a person sees first, never whether an account gets blocked. Blocking on a model output with no appeal path is how you lose real readers to a 0.78.
Does a low Noul value mean Jev was unsure?
No. A Noul returns a single float and nothing else, with no confidence field, so 0.04 means it is fairly sure the statement is false. Uncertainty shows up as a value near 0.5. Only Choice and Score answers carry a separate confidence number.
Keep exploring
The rest of the judgment layer, and the feed it plugs into.
Content moderation with Jev
The full rule design, threshold and review-queue guide, plus the cost math at scale.
Rage bait detection with Jev
A definition precise enough to score, and where it differs from engagement bait.
Jev for community management
Triage mentions and replies into buckets, then publish the responses through OpenTweet.
Choice, Score and Noul
Which primitive answers which shape of question, with real request and response bodies.
OpenTweet replies feed
A niche reply feed with AI drafts. The surface a spam score plugs into.
Will it go viral, free post scorer
A free post scorer built on Jev. No signup, no API key.
Filter first. Then reply to what is left.
OpenTweet gives you the reply feed, the drafts and one API for X, Bluesky and LinkedIn, from $11.99 a month.
7-day free trial. Cancel anytime.