個人開発SaaSの従量課金設計2026 — Stripe Meter API で「使った分だけ課金」を5ファイルで実装する
結論:5ファイルで従量課金が動く
| ファイル | 役割 |
|---|---|
| Stripe Dashboard | Meter と従量課金 Price を設定 |
supabase/migrations/add_usage_events.sql | 使用量を記録するテーブル |
app/api/usage/record/route.ts | 使用イベントを Stripe と Supabase 両方に送信 |
app/api/stripe/webhook/route.ts | 請求確定イベントを Supabase に反映 |
components/UsageMeter.tsx | ダッシュボードに現在の使用量を表示 |
月額固定では「安すぎてヘビーユーザーに使い倒される」か「高すぎてライトユーザーが離脱する」の二択になります。従量課金はこのジレンマを解消し、プロダクトの提供価値と収益をほぼ比例させられます。
masatoman.net では Claude Crew Lab(Free MVP)を運用中です。Standard(¥1,980)・Premium(¥4,980)はローンチ準備段階のため、従量課金の実運用データはまだありません。この記事の実装は Stripe 公式ドキュメントおよびテスト環境での動作確認をベースにしています。業界参考値(Stripe・a16z「State of Cloud 2024」等)として、AI SaaS での従量課金採用率は2024年時点で約45%まで上昇しています。
この記事でわかること:
- Stripe Meter API の仕組みと月額固定との違い
- Supabase で使用量を二重管理する理由
- AI トークン消費を課金指標にする実装手順
- 従量課金が向くプロダクト・向かないプロダクトの判断基準
- 「で、どう稼ぐ?」= 従量課金で収益上限を取り除く設計思想
なぜ月額固定だと限界が来るのか
個人開発SaaSの月額固定が抱える3つの問題
① コストとの乖離: AIの推論コストやAPIコールは使用量に比例します。ヘビーユーザー1人が月500回使っても、ライトユーザーが月10回使っても月額料金が同じなら、ヘビーユーザーへの提供コストが赤字になるリスクがあります。
② 価格設定の難しさ: 「月いくら取ればいいか」を決めるには、平均使用量の見積もりが必要です。フェーズ0のプロダクトでは実績データがなく、価格を外すと転換率に直撃します。
③ 上限スケールの問題: ヘビーユーザーが生まれても月額固定では売上が頭打ちになります。一方、使った分だけ課金する設計なら、利用量が増えるほど収益も増えます。
従量課金が解決すること
従量課金(メータリング課金)は「使った分だけ払う」モデルです。AWS・OpenAI・Vercel など多くのインフラサービスが採用しているのはこのためです。個人開発SaaSでも以下のようなケースで有効です。
- AI機能内蔵のSaaS: トークン消費やAPIコール数を課金指標にする
- ストレージ提供型: アップロードファイル数やGB数を指標にする
- レポート・分析型: 生成レポート数・実行クエリ数を指標にする
Step 0:Stripe ダッシュボードで Meter を作成する
Stripe の従量課金は Meter という概念を中心に動きます。
Meter(計測器の定義)
└── MeterEvent(実際の使用記録、API で送信)
└── UsageRecord(請求書生成時に集計される)
Stripe ダッシュボードで Billing → Meters → Create meter を開き、以下を設定します。
| 項目 | 設定例 |
|---|---|
| Meter name | ai_tokens |
| Event name | ai_token_consumed(API 送信時に使う文字列) |
| Aggregation | SUM(トークン数の合計)または COUNT(イベント回数) |
次に Products から新しい Price を作成します。
- Billing type:
Graduated pricingまたはPer unit - Unit: メーターに紐付け(
ai_tokens) - Price: 例
¥0.05 / 1,000トークン
Step 1:Supabase に使用量テーブルを追加する
Stripe だけで管理すると「現在の使用量をリアルタイムでUIに表示する」が難しくなります。Supabase に二重管理することで、ダッシュボード表示とコスト上限チェックが容易になります。
-- supabase/migrations/add_usage_events.sql
CREATE TABLE IF NOT EXISTS usage_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
event_type text NOT NULL,
quantity integer NOT NULL DEFAULT 1,
metadata jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON usage_events (user_id, event_type, created_at DESC);
月次集計ビューも作っておくと、ダッシュボードのクエリが楽になります。
CREATE OR REPLACE VIEW usage_monthly AS
SELECT
user_id,
event_type,
date_trunc('month', created_at AT TIME ZONE 'Asia/Tokyo') AS month,
SUM(quantity) AS total_quantity
FROM usage_events
GROUP BY user_id, event_type, date_trunc('month', created_at AT TIME ZONE 'Asia/Tokyo');
Step 2:使用イベントを Stripe と Supabase 両方に記録する
アプリ側でAI機能を呼んだあと、このエンドポイントを叩くだけで従量課金が動きます。
// app/api/usage/record/route.ts
import { stripe } from '@/lib/stripe'
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function POST(req: Request) {
const supabase = createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const { eventType, quantity, metadata } = await req.json()
const { data: profile } = await supabase
.from('profiles')
.select('stripe_customer_id')
.eq('id', user.id)
.single()
const promises: Promise<unknown>[] = []
// 1. Supabase に記録(UI表示・上限チェック用)
promises.push(
supabase.from('usage_events').insert({
user_id: user.id,
event_type: eventType,
quantity,
metadata,
})
)
// 2. Stripe Meter に送信(課金計算用)
if (profile?.stripe_customer_id) {
promises.push(
stripe.billing.meterEvents.create({
event_name: eventType,
payload: {
stripe_customer_id: profile.stripe_customer_id,
value: String(quantity),
},
})
)
}
await Promise.all(promises)
return NextResponse.json({ ok: true })
}
AI機能を提供するルートから呼ぶ例:
// app/api/ai/generate/route.ts(抜粋)
const completion = await openai.chat.completions.create({ ... })
const tokensUsed = completion.usage?.total_tokens ?? 0
// バックグラウンドで使用量を記録(レスポンスをブロックしない)
fetch('/api/usage/record', {
method: 'POST',
body: JSON.stringify({
eventType: 'ai_token_consumed',
quantity: tokensUsed,
metadata: { model: 'gpt-4o-mini' },
}),
})
Step 3:Webhook で請求確定イベントを処理する
// app/api/stripe/webhook/route.ts(従量課金関連部分)
case 'invoice.payment_succeeded': {
const invoice = event.data.object as Stripe.Invoice
const customerId = invoice.customer as string
const { data: profile } = await supabase
.from('profiles')
.select('id')
.eq('stripe_customer_id', customerId)
.single()
if (!profile) break
// 請求確定後に当月集計をリセットするフラグ等を管理
await supabase.from('billing_cycles').upsert({
user_id: profile.id,
period_start: new Date(invoice.period_start! * 1000).toISOString(),
period_end: new Date(invoice.period_end! * 1000).toISOString(),
amount_paid: invoice.amount_paid,
})
break
}
Step 4:ダッシュボードに使用量メーターを表示する
// components/UsageMeter.tsx
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
type Props = {
userId: string
eventType: string
limit?: number
label?: string
}
export function UsageMeter({ userId, eventType, limit, label }: Props) {
const [used, setUsed] = useState(0)
const supabase = createClient()
useEffect(() => {
const now = new Date()
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString()
supabase
.from('usage_events')
.select('quantity')
.eq('user_id', userId)
.eq('event_type', eventType)
.gte('created_at', monthStart)
.then(({ data }) => {
const total = (data ?? []).reduce((sum, r) => sum + r.quantity, 0)
setUsed(total)
})
}, [userId, eventType, supabase])
const pct = limit ? Math.min((used / limit) * 100, 100) : null
return (
<div className="space-y-1">
<div className="flex justify-between text-sm text-gray-600">
<span>{label ?? eventType}</span>
<span>{used.toLocaleString()}{limit ? ` / ${limit.toLocaleString()}` : ''}</span>
</div>
{pct !== null && (
<div className="h-2 w-full rounded bg-gray-200">
<div
className={`h-2 rounded transition-all ${pct >= 90 ? 'bg-red-500' : 'bg-blue-500'}`}
style={{ width: `${pct}%` }}
/>
</div>
)}
</div>
)
}
従量課金が向くプロダクト・向かないプロダクト
| プロダクト特性 | 従量課金との相性 |
|---|---|
| AI機能に使用量の差がある | ◎ トークン・APIコール数で計測しやすい |
| ストレージ・帯域依存 | ◎ 実コストと正比例させやすい |
| 利用頻度が均一で予測しやすい | △ 月額固定のほうがユーザーの不安が少ない |
| B2C / 個人ユーザー向け | △ 月末の請求額が読めないことを嫌うユーザーが一定数いる |
| B2B / チーム利用 | ○ 利用量に応じた請求が経費精算しやすい |
個人開発でまず試すならハイブリッド型が現実的です。「月XXX回まで無料(または定額)、超過分は従量」という設計なら、ユーザーに安心感を与えつつコスト増大を防げます。
で、どう稼ぐ?
従量課金の最大の強みは収益の上限がなくなることです。
月額固定では「Standard ¥1,980」のユーザーが何百回使っても売上は ¥1,980 のままです。一方、AIトークンを ¥0.05/1,000トークンで課金するなら、ヘビーユーザーが100万トークン消費すれば ¥50,000 の売上になります。
ただし、個人開発フェーズ0での注意点があります。
- 無料ユーザーのコスト管理: Free ユーザーには使用上限を必ず設ける。Supabase の
usage_eventsテーブルで月次集計し、上限を超えたら機能をゲートする。 - Stripe の請求最小金額: Stripe は ¥50 未満の請求を発行しないため、使用量が少ないユーザーへの月次請求がゼロになることがある。対策として「ベース料金(月 ¥980)+ 超過従量」のハイブリッドが有効です。
- コスト連動の価格設定: OpenAI/Claude APIのコストは変動します。
価格 > コスト × 3を最低ラインとして設計することで、価格改定前にマージンを確保できます。
収益導線として:
- Free → 月XXX回上限に到達 → UsageMeter でビジュアル化 → Standard 課金への自然な誘導
- 超過した回数が多いほどアップグレードの動機付けになる
まとめ:今日できる3つのアクション
- Stripe ダッシュボードで Meter を作成する(10分):
Billing → Meters → Create meterでイベント名を定義 usage_eventsテーブルをマイグレーションする(5分): 上記 SQL をそのまま適用- AI機能の呼び出し元に
fetch('/api/usage/record', ...)を1行追加する(5分): バックグラウンドで送信するため既存のレスポンス速度に影響しない
従量課金は「設計が複雑そう」と後回しにされがちですが、Stripe Meter API を使えば5ファイルで実現できます。フェーズ0のいま実装しておけば、ユーザーが増えたときに収益の上限を自分で外せます。
Next Step
次に読むならこの導線です
【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード
Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude Crew」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。
【第6回】Claude Code で SaaS を 1 週間で組む開発フロー — Skill × MCP の統合設計
Skills、MCP、サブエージェント、Hooks を統合した個人開発の SaaS 構築フローを解説。LP 公開から課金開始までを 1 週間で組み立てるパイプラインの設計と実装を、月 500 万トークン運用の筆者が公開。「売れるかどうか」は別途検証が必要なので、本記事は「作る速度を上げる」フローに特化しています。
【第10回】Claude Code × Supabase で管理画面を30分で生成する Skill 実装ガイド
Supabase のテーブルを参照して管理画面を自動生成する Skill を、実コード付きで解説。Claude Code が DB スキーマから Next.js 管理画面を30分で生成する仕組みを公開します。
【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード
Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude Crew」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。
次の実験記録も追う
Claude Code × 個人開発の実験ログ、失敗、判断変更をまとめて追いたい人向けに、月次でLab Freeを届けます。
個人開発の実験ログを月1回、無料で
失敗 / 実数字 / 仮説 / 次に試すこと。売れた話だけでなく売れなかった理由も共有します。
この記事が役に立ったらシェア