# Phoenix, and the X algorithm release that doesn't compile

URL: https://www.thedeepfeed.ai/posts/2026-05-16-x-algorithm-phoenix-release/
Category: Products
Published: 2026-05-16
Author: the-deep-feed
Tags: x, twitter, algorithm, open-source, recommendation-systems, transformers, grok
Kind: deep

> On May 15, X pushed 18,263 lines of Rust and Python to the public algorithm repo — a complete architectural rewrite away from the 2023 Scala stack, built around a Grok-based transformer. The code does not build, the engagement weights are still missing, and the most interesting thing in it is the model nobody is reading.

## TL;DR

- X pushed **18,263 lines of Rust and Python** to `xai-org/x-algorithm` on May 15, 2026 — the first substantive update since the January 20 reset. **187 files changed; 137 added, 50 modified.** The architecture is a **complete rewrite of the 2023 Scala stack**: no SimClusters, no TwHIN, no EarlyBird, no Light/Heavy Ranker split. Five systems — **Home Mixer, Thunder, Candidate Pipeline (Rust), Phoenix, Grox (Python)** — replace everything.
- Phoenix is **a Grok-based transformer ported from `xai-org/grok-1`** with custom recommendation-system input embeddings and a candidate-isolation attention mask. It predicts **19 binary actions + ≤8 continuous values** per post — not the 14 the README sketch implies. The mini-Phoenix LFS pointer is real: **2,903,518,802 bytes (2.7 GB)**.
- **The code does not compile.** 64 source files reference `crate::params::*` — the module is missing. Every engagement weight constant (`FAVORITE_WEIGHT`, `REPLY_WEIGHT`, ..., `BLOCK_AUTHOR_WEIGHT`, `REPORT_WEIGHT`) is referenced but never defined. **Kafka topic names, SASL passwords, cluster names, prompt templates, `TRACE_USER_IDS` and `TEST_USER_IDS` are all blanked.**
- **Grox is 100% new in May** — 6,500 lines of Python, 59 files, didn't exist in January. It is X's content-understanding layer: VLM-driven classifiers for spam, reply ranking, safety policy enforcement, and **"bangers"** (high-quality posts judged by quality score ≥ 0.4 from a Grok vision-language model, before the network sees them).
- The claim *"X is the most transparent social platform on its main feed algorithm"* is defensible against Meta, ByteDance, and LinkedIn. **It is also incomplete.** Released for show, not for use, lands closer to the truth — and the most useful artifact in the release is the architectural decision document, not the runnable system.

On May 15, 2026, the GitHub repository `xai-org/x-algorithm` received a single commit titled **"Open-source X Recommendation Algorithm."** Author: `CI agent <support@x.ai>`. SHA: `e414c171ed68266341193330bc4864bf3f3534e3`. Stats: **187 files changed, 18,263 insertions, 926 deletions.** Twenty minutes later, **Elon Musk** posted:

> The latest 𝕏 algorithm has been published to GitHub
>
> https://github.com/xai-org/x-algorithm
>
> — [@elonmusk](https://x.com/elonmusk/status/2055277918633562153), May 15, 2026

64,683 likes. The timeline filled up with engineers, growth-hackers, and screenshot threads inside the hour. Within twenty-four hours the chatter had crystallized into the usual three categories — *finally, transparency*; *here is the playbook to go viral*; and *they're hiding the important parts*. All three are partly right. None capture what is actually in the repo, which is more interesting than any of them.

The headline framing (*X open-sourced its algorithm*) is, strictly speaking, four months stale. The repo at `xai-org/x-algorithm` was first published on **January 20, 2026**, then sat at a single commit for [120 days while the chatter on X built up](https://tech.yahoo.com/social-media/articles/x-algorithm-repo-sits-one-142812251.html). The May 15 push is the first real update since the reset. What it shipped is a near-complete rewrite of the 2023 [`twitter/the-algorithm`](https://github.com/twitter/the-algorithm) release: different language stack, different ML paradigm, different orchestration model. The 2023 SimClusters / TwHIN / EarlyBird stack is gone. A Rust+Python codebase built around a **transformer ported from the [Grok-1 open-source release](https://github.com/xai-org/grok-1)** has replaced it.

This piece reads the May 15 release end-to-end. The five systems, the model, the missing pieces, the comparison with 2023, and the gap between what the X community thinks the algorithm does and what the code actually says. The release is much more revealing than the loudest readers are making it. It is also less useful as a how-to-go-viral guide than the same loudest readers are claiming.

# Five systems: three Rust, two Python

The repo ships five top-level systems, three in Rust and two in Python:

| System | Language | Lines | Files | Role |
|---|---|---:|---:|---|
| [`home-mixer/`](https://github.com/xai-org/x-algorithm/tree/main/home-mixer) | Rust | 11,695 | 118 | Request orchestrator. Exposes `ScoredPostsService` and `ForYouFeedService` over gRPC. |
| [`grox/`](https://github.com/xai-org/x-algorithm/tree/main/grox) | Python | 6,500 | 59 | Content-understanding pipeline. Classifiers, embedders, ASR. New in May. |
| [`phoenix/`](https://github.com/xai-org/x-algorithm/tree/main/phoenix) | Python (JAX/Haiku) | 3,880 | 9 | The Grok-based transformer. Retrieval tower + ranking model. |
| [`thunder/`](https://github.com/xai-org/x-algorithm/tree/main/thunder) | Rust | 1,808 | 11 | In-memory per-user post store. Kafka-fed. Sub-ms in-network lookups. |
| [`candidate-pipeline/`](https://github.com/xai-org/x-algorithm/tree/main/candidate-pipeline) | Rust | 1,031 | 10 | Reusable trait framework used by `home-mixer`. |

![Home Mixer at the centre of the X For You stack, with Thunder, Phoenix, Grox, and the candidate-pipeline framework as satellite systems.](/post-images/2026-05-16-x-algorithm-phoenix-release/architecture-overview.jpg)

The decision to split Rust and Python on this clean line is itself a thesis. Rust serves the request-time path where p99 latency, async concurrency, and gRPC streaming matter — `thunder/posts/post_store.rs` uses `Arc<DashMap<...>>` for lock-free reads, the pipeline framework runs hydrators in parallel via tokio, and the whole thing is instrumented with the proc-macro `#[xai_stats_macro::receive_stats(...)]` at every stage. Python carries the ML. Phoenix is **JAX + Haiku** for accelerator inference. Grox is async LLM orchestration around `VisionSampler` and `EapiSampler` calling Grok endpoints.

The 2023 release was a **Scala + Finagle** monolith with TensorFlow ML beside it. None of that infrastructure survived. The 2026 release imports neither `com.twitter.finagle` nor `org.tensorflow` anywhere — there is **zero Scala code** in the tree. This is not an iteration; it is a teardown.

The request flow at runtime, assembled from `home-mixer/server.rs`, `candidate-pipeline/candidate_pipeline.rs:67-137`, and `phoenix_candidate_pipeline.rs:155-351`:

1. **Client** hits `HomeMixerServer` over gRPC.
2. `RequestContext` populates `ScoredPostsQuery`.
3. **Inner pipeline** `PhoenixCandidatePipeline::execute()` runs — 28 query hydrators fetch user context, multiple sources fetch candidates in parallel (Thunder for in-network, Phoenix Retrieval for out-of-network, ads, who-to-follow, prompts), candidate hydrators enrich the result, 14 pre-scoring filters drop ineligibles, then three scorers run in sequence: `PhoenixScorer` (the transformer) → `RankingScorer` (per-user weighted combination) → `VMRanker` (video-specific). A `TopKScoreSelector` sorts.
4. **Post-selection** hydrators run (Visibility Filtering, ads brand-safety, mutual-follow Jaccard), three more filters fire (`VFFilter`, `AncillaryVFFilter`, `DedupConversationFilter`), and side effects publish to Redis and Kafka.
5. **Outer pipeline** `ForYouCandidatePipeline` then mixes the scored organic posts with ads, who-to-follow modules, and prompts using a brand-safety-aware blender, and the result goes back as `ForYouFeedResponse`.

It is a textbook two-stage retrieval-then-ranking system, with the *Grok* novelty being that the ranking stage is a transformer with a custom attention mask. The rest of the structure looks like every modern recommendation system, and that is the first non-obvious finding: **the architecture is mainstream**. The interesting part is one layer down.

# Phoenix, the Grok-based transformer

The architectural claim that organizes the whole release is at line 5 of the top-level [`README.md`](https://github.com/xai-org/x-algorithm/blob/main/README.md):

> The transformer implementation is ported from the [Grok-1 open source release](https://github.com/xai-org/grok-1) by xAI, adapted for recommendation system use cases.

And the editorial pull-quote, fifty lines later:

> We have eliminated every single hand-engineered feature and most heuristics from the system. The Grok-based transformer does all the heavy lifting by understanding your engagement history (what you liked, replied to, shared, etc.) and using that to determine what content is relevant to you.

This is the manifesto. The system reads as if the team made one decision (*replace the entire feature-engineering stack with a single transformer*) and re-derived everything downstream from there.

Phoenix lives in 9 Python files inside [`phoenix/`](https://github.com/xai-org/x-algorithm/tree/main/phoenix). It has two stages.

## Two-Tower retrieval

![Two-Tower retrieval — a tall User Tower and shorter Candidate Tower, connected by a red similarity arrow, with the corpus laid out as dots below.](/post-images/2026-05-16-x-algorithm-phoenix-release/two-tower-retrieval.jpg)

`phoenix/recsys_retrieval_model.py` implements a standard Two-Tower architecture. The **User Tower** (`build_user_representation()` at lines 221-291) concatenates `[user_embeddings, history_embeddings]`, runs them through the full Phoenix transformer, mean-pools over valid positions, and L2-normalizes the result to a `D`-dimensional unit vector. The **Candidate Tower** (`CandidateTower` class at lines 47-112) is deliberately cheap: it takes per-post hash embeddings, optionally runs them through a two-layer MLP with SiLU activation, and L2-normalizes. The similarity computation is one line (`jnp.matmul(user_representation, corpus_embeddings.T)`), followed by `jax.lax.top_k` to grab the top 200 candidates.

This is the same Two-Tower pattern that everyone with an embedding index uses; the novelty is what feeds it. There are **no SimClusters** producing community embeddings. There is **no TwHIN** producing heterogeneous-graph embeddings. There is no RealGraph producing follow-prediction features. The User Tower's representation of a user is its history of `(post_id, author_id, action_type, product_surface, timestamp)` tuples, plus a hash of the user's own ID. The Candidate Tower's representation of a post is hash embeddings of `post_id, author_id, product_surface, post_age_bucket`. Everything else (every fact about the post, every fact about the author, every fact about the conversation) has to be learned through co-engagement.

The bet is that **collaborative filtering at scale, run through a transformer trained on real-time engagement, produces a better representation than any feature-engineered stack the previous team could hand-design**. It is the same bet that Netflix made in 2007 when they retired manual features, and the same bet that YouTube ran in 2016 when they shipped their Deep Neural Network for recommendation. The 2026 X version is one extreme of that bet — *no* features, *only* a transformer over hashed IDs and engagement events.

## The ranking transformer

The ranking stage in `phoenix/recsys_model.py` is the part most readers under-state when they describe Phoenix. The model receives a single sequence (`[user_token, history_tokens..., candidate_tokens...]` of total length `1 + history_seq_len + num_candidates`) and runs it through a standard decoder-only transformer. The clever bit is the attention mask, defined at [`phoenix/grok.py:39-71`](https://github.com/xai-org/x-algorithm/blob/main/phoenix/grok.py):

```python
causal_mask = jnp.tril(jnp.ones((1, 1, seq_len, seq_len)))
attn_mask = causal_mask.at[:, :, candidate_start_offset:, candidate_start_offset:].set(0)
candidate_indices = jnp.arange(candidate_start_offset, seq_len)
attn_mask = attn_mask.at[:, :, candidate_indices, candidate_indices].set(1)
```

Four lines. The user-and-history block has standard causal attention. The candidate block is **zeroed out**, then the diagonal is set back to `1`. The net effect: each candidate attends to the full user-and-history context, but no candidate attends to any *other* candidate. The README calls this **candidate isolation** and gives the reason at line 328:

> During transformer inference, candidates cannot attend to each other — only to the user context. This ensures the score for a post doesn't depend on which other posts are in the batch, making scores consistent and cacheable.

This is the single most consequential decision in the codebase, and the downstream implications stack up. Phoenix scores are **deterministic per-(user, post) pair**: the same post scored against the same user in two different batches gets the same logits. Scores are **cacheable**: once you score post P for user U, you can re-use it. **Batch composition doesn't matter**: there is no game-theoretic interaction where putting bait next to a target shifts the target's score.

It also means the transformer cannot learn to do certain kinds of diversification at inference time. The 2023 stack handled diversification with a separate `AuthorDiversityScorer` (which still exists in 2026 at `home-mixer/scorers/author_diversity_scorer.rs:10-30`, but is **not wired into the production pipeline**, as released). The shape of the bet is: do the heavy work in the transformer, do the cheap diversification work in a post-ranking pass.

![Phoenix attention mask — dark causal-triangle in the upper-left (user+history), red diagonal cells in the lower-right (candidates attending only to themselves).](/post-images/2026-05-16-x-algorithm-phoenix-release/attention-mask.jpg)

## 19 binary actions, 8 continuous heads

![Phoenix's action prediction heads — gray bars for positive engagement (favorite, reply, repost), red bars for negative feedback (block, mute, report).](/post-images/2026-05-16-x-algorithm-phoenix-release/action-heads.jpg)

The README sketch (lines 266-284) lists 14 prediction targets. The actual model predicts **more**. The canonical enum lives at [`phoenix/runners.py:233-253`](https://github.com/xai-org/x-algorithm/blob/main/phoenix/runners.py):

```
favorite_score, reply_score, repost_score, photo_expand_score, click_score,
profile_click_score, vqv_score, share_score, share_via_dm_score,
share_via_copy_link_score, dwell_score, quote_score, quoted_click_score,
follow_author_score, not_interested_score, block_author_score,
mute_author_score, report_score, dwell_time
```

That is **19 binary heads** (the last entry doubles as a continuous label, see `RankingOutput` at lines 274-304). Indices `[14, 15, 16, 17]` (`not_interested`, `block_author`, `mute_author`, `report`) are tagged at line 266 as `NEGATIVE_FEEDBACK_INDICES`. The model predicts the probability that you will *negatively* react to a post, not just the probability that you will positively engage. The architecture itself bakes in "predict the bad outcomes too" as a first-class concern.

Continuous heads add another `≤8` values per post (`recsys_model.py:354`): dwell time, video watch time, scroll depth, and reserved slots. The `ContinuousActionConfig` dataclass at `recsys_model.py:68-72` reveals the only training-loop fingerprint anywhere in the codebase:

```python
class ContinuousActionConfig:
    loss_weight: float = 0.0
    loss_type: str = "mae"
    tweedie_power: float = 1.5
```

A `tweedie_power = 1.5` is a Tweedie regression loss tuned for compound-Poisson-Gamma distributions: the right kind of loss for dwell time, which is mostly zero with a long tail. It is the only hint, anywhere in the release, of how Phoenix is actually trained. The optimizer code, the training data loaders, the loss reduction step, the gradient pipeline: all stripped. Phoenix-as-released is **an inference codebase**.

## Hash-based embeddings, the part that replaces all the features

![Hash embeddings — an ID splits into two hash-bucket lookups, both feeding a central red projection box that emits one combined vector.](/post-images/2026-05-16-x-algorithm-phoenix-release/hash-embeddings.jpg)

The most surprising design decision in Phoenix is also the cheapest to describe. There is no embedding table for *post text*. There is no embedding table for *author bio*. There is no graph embedding for *conversation context*. Every entity in the system (user, post, author) is mapped to **two hash buckets** in a single shared embedding table, and the two vectors are concatenated and projected.

The `HashConfig` at `phoenix/recsys_model.py:93-100`:

```python
@dataclass
class HashConfig:
    num_user_hashes: int = 2
    num_item_hashes: int = 2
    num_author_hashes: int = 2
    num_ip_hashes: int = 0
```

The hash function itself is linear-congruential, at `run_pipeline.py:76-90`:

```python
raw = (ids[i] * scales[j] + biases[j]) % np.int64(modulus)
out[i, j] = 0 if ids[i] == 0 else int((int(raw) % (num_buckets - 1)) + 1)
```

Two hash buckets per entity, drawn from a unified vocabulary table laid out as `[pad(65) | user_vocab | item_vocab | author_vocab]`. The collision rate is not the failure mode you might expect — with two independent hashes, two posts only collide in *both* slots when their hash signatures coincide, which is rare enough at any reasonable bucket count that the model can learn to disambiguate from history.

This is the most extreme version of the "no features" claim. The model doesn't even know what the post says. The post is a 256-dimensional vector pulled by hashing its ID, plus its author's hash, plus its product surface, plus its age bucket. **Everything else has to be learned from co-engagement.**

When the released code says "Grok does the heavy lifting," this is what it means. The heavy lifting is *understanding what kind of post and author this hashed signature represents*, derived entirely from the engagement sequences of the users who saw it.

## The mini Phoenix model — frozen checkpoint, real weights

The README claims a downloadable artifact:

> A pre-trained mini Phoenix model (256-dim embeddings, 4 attention heads, 2 transformer layers) is now packaged as a ~3 GB archive distributed via Git LFS, enabling out-of-the-box inference without training your own model first.

Two things to verify here. First, the artifact is real — `phoenix/artifacts/oss-phoenix-artifacts.zip` is a Git LFS pointer with **`size 2903518802`**, which is exactly 2.7 GB. The SHA-256 (`fbc6017d00588754e22e0c7eb2f786a008a74d309c03c8085fa2fad418a83dac`) is logged in the pointer file. Whether the LFS server actually serves the bytes is a different question and depends on Cloudflare's LFS quota for the org, but the metadata is consistent.

Second, the dimensions disagree across files. The top README says `256-dim, 4 heads, 2 layers`. [`phoenix/README.md`](https://github.com/xai-org/x-algorithm/blob/main/phoenix/README.md#L27) says `128-dim, 4-layer transformer`. The run-scripts (`run_ranker.py:26-51`, `run_retrieval.py:33-58`) hard-code `emb_size=128, num_q_heads=num_kv_heads=2, num_layers=2`. The `run_pipeline.py` end-to-end runner loads dimensions from `config.json` shipped with the LFS artifact, so the artifact's config is the source of truth at load time. The READMEs are aspirational; the run-scripts and shipped configs are the truth. Independent engineers digging in have already flagged a related anomaly:

> i dug into this
>
> in the transformer, all of the actual transformer parameters are zeroes?? so either they're hiding that or for some reason they put a transformer that does nothing in the stack
>
> also, there's a lot missing overall...
>
> — [@kevinlewis4801](https://x.com/kevinlewis4801/status/2055332935868502332), May 15, 2026

This claim (*the transformer parameters are zero*) is plausible if `kevinlewis4801` opened the params files without LFS-pulling, in which case the pointer files contain no actual weights and look like a stub. The model is gated behind Git LFS. Many readers will not have LFS configured. Until someone pulls the 2.7 GB blob, runs `run_pipeline.py` against the sports corpus, and reports back, the answer to "are the weights real?" is *probably yes, conditional on LFS retrieval working*. The pointer's existence and size are not in dispute.

The deeper interpretation in `phoenix/README.md` is the one worth quoting:

> The sample transformer implementation in this repository is ported from the Grok-1 open source release by xAI. The core transformer architecture comes from Grok-1, adapted here for recommendation system use cases with custom input embeddings and attention masking for candidate isolation. **This code is representative of the model used internally with the exception of specific scaling optimizations.**
>
> Smaller model: This is a mini version of the Phoenix model... trained on the same real-time engagement data as the production system. Production uses a larger model with more layers and wider embeddings. **Frozen checkpoint: Production Phoenix is trained continuously on real-time data. This release is a frozen checkpoint from that continuous training process — a snapshot at a point in time.**

That paragraph is the single most useful piece of editorial in the entire release. The shipped mini-Phoenix is **not the production model**, but it is a real model trained on real data, with the same architecture and a similar shape. It is "representative". The production model has more layers, wider embeddings, and continuous online training. The released checkpoint is a frozen January-or-so artifact. Treat it as a competent reference implementation, not a leaked weight set.

# Thunder, the in-memory firehose

![Thunder's three deques per author — gray rows for originals and replies, a red row for video posts, fed from the author ID on the left, time-retained.](/post-images/2026-05-16-x-algorithm-phoenix-release/thunder-stores.jpg)

Thunder is the small one. Eleven Rust files, 1,808 lines. It does one thing well: it serves in-network candidates for the current request in sub-millisecond time.

The architecture is a [Kafka](https://kafka.apache.org/)-fed in-memory store with three per-user deques per author. The core struct at [`thunder/posts/post_store.rs:36-53`](https://github.com/xai-org/x-algorithm/blob/main/thunder/posts/post_store.rs):

```rust
pub struct PostStore {
    posts: Arc<DashMap<i64, LightPost>>,
    original_posts_by_user: Arc<DashMap<i64, VecDeque<TinyPost>>>,
    secondary_posts_by_user: Arc<DashMap<i64, VecDeque<TinyPost>>>,
    video_posts_by_user: Arc<DashMap<i64, VecDeque<TinyPost>>>,
    deleted_posts: Arc<DashMap<i64, bool>>,
    retention_seconds: u64,
    request_timeout: Duration,
}

pub struct TinyPost {
    pub post_id: i64,
    pub created_at: i64,
}
```

Three deques per author — originals, secondaries (replies+reposts), videos. `TinyPost` is sixteen bytes, so the per-author timeline is cache-friendly even when an author has thousands of recent posts. `DashMap` is a sharded-lock concurrent hash map; one author's deque never contends with another's. When a request comes in and asks for the in-network candidates for the user's following list, Thunder iterates the list, picks the deques, drains the latest N, and ships the result.

The bandwidth math works because Thunder is **partition-aware on the read side**. The Kafka topic that feeds Thunder is partitioned by author, and the `tweet_events_listener_v2.rs` consumer (handling the *serving* side) only consumes the partitions whose authors are routed to this Thunder instance. A separate write-path service (`tweet_events_listener.rs`) consumes the raw firehose, re-partitions by author, and produces the derived topic. The two paths split because the firehose is too high-volume to consume on every serving box; the derived topic is downsized to "posts from authors this instance owns."

The retention policy is whatever you set at startup. The default is implicit — `--post_retention_seconds` is a required CLI argument, the released code commits to no fixed number. A background trimmer runs every two minutes (`main.rs:85-89`: `Arc::clone(&post_store).start_auto_trim(2)`) and walks each deque dropping entries older than the retention cutoff.

The Kafka topic names themselves are blanked. [`thunder/kafka_utils.rs:15-19`](https://github.com/xai-org/x-algorithm/blob/main/thunder/kafka_utils.rs):

```rust
const TWEET_EVENT_TOPIC: &str = "";
const TWEET_EVENT_DEST: &str = "";
const IN_NETWORK_EVENTS_DEST: &str = "";
const IN_NETWORK_EVENTS_TOPIC: &str = "";
```

Four string constants, all empty. The SASL passwords, security protocols, and the environment variable name on line 27 (`std::env::var("")` — an empty env var name) are also blanked. The code knows the *shape* of how to connect to Kafka, but the addresses, credentials, and topic names that would let you actually run it are stripped. This is the pattern across the entire release: every secret-flavored constant is gone; every architectural shape is preserved.

The backpressure design is worth quoting. From `thunder/thunder_service.rs:160-170`:

```rust
let _permit = match self.request_semaphore.try_acquire() {
    Ok(permit) => { IN_FLIGHT_REQUESTS.inc(); permit }
    Err(_) => {
        REJECTED_REQUESTS.inc();
        return Err(Status::resource_exhausted("Server at capacity, please retry"));
    }
};
```

A `tokio::sync::Semaphore` with a fixed concurrency cap. The semaphore is *non-blocking*: `try_acquire` returns immediately, and on full capacity the server returns `RESOURCE_EXHAUSTED` rather than queuing. The result is **bounded p99**. The serving box would rather shed load than queue and miss its latency target. Combined with `post_store.rs:228-256`'s per-request timeout check during the iteration over the following-list, the system is **bounded-latency by design** — Thunder responds in sub-millisecond on the happy path and refuses work rather than degrading it.

The README's claim that *"Thunder enables sub-millisecond lookups for in-network content without hitting an external database"* is defensible from the code. No external database is hit on the read path. Everything is in `Arc<DashMap<...>>`. The compression on the gRPC channel is `zstd`; the message wire is protobuf; the in-memory data is a `LightPost` struct with the post body, the author ID, the created-at, and a handful of booleans.

One detail worth flagging because nobody in the X thread is talking about it: **video posts are stored in a third, separate deque**. The eligibility check at `post_store.rs:147-166`:

```rust
let mut video_eligible = post.has_video;
if !video_eligible && post.is_retweet && let Some(source_post_id) = post.source_post_id
    && let Some(source_post) = self.posts.get(&source_post_id) {
    video_eligible = !source_post.is_reply && source_post.has_video;
}
if post.is_reply { video_eligible = false; }
```

A post is video-eligible if it has video and isn't a reply, *or* it's a retweet of an original (not a reply) that has video. Video gets its own deque so the video-surface request (the immersive vertical feed) never has to scan the full posts deque. Replies cannot carry video into the video feed. It is a small, clean separation that mirrors the product surface.

# Home Mixer, the orchestration layer

The biggest of the five systems by file count is [`home-mixer/`](https://github.com/xai-org/x-algorithm/tree/main/home-mixer): 118 Rust files, 11,695 lines. It is the request-handling spine. Two pipelines run inside it.

The **inner pipeline** is `PhoenixCandidatePipeline`. It produces a ranked list of organic posts for a user. The **outer pipeline** is `ForYouCandidatePipeline`. It takes the ranked organic list and mixes in ads, who-to-follow recommendations, and prompts using a brand-safety-aware blender.

The inner pipeline at `home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs:185-232` wires **28 query hydrators** that run in parallel before any candidate is fetched. The hydrators answer: who follows whom, who blocks whom, who mutes whom, what topics is the user following, what starter packs is the user in, what's their geo-IP, what's their inferred gender, what's their impression bloom filter, what's the served-history timeline, what's the mutual-follow MinHash, what posts are cached from a previous request. Then the **candidate sources** fire in parallel: `ThunderSource` (in-network), `PhoenixSource` (out-of-network primary cluster), `PhoenixTopicsSource` (topic-aware retrieval variant), `PhoenixMOESource` (Mixture-of-Experts retrieval variant), `TweetMixerSource` (legacy fallback), `CachedPostsSource` (Redis warmup for follow-up pages).

The fact that there are **three Phoenix retrieval clusters** running in parallel is the kind of detail that gets buried in the architecture diagrams. The MoE cluster routes different users to different expert towers; the topics cluster specializes on topic-affinity retrieval; the primary cluster does generic out-of-network discovery. They feed back through the same `PhoenixScorer` for the ranking pass.

The ten **candidate hydrators** then enrich each candidate with the data the scorer needs:

| Hydrator | What it adds |
|---|---|
| `InNetworkCandidateHydrator` | Sets `in_network = true` if author ∈ followed_user_ids |
| `CoreDataCandidateHydrator` | Post text + core fields from Tweet Entity Service |
| `QuoteHydrator` | Expands quote posts (single-level recursion) |
| `VideoDurationCandidateHydrator` | Pulls video duration in ms |
| `HasMediaHydrator` | Boolean media flag (params-gated) |
| `SubscriptionHydrator` | Paywall / subscriber detection |
| `GizmoduckCandidateHydrator` | Author user-features (verification, age, follower count) |
| `BlockedByHydrator` | "This author blocks you" lookups |
| `FilteredTopicsHydrator` | Topic-id annotations for downstream filtering |
| `LanguageCodeHydrator` | Post language code |

Then **fourteen pre-scoring filters** run, in order:

1. `DropDuplicatesFilter`: multiple sources can return the same post.
2. `CoreDataHydrationFilter`: drop anything that failed to hydrate.
3. `AgeFilter`: drop anything older than `MAX_POST_AGE`.
4. `SelfTweetFilter`: drop the user's own posts.
5. `RetweetDeduplicationFilter`: dedupe reposts of the same content.
6. `IneligibleSubscriptionFilter`: drop paywalled content the user can't read.
7. `PreviouslySeenPostsFilter`: drop what the user has already seen.
8. `PreviouslySeenPostsBackupFilter`: secondary check, new in May.
9. `PreviouslyServedPostsFilter`: drop what's already in this session's served history.
10. `MutedKeywordFilter`: drop posts containing the user's muted keywords.
11. `AuthorSocialgraphFilter`: drop posts from blocked/muted authors.
12. `VideoFilter`: new in May; gates video-only requests.
13. `TopicIdsFilter`: new in May.
14. `NewUserTopicIdsFilter`: new in May; cold-start handling.

Then the **scorers** fire. The pipeline as wired at `phoenix_candidate_pipeline.rs:291-300` runs three of them:

```rust
let scorers = vec![phoenix_scorer, ranking_scorer, vm_ranker];
```

The `PhoenixScorer` (`scorers/phoenix_scorer.rs:60-115`) calls the Phoenix prediction service over gRPC and writes per-action probabilities to each `PostCandidate.phoenix_scores`. The cluster is resolved per-request using a "new-user threshold" (`PhoenixRetrievalNewUserHistoryThreshold` param) and decider keys like `"override_qf_use_lap7"` and `"enable_phoenix_retrieval_lap7_to_fou"`. The fallback design is real engineering: if the egress-sidecar path is enabled and fails, the scorer falls back to a direct client call (`scorers/phoenix_scorer.rs:84-91`).

The `RankingScorer` (`scorers/ranking_scorer.rs:1-60`) takes the 19 raw Phoenix probabilities and combines them into a single relevance score using **per-user weights pulled from feature-switch params at request time**. This is the part of the system that determines whether a like is worth more than a reply for *this specific user* in *this specific experiment cohort*. The weights are not static. They live in `xai_feature_switches::Params` and vary per-decider, per-A/B-test, per-user.

The `VMRanker` is the third scorer — it calls a separate "VM Ranker" gRPC service that almost certainly handles video-specific ranking. The released code is the client glue; the actual VM ranker model is not in this repo.

Three more scorers are defined in source but **not wired** into the production pipeline as released: `WeightedScorer` (the static-weight legacy scorer), `AuthorDiversityScorer` (the per-author exponential decay), and `OONScorer` (out-of-network multiplier). Their presence in the tree is significant — they exist as named modules with imports of `crate::params::*` that the released repo can't satisfy. They are the **ghost scorers**, present in code, dependent on missing params.

# Grox, the content-understanding service

[`grox/`](https://github.com/xai-org/x-algorithm/tree/main/grox) is **100% new in May**. Zero files of it existed in January. Six thousand five hundred lines of Python, fifty-nine files, organized into a multi-process task-scheduler architecture. It is the most interesting addition in the release, because it is the layer that everyone talking about *"how to go viral"* should actually be reading.

Grox is X's **content-understanding** pipeline. It runs vision-language models against posts to produce annotations the recsys can use: spam labels, banger scores, reply-ranking scores, safety verdicts, and multimodal embeddings. The pipeline is fed by Kafka, scheduled by a multiprocess `Dispatcher`, and writes its annotations back to other Kafka topics for the request-time scorers to consume.

![Grox content-understanding pipeline — a Kafka stream flowing into a red dispatcher, fanning out to multiple classifiers and embedders.](/post-images/2026-05-16-x-algorithm-phoenix-release/grox-pipeline.jpg)

The five classifiers at [`grox/classifiers/content/`](https://github.com/xai-org/x-algorithm/tree/main/grox/classifiers/content):

## The banger classifier

`banger_initial_screen.py` is the file that should make every X writer pay attention. The class `BangerInitialScreenClassifier` uses `ModelName.VLM_PRIMARY` at temperature `1e-6` to look at a post (text and images, rendered through `PostRenderer.render(post)`), plus the author's profile, and return:

```python
class BangerInitialScreenResult:
    quality_score: float
    description: str
    tags: list[str]
    taxonomy_categories: list[dict]
    tweet_bool_metadata: TweetBoolMetadata
    is_image_editable_by_grok: bool
    slop_score: int | None
    has_minor_score: float | None
```

A post is positively classified as a "banger" if `quality_score >= 0.4` (line 129). The threshold value is in source. The metric histogram buckets it across `[0.0, 0.1, 0.2, ..., 1.0]`, so the distribution is being tracked at the same resolution.

Two facts about this matter more than every "engagement weight" speculation in the X thread.

**Fact 1: "banger" is a Grok judgment, not an engagement signal.** Whether your post is a banger is decided by a vision-language model *before* the network has seen enough of it to vote. The classifier runs on posts that have crossed a minimum-traction threshold (gated by `MinTractionPostStreamForGroxTaskGenerator` in `generators/stream_generator.py:70-77`), so it is a filter on the candidate pool that survives initial virality — not on every post. But the verdict is editorial, not statistical.

**Fact 2: there is an explicit `slop_score` head.** The classifier returns an integer that explicitly flags AI-generated low-effort content. The threshold and downstream consumer aren't in the released code, but the head's existence is the most concrete statement of editorial position in the entire release. *Grok is reading every traction-positive post and grading how slop-flavored it looks.* The "slop" framing is in the source code, in English, as a field name. The X discourse about "Phoenix knows what bangers are" is reading the right thing from the wrong angle. The model knows what bangers are because **a separate vision-language model graded them as bangers an hour ago**.

The Grok bot (running as `@grok` on X) even summarized the classifier behavior to a curious user in the same hours the release was being read:

> The Phoenix transformer is the Grok-powered AI model that powers X's For You feed algorithm.
>
> It uses a transformer architecture (adapted from xAI's open-sourced Grok-1) to retrieve out-of-network posts and rank them by predicting your likely engagement.
>
> — [@grok](https://x.com/grok/status/2055401212371972218), May 15, 2026

The Grok bot's claim that the model "predicts 14 engagement probabilities" is the README sketch, not the truth. The code has 19. The Grok bot is reading the same README everyone else is.

## The reply ranker

`reply_ranking.py` is the file that resolves the longest-running argument on the timeline (*does X have a reply-quality ranker?*) to *yes, and it's a vision-language model judgment*. The `ReplyScorer` class uses `ModelName.VLM_MINI_CRITICAL` (cheap, fast, mini variant of Grok) with `ModelName.VLM_PRIMARY_CRITICAL` as fallback. It scores replies, not original posts, using a system prompt called `ReplyScoringSystem` parameterized with `large_account_follower_threshold` (line 56, the value is blanked in the release as an empty string).

The output is a 0-3 numeric score with a freeform `reason`. The classifier pairs with `ThreadRenderer` (line 63), which includes the parent post and the conversation context — so reply scoring is **thread-aware, not isolated**. A clever reply to a banger is graded with the banger's context.

The popular X observation —

> Read the whole thing or just know this:
>
> Replies > Reposts > Likes > Links
>
> External links get buried. Long-form beats threads. Premium gets a visibility boost that's widening every quarter.
>
> — [@Alaska0420](https://x.com/Alaska0420/status/2055293616495686138), May 15, 2026

is *underspecified* against the code. The 19-action prediction head is *not* ordered `reply > repost > like` by magnitude; it predicts probabilities for each independently. The per-action weights that combine those probabilities into a relevance score are **per-user feature-switch params**, set by `RankingScorer`, and are **not in the released source code**. The Alaska0420 claim cannot be verified from the May 15 release.

What *is* verifiable: the reply-quality classifier exists, runs on every reply that hits minimum traction, and writes a 0-3 score back to the Kafka stream. Reply ranking on X is not engagement-driven anymore — it is **VLM-judgment-driven**. Whether your reply gets promoted in the reply view depends on what `ReplyScoringSystem` thinks of it. The system prompt itself is not in the release; the imports from `grox.prompts.template` point to a `template.py` file that **was not committed**.

## Spam

`spam.py` is the smallest classifier. 104 lines. The class `SpamEapiLowFollowerClassifier` is (read the name) explicitly targeted at **low-follower accounts**. The system prompt class is `SpamSystemLowFollower`. High-follower accounts skip this classifier entirely.

There are **no hand-engineered spam features**. No Levenshtein. No URL count. No domain blocklist heuristic. The classifier renders the post's thread context through `ThreadRenderer.render` and asks a VLM `spam`/`not_spam`. Binary output. Score `1.0` or `0.0`.

The implication is non-trivial. **Spam detection at the content layer is purely a Grok judgment.** If your spam pattern is novel, the rule-based filters that used to catch it are no longer there. If your spam pattern is novel *and* high-follower, this classifier doesn't see you — you're filtered through `safety_ptos.py` instead (`SafetyPolicyCategory.Spam`, line 220), through a *different* model path with *different* prompt content. Spam is a two-layer system, both layers VLM-driven, neither released.

## Safety PTOS

`safety_ptos.py` is the largest classifier file (288 lines) and the one that maps the platform terms of service to specific policy classes. The category classifier flags which broad policy a post is potentially violating; the policy classifier runs per-category sub-prompts for `ViolentMediaPolicy`, `AdultContentPolicy`, `SpamPolicy`, `IllegalAndRegulatedBehaviorsPolicy`, `HateOrAbusePolicy`, `ViolentSpeechPolicy`, `SuicideOrSelfHarmPolicy`.

There is a "deluxe" mode (`deluxe=True`) that uses EAPI reasoning models (`EAPI_REASONING_INTERNAL`, `EAPI_REASONING`, the expensive, slow, deeper-thinking variant) and a fast mode for high-volume screening. The classifier outputs structured verdicts that feed back into the Visibility Filtering pipeline.

The line that tells you what's been redacted:

```python
_THINKING_RESTRICTION_LINES = {"", ""}
```

Two strings, both empty. The file is structured to load *thinking-restriction lines* from a configurable set — lines about what the model is *not* allowed to think about while making a safety call. Those lines are blanked in the release. The model still has them in production; the public repo gets the placeholder.

## Multimodal embedders v2 and v5

The embedder/ directory has two implementations side by side. The older `multimodal_post_embedder_v2.py` (287 lines) runs **three embedding models in parallel** (`EMBED_PRIMARY` at Qwen3 with 8192 token max, `EMBED_PRIMARY_VIDEO`, and `EMBED_SMALL` at Qwen3 0.6B), with toggles for `use_grok_summary`, `use_media_descriptions`, and `use_post_context_summary`. The older version is a *fat* pipeline that assembles a multimodal post representation by chaining sub-models and summaries.

The newer `multimodal_post_embedder_v5.py` (120 lines) collapses the entire chain to one call. A single model (`ModelName.RECSYS_EMBED_V5`) takes the post and produces a 1024-dimensional vector. No summary toggles. No description fan-out. The model handles multimodal directly. Output is L2-normalized at line 44-55 with hard truncation to `TRUNCATE_DIM = 1024`.

The progression v2 → v5 is the cleanest legible piece of model-evolution in the release: a hand-engineered multimodal pipeline being replaced by a single end-to-end multimodal model. It is the same arc as the rest of the system, applied at a different layer.

# What is *not* in the repo

Three months ago, the most-discussed-on-X charge against the open-sourcing was that it was *theater* — released for show, not for use. Four months sitting at a single commit lent the charge weight. The May 15 push is the substantive update. It is also, on close read, **still a theater release**. The code does not compile or run as-is. The proof is mechanical.

## The `params` module is missing

The `home-mixer/params/` module is referenced by **64 source files**. It does not exist in the tree. `find /tmp/x-algorithm -name "params*" -o -name "params" -type d` returns nothing.

What the missing module defines:

`scorers/weighted_scorer.rs:44-69` reads `p::FAVORITE_WEIGHT`, `p::REPLY_WEIGHT`, `p::RETWEET_WEIGHT`, `p::PHOTO_EXPAND_WEIGHT`, `p::CLICK_WEIGHT`, `p::PROFILE_CLICK_WEIGHT`, `p::VQV_WEIGHT`, `p::SHARE_WEIGHT`, `p::SHARE_VIA_DM_WEIGHT`, `p::SHARE_VIA_COPY_LINK_WEIGHT`, `p::DWELL_WEIGHT`, `p::QUOTE_WEIGHT`, `p::QUOTED_CLICK_WEIGHT`, `p::CONT_DWELL_TIME_WEIGHT`, `p::FOLLOW_AUTHOR_WEIGHT`, `p::NOT_INTERESTED_WEIGHT`, `p::BLOCK_AUTHOR_WEIGHT`, `p::MUTE_AUTHOR_WEIGHT`, `p::REPORT_WEIGHT`, `p::MIN_VIDEO_DURATION_MS`, `p::WEIGHTS_SUM`, `p::NEGATIVE_WEIGHTS_SUM`, `p::NEGATIVE_SCORES_OFFSET`. **Zero of these numbers are anywhere in the repository.**

`scorers/oon_scorer.rs` reads `p::OON_WEIGHT_FACTOR`. Not defined.

`scorers/author_diversity_scorer.rs` reads `p::AUTHOR_DIVERSITY_DECAY` and `p::AUTHOR_DIVERSITY_FLOOR`. Not defined.

`selectors/top_k_selector.rs` reads `params::TOP_K_CANDIDATES_TO_SELECT`. Not defined.

`server.rs:69` references `params::TRACE_USER_IDS` and `params::TEST_USER_IDS`. Not defined.

The only "weights" anywhere in the repo are the demo numbers in `phoenix/run_pipeline.py:355-360`:

```python
weighted = (probs[:, IDX_FAV] * 1.0 + probs[:, IDX_REPLY] * 0.5
          + probs[:, IDX_RT] * 0.3 + probs[:, IDX_DWELL] * 0.2)
```

`FAV=1.0, REPLY=0.5, RT=0.3, DWELL=0.2`. These are toy values for a sports-corpus demo. They are not the production weights. The actual production weights live in `xai_feature_switches::Params` and **vary per-experiment, per-user, per-decider**. Those param defaults are also not in the released code.

This is the single most consequential exclusion. Every confident X thread that says *"Replies are worth 0.5, retweets are worth 0.3, here is the playbook"* is reading the demo file from `run_pipeline.py` and projecting it onto production. The production weights are not in the repo, have never been in the repo, and the design of the `RankingScorer` makes clear they are not even single fixed numbers — they are parameters of an experiment platform.

## Training scripts are missing

`phoenix/` ships **inference code only**. No optimizer, no loss function, no training loop, no data pipeline beyond toy NPZ corpora. The `ContinuousActionConfig` dataclass that mentions `loss_type: "mae", tweedie_power: 1.5` is configuration. The consumer of that configuration is not in the release. Training Phoenix from this codebase requires writing the trainer, the data pipeline, and the optimizer integration from scratch.

The only tests are `test_recsys_model.py` (309 lines) and `test_recsys_retrieval_model.py` (417 lines). They test attention masking, RoPE positional encoding, post-age bucketing, and forward-pass shapes. No training tests.

## Prompts are missing

Every classifier in `grox/classifiers/content/` ships the **inference glue** (prompt building, VLM call, JSON parsing) without the prompts themselves. `SafetyPtos().render()`, `ViolentMediaPolicy().render()`, `ReplyScoringSystem().render()`, `SpamSystemLowFollower().render()` are all imported from `grox.prompts.template`. That file is not in the released tree.

Same for `grox.config.config`, the `ModelName` enum and `grox_config` object referenced across every classifier. The release describes which classifier calls which model name, without exposing which model name maps to which Grok version. Same for `grox.lm.{post, user, convo, thread, post_v5}` — the renderers that build the input strings.

The classifier files are a kit of empty frames. The pictures go in at deploy time.

## Kafka, secrets, and cluster names

Topic names blanked. Cluster names blanked: `local_cache_eds = String::new()`, `atla_phoenix_cache_eds = ""` in `phoenix_candidate_pipeline.rs:357-359`. SASL passwords blanked. The environment variable name on `thunder/kafka_utils.rs:27` is an empty string (`std::env::var("")`), meaning even the *name of the env var holding the credential* has been stripped.

`TRACE_USER_IDS` and `TEST_USER_IDS` are particularly interesting because they are the obvious place a per-account boost would live. **Their contents are blank.** No way to verify from the source whether they contain Musk's account ID or anything else. The 2023 release was [famously caught with a hard-coded check for the Elon-Musk account in the Heavy Ranker code path](https://github.com/twitter/the-algorithm/blob/main/home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/decorator/builder/HomeFeedbackActionInfoBuilder.scala): *not* in `params`, *not* in `TEST_USER_IDS`, but in the `home_mixer` Scala code itself. The 2026 release has **no equivalent hard-coded account-ID check anywhere in the public source**. Any author-level bias in 2026 lives in the learned hash embeddings, in the per-user weighted scorer params, or in the `TRACE_USER_IDS` / `TEST_USER_IDS` lists, none of which are released. The cleanest reading is that the 2023-style boost-Elon check no longer exists in code, but the *mechanism* by which an author can be amplified (via params or trace IDs) is still present and still un-inspectable.

## No build manifests

There is no `Cargo.toml` at the repo root. No `pyproject.toml` at the root. The five Rust subdirectories cannot be built because they depend on workspace-internal crates (`xai_kafka`, `xai_thunder_proto`, `xai_recsys_proto`, `xai_home_mixer_proto`, `xai_candidate_pipeline`, `xai_x_rpc`, `xai_decider`, `xai_feature_switches`, `xai_stats_macro`, `xai_safety_label_store`, `xai_visibility_filtering`, `xai_geo_ip`, `xai_redis_client`) that are not in this repo. The protobufs that define the wire types are not in this repo. The Bazel `BUILD` files are not in this repo.

A reader who clones the repo and tries `cargo build` will get an error within a minute. A reader who clones the repo and tries `python -m phoenix.run_pipeline` against the LFS artifact will, on the other hand, get something close to a runnable demo — `run_pipeline.py` is the most internally consistent file in the release. **The Phoenix mini-model + run_pipeline.py is the only piece of the release that actually runs end-to-end.** Everything else is a description of a system.

# The 2023 release vs the 2026 release

![2023 vs 2026 — the old hierarchical model stack on the left (gray boxes), the unified red Phoenix transformer on the right with retrieval + ranking.](/post-images/2026-05-16-x-algorithm-phoenix-release/2023-vs-2026.jpg)

`twitter/the-algorithm` was published on March 31, 2023. It is still up. It has 73,000 stars. It is also a different codebase in every way that matters.

| Property | 2023 (`twitter/the-algorithm`) | 2026 (`xai-org/x-algorithm`) |
|---|---|---|
| Stars | 73,181 | 16,463 (and rising) |
| License | AGPL-3.0 | Apache-2.0 |
| Primary languages | Scala (66%), Java (20%), Python (3.5%) | Rust (62.9%), Python (37.1%) |
| RPC stack | Finagle (Twitter's Scala framework) | tonic + tokio (Rust async + gRPC) |
| Build system | sbt + Bazel | Cargo (implied) + pyproject |
| Concurrency model | JVM threads + Future | Rust async tasks + DashMap |
| Ranking architecture | Light Ranker (logistic reg) → Heavy Ranker (MaskNet, MultiTask DNN) | Phoenix (single transformer, two stages: retrieval + ranking) |
| Hand-engineered features | ~48 per post (engagement counts, recency, follow-graph signals) | **Zero** for relevance |
| Author/post embeddings | SimClusters + TwHIN + UTEG + RealGraph | Two hash buckets per entity, learned end-to-end |
| In-network retrieval | EarlyBird (Lucene search index) | Thunder (in-memory Kafka-fed store) |
| Out-of-network retrieval | SimClusters / TwHIN heterogeneous | Phoenix Two-Tower (3 cluster variants) |
| Content understanding | None — no LLM components | Grox — 5 VLM classifiers + 2 multimodal embedders + ASR |
| Trust & Safety | Java `tweetypie` filter, VFLib | `safety_ptos` classifier + Visibility Filtering pipeline |
| Ads in repo | No | Yes — `home-mixer/ads/` module |
| Trainable model | TensorFlow + Lightyear (released) | Inference-only (training stripped) |
| Model weights | Source code only | Pre-trained mini Phoenix LFS artifact (2.7 GB) |
| Famous boost-Elon check | Present in source (`HomeFeedbackActionInfoBuilder.scala`) | Not in public source; mechanism implicit in `TRACE_USER_IDS` |
| Will compile as-is | No (missing private deps) | No (missing `params` module + private deps) |
| Production fidelity | "Most of the algorithm and ML code" | "Representative... with the exception of specific scaling optimizations" |

The most important row is the one about hand-engineered features. The 2023 release was, in spirit, a *feature catalog*: every observable property of a tweet or its author was lifted into a column, weighted by a hand-tuned model, and combined by a tree of rankers. The 2026 release is the *opposite* claim — the model is everything, the features are an artifact of the model's training data, and the only inputs are hashed IDs and engagement sequences.

This is not a small evolution. It is a major shift inside the same product. The 2023 codebase has no useful translation to the 2026 codebase. SimClusters does not become a Two-Tower retrieval cluster; it is replaced by one. TwHIN does not become hash embeddings; it is replaced by them. EarlyBird does not become Thunder; it is replaced by it. The 2023 architecture exists in the public source as a complete historical artifact, the 2026 architecture exists as a complete present-tense artifact, and the gap between them is the engineering history of a four-year rewrite. No file in the 2023 repo has a meaningful counterpart in the 2026 repo. The 2023 terminology (Heavy Ranker, Light Ranker, SimClusters, TwHIN, RealGraph, MaskNet, EarlyBird) **does not appear anywhere in the 2026 source tree**.

Crucially, this rewrite happened without an announcement. There was no blog post titled *"How we rewrote the X recommendation algorithm in Rust"*. There was no NeurIPS paper titled *"Phoenix: A Grok-based Recommendation Transformer at Twitter Scale"*. There was no engineering keynote. The first time the public saw the architecture was January 20, 2026, and the first time the public saw the working pipeline was May 15. The interesting deep work shipped quietly inside a private repo for years, and the open-sourcing is the announcement.

# What the X discourse gets right, and wrong

The X thread underneath Musk's tweet split into a fast-forming consensus. Three claims dominate. Read against the code, two are wrong as stated, one is right but irrelevant.

## Claim 1: "Replies > Reposts > Likes > Links" (the engagement-weight hierarchy)

This is the dominant viral framing. Multiple variants of this rank-ordering are getting tens of thousands of impressions in the days after the release. The most-engagement version on the timeline:

> Replies > Reposts > Likes > Links
>
> External links get buried. Long-form beats threads.
> Premium gets a visibility boost that's widening every quarter.
>
> — [@Alaska0420](https://x.com/Alaska0420/status/2055293616495686138), May 15, 2026

The status of this claim against the code is **not in the release**. The actual per-user weights are pulled from `xai_feature_switches::Params` at request time. The released code defines the *shape* of the weighted scorer (`scorers/ranking_scorer.rs`) but never the *values*. The only weights that appear anywhere are the demo values in `phoenix/run_pipeline.py:355-360` (`FAV=1.0, REPLY=0.5, RT=0.3, DWELL=0.2`), which are toy numbers for a sports-corpus demo and have no production meaning.

The ordering *"replies > reposts > likes"* is plausible (it matches the conventional wisdom that engagement-quality scales with action depth), but **it cannot be derived from the May 15 release**. The reader who confidently quotes specific multipliers is reading toy code and writing prescriptions for production.

The honest version of the claim: the Phoenix model predicts probabilities for 19 binary actions. The downstream ranking weights for combining those probabilities are configurable per-experiment and not released. Whatever ordering applies to your account in your experiment cohort is unknown.

## Claim 2: "External links get buried"

This is *partly* in the release. `phoenix/runners.py` has no explicit per-action head for `has_external_link`. The Phoenix scorer does not have an explicit penalty for external links. **But** the `Grox` classifiers include `BangerInitialScreenClassifier` (which judges post quality holistically, including whether the post adds value beyond a link) and the multimodal embedder (which processes the post text, including link URLs).

The mechanism by which an external link could be penalized is therefore indirect: a post that is *just* a link with no commentary plausibly gets a low `quality_score` from the banger classifier, plausibly gets weak engagement predictions from Phoenix because the model has learned that link-only posts under-deliver, and plausibly gets a worse position in the feed. The penalty is **learned, not coded**. The 2023 release had a [hard-coded penalty for external links](https://github.com/twitter/the-algorithm/blob/main/home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/decorator/HomeAdsCandidateFeatureTransformerHydrationContextBuilder.scala). The 2026 release does not. Whether the *effect* is the same depends on what the model learned, which is not auditable from the released code.

## Claim 3: "Premium gets a visibility boost"

This one is interesting. The candidate hydrator `SubscriptionHydrator` (in `home-mixer/candidate_hydrators/`) does add subscription status to every candidate. The Phoenix model takes that as an input feature. The `RankingScorer` does combine it with the rest. Whether the *coefficient* for subscriber status is positive, negative, or zero is determined by the missing `params` module.

There *is* however a relevant filter: `IneligibleSubscriptionFilter` drops paywalled content the user can't access. So premium content that *requires* a subscription is filtered *out* for non-subscribers, not boosted *in* — the opposite of what the X thread is claiming. The "premium visibility boost" claim, if true, applies to *premium accounts* (verified subscribers, blue-check posters), not *premium content*. The released code does not let you distinguish.

## What the engineers see that the timeline misses

The most useful technical reading on the timeline is the negative one:

> i dug into this
>
> in the transformer, all of the actual transformer parameters are zeroes?? so either they're hiding that or for some reason they put a transformer that does nothing in the stack
>
> also, there's a lot missing overall...
>
> — [@kevinlewis4801](https://x.com/kevinlewis4801/status/2055332935868502332), May 15, 2026

Read against the code: the parameters are not zero. The LFS pointer is 2.7 GB. The model weights are there, but they require Git LFS to be pulled. A reader who clones the repo with `git clone https://github.com/xai-org/x-algorithm` *without* `git lfs install` first will find the `phoenix/artifacts/oss-phoenix-artifacts.zip` file is a 130-byte pointer text, and any `.npz` parameter file inside the zip will look like zeros. The bug is in the reader's tooling, not the release. The deeper observation (*also, there's a lot missing overall*) is correct.

The Yahoo Tech reporting on the four-month gap before May 15 captured the same observation more publicly. The repo *did* sit at a single commit for [120 days](https://tech.yahoo.com/social-media/articles/x-algorithm-repo-sits-one-142812251.html). The promise of [monthly refreshes](https://x.com/elonmusk/status/1742514054926500233) made in January was, by May, four months overdue. The May 15 commit is the fulfillment, late, of that promise. It also resets the clock — the next refresh, if there is one, is due in mid-June. The pattern of public commitments to refresh cadence followed by quiet gaps is the **single most useful piece of context for evaluating the May 15 release**. The release happened because the calendar had run out, not because the engineering had stabilized.

# Position: what this release is, and isn't

The most defensible reading of the May 15 release runs in three layers.

**At the marketing layer**, X is right. No other major feed-ranking system (not Meta's, not TikTok's, not LinkedIn's) has published anything close to this much detail about its production architecture. The Apache 2.0 license is permissive. The README is technically substantive. The Phoenix paper-quality description of the candidate-isolation attention mask is the kind of architectural decision that would normally be published at NeurIPS, and here it is in a README in a public repo. **Compared to the alternative of zero transparency, this is real transparency.** The "most transparent main-feed algorithm of any major platform" claim is defensible.

**At the engineering layer**, the release is a sketch. The 18,263 lines do not compile. The five systems describe the *shape* of an architecture without releasing the parts that would let an engineer reproduce, audit, or fork it. Every secret-flavored constant is blanked. Every prompt is missing. Every weight number is in a feature-switch service. The mini-Phoenix LFS artifact is real and runnable, but it is a frozen January-vintage checkpoint of a smaller model than production. The phrase that best fits is **architectural documentation**, not open-source release. The 2023 release at least let you read the Scala that decided your feed; the 2026 release lets you read the *system* that decides your feed, while the *decisions* themselves stay private.

**At the editorial layer**, the most consequential addition in May is **Grox**, and almost nobody is talking about it. The introduction of a content-understanding pipeline that scores posts on aesthetic *quality* (`banger_initial_screen.py`), reply *quality* (`reply_ranking.py`), and *slop detection* (the `slop_score` head) is the most consequential editorial decision X has made since switching to algorithmic ranking in 2016. The platform is no longer *only* an engagement-maximizing recommender; it is an engagement-maximizing recommender *running on top of* an aesthetic classifier that gates which posts get to compete. The threshold (`quality_score >= 0.4`) is in source. The prompt that decides what "quality" means is not. **A vision-language model owned by xAI decides what a banger is, before the network has finished voting on it.** That is a structural change in what kind of platform X is. The release lets you see the structure without letting you see the editorial rubric inside it.

For builders evaluating where this leaves them, three concrete reads.

1. **The "go viral by gaming the algorithm" advice circulating in the days after May 15 is built on demo code.** The toy `FAV=1.0, REPLY=0.5` weights are not production. The production weights are per-user, per-experiment, and not released. Posts ranking the engagement actions in confident order ("Replies > Reposts > Likes > Links") are over-fit to the run-script comment, not the production scorer.

2. **The mechanism that actually decides whether your post gets distribution is two-stage and the first stage is Grok grading you.** The banger classifier runs on every post that passes a minimum-traction threshold. If the VLM thinks your post is slop, low-quality, or off-topic for your followed audience, it never enters the candidate pool with strong signal — regardless of how many likes the first wave of viewers gave it. The most actionable read of the release for creators is **stop optimizing for early-engagement and start optimizing for whatever rubric a Grok VLM would call "high-quality" on your specific post**, which is unknown but plausibly stable across your account's history.

3. **Phoenix is the most architecturally interesting piece of public ML code released by a major social platform since [Pinterest's 2022 PinSAGE paper](https://arxiv.org/abs/2207.07203)**. The candidate-isolation attention mask is a four-line, provably-correct, score-cacheable solution to a problem that recsys teams have been hand-rolling for a decade. The 19+8 action heads with a Tweedie loss for the continuous variables are a clean training target. The hash-only input is the cleanest expression of "let the model learn the features" anyone has shipped at scale. Phoenix-the-architecture is more valuable to the AI/recsys community than Phoenix-the-frozen-mini-checkpoint is to the X-creator community, and it has gotten less attention because the people optimized to read the latter are not the people optimized to read the former.

The May 15 release does not let you run X's algorithm. It does not let you audit X's algorithm. It does not let you know which posts boost Musk. It does not let you derive the engagement-weight table. What it does is **let you read the architecture of a modern production recsys**, written by a team that decided to throw away every hand-engineered feature in their stack and bet entirely on a transformer. That bet is the news. Whether the bet is right is a question the public release is structurally incapable of answering, because the weights (model and ranking) are exactly the parts they kept.

The transparency claim, end-to-end: **the structure is open; the editor is closed; the truth is closer to the second than the first**. That is also, on balance, the most transparent thing any major social platform has done. Both clauses are true. The honest reader holds them at the same time.

## Sources

- [xai-org/x-algorithm — main repo](https://github.com/xai-org/x-algorithm)
- [xai-org/x-algorithm — May 15 2026 commit](https://github.com/xai-org/x-algorithm/commit/e414c171ed68266341193330bc4864bf3f3534e3)
- [twitter/the-algorithm — 2023 release](https://github.com/twitter/the-algorithm)
- [xai-org/grok-1 — the upstream transformer](https://github.com/xai-org/grok-1)
- [Elon Musk — algorithm release announcement (May 15, 2026)](https://x.com/elonmusk/status/2055277918633562153)
- [Yahoo Tech — Repo Sits at One Commit 4 Months After Open-Source Promise](https://tech.yahoo.com/social-media/articles/x-algorithm-repo-sits-one-142812251.html)
- [Chris Groves — X Open-Sourced Their Algorithm. Here's What the Code Actually Reveals](https://notchrisgroves.com/x-algorithm-likes-worthless/)
- [Mandy News — I Studied the X Algorithm Code So You Don't Have To](https://mandynews.com/i-studied-the-x-algorithm-code-so-you-dont-have-to-heres-exactly-how-to-go-viral/)
- [Singh Ajit — X Algorithm Explained: How the Open Source Recommendation System Works](https://singhajit.com/system-design/x-twitter-for-you-algorithm/)
- [36Kr — Musk open-sources Grok-based X recommendation algorithm](https://www.36kr.com/p/3647512439918212)
- [Apache License 2.0 (the license the repo ships under)](https://www.apache.org/licenses/LICENSE-2.0)

---

Canonical: https://www.thedeepfeed.ai/posts/2026-05-16-x-algorithm-phoenix-release/
Site: https://www.thedeepfeed.ai
Full corpus: https://www.thedeepfeed.ai/llms-full.txt