02プロダクト5 min

クレジット、残高、支出上限

五つのプロダクトで従量課金を同じように作りました。クレジットから始めて、最後はセント単位の残高とキーごとの上限に落ち着きます。もうどの段階から始めるべきか分かっています。

著者: Salva Castells

カバー画像: Federico Candoni, CC BY-SA 4.0.
この記事の内容

プロダクトが AI を消費するとき、最初に作るのはいつもクレジットです。プロフィールのテーブルに整数をひとつ置き、生成ごとに一減らす。それだけです。文字起こしのアプリでも、画像生成の SaaS でも、プロジェクトの土台にしているボイラープレートでもやりました。三回とも同じ場所にたどり着きました。小数を持つセント単位の残高と、API キーごとの支出上限です。

道筋はいつも同じなので、どの段階から始めるのがよいかはもう分かっています。

クレジットが最初はいつも十分に見える理由

操作あたりのコストが一定である限り、実際に十分だからです。画像一枚は画像一枚のコストです。無料なら一日一クレジット、サブスクなら二十。check は一行です。

if (!profile || profile.subCredits < estimatedCost) {
  return NextResponse.json({ error: "Insufficient credits" }, { status: 402 });
}

ランディングページでも説明なしで伝わります。整数なので扱いも楽です。丸めはなく、ユーザーは「残り 12」と見て、それ以上は聞いてきません。

このモデルが足りなくなる最初の兆候は技術的なものではなく、価格の話です。ある文字起こしのプロジェクトでは文字数で課金していて、公開から二週間後に一文字あたりの価格をプロバイダのコストの六倍に引き上げました。英語の一文の平均文字数(約 75)とスペイン語の平均(約 90)を数え、2 ドルのパッケージで何文になるかを計算したうえでの判断です。この計算は最初にはやりません。プロバイダの請求書が届き、思っていた利幅ではなかったと気づいてからやります。

実コストが絡んだ途端に壊れるところ

壊れるのは、プロダクトがモデルを二つ以上持った瞬間です。安い文字起こしモデルと精度の高いモデルをユーザーが選べるなら、一クレジットは意味を失います。高いほうを安い扱いで課金して損をするか、すべてを高い価格で課金して安いモデルの存在意義をなくすかのどちらかです。

クレジットの財布が二つになると、もう一度壊れます。画像の SaaS ではサブスクのクレジットの上に無料の日次クレジットを足し、その日のうちに何も生成できない問題を直す羽目になりました。確認は依然として subCredits しか見ていなかったのです。修正は、比較する前に二つの財布を足すことでした。

const totalAvailableCredits = (profile?.dailyCredits || 0) + (profile?.subCredits || 0);

読めば当たり前に見えます。新しい財布があるコミットで追加され、確認が別のファイルにあると、当たり前ではなくなります。

残高がクレジットでは解けなかったものを解く

残高はセント単位の数値で、操作ごとの実コスト分だけ引かれます。おかげでモデル、プロバイダ、そして異なる単位(音声の分数とテキストのトークン)を同じ勘定に混ぜられます。

予想していなかった細部があります。整数のセントでは足りません。安いモデルでの短い文字起こしは一セント未満のコストで、integer だと呼び出しごとにゼロに丸められ、残高が一向に減りませんでした。カラムを浮動小数点に移行する必要がありました。

ALTER TABLE "usage_logs"   ALTER COLUMN "cost_cents"    SET DATA TYPE real;
ALTER TABLE "user_billing" ALTER COLUMN "balance_cents" SET DATA TYPE real;
ALTER TABLE "api_keys"     ALTER COLUMN "spend_limit_cents" SET DATA TYPE real;

はい、お金を real で持っています。引かれてチャージされるだけの前払い残高なら、これで割に合います。丸め誤差は、切り捨てで金額まるごと失うことに比べれば些細です。後払いで請求するなら、存在する最小単位の整数を使います。

残高は価格設定も変えました。「ひとつのプラン」から三つ(月 4.99、14.99、49.99 ユーロ。文字起こしの時間数とトークン数で表示)に、加えて 5、10、25 ユーロの単発チャージです。内部ではどれも同じことをしています。セントを足すだけです。

極端な例: 同じプロダクトに課金モデルが三つ

文字起こしのアプリでは、三つを同時に抱えたことがあります。全部が生き残ったわけではありません。

  • 残高。 残っている額。ゼロ以下なら何も提供しません。
  • ユーザーの月次上限。 既定 4.99 の monthly_limit_cents フィールドで、アカウント画面から編集できます。
  • API キーごとの支出上限。 spend_limit_cents と期間(daily、monthly、total)を、そのキーの利用ログの合計と突き合わせます。

ユーザーの月次上限は消しました。ALTER TABLE "user_billing" DROP COLUMN "monthly_limit_cents" を打ち、セレクタもサーバーアクションも五言語分の翻訳も削除しました。何の役にも立っていませんでした。前払い残高がすでに月次上限そのものだからです。20 ユーロ入れたユーザーが、加えて「20 ユーロ以上は使いたくない」と言う必要はありません。説明すべき画面がひとつ、確認すべき条件がひとつ増えるだけでした。

残したのはキーごとの上限です。こちらには実在の事例があります。スクリプトに入れたキーが暴走する場合です。そして痛い目を見て学んだ細部があります。認証で得たオブジェクトを持っていても、比較の前にキーを id でデータベースから読み直す必要があります。さもないと、古い spendLimitCents を基準にリクエストを処理してしまいます。

ガードは支出する側だけでなく、すべてのエンドポイントに置きます。モデル一覧やプリセットの取得にも入れて、429 を返します。残高がゼロの相手には、高い部分だけでなく API 全体が断るべきです。

いまなら初日にどう決めるか

コストが固定の操作がひとつだけなら、クレジット。モデルが複数あるか、コストが入力サイズに依存するなら、最初のコミットから小数付きのセント残高。あとから移行すると、スキーマに一晩、そして「あなたのクレジットはこれからユーロです」と説明するメールに一か月かかります。

モデルの話とは別に、もうひとつ。チェックアウトの割引クーポンは定数に置いて、テストします。ある日、couponId を「-100 ユーロ」から「-100%」に朝変えて、午後に戻しました。コミット二つ、同じファイル、一行です。

クレジットは、技術的判断のふりをしたマーケティングの判断です。残高はその逆です。