# Assemble a Home Mixer request

> HomeMixerServer CLI flags, QueryBuilder.viewer_id validation, ScoredPostsQuery construction, and For You versus ScoredPosts entry points.

- Repository: xai-org/x-algorithm
- GitHub: https://github.com/xai-org/x-algorithm
- Human docs: https://grok-wiki.com/public/docs/xai-org-x-algorithm-23c09c39074c
- Complete Markdown: https://grok-wiki.com/public/docs/xai-org-x-algorithm-23c09c39074c/llms-full.txt

## Source Files

- `home-mixer/main.rs`
- `home-mixer/server.rs`
- `home-mixer/scored_posts_server.rs`
- `home-mixer/for_you_server.rs`
- `home-mixer/models/query.rs`
- `home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs`
- `home-mixer/candidate_pipeline/for_you_candidate_pipeline.rs`

---

---
title: "Assemble a Home Mixer request"
description: "HomeMixerServer CLI flags, QueryBuilder.viewer_id validation, ScoredPostsQuery construction, and For You versus ScoredPosts entry points."
---

`HomeMixerServer` is the gRPC process started from `home-mixer/main.rs`. It parses CLI flags, boots `XServiceBuilder` under the service name `home-mixer`, and registers `ScoredPostsService` plus `ForYouFeedService`. Every inbound RPC that carries a `pb::ScoredPostsQuery` goes through `QueryBuilder::build`, which rejects `viewer_id == 0` with `INVALID_ARGUMENT` `"viewer_id must be specified"` and constructs the in-memory `models::query::ScoredPostsQuery` both pipelines execute.

This checkout does not ship `home-mixer` crate files such as `params`, `clients`, or `xai_home_mixer_proto`. The assembly path below is in source; the binary is not locally runnable. Use the Phoenix Python pipeline for on-box inference.

<Warning>
`HomeMixerServer` is a production gRPC surface. It requires mTLS (`TlsMode::server_mtls_from_env`), StringCenter, feature-switch, and decider bundles referenced by unpublished `params` constants. A missing or zero `viewer_id` never reaches a pipeline.
</Warning>

## Start the server

`clap` defines the process flags. `shard_coordinate < 0` (the default `-1`) leaves `HomeMixerConfig.shard_coordinate` as `None`; a non-negative value becomes `ShardCoordinate { ordinal, total_size }`.

<ParamField body="grpc-port" type="u16" default="50051">
gRPC listen port.
</ParamField>

<ParamField body="metrics-port" type="u16" default="9090">
Metrics listen port.
</ParamField>

<ParamField body="shard-coordinate" type="i16" default="-1">
Wily shard ordinal. Values `>= 0` enable `ShardCoordinate`; `-1` disables sharding.
</ParamField>

<ParamField body="shard-total-size" type="u16" default="500">
Shard ring size. Used only when `shard-coordinate >= 0`.
</ParamField>

<ParamField body="datacenter" type="string" default="atla">
Datacenter string. Passed to `XServiceBuilder`, `QueryBuilder`, Gizmoduck, and both pipeline `prod` constructors. Also injected into feature-switch recipients as custom string `datacenter`.
</ParamField>

<ParamField body="otel-endpoint" type="string" default="">
OpenTelemetry collector endpoint. Empty string leaves OTel unset.
</ParamField>

<RequestExample>
```bash
# Conceptual production start. Unpublished params/clients/proto crates
# are required; this checkout cannot link the binary.
home-mixer \
  --grpc-port 50051 \
  --metrics-port 9090 \
  --shard-coordinate -1 \
  --shard-total-size 500 \
  --datacenter atla
```
</RequestExample>

Boot sequence after `Args::parse()`:

1. `xai_stringcenter::init_from_file(params::STRINGCENTER_BUNDLE_PATH)`
2. `XServiceBuilder::new("home-mixer")` with gRPC port, metrics port, datacenter, OTel
3. Feature switches from `params::FS_PATH` (second arg `true`)
4. Decider from `params::decider_path()`
5. mTLS from the environment
6. Max connection age `300` seconds
7. gRPC reflection from `pb::FILE_DESCRIPTOR_SET`
8. Dark-traffic layers: `dark_traffic_setup::resolve_layer()` then `RejectDarkTrafficLayer::from_env()`
9. HTTP profiling routes from `xai_profiling::profiling_router()`
10. `run::<HomeMixerServer>(HomeMixerConfig { shard_coordinate })`

Both registered services accept and send Gzip and Zstd, and cap decode/encode size at `params::MAX_GRPC_MESSAGE_SIZE`.

## Service wiring

`HomeMixerServer::build` constructs one shared `QueryBuilder`, one `PhoenixCandidatePipeline`, then wraps that pipeline in `ScoredPostsServer`. `ForYouCandidatePipeline` takes `Arc<ScoredPostsServer>` and the datacenter so For You organic posts reuse the same scored-posts path.

```text
XServiceBuilder("home-mixer")
        │
        ▼
 HomeMixerServer
  ├─ QueryBuilder ── feature_switches, decider, datacenter, GizmoduckClient
  ├─ ScoredPostsServer ── PhoenixCandidatePipeline  → ScoredPost[]
  └─ ForYouFeedServer  ── ForYouCandidatePipeline
                              ├─ ScoredPostsSource  → run_pipeline()
                              ├─ AdsSource
                              ├─ WhoToFollowSource
                              ├─ PromptsSource
                              └─ PushToHomeSource
```

Gizmoduck is `ProdGizmoduckClient::new(shard_coordinate, datacenter, Some("home-mixer.prod"))`. `QueryBuilder::mock()` exists for tests: empty feature switches, empty `DeciderStore`, datacenter `"mock"`, `MockGizmoduckClient`.

## Request path

```mermaid
sequenceDiagram
    participant Client
    participant QB as QueryBuilder
    participant Giz as GizmoduckClient
    participant SP as ScoredPostsServer
    participant FY as ForYouFeedServer
    participant Phoenix as PhoenixCandidatePipeline
    participant FYPipes as ForYouCandidatePipeline

    Client->>QB: pb.ScoredPostsQuery
    QB->>QB: reject viewer_id == 0
    alt viewer_id in TRACE_USER_IDS
        QB->>QB: B3RequestInfo.force_sample()
    end
    QB->>Giz: get_viewer_data (200 ms)
    Giz-->>QB: ViewerData or default
    QB->>QB: feature switches + ScoredPostsQuery::new

    alt GetScoredPosts / GetDebugScoredPosts
        QB->>SP: run_pipeline(query)
        alt user_id in TEST_USER_IDS
            SP-->>Client: empty ScoredPostsResponse
        else
            SP->>Phoenix: execute(query)
            Phoenix-->>Client: ScoredPost[]
        end
    else GetForYouFeed / GetForYouFeedUrt
        Note over FY: URT patches cursor, is_polling, request_context after build
        QB->>FY: get_for_you_feed(query)
        alt user_id in TEST_USER_IDS
            FY-->>Client: empty FeedItem[]
        else
            FY->>FYPipes: execute(query)
            FYPipes->>SP: ScoredPostsSource.run_pipeline
            FYPipes-->>Client: FeedItem[] or URT bytes
        end
    end
```

B3 trace headers are extracted from request metadata and injected on the response. `GetDebugScoredPosts` always calls `force_sample()`. Root spans use endpoint names `scored_posts`, `debug_scored_posts`, `for_you_feed`, or `for_you_feed_urt`.

## Validate viewer_id

`QueryBuilder::build` is the only constructor used by the four RPCs.

<Steps>
<Step title="Reject an unspecified viewer">
If `proto_query.viewer_id == 0`, return `Status::invalid_argument("viewer_id must be specified")`. Proto3 numeric defaults are `0`, so omitting the field fails the same way.
</Step>
<Step title="Force-sample traced users">
If `params::TRACE_USER_IDS` contains `viewer_id`, call `b3_info.force_sample()`. The ID list lives in unpublished `params`.
</Step>
<Step title="Load viewer data">
`fetch_viewer_data` calls `gizmoduck_client.get_viewer_data(viewer_id)` with a `200` ms timeout (`VIEWER_ROLES_TIMEOUT_MS`). Timeout or error becomes `ViewerData::default()`.
</Step>
<Step title="Force in-network when For You is disabled">
`in_network_only = proto_query.in_network_only || viewer_data.allow_for_you_recommendations == Some(false)`. `None` (the default after a Gizmoduck miss) does **not** flip the flag.
</Step>
<Step title="Evaluate feature switches">
`RecipientBuilder` gets `user_id`, `country`, `language`, `client_app_id`, custom `datacenter`, custom `account_age_days` from `days_since_creation(viewer_id)`, custom `has_phone_number`, and `user_roles` when nonempty. Debug RPCs then apply `feature_switch_overrides`.
</Step>
<Step title="Construct ScoredPostsQuery">
`ScoredPostsQuery::new(...)` copies proto fields, Gizmoduck fields, and generated IDs. `request_id = resolve_request_id(proto_query.request_id)`; `prediction_id = generate_request_id()`; `push_to_home_post_id = non_zero(proto_query.push_to_home_post_id)` (`0` becomes `None`); `is_shadow_traffic = is_sampled(request_id, 0.5)`.
</Step>
</Steps>

Viewer fields consumed from Gizmoduck (inferred from `QueryBuilder` usage; `ViewerData` itself is unpublished): `roles`, `has_phone_number`, `allow_for_you_recommendations`, `muted_keywords`, `follower_count`, `subscription_level`, `age_in_years`.

<Note>
`TEST_USER_IDS` is checked **after** `QueryBuilder`. A test user still needs a nonzero `viewer_id`. Both `ScoredPostsServer::run_pipeline` and `ForYouFeedServer::get_for_you_feed` return empty results without executing a pipeline.
</Note>

## Proto fields QueryBuilder reads

`xai_home_mixer_proto::ScoredPostsQuery` is unpublished. These are the fields `QueryBuilder` and the For You URT handler actually read:

| Proto field | Destination | Notes |
|---|---|---|
| `viewer_id` | `query.user_id` | Required; `0` is invalid |
| `client_app_id` | `query.client_app_id` | Also feature-switch recipient |
| `country_code` | `query.country_code` | Feature-switch recipient |
| `language_code` | `query.language_code` | Feature-switch recipient |
| `seen_ids` | `query.seen_ids` | |
| `served_ids` | `query.served_ids` | |
| `in_network_only` | `query.in_network_only` | OR'd with Gizmoduck For You opt-out |
| `is_bottom_request` | `query.is_bottom_request` | URT `BOTTOM` cursor can overwrite |
| `topic_ids` | `query.topic_ids` | `is_topic_request()` / `is_bulk_topic_request()` (`len > 6`) |
| `excluded_topic_ids` | `query.excluded_topic_ids` | |
| `exclude_videos` | `query.exclude_videos` | |
| `request_id` | `query.request_id` | Via `resolve_request_id` |
| `is_preview` | `query.is_preview` | Disables `AdsSource` when `true` |
| `push_to_home_post_id` | `query.push_to_home_post_id` | `0` dropped; enables `PushToHomeSource` |
| `device_status.*` | device / IP fields | Missing message → default empty values |
| `cursor` | `query.cursor` | Applied only on `GetForYouFeedUrt` after `build` |
| `request_context` | `query.request_context` | URT only, after `build` |
| `is_polling` | `query.is_polling` | URT only, after `build` |

`device_status` fields mapped: `ip_address`, `user_agent`, `time_zone` (`timezone_string_to_enum`), `device_network_type` (`network_type_string_to_enum`), `client_version`, `device_id`, `mobile_device_id`, `mobile_device_ad_id`.

`ForYouFeedQuery` must wrap that proto: `feed_query.query` missing returns `"query must be specified"`. `DebugScoredPostsQuery` uses `query.unwrap_or_default()` — a missing inner query becomes `viewer_id == 0` and then fails validation. Debug also passes `feature_switch_overrides: HashMap<String, String>`.

## Constructor defaults

`ScoredPostsQuery::new` copies the arguments above, then fills the rest:

| Field | Constructor default |
|---|---|
| `is_top_request` | `false` (URT `TOP` cursor later sets `true`) |
| `bloom_filter_entries` | `[]` |
| `scoring_sequence` / `retrieval_sequence` | `None` (query hydrators fill later) |
| `columnar_scoring_sequence` / `columnar_retrieval_sequence` | `None` |
| `user_features` | `muted_keywords` + `follower_count`; other lists empty |
| `request_time_ms` | `current_time_ms()` |
| `cached_posts` / `has_cached_posts` | `[]` / `false` |
| `new_user_topic_ids` | `[]` |
| `in_network_replies` | default |
| `viewer_minhash` | `None` |
| `user_demographics` / `ip_location` | `None` |
| `user_inferred_gender` / score | `None` |
| `followed_grok_topics` / `inferred_grok_topics` / `followed_starter_packs` | `None` |
| `is_polling` | `false` |
| `cursor` | `None` |
| `request_context` | `""` |
| `served_history` | `[]` |
| `who_to_follow_eligible` | `false` (`ServedHistoryQueryHydrator` may set it on For You) |
| `non_polling_timestamps` | `None` |
| `impressed_post_ids` | `[]` |

`GetTwitterContextViewer` exposes `user_id`, `client_application_id`, `request_country_code`, and `request_language_code`. `PipelineQuery` exposes `params` and `decider`.

<Info>
`home-mixer/candidate_pipeline/query.rs` defines a narrower leftover `ScoredPostsQuery` (`user_id: i64`, string `request_id`). Live RPCs and both candidate pipelines use `models::query::ScoredPostsQuery`.
</Info>

## Choose an entry point

| RPC | Request | Pipeline | Response | Extra vs QueryBuilder |
|---|---|---|---|---|
| `ScoredPostsService.GetScoredPosts` | `pb::ScoredPostsQuery` | `PhoenixCandidatePipeline` | `ScoredPostsResponse { scored_posts }` | None |
| `ScoredPostsService.GetDebugScoredPosts` | `pb::DebugScoredPostsQuery` | same | `DebugScoredPostsResponse { scored_posts, debug_json }` | Always sampled; FS overrides |
| `ForYouFeedService.GetForYouFeed` | `pb::ForYouFeedQuery` | `ForYouCandidatePipeline` | `ForYouFeedResponse { items }` | Requires nested `query` |
| `ForYouFeedService.GetForYouFeedUrt` | `pb::ForYouFeedQuery` | same | `ForYouFeedUrtResponse { urt }` | Cursor / polling / `request_context`; Thrift URT |

**Scored posts** is the organic ranker. `PhoenixCandidatePipeline` sources Thunder, Tweet Mixer, Phoenix, Phoenix topics, Phoenix MoE, and cached posts; selects with `TopKScoreSelector`; result size `params::RESULT_SIZE`. `in_network_only` disables Phoenix / Tweet Mixer / Phoenix MoE / Phoenix topics. Topic requests flip product-surface logs to `topics`; excluded topics log as `for_you_with_snoozed_topics`; otherwise `for_you` or `ranked_following`.

**For You** is the blended feed. `ForYouCandidatePipeline` hydrates served history and past request timestamps, sources organic posts through `ScoredPostsSource` (`ScoredPostsServer::run_pipeline` → `FeedItem::Post`), then ads, who-to-follow, prompts, and push-to-home. Selector is `BlenderSelector`. Result size `params::FOR_YOU_MAX_RESULT_SIZE`. `is_preview` skips ads. `push_to_home_post_id` enables `PushToHomeSource`.

URT-only post-processing after `build`:

- Copy `request_context` and `is_polling` from the proto.
- If `cursor` is nonempty, `cursor_utils::decode_ordered_cursor` sets `query.cursor`. `CursorType::BOTTOM` sets `is_bottom_request`; `CursorType::TOP` sets `is_top_request`. Decode errors are logged and ignored.
- `urt::make_urt_timeline` then `xai_urt_thrift::serialize_binary`. Serialize failure is `Status::internal("failed to serialize URT: …")`.

<AccordionGroup>
<Accordion title="Flags that change sourcing after assembly">
- `in_network_only` — Thunder still runs; Phoenix / Tweet Mixer / topics / MoE disable.
- `topic_ids` — `is_topic_request()`; `len > 6` is `is_bulk_topic_request()` (Phoenix stays enabled for bulk).
- `is_preview` — `AdsSource` off.
- `push_to_home_post_id` — `PushToHomeSource` on.
- `who_to_follow_eligible` — starts `false`; For You `ServedHistoryQueryHydrator` may flip it before `WhoToFollowSource`.
- `has_cached_posts` — starts `false`; cache hydrators later can skip live sources.
</Accordion>
</AccordionGroup>

## Assemble a client request

<Steps>
<Step title="Pick the surface">
Use `GetScoredPosts` for ranked organic `ScoredPost`s. Use `GetForYouFeed` / `GetForYouFeedUrt` for a blended `FeedItem` timeline. Use `GetDebugScoredPosts` only when you need `debug_json` and FS overrides.
</Step>
<Step title="Set a nonzero viewer_id">
Required on `pb::ScoredPostsQuery`. For You and debug wrap that message; they do not replace it.
</Step>
<Step title="Fill identity and exclusion lists">
Set `client_app_id`, `country_code`, `language_code` (feature switches). Pass `seen_ids` / `served_ids` to suppress already-shown posts. Set `in_network_only` only when the client wants following-only (Gizmoduck can still force it).
</Step>
<Step title="Add optional retrieval controls">
`topic_ids`, `excluded_topic_ids`, `exclude_videos`, `is_preview`, `push_to_home_post_id`, `device_status`, `request_id`. Leave `push_to_home_post_id` at `0` unless a focal post should be injected.
</Step>
<Step title="For You: wrap and optionally set URT fields">
Put the proto in `ForYouFeedQuery.query`. For URT, also set `cursor`, `request_context`, and `is_polling` on the inner `ScoredPostsQuery` — `QueryBuilder` ignores those three; the URT handler copies them after `build`.
</Step>
<Step title="Verify the call">
A zero or omitted `viewer_id` must return `INVALID_ARGUMENT` / `viewer_id must be specified`. A successful scored-posts call returns `scored_posts`; For You returns `items` or URT bytes. IDs in `TEST_USER_IDS` return empty payloads with no pipeline work.
</Step>
</Steps>

<RequestExample>
```text
# Minimal scored-posts query (field names as used in QueryBuilder)
viewer_id:            <nonzero u64>
client_app_id:        <i32>
country_code:         "US"
language_code:        "en"
seen_ids:             []
served_ids:           []
in_network_only:      false
is_bottom_request:    false
topic_ids:            []
excluded_topic_ids:   []
exclude_videos:       false
is_preview:           false
push_to_home_post_id: 0
request_id:           0          # resolve_request_id decides the stored id
device_status:        { ... }    # optional; defaults if omitted

# ForYouFeedQuery
query:                <ScoredPostsQuery above>

# DebugScoredPostsQuery
query:                <ScoredPostsQuery above>
feature_switch_overrides: { "SomeFS": "true" }
```
</RequestExample>

<ResponseExample>
```text
# GetScoredPosts — ScoredPostsResponse
scored_posts: [
  tweet_id, author_id, score, in_network, served_type, ...
]

# GetForYouFeed — ForYouFeedResponse
items: [ FeedItem { position, item: Post | Ad | ... } ]

# GetForYouFeedUrt — ForYouFeedUrtResponse
urt: <Thrift TimelineResponse bytes>

# GetDebugScoredPosts also includes
debug_json: { query, retrieved_candidates, filtered_candidates,
              selected_candidates, stats }
```
</ResponseExample>

## Errors and empty feeds

| Condition | Status / result |
|---|---|
| `viewer_id == 0` or omitted | `INVALID_ARGUMENT` `"viewer_id must be specified"` |
| For You missing `ForYouFeedQuery.query` | `INVALID_ARGUMENT` `"query must be specified"` |
| Debug missing inner `query` | Default proto → same `viewer_id` error |
| `user_id` in `TEST_USER_IDS` | `200`-style success with empty `scored_posts` / `items` |
| Gizmoduck timeout or error | Continue with `ViewerData::default()` |
| Bad URT cursor | Warn, ignore cursor, continue |
| URT serialize failure | `INTERNAL` `"failed to serialize URT: …"` |

`PhoenixSource` later fails with `"PhoenixSource: missing retrieval_sequence"` if query hydrators never fill that field. That is post-assembly; it is not a `QueryBuilder` error.

## Related pages

<CardGroup>
<Card title="ScoredPostsQuery and gRPC" href="/scored-posts-query">
Full query fields, TEST_USER_IDS empty responses, and ScoredPost / ForYouFeed / URT mapping.
</Card>
<Card title="For You request lifecycle" href="/request-lifecycle">
CandidatePipeline.execute stages and how ForYouCandidatePipeline wraps PhoenixCandidatePipeline.
</Card>
<Card title="Runtime boundaries" href="/runtime-boundaries">
What this checkout can execute locally versus production Home Mixer.
</Card>
<Card title="In-network and out-of-network" href="/in-network-out-of-network">
How in_network_only changes Thunder versus Phoenix sourcing.
</Card>
<Card title="Blend ads into the feed" href="/blend-ads">
AdsSource, blenders, and For You insertion after scored posts.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
viewer_id must be specified, TEST_USER_IDS empty feeds, and unpublished crates.
</Card>
</CardGroup>
