メインコンテンツへスキップ
← 記事一覧に戻る
·収益化·13 min read

個人開発SaaSの従量課金設計2026 — Stripe Meter API で「使った分だけ課金」を5ファイルで実装する

個人開発SaaSStripeSupabase収益化

結論:5ファイルで従量課金が動く

ファイル役割
Stripe DashboardMeter と従量課金 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 nameai_tokens
Event nameai_token_consumed(API 送信時に使う文字列)
AggregationSUM(トークン数の合計)または 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つのアクション

  1. Stripe ダッシュボードで Meter を作成する(10分): Billing → Meters → Create meter でイベント名を定義
  2. usage_events テーブルをマイグレーションする(5分): 上記 SQL をそのまま適用
  3. AI機能の呼び出し元に fetch('/api/usage/record', ...) を1行追加する(5分): バックグラウンドで送信するため既存のレスポンス速度に影響しない

従量課金は「設計が複雑そう」と後回しにされがちですが、Stripe Meter API を使えば5ファイルで実現できます。フェーズ0のいま実装しておけば、ユーザーが増えたときに収益の上限を自分で外せます。


Next Step

次に読むならこの導線です

すべての記事を見る
有料で次へ進む¥1,000

【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード

Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude Crew」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。

次の実験記録も追う

Claude Code × 個人開発の実験ログ、失敗、判断変更をまとめて追いたい人向けに、月次でLab Freeを届けます。

個人開発の実験ログを月1回、無料で

失敗 / 実数字 / 仮説 / 次に試すこと。売れた話だけでなく売れなかった理由も共有します。

この記事が役に立ったらシェア