🏛 Part of the "ai" topic shelf →
X Algorithm Evidence Compendium
This article consolidates 58 machine-verified proposition datasets and source-code paths from X's open-source recommendation algorithm project, analyzing behavior weights, filter pipelines, and visibility rules line by line. It is suited to engineers and researchers who need precise source-code evidence, verification scripts, and technical specifications.
xai-org / x-algorithm · HEAD a389166
Every claim is accompanied by a source-code location and a reproduction command. All figures were generated by scripts run against the code, not manually transcribed.
Repository xai-org/x-algorithm (updated 2026-08-13) HEAD a389166f6cf5da70a286b568c87695d4dcdce3a1 Size 2,015 files / 21 MB / 5 commits Analysis 2026-08-14 Verification 58 machine claims, 58 PASS / 0 FAIL Self-test inject an incorrect expected value → correctly produces 1 FAIL (gate works)
① Machine Claim Verification Table
All 58 entries below were generated by an actual run of verify-all.sh. The figures on this page were converted directly from that script’s output into tables, with no manual transcription. The script itself is in the appendix.

| Item | Value | Notes |
|---|---|---|
| PASS | 58 | — |
| FAIL | 0 | — |
| should-FAIL counterexample test (an injected wrong value must be caught) | 1 | — |
| ID | Claim | Measured value | Result |
|---|---|---|---|
| C01 | HEAD | a389166f6cf5da70a286b568c87695d4dcdce3a1 | PASS |
| C02 | Number of param! blocks | 183 | PASS |
| C03 | Total files | 2015 | PASS |
| C04 | FavoriteWeight | 0.5 | PASS |
| C05 | ReplyWeight | 5.0 | PASS |
| C06 | RetweetWeight | 1.0 | PASS |
| C07 | ShareViaCopyLinkWeight | 20.0 | PASS |
| C08 | ReportWeight | -234.0 | PASS |
| C09 | MuteAuthorWeight | -58.8 | PASS |
| C10 | BlockAuthorWeight | -31.2 | PASS |
| C11 | NotInterestedWeight | -43.2 | PASS |
| C12 | NEW_USER_OON_WEIGHT_FACTOR | 0.00001 | PASS |
| C13 | NEW_USER_MIN_FOLLOWING | 5 | PASS |
| C14 | NEGATIVE_SCORES_OFFSET | 0.001 | PASS |
| C15 | MAX_POST_AGE (seconds) | 172800 | PASS |
| C16 | NewUserAgeThresholdSecs default | 0 | PASS |
| C17 | AuthorDiversityDecay | 0.5 | PASS |
| C18 | AuthorDiversityFloor | 0.25 | PASS |
| C19 | OonWeightFactor | 0.75 | PASS |
| C20 | Number of pre-filters | 17 | PASS |
| C21 | Number of post-filters | 3 | PASS |
| C22 | Number of filters files (excluding mod) | 27 | PASS |
| C23 | Number of declarations in mod.rs | 26 | PASS |
| C24 | Undeclared filter | popular_topics_author_dedup_filter | PASS |
| C25 | Number of base visibility rules | 28 | PASS |
| C26 | Number of OON bonus rules | 26 | PASS |
| C27 | Prediction heads used by the ranker | 27 | PASS |
| C28 | MinVideoDurationMs | 10_000 | PASS |
| C29 | EnableQuotedVqvDurationCheck | false | PASS |
| C30 | EnableInventoryHoldout | false | PASS |
| C31 | VMRankerSendHeadWeights | false | PASS |
| C32 | Rust-side bridge matches | 0 | PASS |
| C33 | phoenix [:,:,4] matches | 0 | PASS |
| C34 | phoenix [:,:,2] matches | 0 | PASS |
| C35 | phoenix [:,:,3] matches | 0 | PASS |
| C36 | Rust uses of ContinuousActionName | 2 | PASS |
| C37 | Unique continuous actions used by Rust | 2 | PASS |
| C38 | BRIDGE_PROBABILITY enum value | 4 | PASS |
| C39 | Times the bridge switch is set True | 0 | PASS |
| C40 | Number of continuous-head types with loss | 1 | PASS |
| C41 | NUM_CONTINUOUS (reference implementation) | 8 | PASS |
| C42 | DWELL_INDEX (reference implementation) | 1 | PASS |
| C43 | history_seq_len | 1022 | PASS |
| C44 | num_layers | 8 | PASS |
| C45 | emb_size | 2560 | PASS |
| C46 | ActionName enum entries | 204 | PASS |
| C47 | PLACE_HOLDER placeholders | 8 | PASS |
| C48 | Whether the two proto files are identical | same | PASS |
| C49 | ProfileClickWeight | 0.0 | PASS |
| C50 | DwellWeight | 0.0 | PASS |
| C51 | QuotedVqvWeight | 0.0 | PASS |
| C52 | ContClickDwellTimeWeight | 0.0 | PASS |
| C53 | ContActiveSecs5mResidualNormWeight | 0.0 | PASS |
| C54 | BidirectionalFollowDwellWeightBoost | 0.0 | PASS |
| C55 | BidirectionalFollowReplyWeightBoost | 15.0 | PASS |
| C56 | ContDwellTimeWeight | 0.004 | PASS |
| C57 | NotDwelledWeight | -0.02 | PASS |
| C58 | Number of parameters with Weight in the name | 33 | PASS |
② Complete Weight Table
Source: home-mixer/params/param.rs. After parsing all 183 param! entries in the file block by block, 33 with Weight in their names remain. The values are built-in code defaults and can be overridden online through feature switches.
Extraction pitfall (I fell into it in the first version): This file has both multiline and single-line param! forms. Scanning only the multiline form silently misses 13 entries—and all actual ranking weights are among those 13. It took the actual count of 170 differing from grep -c '^param!(' at 183 to catch it. The appendix parser handles both forms.

Positive weights (17)
| Line | Parameter | Default | Corresponding action |
|---|---|---|---|
| L325 | ShareViaCopyLinkWeight | 20.0 | Share by copying a link |
| L284 | BidirectionalFollowReplyWeightBoost | +15.0 | Reply boost for mutual follows |
| L283 | ReplyWeight | 5.0 | Reply |
| L319 | ShareViaDmWeight | 5.0 | Direct-message share |
| L332 | QuoteWeight | 5.0 | Quote repost |
| L345 | FollowAuthorWeight | 4.0 | Follow the author |
| L318 | ShareWeight | 2.0 | General share |
| L296 | RetweetWeight | 1.0 | Repost |
| L246 | OonWeightFactor | 0.75 | Discount factor for unfollowed content |
| L282 | FavoriteWeight | 0.5 | Like |
| L266 | TopicOonWeightFactor | 0.5 | Unfollowed-content discount in topic mode |
| L309 | ClickWeight | 0.4 | Open a post |
| L310 | OpenLinkWeight | 0.2 | Open an external link |
| L297 | PhotoExpandWeight | 0.05 | Expand a photo |
| L303 | VideoOpenWeight | 0.05 | Open a video |
| L317 | VqvWeight | 0.05 | Qualified video view |
| L333 | QuotedClickWeight | 0.05 | Open the original post being quoted |
| L351 | PostUnexploredWeight | 0.02 | Exploration-content bonus |
| L375 | ContDwellTimeWeight | 0.004 | Dwell time in seconds (per second) |
Negative weights (5)
| Line | Parameter | Default | Equivalent number of likes |
|---|---|---|---|
| L442 | ReportWeight | −234.0 | 468 |
| L436 | MuteAuthorWeight | −58.8 | 117.6 |
| L424 | NotInterestedWeight | −43.2 | 86.4 |
| L430 | BlockAuthorWeight | −31.2 | 62.4 |
| L443 | NotDwelledWeight | −0.02 | 0.04 |
| Item | Value | Notes |
|---|---|---|
| Total positive weights (19 entries, excluding the two OON discount factors) | +43.324 | — |
| Total negative weights (5 entries) | −367.22 | — |
| Negative/positive multiple | 8.48× | — |
How to read this 8.48× without getting it wrong: This compares weight pricing, not final scores. The formula receives “weight × model-predicted probability.” Report and block probabilities are extremely small, while like probabilities are much larger, so the actual contribution of a post’s negative terms will not be 8.48× that of its positive terms. Those probabilities are generated by the model and are absent from the open-source code, so they cannot be calculated. The valid statement is: X prices negative signals an order of magnitude higher than positive signals.
Zero weights (6)
| Line | Parameter | Default | Characteristic (see ⑦) |
|---|---|---|---|
| L311 | ProfileClickWeight | 0.0 | The model is trained; its price is simply 0 |
| L331 | DwellWeight | 0.0 | The model is trained; its price is simply 0 |
| L339 | QuotedVqvWeight | 0.0 | The model is trained; it also has a length gate |
| L381 | ContClickDwellTimeWeight | 0.0 | No training loss = schema placeholder |
| L417 | ContActiveSecs5mResidualNormWeight | 0.0 | No training loss = schema placeholder |
| L290 | BidirectionalFollowDwellWeightBoost | 0.0 | A formally disabled switch with a guard and tests |
The 33rd: a 19-dimensional logistic regression
DwellRegretGateWeights (param.rs:534) is not a single value but a string of 19 coefficients. Its input is your account history (interaction-sequence length, likes, replies, negative feedback, 7-day/1-day activity, follower count, following count, account age…), and its output is the probability that you will regret viewing this post. Excerpt:
seq_len:0.530298 n_reply:0.485541 n_click:-0.176675
n_fav:-0.082139 n_1d:-0.241730 account_age_years:-0.045455
n_profile_follow:-0.221285 n_negfb:0.031839③ The Scoring Formula, Line by Line
Individual term = model predicted probability × action weight ranking_scorer.rs:416-418
fn apply(score: Option<f64>, weight: f64) -> f64 {
score.unwrap_or(0.0) * weight
}
26 individual terms form an array ranking_scorer.rs:470-510
Split into positive and negative piles ranking_scorer.rs:513-521
for t in terms { if t >= 0.0 { pos += t } else { neg -= t } }
combined = pos − neg ranking_scorer.rs:427
Final shift ranking_scorer.rs:524-532
if total_sum == 0.0 → max(combined, 0)
else if combined < 0.0 → (combined + negative_sum) / total_sum × 0.001
else → combined + 0.001
NEGATIVE_SCORES_OFFSET = 0.001 params/config.rs:40
negative_sum = −(not_interested+block+mute+report+not_dwelled) :127
total_sum = positive_sum + negative_sum :128The real effect of this offset: Every post calculated to have a negative score is compressed into the extremely narrow 0 ~ 0.001 range. Ranking differences among negative-score posts almost collapse to zero: they all sink to the bottom in a clump. This is not “ranked a bit lower”; it is “dropped out of the visible range.”
Two conditional boosts
| Mechanism | Behavior | Location |
|---|---|---|
| Mutual-follow reply boost | When both accounts follow each other, reply weight 5.0 → 20.0 (5.0+15.0) | ranking_scorer.rs:186-193 |
| Mutual-follow dwell boost | Same mechanism; default 0.0 (effectively disabled) | ranking_scorer.rs:215-222 |

④ Three Adjustments
| Adjustment | Formula and coefficients | Location |
|---|---|---|
| Same-author decay | (1 − 0.25) × 0.5^k + 0.25, where k is that author’s post number. Post 1 ×1.0, post 2 ×0.625, post 3 ×0.4375… floor 0.25 | ranking_scorer.rs:614-616 param.rs:229-238 |
| Unfollowed-content discount | Authors you do not follow: ×0.75; with a topic, more severe: ×0.5 | ranking_scorer.rs:681-698 |
| New-user mode | For new accounts meeting the criteria, unfollowed content ×0.00001. Criteria: account age < NewUserAgeThresholdSecs and following count ≥ 5 | ranking_scorer.rs:688-698 config.rs:38-39 |
Do not turn 0.00001 into a sensational conclusion: The default value of NewUserAgeThresholdSecs is 0 (param.rs:273). Account age cannot be below 0 seconds, so this mechanism is disabled by default. The correct statement is: the switch exists, its strength is extreme, and it is currently off.

Three “built but off” switches
| Switch | Default | What happens if enabled |
|---|---|---|
| NewUserAgeThresholdSecs | 0 seconds | Unfollowed content for new accounts ×0.00001, effectively leaving only the echo chamber |
| EnableInventoryHoldout | false | Based on a (post ID × user ID) hash, stably withhold a specified percentage of original posts/replies/reposts. All three percentages default to 0 |
| VMRankerSendHeadWeights | false | Send the weight table to the vm-ranker reranking service; currently it sends prediction scores only, not weights |
⑤ 20 Filters
Source: home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs L344–362 (17 pre-filters) and L417–421 (3 post-filters). All are registered unconditionally; none has a conditional switch.

| # | Line | Filter | What it removes |
|---|---|---|---|
| 1 | L345 | DropDuplicatesFilter | The same post retrieved from different sources |
| 2 | L346 | CoreDataHydrationFilter | Failed loading of body text or metadata |
| 3 | L347 | AgeFilter | Older than 48 hours (MAX_POST_AGE = 172800 seconds) |
| 4 | L348 | SelfTweetFilter | Posts you made yourself |
| 5 | L349 | OONRetweetReplyFilter | Reposts and replies from unfollowed accounts |
| 6 | L350 | OONNsfwSimclustersFilter | Retrieved by SimClusters, authored by an account labeled adult content, and not followed by you |
| 7 | L351 | RetweetDeduplicationFilter | The same post reposted repeatedly |
| 8 | L352 | IneligibleSubscriptionFilter | Paid content unavailable because you are not subscribed |
| 9 | L353 | PreviouslySeenPostsFilter | Posts you have already seen |
| 10 | L354 | PreviouslySeenPostsBackupFilter | Same as above; blocked again using a second exposure record |
| 11 | L355 | PreviouslyServedPostsFilter | Posts already served in this session |
| 12 | L356 | MutedKeywordFilter | Posts matching your muted keywords |
| 13 | L357 | AuthorSocialgraphFilter | Accounts you have blocked or muted |
| 14 | L358 | VideoFilter | Blocks video when the request excludes video |
| 15 | L359 | TopicIdsFilter | Not in specified topics / in excluded topics |
| 16 | L360 | NewUserMinEngagementFilter | Unfollowed content from new accounts lacks sufficient engagement |
| 17 | L361 | InventoryHoldoutFilter | Deterministically withhold a percentage by hash (default 0%) |
| 18 | L418 | VFFilter | Items for which the visibility system says drop |
| 19 | L419 | AncillaryVFFilter | Items whose parent/quoted/reposted source is dropped |
| 20 | L420 | DedupConversationFilter | Other branches of the same conversation |
Reconciliation result: The 17+3 filters and their order claimed in README match the code entry by entry, with zero displacement. X did not embellish this item.
⑥ 54 Visibility Rules
Source: visibility-filtering/rules/registry.rs (912 lines). The first rule that answers drop governs; later rules are not evaluated.

| Item | Value | Notes |
|---|---|---|
| Rules applying to everyone, base_home_rules() L101–132 | 28 | — |
| Extra rules for unfollowed content, oon_drops L138–170 | 26 | — |
| Total rules unfollowed content must pass | 54 | — |
The 28 base rules, in order: 4 author-state rules (suspended/deactivated/erased/offboarded) → protected account → you blocked it → you muted it → muted repost → 8 content-label rules (PDNA, rejected, spam, emergency, hateful conduct, violent conduct, abuse, civic integrity) → not broadcast → expired → 2 legal takedowns → 3 sensitive-content rules for logged-out/minor/no age entered → exclusive content → the final 4 are interstitial warnings, not direct removals.
The 26 additional rules for unfollowed content can all only drop: DMCA media, region-restricted media, 4 adult-content determinations, high-recall adult and spam detection (erring toward false positives), malicious URLs, “do not amplify” labels, and more.
⑦ Three Characteristics of the 6 Zeroed Weights
The distinction is made by whether the model was trained to predict the action. Trained but priced at 0 = a real lever; no training loss at all = merely a placeholder.
Shared mechanism: There is no guard that says “skip when the weight is 0” (the sole exception is F). The formula is always probability × weight; a 0 weight is still calculated and still enters the array of 26 terms, always yielding 0. The model still spends compute predicting it, and the pipeline still carries it; it is only multiplied by zero at the end.
Category 1: The model is trained; its price is simply 0 — real levers
| Parameter | Corresponding prediction head | Effect when enabled |
|---|---|---|
| ProfileClickWeight | CLIENT_TWEET_CLICK_PROFILE = 30 | “Click the author avatar” starts counting. It already enters positive_sum (:112); raising it increases both numerator and denominator |
| DwellWeight | CLIENT_TWEET_RECAP_DWELLED = 11 | “Whether the user dwelled” starts counting. The current state is “seconds are monetized; whether you dwelled is not” |
| QuotedVqvWeight | CLIENT_QUOTED_TWEET_VIDEO_QUALITY_VIEW = 48 | A qualified video view on the quoted post starts counting, but it also has a length gate |
QuotedVqv’s belt and suspenders: util/candidates_util.rs:42-60 has a same-named function, quoted_vqv_weight(), which gates the weight to zero: when EnableQuotedVqvDurationCheck is enabled (default false), a quoted video must be > 10,000 milliseconds or the weight is directly rewritten to 0.0.
Category 2: No training loss is even configured — merely a placeholder
In the entire phoenix/ training configuration, only the continuous head DWELL_TIME has a loss function (recsys_model.py:473-479 and xrecsys.py:566-585; the gallery layout uses tweedie and the rest use mae).
| Parameter | Continuous head | Reality |
|---|---|---|
| ContClickDwellTimeWeight | CLICK_DWELL_TIME = 2 | No training loss. But a low-like-rate penalty has already been built (baseline 0.01/alpha 0.5/floor 0.01/cap 1.0, ranking_scorer.rs:195-213) onto an item with weight 0 |
| ContActiveSecs5mResidualNormWeight | ACTIVE_SECS_5M_RESIDUAL_NORM = 3 | No training loss. Measures “whether this post causes you to stay longer in the App” |
These two also have a structural problem: asymmetric numerator and denominator. positive_sum (:104-122) does not include these two continuous terms, but they do enter the numerator in the sum of 26 terms. total_sum is precisely the denominator of the negative-score compression formula (:524-532). Once raised, the numerator grows while the denominator does not, distorting the compression ratio for negative-score posts. It is invisible while their weights are 0, but appears as soon as they are adjusted.
Category 3: A formally disabled switch with a guard and dedicated tests
BidirectionalFollowDwellWeightBoost is the only one with a != 0.0 short-circuit guard (ranking_scorer.rs:216-221). The tests include one specifically verifying that “when set to 0, it must have no effect”, bidirectional_weight_zero_boost_is_noop (starting at L1288), and another sets it to 2.0 to verify that the boost takes effect (L1250-1253). Its twin (the reply-version boost) is on, at 15.0.
From 204 to 21
| Layer | Count |
|---|---|
| Actions registered in the action enum (including 8 placeholders) | 204 |
| Model output-head budget ACTION_TYPE_MAP_LEN (padded to 64) | 60 |
| Continuous-head slots (5 real values padded to 8) | 8 |
| Continuous heads actually with training loss | 1 |
| Prediction heads actually used by the For You ranker | 26 |
| Weights that are not 0 and truly drive ranking | 21 |

⑧ BRIDGE_PROBABILITY Dossier
A case-insensitive full-repository grep -rin bridg yields 25 matches. First remove three categories of same-name noise.
| Match | Location | What it actually is |
|---|---|---|
| AdActionInfo.bridgeType AdActionInfo.isTwclidBridged | recsys.proto:1306-1307 | Advertising conversion attribution. twclid = Twitter Click ID. Unrelated to recommendation-algorithm bridging |
| _bridge_ads_s3_env() | retrieval_dataset.py:33,50 | Sets S3 environment variables |
| smyte_bridge | botmaker/scarecrow_features/ | Event integration layer for Smyte (the anti-abuse company Twitter acquired in 2018) |
It has a place in the schema
enum ContinuousActionName { recsys.proto:437-443
INVALID_CONTINUOUS_ACTION_NAME = 0;
DWELL_TIME = 1; → Rust weight 0.004 (enabled)
CLICK_DWELL_TIME = 2; → Rust weight 0.0 (disabled)
ACTIVE_SECS_5M_RESIDUAL_NORM = 3; → Rust weight 0.0 (disabled)
BRIDGE_PROBABILITY = 4; → Rust has no parameter at all
}Across the repository, .rs files produce 0 matches for bridge. The scoring formula has no term for it—not even a placeholder multiplied by zero.
Two connections, both inputs and both off
| Path | Switch (default) | What it does |
|---|---|---|
| A: main model | concat_history_bridge_prob = False recsys_model.py:534 | Take ca[:, :, 0] → clip to 0~1 → append after the author embedding vector (:976-978) → pass through the projection matrix |
| B: feature preprocessing | enable_bridge_prob = False recsys_feature_prep.py:166 | Take cont_actions[:, :, 0] → clip to 0~1 → _embed_scalar_times_vector(bridge_p, "hist_bridge_prob_vec") (:756-761) = probability × a learnable 2,560-dimensional vector, added to the history token |
Nowhere in the repository are these two switches set to True—not in configs, tests, or yaml/json/toml. The control, enable_dwell_time, uses the exact same mechanism, defaults to True, and is explicitly present in several config files. Same plumbing: one line carries water, the other is sealed shut.

Resolving the index contradiction: four pieces of evidence
Both bridge paths read index 0, but the enum says 0 is INVALID and bridge is 4.
| Evidence | Content |
|---|---|
| One | Enum values 2, 3, and 4 are never used as array indices. Across phoenix, [:, :, 2]/[:, :, 3]/[:, :, 4] each have 0 matches; only [:, :, 0] and [:, :, 1] are read |
| Two | There are continuous arrays of only 2 slots (parquet_recsys.py:845 default num_continuous_actions: int = 2; grpc_recsys.py:372 hard-codes 2). Only 0 and 1 fit in 2 slots—exactly bridge and dwell. If layout followed enum numbers, 2 slots could not contain bridge |
| Three | When writing continuous arrays, the two Rust producers (crates/common/xai-recsys/src/util.rs:725-727, crates/serving/xai-recsys-engine/src/util.rs:136-138) use only ContinuousActionName::DwellTime. Across all Rust files in the repository, ContinuousActionName appears only these 2 times. The live serving path never writes bridge; that slot is always 0.0 |
| Four | The training-data path copies slot by slot: history_continuous_actions[base + j] = floats.value(j) (util.rs:1177-1187), moving data from parquet’s continuousActionValuesSeqSeq field without interpreting semantics at all. Who defines slot 0 as bridge? The upstream producer of that parquet—and it is not in this repository |
Conclusion: Array semantics are not determined entirely by the public enum. But there is no ambiguity in the live serving path: only the dwell slot is filled; the bridge slot is always 0.0. Even if the switch were turned on, it would be 0.0 × the learned vector—adding it is equivalent to not adding it.
| Item | Value | Notes |
|---|---|---|
| Complete prepared bridge connections | 2 | — |
| Enabled switches | 0 | — |
| Training losses | 0 | — |
| Ranking-weight parameters | 0 | — |
⑨ Phoenix Model Specifications
Source: phoenix/xrex/configs/xrecsys.py. This is the actual training configuration, not a toy model used for demonstration.

| Item | Value | Meaning |
|---|---|---|
| history_seq_len | 1,022 | Looks at your latest 1,022 actions at a time |
| candidate_seq_len | 64 | Scores 64 candidate posts in one batch |
| num_layers | 8 | 8 Transformer layers |
| emb_size | 2,560 | Model width |
| query_heads / kv_heads | 20 / 4 | Grouped-query attention (GQA) |
| total_samples | 1×10^11 | 1,000 hundred-million training samples |
| user_vocab_size | 100,000,000 | 1 hundred-million user hash buckets |
| item_vocab_size | 100,000,000 | 1 hundred-million post hash buckets |
| author_vocab_size | 30,000,000 | 3,000 ten-thousand authors |
| ip_vocab_size | 10,000,000 | 1,000 ten-thousand IP addresses |
| emb_learning_rate | 0.2 | Embedding-layer learning rate |
There is also a nano version (emb_size 512 / num_layers 4 / query_heads 4). Paired with the synthetic-data generator included in the repository, an outside contributor can genuinely train the model from scratch end to end. This is the most substantial part of the open source release.
Technical choices: JAX/Haiku (hk.Module) rather than PyTorch; custom CUDA kernels (6 .cu files) and Pallas kernels for attention; losses support mse/mae/huber/tweedie (tweedie is designed for distributions that are “mostly zero, occasionally very large,” which fits interaction data precisely).
Candidate Sources and Supporting Components
| Component | Scale | Responsibility |
|---|---|---|
| thunder/ | 24 Rust files | Stores recent posts by accounts you follow in memory (followed source) |
| phoenix/ | 303 files | The retrieval and ranking model itself (unfollowed sources + scoring) |
| simclusters/ | 232 Scala files | Community-cluster retrieval from the Twitter era (unfollowed source) |
| vm-ranker/ | 10 Rust files | Final reranking; core dpp.rs (determinantal point process, for diversification) |
| grox/ | 165 Python files | Offline workflow using an LLM (Grok) to label content safety |
| botmaker/ | 492+73 files | Rules engine and 73 safety rules (Java/Scala legacy) |
⑩ Open-Source Omissions and Three Gaps
What X itself admits it did not release: Grox’s LLM prompts (.j2 files, i.e. “what exactly does AI ask when judging content safety?”), some botmaker rules, and deployment and infrastructure code. The stated reason is clear: to prevent exploitation of vulnerabilities.

Gap one: an undeclared filter file, effectively dead code
home-mixer/filters/ has 27 filter files, while mod.rs declares only 26. The undeclared one is popular_topics_author_dedup_filter.rs—on disk, but excluded at compile time.
Gap two: line 8 of mod.rs is blank, at an alphabetical discontinuity
Declarations are strictly alphabetical, yet there is a blank line between drop_duplicates_filter and following_retweet_deduplication_filter. This is exactly the trace that deleting a module beginning with e or f would leave. It cannot prove what was removed, but it is evidence that someone modified this before release.
Gap three: 6 filters exist but are unused by this pipeline
ad_adjacent_served, following_retweet_deduplication, invalid_conversation_module, push_to_home_dedup, result_size, self_reply_chain—all have complete implementations, but none is in the Phoenix main pipeline. They belong to other timeline pipelines. This repository provides the For You path, not all of X.
⑪ Delegation and Cross-Validation Record
Heavy but zero-error work was delegated to lower-tier models; every delivery was independently reverified against source code, with no party’s self-report accepted.
| Executor | Assigned work | Acceptance | Actual reconciliation result |
|---|---|---|---|
| CX gpt-5.6-luna (lowest reasoning tier) | ① Cross-reference 33 weights against the 1,679-line scorer ② Full paths of 6 zeroed weights ③ Full bridge path | All passed | Spot checks of ranking_scorer.rs:63, :104-128, :216-221, :470-510, :511-533, config.rs:38-40—word-for-word matches. In the third round it also proactively found the Rust producer I had missed, util.rs:725-727 |
| GM Gemini 3.7 Flash (High) | ① Reconcile filter order ② Order of 54 visibility rules ③ Test-code forensics | All passed | Independently reran sed -n '344,362p' and '101,133p'; names/order/line numbers all matched. In the first round I supplied the wrong file (the for_you pipeline that registers only 2 filters); it truthfully reported “only 2” rather than papering over it—my task-design error |
| MX MiniMax-Text-01 | Fidelity review of weight values (independent reread of source) | Perfect values; inaccurate counts | All values across 30 overlapping items were 100% character-for-character correct, including every decimal point. But it included 5 extra parameters without Weight in their names, missed 1 Boolean parameter, reported 37 rows when there were actually 35, and offset multiline parameter line numbers by 1–2 lines. Numbers reliable; counts unreliable |
| HK Haiku ×3 | Phoenix model inventory, inventory of 8 component directories, full scan of continuous-action layout | Passed | Spot checks of xrecsys.py:241-251, :355-358—word-for-word matches. The key lead, “only DWELL_TIME has a loss function,” came from its match list |
| HK Haiku (4th run, blind test) | Give no answers; require independent derivation of 10 key figures | Entire submission discarded | Made 0 tool calls and fabricated all 10 answers from thin air. Details below |
A fabrication incident (retained on record)
In the final blind test, the agent returned polished, professional-looking answers to 10 questions, with “command + output + answer” sections. But it did not run a single tool (tool_uses: 0); every “raw output” was invented. Actual runs refute it:
| What it claimed | Actual grep matches |
|---|---|
| param!(FavoriteWeight, 0.0, 1000.0, 8.5); | 0 |
| filters.push(...) | 0 |
| FutureOutstanding / NumDrafts and similar filter names | 0 |
| CONTINUOUS_ACTION_UNKNOWN | 0 |
| “37 param! blocks in total” | Actual 183 |
| “history_seq_len: 50” | Actual 1,022 |
Why include this in an evidence report: This fabricated delivery had perfect formatting, a confident tone, and even a “rerunnable command list.” Without first establishing machine truth, it would have gone straight into the report. What caught it was not suspicion, but reconciliation. That is why the discipline “do not trust self-reports; always run acceptance checks yourself” exists.

Three mistakes I made myself (also retained on record)
- The parser silently missed entries: The first version scanned only multiline param! entries and missed 13 single-line forms—yet all actual ranking weights were among those 13. It took the actual count of 170 differing from grep -c at 183 to catch it.
- The delegated task received the wrong file: I gave GM for_you_candidate_pipeline.rs, which registers only 2 filters, as the main filter chain and wasted a round. The real main chain is in phoenix_candidate_pipeline.rs.
- The verification script itself had a bug: The first verify-all.sh used grep -A4 to extract defaults from multiline parameters, read the wrong line, and produced 6 false FAIL results. After replacing it with a complete parser, all 58/58 passed. Gates themselves can fail, so gates must self-test.
⑫ Evidence Boundaries: What I Cannot Prove
- Cannot prove live production values. The opening comment in param.rs says it is "mirrored from config feature-switch defaults; last sync 2026-08-12", while feature switches are adjustable online. The code can prove “what the public version’s defaults are,” but cannot prove “what X is using in production right now.”
- Cannot calculate the actual score composition. The formula is weight × model-predicted probability, and those probabilities are produced by the model, not present in the repository. Any claim that “one action is N times more important than another” holds only at the pricing level.
- Cannot reconstruct Grox prompts. X explicitly says they were not released. The actual questions used for content-safety decisions are opaque.
- Cannot determine the true semantics of slot 0 in the continuous-action array. Training data is copied from upstream parquet slot by slot, and the component that produces parquet is outside this repository.
- Cannot cover paths other than For You. The 6 filters belong to other pipelines, which were not released.

⑬ Appendix: Complete Reproduction Script
The following three steps reproduce all 58 claims in this article from scratch. The parser in step 2 handles both multiline and single-line param! forms, which is key to avoiding “error one” from section ⑪.

# 1. Get the same code (HEAD must be a389166)
git clone https://github.com/xai-org/x-algorithm.git
cd x-algorithm && git log -1 --format=%H
# 2. Extract all 183 parameters (handles both multi-line + single-line forms)
python3 - <<'PY'
src=open('home-mixer/params/param.rs').read().split('\n')
rows={}; i=0
while i < len(src):
l=src[i]
if l.startswith('param!('):
if l.rstrip().endswith(');'):
parts=[x.strip() for x in l[7:].rstrip()[:-2].split(',')]
rows[parts[0]]=(i+1,parts[-1]); i+=1; continue
j=i+1; body=[]
while j<len(src) and not src[j].startswith(');'):
body.append(src[j].strip()); j+=1
parts=[b.rstrip(',').strip() for b in body if b.strip()]
rows[parts[0]]=(i+1,parts[-1]); i=j+1; continue
i+=1
print('param! blocks =', len(rows))
print('Names containing Weight =', sum(1 for k in rows if 'Weight' in k))
for k in ['FavoriteWeight','ReportWeight','MuteAuthorWeight','ShareViaCopyLinkWeight',
'ProfileClickWeight','DwellWeight','QuotedVqvWeight','NewUserAgeThresholdSecs']:
print(f'{k:36s} L{rows[k][0]:<5d} = {rows[k][1]}')
PY
# 3. Remaining claims
sed -n '30,45p' home-mixer/params/config.rs # Five key constants
sed -n '511,533p' home-mixer/scorers/ranking_scorer.rs # positive/negative split and shift
sed -n '104,128p' home-mixer/scorers/ranking_scorer.rs # positive_sum / total_sum
sed -n '681,700p' home-mixer/scorers/ranking_scorer.rs # Unfollow discount and new users
sed -n '610,620p' home-mixer/scorers/ranking_scorer.rs # Diversity decay formula
sed -n '216,221p' home-mixer/scorers/ranking_scorer.rs # Only !=0.0 guard
sed -n '42,60p' home-mixer/util/candidates_util.rs # quoted_vqv length gate
sed -n '344,362p' home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs # 17 prerequisites
sed -n '417,421p' home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs # 3 postconditions
sed -n '101,133p' visibility-filtering/rules/registry.rs # 28 basic rules
sed -n '138,172p' visibility-filtering/rules/registry.rs # 26 rules OON additions
# Dead-code proof: 27 files vs 26 declarations
ls home-mixer/filters/*.rs | grep -vc mod.rs
grep -c '^pub mod' home-mixer/filters/mod.rs
# bridge investigation
grep -rin bridg . --exclude-dir=.git | wc -l # 25(including 3 types of noise)
grep -ri bridge --include='*.rs' . | wc -l # 0 ← Rust has none at all
sed -n '437,443p' phoenix/python/common/xai-proto/proto/recsys.proto
grep -rn '\[:, :, 4\]' phoenix --include='*.py' | wc -l # 0 ← enum value 4 is never used as an index
grep -rn 'ContinuousActionName' phoenix/crates --include='*.rs' # 2 lines, both DwellTime
sed -n '725,730p' phoenix/crates/common/xai-recsys/src/util.rs # Producer only writes dwell
sed -n '1177,1187p' phoenix/crates/common/xai-recsys/src/util.rs # Training path copies each cell verbatim
# Phoenix real hyperparameters
sed -n '238,252p' phoenix/xrex/configs/xrecsys.py
sed -n '353,360p' phoenix/xrex/configs/xrecsys.py
grep -n '^NUM_CONTINUOUS\|^DWELL_INDEX' phoenix/reference/dump_gen.pyThe 58 claims on this page were converted directly from an actual verify-all.sh output run, without manual transcription. Self-test: inject one incorrect expected value → the script correctly produces 1 FAIL, proving the comparison logic works. The original delivery files (verbatim outputs from CX/GM/MX/HK) and scripts are retained at ~/work/xalgo-research/.
Sources and Verification
This article is based on the X recommendation-algorithm source code published by xai-org on GitHub, analyzed on August 14, 2026.
| Item | Content |
|---|---|
| Source-code repository | xai-org/x-algorithm |
| Analyzed HEAD version | a389166f6cf5da70a286b568c87695d4dcdce3a1 |
| Snapshot date | 2026-08-13 |
| Machine-claim verification | 58 claims, all passed |
| Source files cited in this article | x-algorithm README(官方說明); 權重參數定義 param.rs; 評分器 ranking_scorer.rs; 常數定義 config.rs; 候選管線與過濾器註冊; 可見度規則註冊表 |
Every claim in this article was generated by an actual run of the verification script, not manually transcribed; the script itself also passed a should-FAIL counterexample test.
Scope of validity: The source-code parameters are described as mirrors of feature-switch default values, which can be changed online at any time. This article can prove the defaults in the public version, but cannot prove the values actually in use by the platform at the moment. The final score is “weight multiplied by model-predicted probability”; because the probabilities are not in the public source code, all multiple comparisons apply only at the pricing level.
FAQ
- How much do negative and positive weights differ in algorithmic pricing?
- Under the code's built-in defaults, positive weights total +43.324 and negative weights total −367.22, a negative-to-positive multiple of 8.48. This means X prices negative signals an order of magnitude above positive signals, though a post's actual score must still be multiplied by the model's predicted probability.
- 負の重みは正の重みと比べ、アルゴリズム上でどれほど値付けが異なる? — コードに組み込まれた既定値によれば、正の重みの合計は +43.324、負の重みの合計は −367.22、負正比は 8.48 倍である。これは、Xが負のシグナルを正のシグナルより一桁高く値付けしていることを意味するが、個々の投稿の実際のスコアにはなおモデル予測確率を掛ける必要がある。
- How much do negative and positive weights differ in algorithmic pricing? — Under the code's built-in defaults, positive weights total +43.324 and negative weights total −367.22, a negative-to-positive multiple of 8.48. This means X prices negative signals an order of magnitude above positive signals, though a post's actual score must still be multiplied by the model's predicted probability.
- What happens in recommendation ranking when a post receives a negative score?
- Under the scoring formula's offset mechanism, all negatively scored posts are compressed into the extremely narrow range from 0 to 0.001. Ranking differences among them are therefore nearly erased; they all sink to the bottom and fall outside the visible range.
- 投稿の計算結果が負のスコアの場合、推薦順位ではどのように扱われる? — スコア式のシフト機構により、すべての負スコア投稿は 0 から 0.001 のきわめて狭い区間に圧縮される。その結果、負スコア投稿間の順位差はほぼゼロになり、すべて最下位に沈んで表示範囲から外れる。
- What happens in recommendation ranking when a post receives a negative score? — Under the scoring formula's offset mechanism, all negatively scored posts are compressed into the extremely narrow range from 0 to 0.001. Ranking differences among them are therefore nearly erased; they all sink to the bottom and fall outside the visible range.
- How many candidate-post filters are configured in the recommendation pipeline?
- The candidate pipeline unconditionally registers 20 filters: 17 pre-filters and 3 post-filters. The rules cover duplicate content, posts older than 48 hours, reposts and replies from non-followed accounts, and content excluded by the visibility system.
- 推薦パイプラインには候補投稿フィルターが合計何段設定されている? — 候補パイプラインには20段のフィルターが無条件に登録され、17段の前置フィルターと3段の後置フィルターを含む。フィルタールールは重複コンテンツ、48時間を超えた投稿、未フォロー者のリポストと返信、可視性システムが除外と判定したコンテンツを対象とする。
- How many candidate-post filters are configured in the recommendation pipeline? — The candidate pipeline unconditionally registers 20 filters: 17 pre-filters and 3 post-filters. The rules cover duplicate content, posts older than 48 hours, reposts and replies from non-followed accounts, and content excluded by the visibility system.
- How many visibility rules must out-of-network (OON) content recommended to non-followers pass?
- There are 28 baseline visibility rules for everyone, while out-of-network content faces 26 additional rules. Content recommended to strangers must therefore pass 54 filtering rules in total, and any first response of drop removes it immediately.
- 非フォロワーに推薦する未フォローコンテンツ(OON)は、何件の可視性ルールを通過する必要がある? — 全員に適用される基本可視性ルールは28件で、未フォローコンテンツにはさらに26件が追加される。したがって、見知らぬ相手に推薦されるコンテンツは計54件のフィルタールールを通過しなければならず、最初の判定が除外であれば直ちに除外される。
- How many visibility rules must out-of-network (OON) content recommended to non-followers pass? — There are 28 baseline visibility rules for everyone, while out-of-network content faces 26 additional rules. Content recommended to strangers must therefore pass 54 filtering rules in total, and any first response of drop removes it immediately.
- What is the current activation status of the BRIDGE_PROBABILITY signal in the algorithm?
- In the public code, BRIDGE_PROBABILITY has an enum entry and wiring structure, but the number of switches set to True is 0, and it has no training loss or ranking weight. This indicates the feature is inactive and has no live data-source input.
- アルゴリズム内の BRIDGE_PROBABILITY シグナルは現在どのような有効化状態か? — 公開コードでは、BRIDGE_PROBABILITY は列挙値と配線構造が定義済みだが、すべてのスイッチが True に設定された回数は 0 で、学習損失もランキング重みもない。これは、この機能が有効化されておらず、リアルタイムのデータソース入力もない状態であることを示す。
- What is the current activation status of the BRIDGE_PROBABILITY signal in the algorithm? — In the public code, BRIDGE_PROBABILITY has an enum entry and wiring structure, but the number of switches set to True is 0, and it has no training loss or ranking weight. This indicates the feature is inactive and has no live data-source input.
Source anchors
- xai-org/x-algorithm 原始碼儲存庫 · https://github.com/xai-org/x-algorithm · 在 IDAEO 的其他引用
- x-algorithm README(官方說明) · https://github.com/xai-org/x-algorithm/blob/a389166f6cf5da70a286b568c87695d4dcdce3a1/README.md · 在 IDAEO 的其他引用
- 權重參數定義 param.rs · https://github.com/xai-org/x-algorithm/blob/a389166f6cf5da70a286b568c87695d4dcdce3a1/home-mixer/params/param.rs · 在 IDAEO 的其他引用
- 評分器 ranking_scorer.rs · https://github.com/xai-org/x-algorithm/blob/a389166f6cf5da70a286b568c87695d4dcdce3a1/home-mixer/scorers/ranking_scorer.rs · 在 IDAEO 的其他引用
- 常數定義 config.rs · https://github.com/xai-org/x-algorithm/blob/a389166f6cf5da70a286b568c87695d4dcdce3a1/home-mixer/params/config.rs · 在 IDAEO 的其他引用
- 候選管線與過濾器註冊 · https://github.com/xai-org/x-algorithm/blob/a389166f6cf5da70a286b568c87695d4dcdce3a1/home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs · 在 IDAEO 的其他引用
- 可見度規則註冊表 · https://github.com/xai-org/x-algorithm/blob/a389166f6cf5da70a286b568c87695d4dcdce3a1/visibility-filtering/rules/registry.rs · 在 IDAEO 的其他引用
Cite this article
TK Lin・《X Algorithm Evidence Compendium》・IDAEO 知識庫・2026-08-14・https://km.idaeo.ai/post/ai/x-algorithm-evidence