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$01,500 ツイート/月
Basic$20050,000 ツイート/月
Pro$5,0001,000,000 ツイート/月

研究目的で「フォロワー 1,000 以下なのに 5,000 いいね以上ついた投稿」を集めたいだけなら、Free は少なすぎ、Basic は重すぎる。間がありません。

そこで twikit + サブ垢 Cookie 認証 という構成にしました。X 側の内部 API を直接叩くため、追加コスト 0 円・取得上限実質なし (常識的なレートで使う限り)。ただし、X 側の仕様変更で頻繁に壊れます。本記事はその「壊れ方」と「直し方」のログです。

API 課金を疑う前に、自分で組める選択肢を持っていますか?
細マッチョ企業診断 / 3 分 8 問
診断する

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.pyget_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 版で置き換えました。

`python

phin/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() 形式に書き換え。

`python

Before (壊れる)


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
防御的アクセスへの書き換え 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 つのエラーと突破策まとめ
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 週間で立ち上げ可能です。