「ユーザーが Claude API を使えば使うほど、こちらの原価も比例して増えていく。でも料金は月額固定 — このままでは赤字になる SaaS を、自分は本気でリリースしようとしているのか?」
これは私が個人開発で API ベースの SaaS を立ち上げる前に、何度も自問した問いです。月額固定プランは設計が楽な代わりに、ヘビーユーザーが現れた瞬間に粗利が崩壊します。逆に純粋な従量課金は、ユーザー側の心理的ハードルが高く、契約率が落ちます。
ここではClaude API のトークン消費量を Stripe Meter Events で正確に集計し、月額ベースプラン + 上限超過分の従量課金というハイブリッドモデルを実装します。Cloudflare Workers + Stripe + Cloudflare KV を前提にしていますが、考え方は他のスタックでも応用できます。
なぜ Stripe Meter Events を選ぶのか — 自前カウンタとの違い
従量課金を実装する方法は、大きく分けて3つあります。自前のデータベースでカウンタを持って月末に課金する方法、Stripe の旧 Usage Records API を使う方法、そして Stripe Meter Events を使う方法です。
私が最終的に Meter Events を選んだ理由は、以下の3点に集約されます。
第1に、リアルタイム集計です。送信したイベントは数秒〜数分で Stripe Dashboard に反映され、ユーザーごとの消費量が即座に確認できます。自前カウンタの場合、月末バッチ処理で集計する必要があり、エラー発生時のリカバリが大変です。
第2に、冪等性が標準で備わっている点です。Meter Events は identifier フィールドで重複排除を制御できるため、Webhook の再送やリトライによる二重課金を防ぎやすい設計になっています。
第3に、Stripe Customer Portal との統合です。ユーザーが自身の消費量を見たい場合、Stripe 側のダッシュボードに誘導するだけで済みます。自前画面を作り込まなくてよいのは、個人開発者にとって大きなメリットです。
ただし、Meter Events には注意点もあります。料金の計算式は Stripe Dashboard 側で完結するため、複雑な階段料金や時間帯別レートを実装したい場合は、自前計算と組み合わせる必要があります。
全体アーキテクチャ — 5層に分けて理解する
実装に入る前に、全体像を5つの層で整理しておきます。各層の責務を明確に分けないと、エラー時に「どこで何が起きているのか」が追跡できなくなります。
第1層は Stripe Dashboard 側の設定 です。Meter(メーター)の作成、Price(価格)への紐付け、Product への組み込みまでを Stripe 側で完了させておきます。
第2層は API Gateway 層 です。クライアントから Claude API へのリクエストを受け、ユーザー認証・上限チェック・Claude API への中継を行います。
第3層は Claude API クライアント層 です。Anthropic SDK を使って Claude を呼び出し、レスポンス内の usage フィールドからトークン数を取り出します。
第4層は Meter Events 送信層 です。取得したトークン数を Stripe Meter Events として送信します。冪等性キーの生成と再送制御をここで行います。
第5層は モニタリング層 です。Stripe Webhook 経由で課金イベントを受け、KV に消費量履歴を保存し、ユーザー画面でリアルタイム表示します。
ステップ1: Stripe Dashboard で Meter を作る
Stripe Dashboard にログインし、「Billing → Meters」から新規メーターを作成します。私が運用している例では、以下のような設定にしています。
- Display name: Claude API トークン消費量
- Event name:
claude_token_usage
- Aggregation: Sum(合計値で課金)
- Payload key:
tokens(このキーの数値が課金対象)
- Customer mapping:
stripe_customer_id(イベント送信時にこのキーで顧客を特定)
メーターを作成したら、次に Product と Price を作ります。Price 作成時に「Meter pricing」を選択し、上で作成したメーターを紐付けます。料金は「100万トークンあたり $20」のような単価で設定します。
Tier 1: 0 〜 1,000,000 tokens → $0.00 (基本プラン込み)
Tier 2: 1,000,001 tokens 以上 → $20.00 / 1M tokens
ベースの月額 $30 + 1M トークンまで無料 + 超過分は従量、という構成にすると、ライトユーザーは固定費で利用でき、ヘビーユーザーには従量課金が発動します。
ステップ2: Claude API 呼び出しでトークン数を取得する
Claude API のレスポンスには、usage フィールドに入出力トークン数が含まれます。この値を取り出して Stripe に送信するのが、本記事の核心部分です。
// src/lib/claude-with-metering.ts
import Anthropic from "@anthropic-ai/sdk";
import { sendMeterEvent } from "./stripe-meter";
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY!,
});
export interface ClaudeRequest {
userId: string;
stripeCustomerId: string;
prompt: string;
model?: string;
}
export async function callClaudeWithMetering(req: ClaudeRequest) {
// 1. 上限チェック(KV から消費量を読み取る)
const usageThisMonth = await getMonthlyUsage(req.userId);
if (usageThisMonth > 5_000_000) {
// 5M トークン上限を超えたら強制停止(ランナウェイ防止)
throw new Error("Monthly token limit exceeded");
}
// 2. Claude API 呼び出し
const response = await anthropic.messages.create({
model: req.model ?? "claude-sonnet-4-6",
max_tokens: 2048,
messages: [{ role: "user", content: req.prompt }],
});
// 3. usage フィールドからトークン数を取り出す
const inputTokens = response.usage.input_tokens;
const outputTokens = response.usage.output_tokens;
const totalTokens = inputTokens + outputTokens;
// 4. Stripe Meter Events に送信(冪等性キー付き)
// response.id は Anthropic が発行するユニークIDなので、これを冪等性キーに使う
await sendMeterEvent({
customerId: req.stripeCustomerId,
tokens: totalTokens,
identifier: response.id, // 重複排除キー
});
// 5. KV にローカル消費量を加算(上限チェック用)
await incrementMonthlyUsage(req.userId, totalTokens);
return {
content: response.content,
usage: { inputTokens, outputTokens, totalTokens },
};
}
この関数で重要なのは、ステップ1の上限チェックです。Stripe 側のメーターは「課金するための集計」であって「上限制御」ではありません。Meter Events を送信し続けても Stripe は止めてくれないため、サービス側で必ず上限ロジックを実装する必要があります。
なぜ KV で別管理するのか
「Stripe API で当月使用量を取得すればよいのでは?」と思うかもしれませんが、API 経由の使用量取得は数秒〜数分の遅延があります。レート制限ロジックの判定材料としては不向きです。
KV や Redis などの低レイテンシストアに、リクエスト単位で即時に消費量を加算しておくことで、サブミリ秒で上限チェックができます。月初にリセットするバッチを別途用意するか、TTL を月初に合わせる設計にしておきます。
ステップ3: Stripe Meter Events 送信実装
Meter Events の送信は、シンプルに見えて落とし穴が多い処理です。冪等性、リトライ、エラー時の挙動を最初から正しく設計しておく必要があります。
// src/lib/stripe-meter.ts
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-10-28.acacia",
});
export interface MeterEventArgs {
customerId: string;
tokens: number;
identifier: string; // 冪等性キー
}
export async function sendMeterEvent(args: MeterEventArgs) {
let attempt = 0;
const maxAttempts = 3;
let lastError: unknown;
while (attempt < maxAttempts) {
try {
const event = await stripe.billing.meterEvents.create({
event_name: "claude_token_usage",
payload: {
stripe_customer_id: args.customerId,
tokens: args.tokens.toString(), // 文字列で送る
},
identifier: args.identifier, // 同じ identifier は1回しか集計されない
});
return event;
} catch (err) {
lastError = err;
attempt++;
// 指数バックオフ: 1秒 → 2秒 → 4秒
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt - 1)));
}
}
// 3回失敗したら、別途 Dead Letter Queue に保存して後でリトライ
// ここでエラーを throw してしまうと、Claude API は成功したのに課金できない状況になる
console.error("[CRITICAL] Meter event failed after 3 attempts", lastError);
await saveToDLQ(args);
return null;
}
冪等性キーの設計が最重要
identifier フィールドが従量課金システムの安全性を支えています。同じ identifier で複数回送信しても、Stripe 側で1回分しか集計されません。
私のおすすめは、Anthropic API レスポンスの response.id をそのまま使うことです。これは Anthropic 側で一意に保証されているため、リトライや並行処理で同じイベントが二度送信されても、二重課金になりません。
自前で UUID を生成する方法もありますが、その場合は「リクエスト送信前に UUID を生成して保存し、レスポンス取得後に同じ UUID で Stripe に送る」というフローにする必要があります。失敗時に新しい UUID を生成すると、二重送信になる可能性があるためです。
Dead Letter Queue(DLQ)の重要性
3回リトライしても失敗するケースは、ネットワーク問題や Stripe 側の障害で稀に発生します。このとき、エラーを上位に投げてしまうと、ユーザーは Claude のレスポンスを受け取れているのに、こちらは課金できていない状態になります(こちらの取りこぼし)。
DLQ(Cloudflare Queues や Durable Objects、または KV のリストでも可)に「未送信イベント」を蓄積し、別途バッチで再送する仕組みを必ず入れておきます。
ステップ4: 上限制御 — ランナウェイ請求を防ぐ
従量課金システムで最も恐ろしいのは、悪意のあるユーザーや無限ループバグによる青天井の API 消費です。「気づいたら1日で $10,000 課金されていた」という事態を防ぐため、3層の上限制御を入れておきます。
第1層は、ユーザー単位の月間上限です。これは KV で管理し、リクエスト時に即座に判定します。
第2層は、ユーザー単位の1分間レートリミットです。短時間の集中アクセスをブロックします。
第3層は、サービス全体の月間上限です。何らかの理由で第1層・第2層を突破された場合の最終防衛線です。
// src/lib/rate-limiter.ts
import type { KVNamespace } from "@cloudflare/workers-types";
interface RateLimiterArgs {
kv: KVNamespace;
userId: string;
tokens: number;
}
export async function checkAndIncrementUsage(args: RateLimiterArgs) {
const now = new Date();
const monthKey = `usage:${args.userId}:${now.toISOString().slice(0, 7)}`; // usage:user_xxx:2026-05
const minuteKey = `rate:${args.userId}:${now.toISOString().slice(0, 16)}`;
// 第1層: 月間上限(5M tokens)
const monthly = parseInt((await args.kv.get(monthKey)) ?? "0", 10);
if (monthly + args.tokens > 5_000_000) {
throw new Error("Monthly limit exceeded");
}
// 第2層: 1分間上限(50K tokens)
const perMinute = parseInt((await args.kv.get(minuteKey)) ?? "0", 10);
if (perMinute + args.tokens > 50_000) {
throw new Error("Rate limit: too many tokens per minute");
}
// 上限に当たらなければ加算
// expirationTtl で自動削除されるため、月初リセットバッチ不要
await args.kv.put(monthKey, String(monthly + args.tokens), {
expirationTtl: 35 * 24 * 60 * 60, // 35日後に自動削除
});
await args.kv.put(minuteKey, String(perMinute + args.tokens), {
expirationTtl: 120, // 2分後に自動削除
});
}
この実装で気をつけるべきは、KV の eventual consistency です。同じユーザーが同時に複数リクエストを送ると、両方とも上限以下と判定されてしまう可能性があります。完全な厳密性が必要な場合は、Cloudflare Durable Objects を使うか、Stripe 側の上限通知 Webhook と組み合わせます。
ステップ5: Webhook で課金状態を同期する
Stripe Meter Events を送信するだけでは、ユーザーが「今月いくら使ったか」を画面で見せられません。Stripe Webhook を受けて、KV に消費量サマリを保存する仕組みを追加します。
// src/api/stripe-webhook.ts (Cloudflare Workers / Hono 想定)
import type { Hono } from "hono";
import Stripe from "stripe";
export const handleStripeWebhook = async (c: any) => {
const stripe = new Stripe(c.env.STRIPE_SECRET_KEY, {
apiVersion: "2024-10-28.acacia",
});
const sig = c.req.header("stripe-signature");
const body = await c.req.text();
let event: Stripe.Event;
try {
// Cloudflare Workers では constructEventAsync を使う(同期版は動かない)
event = await stripe.webhooks.constructEventAsync(
body,
sig!,
c.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return c.json({ error: "Invalid signature" }, 400);
}
// 月次請求書イベントを購読して、当月使用量を KV に書き戻す
if (event.type === "invoice.created") {
const invoice = event.data.object as Stripe.Invoice;
const customerId = invoice.customer as string;
// line_items から meter 関連のライン情報を取得して KV に書き戻す処理
// ...
}
return c.json({ received: true });
};
Cloudflare Workers では stripe.webhooks.constructEventAsync を使う必要があります。同期版の constructEvent は内部で Node.js の crypto モジュールを使うため、Workers 環境ではエラーになります。
ステップ6: 本番運用のためのモニタリング
実装が一通り終わったら、運用時のモニタリングを整備します。最低限以下の3つは Stripe Dashboard と組み合わせて見られるようにしておきます。
- DLQ の滞留件数: 1件でも溜まったら即座にアラート
- 当月のメーターイベント送信失敗率: 0.1% 以上で要調査
- ユーザー別 token 使用量の異常値: 平均の10倍を超えるユーザーをハイライト
私の運用では、Cloudflare Analytics Engine にイベントログを書き出して、Grafana でダッシュボードを見ています。Stripe Dashboard 側のメーター表示と突合することで、「Stripe 側に届いていないイベント」をすぐに発見できます。
よくある間違い・落とし穴
実装中に私が実際にハマったポイントを共有します。
間違い1: tokens を数値で送ってしまう
Stripe Meter Events の payload は文字列のみ受け付けます。数値を送ると 400 エラーになります。tokens.toString() で文字列化することを忘れずに。
間違い2: response.id を保存していない
Claude API のレスポンスを取得後、すぐに別の処理に入って response.id を捨ててしまうと、Meter Events のリトライ時に冪等性キーが再生成されてしまいます。レスポンス受信直後に必ず保存すること。
間違い3: KV の expirationTtl 忘れ
KV の rate limit キーに TTL を付け忘れると、KV 容量が無限に膨張します。Cloudflare KV の無料枠は1GB、有料枠でも書き込みコストがかさむため、必ず TTL を設定すること。
間違い4: Stripe Webhook の冪等性未実装
Stripe Webhook は同じイベントを複数回送信することがあります。受信側で event.id をキーにした冪等性チェックを実装しないと、KV のサマリが二重加算されます。
間違い5: ローカル開発で Meter Events を本番メーターに送ってしまう
開発環境と本番環境で Stripe アカウントを分けるか、メーター名を dev_claude_token_usage のように分けることをお勧めします。私は最初にこれを怠り、開発時のテストイベントが本番メーターに混入する事故を起こしました。
月額固定 + 従量のハイブリッドプラン設計
最後に、私が運用しているハイブリッドプランの構成を共有します。
- Starter: $0/月 + 100K tokens まで無料、超過分は1Mあたり $25
- Pro: $30/月 + 2M tokens 込み、超過分は1Mあたり $20
- Business: $200/月 + 20M tokens 込み、超過分は1Mあたり $15
このように、月額が高いプランほど超過レートを安くすることで、ヘビーユーザーが上位プランに自然と移行する動線を作ります。
Stripe 側では、各プランごとに別の Price を作成し、メーターは共通でも tier ごとに料金を変える設定にします。Customer Portal で自由にプラン変更できるように設定すると、サポート工数も大きく減ります。
まずは Stripe テストモードで通しで動かしてみる
ここまでの実装を一気に本番投入するのはお勧めしません。まず Stripe テストモードで、テストカード(4242 4242 4242 4242)を使って一通りの流れを再現してください。
特に確認すべきは、3つあります。Meter Events が Dashboard に反映されること、月末に請求書が正しい金額で生成されること、そして DLQ にイベントが流れた場合のリカバリ処理が動くことです。この3点が確認できれば、本番投入の準備は整っています。
Customer Portal でユーザーに使用量を見せる
ユーザーが「今月いくら使ったか」を自分で確認できるようにすると、サポート問い合わせが大きく減ります。Stripe Customer Portal は、メーター付き商品にも対応しているため、設定だけで使用量と請求予定額をユーザーに表示できます。
// src/api/portal-session.ts
import Stripe from "stripe";
export const createPortalSession = async (
stripeCustomerId: string,
returnUrl: string
) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-10-28.acacia",
});
const session = await stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url: returnUrl,
});
// session.url にユーザーをリダイレクトする
return session.url;
};
Stripe Dashboard 側で「Settings → Customer portal」を開き、「Subscriptions の使用量を表示」を有効にしておきます。これで、ユーザーは自分の使用量と次回請求額をリアルタイムで確認できるようになります。
自前で使用量画面を作ることもできますが、初期段階では Stripe Portal で十分です。デザインの一貫性を求める段階になってから、独自実装に切り替えれば遅くありません。
返金とクレジットノートの扱い
従量課金で意外と詰まるのが返金処理です。Meter Events で送信したイベントは、過去にさかのぼって取り消すことはできません。返金が必要な場合は、Stripe 側でクレジットノート(Credit Note)を発行する形で相殺します。
// src/api/refund-meter-charges.ts
import Stripe from "stripe";
export const issueMeterRefund = async (
invoiceId: string,
refundAmountInCents: number,
reason: string
) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-10-28.acacia",
});
// クレジットノートで返金(次回請求時に相殺される)
const creditNote = await stripe.creditNotes.create({
invoice: invoiceId,
amount: refundAmountInCents,
credit_amount: refundAmountInCents,
reason: "duplicate", // duplicate / fraudulent / order_change / product_unsatisfactory
memo: reason,
});
return creditNote;
};
クレジットノートの作成と同時に、当月の KV カウンタも調整しておきます。返金分のトークンを差し引かないと、ユーザーから見た「今月の使用量」と請求金額がずれてしまうためです。
マルチテナント運用での注意点
複数組織が同じシステムを使う B2B SaaS の場合、メーターをどの単位で持つかを最初に決める必要があります。組織単位で1つのメーターにするか、組織内のユーザー単位で個別メーターにするかで、設計が大きく変わります。
私が推奨するのは、Stripe Customer は組織単位で1つだけ作り、内部の usage メタデータでユーザー別の内訳を持つ方法です。Stripe 上での請求は組織単位で集約され、組織内の利用配分は自前 DB で計算します。
この設計の利点は、Stripe Dashboard 側がシンプルに保てること、組織間のプラン変更がしやすいこと、そして「組織内で誰がどれだけ使ったか」を自由に集計できることです。欠点は、組織内ユーザーの上限制御を自前で実装する必要があることです。
メーターイベントのバッチ送信でコストとレイテンシを下げる
リクエストごとに毎回 Stripe API を叩くと、レイテンシが数十〜数百ミリ秒上乗せされます。高頻度な API では、これが体感性能を下げます。
対策として、メーターイベントを一定時間バッファして、1秒間隔などでまとめて送信するパターンが有効です。Cloudflare Workers では Durable Objects、AWS Lambda なら SQS + Lambda で実装できます。
// 概念的な実装スケッチ
class MeterEventBatcher {
private buffer: MeterEventArgs[] = [];
private timer: ReturnType<typeof setTimeout> | null = null;
add(event: MeterEventArgs) {
this.buffer.push(event);
if (!this.timer) {
this.timer = setTimeout(() => this.flush(), 1000);
}
if (this.buffer.length >= 100) {
this.flush();
}
}
private async flush() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
const batch = this.buffer.splice(0);
// 並列送信(順序は問わないが冪等性キーで重複は防げる)
await Promise.all(batch.map(sendMeterEvent));
}
}
ただし、バッファ中にプロセスがクラッシュすると、未送信イベントは失われます。重要な課金データは KV や DB にも一時的に書き込んでおき、起動時に未送信分をリプレイする仕組みも合わせて入れておきます。
ライセンス試算 — 損益分岐を可視化する
最後に、自分のサービスの損益分岐がどこにあるかを試算しておきます。これを最初に確認しないと、リリース後に「思ったより儲からない」と気付くことになります。
仮に「Pro プラン $30/月 + 2M tokens 込み + 超過1M あたり $20」という構成にした場合、Sonnet 4.6(出力1M で $15)を主力にするなら、原価は最大でユーザー1人あたり月 $30 程度です。粗利率は50%前後を確保できる計算になります。
逆に、ユーザー1人が月10M トークン使うヘビーユーザーになると、超過分の8M に対して $160 の追加課金が発生します。原価は8M × $15 / 1M = $120。差額の $40 が追加粗利です。プランがうまく機能している証拠と言えます。
この試算を Notion や Spreadsheet にまとめておき、ユーザー数の増加と粗利の変動を月次で追えるようにしておくと、価格改定のタイミングを見誤らずに済みます。