# How the X (Twitter) algorithm works in 2026

**Last updated 2026-10-11.** This page reads the public For You pipeline. X is the product name. Search still calls the same system the Twitter algorithm. It is not a growth guide and not a measurement of anyone’s impressions.

It follows [`X_ALGORITHM_BOOST_PLAYBOOK.md`](https://github.com/abhirupghosh/tweet-factory/blob/main/X_ALGORITHM_BOOST_PLAYBOOK.md), checked against [`xai-org/x-algorithm`](https://github.com/xai-org/x-algorithm) commit [`cb45b55`](https://github.com/xai-org/x-algorithm/commit/cb45b55) (2026-10-09, “Open-source X Recommendation Algorithm”). The production weight mirror in `home-mixer/params/param.rs` is stamped `last sync 2026-10-08T16:00:35Z`. The dated commit list is [X algorithm changes in 2026](x-algorithm-changes-2026.md). A file-by-file map of the GitHub repo is [Twitter algorithm on GitHub](twitter-algorithm-github.md).

Where a number appears below, it is a published default in that tree. Experiments can override defaults per viewer. `WeightPerturbationSigma` defaults to `0.0`.

## What a For You request does

`README.md` and `home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs` build a request in this order:

1. **Query hydration.** The viewer’s recent engagements (the model’s main input), follow list, blocks and mutes, muted keywords, posts already seen or served, and followed topics.
2. **Candidate sources, in parallel.** In-network: Thunder. Out-of-network: Phoenix retrieval, SimClusters, and a popular-posts list that has been on since the 2026-10-07 param sync. Tweet Mixer, the SID source, and the Phoenix MoE source exist and default off.
3. **Hydration.** Text, media, author, quote target, language, engagement counts, semantic id, and whether the author mutually follows the viewer.
4. **Pre-scoring filters.** Duplicates, age, the viewer’s own posts, out-of-network replies and reposts, muted keywords, blocks, posts already seen or served, and the other filters below.
5. **Scoring.** Phoenix predicts one value per action. A weighted sum turns those predictions into one number. When `vm-ranker` recomputes the sum, it applies author-diversity decay and an out-of-network factor. A cold-start lift can move a new post up.
6. **Selection.** `TopKScoreSelector` keeps the top 50 (`TOP_K_CANDIDATES_TO_SELECT` in `home-mixer/params/config.rs`). The served page is smaller (`RESULT_SIZE` 35, plus modules).
7. **DPP reorder.** `vm-ranker` runs a determinantal point process so neighboring posts are less similar (`vm-ranker/dpp.rs`). Defaults: enabled, theta `0.65`, pool rank `150`.
8. **Post-selection filters.** Visibility filtering, then conversation dedup.

Visibility (drop, notice, interstitial, or allow) is a separate service, `visibility-filtering/`. The ranker does not decide it.

## Where candidates come from

| Source | Default | What the code says |
|---|---|---|
| Thunder | on | Recent posts from accounts the viewer follows. Cap `ThunderMaxResults` 1200. Per author, the return caps in `thunder/config.rs` are 50 originals, 30 replies, and 100 videos. |
| Phoenix retrieval | on | Two-tower retrieval over engagement history. `PhoenixMaxResults` is 1000. The cold-start retrieval cap is 200. |
| SimClusters | on | Favorite-graph clusters (`LOG_FAV_BASED_TWEET`). The source keeps 800; the ANN query cap is 200. Candidate age cap is 48 hours. |
| Popular posts | on since 2026-10-07 | A shared list. Cap 500. The README describes the initial list as posts from the most-followed active accounts. |
| Tweet Mixer, SID, Phoenix MoE | off | Present in the tree, not on by default. |

The hard age cut for For You is `AgeFilter` at `MAX_POST_AGE` = 48 hours. A “Thunder keeps posts for two days” reading comes from a test constructor, not from that filter.

## Filters that run before a score exists

From the README’s filtering section and `home-mixer/filters/`:

- Older than 48 hours.
- Reposts and replies from accounts the viewer does not follow (`OONRetweetReplyFilter`). `EnablePhoenixOonReplies` defaults **false**.
- The viewer has blocked or muted the author.
- Already seen. A bloom filter plus an explicit seen-id set. Recycling the same post id does not get a second impression.
- The viewer’s muted keywords, including hashtag keywords.
- Subscriber-only posts the viewer cannot access.
- The viewer’s own posts (`SelfTweetFilter`).
- A protected author’s post, when the viewer does not follow them (`protected_author` in `visibility-filtering/rules/author_rules.rs`). Followers can still be shown it. A protected account gets no out-of-network For You distribution.

After selection, conversation dedup (`DedupConversationFilter`) keeps one post per conversation id. The highest score wins, whichever post that is. A reply can beat its own thread starter.

## How the number is computed

Phoenix emits a probability, or a continuous value such as dwell time, per head. `compute_weighted_score` in `xai-value-model/scoring.rs` is:

```
weighted = Σ (weight_i × prediction_i)
```

A weight multiplies **that viewer’s predicted probability** of the action (or a predicted duration). It does not multiply raw like or reply counts. The same comments say a report’s weight is large because a report is rare: the baseline probability of a report is more than 1000× lower than a like, so the coefficient has to be large for the prediction to move rank. “One report cancels N likes” is not a reading the code supports.

The published defaults, and the out-of-network and author-diversity multipliers, are on [the weights page](x-algorithm-engagement-weights.md).

`offset_score` then shifts a negative weighted sum so it stays sortable (`NEGATIVE_SCORES_OFFSET = 0.001`). With the published default `multiplier_pre_offset = false`, author diversity and the out-of-network factor multiply after that offset.

## What this tree does not contain

Trained Phoenix weights. The Grox classifier prompts (they are Jinja templates loaded by name, and the README says they are not in the repo). The numeric rule that turns `slop_score` or a safety label into a visibility decision. The experiment bucket a given viewer is actually in. A day-of-week posting constant.

The old quality gate is also gone. There is no `0.4` cutoff on the current Gemma screen. That screen parses an integer `slop_score` and stores it. No cutoff appears on that path.

Safety labels still exist. Grox PTOS categories in `grox/flows/ptos/state.py` are eleven: `HateOrAbuse`, `ViolentSpeech`, `ChildSafety`, `IllegalAndRegulatedBehaviors`, `Spam`, `SuicideOrSelfHarm`, `AdultContent`, `ViolentMedia`, `TerrorismOrViolentExtremism`, `CivicIntegrity`, `AgeGatingHarmfulText`. Spam subtypes include `SpamEngagementFarming`, `SpamEngagementTrading`, `SpamHashTagAbuse`, `SpamScamLinks`, `SpamBotProduced`, and `SpamPlatformManipulation`. The prompts that fire them are unpublished, so this page does not invent a threshold.

tweet-factory does not run this pipeline. [How drafts are scored](how-tweet-factory-scores-drafts.md) is a separate, local heuristic.

## Changelog

The pipeline above is commit `cb45b55`. Older snapshots of this same repository changed defaults without changing the request shape. The short list, with dates from the playbook and the repo README:

| Date | What changed in the published tree |
|---|---|
| 2026-07-24 | Mutual-follow reply boost settled at +15, after a test at +20 (`docs/BIDIRECTIONAL_BOOST_CHANGE.md`). |
| 2026-08-13 | README release: published weights, visibility filtering, and Phoenix training code. The demonstration model was replaced. |
| 2026-09-04 | `e4dcedd` dedupes in-network ids before a visibility-filtering lookup. |
| 2026-09-29 | README: Brazil 2026 election account list updated. The filter drops reported accounts unless the viewer follows them. |
| 2026-10-06 | `e62790c` moves the vm-ranker cluster default from `Experiment6` to `Experiment1`. |
| 2026-10-07 | `78460ca` sets `PostUnexploredWeight` from 0.02 to 0.015 and turns the popular-posts source on. |
| 2026-10-08 | `35650fb` adds favorite-count hydration on the Phoenix index. Video carousel params ship default off. Param mirror stamp `2026-10-08T16:00:35Z`. |
| 2026-10-09 | `cb45b55` changes ranker attention and sequence packing inside `phoenix/xrex/`. The param file bump is the sync timestamp. |

The full notes are on [X algorithm changes in 2026](x-algorithm-changes-2026.md). A browser draft scorer, separate from this ranker, is [tweetfactory.sh/score](https://tweetfactory.sh/score/).

## Related

- [Engagement weights](x-algorithm-engagement-weights.md)
- [Myths: links, hashtags, bookmarks, Premium](x-algorithm-myths.md)
- [Why replies stay in network](why-replies-stay-in-network.md)
- [X algorithm changes in 2026](x-algorithm-changes-2026.md)

## FAQ

### How does the Twitter algorithm work in 2026?

Same pipeline as the X algorithm. At commit cb45b55 the For You request hydrates the viewer, pulls in-network posts from Thunder and out-of-network posts from Phoenix retrieval, SimClusters, and a popular-posts list, drops posts that fail age, social, seen, and reply filters, scores the rest as a weighted sum of predicted actions, keeps the top 50, reorders for diversity, then applies visibility filtering and conversation dedup.

### How does the X algorithm work?

The open-source For You path, at commit cb45b55, hydrates the viewer, pulls candidates from Thunder (in network) and from Phoenix retrieval, SimClusters, and a popular-posts list (out of network), drops posts that fail age, social, seen, and reply filters, scores the rest as a weighted sum of predicted actions, keeps the top 50, reorders for diversity, then applies visibility filtering and conversation dedup.

### Is the X algorithm open source explained by one file of weights?

The weight defaults are published in `home-mixer/params/param.rs` and duplicated, with one difference, in `vm-ranker/params.rs`. The ranker is the whole pipeline above. Trained model weights and the Grox prompts are not in the repo. A default of 0 means that head is off in the mirrored primary config, not that the head does not exist.

### Does the X algorithm rank by raw like counts?

No. `home-mixer/params/param.rs`, `xai-value-model/scoring.rs`, and the README’s 2026-08-14 note say a weight multiplies a predicted probability or a predicted duration. `favorite`’s published coefficient is 0.5. Copy-link share is 20.0, reply is 5.0, and quote is 5.0. Those are coefficients on probabilities, not “one reply equals ten likes.”
