5 月 5 日、深夜 0 時。X バイラル投稿のリサーチで、 スクリプトから X Harness Worker に POST リクエストを送ったら HTTP 403 Error 1010 "browser_signature_banned" が返ってきました。「リトライしないでください」と書かれた Cloudflare の素っ気ないエラーページです。同じスクリプトの別エンドポイント呼び出しは通っているのに、特定の Worker だけ拒否される。原因が分かるまで 30 分かかりました。
本記事では、X API の月$200 を $0 にするために組んだ twikit + サブ垢のスクレイピング環境で、実装中に踏んだ 4 つの破壊的変更とそれぞれの突破方法 を、スタックトレースと修正コード込みで共有します。
完成したスタック — 数字で並べる
| 項目 | 値 |
|---|---|
| ベースライブラリ | twikit 2.3.3.post8 (phin/twikit fork) |
| 認証方式 | サブ垢 (auth_token + ct0) |
| 実装言語 | Python 3.13 |
| インフラ追加コスト | $0/月 (X API Basic $200 比で 100% 削減) |
| 取得実績 | 1 日 約 2,500 ツイート (10 アカウント + 20 検索) |
| エラー回避パッチ数 | 3 (transaction.py + user.py + User-Agent 偽装) |
| 突破できなかったもの | 1 (リプ欄 tweet.replies のスキーマエラー、別ルートで代替) |
なぜ X API ではなく twikit なのか
X API の料金体系は 2023 年に大きく変わりました。
| プラン | 月額 | 読み取り上限 |
|---|---|---|
| Free | $0 | 1,500 ツイート/月 |
| Basic | $200 | 50,000 ツイート/月 |
| Pro | $5,000 | 1,000,000 ツイート/月 |
研究目的で「フォロワー 1,000 以下なのに 5,000 いいね以上ついた投稿」を集めたいだけなら、Free は少なすぎ、Basic は重すぎる。間がありません。
そこで twikit + サブ垢 Cookie 認証 という構成にしました。X 側の内部 API を直接叩くため、追加コスト 0 円・取得上限実質なし (常識的なレートで使う限り)。ただし、X 側の仕様変更で頻繁に壊れます。本記事はその「壊れ方」と「直し方」のログです。
4 つの破壊的変更との戦い
エラー 1: Couldn't get KEY_BYTE indices
twikit の本家 d60/twikit を pip install twikit でインストールし、最初のリクエストを叩いたら出たエラーです。
```
File "...twikit/x_client_transaction/transaction.py", line 54, in get_indices
raise Exception("Couldn't get KEY_BYTE indices")
Exception: Couldn't get KEY_BYTE indices
Situation: ジェーン・スーのアカウント情報を client.get_user_by_screen_name("janesu112") で取得しようとした時。
Trip: スタックトレースを追うと transaction.py の get_indices() でコケている。X が認証用に使う X-Client-Transaction-Id を生成するために、X.com のホームページから JavaScript の ondemand.s バンドルを取得して、その中から「KEY_BYTE indices」と呼ばれる正規表現マッチを抽出するロジックです。
Aha: X 側が JavaScript チャンクの埋め込み形式を変更していました。旧形式は 'ondemand.s': 'HASH' という単純なキーでしたが、新形式は数値の chunk ID を経由する 2 段階マッピングに変わっていました。
``
旧: 'ondemand.s': '5888cd7'
新: {20113: 'ondemand.s', ...}, {20113: '5888cd7', ...}
突破: phin/twikit のフォークが PR #7 でこのマッピングに対応していたので、本家の transaction.py を phin 版で置き換えました。
`pythonphin/twikit が追加した resolver
@staticmethod
def _resolve_on_demand_url(response_str: str):
# Method 1 (legacy): direct 'ondemand.s': 'HASH'
on_demand_file = ON_DEMAND_FILE_REGEX.search(response_str)
if on_demand_file:
return f"https://abs.twimg.com/.../ondemand.s.{on_demand_file.group(1)}a.js"
# Method 2 (current): two-step chunk ID -> hash resolution
chunk_id_match = ON_DEMAND_CHUNK_ID_REGEX.search(response_str)
if chunk_id_match:
chunk_id = chunk_id_match.group(1)
hash_pattern = re.compile(rf"""[{{,]{chunk_id}:\s*['|\"]+([a-f0-9]+)['|\"]+""")
hash_match = hash_pattern.search(response_str)
if hash_match:
return f"https://abs.twimg.com/.../ondemand.s.{hash_match.group(1)}a.js"
return None
`
教訓: 本家ライブラリが追従できていない時は、フォークを探す。phin/twikit のように直近の変更に追従している派生があります。
エラー 2: KeyError: 'urls'
phin/twikit に切り替えて再実行したら、別のエラーが出ました。
`python`
File "...twikit/user.py", line 102, in __init__
self.description_urls: list = legacy['entities']['description']['urls']
KeyError: 'urls'
Situation: 上記 transaction.py 修正後のリクエストで、ユーザーオブジェクトを構築する箇所。
Trip: X が User オブジェクトの legacy フィールド構造を変更し、一部のフィールドが省略されるようになった。phin/twikit ですら追従できていない。
Aha: legacy['xxx'] という直接アクセスは、フィールドが消えた瞬間に KeyError で死にます。防御的アクセスに書き換える のが安全です。
突破: user.py を全面的に .get() 形式に書き換え。
`pythonBefore (壊れる)
self.description_urls: list = legacy['entities']['description']['urls']
self.want_retweets: bool = legacy['want_retweets']
self.followers_count: int = legacy['followers_count']
self.withheld_in_countries: list[str] = legacy['withheld_in_countries']
After (壊れない)
self.description_urls: list = legacy.get('entities', {}).get('description', {}).get('urls', []) self.want_retweets: bool = legacy.get('want_retweets', False) self.followers_count: int = legacy.get('followers_count', 0) self.withheld_in_countries: list[str] = legacy.get('withheld_in_countries', []) `
💡 KEY TAKEAWAYS
外部 API レスポンスを受け取るコードは、最初から
dict.get(key, default) で書くべきです。dict[key] はテストが通っているうちは平気ですが、本番で API が仕様変更した瞬間に全件失敗します。

防御的アクセスへの書き換え Before/After
エラー 3: Cloudflare 1010 — 「お前の User-Agent は禁止」
スクレイピングが動き出して、12 キャンペーン分の Engagement Gate を一括登録する Python スクリプト (
setup_campaigns.py) を書きました。X Harness の Cloudflare Worker に POST するだけのシンプルなコードです。
`python
req = request.Request(
url,
data=json.dumps(body).encode("utf-8"),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
},
method="POST",
)
`
実行したら HTTP 403 が返ってきました。
`
{
"type": "https://developers.cloudflare.com/.../error-1010/",
"title": "Error 1010: Access denied",
"status": 403,
"detail": "The site owner has blocked access based on your browser's signature.",
"error_code": 1010,
"error_name": "browser_signature_banned"
}
`
Situation: X Harness Worker は別エンドポイント (
/api/health、/api/x-accounts) では curl で疎通確認できていました。Python urllib からだけ弾かれる。
Trip: 30 分悩みました。Cookie を疑い、API Key を疑い、JSON ペイロードを疑い…全部空振り。
Aha: Cloudflare の Bot Management が Python urllib のデフォルト User-Agent (
Python-urllib/3.13) を Bot 認定して 1010 でブロックしていました。「browser's signature」というエラーメッセージのとおり、UA で判別している。
突破: User-Agent をブラウザ風に偽装するだけで通りました。
`python
req = request.Request(
url,
data=json.dumps(body).encode("utf-8"),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", # ← これ追加
},
method="POST",
)
`
教訓: 自社の Cloudflare Worker でも、自分の Python から弾かれることがある。Cloudflare の Bot Management は POST リクエストに対して特に厳しい。
requests ライブラリ (デフォルト UA は python-requests/2.x) でも同じ症状です。
エラー 4:
scenario_id が文字列リテラルのまま
最後は API スキーマの取り違えでした。Engagement Gate を登録する API ペイロードで、
lineHarnessScenarioId フィールドに私が 設計書のドキュメント名を文字列リテラルのまま 入れていたのです。
`python
間違い
{
"lineHarnessScenarioId": "scenario-found-pin-PDF", # ← これは設計書名
...
}
`
Situation: 12 キャンペーン分の
setup_campaigns.py の CAMPAIGNS 定義を書いた時。
Trip: API は 201 を返してきた。「OK 動いた」と思ったのですが、verify API のテストでシナリオが起動しない。
Aha: LINE Harness 側でシナリオを実際に POST して取得した UUID は
fbbcf8ea-4eb8-4203-bf37-5279de31e7c6 でした。X Harness の Gate には UUID を入れる べきところを、設計書の人間可読名のまま入れていた。
突破: 実装フローを見直したら、Form 側の
onSubmitScenarioId で UUID を指定すれば実起動するので、Gate 側はそのままでも問題ない構造でした。Form を PUT で更新して、UUID を埋めて完了。
`bash
curl -X PUT \
-H "Authorization: Bearer $LINE_KEY" \
-H "Content-Type: application/json" \
"https://line-harness.../api/forms/$FORM_ID" \
-d "{
\"onSubmitWebhookUrl\": \"https://x-harness.../api/engagement-gates/$GATE_ID/verify?username={x_username}\",
\"onSubmitScenarioId\": \"fbbcf8ea-4eb8-4203-bf37-5279de31e7c6\"
}"
`
教訓: API スキーマで「id」と書かれているフィールドには UUID を入れる。設計書の名前を流用してはいけません。
突破できなかったもの —
tweet.replies のスキーマエラー
正直に書きます。1 つだけ突破できなかったエラーがあります。
`
ERROR: 'itemContent'
`
twikit の
get_tweet_by_id() レスポンスから tweet.replies を辿ると、X 側のレスポンス構造で itemContent フィールドが見つからずに失敗します。Top50 投稿のリプライを 50 件ずつ取りたかったのですが、全件失敗しました。
修正は試みましたが、X 側のレスポンス構造が複雑で時間切れ。代替策として、取得済みの 492 投稿の本文から頻出語抽出 に切り替えました。結果として、リプ欄に近い「の一次言語」が取得できたので、実用上は問題なく着地しています。
教訓: 全部直そうとしない。代替手段がある場合は、未解決のままでもスケジュールを優先する判断が必要です。

4 つのエラーと突破策まとめ
完成した運用環境 —
MCP/agent-reach/
最終的に組み上がった環境はこんな構造です。
ディレクトリ構成
`
MCP/agent-reach/
├── .venv/ # Python 仮想環境 (twikit 2.3.3.post8)
├── credentials/
│ ├── cookies_raw.json # Cookie-Editor からの生エクスポート
│ └── cookies.json # twikit 用 dict 形式 (gitignore済)
├── src/
│ ├── targets.json # 10 アカウント + 10 検索 KW 定義
│ ├── collect.py # アカウント+検索一括収集
│ ├── collect_underground.py # min_faves:5000 純粋バズ抽出
│ ├── analyze.py # 型分類・トリガー要素マッピング
│ ├── analyze_underground.py # 4 フィルタ条件で多角解析
│ ├── language_extract.py # 本文から頻出語抽出 (リプ代替)
│ └── gen_card_c05.py # PIL によるブランド画像自動生成
└── results/
├── raw_collected.json # 取得データ (492 ツイート)
├── viral_top.json # スコア順 Top50
├── underground_filtered.json # フォロワー≤1000 ×fav≥5000 14 本
└── Image01_C05_pin_saku.png # 自動生成画像
`
改善したい点
1. 自動再パッチ機能
twikit を
pip install --upgrade した瞬間に手動パッチが消えます。次は postinstall フック相当のスクリプトで、legacy['xxx'] を一括で .get(xxx, default) に置換するスクリプトを書く予定。
2. リプ欄取得のリトライ
search_tweet("conversation_id:NNN", "Latest")` 経由でリプを取れる可能性があります。次の収集サイクルで実装する。
3. 監視ダッシュボード
twikit が壊れた瞬間に気づけるよう、毎朝のヘルスチェック (1 ツイート取得して成功率を Slack 通知) を組む。X 側のスキーマ変更は予測不能なので、検出だけは早くしたい。
次のアクション
EXBANK では、こうした 「課金前提を疑う」エンジニアリング支援 も行っています。「ウチも API 課金を見直したい」「外部ツール依存を減らしたい」と感じた方は、まず 細マッチョ企業診断 で 3 分セルフチェックしてみてください。AI と組める「業務体力」(自前で組める範囲を広げる体質) が 5 軸で可視化されます。
具体的なスクレイピング環境構築や、X / LINE Harness のクライアント案件支援は、お問い合わせフォームでご連絡ください。本記事の構成をベースに 1〜2 週間で立ち上げ可能です。
