Research — The X algorithm, September 2026
Your posts compete with each other.
X’s open-source For You code makes one author’s posts compete. It happens inside each feed request, by rank, not on a clock, and no posting schedule follows from it.
xai-org/x-algorithm· commit 44d37eb· For You only· repository defaults, not production values
How to read the labels
- Direct
- Read in the source at commit 44d37eb. Each section lists its files and lines in the margin.
- Derived
- Follows mathematically from the source, under the assumptions stated with it.
- Inference
- Follows from the architecture, but is not written as a rule in the code.
- Synthetic
- Output of a simulation written for this note, under chosen inputs. Not evidence about X.
- Unknown
- Cannot be determined from the public repository.
Repository default means a value compiled into the code at 44d37eb. X reads most of them at runtime through feature-switch overlays that are not public, so none is asserted to be live. Compile-time constant means changing it takes a deploy: stronger evidence, and still not proof of production.
01 · Execution order
DirectWhere a post’s score is decided.
Source · 44d37eb
- home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs418–419scorers: phoenix_scorer, vm_ranker
- home-mixer/scorers/value_model.rs40–42enable_author_diversity: false
- home-mixer/scorers/vm_ranker_request.rs32–36value_model_id "dpp", compute: true
- xai-value-model/scoring.rs246–286compute_value_scores_with_adjustment
- vm-ranker/scoring/mod.rs31–37DPP needs the context and the switch
- home-mixer/selectors/top_k_score_selector.rs8–15TopKScoreSelector
- home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs443–447VFFilter, AncillaryVFFilter, DedupConversationFilter
- candidate-pipeline/candidate_pipeline.rs135–141survivors cut to result_size()
- home-mixer/params/param.rs726–731AdsBlenderType, default "multi_risk"
- home-mixer/ads/multi_risk_blender.rs230items.truncate(RESULT_SIZE), ads placed
- home-mixer/candidate_pipeline/for_you_candidate_pipeline.rs323–325response cap, 47
- home-mixer/scorers/vm_ranker.rs108–114on RPC failure, serve local scores
- README.md128–129RankingScorer, as the README draws it
Each For You request runs one pipeline. Candidates are gathered and filtered, a model predicts what this viewer will do with each, the predictions become one value per post, the multipliers apply, a fixed number are selected, and ads are blended in.
The order matters more than any single constant. The repository’s README draws one ranking stage that applies the weighted sum, the author decay, the network discount and a new-author boost. In the code, that work is split between the home-mixer and a separate ranking service, and the diagram follows the code.
- 01≤ 1,200 + 1,000
Retrieval
From accounts the viewer follows: newest first, at most 50 originals and 30 replies or retweets per author, seen posts excluded, about 1,200 in all. From outside the network: embedding retrieval, up to 1,000, and SimClusters.
thunder/thunder_service.rs · 322–326 - 0219 filters
Pre-scoring filters
Nineteen, including: older than 48 hours, the viewer’s own posts, out-of-network replies and retweets, duplicate retweets, already seen, recently served, muted or blocked authors.
home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs · 358–387 - 03p(action)
PhoenixScorer
A transformer predicts, for this viewer, how likely each of about two dozen actions is (reply, like, repost, share, report and the rest), plus a few continuous values such as dwell time.
home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs · 399–406 - 04x = Σ w·p
VMRanker, in the home-mixer
Weights the predictions into one base value per post and decides the small-account lift. Author diversity is switched off here. Everything is sent on to the ranking service.
home-mixer/scorers/vm_ranker.rs · 59–75 - 05x · D(k) · O
Ranking service, value model
Applies the lift, ranks each author’s candidates by base value to get k, then multiplies by the author and network multipliers.
xai-value-model/scoring.rs · 278–285 - 06150 → 50 ?
DPP selection, if it runs
Keeps up to 50 of the top 150 that are both high-scoring and unlike each other; every other candidate is scored 0. Whether this runs in production cannot be determined.
vm-ranker/scoring/dpp_model.rs · 196–206 - 0750
Top-K selection
The 50 highest scores go on. Everything else is out of this request.
home-mixer/selectors/top_k_score_selector.rs · 8–15 - 08
Post-selection filters
Visibility filtering, then only the highest-scoring post of each conversation.
home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs · 443–447 - 0935
Cut to 35
Of the posts that survive, only the 35 highest-scoring go on, whether or not ads are coming.
candidate-pipeline/candidate_pipeline.rs · 135–141 - 10≤ 35 with ads
Blending
Ads and modules are interleaved. When ads are placed, posts and ads together are cut back to 35 items, so each ad displaces a post. The default blender is multi_risk.
home-mixer/ads/multi_risk_blender.rs · 230 - a possible impression
Two details carry most of what follows. The same-author rank k is computed on the base value, before the network multiplier. And every multiplier acts before the first cut, at 50.
Some paths skip the multipliers entirely. If the call to the ranking service fails, if a post is missing from its answer, or if the service has no configuration for the request, the served score is the home-mixer’s own, with no author diversity and no network multiplier. How often that happens cannot be known from the repository.
02 · Same-author diversity
DirectEach post after an author’s best is discounted.
Inside one request, the ranking service sorts each author’s candidates by base value and multiplies the post in position k by
d = 0.5 · f = 0.25 · repository defaults at 44d37eb, not confirmed production values
The author’s best candidate keeps its full score, D(0) = 1. The second is multiplied by 0.625, the third by 0.4375, the fourth by 0.34375, the fifth by 0.296875, and the curve flattens toward the floor of 0.25.
What k is
The position of this post among the same author’s candidates in one viewer’s request, ordered by base value: the score before this multiplier and before the network multiplier.
What k is not
- Minutes since your last post.
- Your post number that day.
- Your lifetime post count.
D(0) = 1 is all the protection the best candidate gets. It can still be discounted as out-of-network, removed by a filter, or miss the cut.
The decay and the floor are read at runtime through feature switches, with per-datacenter overlays that are not in the repository, and no first-party statement of their live values was found. They are repository defaults, and so is every number on this page drawn from them.
03 · Replacement
DerivedA stronger new post demotes the old one. A weaker one does not.
Take an older post, A, that is currently your strongest candidate for some viewer. It gets D(0) = 1. Now publish B.
- If B scores lower for that viewer, B takes k = 1. A stays at k = 0, and its score does not change.
- If B scores higher, B takes k = 0 and A drops to k = 1. A’s multiplier falls from 1 to 0.625.
Before
After a stronger B
That is the cleanest form of cannibalization in this code: a new post takes an older post’s rank, for the viewers who have both as candidates. Nothing in it runs on a clock.
“Stronger” means a higher base value for that viewer, a weighted sum of predicted probabilities, not quality in any global sense. Base values are computed per viewer, so the same two posts can rank one way for one follower and the other way for the next.
04 · The cannibalization band
DerivedWhich older posts a stronger one can push out.
A lower score only costs A something if it moves A across a cut. Call τ the score of the last candidate that still makes this viewer’s top 50, and take the simplest case: both posts carry the same network multiplier, and competing scores are dense around the cut.
So A is pushed below the cut exactly when
with D(1) = 0.625, the repository default
This does not say the new post must be 1.6 times better. It says which older posts are exposed: those whose base value sits between the cut and 1.6 times the cut. Posts well above that survive the demotion, and posts below the cut were not being selected anyway.
In the example above, A falls from 0.80 to 0.50, so it is pushed out for any cut from 0.50 up to, but not including, 0.80.
Show the exact derivation
Write the competitors’ final scores in order, q(1) ≥ q(2) ≥ …, with K = 50 places and A in position r among its author’s candidates. A stronger B displaces A exactly when
Two effects are in it. A’s score falls by D(r+1)/D(r), which is 37.5% for r = 0. And B now holds a place above A, so the competitor A has to beat moves up one, from q(K−r) to q(K−r−1). With dense competitor scores the band is D(r)/D(r+1) wide: 1.60 for r = 0, then 1.43, 1.27, 1.16 and 1.09. The first demotion is the largest.
Assumptions: selection is pure Top-K, the author’s candidates share one network multiplier, there are no ties, and the small-account lift is held fixed.
05 · Finite selection
DirectDerivedSyntheticA positive multiplier is not positive reach.
Source · 44d37eb
- home-mixer/params/config.rs17–18TOP_K 50, RESULT_SIZE 35
- home-mixer/selectors/top_k_score_selector.rs8–15TopKScoreSelector
- home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs443–447VFFilter, AncillaryVFFilter, DedupConversationFilter
- home-mixer/filters/dedup_conversation_filter.rs19–48one per conversation
- candidate-pipeline/candidate_pipeline.rs135–141survivors cut to result_size()
- home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs1015–1017result_size() = RESULT_SIZE (35)
- home-mixer/ads/multi_risk_blender.rs230items.truncate(RESULT_SIZE), ads placed
- Figure 5: closed form under an assumed lognormal spread of post quality
D(k) never falls below 0.25, so author diversity never takes a score to zero. An early draft of this research read that as: every extra post adds some reach. It does not follow.
Correction · withdrawn in revision 2
WasBecause the floor is 0.25, each additional post always contributes, and total daily reach rises with every post.
NowA score is only an input to a selection with a fixed number of places. Whether an extra post adds reach cannot be derived from the code.
A post earns something from a request only if it is retrieved, survives nineteen filters, lands in the top 50 against every other author’s candidates (after being picked by the DPP stage, if that runs), survives visibility filtering and conversation deduplication, and is still among the 35 highest-scoring posts that remain. When ads are placed, they take some of those 35 places.
Where the cut falls is set by the competing candidates for that viewer, and nothing in the repository describes their scores. So whether another post adds reach is an empirical question.
The cut also makes the discount nonlinear. The same 37.5% score cut, from k = 0 to k = 1, costs little when the cut is easy and nearly everything when it is hard. Under an assumed lognormal spread of one author’s post quality:
| Spread σ | Cut p50 | Cut p70 | Cut p80 | Cut p90 | Cut p95 | Cut p99 |
|---|---|---|---|---|---|---|
| 0.4uniform posts | 24% | 15% | 11% | 7.0% | 4.8% | 2.3% |
| 0.8 | 56% | 44% | 38% | 31% | 26% | 18% |
| 1.2uneven posts | 70% | 60% | 54% | 47% | 42% | 33% |
Illustrative model — not X production data
A 37.5% cut in score becomes anything from a 30% to a 98% cut in the chance of clearing the cut. Authors whose posts are uniform in quality are hit harder than authors whose posts vary widely, because the discount is large against their natural variation.
06 · Posting more
DerivedSyntheticInferenceAnother post can replace a winner. In the simple case, it cannot lower the count.
Source · 44d37eb
- home-mixer/selectors/top_k_score_selector.rs8–15TopKScoreSelector
- xai-value-model/scoring.rs147–164author_pool_counts
- xai-value-model/scoring.rs166–175oon_applies
- vm-ranker/params.rs157–162OON rescore for in-network replies, retweets
- xai-value-model/scoring.rs278–285rank on adjusted, then × D(k) × O
- home-mixer/sources/thunder_source.rs106–112author_id, retweeted_tweet_id
- home-mixer/filters/retweet_deduplication_filter.rs19–29RetweetDeduplicationFilter
- Simulation: pure Top-K, 5 competitor scenarios × 1–50 posts × 20,000 trials
Hold the simplest version of the mechanism: pure Top-50 selection, all of an author’s candidates carrying the same network multiplier, and the small-account lift held fixed. Add one more post, of any strength. The number of that author’s posts selected for the request either stays the same or rises by one.
The proof is short. Inserting one value into a sorted list interlaces the order statistics, so the author’s new scores are at least the old ones at every rank. A stronger post can still replace a particular older one, which is the band in 04, but it takes that place rather than removing one.
Two related results hold in the same setting. An author’s selected posts are always their top-ranked ones, with no gaps. And whether the author’s single best post is selected does not involve D(k) at all: it depends only on its own score and the competitors’.
A simulation agrees. Across five competitor scenarios, author set sizes from 1 to 50 and 20,000 trials each, adding a post never lowered the author’s count. Synthetic
Where the simple case breaks
Your retweets and replies. For a follower, your originals carry a network multiplier of 1 and your retweets and replies 0.75, and k is ranked before that multiplier applies. A retweet with a higher base value takes k = 0 and demotes your original to 0.625, while its own score is cut to 0.75. Both can land under the cut. With dense competitor scores the count falls from 1 to 0 whenever
| Post | Base | D(k) | O | Final | Top 3 |
|---|---|---|---|---|---|
| Before · original | 1.10 | 1 | 1 | 1.10 | yes |
| After · retweet | 1.20 | 1 | 0.75 | 0.90 | no |
| After · original | 1.10 | 0.625 | 1 | 0.6875 | no |
| The author’s selected posts: 1 → 0. | |||||
The ordering and the multipliers are read from the code. That a retweet counts toward the retweeter’s own rank is an inference from how the in-network source fills the author field. A retweet is only removed as a duplicate when its source post is already a candidate earlier in the list.
The DPP stage, if it runs. A selection that penalises similarity can lower the count outright; see 15.
07 · A folk rule
DerivedDirect“Don’t post while a tweet is going viral.”
The repository does not support the rule as stated.
If your successful post is still your highest-scoring candidate for a viewer, a weaker new original cannot demote it through D(k). Its multiplier stays at 1, and under pure Top-K selection whether it is selected does not depend on how many other posts of yours are in that request.
A new post can cost the older one something only for viewers where all four of these hold:
- Both posts are candidates in the same request.
- The new post has the higher base value for that viewer.
- The viewer has not already seen the older post. Seen posts are excluded at retrieval, so the effect can only reach the older post’s not-yet-reached audience.
- The drop from 1 to 0.625 actually moves the older post across the cut.
So “never post while a tweet is going viral” is much too strong. The worry is most plausible when the next post is comparably strong and much of the older post’s audience has not seen it yet. Even then, in the simple model, the author’s number of selected posts does not fall: one post replaces the other.
One exception runs the other way: a strong retweet or reply of yours in that window can demote the original for your followers (06). None of this turns into a better universal rule. It turns into a measurement, hypothesis H1 in the experiment below.
08 · Spacing
DirectDerivedUnknownThere is no spacing number in the code.
Source · 44d37eb
- thunder/posts/post_store.rs318–326newest first, excluded ids skipped
- thunder/posts/post_store.rs251–283per-author caps applied
- thunder/thunder_service.rs322–326score_recent: newest first
- thunder/thunder_service.rs223–229max_results
- home-mixer/sources/thunder_source.rs29–42seen_ids excluded
- thunder/config.rs1–61200; 30 replies, 50 originals per author
- home-mixer/params/param.rs23–28ThunderMaxResults 1200
- home-mixer/params/config.rs36MAX_POST_AGE = 48 h
- thunder/args.rs48–49post_retention_seconds 172800
- The 1,200 is ThunderMaxResults, a repository default passed through a load factor defined outside the repository
Nothing here supports 30 minutes, 60, 90, two hours, three hours, or any other interval as a universal optimum. Spacing does not appear in D(k): k is a rank within one request, and the multiplier has no time term.
Spacing can still matter indirectly, because it changes which of your posts are candidates at the same time. For a follower, a post is retrieved in-network only while all of these hold:
- It is under 48 hours old.
- The viewer has not seen it. Seen posts are excluded before anything is counted.
- It is among your 50 newest unseen originals.
- Fewer than about 1,200 newer unseen posts have arrived from everyone the viewer follows. Retrieval is newest first.
Call λj the rate at which the accounts viewer j follows produce eligible posts. The window in which an older post can still share a request with a newer one is then roughly
Structural approximation: constant arrival rate, per-author caps and request timeouts ignored. λj is unknown for every viewer.
Take two hypothetical viewers. One follows 50 quiet accounts: 1,200 newer posts may never arrive within 48 hours, so your two posts can meet for most of the window. The other follows 2,000 hyperactive accounts: your older post can fall out of retrieval within hours. Same posts, same spacing, different candidate sets.
So spacing does not change D(k). It changes how much of the older post’s not-yet-reached audience a newer post can meet while both are candidates, and viewers who already saw the older post cannot be affected at all. For viewers outside your network, retrieval is embedding-based and learned, and no window can be derived. No single spacing number can come out of repository constants.
09 · Lottery tickets
DerivedSyntheticUnknownMore posts are more lottery tickets.
Source · 44d37eb
- xai-value-model/scoring.rs143–145diversity_multiplier
- home-mixer/selectors/top_k_score_selector.rs8–15TopKScoreSelector
- Figure 8: lognormal post quality, closed form, no fitted data
Higher cadence pulls two ways at once.
For
Each original is another draw from your own quality distribution. More draws raise the chance that one is unusually strong, and the author’s best candidate is exactly the one D(k) cannot touch.
Against
More of your posts in the same request means more candidates discounted by D(k), competing for fixed places. Beyond the code: quality dilution, audience saturation, and whatever the authenticity models make of the pattern.
If quality held steady as you posted more, the expected best of five posts would be about 2.14 times a single post, and of ten about 2.80 times, at a spread of σ = 0.8. That is the exploration effect, the one force author diversity cannot reach. Nothing in the repository says whether quality does hold steady.
Written out, the trade-off is
Three of the five terms are unknown. That is why there is no universal answer.
10 · Posting frequency
DirectSyntheticNo posts-per-day number follows from the code.
Source · 44d37eb
- thunder/config.rs1–61200; 30 replies, 50 originals per author
- home-mixer/params/config.rs17–18TOP_K 50, RESULT_SIZE 35
- candidate-pipeline/candidate_pipeline.rs135–141survivors cut to result_size()
- home-mixer/ads/multi_risk_blender.rs230items.truncate(RESULT_SIZE), ads placed
- home-mixer/params/config.rs36MAX_POST_AGE = 48 h
- phoenix/xrex/configs/xrecsys.py703–714feature "post_age"
- bdsm/README.md17–34time-aware RoPE, task heads
- Search: 23 terms across 14 directories, hits triaged by hand
- Figure 9: synthetic sweep of the per-request model
The repository cannot produce “post 4 times a day”, “maximum 6 a day” or “10 a day suppresses you”. A documented search of the ranking and safety code, 23 terms across 14 directories, found no explicit daily threshold and no author post-rate feature. That is a statement about the inspected open-source paths, not about all of X.
What the code does contain, none of it a daily number:
- A per-author cap in in-network retrieval: 50 originals and 30 replies or retweets. Compile-time constants.
- A fixed number of places: the top 50, then the top 35 posts, which placed ads share.
- Author diversity, D(k).
- Seen state: posts a viewer has seen are not retrieved again.
- A 48-hour age cutoff, and post age as a learned model input.
- A behavioural-authenticity model that represents burstiness and mechanical cadence.
Add the two things the code cannot supply, how an author’s quality moves with cadence and how selective each viewer’s feed is, and the optimum is not identified. In a synthetic sweep of the per-request model, where every setting is consistent with the repository, the number of co-present posts that maximised selections ranged from 1 to 50, the whole range tested.
σ 0.4 · uniform posts
| Cut, by quality loss kappa | 0 | .05 | .1 | .2 | .3 | .5 |
|---|---|---|---|---|---|---|
| p50 | 50 | 50 | 50 | 19 | 2 | 1 |
| p70 | 50 | 50 | 50 | 7 | 1 | 1 |
| p80 | 50 | 50 | 50 | 4 | 1 | 1 |
| p90 | 50 | 50 | 50 | 2 | 1 | 1 |
| p95 | 50 | 50 | 50 | 1 | 1 | 1 |
| p99 | 50 | 50 | 50 | 1 | 1 | 1 |
rows: the cut · columns: quality loss κ
σ 0.8
| Cut, by quality loss kappa | 0 | .05 | .1 | .2 | .3 | .5 |
|---|---|---|---|---|---|---|
| p50 | 50 | 50 | 50 | 50 | 50 | 4 |
| p70 | 50 | 50 | 50 | 50 | 50 | 2 |
| p80 | 50 | 50 | 50 | 50 | 49 | 1 |
| p90 | 50 | 50 | 50 | 50 | 16 | 1 |
| p95 | 50 | 50 | 50 | 50 | 6 | 1 |
| p99 | 50 | 50 | 50 | 50 | 1 | 1 |
rows: the cut · columns: quality loss κ
σ 1.2 · uneven posts
| Cut, by quality loss kappa | 0 | .05 | .1 | .2 | .3 | .5 |
|---|---|---|---|---|---|---|
| p50 | 50 | 50 | 50 | 50 | 50 | 50 |
| p70 | 50 | 50 | 50 | 50 | 50 | 30 |
| p80 | 50 | 50 | 50 | 50 | 50 | 15 |
| p90 | 50 | 50 | 50 | 50 | 50 | 6 |
| p95 | 50 | 50 | 50 | 50 | 50 | 2 |
| p99 | 50 | 50 | 50 | 50 | 50 | 1 |
rows: the cut · columns: quality loss κ
Illustrative model — not X production data
That sweep is per request, and public evidence does not link a request to a day either. The defensible conclusion is narrow: N*, the best number of posts per day, has to be measured, account by account.
11 · Mechanical cadence
DirectUnknownTiming patterns are modelled. A cadence penalty is not documented.
Source · 44d37eb
The repository’s safety code includes BDSM, a transformer that reads an account’s recent actions as a sequence. Its position encoding is driven by action timestamps, so, in its README’s words, the model represents “inter-action timing (burstiness, mechanical cadence)”. Its task heads include TweetSpamBot, ReplySpamBot, EngagementAmplifier and MultiActionBot.
That establishes that timing patterns can be represented. It does not expose any rule that turns a cadence into a penalty, and it does not support claims like “posting every hour gets you penalised”. What the model does with timing is unknown.
12 · Post age
DirectUnknownAge is a model input, not a 48-hour constant.
Source · 44d37eb
- home-mixer/params/config.rs36MAX_POST_AGE = 48 h
- home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs361AgeFilter(MAX_POST_AGE)
- phoenix/xrex/configs/xrecsys.py703–714feature "post_age"
- phoenix/xrex/models/recsys_feature_prep.py46–574800 min, 60-min buckets
- phoenix/xrex/configs/xrecsys.py722–733local_hour_of_day, local_day_of_week
There is a hard cutoff: candidates older than 48 hours are removed before scoring, by a compile-time constant. Inside that window the scoring code has no hand-written decay multiplier.
But post age is also an input feature of the ranking model, in hourly buckets across 80 hours. The model can learn how predicted value moves with age, so there is no basis for saying a score stays constant for 48 hours. The learned curve cannot be recovered from the repository.
The same feature list includes the viewer’s local hour of day and day of week. “Best time to post” has a real mechanism; what the model learned from it is unknown.
13 · Small-account exploration
DirectThe small-account lift is an account test first.
Source · 44d37eb
- home-mixer/scorers/author_cold_start.rs171–176cold_start_base_eligible
- home-mixer/scorers/author_cold_start.rs289–338apply_cold_start
- home-mixer/scorers/author_cold_start.rs220–230cold_start_target
- home-mixer/params/param.rs452–500ColdStart… defaults
- xai-value-model/scoring.rs223–244apply_cold_start_decisions
One mechanism favours small accounts. In each request at most one candidate is lifted, and at repository defaults a post qualifies only if all of these hold:
- It is an original: not a reply, not a retweet.
- Its author has at most 1,000 followers (ColdStartFollowerCap).
- It is at most 48 hours old (ColdStartMaxPostAgeSecs).
- It has fewer than 1,000 views on Home (ColdStartImpressionThreshold, a separate parameter that happens to share the value).
- It already ranks in the top 85% of the request’s candidates with a nonzero score (LowImpressionsMaxPositionRatio).
- The viewer’s experiment arm and the author’s bucket allow it.
Among qualifying posts, the highest-scoring one is lifted to at least the score of the request’s 16th-best candidate: the target rank is drawn from [15, 16), counting from zero. The viewer-level switch that enables all of this is on by default.
The lift comes before D(k) and the network multiplier. A lifted out-of-network post is then multiplied by 0.75, and a lifted post that is its author’s second candidate by 0.625. It assigns a score; it does not reserve a place, and the post still has to survive selection, filtering and blending.
So the mechanism is gated on the account first. The post’s own Home views are a second, separate condition. The earlier reading of it as simply “posts under 1,000 impressions” missed the account test.
14 · Replies and threads
DirectReplies and threads, as For You handles them.
Source · 44d37eb
- home-mixer/filters/oon_retweet_reply_filter.rs13–18OONRetweetReplyFilter
- thunder/posts/post_store.rs339–363reply-depth rule
- home-mixer/filters/dedup_conversation_filter.rs19–48one per conversation
- home-mixer/candidate_pipeline/reverse_chron_posts_pipeline.rs164SelfReplyChainFilter
- xai-value-model/scoring.rs166–175oon_applies
- vm-ranker/params.rs157–162OON rescore for in-network replies, retweets
An earlier revision of this research took its reply rules from SelfReplyChainFilter. That filter is only used in the reverse-chronological pipeline, not in For You. The For You rules are three:
- Out-of-network replies and retweets are removed. A reply reaches a For You viewer only if that viewer follows the person who wrote it.
- In-network, a reply to an original post is retrieved with no requirement to follow the original author. A reply to a reply is retrieved only if the viewer also follows the person being replied to. Deeper replies are not retrieved, nor is a reply whose parent is missing from the in-network store.
- Only the highest-scoring post of each conversation survives a request.
So a reply to a large account does not reach that account’s audience through For You: those viewers do not follow you. Any exposure there comes from the conversation view, notifications, search or profiles, which this repository does not cover.
Threads follow from the same rules. Continuations are replies, so only the first post can travel out-of-network, at most the first post and two continuations are retrievable in-network, and a thread yields at most one post per request. But every co-present thread post still takes a same-author rank before the conversation is collapsed, so a strong continuation can demote a separate original and then be removed itself.
| Post | Out-of-network | In-network (follows you) | Multiplier O | Posts per request |
|---|---|---|---|---|
| Original | Yes | Yes | 1, or 0.75 outside the network | 1 |
| Thread, first post | Yes | Yes | 1, or 0.75 outside the network | 1 for the thread |
| Thread continuation | No | The first two only | 0.75 | Shares the thread’s 1 |
| Reply to an original | No | Yes, with no need to follow the original author | 0.75 | 1 per conversation |
| Reply to a reply | No | Only if the viewer follows the person replied to | 0.75 | 1 per conversation |
| Retweet | No | Yes | 0.75 | Deduplicated against its source |
15 · DPP
DirectDerivedSyntheticUnknownA possible second diversity system.
Source · 44d37eb
- vm-ranker/scoring/dpp_model.rs196–206not selected → 0.0
- vm-ranker/dpp.rs46–56pool: top max_selected_rank
- vm-ranker/dpp.rs92–133kernel: exp(α·s/max s), cosine
- vm-ranker/dpp.rs223–314greedy_dpp
- vm-ranker/scoring/mod.rs31–37DPP needs the context and the switch
- vm-ranker/args.rs15–25--dpp-enabled, default false
- vm-ranker/main.rs29–37DPP context built only with the flag
- vm-ranker/params.rs199–211DppEnabled true, DppTheta 0.65
- home-mixer/scorers/vm_ranker_request.rs32–36value_model_id "dpp", compute: true
- README.md281vm-ranker: determinantal point process
- Figure 11: 400 trials per setting, 64-dimension embeddings
The ranking service also contains a determinantal point process, DPP: a selection over post embeddings. If it runs, it takes the top 150 candidates by value score, greedily picks up to 50 that are both high-scoring and unlike each other, and gives every other candidate a score of 0. It is a gate, not a reordering.
It penalises similarity in general, not only the same author twice. And its quality term compresses scores: at the switch default θ = 0.65, the whole pool’s score range maps into a factor of at most e2α ≈ 6.4. Near the cut, a post scoring 10% more than another, both at a quarter of the pool’s top score, gains only about 4.7% in the kernel, so similarity to posts already picked can outweigh a higher score.
Whether it runs in production cannot be established:
- The service’s --dpp-enabled flag defaults to false, and the DPP context is only built when it is set.
- The feature switch DppEnabled defaults to true.
- The home-mixer always asks for the model id “dpp”, and the README describes the stage as part of the system.
- The deployment’s actual flags are not public.
The request and the README show intent, not state. So this page keeps the two paths apart and assumes neither.
If DPP runs, an author whose posts sit close together in embedding space levels off near one selected post per request, beyond anything D(k) does. And adding a post can lower the author’s count outright.
| Selection | Picks | Author’s posts |
|---|---|---|
| Before · DPP or Top-K | C1, A1, A2 | 2 |
| After B · DPP | C1, C2, B | 1 |
| After B · pure Top-K | C1, A1, B | 2 |
In the simulation this was rare but real: in every setting, at most 2% of added posts lowered the count. In one case a new post 10% stronger than the author’s existing best took the author from one selected post to none.
16 · Self-correction
What changed while I investigated.
Revisions
- 1 · First draft, from the code
- 2 · Adversarial audit: fourteen claims corrected, scoped or withdrawn
- 3 · Execution order rebuilt from the code: ten more corrections
The research went through three revisions and an adversarial audit, and it was wrong in places before it was right. These are the corrections that changed a conclusion, including one to this page itself.
Was
No time decay: a score holds for 48 hours.NowPost age is a model input, in hourly buckets. Its effect is learned and cannot be read.
Was
The multiplier never reaches zero, so every extra post adds reach.NowA positive score is not a selection, and a selection is not an impression. Marginal reach cannot be derived.
Was
The small-account lift means posts under 1,000 impressions.NowIt is gated on the author first: at most 1,000 followers, originals only. A Home-view limit is a separate condition. It lifts a score and reserves no slot.
Was
Reply rules come from SelfReplyChainFilter.NowThat filter belongs to the reverse-chronological pipeline. For You uses the out-of-network filter and the in-network reply-depth rule.
Was
There is no per-author cap.NowIn-network retrieval keeps at most 50 originals per author: MAX_ORIGINAL_POSTS_PER_AUTHOR = 50.
Was
Thread continuations can travel out-of-network.NowOnly a thread’s first post can.
Was
Test a spacing of 45 to 90 minutes.NowNo spacing value has support in the code, the documentation or data. Withdrawn.
Was
The ranking service reorders candidates.NowA DPP stage would remove them, and whether it runs is undetermined.
Was
This page said X released the code on 13 August 2026.NowThat is the date of commit a389166, which the first notes read. The repository’s history begins on 20 January 2026.
Conclusion
What can actually be said.
Supported by the source
- Same-author competition exists in the open-source system.
- It is candidate-based: a rank inside one viewer’s request, not a posts-per-day rule.
- A stronger new candidate can cut an older candidate’s diversity multiplier, from 1 to 0.625 at repository defaults.
- Finite selection makes the effect nonlinear: it matters only near the cut.
- Spacing changes how posts overlap, not D(k).
- No universal cadence can be derived from the repository.
Not supported
- That the repository defaults are current production values.
- That 60, 90 or 120 minutes between posts is optimal.
- That X has a documented posts-per-day threshold.
- That posting more always increases total reach.
- That one weight tells you the causal value of an action. Weights multiply predicted probabilities, and the code’s own comments warn against reading their ratios as counts.
Next
The next experiment.
The next step is measurement, on my own account, @itzs_julien. No results are claimed here.
Primary outcomes: daily non-follower impressions, and net follows per day. Not average impressions per post, which has to fall as cadence rises, by the structure of D(k) and the fixed places, so optimising it would always pick one post.
Experimental arms, not recommendations
Design: a randomised crossover over 30 days. Each day is assigned one arm, balanced across weekdays, with no arm repeated more than twice in a row. Spacing varies at random within each day, independently of the arm, instead of running on a fixed timer.
The most useful single test is H1 with H5: whether a new post that is stronger than a still-active one cuts that post’s remaining reach more than a weaker one does, and whether the effect fades with spacing. It needs impressions over time for each post rather than lifetime totals, and a pre-written queue published in random order, so the next post’s strength does not depend on how the last one is doing.
No power calculation is given: there is no variance estimate for this account until the first week of data.
The record
- Reads
- xai-org/x-algorithm, the open-source code of X’s For You recommendation system.
- Pinned to
- Commit 44d37eb, 24 September 2026. Line references are to that commit and drift after it.
- History
- 35 commits, the first dated 20 January 2026.
- Scope
- For You only. Following, Search, notifications and profiles are outside it, and may carry much of a post’s impressions.
- Values
- Repository defaults. The runtime overlays are not public, and no value here is asserted to be live.
- Simulations
- Written for this note in Python, with fixed seeds. Illustrative models under chosen inputs, not X data.
- Also read
- twitter/the-algorithm at c54bec0, last commit 3 September 2025. Its heavy-ranker architecture is superseded, though much circulating Twitter algorithm advice still descends from it.
- Supersedes
- The earlier version of this page, which read commit a389166.
- Upstream licence
- Apache-2.0, quoted for commentary.
- Status
- Research note, revision 3. Not growth advice, and not affiliated with X.
If I can’t leave a thought alone, I make it real.
Some become products. Some become research. Some only needed to exist once.
Griotta is the one I’m building now.
julien@ombrise.com