What X's Open-Source Algorithm Says About Automated Posting

X open-sourced its For You ranking code. The real weights, why the viral '468 likes' claim is wrong, and what the code says about posting through an API.

What X's Open-Source Algorithm Says About Automated Posting
Quick answer

X publishes its For You ranking code at github.com/xai-org/x-algorithm under Apache 2.0. Posts are scored as a weighted sum of predicted actions: copy-link share 20.0, reply 5.0, repost 1.0, like 0.5, report -234.0. Nothing in the published ranking path reads how a post was created, so API and scheduled posts are scored identically to app posts.

X has been publishing the code behind its For You feed since January 2026, with two substantial updates on August 13 and 14. The coverage since then has been heavy on takes and light on reading the source. The most widely shared claim from the release — that one report cancels out 468 likes — is a misreading that X rebuts in its own README.

Two things in the code matter if you publish programmatically. The first is what the ranker sees when it scores your post. The second is what gets a post labeled and filtered before ranking is even relevant. Neither is what most of the commentary says it is.


What did X actually open-source?

The repository is xai-org/x-algorithm, Apache 2.0, mostly Rust with Python for model training. It covers the request path end to end.

Retrieval comes from three sources: thunder/ for in-network posts from accounts you follow, phoenix/ for out-of-network retrieval, and simclusters/ for cluster-similarity candidates. home-mixer/ orchestrates the pipeline stages — sources, hydrators, filters, scorers, selectors — and holds the scoring weights. phoenix/ also contains the ranking model that scores every candidate. vm-ranker/ reorders the scored set with a determinantal point process over post embeddings, trading a little score for less similarity between neighbours.

Separately from ranking, visibility-filtering/ decides whether a post can be shown at all, reading labels produced by botmaker/, botmaker-rules/, scarecrow/, grox/, agatha/, and several account-scoring models. The README is explicit that these are different systems: “Ranking sets the order. Whether a post can be shown at all is decided separately.”

The August 13 update added the configuration parameters, including the scoring weights, plus the visibility filtering systems and the code that trains the ranking model. August 14 added the Brazil 2026 election filter and a set of code comments correcting how people were reading the weights.

Two categories are deliberately withheld: the Grok prompt files used by the grox/ classifiers, and some botmaker rules. X’s stated reason is gaming risk. Production model weights are also not included — phoenix/ ships training code and a synthetic data generator so you can train and serve a small model end to end, not the model that ranks your timeline.


Why the “one report cancels 468 likes” claim is wrong

The report weight is -234.0 and the like weight is 0.5. Divide one by the other and you get 468. That arithmetic produced a lot of posts and a lot of coverage, and it does not describe what the code does.

The weights do not multiply engagement counts. They multiply predicted probabilities. The model estimates, for you specifically, how likely you are to like this post, reply to it, mute the author, or report it. The weighted sum of those probabilities is the score. From home-mixer/params/param.rs:

Each weight multiplies the predicted probability of that action (P(favorite), P(repost), …) or a continuous value e.g. watch time — the weights do not multiply raw engagement counts.

The reason the report weight is large is stated in the same comment block: “the baseline probability of a Report is more than 1000x lower than a Like, so it’s weighted more to allow the prediction to affect the final ranking at all.” A number that rarely fires needs a large coefficient to move the sum at all. The ratio between two coefficients is not an exchange rate between two actions.

X added those comments in the August 14 update, and said why: so that “LLMs or people reading it are more likely to understand it correctly.” That is a reasonable thing to worry about, given how much of the reporting on this release was assembled from other people’s summaries.

The same code block addresses coordinated reporting. Predictions are personalized, so reports from a cluster of bad actors mostly affect what gets recommended to users who resemble those bad actors. And engagement only counts toward ranking when it happens on a post served in the Home Timeline — “Directly navigating to a post (i.e., coordinating via groupchat) has no ranking impact.”


What are the real ranking weights?

Every candidate is scored as Final Score = Σ (weight_i × P(action_i)). The values below are the defaults in home-mixer/params/param.rs; the arithmetic is in home-mixer/scorers/ranking_scorer.rs.

Positive weights:

ActionWeight
Share via copy link20.0
Reply — original post from a mutual follow20.0 (5.0 + 15.0 boost)
Reply5.0
Quote5.0
Share via DM5.0
Follow author4.0
Share2.0
Repost1.0
Favorite (like)0.5
Post click0.4
Open link0.2
Photo expand / video open / video quality view0.05 each
Quoted post click0.05
Post unexplored0.02
Continuous dwell time0.004
Dwell0.0
Profile click0.0

Negative weights:

ActionWeight
Report-234.0
Mute author-58.8
Not interested-43.2
Block author-31.2
Not dwelled-0.02

Read as coefficients on personalized probabilities rather than as a scoreboard, the shape is still informative. A copy-link share carries forty times the coefficient of a like, and a DM share ten times. Both are private actions — they signal that a post was worth sending to a specific person. Likes and reposts, the two metrics most people optimize for, carry the smallest positive coefficients of any explicit engagement.

Two weights are currently zeroed: plain dwell and profile click. They exist in the scoring code and contribute nothing at these defaults.


Does posting through an API affect ranking?

Not in any way the published code exposes.

The struct that flows through the ranking pipeline is PostCandidate in home-mixer/models/candidate.rs. Its fields cover post and author IDs, reply and quote and repost relationships, engagement counts, media info, language, follow relationships, topics, and safety labels. There is no field for the client or application that created the post.

The same holds at the model boundary. as_tweet_info() builds the payload sent to the ranking model, and it carries tweet ID, author ID, quote and reply IDs, whether the viewer follows the author, a safety label mask, video duration, and engagement counts. Nothing about how the post was authored.

There is a client_app_id in the code, and it is easy to misread. It travels on ScoredPostsQuery — the request for a timeline — and it identifies the viewer’s device. It is consumed in home-mixer/util/urt/navigation.rs to decide which navigation instructions to send to iOS versus Android. It describes who is reading the feed, not who wrote the post.

Stated precisely: nothing in the published ranking path reads how a post was created. That is a claim about this repository, not a guarantee about X as a whole — some botmaker rules are withheld, and platform policy on automated accounts is a separate matter from ranking. But the specific fear that a post loses reach because it arrived through an API rather than a phone is not supported by the code X published. Posting via the X API produces the same candidate the app does.


What actually gets a post suppressed?

Duplicate text. This is where automated publishing carries real risk, and it has nothing to do with automation itself.

botmaker-rules/scarecrow/bot/BBQDuplicateTextProd.bot reacts to scheduled jobs named bbq_duplicate_text_unigrams and bbq_duplicate_text_characters_cjk. Those jobs cluster posts by text similarity and hand back a tweetIdCluster. The rule then calls:

TweetRtfApplyLabel("COPYPASTA_SPAM", :tweetId);

A sibling rule, BBQDuplicateTextRepliesProd.bot, does the same for replies. Both skip accounts that are gray-verified, on a temporary skip list, or that pass IsHighPageRankUser — a reputation carve-out, computed by the PageRank job in user-cred-v2/. Everyone else gets labeled.

Labels are what visibility-filtering/ reads. Its verdicts, in visibility-filtering/filter_tweets.rs, are Drop or Interstitial — the post is removed from the feed, or shown behind a click-through. This happens independently of the score. A post can rank well and still never be served.

So the mechanism people worry about does exist, and it is aimed at something specific. Not scheduling. Not API access. Text that closely matches other text.

This is exactly what the laziest cross-posting produces: one caption, sent verbatim to every account and every network, sometimes on a repeating schedule. The same text cannot go everywhere unchanged, and X now publishes the job that notices. Varying copy per platform has always been the better practice for readers; this is the first time there’s a public rule file behind it. Postproxy sends per-platform content from a single API call for the same reason — the platform reference covers what X accepts. If you publish the same content more than once on purpose, idempotency keys keep retries from becoming duplicates you didn’t intend.


Author diversity changes how you should schedule

Three adjustments run after the weighted sum, and one of them has direct scheduling consequences.

Author diversity applies a decaying multiplier to each additional post from the same author within a single feed slate. The formula in ranking_scorer.rs is (1.0 - floor) * decay^k + floor, with decay at 0.5 and floor at 0.25. Your first post in a slate scores at full value. The second is multiplied by 0.625, the third by roughly 0.44, the fourth by 0.34, approaching a floor of 0.25.

Posting five times in the window where one viewer refreshes does not get you five placements. It gets you one at full weight and four progressively discounted — and they compete against each other for the same slots. Spacing posts out is not a superstition about optimal posting times; it is how you avoid paying this multiplier. Queues that space content across a day exist for exactly this.

The out-of-network discount multiplies posts from accounts the viewer does not follow by OonWeightFactor, 0.75. Replies and reposts from accounts the viewer does follow take the same discount. For viewers reaching you through a topic, TopicOonWeightFactor is 0.5. New accounts get NEW_USER_OON_WEIGHT_FACTOR, 0.00001 — effectively in-network only until they have been around long enough and follow enough accounts.

The new-author boost lifts posts from accounts under an impression threshold toward a target slot. The parameters in author_cold_start.rs set the impression threshold at 1000, the follower cap at 1000, maximum post age at 86400 seconds, and the target slot range at 15 to 16. It is a narrow, twenty-four-hour window for genuinely small accounts.

One more asymmetry is worth noting. The +15.0 mutual-follow reply boost applies only when the candidate is an original post — in_reply_to_tweet_id.is_none() && retweeted_tweet_id.is_none(). Replies and reposts from your mutuals never receive it, and take the out-of-network discount besides. Original posts are structurally advantaged over reposts in a way that is now visible in the source.


Why you still cannot reverse-engineer the algorithm

The weights in this post are accurate and also provisional, and the repository is candid about why.

The first line of home-mixer/params/param.rs reads // mirrored from config feature-switch defaults; last sync 2026-08-12T04:09:22Z. The values are not compiled in. The README explains that tunables are “read from a configuration system rather than written into the code,” and that cron scripts periodically sync production defaults back into the repository. What you are reading is a dated snapshot of defaults, and the running value for any given viewer may differ because experiments are live at any time. X’s stated aim is for experiments at 10% of traffic or more to be visible in the repository.

docs/BIDIRECTIONAL_BOOST_CHANGE.md documents one parameter’s life as a worked example. The mutual-follow reply boost entered an A/B test on July 10, 2026 with arms at 0, 5, 10, 15 and 20. It launched broadly at 20 on July 13. On July 24, after feedback that World Cup discussion was being crowded out, it was lowered to 15. Three values in fourteen days for a single number.

Then there is personalization. Every weight scales a probability the model predicts for one viewer, from that viewer’s own behavior. There is no global ranking of your post. There are as many scores as there are viewers, and the coefficients are the only part that is public.

What the release is good for is settling factual questions. Does the ranker know a post came from an API? The struct definitions answer that. Does duplicate text get labeled? There is a rule file. Are replies and reposts discounted relative to original posts? The multiplier is in the scorer. Those answers are durable in a way that tactics derived from coefficient ratios are not — as the 468 number demonstrated within a day of the weights going public.

Ready to get started?

Start with our free plan and scale as your needs grow. No credit card required.