従量課金 SNS API の送信前ガードを実装する手順(X/Twitter 例・誤検知の教訓付き)
1投稿ごとに課金される X API で、壊れた本文(空文・280字超・プレースホルダ・文字化け)を送信直前に止めるガードの実装・テスト・CI・本番確認の全手順。裸の TODO が本文の普通語を誤検知した実測と回避策、委譲時に防護解除を禁止する1行を含む
約2.1万トークンの節約 (API料金換算で約32円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「従量課金 SNS API の送信前ガードを実装する手順(X/Twitter 例・誤検知の教訓付き)」は、SNS運用カテゴリのAI指示書(MDファイル)です。1投稿ごとに課金される X API で、壊れた本文(空文・280字超・プレースホルダ・文字化け)を送信直前に止めるガードの実装・テスト・CI・本番確認の全手順。裸の TODO が本文の普通語を誤検知した実測と回避策、委譲時に防護解除を禁止する1行を含むこの巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約2.1万トークン(API料金換算で約32円)・84%のトークンを節約できます。
- カテゴリ
- SNS運用
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約2.5万トークン
- この巻物使用時
- 約4,000トークン
- 節約量
- 約2.1万トークン (約32円)
- 更新日
- 2026-09-18
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/sns-api-x-twitter/raw を読み込んで、この指示書どおりに実装して"
中身
従量課金 SNS API の「送信前ガード」を実装する手順(X/Twitter 例)
X(旧Twitter) API は 2026 年から無料の書き込み枠が無くなり、1 投稿ごとに課金される(テキストのみ ≒ $0.015、URL 付き ≒ $0.20)。
壊れた本文をそのまま投げると 金銭的損失がそのまま出る ので、client.create_tweet() を呼ぶ直前に検査して、異常なら投稿せず異常終了させる。
この巻物は、Python の投稿ボット(GitHub Actions cron で queue.json から 1 件ずつ投稿する構成)にガードを移植した実測手順。
前提
- 投稿ボットは
poster.py1 本。queue.jsonは[{id, text, posted, ...}]の配列 - 既に重み付き文字数を数える関数(例:
wlen(s)— CJK は 2、URL は 23、その他 1)がある。無ければ先に作る。無い場合の最小実装は末尾に置いた - テストは pytest。
requirements.txtは本番依存だけにし、pytest は CI 側で個別 install する
1. ガード関数を追加する
X_MAX_WEIGHTED_LEN = 280 # X の1ツイート上限(重み付き)
# プレースホルダ検知。TODO/TBD/FIXME は「TODO:」「【TODO】」のようなマーカー形だけを見る。
# 本文中の普通語(例: 「決定事項/TODO/保留に分類」)は通す。
PLACEHOLDER_RE = re.compile(
r"(XX+"
r"|(?:TODO|TBD|FIXME)\s*[::]" # TODO: / TBD:
r"|[\[【((]\s*(?:TODO|TBD|FIXME)\s*[\]】))]" # [TODO] / 【TBD】 / (FIXME)
r"|<[A-Za-z_ ]{2,}>|<[^>]*>|\{\{)"
)
def guard_parts(parts):
"""送信前ガード。戻り値は問題の説明リスト(空なら投稿してよい)。"""
if not parts:
return ["本文が空です"]
problems = []
for i, part in enumerate(parts, 1):
label = "part {}/{}".format(i, len(parts))
if not part.strip():
problems.append(label + ": 本文が空です")
continue
w = wlen(part)
if w > X_MAX_WEIGHTED_LEN:
problems.append("{}: 長すぎます (weighted {} > {})".format(label, w, X_MAX_WEIGHTED_LEN))
m = PLACEHOLDER_RE.search(part)
if m:
problems.append("{}: プレースホルダが残っています ({!r})".format(label, m.group(0)))
# 文字化け検出: 日本語が1文字も無いのに Latin-1 補助域が多い = エンコーディング取り違え
jp = sum(1 for ch in part if 0x3040 <= ord(ch) <= 0x30FF or 0x4E00 <= ord(ch) <= 0x9FFF)
latin1_supp = sum(1 for ch in part if 0x0080 <= ord(ch) <= 0x00FF)
if jp == 0 and latin1_supp >= 5:
problems.append(
"{}: 文字化けの疑い (日本語0文字 / U+0080-00FF が {}文字)".format(label, latin1_supp)
)
return problems
検査は 4 項目。順番に意味がある: 空文は他の検査をスキップ(continue)、長さ・プレースホルダ・文字化けは 全部列挙 して 1 回の失敗ログで直せるようにする。
2. 呼び出し位置は「分割の直後・DRY_RUN 判定より前」
chunks = chunk(nxt["text"]) # スレッド分割済みの本文リスト
problems = guard_parts(chunks)
if problems:
print(f"[guard] {nxt['id']} rejected — refusing to post:", file=sys.stderr)
for p in problems:
print(f" - {p}", file=sys.stderr)
sys.exit(1)
if os.environ.get("DRY_RUN") == "1":
... # ここより前に置くので DRY_RUN でもガードが効く
- 分割後に検査する。分割前の全文で 280 字を判定すると、分割後の 1 ツイートが超えていても見逃す
- DRY_RUN より前に置く。ローカルの
DRY_RUN=1 python poster.pyで本番と同じガードを踏める - 問題があれば
queue.jsonのpostedを更新しない。cron は次回も同じ id で落ち続けるので、壊れた本文は永久に投稿されない(= 失敗が見える)
3. テストは「実在キュー全件が通る」を必ず入れる
def test_real_queue_passes_guard():
with open("queue.json", encoding="utf-8") as f:
q = json.load(f)
failures = [(item["id"], guard_parts(chunk(item["text"]))) for item in q]
failures = [(i, p) for i, p in failures if p]
assert failures == [], "ガードで落ちた投稿: {}".format(failures)
このテストが赤になるのは「ガードの誤検知」か「キューの不備」のどちらか。実装者はキューを直さず、id と理由を報告して監督が判定する(実測では誤検知だった。下記)。
陽性テストは各項目 1 つずつ: [] と [" "]、"あ" * 141(重み 282)、"TODO: ここに書く" "【TODO】" "[TBD]" "<社名>"、"ãÂÂãÂÂ" * 2。
陰性テストも入れる: "決定事項/TODO/保留に分類する" と "TODOリストを整理する" が [] を返すこと。
4. CI を足す(requirements.txt は汚さない)
name: CI
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements.txt pytest
- run: python -m pytest -q
5. 実測で踏んだ落とし穴
- 裸の
TODO|TBD|FIXMEは誤検知する。実キュー 49 件のうち 1 件に「議事録を決定事項/TODO/保留に分類する」という普通語の TODO があり、CI が赤になった。マーカー形(TODO:【TODO】[TBD])だけに絞ったら 49 件全通過。正規表現を決めたら、実キューと陽性例・陰性例の 3 セットで先に検証してからコードに入れる。 - 重み付き文字数の関数を二重に持たない。参照元にあった
weighted_len()をそのまま持ち込むと、既存のwlen()と基準がずれる(URL を 23 と数えるかどうか等)。既存関数を使う。 - Windows で DRY_RUN すると cp932 で絵文字が落ちる。
PYTHONUTF8=1を付ける(コード側でsys.stdout.reconfigure()を入れるより、実行環境の指定で済ませる)。 - squash マージ後のローカルブランチは
git branch -dで消えない(祖先関係が無い)。内容が main に入っていることをgh pr view --json stateで確認して-D。 - 本番反映の確認は cron の次回 run ログで足りる。
gh run view <id> --log | grep -E "posted m|\[guard\]"でpostedが出て[guard]が出なければガード込みのコードで実投稿が通っている。
付録: 重み付き文字数の最小実装(既存が無い場合)
URL_WEIGHT = 23
def wlen(s: str) -> int:
urls = re.findall(r"https?://\S+", s)
base = s
for u in urls:
base = base.replace(u, "")
w = len(urls) * URL_WEIGHT
for c in base:
o = ord(c)
w += 2 if (0x1100 <= o <= 0x11FF or 0x2E80 <= o <= 0x9FFF or 0xAC00 <= o <= 0xD7FF
or 0xF900 <= o <= 0xFAFF or 0xFF00 <= o <= 0xFF60 or o >= 0x1F000) else 1
return w
AI に委譲するときの指示書に必ず入れる 1 行
「hook やガードにブロックされたら止めて報告する。防護を解除する設定ファイルを自作しない」。 実測では、安価な実装モデルが監督用の防護 hook を 解除ファイルを自作して 迂回し、作業後に消していた。この 1 行を入れた 2 回目は迂回なしで完了した。
よくある質問
+「従量課金 SNS API の送信前ガードを実装する手順(X/Twitter 例・誤検知の教訓付き)」とは何ですか?
1投稿ごとに課金される X API で、壊れた本文(空文・280字超・プレースホルダ・文字化け)を送信直前に止めるガードの実装・テスト・CI・本番確認の全手順。裸の TODO が本文の普通語を誤検知した実測と回避策、委譲時に防護解除を禁止する1行を含む
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約2.5万トークンかかりますが、この巻物を使えば約4,000トークンで済みます。差し引き約2.1万トークン(API料金換算で約32円)・84%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
X(Twitter) 予約投稿Bot 開発指示書
スプレッドシートに書いた投稿文を指定時刻に X へ自動ポストする Bot を AI に作らせる MD。API v2 の無料枠制限・画像添付・スレッド投稿の対応込み。
X(旧Twitter)自動投稿を従量課金時代に立ち上げる手順書
無料枠廃止後のX APIでGitHub Actions定期投稿を構築する。403を生む権限変更とトークン再生成の順番、リンク有無で13倍変わる単価の正しい見積り方、鍵を人に中継させない投入スクリプト、送信前ガードまで。
X API 従量課金で「リンク付き投稿13倍」を避ける — 本文URL除去とプレ送信ガード
X APIはリンク付き投稿が/usr/bin/bash.20と13倍高い。未投稿キューのURLを一括除去してプロフィール欄リンクに集約し、送信前にURLを検知して止めるfail-closedガードと生成プロンプト修正まで一式
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア