A/B test post variantswith Jev before publishing
The pattern is three lines long. An LLM writes five versions of the post. Jev scores all five in parallel, for a fraction of a cent on TypeSafe's published rates. Your code publishes the winner. The part that makes it work rather than look like it works is that a Jev Score comes back as a probability distribution, so you can tell a real difference between two variants from a coin flip.
Last updated: September 2026
No signup. Paste one, paste the other, compare.
The five steps
Generation is the slow, expensive half. Judgment is the cheap half, which is why you run it on everything.
Generate N variants with an LLM
Three to five. Push for different angles rather than different wordings, because the scorer cannot rank five paraphrases of one idea.
Score all N with Jev, in parallel
One request per variant, fired together. Each returns a probability-weighted score, the distribution behind it, and a confidence.
Apply the vetoes before the ranking
Engagement bait and AI-slop are Nouls, so they are yes-or-no gates. A variant that fails one is dropped, no matter how well it scored on strength.
Check the gap, not just the order
If first and second are inside the noise floor, you have a tie rather than a winner. Break it on something deterministic and say so in the log.
Publish the winner, keep the losers
Send the winner to the posting API. Store every variant with its score, because comparing scores against what actually happened is the only real validation there is.
Why the distribution is the whole point
Two variants can score exactly the same and mean completely different things. A single scalar rating cannot show you the difference. A distribution can.
A Jev Score is not a number the model wrote. It is the probability-weighted mean of the rubric level indices. TypeSafe's own worked example: 0 × 0.0 plus 1 × 0.70 plus 2 × 0.30 gives 1.30. The answer carries the probabilities that produced it and a confidence statistic derived from how concentrated they are.
Here are two variants that both score 2.00 on a five-level rubric. The numbers below are illustrative, not measured, but the shape is the thing to look at.
| Variant A | Variant B | |
|---|---|---|
| Score | 2.00 | 2.00 |
| P(level 0), flops | 0.02 | 0.32 |
| P(level 1), typical | 0.14 | 0.10 |
| P(level 2), above typical | 0.68 | 0.16 |
| P(level 3), strong | 0.14 | 0.10 |
| P(level 4), breakout | 0.02 | 0.32 |
| Confidence | High | Low |
| What it means | The model thinks this is above typical | The model has no idea |
| What to do with it | Publish it | Send it to a human, or rewrite |
Illustrative distributions on a five-level Score, both weighting out to 2.00. If your ranking code reads only the score, A and B are interchangeable. If it reads the distribution, only one of them is publishable.
Variant B is the interesting failure. A bimodal distribution with mass at both ends is the model saying this could go either way, and the mean lands in the middle purely as an artefact of averaging. Publishing it because "it scored 2.0" is publishing a coin flip. This is also why TypeSafe's own docs say a Score is for ranking and thresholding rather than for measuring magnitude, and why 1.5 does not mean halfway between level 1 and level 2 in any useful sense.
The practical rule: rank on the score, gate on the confidence, and treat a gap smaller than your rubric noise floor as a tie rather than a result. Where to set that line.
Three ways to compare two posts
Only one of them is evidence, and it is the slow one.
| Jev Score | Ask an LLM to rate it | Publish both and measure | |
|---|---|---|---|
| What you get back | A probability per rubric level, plus a confidence | A number the model wrote as text | Impressions, replies, reposts on a live post |
| Can you tell a tie from a decision | Yes. Read the distribution | No. 7 and 8 look the same whether it was sure or not | Yes, with enough volume |
| Latency for five variants | Parallel, one round trip each | Seconds to tens of seconds | Days |
| Cost for five variants | Under a tenth of a cent | Cents | Two published posts you cannot unpublish |
| Is it evidence | No. It is a prior | No. It is a prior | Yes, for that account, that hour, that audience |
| Can it be wrong | Yes. Schema-valid does not mean correct | Yes, and it can also return an unparseable answer | It can be noise, which is a different problem |
Jev latency per TypeSafe's published 70ms to 500ms end-to-end figure. LLM latency is typical provider behaviour, not measured here. The pre-publish column is not a replacement for the published one. It is what you run when publishing both is not an option, which for a single social account is almost always.
The code
Four files: generate, rank, pick, publish. Nothing in here is more than about forty lines.
First, the generation step. The one parameter worth thinking about is temperature, because five near-identical variants give the scorer nothing to work with.
const OPENROUTER = 'https://openrouter.ai/api/v1/chat/completions';
export async function writeVariants(brief: string, n: number): Promise<string[]> {
const res = await fetch(OPENROUTER, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.OPENROUTER_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'anthropic/claude-sonnet-4.5',
// Temperature matters more here than anywhere else in the pipeline:
// five near-identical variants give the scorer nothing to rank.
temperature: 1,
messages: [
{
role: 'system',
content:
'You write posts for X. One post per line, nothing else. ' +
'Each line must take a genuinely different angle on the brief, ' +
'not a reworded version of the same one.',
},
{ role: 'user', content: 'Write ' + n + ' posts about: ' + brief },
],
}),
});
const data = await res.json();
return String(data.choices[0].message.content)
.split('\n')
.map((line: string) => line.trim())
.filter(Boolean)
.slice(0, n);
}Then the ranking. One Jev request per variant, all fired together. Jev evaluates every declared question in one parallel pass rather than decoding token by token, so a three-question request is not three times the wait.
import { TypeSafeClient, noul, score } from '@typesafe-ai/sdk';
const client = new TypeSafeClient({ timeout: 4000 });
const QUESTIONS = {
strength: score(
'How far will this post travel compared to a typical post from the same author?',
[
'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.',
]
),
slop: noul('This reads like it was generated by an AI rather than written by a person.'),
bait: noul('This post explicitly asks the reader for likes, reposts, replies, follows, or bookmarks.'),
} as const;
export interface Ranked {
text: string;
score: number; // 0 to 4
confidence: number; // 0 to 1
distribution: Record<string, number>; // one entry per rubric level
slop: number;
bait: number;
}
export async function rank(variants: string[]): Promise<Ranked[]> {
// One request per variant, all in flight together. Parallel requests finish
// in about the time the slowest one takes.
const scored = await Promise.all(
variants.map(async (text): Promise<Ranked> => {
const { answers } = await client.systemOne({
state: { draft_post: text, platform: 'X (Twitter)' },
questions: QUESTIONS,
});
return {
text,
score: answers.strength.score,
confidence: answers.strength.confidence,
distribution: answers.strength.probabilities as Record<string, number>,
slop: answers.slop.noul,
bait: answers.bait.noul,
};
})
);
return scored.sort((a, b) => b.score - a.score);
}Then the choosing, which is where the distribution earns its keep. The vetoes run before the ranking, and a gap inside the noise floor is reported as a tie rather than dressed up as a decision.
import type { Ranked } from './rank';
// A gap smaller than this is not a result, it is rubric noise.
const MEANINGFUL_GAP = 0.25;
const MIN_CONFIDENCE = 0.6;
export function pick(ranked: Ranked[]) {
const eligible = ranked.filter((v) => v.bait < 0.3 && v.slop < 0.5);
if (eligible.length === 0) return { action: 'regenerate', reason: 'every variant failed a gate' };
const [first, second] = eligible;
// The winner is only a winner if the model was sure about it.
if (first.confidence < MIN_CONFIDENCE) {
return { action: 'review', winner: first, reason: 'flat distribution' };
}
// Two variants inside the noise floor are a tie, not a ranking. Ship the
// shorter one and stop pretending the model chose.
if (second && first.score - second.score < MEANINGFUL_GAP) {
const shorter = first.text.length <= second.text.length ? first : second;
return { action: 'publish', winner: shorter, reason: 'tie broken on length' };
}
return { action: 'publish', winner: first, reason: 'clear winner' };
}And the publish call. One request sends the winner to X, Bluesky and LinkedIn, and each network reports its own outcome.
const variants = await writeVariants('we cut our p99 from 1.2s to 180ms', 5);
const ranked = await rank(variants);
const decision = pick(ranked);
if (decision.action === 'publish') {
const res = await fetch('https://opentweet.io/api/v1/posts', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.OPENTWEET_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': 'p99-post-2026-09-22',
},
body: JSON.stringify({
text: decision.winner.text,
platforms: ['x', 'bluesky', 'linkedin'],
category: 'Engineering',
scheduled_date: '2026-09-22T14:00:00Z',
}),
});
const data = await res.json();
if (!res.ok) throw new Error((data.code || res.status) + ': ' + data.error);
console.log(data.posts[0].id, data.posts[0].status);
}
// Keep the losers. A week of (variant, score, what actually happened) rows is
// the only thing that will ever tell you whether the ranking was any good.Drop scheduled_date and the post is saved as a draft for a human to approve, which is the right mode until you trust what the ranker picks. Full REST reference.
Be honest about what this is
A model score is a prior, not a result
Nothing on this page is an A/B test. An A/B test publishes both versions to comparable audiences and measures what happened. This ranks variants before anyone has seen any of them, using a model's opinion of the text. It is worth doing because it is instant and nearly free, not because it is evidence.What ranking genuinely buys you
- The obviously worse variants never reach a timeline
- Bait and slop get vetoed before anyone decides they are clever
- A tie is reported as a tie instead of a false winner
- It runs on every draft, including the ones you would have skipped
What it cannot do
- Tell you what would have happened if you had posted the other one
- Account for your audience, the hour, or what else is trending
- Beat a real published-data comparison, where one is possible
- Be right every time. Schema-valid is not the same as correct
The way to close the gap is to keep the losers. Log every variant with its score, its distribution and the model version, then join that against what the published winner actually did. After a few dozen posts you can plot score against outcome on your own account and find out whether the ranking is informative at all. That is a measurement you can make. The ranking on its own is not.
TypeSafe's own numbers deserve the same treatment. On its four-workflow eval Jev scores 67.8% against GPT-5.6 Terra's 67.9%, behind Sol at 74.1% and Opus 5 at 73.1%, and the reference answers are the average of two other models rather than human labels. The case for Jev here is cost and latency at comparable accuracy, which is exactly what a ranking step needs. The full benchmark table.
Frequently asked questions
How do you A/B test post variants with Jev?
Generate several versions with an LLM, send each one to Jev as its own request with the same Score question, and let your code rank them. Every request comes back with a probability distribution across the rubric levels, not just a number, so you can check whether the gap between first and second is real before you publish the winner.
Is this really an A/B test?
No, and calling it one is the mistake worth avoiding. A real A/B test publishes both versions and measures what happened. This is a pre-publish ranking: a model prior over which variant is more likely to work, produced before anyone has seen either. It is useful because it is cheap and instant, not because it is evidence.
Why does the probability distribution matter?
Because it separates a judgement from a guess. A Jev Score is the probability-weighted mean of the level indices, so a variant at 2.0 with most of its mass on level 2 and a variant at 2.0 that the model split between 0 and 4 look identical if you only read the number. The first is a real 2.0. The second is a coin flip. A single scalar rating from an LLM cannot tell you which one you have.
How many variants should I generate?
Three to five is the useful range for one post. Below three there is nothing to rank. Above five the variants start repeating each other, because the generation model is sampling around one idea rather than producing genuinely different angles. If you want more spread, change the brief rather than raising the count.
What does it cost to rank five variants?
Jev is $0.042 per million input tokens, per docs.typesafe.ai/models, with output tokens free, so scoring five short variants is a small fraction of a cent. The generation step is the expensive half, because that is where you pay for output tokens.
Can Jev generate the variants as well as score them?
No. Jev emits no free-form text at all. It takes state plus typed questions and returns typed answers. Writing the variants is a job for an LLM, and the two models sit next to each other rather than replacing each other.
Should I publish the winner automatically?
Only after you have watched it choose for a while. Send neither scheduled_date nor publish_now to the OpenTweet API and the winner is saved as a draft instead, so a person approves before anything goes out. Run it in that mode first, compare what it picked against what you would have picked, and switch on scheduling once the two agree often enough.
Keep exploring
The single-draft version of this, the primitives behind it, and the agent it plugs into.
Scoring one draft
The pre-publish quality gate this pattern is built on, and the free scorer that runs it.
The free post scorer
Paste two variants, score both, see which dimension separates them. No signup.
Choice, Score and Noul
Why ranking is a Score problem and vetoing is a Noul problem.
Picking a confidence threshold
How to decide what counts as sure enough to publish without a human.
Build an AI social media agent
The three-layer architecture this ranking step lives inside, with the full code.
What happened after
The published side of the loop, where a prior finally gets checked against a result.
Rank five. Publish one.
The ranking is yours to build. The publishing is one call to X, Bluesky and LinkedIn, with an API key, the hosted MCP server and the CLI on every plan from $11.99 a month.