How tweet-factory scores drafts
tweet-factory does not run Phoenix. It cannot know a hypothetical viewer’s predicted probabilities. After the model returns tweet text, src/rank/score.ts scores each variant on this machine. The 0–100 number is that local score.
The weights are the published defaults in src/rank/weights.ts, copied from xai-org/x-algorithm commit cb45b55. The same module builds OPTIMIZER_SYSTEM in src/prompts/optimizer.ts, so the prompt and the scorer share one table. The long write-up is X_ALGORITHM_BOOST_PLAYBOOK.md. The weights page is the ranker. This page is the draft score.
What tf generate asks for
src/llm/generate.ts builds one user message: your persona, measured voice stats, optional flush and Notion text, a calibration block from tweets.db, and an instruction to return JSON. The system prompt is voiceSystem() plus OPTIMIZER_SYSTEM. voiceSystem() ends with the style rules from src/style/tells.ts (no em dashes, no AI tells). Before scoring, every variant goes through the style check, so the score is computed on the rewritten text.
The default ask is 4 ideas (DEFAULT_IDEA_COUNT). --n changes that count. Each idea has 2 or 3 variants, aimed at different heads (copy-link share, reply, quote, or follow). The prompt tells the model to leave score at 0 and to post one variant per idea.
Zod accepts ideas[] (each variant has text, plus optional aim and note) or a legacy flat drafts[]. Any score in that object is discarded. src/rank/bundle.ts keeps at most 3 variants per idea, drops empty text, and sorts variants and ideas by the local score, then by weighted total. It writes a flat drafts array back with text, score, signal (the strongest positive head), and a note.
onboarding.json stores ideas, that drafts array, and calibration (reliable, reason, window).
Direct API calls use temperature 0.8 and a token cap of 8,000 so four ideas fit. The default backend is Claude Code (claude -p --output-format json --tools ""), with a 240 second timeout and no API key. Details are on LLM providers. Scraped tweets stay inside a reference fence, and the subprocess has no built-in tools.
The 0–100 formula
For each head, the scorer assigns an editorial strength from 0 to 1, then multiplies by that head’s published weight. The weighted total is the sum of those contributions. It can go negative when a negative head fires.
The displayed score is that total divided by the sum of the positive weights in effect, times 100, rounded, and clamped to 0–100.
Text that does not start with @ is treated as an original a mutual follow would see. Reply’s weight is then 20 (the published 5 plus the +15 mutual-original boost), and the positive-weight sum is 55.5. Text that starts with @ is a reply: reply’s weight is 5, the format is reply, and the positive-weight sum is 40.5.
Author diversity (a second post from you in one viewer’s pool is ×0.625) and the 0.75 factor on out-of-network posts and on in-network replies/reposts are serving rules. They are not extra multipliers inside scoreDraft. The drafts file tells you to post one variant because of them. How a For You request is built is where those factors are defined.
Heads in the local table, in the order the file prints them:
| Head | Published weight |
|---|---|
| Copy-link share | 20 |
| Reply | 5, or 20 on a mutual original |
| DM share | 5 |
| Quote | 5 |
| Follow | 4 |
| Repost | 1 |
| Like | 0.5 |
| Report | −234 |
| Mute | −58.8 |
| Not interested | −47.52 |
| Block | −31.2 |
Dwell, click, open-link, photo expand, and video open are published ranker heads. They are not rows in this local score. The prompt says open-link is a small positive in the ranker (+0.2) and that a URL does not zero the post. Bookmarks are not a head.
Strengths come from the draft text. A specific question, and the word “you” or “your”, raises reply strength. “thoughts?”, “agree?”, “who agrees”, and “hot take?” count as a generic question and raise it much less. A first line of 40–200 characters with at least six words, and a digit, raise copy-link strength. A line break and a post of 12–70 words raise DM-share strength. First person raises follow strength. A digit and a post of 6–40 words raise repost strength. Like strength starts at 0.4 for any non-empty draft.
Engagement-bait phrases and rage words raise report, mute, or block strength. Three or more # characters raise report, mute, and not-interested strength, and six or more add more report strength. Under four words raises not-interested and lowers several positive strengths. A URL (http, https, or t.co) withholds a 0.1 copy-link bonus. It does not apply a fixed link penalty.
If report or mute strength is at least 0.05, the note says those weights dwarf a reply when that probability is not tiny. At a score of 70 or above, the note names the strongest positive head. Below 70, it names the weakest. The CLI keeps every variant. 70 is a label in the quip, not a filter.
X_ALGORITHM_BOOST_PLAYBOOK.md also prints a writing checklist (reply pull 24, quote/copy-link 22, and the rest of that 100-point table) and marks it [heuristic]. tf generate does not add those points. The number in drafts/ is the per-head formula.
Calibration from your own posts
src/rank/calibrate.ts computes the rates. src/rank/calibrateDb.ts reads rows with source='own' in tweets.db. It does not change the published weights. When the history is thick enough, it scales four strengths — reply, quote, repost, and like — by that format’s per-impression rate divided by the account-wide rate, clamped to 0.6–1.6. The product is clamped to 0–1 again.
- Fewer than 8 own posts with
impression_count > 0:reliableis false,windowisnone, and every scale stays at 1. The file says scoring uses the published weights with no account adjustment. - At least 8 of those posts older than 48 hours: rates use that settled window. 48 hours is the same span as
AgeFilter. - Fewer than 8 settled posts, but at least 8 posts with impressions: rates use every impressed post, including ones still inside the age gate (
windowall). - A format is scaled only when the account is reliable and that format has at least 4 impressed posts. Format comes from
referenced_type: reply, quote, repost, or original. - Bookmarks are printed as a per-impression rate. They do not scale a head.
- Up to three of your posts with at least 100 impressions, the highest reply-per-impression first, are copied into the prompt (text cut to 140 characters) and labeled as examples not to copy.
roast() in src/analyze.ts calls scoreDraft with no calibration. tf analyze prints whether the history is thick enough to calibrate. tf analyze --roast scores recent posts with that same uncalibrated function and prints the calibration reason above them.
The drafts file
src/commands/generate.ts writes drafts/YYYY-MM-DD.md through src/rank/render.ts. The header says to post one variant, quotes the ×0.625 second-post factor, names commit cb45b55, states the score definition, then prints the calibration reason and the bookmark note.
The shape, with sample figures (this is the file layout, not a measurement from an account):
# Drafts — YYYY-MM-DD
## the idea
### Variant 1 — score 72 — weighted 31.40
Aim: reply
the tweet
> why this variant
| Head | Strength | Weight | Contribution |
| Reply (mutual original, +15) | 0.82 | 20.0 | 16.40 |
Weighted total 31.40. Score 72/100. strongest head: reply (mutual original, +15).When calibration moved a strength, a line under the table names the format and the reply, quote, repost, and like scales.
An optional skill at ~/.claude/skills/twitter-algorithm-optimizer/ can stand in for the playbook when a person runs that skill. tf generate does not load it. The binary scores with src/rank/.
Browser checker
tweetfactory.sh/score is a free page that reuses src/rank/weights.ts and the weighted sum in src/rank/score.ts (rollup). Its strengths are a separate heuristic for a pasted draft: a question or CTA raises reply, a number or a standalone first line raises copy-link and DM share, and hot-take wording raises mute and not-interested. Hashtags do not move it. A link adds open_link at +0.2 and does not subtract from the other heads. Toggles apply the out-of-network factor (0.75), the mutual-original reply boost, a media flag (photo_expand 0.05), and the second-post factor (0.625). Signed out, the draft stays in the browser. Sign in with X reads recent post metrics, scales those four heads with the same calibratePosts rates the CLI uses, and stores drafts you save. The page does not call a model. Details are on the privacy page.
Related
FAQ
How does tweet-factory score a draft?
tf generate asks your LLM for four ideas, each with two or three variants, and tells it to leave scores at 0. After Zod checks the JSON, src/rank/score.ts scores every variant. The CLI writes drafts/YYYY-MM-DD.md with a per-head table and writes onboarding.json with ideas, drafts, and calibration. The process does not call the X ranker.
Does the score use the 2026 open-source weights?
The coefficients do. src/rank/weights.ts uses the published defaults from x-algorithm commit cb45b55: copy-link share 20, reply 5 (20 on an original a mutual follow sees), DM share 5, quote 5, follow 4, repost 1, like 0.5, and the four negative heads. The 0–1 strength beside each weight is an editorial read of the draft text. It is not Phoenix’s predicted probability for a viewer.
What does a score of 70 mean?
70 is the quip threshold in scoreDraft. At or above it, the note names the strongest positive head. Below it, the note names the weakest. The CLI keeps drafts under 70. The number is 100 times the weighted total, divided by 55.5 for a mutual original or 40.5 for a reply, then rounded and clamped to 0–100.
Is there a free tweet score calculator?
Yes. tweetfactory.sh/score scores a pasted draft in the browser with the same published weights and the same weighted-sum function. It labels the result as a heuristic estimate. Signed out, it uses the published weights with no impression history. Sign in to scale reply, quote, repost, and like from recent posts and to save drafts. Install tf for voice-aware drafts from your own LLM.
When does my own tweet history change the score?
When tweets.db has at least eight of your posts with impressions. Reply, quote, repost, and like strengths for a format are then multiplied by that format’s per-impression rate divided by the account-wide rate, and clamped between 0.6 and 1.6, if that format has at least four impressed posts. Posts older than 48 hours are preferred once eight of them exist. Bookmarks are printed and do not scale a head. Fewer than eight impressed posts leaves every scale at 1. On tweetfactory.sh/score, the same rate math runs after you sign in with X, using recent posts from the X API.