Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ This repository contains the core code that determines which posts a viewer sees

## Table of Contents

- [Latest update](#latest-update--august-13th-2026)
- [Latest Updates](#latest-updates)
- [August 14th, 2026](#august-14th-2026)
- [August 13th, 2026](#august-13th-2026)
- [Overview](#overview)
- [System Architecture](#system-architecture)
- [Request Path](#request-path)
Expand All @@ -23,7 +25,16 @@ This repository contains the core code that determines which posts a viewer sees



## Latest update — August 13th, 2026
## Latest Updates

### August 14th, 2026

Notable updates:

- **How weights work.** There's a common misconception about how weights related to actions (e.g. Like, Share, Block, Report, etc) work in ranking. The weights scale the predicted probabilities of such actions (or predicted continuous values, e.g. dwell time) — they do *not* scale the raw engagement counts, so e.g. it'd be incorrect to see that a report has 468 times higher weight than a like and conclude that e.g. "1 report cancels out 468 likes". The weights are a multiple on your own predicted probability of Liking, Reporting, etc, which is substantially driven by your own behavior. We've [added comments](home-mixer/params/param.rs) [to the code](home-mixer/scorers/ranking_scorer.rs) so that LLMs or people reading it are more likely to understand it correctly.
- **Brazil 2026 Elections.** As [announced by X](https://x.com/XBR/status/2088341967864320507?s=20), in accordance with Brazilian electoral law, For You now runs `Brazil2026ElectionFilter`, which removes posts from accounts reported to Brazil's Electoral Court for the 2026 election, unless the viewer explicitly follows the account. A benefit of open-source is that you can see that changes like this exist, and exactly how they work — take a [look at the code](home-mixer/filters/brazil_2026_election_filter.rs).

### August 13th, 2026

This release:

Expand Down Expand Up @@ -329,6 +340,8 @@ Final Score = Σ (weight_i × P(action_i))

Positive actions carry positive weights, negative actions negative ones. The weights are in [`home-mixer/params/param.rs`](home-mixer/params/param.rs); the arithmetic is in [`home-mixer/scorers/ranking_scorer.rs`](home-mixer/scorers/ranking_scorer.rs).

There is a common misconception to be aware of about the weights: they scale the predicted probabilities (or predicted continuous values, e.g. dwell time) — they do *not* scale the raw engagement counts, so e.g. it'd be incorrect to see that a report has 468 times higher weight than a like and conclude that e.g. "1 report cancels out 468 likes". The weights are a multiple on your own predicted probability of Liking, Reporting, etc, which is substantially driven by your own behavior.

Three adjustments follow:

- **Author Diversity**: each post after an author's first is multiplied by a decaying factor, down to a floor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ rules:
when: "!user.present"
then: { kind: skip, reason: not_present }
- id: cred_skip
when: cred.is_high || cred.follower_count >= 1234 || cred.score >= 7.5
when: cred.is_high || cred.follower_count >= 12.34 || cred.score >= 7.5
then: { kind: skip, reason: cred_skip }
- id: composite
when: '"composite_label" in score.labels'
Expand Down
14 changes: 11 additions & 3 deletions bdsm/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Behavioral Inauthentic-Account Detection
# Behavioral Detection Sequence Model (BDSM)

A sequence-of-actions transformer that detects inauthentic (bot / spam /
coordinated) accounts from their behavioral event streams, together with the
Expand Down Expand Up @@ -110,5 +110,13 @@ The scorer publishes an 8-wide row in `heads.HEAD_NAMES` order.
detector's evasion boundary. The policy *structure*, head names, and gate
logic are real and unredacted; only the tuned numbers are withheld. Supply
your own via `--policy-file` / `BDSM_SINK_POLICY`.
- Per-head **appeal-note templates** that described the tripping features in
prose are withheld. The `enforcement_note` proto field remains and is empty.
- Per-head **appeal-note templates**: the production sink interpolates a
short prose paragraph from the dominant bot head and selected histogram
counts (`build_enforcement_note` in `runtime/score_results_sink_focal.py`).
The public package keeps the **gates** (MIN_ACTIONS, dominant-head pick)
and the `enforcement_note` proto field. The template *strings* and the
per-head `key_actions` interpolator are the sentinel `"<redacted>"` —
same idea as the `9.99` operating points. When a note would have fired
it carries that sentinel plus the model-head suffix, not the internal
appeal paragraph or the action types each head keys off. ActionName
proto enums are unchanged.
3 changes: 3 additions & 0 deletions bdsm/proto/abuse_inference.proto
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ message ScoreResult {

repeated DecodedAction decoded_actions = 18;

// Optional human-readable note. In this release templates and
// key_actions are the sentinel "<redacted>" (see README); production
// fills the real appeal paragraph. ActionName enums are unchanged.
string enforcement_note = 19;
}

Expand Down
55 changes: 52 additions & 3 deletions bdsm/runtime/score_results_sink_focal.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,57 @@ def _resolve_head_names(_model_version):
return HEAD_NAMES


def build_enforcement_note(*_args, **_kwargs):
return None
# Public-release note: per-head appeal-note templates are REDACTED
# (the prose AND the key_actions each head interpolates from the
# histogram). What ships: MIN_ACTIONS gate, dominant bot-head pick,
# and the sentinel "<redacted>" plus a [model: Head=score] suffix.
# Production fills placeholders from an internal table that is not
# part of this release. ActionName proto enums are untouched.
_REDACTED_TEMPLATE = "<redacted>"

_ENFORCEMENT_TEMPLATES = {
"FollowBot": _REDACTED_TEMPLATE,
"LikeBot": _REDACTED_TEMPLATE,
"EngagementAmplifier": _REDACTED_TEMPLATE,
"ReplySpamBot": _REDACTED_TEMPLATE,
"TweetSpamBot": _REDACTED_TEMPLATE,
"RTBot": _REDACTED_TEMPLATE,
"MultiActionBot": _REDACTED_TEMPLATE,
}


def build_enforcement_note(head_scores_list, action_hist_list):
"""Build an enforcement note from head scores + action histogram.

Returns None when no bot head is above 0.5 or the sequence is shorter
than 30 actions. In this public release the note is the sentinel
"<redacted>" plus a model-head suffix; production interpolates from a
private template table (prose and key_actions).
"""
if not head_scores_list:
return None

total = sum(h["cnt"] for h in (action_hist_list or ()))
if total < 30:
return None

bot_heads = [
(h["head_name"], h["score"])
for h in head_scores_list
if h["head_name"] != "LegitimateUser" and h["score"] > 0.5
]
if not bot_heads:
return None

bot_heads.sort(key=lambda x: x[1], reverse=True)
dominant_name, dominant_score = bot_heads[0]
note = _ENFORCEMENT_TEMPLATES.get(dominant_name, _REDACTED_TEMPLATE)
note += f" [model: {dominant_name}={dominant_score:.2f}"
if len(bot_heads) > 1:
secondary = ", ".join(f"{n}={s:.2f}" for n, s in bot_heads[1:3])
note += f", also: {secondary}"
note += "]"
return note


def _build_flag_config(head_names):
Expand Down Expand Up @@ -1116,7 +1165,7 @@ def _build_bq_row(
"head_scores": user_head_scores,
"action_histogram": user_action_hist,
"total_actions": total_actions,
"enforcement_note": build_enforcement_note(),
"enforcement_note": build_enforcement_note(user_head_scores, user_action_hist),
"labels": user_labels,
"model_version": args.model_version,
"pipeline_version": "gpu_scorer_kafka_v3",
Expand Down
6 changes: 6 additions & 0 deletions bdsm/runtime/sink_policy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
version: "2026-08-13"

# Public-release note: the per-head operating points below (the 2-D
# [tau, lambda] pairs, cusp_delta, and reply_spam_hard_suspend_tau) are
# REDACTED in the open-source export — they are replaced with an
# out-of-range 9.99 sentinel (these are probabilities in [0, 1], so a
# 9.99 threshold never fires and is plainly a stub, not a real value).
# The production values are configured internally and are not part of
# this release. Structure, head names, and the action map are real.

min_actions_for_enforcement: 30

Expand Down
41 changes: 41 additions & 0 deletions bdsm/tests/test_sink_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,44 @@ def test_invalid_policy_key_fails_loudly(tmp_path):
bad.write_text("version: x\nnot_a_policy_key: 1\n")
with pytest.raises(ValueError):
m._load_policy(str(bad))


def _hist(*pairs):
return [{"action_type": a, "cnt": n} for a, n in pairs]


def _heads(**scores):
return [
{"head_index": i, "head_name": n, "score": s} for i, (n, s) in enumerate(scores.items())
]


def test_enforcement_note_gates_and_redacted_prose():
m = _load_sink_module()
heads = _heads(FollowBot=0.91, LegitimateUser=0.1)
short = _hist(("SERVER_PROFILE_FOLLOW", 10))
long = _hist(("SERVER_PROFILE_FOLLOW", 40), ("SERVER_PROFILE_UNFOLLOW", 5))

assert m.build_enforcement_note(heads, short) is None
assert m.build_enforcement_note(heads, None) is None
assert m.build_enforcement_note(heads, []) is None
assert m.build_enforcement_note(_heads(FollowBot=0.4, LegitimateUser=0.8), long) is None

note = m.build_enforcement_note(heads, long)
assert note == f"{m._REDACTED_TEMPLATE} [model: FollowBot=0.91]"


def test_enforcement_templates_are_the_redacted_sentinel():
m = _load_sink_module()
assert set(m._ENFORCEMENT_TEMPLATES) == {
"FollowBot",
"LikeBot",
"EngagementAmplifier",
"ReplySpamBot",
"TweetSpamBot",
"RTBot",
"MultiActionBot",
}
for tmpl in m._ENFORCEMENT_TEMPLATES.values():
assert tmpl == m._REDACTED_TEMPLATE
assert not hasattr(m, "_ACTION_FRIENDLY")
4 changes: 2 additions & 2 deletions grox/flows/reply_spam/task_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@


class TaskSpamFilter(TaskFilterWithPost):
FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION = 15000
FOLLOWER_COUNT_THRESHOLD_FOR_SPAM_DETECTION = 30000

@override
@classmethod
Expand Down Expand Up @@ -182,7 +182,7 @@ async def _eligible_with_post(cls, post: Post, ctx: TaskContext) -> bool:


class TaskReplyRankingFilter(TaskFilterWithPost):
FOLLOWER_COUNT_THRESHOLD_FOR_REPLY_RANKING = 15000
FOLLOWER_COUNT_THRESHOLD_FOR_REPLY_RANKING = 30000

@override
@classmethod
Expand Down
11 changes: 11 additions & 0 deletions home-mixer/candidate_pipeline/phoenix_candidate_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use crate::clients::vm_ranker_client::{MockVMRankerClient, ProdVMRankerClient, V
use crate::filters::age_filter::AgeFilter;
use crate::filters::ancillary_vf_filter::AncillaryVFFilter;
use crate::filters::author_socialgraph_filter::AuthorSocialgraphFilter;
use crate::filters::brazil_2026_election_filter::Brazil2026ElectionFilter;
use crate::filters::core_data_hydration_filter::CoreDataHydrationFilter;
use crate::filters::dedup_conversation_filter::DedupConversationFilter;
use crate::filters::drop_duplicates_filter::DropDuplicatesFilter;
Expand Down Expand Up @@ -355,6 +356,16 @@ impl PhoenixCandidatePipeline {
Box::new(PreviouslyServedPostsFilter),
Box::new(MutedKeywordFilter::new()),
Box::new(AuthorSocialgraphFilter),
// Brazil 2026 election filter

// Application providers that use a recommendation system for users must exclude from the
// results the channels and profiles reported to the Electoral Court under the terms of
// § 1º of this article and, except in cases of paid boosting, the content posted on them.

// https://dadosabertos.tse.jus.br/dataset/candidatos-2026

// OmarAzizSenador deleted his account at the time this code was written.
Box::new(Brazil2026ElectionFilter),
Box::new(VideoFilter),
Box::new(TopicIdsFilter),
Box::new(NewUserMinEngagementFilter),
Expand Down
Loading