twitter/communitynotes: Documentation and source code powering Twitter’s Community Notes
Is it OK to help with [Wayback/Archive] twitter/communitynotes: Documentation and source code powering Twitter's Community Notes? Python source code: [Wayback/Archive] communitynotes/sourcecode at main ·…
پروژه این هفته من:
۳۰ تا ایجنت Claude، پنج روز بیوقفه موازی کار کردن و بالاخره الگوریتم X رو ریورسانجینیر کردم 🤯
کامل، رایگان و Open Source گذاشتمش اینجا 👇
حالشو ببرین:
twitter/the-algorithm
125 likes · 2 reposts · 6,187 views
Day 550 of asking @elonmusk to change the troll emoji on X from 🧌 to the troll face
- Pull request: twemoji#1430
- Article: x.com
- Petition: c.org t.co
34 likes · 16 reposts · 559 views
Xアルゴリズムの仕組み:公開ソースコードの重みを全部読んでみた(10月27日の変更まで)
:::note info
本記事には筆者が開発に関わっているサービス(GetXAPI)の紹介を含みます。重みと設定値はすべて、2026年10月28日(UTC)に xai-org/x-algorithm リポジトリを筆者が直接読んで確認したものです。
:::
「いいねは0.5点」「通報1件でいいね355件分のマイナス」「プロフィールクリックは12点」。Xのアルゴリズムについて、日本語の解説記事やAIの回答にはこうした数字が並んでいます。ところが、いま公開されているコードを読むと、いくつかは2023年の旧Twitterのコードの数字で、いくつかは10月13日より前の古い値です。この記事では、Xのアルゴリズムの仕組みを公開ソースコードから行番号つきで整理し、重みがいつ変わったかも全コミットから洗い出しました。
2026年10月28日更新。重みは commit 2f25214(10月27日 01:28 UTC)の home-mixer/params/param.rs から引用し、翌日の commit 5ed7963 でも重みに変更がないことを確認しています。
Xのアルゴリズムはどういう仕組みですか?
リクエストのたびに候補の投稿を集め、各投稿に対してあなたが取りそうな行動の確率を予測し、重みをかけて足し合わせた点数で並べます。 候補はフォロー中のアカウント(Thunder)と、フォロー外のアカウント(Phoenixの検索、SimClusters、人気投稿)から集まります。点数は Σ(重み × その行動の予測確率) で、READMEにもこの式が書かれています。投稿から48時間を過ぎた投稿はスコア計算の前に除外されます。
いいねの重みは0.5ですか?
10月13日までは0.5でした。 commit e1a0375 で0.6に、翌日の 17c96bf で 0.66 に上がりました。0.5という数字は、いまも vm-ranker/params.rs の3行目にフォールバック(既定値)として残っています。本番の設定から同期されているのは home-mixer 側のファイル(1行目に「mirrored from config feature-switch defaults」とある)なので、引用するならこちらです。
通報1件はいいね何件分ですか?
そういう換算はできません。 重みは「回数」ではなく「予測確率」にかかります。param.rs の319〜346行目のコメントでは、X自身が「通報1件でいいね468件が帳消し」という読み方を誤りと書き、通報はいいねより1,000倍以上起きにくいと説明しています。
「Heavy Ranker」「プロフィールクリック12点」は今も正しいですか?
いいえ、2023年の旧Twitterのリポジトリ(twitter/the-algorithm)の話です。 現在のおすすめはPhoenixモデルで順位付けされ、プロフィールクリックの重みは 0、作者のリプライ返し(+75)という項目も今の重み一覧にはありません。「候補を約1,500件抽出」という数字も、現在のコードでは確認できませんでした。
Xアルゴリズムの重み一覧(commit 2f25214)
home-mixer/params/param.rs の348行目(FavoriteWeight)から460行目(ReportWeight)までの値です。
| 予測する行動 | 重み |
|---|---|
| リンクをコピーして共有 | 20.0 |
| リプライ | 5.0(相互フォロー同士なら +15.0) |
| 引用 | 5.0 |
| DMで共有 | 5.0 |
| 作者をフォロー | 4.0 |
| 共有 | 2.0 |
| リポスト | 1.0 |
| いいね | 0.66 |
| クリック後の滞在時間(連続値) | 0.4 |
| クリック | 0.3 |
| リンクを開く | 0.2 |
| 画像の拡大、滞在 | 各0.05 |
| 動画を開く | 0.02 |
| 滞在時間(連続値) | 0.006 |
| プロフィールクリック、動画の視聴品質 | 0 |
| 滞在しない | −0.02 |
| ブロック | −31.2 |
| 興味がない | −52.272 |
| ミュート | −58.8 |
| 通報 | −234 |
ブックマークには重みがありません。ただし viewer_history.rs の30行目で ClientTweetBookmark は「ポジティブな行動」に入っていて、モデルが読む行動履歴には使われます。
重みはいつ変わった?(8月13日以降の全変更)
param.rs に触れた48コミットをすべて再生し、重みが変わったものだけを抜き出しました。
| 日付 | コミット | 変更 |
|---|---|---|
| 8月25日 | 0d3cdd8 | 滞在 0→0.05、動画を開く 0.05→0.07、動画の視聴品質 0.05→0 |
| 9月29日 | a707cc2 | クリック 0.4→0.3、クリック後の滞在時間 0→0.4、興味がない −43.2→−47.52 |
| 10月7日 | 78460ca | 未探索の投稿 0.02→0.015 |
| 10月13日 | e1a0375 | いいね 0.5→0.6、動画を開く 0.07→0.02、滞在時間 0.004→0.006 |
| 10月14日 | 17c96bf | いいね 0.6→0.66、興味がない −47.52→−52.272 |
相互フォローのリプライ加点は同期ファイルができる前の変更で、リポジトリ内の docs/BIDIRECTIONAL_BOOST_CHANGE.md によると7月13日に20で導入され、7月24日に15へ下げられています。
10月27日の変更で何が変わった?
重みは変わっていません。 変わったのは VMRankerSendDebiasInputs が false から true になったこと(param.rs 508行目)で、投稿ごとに7つの特徴量(動画の有無、最長動画の長さ、画像の有無、メディア数、文字量、改行数、URLの有無)が再ランキングのサービスに送られるようになりました。ただし、それを使う側の UserDebiasEnabled は vm-ranker/params.rs で false のままで、オフのときは処理がそのまま返ります。「リンク付き投稿は減点される」という説明も見かけますが、重みの上ではリンクを開く行動は +0.2 で、マイナスの項目はありません。
自分のアカウントに影響があったか測る
コードからわかるのは「何が変わったか」までです。自分の投稿に効いたかは、変更日の前後で数字を比べるしかありません。GetXAPIのユーザー投稿取得エンドポイントは、最新投稿約20件の表示回数・いいね・リプライ・リポスト・引用・ブックマークを 1回0.001ドル で返します。
import csv
import os
from datetime import datetime, timezone
import requests
API_KEY = os.environ["GETXAPI_KEY"]
FIELDS = ["viewCount", "likeCount", "replyCount", "retweetCount", "quoteCount", "bookmarkCount"]
def snapshot(username, out="post_metrics.csv"):
r = requests.get(
"https://api.getxapi.com/twitter/user/tweets",
params={"userName": username},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
r.raise_for_status()
taken = datetime.now(timezone.utc).isoformat(timespec="seconds")
new_file = not os.path.exists(out)
with open(out, "a", newline="", encoding="utf-8") as f:
w = csv.writer(f)
if new_file:
w.writerow(["taken_at", "post_id", "created_at"] + FIELDS)
for p in r.json()["tweets"]:
w.writerow([taken, p["id"], p["createdAt"]] + [p.get(k) for k in FIELDS])
snapshot("your_handle")
検証結果: 2026年10月28日に公開アカウント(@nikitabier)で実行し、1回の呼び出しで 22件 の投稿と6つの指標がCSVに書き込まれました。cronで毎日動かし、リポジトリのコミット履歴と見比べてください。
このコードからわからないこと
- 返信欄の並び順。 XのヘルプセンターによるとX Premium(有料)の加入有無は返信の並びに考慮されますが、おすすめの順位付けコードにはPremiumや認証の項目はありませんでした。
- 非公開の部分。 一部のルールファイルとGroxという分類器のプロンプトは公開されていません。
- 一人ひとりの順位。 予測確率は人によって違うので、同じ投稿でも点数は人ごとに変わります。
よくある質問
Xのアルゴリズムはオープンソースですか?
はい。github.com/xai-org/x-algorithm に、おすすめタイムラインのパイプライン、重み、フィルター、Phoenixモデルのコードが公開されています。一部のルールファイルと分類器のプロンプトは非公開です。
おすすめに表示させない・おすすめを変えるには?
見たくない投稿には「興味がない」(重み −52.272)を使い、アカウントのミュートやブロック、ミュートワードを設定すると、その投稿はスコア計算の前に除外されます。フォロー中のアカウントだけを見たいときは「フォロー中」タブに切り替えます。
Xのアルゴリズムはどのくらいの頻度で変わりますか?
リポジトリにはほぼ毎日コミットがありますが、重みが変わったのは8月13日から10月28日までで5回です。多くのコミットは同期時刻の更新や、オフの新しい設定の追加です。
参考
- xai-org/x-algorithm README(commit 2f25214): github.com/xai-org/x-algorithm/blob/2f252142b9284…
- home-mixer/params/param.rs(重み、commit 2f25214): github.com/xai-org/x-algorithm/blob/2f252142b9284…
- vm-ranker/params.rs(フォールバック値、UserDebiasEnabled): github.com/xai-org/x-algorithm/blob/2f252142b9284…
- param.rs のコミット履歴: github.com/xai-org/x-algorithm/commits/main/home-…
- 10月27日の差分: github.com/xai-org/x-algorithm/compare/6258a15...…
- X ヘルプセンター「おすすめタイムラインについて」: help.x.com
- twitter/the-algorithm(2023年の旧Twitter版): twitter/the-algorithm
- GetXAPI docs「User Tweets」: docs.getxapi.com
- 英語版の詳しい解説: getxapi.com
1 like
You missed this internet classic the-algorithm#1999
Downed in Arc Raiders PvE? Crawl after 8s and robots can target you again 🤖
anemity.com #ArcRaiders Stay still or crawl?
Music by Karl Casey @ White Bat Audio.
Twemoji by Twitter and contributors: twitter/twemoji; CC BY 4.0 creativecommons.org; animated.
string.length is wrong for X and Bluesky. My counter failed 8 of 34 tests against X's own library
I'm Alex. I build Fuxux, a social media scheduler: you write a post once and it goes out to X, Bluesky, Instagram, LinkedIn, Threads and a few more.
A scheduler has to answer one boring question before anything is sent: does this text fit? I assumed that was text.length <= limit. It isn't, and the two networks with the smallest limits are the two where it goes wrong.
I also thought I had already solved it. Then I sat down to write this post, checked my own code against X's library to get the examples right, and it was wrong on 8 of 34 strings. So this is the corrected version, with the mistake left in.
Three ways to count one string
Take ok 👍🏽. That's "ok", a space, and a thumbs up with a skin tone.
const text = "ok 👍🏽";
text.length; // 7 UTF-16 code units
[...text].length; // 5 code points
[...new Intl.Segmenter(undefined, { granularity: "grapheme" })
.segment(text)].length; // 4 what a person sees
Three honest answers. Which one a network uses decides whether your post is accepted.
| String | length | X counts | Bluesky counts |
|---|---|---|---|
| ok 👍🏽 | 7 | 5 | 4 |
| 👨👩👧👦 (family) | 11 | 2 | 1 |
| 🇪🇸 (flag) | 4 | 2 | 1 |
| こんにちは | 5 | 10 | 5 |
| 𝗯𝗼𝗹𝗱 (bold "font") | 8 | 8 | 4 |
| read fuxux.com now | 18 | 32 | 18 |
| a 140-character URL | 140 | 23 | 140 |
The X column is from X's own library. The Bluesky column is the grapheme count.
X: weights, not characters
X publishes how it counts in an open source library, twitter-text. The limit is 280, but 280 of a weighted unit:
- Latin letters, digits and common punctuation weigh 1. In code point terms: U+0000 to U+10FF, U+2000 to U+200D, U+2010 to U+201F and U+2032 to U+2037.
- Everything else weighs 2. That's why five Japanese characters cost 10, and why those bold "fonts" people paste into bios cost double: they are mathematical letters far outside the Latin range.
- An emoji weighs 2, as a whole. A family of four is seven code points glued with joiners and it is still 2.
- Any link weighs 23, whether it is 9 characters or 140.
- The text is normalised to NFC first, so e plus a combining accent counts as one é.
The link rule bites in two directions. A long URL is cheaper than it looks. A short one is more expensive: fuxux.com is 9 characters and costs 23.
And it catches text you didn't mean as a link. It works.So does this is 21 characters. X counts 36, because a missing space after the full stop makes works.So look like a domain (.so is a real ending).
Where I got it wrong
My first version walked the string one code point at a time and added 1 or 2:
function weigh(text) {
let total = 0;
for (const char of text) total += xWeight(char.codePointAt(0));
return total;
}
That handles a plain 👍 (one code point, weight 2). It falls apart on anything built from several:
| String | X | My first version |
|---|---|---|
| ok 👍🏽 | 5 | 7 |
| 👨👩👧👦 | 2 | 11 |
| 🇪🇸 | 2 | 4 |
| 1️⃣ | 2 | 5 |
| ❤️ | 2 | 4 |
It always over-counted, so nobody could post something too long because of it. It just told people their post didn't fit when it did. On a 280 limit, a few emoji with skin tones is enough to matter.
The fix is to split into graphemes first, and count a grapheme that is an emoji as 2 no matter what it is made of:
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
// Drawn as an emoji by default, or a symbol followed by the emoji
// selector, or a flag letter, or a keycap like 1️⃣.
const EMOJI =
/\p{Emoji_Presentation}|\p{Extended_Pictographic}️|\p{Regional_Indicator}|[0-9#*]️?⃣/u;
function xWeight(cp) {
return (cp <= 0x10ff) ||
(cp >= 0x2000 && cp <= 0x200d) ||
(cp >= 0x2010 && cp <= 0x201f) ||
(cp >= 0x2032 && cp <= 0x2037) ? 1 : 2;
}
function weigh(text) {
let total = 0;
for (const { segment } of segmenter.segment(text)) {
if (EMOJI.test(segment)) { total += 2; continue; }
for (const char of segment) total += xWeight(char.codePointAt(0));
}
return total;
}
function countForX(text, urlPattern) {
const s = text.normalize("NFC");
let total = 0, last = 0;
for (const m of s.matchAll(urlPattern)) {
total += weigh(s.slice(last, m.index)) + 23;
last = m.index + m[0].length;
}
return total + weigh(s.slice(last));
}
My second attempt was also wrong, in a smaller way. I used \p{Extended_Pictographic} on its own, and © 2026 came out as 7 instead of 6. A bare © is in that Unicode class, but X only treats it as an emoji when the emoji selector (U+FE0F) follows it. Same for a text heart ❤ versus ❤️. Hence the longer pattern above.
It now matches twitter-text on 43 of my 44 test strings. The one it misses is a link ending I don't carry: X knows every domain ending there is, and I keep a short list of the common ones, so a.community is a link to X and plain text to me. If you need that last case, use their list.
Which is the real advice here: if you can ship the dependency, call twitter.parseTweet(text).weightedLength and be done. I wanted something small that runs in the browser as you type, so I wrote my own and test it against theirs.
Bluesky: graphemes, and a second limit behind it
Bluesky's limit is 300, and it counts graphemes. The post schema says so directly: maxGraphemes: 300. So the family emoji is 1, the flag is 1, and ok 👍🏽 is 4. Intl.Segmenter gives you that number in every current browser and in Node.
The same schema also has maxLength: 3000, and that one is in UTF-8 bytes. Normal text never gets near it. Three hundred family emoji would, at 25 bytes each.
The rest
For Instagram, LinkedIn, Threads, Facebook and the others I count text.length. I have not checked each of them emoji by emoji the way I just did for X, so I won't claim more than that. Their limits are large enough (500 on Threads, 2,200 on Instagram, 3,000 on LinkedIn) that a few units either way rarely decides anything. On 280 and 300 it does.
If you only need the numbers, I keep the limits for all 12 networks in one table.
Try it
The counter is free and needs no account: fuxux.com. Paste a post and it shows the count for each network side by side, using the rules above. Paste ok 👍🏽 and you'll see X and Bluesky disagree.
The limits in it are the same numbers Fuxux uses when it publishes. If you'd rather not think about any of this, that's what the scheduler is for, and there's a free plan (3 social accounts, 5 posts a month, no card).
If you know a string where X's count surprised you, put it in the comments. I'll add it to the tests.
2 reactions · 8 comments
Day 533 of asking @elonmusk to change the troll emoji on X from 🧌 to the troll face
- Pull request: twemoji#1430
- Article: x.com
- Petition: c.org t.co
37 likes · 20 reposts · 472 views
Character Counting Is Harder Than It Looks: A Dev's Guide
---
title: "Character Counting Is Harder Than It Looks: A Dev's Guide"
published: true
description: "String.length lies about emoji, X weights every URL as 23 characters, and SMS counts in segments. A developer's guide to counting text correctly — with code and a free tool."
tags: tutorial, webdev, javascript, productivity
cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com
canonical_url: toolkitloop.blogspot.com
---
Every developer has written something like if (text.length > 280). And every developer with users who type emoji has watched that check break in production. Counting characters is one of those problems that looks trivial until a platform, an encoding, or a family emoji proves otherwise.
Here is a field guide to the three counting regimes you will actually meet — and the code that handles each.
Regime 1: graphemes — what the reader sees
A string is not a sequence of characters. It is a sequence of UTF-16 code units, and one visible character can be many of them:
"👨👩👧👦".length; // 11 — but your eyes see ONE character
"é".length; // 2 if built from e + combining accent
The fix is the grapheme cluster — what a reader perceives as a single character — and the browser already has an API for it:
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const count = [...segmenter.segment("👨👩👧👦")].length; // 1
If you build any "max N characters" validation, count segments, not .length. Your users' emoji will thank you. (One caveat: Intl.Segmenter support is broad in modern browsers but check your target — Node 16+ and all evergreen browsers have it.)
Regime 2: X's weighted counting — URLs cost 23, always
A standard X post allows 280 weighted characters. The weighting rules (in the open-source twitter-text config) mean:
- Every URL counts as 23 characters, no matter its real length — X wraps links in its own shortener.
- Some ranges (emoji included) weigh more than 1.
So this announcement — 193 characters as your eyes count them — is only about 169 weighted: the 47-character URL collapses to 23 while the rocket counts double.
// sketch of the idea, not the full config
function weightedLength(text) {
const urlRegex = /https?:\/\/\S+/g;
const withoutUrls = text.replace(urlRegex, "");
const urls = (text.match(urlRegex) || []).length;
// emoji & wide ranges weigh 2 (simplified)
const body = [...withoutUrls].reduce(
(n, ch) => n + (/\p{Extended_Pictographic}/u.test(ch) ? 2 : 1), 0);
return body + urls * 23;
}
Moral: never validate against X's limit with a plain character count. Use the platform's rule — or a tool that applies it for you.
Regime 3: SMS segments — the encoding tax
SMS is counted in segments, and the segment size depends on encoding:
| Encoding | Single message | Concatenated segments |
|---|---|---|
| GSM-7 (plain Latin) | 160 chars | 153 chars each |
| UCS-2 (emoji/CJK) | 70 chars | 67 chars each |
One emoji flips the entire message to UCS-2. Even € costs two GSM-7 units. A 165-character marketing text with one emoji can silently become three billable segments instead of two. If you build anything that sends SMS, surface the segment count before the user hits send.
The tool I built for all three
Disclosure first: I built toolkitloop.com — one of 30 free browser tools I maintain. It counts visible graphemes (family emoji = 1), has an X mode that applies the weighted URL rule, shows with/without-spaces totals, and runs 100% client-side — no signup, nothing uploaded. Handy as a pre-publish check before the platform's own composer.
Your turn
What is the worst character-counting bug you have shipped or seen? The emoji-that-ate-the-limit? The SMS bill surprise? Drop it in the comments — I am collecting edge cases to make the tool smarter.
1 reaction
Day 532 of asking @elonmusk to change the troll emoji on X from 🧌 to the troll face
- Pull request: twemoji#1430
- Article: x.com
- Petition: c.org t.co
30 likes · 12 reposts · 312 views
Mentions
twitter/communitynotes: Documentation and source code powering Twitter’s Community Notes Is it OK to help with [Wayback/Archive] twitter/communitynotes: Documentation and source code powering Twitter's Community Notes? Python source code: [Wayback/Archive] communitynotes/sourcecode at main ·…
پروژه این هفته من: ۳۰ تا ایجنت Claude، پنج روز بیوقفه موازی کار کردن و بالاخره الگوریتم X رو ریورسانجینیر کردم 🤯 کامل، رایگان و Open Source گذاشتمش اینجا 👇 حالشو ببرین: twitter/the-algorithm
Day 550 of asking @elonmusk to change the troll emoji on X from 🧌 to the troll face - Pull request: twemoji#1430 - Article: x.com - Petition: c.org t.co
Xアルゴリズムの仕組み:公開ソースコードの重みを全部読んでみた(10月27日の変更まで) :::note info 本記事には筆者が開発に関わっているサービス(GetXAPI)の紹介を含みます。重みと設定値はすべて、2026年10月28日(UTC)に xai-org/x-algorithm リポジトリを筆者が直接読んで確認したものです。 ::: 「いいねは0.5点」「通報1件でいいね355件分のマイナス」「プロフィールクリックは12点」。Xのアルゴリズムについて、日本語の解説記事やAIの回答にはこうした数字が並んでいます。ところが、いま公開されているコードを読むと、いくつかは2023年の旧Twitterのコードの数字で、いくつかは10月13日より前の古い値です。この記事では、Xのアルゴリズムの仕組みを公開ソースコードから行番号つきで整理し、重みがいつ変わったかも全コミットから洗い出しました。 2026年10月28日更新。重みは commit 2f25214(10月27日 01:28 UTC)の home-mixer/params/param.rs から引用し、翌日の commit 5ed7963 でも重みに変更がないことを確認しています。 Xのアルゴリズムはどういう仕組みですか? リクエストのたびに候補の投稿を集め、各投稿に対してあなたが取りそうな行動の確率を予測し、重みをかけて足し合わせた点数で並べます。 候補はフォロー中のアカウント(Thunder)と、フォロー外のアカウント(Phoenixの検索、SimClusters、人気投稿)から集まります。点数は Σ(重み × その行動の予測確率) で、READMEにもこの式が書かれています。投稿から48時間を過ぎた投稿はスコア計算の前に除外されます。 いいねの重みは0.5ですか? 10月13日までは0.5でした。 commit e1a0375 で0.6に、翌日の 17c96bf で 0.66 に上がりました。0.5という数字は、いまも vm-ranker/params.rs の3行目にフォールバック(既定値)として残っています。本番の設定から同期されているのは home-mixer 側のファイル(1行目に「mirrored from config feature-switch defaults」とある)なので、引用するならこちらです。 通報1件はいいね何件分ですか? そういう換算はできません。 重みは「回数」ではなく「予測確率」にかかります。param.rs の319〜346行目のコメントでは、X自身が「通報1件でいいね468件が帳消し」という読み方を誤りと書き、通報はいいねより1,000倍以上起きにくいと説明しています。 「Heavy Ranker」「プロフィールクリック12点」は今も正しいですか? いいえ、2023年の旧Twitterのリポジトリ(twitter/the-algorithm)の話です。 現在のおすすめはPhoenixモデルで順位付けされ、プロフィールクリックの重みは 0、作者のリプライ返し(+75)という項目も今の重み一覧にはありません。「候補を約1,500件抽出」という数字も、現在のコードでは確認できませんでした。 Xアルゴリズムの重み一覧(commit 2f25214) home-mixer/params/param.rs の348行目(FavoriteWeight)から460行目(ReportWeight)までの値です。 | 予測する行動 | 重み | |---|---| | リンクをコピーして共有 | 20.0 | | リプライ | 5.0(相互フォロー同士なら +15.0) | | 引用 | 5.0 | | DMで共有 | 5.0 | | 作者をフォロー | 4.0 | | 共有 | 2.0 | | リポスト | 1.0 | | いいね | 0.66 | | クリック後の滞在時間(連続値) | 0.4 | | クリック | 0.3 | | リンクを開く | 0.2 | | 画像の拡大、滞在 | 各0.05 | | 動画を開く | 0.02 | | 滞在時間(連続値) | 0.006 | | プロフィールクリック、動画の視聴品質 | 0 | | 滞在しない | −0.02 | | ブロック | −31.2 | | 興味がない | −52.272 | | ミュート | −58.8 | | 通報 | −234 | ブックマークには重みがありません。ただし viewer_history.rs の30行目で ClientTweetBookmark は「ポジティブな行動」に入っていて、モデルが読む行動履歴には使われます。 重みはいつ変わった?(8月13日以降の全変更) param.rs に触れた48コミットをすべて再生し、重みが変わったものだけを抜き出しました。 | 日付 | コミット | 変更 | |---|---|---| | 8月25日 | 0d3cdd8 | 滞在 0→0.05、動画を開く 0.05→0.07、動画の視聴品質 0.05→0 | | 9月29日 | a707cc2 | クリック 0.4→0.3、クリック後の滞在時間 0→0.4、興味がない −43.2→−47.52 | | 10月7日 | 78460ca | 未探索の投稿 0.02→0.015 | | 10月13日 | e1a0375 | いいね 0.5→0.6、動画を開く 0.07→0.02、滞在時間 0.004→0.006 | | 10月14日 | 17c96bf | いいね 0.6→0.66、興味がない −47.52→−52.272 | 相互フォローのリプライ加点は同期ファイルができる前の変更で、リポジトリ内の docs/BIDIRECTIONAL_BOOST_CHANGE.md によると7月13日に20で導入され、7月24日に15へ下げられています。 10月27日の変更で何が変わった? 重みは変わっていません。 変わったのは VMRankerSendDebiasInputs が false から true になったこと(param.rs 508行目)で、投稿ごとに7つの特徴量(動画の有無、最長動画の長さ、画像の有無、メディア数、文字量、改行数、URLの有無)が再ランキングのサービスに送られるようになりました。ただし、それを使う側の UserDebiasEnabled は vm-ranker/params.rs で false のままで、オフのときは処理がそのまま返ります。「リンク付き投稿は減点される」という説明も見かけますが、重みの上ではリンクを開く行動は +0.2 で、マイナスの項目はありません。 自分のアカウントに影響があったか測る コードからわかるのは「何が変わったか」までです。自分の投稿に効いたかは、変更日の前後で数字を比べるしかありません。GetXAPIのユーザー投稿取得エンドポイントは、最新投稿約20件の表示回数・いいね・リプライ・リポスト・引用・ブックマークを 1回0.001ドル で返します。 import csv import os from datetime import datetime, timezone import requests API_KEY = os.environ["GETXAPI_KEY"] FIELDS = ["viewCount", "likeCount", "replyCount", "retweetCount", "quoteCount", "bookmarkCount"] def snapshot(username, out="post_metrics.csv"): r = requests.get( "https://api.getxapi.com/twitter/user/tweets", params={"userName": username}, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=30, ) r.raise_for_status() taken = datetime.now(timezone.utc).isoformat(timespec="seconds") new_file = not os.path.exists(out) with open(out, "a", newline="", encoding="utf-8") as f: w = csv.writer(f) if new_file: w.writerow(["taken_at", "post_id", "created_at"] + FIELDS) for p in r.json()["tweets"]: w.writerow([taken, p["id"], p["createdAt"]] + [p.get(k) for k in FIELDS]) snapshot("your_handle") 検証結果: 2026年10月28日に公開アカウント(@nikitabier)で実行し、1回の呼び出しで 22件 の投稿と6つの指標がCSVに書き込まれました。cronで毎日動かし、リポジトリのコミット履歴と見比べてください。 このコードからわからないこと - 返信欄の並び順。 XのヘルプセンターによるとX Premium(有料)の加入有無は返信の並びに考慮されますが、おすすめの順位付けコードにはPremiumや認証の項目はありませんでした。 - 非公開の部分。 一部のルールファイルとGroxという分類器のプロンプトは公開されていません。 - 一人ひとりの順位。 予測確率は人によって違うので、同じ投稿でも点数は人ごとに変わります。 よくある質問 Xのアルゴリズムはオープンソースですか? はい。github.com/xai-org/x-algorithm に、おすすめタイムラインのパイプライン、重み、フィルター、Phoenixモデルのコードが公開されています。一部のルールファイルと分類器のプロンプトは非公開です。 おすすめに表示させない・おすすめを変えるには? 見たくない投稿には「興味がない」(重み −52.272)を使い、アカウントのミュートやブロック、ミュートワードを設定すると、その投稿はスコア計算の前に除外されます。フォロー中のアカウントだけを見たいときは「フォロー中」タブに切り替えます。 Xのアルゴリズムはどのくらいの頻度で変わりますか? リポジトリにはほぼ毎日コミットがありますが、重みが変わったのは8月13日から10月28日までで5回です。多くのコミットは同期時刻の更新や、オフの新しい設定の追加です。 参考 - xai-org/x-algorithm README(commit 2f25214): github.com/xai-org/x-algorithm/blob/2f252142b9284… - home-mixer/params/param.rs(重み、commit 2f25214): github.com/xai-org/x-algorithm/blob/2f252142b9284… - vm-ranker/params.rs(フォールバック値、UserDebiasEnabled): github.com/xai-org/x-algorithm/blob/2f252142b9284… - param.rs のコミット履歴: github.com/xai-org/x-algorithm/commits/main/home-… - 10月27日の差分: github.com/xai-org/x-algorithm/compare/6258a15...… - X ヘルプセンター「おすすめタイムラインについて」: help.x.com - twitter/the-algorithm(2023年の旧Twitter版): twitter/the-algorithm - GetXAPI docs「User Tweets」: docs.getxapi.com - 英語版の詳しい解説: getxapi.com
You missed this internet classic the-algorithm#1999
Downed in Arc Raiders PvE? Crawl after 8s and robots can target you again 🤖 anemity.com #ArcRaiders Stay still or crawl? Music by Karl Casey @ White Bat Audio. Twemoji by Twitter and contributors: twitter/twemoji; CC BY 4.0 creativecommons.org; animated.
string.length is wrong for X and Bluesky. My counter failed 8 of 34 tests against X's own library I'm Alex. I build Fuxux, a social media scheduler: you write a post once and it goes out to X, Bluesky, Instagram, LinkedIn, Threads and a few more. A scheduler has to answer one boring question before anything is sent: does this text fit? I assumed that was text.length <= limit. It isn't, and the two networks with the smallest limits are the two where it goes wrong. I also thought I had already solved it. Then I sat down to write this post, checked my own code against X's library to get the examples right, and it was wrong on 8 of 34 strings. So this is the corrected version, with the mistake left in. Three ways to count one string Take ok 👍🏽. That's "ok", a space, and a thumbs up with a skin tone. const text = "ok 👍🏽"; text.length; // 7 UTF-16 code units [...text].length; // 5 code points [...new Intl.Segmenter(undefined, { granularity: "grapheme" }) .segment(text)].length; // 4 what a person sees Three honest answers. Which one a network uses decides whether your post is accepted. | String | length | X counts | Bluesky counts | |---|---|---|---| | ok 👍🏽 | 7 | 5 | 4 | | 👨👩👧👦 (family) | 11 | 2 | 1 | | 🇪🇸 (flag) | 4 | 2 | 1 | | こんにちは | 5 | 10 | 5 | | 𝗯𝗼𝗹𝗱 (bold "font") | 8 | 8 | 4 | | read fuxux.com now | 18 | 32 | 18 | | a 140-character URL | 140 | 23 | 140 | The X column is from X's own library. The Bluesky column is the grapheme count. X: weights, not characters X publishes how it counts in an open source library, twitter-text. The limit is 280, but 280 of a weighted unit: - Latin letters, digits and common punctuation weigh 1. In code point terms: U+0000 to U+10FF, U+2000 to U+200D, U+2010 to U+201F and U+2032 to U+2037. - Everything else weighs 2. That's why five Japanese characters cost 10, and why those bold "fonts" people paste into bios cost double: they are mathematical letters far outside the Latin range. - An emoji weighs 2, as a whole. A family of four is seven code points glued with joiners and it is still 2. - Any link weighs 23, whether it is 9 characters or 140. - The text is normalised to NFC first, so e plus a combining accent counts as one é. The link rule bites in two directions. A long URL is cheaper than it looks. A short one is more expensive: fuxux.com is 9 characters and costs 23. And it catches text you didn't mean as a link. It works.So does this is 21 characters. X counts 36, because a missing space after the full stop makes works.So look like a domain (.so is a real ending). Where I got it wrong My first version walked the string one code point at a time and added 1 or 2: function weigh(text) { let total = 0; for (const char of text) total += xWeight(char.codePointAt(0)); return total; } That handles a plain 👍 (one code point, weight 2). It falls apart on anything built from several: | String | X | My first version | |---|---|---| | ok 👍🏽 | 5 | 7 | | 👨👩👧👦 | 2 | 11 | | 🇪🇸 | 2 | 4 | | 1️⃣ | 2 | 5 | | ❤️ | 2 | 4 | It always over-counted, so nobody could post something too long because of it. It just told people their post didn't fit when it did. On a 280 limit, a few emoji with skin tones is enough to matter. The fix is to split into graphemes first, and count a grapheme that is an emoji as 2 no matter what it is made of: const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); // Drawn as an emoji by default, or a symbol followed by the emoji // selector, or a flag letter, or a keycap like 1️⃣. const EMOJI = /\p{Emoji_Presentation}|\p{Extended_Pictographic}️|\p{Regional_Indicator}|[0-9#*]️?⃣/u; function xWeight(cp) { return (cp <= 0x10ff) || (cp >= 0x2000 && cp <= 0x200d) || (cp >= 0x2010 && cp <= 0x201f) || (cp >= 0x2032 && cp <= 0x2037) ? 1 : 2; } function weigh(text) { let total = 0; for (const { segment } of segmenter.segment(text)) { if (EMOJI.test(segment)) { total += 2; continue; } for (const char of segment) total += xWeight(char.codePointAt(0)); } return total; } function countForX(text, urlPattern) { const s = text.normalize("NFC"); let total = 0, last = 0; for (const m of s.matchAll(urlPattern)) { total += weigh(s.slice(last, m.index)) + 23; last = m.index + m[0].length; } return total + weigh(s.slice(last)); } My second attempt was also wrong, in a smaller way. I used \p{Extended_Pictographic} on its own, and © 2026 came out as 7 instead of 6. A bare © is in that Unicode class, but X only treats it as an emoji when the emoji selector (U+FE0F) follows it. Same for a text heart ❤ versus ❤️. Hence the longer pattern above. It now matches twitter-text on 43 of my 44 test strings. The one it misses is a link ending I don't carry: X knows every domain ending there is, and I keep a short list of the common ones, so a.community is a link to X and plain text to me. If you need that last case, use their list. Which is the real advice here: if you can ship the dependency, call twitter.parseTweet(text).weightedLength and be done. I wanted something small that runs in the browser as you type, so I wrote my own and test it against theirs. Bluesky: graphemes, and a second limit behind it Bluesky's limit is 300, and it counts graphemes. The post schema says so directly: maxGraphemes: 300. So the family emoji is 1, the flag is 1, and ok 👍🏽 is 4. Intl.Segmenter gives you that number in every current browser and in Node. The same schema also has maxLength: 3000, and that one is in UTF-8 bytes. Normal text never gets near it. Three hundred family emoji would, at 25 bytes each. The rest For Instagram, LinkedIn, Threads, Facebook and the others I count text.length. I have not checked each of them emoji by emoji the way I just did for X, so I won't claim more than that. Their limits are large enough (500 on Threads, 2,200 on Instagram, 3,000 on LinkedIn) that a few units either way rarely decides anything. On 280 and 300 it does. If you only need the numbers, I keep the limits for all 12 networks in one table. Try it The counter is free and needs no account: fuxux.com. Paste a post and it shows the count for each network side by side, using the rules above. Paste ok 👍🏽 and you'll see X and Bluesky disagree. The limits in it are the same numbers Fuxux uses when it publishes. If you'd rather not think about any of this, that's what the scheduler is for, and there's a free plan (3 social accounts, 5 posts a month, no card). If you know a string where X's count surprised you, put it in the comments. I'll add it to the tests.
Day 533 of asking @elonmusk to change the troll emoji on X from 🧌 to the troll face - Pull request: twemoji#1430 - Article: x.com - Petition: c.org t.co
Character Counting Is Harder Than It Looks: A Dev's Guide --- title: "Character Counting Is Harder Than It Looks: A Dev's Guide" published: true description: "String.length lies about emoji, X weights every URL as 23 characters, and SMS counts in segments. A developer's guide to counting text correctly — with code and a free tool." tags: tutorial, webdev, javascript, productivity cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com canonical_url: toolkitloop.blogspot.com --- Every developer has written something like if (text.length > 280). And every developer with users who type emoji has watched that check break in production. Counting characters is one of those problems that looks trivial until a platform, an encoding, or a family emoji proves otherwise. Here is a field guide to the three counting regimes you will actually meet — and the code that handles each. Regime 1: graphemes — what the reader sees A string is not a sequence of characters. It is a sequence of UTF-16 code units, and one visible character can be many of them: "👨👩👧👦".length; // 11 — but your eyes see ONE character "é".length; // 2 if built from e + combining accent The fix is the grapheme cluster — what a reader perceives as a single character — and the browser already has an API for it: const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }); const count = [...segmenter.segment("👨👩👧👦")].length; // 1 If you build any "max N characters" validation, count segments, not .length. Your users' emoji will thank you. (One caveat: Intl.Segmenter support is broad in modern browsers but check your target — Node 16+ and all evergreen browsers have it.) Regime 2: X's weighted counting — URLs cost 23, always A standard X post allows 280 weighted characters. The weighting rules (in the open-source twitter-text config) mean: - Every URL counts as 23 characters, no matter its real length — X wraps links in its own shortener. - Some ranges (emoji included) weigh more than 1. So this announcement — 193 characters as your eyes count them — is only about 169 weighted: the 47-character URL collapses to 23 while the rocket counts double. // sketch of the idea, not the full config function weightedLength(text) { const urlRegex = /https?:\/\/\S+/g; const withoutUrls = text.replace(urlRegex, ""); const urls = (text.match(urlRegex) || []).length; // emoji & wide ranges weigh 2 (simplified) const body = [...withoutUrls].reduce( (n, ch) => n + (/\p{Extended_Pictographic}/u.test(ch) ? 2 : 1), 0); return body + urls * 23; } Moral: never validate against X's limit with a plain character count. Use the platform's rule — or a tool that applies it for you. Regime 3: SMS segments — the encoding tax SMS is counted in segments, and the segment size depends on encoding: | Encoding | Single message | Concatenated segments | |---|---|---| | GSM-7 (plain Latin) | 160 chars | 153 chars each | | UCS-2 (emoji/CJK) | 70 chars | 67 chars each | One emoji flips the entire message to UCS-2. Even € costs two GSM-7 units. A 165-character marketing text with one emoji can silently become three billable segments instead of two. If you build anything that sends SMS, surface the segment count before the user hits send. The tool I built for all three Disclosure first: I built toolkitloop.com — one of 30 free browser tools I maintain. It counts visible graphemes (family emoji = 1), has an X mode that applies the weighted URL rule, shows with/without-spaces totals, and runs 100% client-side — no signup, nothing uploaded. Handy as a pre-publish check before the platform's own composer. Your turn What is the worst character-counting bug you have shipped or seen? The emoji-that-ate-the-limit? The SMS bill surprise? Drop it in the comments — I am collecting edge cases to make the tool smarter.
Day 532 of asking @elonmusk to change the troll emoji on X from 🧌 to the troll face - Pull request: twemoji#1430 - Article: x.com - Petition: c.org t.co