The README says the tool will be shaped by feedback, so here is a report on the download path.
What an eligible account gets today
x.com/i/under_the_hood renders a title, two eligibility checkmarks and a Download button. Clicking it hands over the reportJson string from UserUnderTheHoodReport verbatim, as a JSON blob. There is no rendered view of the report anywhere in the product.
Shape of the payload (values synthetic):
{
"notes": "You can learn more about instances when a post's reach may be limited by reading our open-source code at ...",
"period": { "startDate": "2026-08-01", "endDate": "2026-08-31", "timezone": "UTC" },
"generatedAt": "2026-09-09T23:59:59Z",
"postCount": "412",
"postLabels": [
{
"label": "SPAM_HIGH_RECALL",
"about": "Post detected by automated systems as one that may contain spam.",
"effect": "Post hidden from recommendations to non-followers.",
"posts": "1",
"totalPostsInMonth": "412",
"percentageOfPosts": "0.24%"
}
],
"accountLabels": [],
"totalAccountLabels": 0
}
Everything needed for a human-readable view is already computed. postLabelRow in under-the-hood/strato/columns/underTheHoodReport.User.strato:180-212 already attaches plain-English about and effect text per label, and percentageOfPosts is already a formatted display string rather than a number. That prose is then serialized into a file and handed over.
So the gap is presentation, not data. The audience for this tool is every eligible account, not the subset of people who open .json files.
Ask
Render the report on the page in X's own UI: a period header, a plain state line ("No labels were applied to your account in August 2026" vs "2 labels"), then one card per label showing the label's human name, its about, its effect, and the count with its percentage. Keep the JSON download as a secondary action for people who want the raw file.
An account that is clean should be able to learn that in one glance without downloading anything.
And the thing most people actually open this tool to find out: which posts were flagged. See the section below - that one is a bigger ask than the rest, because the post identity is discarded upstream in the daily job, not at the serving layer.
Four things the served report drops that a rendered view would want
All four are computed upstream and discarded at the serving layer, so they are worth settling before a UI is designed:
-
Per-day timeline. UthPostLabelAggregate.days is a list<UthDayCarriedRemoved> carrying dayOfMonth (under-the-hood/thrift/uth_serving.thrift:10-14, 53-57), but sumCarried collapses it to a single integer (underTheHoodReport.User.strato:155-158). "1 post, sometime in August" answers much less than a sparkline showing which day.
-
removed is never read. UthDayCarriedRemoved carries both carried and removed; sumCarried sums only carried. A label that was applied and later lifted is therefore indistinguishable from one that was never applied, which is exactly the case a worried author most wants resolved.
-
countryCodes is not a field. It reaches postLabelRow (:182) and accountLabelRow (:218) only to be interpolated into the takedown label's name and its about/effect sentences (:205-207, :241-243). For the legal-withholding data added in the September update, "which country" deserves a structured field, not a sentence a UI has to parse back out.
-
No peer context. UthReferenceCohortStats computes mean and p10/p25/p50/p75/p90/p99 per label per UthFollowerClass (uth_serving.thrift:25-34, 99-108), published by UthReferenceMonthMhPublisherJob.scala. But referenceMonth does not appear anywhere in underTheHoodReport.User.strato, so none of it reaches the user. Without it a reader has no way to judge whether their rate is normal. One line of the form "typical for accounts your size: N%" turns a number into an answer.
The biggest gap: the report never says which posts were flagged
"1 of your 412 posts was labelled SPAM_HIGH_RECALL" tells an author that something happened and gives them no way to act on it. They cannot see the post, cannot judge whether the call was right, cannot appeal it, and cannot learn what to avoid. A count without an identity is a notification, not transparency.
The post ID exists in the pipeline and is deliberately dropped at the aggregation step. In under-the-hood/scalding/UthDailyPostsJob.scala:
:365 builds the reduce key as ((logicalId, userId, day, label, createdMs), ...) - logicalId is the post.
:383 destructures that same key as case ((_, userId, day, label, createdMs), ...) - the leading _ is the post ID being discarded.
:388 emits ((userId, day, label), (1L, removed)), and the job's return type at :343 is TypedPipe[(Long, Int, String, Long, Long)] - user, day, label, carried, removed. No room for an ID.
So this is not a case of data that was never collected. It is one underscore, plus the aggregate types downstream (UthPostLabelAggregate in under-the-hood/thrift/uth_serving.thrift:53-57 holds a label, day counts and country codes, with no post list).
Understood that per-post disclosure raises questions the aggregate does not: payload size for high-volume accounts, whether surfacing a spam classification teaches evasion, and what to show once a post is deleted. Those are worth answering out loud rather than settling by omission. Some options short of a full list:
- Cap it: the N most recent labelled post IDs per label, rather than all of them.
- Put it in the post's own surface instead of the report - an indicator on the author's own view of a labelled post, with the report linking to it.
- Ship it for the label classes where evasion is not the concern first, for example legal withholding, where the author arguably has the strongest claim to know which post and which country.
Whatever the answer, it deserves to be stated in the README rather than left as a silent gap, because "see the labels on your posts" reads as a promise that the current report does not keep.
One shape issue worth fixing regardless of the UI
totalPostLabels and totalAccountLabels are emitted only when the corresponding list is empty (underTheHoodReport.User.strato:296-332), so the object has four different key sets depending on which lists are populated. Every consumer has to branch on key presence. Emitting both counts unconditionally would be simpler for a UI and for anyone parsing the file.
PR
I have not opened one because the page's frontend is not in this repository: under-the-hood/ ships the daily jobs and the serving layer only. Sent: #214, covering the serving-layer items that are a patch rather than a product decision (1, 2, 3 and the count shape). Item 4 and the per-post question above are deliberately left here, because both are calls for you to make rather than changes to apply.
Verified against main on 2026-09-20: the live page, the UserUnderTheHoodReport GraphQL response, and the checked-in under-the-hood/ sources. Related but distinct: #39 (label-source provenance).
The README says the tool will be shaped by feedback, so here is a report on the download path.
What an eligible account gets today
x.com/i/under_the_hoodrenders a title, two eligibility checkmarks and a Download button. Clicking it hands over thereportJsonstring fromUserUnderTheHoodReportverbatim, as a JSON blob. There is no rendered view of the report anywhere in the product.Shape of the payload (values synthetic):
{ "notes": "You can learn more about instances when a post's reach may be limited by reading our open-source code at ...", "period": { "startDate": "2026-08-01", "endDate": "2026-08-31", "timezone": "UTC" }, "generatedAt": "2026-09-09T23:59:59Z", "postCount": "412", "postLabels": [ { "label": "SPAM_HIGH_RECALL", "about": "Post detected by automated systems as one that may contain spam.", "effect": "Post hidden from recommendations to non-followers.", "posts": "1", "totalPostsInMonth": "412", "percentageOfPosts": "0.24%" } ], "accountLabels": [], "totalAccountLabels": 0 }Everything needed for a human-readable view is already computed.
postLabelRowinunder-the-hood/strato/columns/underTheHoodReport.User.strato:180-212already attaches plain-Englishaboutandeffecttext per label, andpercentageOfPostsis already a formatted display string rather than a number. That prose is then serialized into a file and handed over.So the gap is presentation, not data. The audience for this tool is every eligible account, not the subset of people who open
.jsonfiles.Ask
Render the report on the page in X's own UI: a period header, a plain state line ("No labels were applied to your account in August 2026" vs "2 labels"), then one card per label showing the label's human name, its
about, itseffect, and the count with its percentage. Keep the JSON download as a secondary action for people who want the raw file.An account that is clean should be able to learn that in one glance without downloading anything.
And the thing most people actually open this tool to find out: which posts were flagged. See the section below - that one is a bigger ask than the rest, because the post identity is discarded upstream in the daily job, not at the serving layer.
Four things the served report drops that a rendered view would want
All four are computed upstream and discarded at the serving layer, so they are worth settling before a UI is designed:
Per-day timeline.
UthPostLabelAggregate.daysis alist<UthDayCarriedRemoved>carryingdayOfMonth(under-the-hood/thrift/uth_serving.thrift:10-14, 53-57), butsumCarriedcollapses it to a single integer (underTheHoodReport.User.strato:155-158). "1 post, sometime in August" answers much less than a sparkline showing which day.removedis never read.UthDayCarriedRemovedcarries bothcarriedandremoved;sumCarriedsums onlycarried. A label that was applied and later lifted is therefore indistinguishable from one that was never applied, which is exactly the case a worried author most wants resolved.countryCodesis not a field. It reachespostLabelRow(:182) andaccountLabelRow(:218) only to be interpolated into the takedown label's name and its about/effect sentences (:205-207,:241-243). For the legal-withholding data added in the September update, "which country" deserves a structured field, not a sentence a UI has to parse back out.No peer context.
UthReferenceCohortStatscomputesmeanandp10/p25/p50/p75/p90/p99per label perUthFollowerClass(uth_serving.thrift:25-34, 99-108), published byUthReferenceMonthMhPublisherJob.scala. ButreferenceMonthdoes not appear anywhere inunderTheHoodReport.User.strato, so none of it reaches the user. Without it a reader has no way to judge whether their rate is normal. One line of the form "typical for accounts your size: N%" turns a number into an answer.The biggest gap: the report never says which posts were flagged
"1 of your 412 posts was labelled SPAM_HIGH_RECALL" tells an author that something happened and gives them no way to act on it. They cannot see the post, cannot judge whether the call was right, cannot appeal it, and cannot learn what to avoid. A count without an identity is a notification, not transparency.
The post ID exists in the pipeline and is deliberately dropped at the aggregation step. In
under-the-hood/scalding/UthDailyPostsJob.scala::365builds the reduce key as((logicalId, userId, day, label, createdMs), ...)-logicalIdis the post.:383destructures that same key ascase ((_, userId, day, label, createdMs), ...)- the leading_is the post ID being discarded.:388emits((userId, day, label), (1L, removed)), and the job's return type at:343isTypedPipe[(Long, Int, String, Long, Long)]- user, day, label, carried, removed. No room for an ID.So this is not a case of data that was never collected. It is one underscore, plus the aggregate types downstream (
UthPostLabelAggregateinunder-the-hood/thrift/uth_serving.thrift:53-57holds a label, day counts and country codes, with no post list).Understood that per-post disclosure raises questions the aggregate does not: payload size for high-volume accounts, whether surfacing a spam classification teaches evasion, and what to show once a post is deleted. Those are worth answering out loud rather than settling by omission. Some options short of a full list:
Whatever the answer, it deserves to be stated in the README rather than left as a silent gap, because "see the labels on your posts" reads as a promise that the current report does not keep.
One shape issue worth fixing regardless of the UI
totalPostLabelsandtotalAccountLabelsare emitted only when the corresponding list is empty (underTheHoodReport.User.strato:296-332), so the object has four different key sets depending on which lists are populated. Every consumer has to branch on key presence. Emitting both counts unconditionally would be simpler for a UI and for anyone parsing the file.PR
I have not opened one because the page's frontend is not in this repository:
under-the-hood/ships the daily jobs and the serving layer only. Sent: #214, covering the serving-layer items that are a patch rather than a product decision (1, 2, 3 and the count shape). Item 4 and the per-post question above are deliberately left here, because both are calls for you to make rather than changes to apply.Verified against
mainon 2026-09-20: the live page, theUserUnderTheHoodReportGraphQL response, and the checked-inunder-the-hood/sources. Related but distinct: #39 (label-source provenance).