Open source · read from the source

X's algorithm, read from the source

On 13 August 2026, X published the numeric ranking weights it had withheld in January. Most of the figures circulating about “the X algorithm” are wrong — usually because they were right about the 2023 model, which no longer exists. Every number on this page is a link to the line of source it came from, pinned to one commit.

Repository xai-org/x-algorithmCommit 76843a5 (2026-10-02)License Apache-2.0Parameters synced 2026-10-01T16:00:46ZPage data regenerated 2026-10-02Checked against the repository every day

182

live parameters

changeable without a deploy

23

compiled constants

need a code change

135

pipeline components

sources, filters, scorers

12

claims checked

each against its source line

2798

accounts named in the code

removed from For You unless you follow them

Check a post against the code

Paste a link to any public post. You get the rules that actually apply to it — including the country restrictions that can hide it — read from the pinned commit.

The weights

What each action is actually worth

X states it runs cron scripts that set the defaults in the repository's code to be the primary production values, and aims for experiments running at 10% or more of traffic to be visible in the repository. The values below are those defaults, read at the pinned commit.

These are the weights the scorer applies — measured, not promised. Before scoring, `perturbed()` multiplies 25 of the 27 weights below by `(sigma * perturbation_sign(salt, user_id, head)).exp()` — the sign fixed per viewer and per head. The 2 it leaves alone are the modifier weights: BidirectionalFollowDwellWeightBoost, BidirectionalFollowReplyWeightBoost. `WeightPerturbationSigma` and `WeightPerturbationSalt` are feature switches: X can change them without shipping code. At the pinned commit `WeightPerturbationSigma` is 0.0, and the guard `sigma <= 0.0` returns them unchanged. Read it at the source

Every row carries the code it was read from — pressExplainto unfold the actual lines of Rust, without leaving the page.

ActionWeightvs a likeSource
binaryMultiplies the predicted probability of a discrete action.
share via copy linkShareViaCopyLinkWeight2040×
param.rs
quoteQuoteWeight510×
param.rs
replyReplyWeight510×
param.rs
share via dmShareViaDmWeight510×
param.rs
follow authorFollowAuthorWeight48×
param.rs
shareShareWeight24×
param.rs
retweetRetweetWeight12×
param.rs
favoriteFavoriteWeight0.51×
param.rs
clickClickWeight0.30.6×
param.rs
open linkOpenLinkWeight0.20.4×
param.rs
video openVideoOpenWeight0.070.1×
param.rs
dwellDwellWeight0.050.1×
param.rs
photo expandPhotoExpandWeight0.050.1×
param.rs
quoted clickQuotedClickWeight0.050.1×
param.rs
post unexploredPostUnexploredWeight0.020×
param.rs
profile clickProfileClickWeight0—
param.rs
quoted vqvQuotedVqvWeight0—
param.rs
vqvVqvWeight0—
param.rs
not dwelledNotDwelledWeight-0.02−0×
param.rs
block authorBlockAuthorWeight-31.2−62×
param.rs
not interestedNotInterestedWeight-47.52−95×
param.rs
mute authorMuteAuthorWeight-58.8−118×
param.rs
reportReportWeight-234−468×
param.rs
continuousApplies to a quantity (watch or dwell seconds), not a probability.
cont click dwell timeContClickDwellTimeWeight0.4—
param.rs
cont dwell timeContDwellTimeWeight0.004—
param.rs
modifierNot a head weight: adds to another weight, or gates a branch.
bidirectional follow reply weight boostBidirectionalFollowReplyWeightBoost15—
param.rs
bidirectional follow dwell weight boostBidirectionalFollowDwellWeightBoost0—
param.rs

Grouping into binary, continuous and modifier is ours — the weights struct and each field's use in vm-ranker/scoring/value_model.rs. X does not publish that taxonomy; we separate them because only the binary ones are comparable to each other.

Set a predicted probability for each action and watch its contribution to the score. Only the weights that multiply a probability are offered here — that is the arithmetic the code actually performs.

0% → 0.000
0% → 0.000
0% → 0.000
0% → 0.000
0% → 0.000
0% → 0.000
0% → 0.000
0% → 0.000
Weighted sum0.000

Why there is no “likes equivalent” here: Weights multiply a predicted probability, not an engagement count. Dividing one weight by another does not yield an exchange rate between actions.

Claim check

Numbers everyone repeats, checked against the code

These are not inventions. Most were accurate figures for the heavy ranker Twitter published in 2023, a Scala pipeline that has since been replaced. They kept circulating after the thing they described stopped existing.

Each claim below carries the searches that back it — the pattern, the files, the count — so you can run them yourself against the pinned commit. You will never see one here that contradicts us: if it did, this page would fail to build rather than publish the sentence it disproves.

Contradicted by the source

“A repost is worth 20 times a like.”

RetweetWeight is 1.0 and FavoriteWeight is 0.5, so a repost carries twice the weight of a like — not twenty times.

Where it comes from: No published version of the algorithm ever used 20. The 2023 heavy ranker also weighted retweet at 1.0 against favourite at 0.5.

Was true, of an older version

“A reply is worth 27 times a like (or 13.5 times).”

ReplyWeight is 5.0 against FavoriteWeight 0.5, so ten times — and forty times between mutual follows, because BidirectionalFollowReplyWeightBoost adds 15.0 to the reply weight for original posts between accounts that follow each other.

Where it comes from: Both figures were real, for a model that no longer exists: the 2023 heavy ranker published 27 on 2023-03-31, then 13.5 on 2023-04-05. They describe a Scala pipeline replaced by the current Rust one.

Was true, of an older version

“Clicks on your profile are worth 12 times a like.”

ProfileClickWeight is 0.0. A profile click contributes nothing to the weighted score.

Where it comes from: 12.0 was the 2023 weight for `good_profile_click` — a different, compound signal (opening a profile AND then liking or replying). The current model has a plain profile-click head, weighted zero.

No such parameter exists

“A bookmark is worth 10 likes — bookmarks are the strongest signal.”

There is no bookmark weight in param.rs, so bookmarks do not enter the weighted score at all. They are not, however, invisible: bookmark_count is hydrated onto every candidate as a model feature, and ClientTweetBookmark counts as a positive engagement in the viewer's history. Read, but unweighted.

Where it comes from: The 2025 Scala refresh did declare a home_mixer_model_weight_bookmark parameter — with a default of 0.0. No published version ever weighted it at 10.

What we searched for, at the pinned commit

  • There is no bookmark weight in param.rs

    bookmark · 2 files · 0 matches

  • bookmark_count is hydrated onto every candidate as a model feature

    bookmark_count · 1 file · 5 matches

  • ClientTweetBookmark counts as a positive engagement in the viewer's history

    ClientTweetBookmark · 1 file · 1 match

Right numbers, wrong reading

“One report cancels 468 likes (−234.0 ÷ 0.5).”

The arithmetic is right and the conclusion is still wrong. Weights multiply a PREDICTED PROBABILITY, not a count of actions. X states the baseline probability of a report is more than a thousand times lower than that of a like, so dividing one weight by another does not give an exchange rate between actions.

Where it comes from: This one is not stale — it is a misreading of correct numbers, and it appears even on pages that publish the right table. X added an explicit warning about it to its own README on 2026-08-14.

No such parameter exists

“Engagement velocity counts 1000×, author authority 50×, recency 22×.”

No parameter named for velocity, authority or recency exists in param.rs. Age is not a weight at all — it is a hard cut: AgeFilter drops any post older than MAX_POST_AGE, a compiled constant of 48 hours. Nothing multiplies a post's score by its freshness.

What we searched for, at the pinned commit

  • No parameter named for velocity, authority or recency exists in param.rs

    velocity|authority|recency · 1 file · 0 matches

  • AgeFilter drops any post older than MAX_POST_AGE

    AgeFilter::new\(.*MAX_POST_AGE · 1 file · 1 match

Contradicted by the source

“A coordinated group can bury your post by mass-reporting or mass-blocking it.”

X addresses this directly in the comments of param.rs, and the mechanism it describes is checkable: the model predicts YOUR likelihood of an action, so recommendations are personalised — reports from a brigade mostly affect what gets recommended to people similar to that brigade. And an engagement only counts if it happens on a post served in the Home Timeline: navigating straight to a post, for instance from a group chat, has no ranking impact.

Where it comes from: A reasonable inference from the very large negative weights, which the weights alone do not support.

What we searched for, at the pinned commit

  • X addresses this directly in the comments of param.rs

    recommendations are personalized · 1 file · 1 match

Was true, of an older version

“X published the code but withheld the ranking weights for security reasons.”

True of the January 2026 release, false since 2026-08-13. The numeric defaults are now in param.rs, and X states cron scripts keep them equal to the primary production values.

Where it comes from: Accurate reporting of the January 2026 drop, never updated after August.

What we searched for, at the pinned commit

  • X states cron scripts keep them equal to the primary production values

    cron scripts that set the defaults · 1 file · 1 match

Contradicted by the source

“Posts containing external links are downranked.”

In the published ranking code OpenLinkWeight is POSITIVE at 0.2 and applied unconditionally — opening a link is rewarded. Links are penalised only through safety labels such as MALICIOUS_URL and DO_NOT_AMPLIFY, which are about the destination, not about linking out.

What we searched for, at the pinned commit

  • Links are penalised only through safety labels such as MALICIOUS_URL

    MALICIOUS_URL · 7 files · 1 match

  • … and DO_NOT_AMPLIFY

    DO_NOT_AMPLIFY · 7 files · 1 match

Contradicted by the source

“A paid checkmark buys a ranking boost.”

No premium, verified or subscription multiplier exists in param.rs, and no such rescorer appears in the published scorers. The 2023 code did carry one — multipliers of 4.0 in-network and 2.0 out-of-network for Blue Verified authors — and it is gone from the current tree. Honest limit: Phoenix is a learned model whose weights are not published, so a learned correlation cannot be excluded by reading the code.

Where it comes from: The 2023 Scala pipeline really did apply a 4.0 / 2.0 Blue Verified multiplier.

What we searched for, at the pinned commit

  • No premium, verified or subscription multiplier exists in param.rs

    premium|verified|subscription · 1 file · 0 matches

  • no such rescorer appears in the published scorers

    premium|verified|subscription · 8 files · 0 matches

Contradicted by the source

“X now publishes government takedown requests in its open-source repository, and Brazil is the one country listed.”

The repository publishes the enforcement MECHANISM, never a request. The rules that drop a post for viewers in a withheld country — LegalTakedown and LocalLawsTakedown — read a reason that carries the country code, but that reason is fetched at runtime from an unpublished service, and the type that holds it (TakedownReason) is defined in a crate absent from the repository. X also publishes the plumbing around that type, in Scala: a gizmoduck read filter that strips unenforced takedowns before a read returns, a Tweet Entity Service column and transform that fetch a post's takedown reasons, and the under-the-hood jobs that turn those reasons into the labels an account is shown about itself. So the Thrift structs that carry it — Takedowns { country_codes, takedown_country_reasons } — are read by published code, in 3 files: `takedowns/gizmoduck/RedactUnenforcedTakedownsFilter.scala`, `takedowns/tweet-entity-service/TakedownReasonsTransform.scala`, `under-the-hood/scalding/UthPctdAccountTakedownEventsJob.scala`. What that adds is more MECHANISM, not one request: at the pinned commit, not one country code is written next to a takedown reason outside test fixtures — the only two-letter codes the takedown code holds are `xx`, `xy`, the worldwide sentinels the source names itself, which mean everywhere and are the opposite of a country. And the method that would ask the Tweet Entity Service for those codes under its own name, get_takedown_country_codes, does not appear anywhere in the repository at all. Brazil is a different thing entirely: a hard-coded list of 2795 accounts reported to the Electoral Court, sourced from the public candidates dataset, dropped from For You for every viewer worldwide unless they follow the account — with no country condition at all, which is the opposite of how a legal takedown works here. Honest limit: a repository with no hard-coded country is not a platform without country takedowns. The requests exist; they live off the repository, aggregated, in the Transparency Center and the DSA report.

Where it comes from: X's own 14 August post announced two things: the ranking weights and the Brazil election filter. Musk quote-posted it the next day in 61 characters — “Any censorship required by governments is now clearly visible” — naming neither the repository, nor requests, nor countries; the press tied it to a user-facing notice that was announced, not shipped. The words “requests” and “in the open-source algorithm” first appear together on an aggregator account.

What we searched for, at the pinned commit

  • The published Scala plumbing reads the Thrift takedown structs — Takedowns { country_codes, takedown_country_reasons }

    \bTakedowns\b|\btakedownCountryReasons\b · 43 files · 15 matches

Contradicted by the source

“Muting a keyword hides every post that contains it.”

A muted keyword is not one rule. The published timeline service applies it with 2 filters that do not read the same text: ViewerMutedKeywordFilter, registered in phoenix_candidate_pipeline.rs, compares it against tweet_text; FollowingViewerMutedKeywordFilter, registered in reverse_chron_posts_pipeline.rs, compares it against ancestor_texts, quoted_tweet_text and tweet_text. The fields only some of them read — ancestor_texts and quoted_tweet_text — are written by conversation_gap_ancestor_hydrator.rs, quote_hydrator.rs and quoted_post_text_hydrator.rs, registered in phoenix_candidate_pipeline.rs, phoenix_scores_pipeline.rs and reverse_chron_posts_pipeline.rs. A pipeline that does not run those hydrators never fills them: there the text of a quoted or ancestor post is not merely unread, it is absent. Beyond the timeline, the visibility-filtering rule engine — the layer that can hide a post wherever it appears — carries ViewerMutesAuthor (Mutes) and ViewerMutesRetweets (MuteRetweets), and no rule whose decision reads a keyword. The only muted-keyword outcome this repository names, MatchesMutedKeyword, appears exactly 1 time — in graphql_results.rs — and only on the left of a match arm mapping it to an interstitial label. Nothing here decides it; whatever does is not published. Two honest limits. This repository is the service that builds a timeline: what muting does in notifications or in search is decided elsewhere, in code X does not publish, and an absence here is an absence from the repository, not from the product. And we do not tell you which tab each pipeline serves — that wiring runs through several layers, and a mapping we cannot extract cleanly is a mapping we do not publish.

Where it comes from: Mute is a single setting in the product, so it reads as a single rule that applies everywhere. The published code shows something narrower: a muted keyword is applied while a timeline is assembled, and the filters that apply it do not all read the same text — so a post can carry the word in a place no filter on that path ever looks at.

The pipeline

What happens to a post, in order

Candidate sources produce posts, hydrators attach features, filters can delete a post outright, scorers can only move it. Each node links to its file. Edges declare how we derived them — read from the code, given by the directory layout, or, at their weakest, matched by name.

By country

What changes depending on where you read from

Two lists of countries decide what changes with where you read from, and they do not mean the same thing. One withholds sensitive media from viewers who have not stated an age. The other simply decides which countries the model is allowed to tell apart — every other country becomes “other”, which is not a restriction, only a blind spot. Each card below says where its own list is read from: a constant the code applies, or a fallback a runtime parameter can replace.

…

Click a country in rose to read the accounts this code names there, one by one. In the whole repository, exactly one country has such a list.

How this is checked: “the whole repository” is checked on the whole repository, not on one language: every file under the pinned commit is read, and a list of named accounts is found by its SHAPE — an @handle written in a comment line, immediately followed by the account's numeric id. That shape does not depend on Rust, so the day the same list is published in another language, the build says so instead of the page going quiet.

2798 accounts are named directly in the source code, by numeric id and by handle. Everything else in the enforcement path acts on a label attached at runtime, and that labelling is not published.

Brazil — 2795 accounts named in the code

User ids reported to the Electoral Court for the Brazil 2026 election.

Brazil 2026 election filter Art. 28 § 1º-A of Electoral Resolution No. 23.610: Application providers that use a recommendation system for users must exclude from the results the channels and profiles reported to the Electoral Court under the terms of § 1º of this article and, except in cases of paid boosting, the content posted on them. https://dadosabertos.tse.jus.br/dataset/candidatos-2026
2795
accounts written into the source, by id and by handle
unless you follow
their posts, reposts, quotes and thread ancestors are removed from For You — unless the viewer already follows the account
no country test
no country, market or locale condition appears in the filter or where it is registered

⚠ No country, market or locale condition appears in the filter file or around the line that registers it. That is a statement about the published code — which pipeline serves which market may be decided in a layer X does not publish.

52 further accounts are named in this file and are NOT filtered — the code says why each one was left alone: 44 × “@… no live account found.”; 8 × “We believe the account @… reported by the candidate is not the candidate's actual account, so we are not currently filtering it.” So 2847 named accounts are accounted for here, 2795 of them actually filtered.

Registered in the pipeline at home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs:391

  • @madeleinelacsko
  • @nanacachen
  • @renildo
  • @VladimirAlves
  • @renatoroseno
  • @pedro_lupion
  • @erasintetica
  • @soninhafrancine
  • @AecioNeves
  • @tatyanavaleria
  • @ClariceChacon
  • @rosilenedf
  • @raulchristiano
  • @Rafael_Parente
  • @RicardoFabrizio
  • @marlonluz
  • @eduardopinheiro
  • @falcaopatos
  • @diegotavares
  • @RubinhoDivi
  • @depmariomotta
  • @depchinaglia
  • @Sen_Cristovam
  • @marcelvanhattem
  • @lgmbrasilia
  • @nilvanferreira
  • @DelioPinheiro
  • @RafaellMilas
  • @chagasvieira
  • @GabrielSouza_RS
  • @Pimenta13Br
  • @fabriziomeller
  • @rigotto
  • @gustavotutuca
  • @camasao50
  • @radiovaldo
  • @euserafimcorrea
  • @merisio
  • @rafangeli
  • @pauloteixeira13
  • @crismonteirosp
  • @ManuelaDavila
  • @tomioyano
  • @CintyaMuniz
  • @rodrigolimamdh
  • @Biango
  • @samiabomfim
  • @profsta
  • @Jfelippeneto
  • @depguibismarck

2745 more accounts are in this list

Leave an email to open the full list here, and we will tell you when it changes — the daily watch already compares our pinned commit to theirs.

To be clear: this list is public. It lives in an Apache-2.0 repository, and you can read the same file for free — home-mixer/filters/brazil_2026_election_filter.rs. The email buys convenience here, not access.

The numeric ids in the file are obfuscated — the repository says so itself. The handles are not.

3 more accounts are named with no country attached
ThunderServiceImplthunder/thunder_service.rs:30
3
accounts written into the source, by id and by handle
unless you follow
their posts, reposts, quotes and thread ancestors are removed from For You — unless the viewer already follows the account
no country test
no country, market or locale condition appears in the filter or where it is registered

⚠ No country, market or locale condition appears in the filter file or around the line that registers it. That is a statement about the published code — which pipeline serves which market may be decided in a layer X does not publish.

All 3 accounts, in source order

  • @grok
  • @gork
  • @products

The numeric ids in the file are obfuscated — the repository says so itself. The handles are not.

This list is reproduced from a public Apache-2.0 repository, which states the handles are included for transparency. We report what the code does. We do not endorse, verify or judge the reports that placed any account here.

Government takedowns

The mechanism is published. The requests are not.

The repository ships the rules that hide a post from readers in a country that asked for it. It ships no request, and no country: the reason that carries the country code is fetched at runtime from a service X does not publish.

0

takedown requests published in the repository

0

countries attached to one

2

rules that act on a takedown reason

How this is counted: every file in the repository is scanned at build time — not only the Rust ones, because the takedown plumbing also lives in Scala, and an absence measured where the thing no longer is is not an absence. Test code is removed at two scales — files a parent declares `#[cfg(test)] mod …;` are excluded, and in the rest the `#[cfg(test)]` body is cut — and what is looked for is a country code written next to a takedown reason. The two-letter codes the source itself names worldwide (`xx`, `xy`) mean everywhere rather than a country: they are set aside, and listed in the open. The build fails if this number ever stops being zero — the claim above cannot go stale on its own.

Where the country comes from: the type that carries it, TakedownReason, is defined in no file of this repository — it is imported from xai_core_entities, a crate that is not published here.

And the method that would fetch the countries of a takedown, get_takedown_country_codes, does not appear anywhere in the repository at all. The interface exists on X's side; the published code never calls it.

Read this the right way: a repository with no hard-coded country is not a platform without country takedowns. The requests exist — they live outside this repository, aggregated by country and by half-year in X's transparency reporting, never notice by notice. What is measured here is what the code says, not what the company does.

Method and limits

What this page cannot tell you

The model is not the code

The weights above are applied to probabilities produced by Phoenix, a learned transformer whose trained parameters are not published. Reading the code cannot rule out a correlation the model learned on its own.

Some things are withheld

X states it holds back the Grox LLM prompt templates and some enforcement rules, to reduce gaming. The classifier code and the rule engine are published; the prompt text is not.

Nobody can verify what runs

X says cron scripts keep these defaults equal to production values, and that experiments above roughly 10% of traffic should be visible here. Both are claims about a system nobody outside X can observe.

Parameter names and code excerpts are kept in English: they quote source code, and a translated identifier stops matching the line it cites. The claim checks are translated — the identifiers inside them are left verbatim.

Why a data company built this

This is the method we sell, applied to a source everyone argues about: take something opaque, turn it into a graph where every value carries the line it came from, and refuse to publish a number we cannot cite. We do the same for public procurement, research funding and company data.