Pre-publish quality gate

Score a post with Jevbefore you publish it

A pre-publish quality gate reads the draft and rates it against a rubric you wrote, before it goes anywhere. There is a working one at opentweet.io/tools/will-it-go-viral. It is free, it needs no signup and no API key, and it scores on every keystroke pause. Seven questions per draft, an answer while you are still typing, for a fraction of a cent.

Last updated: September 2026

Score a draft now

No login. Nothing is published.

Why scoring before beats analysing after

Analytics tells you what happened. A gate tells you what is about to happen while you can still do something about it. They answer different questions and only one of them changes the post.

Scoring before you publishAnalytics after you publish
When it runsBetween writing and publishingHours or days after the post is live
What you can changeThis post. All of itThe next post, if you remember
What it measuresThe writing: hook, clarity, specificity, toneThe outcome: impressions, replies, reposts
Sample size to be usefulOne draftDozens of posts, and even then the account and the hour are confounders
What it cannot tell youWhether it will actually travelWhether a different version would have done better
Cost per checkFractions of a centFree, but paid for with a published post you cannot take back

Both are worth having. OpenTweet ships analytics too. The point is that the loop from analytics back to a better post is weeks long, and the loop from a gate back to a better post is one rewrite.

The reason nobody did this before Jev is not that the idea was hard. It is that a gate has to run on every draft, including the ones you throw away, and a frontier LLM at a few seconds and a few cents a call is too slow to sit in a typing loop and too expensive to run on work that never ships. The latency and pricing TypeSafe publishes are what make a gate practical rather than a demo.

What the OpenTweet scorer actually asks

Seven questions, one request. Four Scores and three Nouls. This is the real list, not a simplified one. It is what OpenTweet sends on every scored draft, and the hook rubric additionally carries example phrases on each level so the model scores against the same words the result names.

Question idTypeWhat it asksWhat comes back
strengthScore, 5 levelsHow far will this post travel compared to a typical post from the same author?A number from 0 to 4, plus the distribution and a confidence
hookScore, 3 levelsHow well does the opening line stop someone mid scroll?A number from 0 to 2, plus the rubric level it landed on
clarityScore, 3 levelsHow easily does a reader with no context understand this?A number from 0 to 2, plus the rubric level it landed on
specificityScore, 3 levelsHow concrete is this?A number from 0 to 2, plus the rubric level it landed on
slopNoulThis reads like it was generated by an AI rather than written by a person.A single float from 0 to 1. No confidence field
baitNoulThis post explicitly asks the reader for likes, reposts, replies, follows, or bookmarks.A single float from 0 to 1. No confidence field
toxicityNoulThis text contains slurs, harassment, sexual content, or abuse targeted at a person or group.A single float from 0 to 1. No confidence field

Note the last three rows. A Noul returns only noul, a float from 0 to 1. There is no confidence field on a Noul answer, which is the single most common mistake in third-party Jev writing. The probability is the confidence. All three primitives, side by side.

Why four Scores and three Nouls

  • Strength is the headline number, and it is the least actionable one
  • Hook, clarity and specificity are the dimensions a writer can actually fix
  • Slop, bait and toxicity are vetoes, so they are yes-or-no questions, not ratings
  • All seven ride on one copy of the draft, and one round trip, instead of seven

What the numbers are not

  • A Score is for ranking and thresholding, not for measuring magnitude
  • 1.5 does not mean halfway between level 1 and level 2 in any meaningful sense
  • None of it knows your follower count, the hour, or what else is trending
  • A high score on a bad idea is still a bad idea scored well

The distribution is the interesting part

A Score answer is not a single number that got rounded. Jev returns a probability for every level, and the score is the probability-weighted mean of the level indices. TypeSafe's own worked example: 0 × 0.0 plus 1 × 0.70 plus 2 × 0.30 gives 1.30. The confidence field is a statistic computed from that distribution, so it tells you how concentrated the model's opinion was.

This matters because two drafts can score 2.0 for completely different reasons. One can be a confident 2.0, with most of the probability mass on level 2. The other can be an average of a model that thought it was either a 0 or a 4 and could not decide. The first is a judgement. The second is a coin flip wearing a number, and it should not pass a gate.

The public scorer shows this as a confidence band rather than a bare number, because the band is what Jev actually returned. Where to set the threshold depends on what happens when you are wrong, and TypeSafe's docs deliberately refuse to publish a universal number for the same reason.

Build your own gate

The scorer, the thresholds, and the publish call. About sixty lines end to end.

score-draft.ts
import { TypeSafeClient, noul, score } from '@typesafe-ai/sdk';

const STRENGTH_LEVELS = [
  'Flops. Gets less attention than a typical post from the same account.',
  'Typical. Performs about the same as the account usually does.',
  'Above typical. Noticeably more reach and replies than usual.',
  'Strong. Several times the usual reach. Spreads past the existing audience.',
  'Breakout. Orders of magnitude beyond usual. Reaches people who have never heard of the author.',
] as const;

const client = new TypeSafeClient({ timeout: 4000, retry: { maxRetries: 1 } });

const QUESTIONS = {
  strength: score(
    'How far will this post travel compared to a typical post from the same author?',
    STRENGTH_LEVELS
  ),
  hook: score('How well does the opening line stop someone mid scroll?', [
    'No hook. Opens with context, throat clearing, or a greeting.',
    'Functional. States the subject plainly without creating a reason to continue.',
    'Strong. Creates tension, a gap, or a surprise in the first line.',
  ]),
  clarity: score('How easily does a reader with no context understand this?', [
    'Confusing. Depends on context the reader does not have.',
    'Understandable, but takes a second read.',
    'Immediately clear on one pass.',
  ]),
  specificity: score('How concrete is this?', [
    'Abstract. Generic advice that could apply to anyone.',
    'Somewhat concrete. Gestures at specifics without giving them.',
    'Concrete. Names real numbers, events, or details.',
  ]),
  slop: noul('This reads like it was generated by an AI rather than written by a person.', {
    true: 'Em dashes, triadic lists, uniformly long sentences, words like delve or landscape.',
    false: 'Uneven rhythm, specific detail, an identifiable voice.',
  }),
  bait: noul('This post explicitly asks the reader for likes, reposts, replies, follows, or bookmarks.'),
  toxicity: noul('This text contains slurs, harassment, sexual content, or abuse targeted at a person or group.'),
} as const;

export async function scoreDraft(text: string) {
  const { answers, usage } = await client.systemOne({
    state: { draft_post: text, platform: 'X (Twitter)' },
    questions: QUESTIONS,
  });

  return {
    strength: answers.strength.score,            // 0 to 4
    confidence: answers.strength.confidence,     // 0 to 1
    distribution: answers.strength.probabilities,
    hook: answers.hook.score,                    // 0 to 2
    clarity: answers.clarity.score,              // 0 to 2
    specificity: answers.specificity.score,      // 0 to 2
    slop: answers.slop.noul,                     // 0 to 1
    bait: answers.bait.noul,                     // 0 to 1
    toxicity: answers.toxicity.noul,             // 0 to 1
    inputTokens: usage.input_tokens,
  };
}

One request, seven questions, evaluated in a single parallel pass. Jev does not decode token by token, so asking seven questions is not seven times the wait.

Then the thresholds. Keep them in one file so changing the bar is a reviewable diff.

gate.ts
import { scoreDraft } from './score-draft';

export async function gate(text: string) {
  const s = await scoreDraft(text);

  // Vetoes. Not ranking signals, and not negotiable by a high strength score.
  if (s.toxicity > 0.05) return { verdict: 'block', reason: 'toxicity' };
  if (s.bait > 0.3) return { verdict: 'block', reason: 'engagement bait' };
  if (s.slop > 0.5) return { verdict: 'rewrite', reason: 'reads as AI-written' };

  // The weak dimension is more actionable than the headline number, because it
  // names the one thing to fix rather than telling the writer the post is a 41.
  if (s.hook < 1) return { verdict: 'rewrite', reason: 'the first line is not pulling anyone in' };
  if (s.specificity < 1) return { verdict: 'rewrite', reason: 'nothing concrete in here yet' };

  // A flat distribution means the number is an average of disagreement, not a judgement.
  if (s.confidence < 0.5) return { verdict: 'review', reason: 'low confidence' };

  return s.strength >= 2 ? { verdict: 'publish', score: s } : { verdict: 'rewrite', reason: 'typical at best' };
}

And the publish side, once a draft clears. The OpenTweet REST API takes the text and the target networks in one call.

publish.ts
const decision = await gate(draft);

if (decision.verdict === 'publish') {
  await fetch('https://opentweet.io/api/v1/posts', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.OPENTWEET_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      text: draft,
      platforms: ['x', 'bluesky', 'linkedin'],
      scheduled_date: '2026-09-22T14:00:00Z',
    }),
  });
}

Drop both scheduled_date and publish_now and the post is saved as a draft instead, which is the right default while you are still calibrating the thresholds. The full agent this fits into.

What a score honestly is

A gate is not a prediction

Nothing can promise a post goes viral, and anything that claims to is guessing. Reach depends on who posted it, when, and what else was happening that hour, and none of that is in the text. What a gate measures is the part you control: whether the opening line earns the second one, whether a stranger gets it on one pass, and whether it says anything concrete.

The other honest limit is the model. Jev cannot emit a value outside the schema you declared, which is a guarantee about shape and not about correctness. TypeSafe's own launch post says the 0% hallucination figure "is not empirical" and that schema matching is what is guaranteed. The only independent test so far, Mike Taylor's at Every, put 777 judgments through and found Jev caught 6 of 7 planted defects where Claude Fable 5.1 caught 7 of 7. His own conclusion was that it works as an early warning system, not as a verdict. The full version of that correction.

Which is exactly how a gate should be used. Run it on everything, act on the clear cases in bulk, and send the ambiguous ones to a person.

Frequently asked questions

Can you score a tweet before posting it with AI?

Yes. A model reads the draft and rates it against a rubric you defined, before it goes anywhere. There is a working one at opentweet.io/tools/will-it-go-viral: seven questions per draft, an answer while you are still typing, for a small fraction of a cent. What a score cannot do is predict reach, because reach depends on who posted it, when, and what else happened that hour.

What is a pre-publish quality gate?

A check that runs between writing and publishing, and that can stop the post. It is different from analytics, which runs after and can only inform the next post. A gate needs a number and a threshold: score this draft, and if it falls below the line, rewrite it or send it to a human instead of publishing it.

What does the OpenTweet scorer actually score?

Seven questions in one Jev request. One Score for reach strength against five ordered rubric levels from Flops to Breakout. Three more Scores for hook, clarity and specificity, three levels each. And three Nouls for whether it reads as AI-written, whether it asks the reader for likes or replies, and whether it contains abuse. The Score answers come back with a probability distribution and a confidence, the Noul answers come back as a single float from 0 to 1.

How much does it cost to score a post with Jev?

Jev is priced at $0.042 per million input tokens, per docs.typesafe.ai/models, and output tokens are free, so the cost is set by how much text you send, not by how many questions you ask or how long the answer is. A short post carrying seven questions is a small fraction of a cent. That is why it is practical to score every draft rather than only the ones you are unsure about.

Is the Jev post scorer free to use?

The scorer at opentweet.io/tools/will-it-go-viral is free and needs no signup and no API key. Calling Jev yourself is not free: TypeSafe has no published free tier, access is waitlist-gated at typesafe.ai, and it is also reachable without the waitlist through Vercel AI Gateway, OpenRouter and Cloudflare.

Can the score be wrong?

Yes. Jev cannot return a value outside your schema, which is a guarantee about shape, not about being right. TypeSafe says the 0% hallucination figure is not empirical and that schema matching is what is guaranteed. Treat a score as a fast opinion worth acting on in bulk, not as a verdict, and keep a human on anything that matters.

Does Jev write a better version of the post?

No. Jev generates no text at all. It scores the draft and returns numbers. Rewriting is a job for an LLM, and the useful pattern is to let the LLM write several versions and let Jev rank them.

Score it here, then publish it here

The scorer is free and needs no account. When you want the post scheduled to X, Bluesky and LinkedIn as well, that is OpenTweet, from $11.99 a month with the API and the MCP server on every plan.