請求書より先にアプリが守り始めた — AIO HelperのAI cost-cap spend guard

上原正吉(EarthLink Network Co., Ltd.)。Claude Codeを開発の主体に据え、20を超えるプロダクトを1人で同時に開発・運用しています。これは、その現場の実測記です。

請求書より先にアプリが守り始めた — AIO HelperのAI cost-cap spend guard

請求書より先にアプリが守り始めた — AIO HelperのAI cost-cap spend guard

結論

2026年4月16日、SEO分析SaaS「AIO Helper」のAPIサーバーに、AI APIの月次利用額がサイトごとの上限(既定$50)を超える前に、処理を止める仕組みを入れました。見積額が残額を超える要求は、AIを呼ぶ前に止めます。利用額の記録は、AI呼び出しが成功した後です。各用語は本文の初出で説明します。

  • 利用額の記録と上限確認を1つのINSERTにまとめ、2つの処理へ分けない形にしました。ただし、同時実行時の厳密な上限保証は実際のPostgresで未確認です。また、見積もりを超えた実費や同時実行による超過は、事後の記録では止められません。
  • 4月23日には残額が10%未満になった時点で警告する機能を追加しました(実装後に240件のテストが通過・当時の実行記録)。
  • 確認を入れたのは、文章を生成するAIを呼ぶ3つの機能(ページ目標の自動生成・タイトルと説明文の提案・一括提案)です。一括提案では、ページごとに呼ぶ直前で確認し直します。検索用のベクトルを作るAI呼び出しは、この時点では対象外でした。

本文(読了 約8分)

何を作っていたか

対象は、AIO Helperという自社のSaaS(Software as a Service、インターネット経由で利用するソフトウェア)です。SEO(Search Engine Optimization、検索結果で見つけられやすくする最適化)の運用を支援します。Google Search Consoleなどからサイトのデータを取り込み、ページごとの改善案を出します。構成は、利用者が操作する管理画面と、処理を受け持つAPIサーバー(seo-api)の2つです。製品の全体像はAIO Helperの紹介記事にまとめています。

AI(Artificial Intelligence、学習済みモデルで推論や生成を行う技術)を使うのはAPIサーバー側で、2026年4月の時点で、文章を生成するAIを呼ぶ機能は次の3つでした。いずれも生成AIのAPI(Application Programming Interface、システム間で機能を呼び出す窓口)を呼びます。

  • ページ目標の自動生成(POST /v1/page-goals/auto-generate)。ページのタイトルや本文から、狙うキーワード・ページの目的・想定読者を下書きします。
  • タイトルと説明文の提案(POST /v1/suggest)。1ページ分の改善案を出します。
  • 一括提案(POST /v1/suggest/batch)。クリック数の多いページから順に、まとめて提案を出します。

このほかに、関連する文書を探すための検索用ベクトル(embedding、文章を数値の並びに変換したもの)を作る呼び出しがあります。提案の2機能が内部で使うものと、ベクトルを作り直すAPI(POST /v1/embeddings/rebuild)です。4月の仕組みは、これらを対象にしていません(後の「確認できていないこと」で説明します)。

使う人にとっては、分析が早く終わり、手作業で調べる時間が減るのが価値です。

ただし、AI APIは呼ぶたびに費用が増えます。利用者が増えたときだけでなく、バグで同じ処理を繰り返したときにも請求は増えます。管理画面に利用額を表示するだけでは、気づいた時点ではすでに費用が発生しています。

そこで、次の2つを作りました。

  • 使う前に止める仕組み。サイトごとに月次上限を持ち、残額が足りない要求はAI APIへ送りません。
  • 使った後に追える台帳。サイト、モデル、呼び出し先、トークン数、費用を、文章生成の呼び出し1回ごとに残します。

AIO Helperのどこに上限確認が入っているか(2026年4月の実装をもとにした再構成)

サイトごとの月次上限は、4月16日の時点ではAPI(PUT /v1/sites/:siteId/budget)で変更し、4月23日には管理画面に「AI Budget」タブも加えました。上限と利用記録はAPIサーバーのデータベースにあり、文章を生成するAIを呼ぶ3つの機能は、AIを呼ぶ直前に同じテーブルを見ます。

目的は、単に月$50で止めることではありません。どの機能がどれだけ費用を使ったかを追えるようにし、使い方を変えた後に本当に減ったかを確認することです。「止める」と「改善する」を同じ記録から行える状態を目指しました。

1回の要求をどこで止めるか

処理の順番は次のとおりです。

  1. APIサーバーが、文章を生成するAIを呼ぶ3つの機能のどれかの要求を受け取ります。
  2. 使うモデルと入出力の見込みから費用を計算します。
  3. 当月の利用額と見込み費用をデータベース内で比べます。
  4. 残額が足りなければ、外部のAI APIは呼びません。ページ目標の自動生成と1ページの提案は、HTTP(Hypertext Transfer Protocol、Web通信の規約)402を返します。一括提案は残額が足りないページを飛ばし、飛ばしたページの件数を付けて、HTTP 200で結果を返します。
  5. 残額が足りればAI APIを呼び、成功した後に実績を利用台帳へ記録します。記録の時にも、同じSQL文の中で上限を確認します。

AI APIを呼ぶ前に月次上限を確認する処理フロー(実装をもとにした再構成)

この順番で重要なのは、止める判断を「AI APIがエラーを返した後の後処理」にしないことです。外部へ送った後では費用を取り消せません。アプリの入口で止めることで、初めて上限が制御として機能します。

一方で、この記録は請求書の代わりではありません。アプリは自分が把握しているトークン数と価格表から見積もります。プロバイダ側の最終的な請求と完全に一致するとは限りません。アプリ内台帳は早く止めるための数字、請求書は最終的に支払う数字と役割を分けます。

きっかけは「表示はあるのに上限がない」

発端はテスト監査でした。AIO Helperの管理画面には「今月のAI利用額」を表示するパネルがありましたが、上限を強制するテストは0件でした。利用額を計算できても、上限を超える前に止められなければ、従量課金の生成AIモデルを呼び続けてしまいます。

そこで最初にやったのは、料金計算とデータモデルの分離です。価格表は外部 API に問い合わせず、コードに静的に持つと決めました。理由は3つあります。

  • AI APIの応答に費用が含まれません。AIO HelperはOpenAIのAPIを直接呼んでいますが、応答に含まれるのはトークン数だけです。
  • 価格はそう頻繁には変わりません。静的な価格表でも保守が現実的です。
  • 課金判断を外部呼び出しに依存させたくありません。コストを知るための API 呼び出しが失敗したら課金判断ができなくなる、という循環を避けます。
// ai-cost.ts — 未知モデルはわざと高く見積もる
const FALLBACK_PRICE = { input: 0.02, output: 0.08 };

// calcCost() は 6 桁に丸め、負値は 0 にクランプし、決して throw しない
// estimateCostFromBytes() は 3 bytes/token(英語 ~4・日本語 ~1.5-2 の中間)
// 出力は入力の 25% と仮定してプリフライト見積もりに使う

FALLBACK_PRICE を意図的に高く($0.02/$0.08 per 1K)したのがポイントです。新しいモデルを追加したのに価格表への登録を忘れると、コストが 0 と見なされて上限がすり抜けます。だから未知モデルは「高い」と扱うのが安全側です。

上限確認と利用記録を1文で行う

課金を止める本体は budget.ts に置きました。テーブルは2つです。

-- seo_site_budgets: サイトごとの月次上限(デフォルト $50)
--   monthly_usd_cap NUMERIC(10,2)
-- seo_ai_usage: 1コールごとの追記専用台帳
--   cost_usd NUMERIC(10,6), created_at timestamptz

金額をFLOATではなくNUMERICにしたのは、数百万回積み上がったときの丸め誤差を避けるためです。一番悩んだのは、同時に2つの要求が上限ぎりぎりに来た場合の制御でした。SELECT FOR UPDATEとトランザクションを使う案ではなく、上限確認を含む単一のINSERT文を選びました。

INSERT INTO seo_ai_usage (site_id, model, endpoint, input_tokens, output_tokens, cost_usd)
SELECT $1::text, $2::text, $3::text, $4::int, $5::int, $6::numeric
 WHERE (
   COALESCE((SELECT monthly_usd_cap FROM seo_site_budgets WHERE site_id = $1), $7::numeric)
   - COALESCE((
       SELECT SUM(cost_usd) FROM seo_ai_usage
        WHERE site_id = $1
          AND created_at >= date_trunc('month', NOW())
     ), 0)
 ) >= $6::numeric
RETURNING id;

$7は、サイトに予算行が無いときの既定上限($50)です。事前の残額確認はcheckBudget()が行い、不足なら上流の AI fetch を呼ばずに止めます(1ページ単位の2機能は HTTP 402 Payment Required を返します)。AI呼び出し成功後のrecordUsage()は、この INSERT が 0 行を返したら{ ok: false }を返します。この時点では費用がすでに発生しているため、要求は失敗にせず、警告ログを出します。台帳には記録されないので、その呼び出しの費用は全額が台帳から抜けます。台帳の利用額が増えないため、その後の上限確認にもこの費用は反映されません。cap = 0 は「1円も使わせない」停止設定として機能させました。

単一のINSERT文で確認できる範囲

上限チェックを別のSELECT文にせず、INSERTのWHERE句へ相関サブクエリとして含めています。これにより、1つの文の中では集計と書き込みの間にアプリケーション側の別処理が入りません。

ただし、独立に確認できたのはこのSQLの構造までです。同時に始まった2つの文が必ず先行行を集計へ含めることは、実際のPostgresを使った競合テストで確認できていません。厳密な上限保証が必要なら、予算行のロック、直列化可能な分離レベル、またはアドバイザリーロックなどで、サイト単位の処理順を明示する必要があります。

この設計では、テスト環境にも問題がありました。テストはpg-mem(メモリ内で動くPostgres)で回していましたが、date_trunc('month', NOW())が未実装だったため、当時の記録では18件中12件が500で失敗しました。

12 out of 18 tests in integration-ai-cost-cap.test.ts failed
Only the 3 pure calcCost unit tests pass

記録の時点で通っていたのは、データベースへ触れないcalcCostの単体テストだけでした。500とは別の要因で失敗したテストもあり、記録に残っているのは、PUT budgetが入力検証の問題で400を返した件です。

当時のテスト記録では、db.public.registerFunction()でdate_truncを登録すると失敗は5件に減りました。残りはINSERT...SELECTの中で値の型を解決できない問題です。node-postgresは型を指定していない値を文字列として送るため、$1::textのように明示的な型変換を足して解消しています。

当時のテスト記録では、意図的に date_trunc の WHERE 句を壊す実験も行っています。18件中17件は月次の抽出条件が壊れても通り、「先月分は数えない」テストだけが失敗しました。前月の行を60日前の日付で追加し、集計されないことを確認するテストです。月次フィルタを誤って削除した場合、この1件しか検知できません。重要な条件を検証するテストが1件に集中していると分かりました。

4月23日、残額10%未満の警告を加える

上限で止めるだけでは、使う側には突然 402 が返り、なぜ止まったのか分かりません。そこで残額が 10% を切ったら警告フラグを立てる soft-warning を追加しました。

export const WARNING_REMAINING_PCT = 0.1;
function isNearLimit(cap: number, remaining: number): boolean {
  if (cap <= 0) return false;              // キルスイッチは「警告」ではない
  return remaining / cap < WARNING_REMAINING_PCT;
}

cap <= 0を明示的に除外したのは、「利用を停止している」状態と「上限が近い」状態を別の通知として扱うためです。当時の実行記録では、失敗するテスト4件を先に書き、警告条件の実装後に240件のテストが通っています。変更はfeat: AI budget soft-warning flag at <10% remainingとしてコミットしました。

月次上限に対する利用状態の分け方(当時の実装記録をもとにした再構成)

警告と停止を分けたことで、画面や通知の文言も分けられます。残額が少ないなら、利用者は処理量を減らすか、管理者へ上限変更を依頼できます。上限を0にした場合は、管理者が意図的に止めた状態です。同じ黄色い警告で見せると、利用者は「待てば戻る」と誤解します。

台帳を「止める仕組み」だけで終わらせない

上限に達したことだけを通知しても、次に何を直すかは分かりません。運用で必要なのは、少なくとも次の5つです。

  • 今月いくら使ったか。月次上限と比べるための基本数字です。
  • 今日どれだけ増えたか。急な増加は、利用者増だけでなく、再試行ループやバグの可能性もあります。
  • どの機能が使ったか。呼び出し先を残しておかないと、削減候補を選べません。
  • 1回あたりと成果物1件あたりの単価。呼び出し回数が増えても、提案の件数が同じ比率で増えているなら意味が異なります。
  • 変更の前後でどう変わったか。モデルや入力量を変えた日を残し、その後の単価と比べます。

4月のseo_ai_usageは、このうち上の3つを出せる列(サイト・モデル・呼び出し先・トークン数・費用・日時)を持っています。日別の増え方や変更前後の比較を見る画面は、まだありません。それでも1回ごとの記録が残っていれば、後から集計して削減を「気分」ではなく数字で比べられます。

この設計で確認できていないこと

この記事を「同時実行でも絶対に予算を超えない完成版」とは書けません。理由は4つあります。

  • 実際のPostgresで競合テストを終えていません。単一のSQL文にまとめたことと、サイト単位の完全な順序制御は同じではありません。
  • 台帳に残らない呼び出しがあります。記録はAI呼び出しが成功した後だけで、失敗した呼び出しは記録しない設計です。ただし、ページ目標の自動生成では、AIが応答した後に応答の読み取りで失敗するとエラーを返し、台帳に記録しません。費用は発生しているのに、台帳には残りません。
  • 検索用ベクトルの作成は上限の外でした。提案の2機能が内部で作るembeddingと、ベクトルを作り直すAPIは、上限確認も台帳への記録もしていませんでした。2026年9月に、作り直すAPIには上限確認と記録を、提案内部のembeddingには記録を加えています。
  • 予算用データベースが使えないときの動作も未確認です。処理を続ければ費用上限を守れず、止めれば利用者の作業を止めます。どちらを優先するかを決め、障害時のテストと通知を用意する必要があります。

だから、この実装は終点ではなく、費用を制御対象として扱い始めた最初の段階です。後続の記事で、請求データとの照合、固定費の特定、変更後の再計測へ進みます。この順番を飛ばすと、削減効果を数字で説明できません。

その後

この利用上限制御と台帳は、5月以降のコスト改善で使う計測・上限・記録の原形になりました。翌月にはCost Explorerが $0 を返す問題を調べ、その後「測定→修正→再測定」を記録に残す運用へ進みます。その考え方は、この4月にすでにありました。

一方、この仕組みが対象にするのはAIO HelperのAI API利用額です。Elastic Container Serviceの常時起動やNAT(Network Address Translation、内部ネットワークから外部への通信を中継する仕組み) Gatewayの固定費など、クラウド全体の請求は止められません。アプリケーションの外で発生する費用には、Cost Explorerや予算監視を別に用意する必要があります。

転用できる教訓

  • 未知は「高い」と見積もります。価格表に無いモデルを 0 円扱いにすると上限がすり抜けます。フォールバック価格は意図的に高くします。
  • 集計と書き込みを1文にまとめても、同時実行時の保証は別に検証します。厳密な上限が必要なら、実際のPostgresによる競合テストと、処理順を保証する仕組みが必要です。
  • 停止設定と残額警告は分けます。cap = 0(止まっている)を「もうすぐ」警告に混ぜません。
  • 請求書を読む前から1回ごとの費用を残します。呼び出し先とトークン数を添えてアプリ側で記録しておくと、後の削減戦略が数字で立てられます。

このクラウドコストとの戦いについての記事は、帰属・実測・構造ガード・定点観測の方法論を、順次シリーズとして公開していきます。 興味のある方は、ぜひ「いいね」と記事の購読をお願いいたします。

この方法論をもとにしたコスト管理サービス Costwary を https://costwary.com で提供しています。 そのほかの自社プロダクトは https://www.eln.ne.jp/products にまとめています。

筆者について

上原正吉。EarthLink Network Co., Ltd. でAI開発をしています。2025年からClaude Codeを開発の主体に据え、今は20を超えるプロダクトを1人で同時に開発・運用しています。この連載では、その現場で実際に起きたこと(うまくいったことも、失敗も)を、数字と一緒に書いていきます。

また、AIで業務や開発を組み替えたい会社・チーム向けに、AI活用のコンサルティングも受け付けています。ご相談は www.eln.ne.jp からどうぞ。


EarthLink Network は、会社の全業務を AI で回すために、必要になったものを自社で作っています。いま作っているプロダクトの一覧と概要は、こちらにまとめています。

→ EarthLink Network が自社でつくっている18のプロダクト

会社と各プロダクトの詳細は、公式サイト www.eln.ne.jp をご覧ください。

請求書より先にアプリが守り始めた — AIO HelperのAI cost-cap spend guard