月額 580 円でアプリを提供していた頃、ある月に一人のユーザーが AI チャット機能を 3,000 回以上使ったとき、正直焦りました。OpenAI API の請求を見ると、そのユーザー一人分だけで月額料金をゆうに超えていたんです。
固定のサブスクリプションモデルは、ユーザーにとって分かりやすくて良いのですが、AI 機能を提供する側にとっては「ライトユーザーに厚く、ヘビーユーザーに薄い」という歪みが生じます。この問題を根本的に解決するのが、Stripe が 2024 年後半に一般提供を開始した Meter Billing(使用量ベース課金) です。
Rork で作った AI 搭載アプリに Stripe Meter Billing を組み込み、ユーザーの実際の使用量に応じて公平に課金する仕組みを実装する全工程を順を追って整理していきます。
使用量課金が AI アプリに必要な理由
固定月額モデルには構造的な問題があります。
AI アプリでは、ユーザーによって使用量が 10 倍〜100 倍以上も変わることがざらにあります。チャットアシスタントアプリを例にとると、月に 50 回しか使わないユーザーと、毎日 200 回以上使うヘビーユーザーが同じ月額料金を支払うことになります。開発者側から見ると、ヘビーユーザーへの AI API コストが増大するほど赤字になっていきます。
使用量課金(Usage-based Billing)を導入すると、次のメリットが生まれます。
まず、コストの公平な分担 ができます。使えば使うほど課金される仕組みなので、開発者がコスト増大に怯える必要がなくなります。次に、低価格のエントリープランを設けやすく なります。「基本料金 300 円 + 使用量課金」にすることで、試したいユーザーのハードルを下げながら、ヘビーユーザーからは適切に収益化できます。そして、SaaS 的なビジネスモデルとの親和性 が高まります。API 提供型や B2B ツールへの展開も同じ課金基盤で対応できます。
私が実際に試したところ、使用量課金に移行してから解約率が下がりました。軽いユーザーの負担が減り、「少しだけ使いたい」層が定着するようになったからだと感じています。
Stripe Meter Billing の全体設計を理解する
実装を始める前に、Stripe Meter の仕組みを把握しておきましょう。
Stripe Meter は以下の 3 つのコンポーネントで構成されています。
Meter(メーター) : 何をカウントするかを定義するオブジェクト(例:AI API 呼び出し回数)
Meter Event(メーターイベント) : 実際の使用量を Stripe に報告するイベント(サーバーサイドから送信)
Price with Meter(メーター連携プライス) : Meter の累積値に基づいて請求する価格設定
アプリの流れはこうなります。
ユーザーがAI機能を使用
↓
Rorkアプリ → Cloudflare Workers(バックエンド)に使用量を報告
↓
Workers → stripe.billing.meterEvents.create() でStripeに送信
↓
Stripe が月末に累積使用量を集計して請求
↓
ユーザーのカードから自動引き落とし
重要なポイントは、使用量の報告はサーバーサイドからのみ行う ことです。Rork アプリ(クライアント)から直接 Stripe の Meter Event API を叩いてはいけません。Stripe の Secret Key がフロントエンドに露出するリスクがあるためです。必ず Cloudflare Workers などのバックエンドを経由させてください。
Step 1: Stripe ダッシュボードで Meter を設定する
Stripe ダッシュボード(Billing → Meters )を開き、「Create meter」をクリックします。
設定項目は以下の通りです。
Display name : AI API Calls(管理画面での表示名)
Event name : ai_api_call(コードから送信する際に使うキー名)
Value settings : Default(各イベントを 1 としてカウント)
AI のトークン数で課金したい場合は、Value settings で「Custom event payload value」を選択し、イベントのペイロード内の tokens_used フィールドの値を使うよう設定します。
Meter を作成すると mtr_xxxxxxxx という形式の ID が発行されます。この ID は後の価格設定で使用しますので、控えておいてください。
次に、このメーターに紐づく Price を作成します。Products から新しい Product を作成し、Price の設定で以下を選択します。
Billing period : Monthly
Pricing model : Usage-based(Graduated または Per unit)
Meter : 先ほど作成した AI API Calls を選択
Price : 1 回あたり ¥0.5(または $0.005)など
これで Stripe 側の準備は完了です。
Step 2: Cloudflare Workers でメーターイベントを報告する
Rork アプリから使用量を受け取り、Stripe に転送するバックエンドを実装します。
// cloudflare-workers/src/meter.ts
import Stripe from 'stripe' ;
interface Env {
STRIPE_SECRET_KEY : string ;
}
interface MeterEventBody {
customerId : string ;
eventName : string ;
value ?: number ; // トークン数など(省略時は1)
}
export async function handleMeterEvent (
request : Request ,
env : Env
) : Promise < Response > {
// リクエストボディのパース
let body : MeterEventBody ;
try {
body = await request. json ();
} catch {
return new Response ( JSON . stringify ({ error: 'Invalid JSON body' }), {
status: 400 ,
headers: { 'Content-Type' : 'application/json' },
});
}
// 必須フィールドのバリデーション
if ( ! body.customerId || ! body.eventName) {
return new Response (
JSON . stringify ({ error: 'customerId and eventName are required' }),
{
status: 400 ,
headers: { 'Content-Type' : 'application/json' },
}
);
}
const stripe = new Stripe (env. STRIPE_SECRET_KEY , {
apiVersion: '2024-11-20.acacia' ,
});
try {
// Stripe Meter Event を送信
const meterEvent = await stripe.billing.meterEvents. create ({
event_name: body.eventName, // 例: 'ai_api_call'
payload: {
stripe_customer_id: body.customerId,
// トークン数など追加データも送れる
... (body.value !== undefined && { value: String (body.value) }),
},
});
return new Response (
JSON . stringify ({ success: true , identifier: meterEvent.identifier }),
{
status: 200 ,
headers: { 'Content-Type' : 'application/json' },
}
);
} catch (error) {
// Stripe のエラーを適切に処理する
if (error instanceof Stripe . errors . StripeInvalidRequestError ) {
return new Response (
JSON . stringify ({ error: 'Invalid Stripe request' , details: error.message }),
{
status: 400 ,
headers: { 'Content-Type' : 'application/json' },
}
);
}
// 予期しないエラーはログに記録して 500 を返す
console. error ( 'Meter event error:' , error);
return new Response (
JSON . stringify ({ error: 'Internal server error' }),
{
status: 500 ,
headers: { 'Content-Type' : 'application/json' },
}
);
}
}
このコードのポイントは 2 点あります。一つ目は stripe_customer_id をペイロードに含めることです。これがないと Stripe はどの顧客の使用量か判断できません。二つ目は、エラーを Stripe.errors.StripeInvalidRequestError で個別にキャッチしていることです。Stripe のエラーと一般的なサーバーエラーを区別することで、クライアント側が適切にリトライできます。
Step 3: Rork アプリ側から使用量を報告する
Rork アプリ側では、AI 機能を使用するたびにバックエンドの /api/meter-event を呼び出します。
// Rork アプリの AI チャット機能(TypeScript)
interface ChatResponse {
message : string ;
tokensUsed : number ;
}
async function sendChatMessage (
userMessage : string ,
stripeCustomerId : string
) : Promise < ChatResponse > {
// 1. AI API を呼び出す(例: Gemini 2.5 Flash)
const aiResponse = await callGeminiAPI (userMessage);
// 2. 使用量をバックエンドに報告する
// ※ AI 呼び出しが成功してから報告する(失敗した呼び出しは課金しない)
try {
await reportUsage ({
customerId: stripeCustomerId,
eventName: 'ai_api_call' ,
value: aiResponse.tokensUsed,
});
} catch (reportError) {
// 使用量報告に失敗してもユーザーへのレスポンスは返す
// 失敗はログに記録してリトライキューに積む
console. warn ( 'Usage report failed, will retry:' , reportError);
await queueRetry ({ customerId: stripeCustomerId, tokensUsed: aiResponse.tokensUsed });
}
return {
message: aiResponse.content,
tokensUsed: aiResponse.tokensUsed,
};
}
async function reportUsage ( params : {
customerId : string ;
eventName : string ;
value ?: number ;
}) : Promise < void > {
const response = await fetch ( 'https://your-workers.your-domain.workers.dev/api/meter-event' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
// 内部APIの認証(環境変数から取得)
'Authorization' : `Bearer ${ process . env . EXPO_PUBLIC_INTERNAL_API_KEY }` ,
},
body: JSON . stringify (params),
});
if ( ! response.ok) {
const error = await response. json ();
throw new Error ( `Usage report failed: ${ error . error }` );
}
}
ここで重要なのは、AI 呼び出しが成功してから使用量を報告すること です。AI API がエラーを返した場合にも課金してしまうと、ユーザーから不満が出ます。実際に価値を提供できた分だけを報告するようにしてください。
また、使用量報告の失敗がユーザー体験を壊さないよう、try/catch で囲んで AI のレスポンスは必ず返すようにしています。報告失敗はリトライキューに積んで後から処理します(後述)。
Step 4: 基本料金+従量課金のサブスクリプションを設計する
純粋な従量課金だけでは収益が不安定になります。個人開発では「基本料金 + 使用量課金」のハイブリッドモデルが最も安定します。
Stripe でこのモデルを実現するには、一つのサブスクリプションに複数の Price を紐づけます。
// バックエンド: ハイブリッドサブスクリプションの作成
async function createHybridSubscription (
stripe : Stripe ,
customerId : string
) : Promise < Stripe . Subscription > {
return await stripe.subscriptions. create ({
customer: customerId,
items: [
{
// 基本料金(固定月額)
price: 'price_base_monthly_300yen' , // 月額 300 円
},
{
// 使用量課金(Meter 連携プライス)
price: 'price_meter_ai_calls' , // 1 呼び出しあたり ¥0.5
// Meter Price は quantity 不要(自動集計される)
},
],
payment_behavior: 'default_incomplete' ,
payment_settings: {
save_default_payment_method: 'on_subscription' ,
},
expand: [ 'latest_invoice.payment_intent' ],
});
}
このモデルでは、ユーザーは最低でも月 300 円を支払い、AI 機能を使った分だけ追加課金されます。「基本料金があるから少しくらい使わないと損」という心理が継続率向上に寄与します。
価格設計の目安として、私が試した例を共有します。
基本料金 : ¥300/月(ライトプラン)または ¥580/月(スタンダードプラン)
無料枠 : 基本料金に含まれる 100 回/月
超過分 : 1 回あたり ¥1(または 100 トークンあたり ¥0.1)
上限設定 : ¥3,000/月のスペンドキャップ(Billing Controls で設定可能)
スペンドキャップは Stripe ダッシュボードの「Billing Controls」から設定できます。ヘビーユーザーへの無限課金を防ぎ、ユーザーに安心感を与えるために必ず設定することをお勧めします。
Step 5: 無料枠の実装(Graduated Pricing)
基本料金に含まれる無料枠は、Stripe の Graduated Pricing を使って実装します。
Stripe ダッシュボードで Meter に紐づく Price を作成する際、「Graduated pricing」を選択します。
第1階層: 0〜100回 → ¥0/回(無料枠)
第2階層: 101〜500回 → ¥0.5/回
第3階層: 501回以上 → ¥0.3/回(ボリュームディスカウント)
コード側では特に変更は不要です。Stripe が月末に自動的に階層を計算して請求してくれます。
注意点として、無料枠はサブスクリプションプランごとに設定する のが理想です。ライトプランは 100 回無料、プレミアムプランは 500 回無料、という設計にすることでプラン間の明確な差別化ができます。
Step 6: 使用量のリアルタイム表示(ユーザー向け)
ユーザーが自分の使用量をリアルタイムで把握できる UI を提供することで、課金に対する不安を減らせます。
// バックエンド: 今月の使用量を取得する
async function getCurrentMonthUsage (
stripe : Stripe ,
customerId : string ,
meterId : string
) : Promise <{ used : number ; limit : number | null }> {
const now = new Date ();
const startOfMonth = new Date (now. getFullYear (), now. getMonth (), 1 );
// Stripe Meter Summary からデータを取得
const meterSummary = await stripe.billing.meters. listEventSummaries (meterId, {
customer: customerId,
start_time: Math. floor (startOfMonth. getTime () / 1000 ),
end_time: Math. floor (now. getTime () / 1000 ),
value_grouping_window: 'month' ,
});
const totalUsed = meterSummary.data. reduce (
( sum , summary ) => sum + summary.aggregated_value,
0
);
return {
used: totalUsed,
limit: null , // スペンドキャップから取得する場合は別途実装
};
}
Rork アプリ側では、ホーム画面やプロフィール画面にこの使用量を表示します。「今月 47 / 100 回使用済み」のようなプログレスバーがあるだけで、ユーザーの安心感が大きく変わります。
よくある落とし穴と対処法
Stripe Meter Billing の実装で私が詰まったポイントを 3 つ紹介します。
落とし穴 1: Meter Event が重複報告される
ネットワークエラーによるリトライや、ダブルタップによる 2 回送信で、同じ使用量が重複して報告されることがあります。
Stripe には Idempotency Key という仕組みがあります。同じ Idempotency Key で複数回リクエストを送っても、Stripe は 1 回だけ処理します。
// 重複排除のための Idempotency Key 生成
async function reportUsageWithIdempotency ( params : {
customerId : string ;
sessionId : string ; // AI セッションごとのユニーク ID
tokensUsed : number ;
}) : Promise < void > {
// sessionId を使って冪等キーを生成する
const idempotencyKey = `meter_${ params . customerId }_${ params . sessionId }` ;
const stripe = new Stripe (env. STRIPE_SECRET_KEY );
await stripe.billing.meterEvents. create (
{
event_name: 'ai_api_call' ,
payload: {
stripe_customer_id: params.customerId,
value: String (params.tokensUsed),
},
},
{
idempotencyKey, // これで重複を防ぐ
}
);
}
AI セッション(1 回の会話)ごとにユニークな sessionId を生成し、それを Idempotency Key に含めることで重複報告を防げます。
落とし穴 2: テスト環境でメーターが集計されない
開発中に気づきやすい問題ですが、Stripe のテストモードでは Meter Event の集計には最大 5〜10 分のラグがあります。送信後すぐに使用量を確認しようとしても、0 のままになっていることがあります。
Stripe ダッシュボードの「Developers → Events」から、送信した Meter Event が届いているかを先に確認してください。Event が届いていれば、数分後に集計に反映されます。
もう一つ注意したいのが、テストモードと本番モードで Meter の ID が異なる点です。必ず環境変数で切り替えるようにしてください。
const METER_ID = process.env. NODE_ENV === 'production'
? 'mtr_prod_xxxxxxxxxx'
: 'mtr_test_xxxxxxxxxx' ;
落とし穴 3: Customer ID の管理が分散する
Rork アプリ内の User ID と Stripe の Customer ID は別物です。サインアップ時に Stripe Customer を作成し、アプリの User ID と紐づけて保存しておかないと、後で「どの Stripe Customer にメーターを報告すればいいか分からない」事態になります。
// サインアップ時の処理(Supabase の例)
async function onUserSignUp ( userId : string , email : string ) : Promise < void > {
const stripe = new Stripe (env. STRIPE_SECRET_KEY );
// Stripe Customer を作成
const customer = await stripe.customers. create ({
email,
metadata: {
app_user_id: userId, // アプリ内の User ID を紐づける
},
});
// Supabase の users テーブルに保存
await supabase
. from ( 'users' )
. update ({ stripe_customer_id: customer.id })
. eq ( 'id' , userId);
}
サインアップ時に必ず Stripe Customer ID を作成・保存する設計にしておくことで、後の混乱を防げます。このマッピングテーブルが Meter Billing の核心なので、サービス開始前に設計を固めておくことをお勧めします。
報告失敗を取りこぼさない — リトライキューの実装
Step 3 のコードで、報告失敗は「リトライキューに積む」とだけ書きました。ここではその中身を具体化します。
私自身、最初は「失敗したらログに残して、あとで手で補填すればいい」と考えていました。しかし実際に運用してみると、報告失敗は深夜のネットワーク断や Stripe 側の一時的な 5xx でまとまって発生します。1 件ずつの手作業での補填は、すぐに現実的でなくなりました。
Cloudflare Queues を使うと、Workers の中だけでリトライの仕組みを完結できます。まず wrangler.toml にキューを定義します。
# wrangler.toml
[[ queues . producers ]]
queue = "meter-retry"
binding = "METER_RETRY"
[[ queues . consumers ]]
queue = "meter-retry"
max_batch_size = 10
max_retries = 5
dead_letter_queue = "meter-retry-dlq"
Step 2 のハンドラで Stripe への送信が失敗したら、catch 節からキューへ積みます。
// 送信失敗時にキューへ積む(Step 2 の catch 節から呼ぶ)
await env. METER_RETRY . send ({
customerId: body.customerId,
eventName: body.eventName,
value: body.value,
idempotencyKey, // 初回送信と同じキーを必ず引き継ぐ
failedAt: Date. now (),
});
コンシューマ側は、同じ Worker に queue() ハンドラを追加するだけです。
interface RetryPayload {
customerId : string ;
eventName : string ;
value ?: number ;
idempotencyKey : string ;
failedAt : number ;
}
export default {
async queue ( batch : MessageBatch < RetryPayload >, env : Env ) : Promise < void > {
const stripe = new Stripe (env. STRIPE_SECRET_KEY );
for ( const msg of batch.messages) {
try {
await stripe.billing.meterEvents. create (
{
event_name: msg.body.eventName,
payload: {
stripe_customer_id: msg.body.customerId,
... (msg.body.value !== undefined && { value: String (msg.body.value) }),
},
},
{ idempotencyKey: msg.body.idempotencyKey }
);
msg. ack ();
} catch {
msg. retry ({ delaySeconds: 300 }); // 5分後に再試行
}
}
} ,
} ;
設計の要点は 2 つあります。
一つ目は、冪等キーは初回送信の前に生成し、リトライでも同じキーを使い回す ことです。リトライのたびにキーを作り直すと、「初回のリクエストは実は Stripe に届いていた」というケースで同じ使用量が二重に計上されます。落とし穴 1 で紹介した sessionId ベースのキーを、キューのペイロードに乗せてそのまま運ぶのが安全です。
二つ目は、max_retries を使い切って DLQ(デッドレターキュー)に落ちた分は課金しない と先に決めておくことです。取りすぎてユーザーの信頼を失うより、取り漏らして自分が少し損をする方を選びます。DLQ の件数は週次で確認し、恒常的に増えていくようであれば送信側の実装かペイロードの形式を疑ってください。
送信回数を絞る — 高頻度イベントの集約設計
チャットアプリで「1 メッセージ = 1 イベント」をそのまま送る設計は、ユーザー数が少ないうちは何の問題もありません。ただ、利用が伸びてくると Stripe API のレート制限と Workers の呼び出し回数が気になり始めます。伸びてから直すのは大変なので、集約の設計だけは先に知っておくと安心です。
方針はシンプルで、Stripe に送る前に自分の側で集計してしまう ことです。Durable Object にユーザーごとのカウンタを持たせ、アラームで 5 分ごとにまとめて 1 イベントとして送ります。
// Durable Object: ユーザーごとの使用量を貯めて、5分ごとにまとめて送る
export class MeterBuffer {
constructor (
private state : DurableObjectState ,
private env : Env
) {}
async fetch ( request : Request ) : Promise < Response > {
const { customerId , value } = await request. json <{
customerId : string ;
value : number ;
}>();
// ストレージ上のカウンタに加算(DO の休止・再起動に耐える)
const key = `count:${ customerId }` ;
const current = ( await this .state.storage. get < number >(key)) ?? 0 ;
await this .state.storage. put (key, current + value);
// アラーム未設定なら 5 分後の flush を予約
if (( await this .state.storage. getAlarm ()) === null ) {
await this .state.storage. setAlarm (Date. now () + 5 * 60 * 1000 );
}
return new Response ( 'ok' );
}
async alarm () : Promise < void > {
const stripe = new Stripe ( this .env. STRIPE_SECRET_KEY );
const windowId = Math. floor (Date. now () / ( 5 * 60 * 1000 )); // 5分窓のID
const entries = await this .state.storage. list < number >({ prefix: 'count:' });
for ( const [ key , total ] of entries) {
const customerId = key. slice ( 'count:' . length );
await stripe.billing.meterEvents. create (
{
event_name: 'ai_api_call' ,
payload: {
stripe_customer_id: customerId,
value: String (total),
},
},
{ idempotencyKey: `flush_${ customerId }_${ windowId }` }
);
await this .state.storage. delete (key);
}
}
}
flush 中に Stripe への送信が失敗した場合は、前のセクションで作ったリトライキューにそのまま接続できます。カウンタをメモリではなくストレージに置いているのは、Durable Object が休止・再起動してもカウントが消えないようにするためです。
トレードオフも正直に書いておきます。集約すると、ユーザー向けのリアルタイム使用量表示が最大で集約窓の分だけ遅れます。表示用のカウンタはアプリ側(またはバックエンドの KV)で別に持ち、Stripe のメーターは「請求の真実」、アプリ内カウンタは「表示の速報値」と役割を分けるのが私の落としどころです。集約窓を大きくしすぎると請求期間の締めをまたぐ事故が起きやすくなるため、5〜15 分程度に収めておくのが扱いやすいと考えています。
実用的な応用例
AI チャットアプリ
チャットアプリでは「メッセージ 1 通 = 1 イベント」として課金するシンプルな設計がユーザーに理解されやすいです。トークン数での課金は正確ですが、ユーザーには直感的に分かりにくいため、見せ方を工夫する必要があります。
表示: 「今月の使用: 47 / 100 メッセージ(無料枠)」
課金: 超過分は 1 メッセージあたり ¥1
画像生成アプリ
画像生成は 1 枚の処理コストが高いため、「生成 1 回 = 5 単位」のように重みをつけた報告が有効です。
// 画像生成の場合は value を 5 に設定
await reportUsage ({
customerId: stripeCustomerId,
eventName: 'ai_api_call' ,
value: 5 , // テキスト生成の5倍コストとして換算
});
この設計にすると、テキスト・画像・動画など異なる処理を同一の Meter で管理しながら、コストを適切に反映した課金ができます。
音声認識・要約アプリ
音声ファイルの長さに応じた課金に使えます。1 分の音声を 1 単位として Meter Event を送信し、長い録音ほど多く課金する設計です。
const durationMinutes = Math. ceil (audioFileDurationSeconds / 60 );
await reportUsage ({
customerId: stripeCustomerId,
eventName: 'ai_api_call' ,
value: durationMinutes,
});
前払いクレジットという選択肢 — 従量課金とどちらを選ぶか
ここまで従量課金の実装を進めてきましたが、公平な課金を実現する方法はもう一つあります。「100 回分クレジット ¥500」のように先にまとめて購入してもらう、前払いクレジット方式です。どちらを選ぶかはアプリの配信形態でほぼ決まるので、判断材料を表に整理しておきます。
観点 従量課金(Stripe Meter) 前払いクレジット(消耗型 IAP)
収益のタイミング 後払い(月末にまとめて請求) 先払い(購入時に確定)
ユーザー心理 「今月いくらになるか」の不安が出やすい 使い切り型で金額が先に見える安心感
iOS 単体アプリでの利用 ガイドライン 3.1.1 の制約で使える場面が限られる App Store の標準的な仕組みに乗れる
実装の中心 メーター報告と集計の信頼性設計 残高管理・復元・失効ポリシーの設計
未使用分の扱い そもそも発生しない 失効や返金への方針決めが必要
B2B・Web への展開 請求書払い・API 課金へ自然に拡張できる Web では別の決済導線を作る必要がある
私の使い分けはこうです。iOS アプリ内で完結するデジタル機能なら消耗型 IAP のクレジット方式、Web 版や B2B の API 提供があるなら Stripe Meter 。アプリ審査の観点では、アプリ内で消費されるデジタルサービスの課金は In-App Purchase を経由するのが原則なので、モバイル単体のアプリで Stripe Meter を選ぶ理由はあまりありません。逆に Web と併売するなら、内部の使用量カウンタを 1 つに統一した上で、アプリはクレジット残高から引き、Web ユーザーは Meter で後払い、という二本立てが運用しやすい形でした。
前払い方式には収益面の利点もあります。購入時点で売上が確定するため、月末まで請求額が読めない従量課金と比べて資金繰りの見通しが立てやすくなります。個人開発で AI の API 原価を先に支払っている身としては、この「先にもらえる」性質は想像以上に効きました。
使用量課金導入後に備えるべきこと
実装が完了したら、次の 2 点を必ず確認してください。
一つ目は Billing Alerts(請求アラート)の設定 です。Stripe ダッシュボードから、特定の閾値を超えた際にメールで通知を受け取れます。ユーザーの使用量が急増した場合に素早く対応できるようになります。
二つ目は 利用規約とアプリ内での事前説明 です。使用量課金はユーザーにとって「いくら取られるか分からない」不安感が生まれやすいです。アプリのオンボーディング画面で「100 回まで無料、超過分は 1 回 ¥1」と明示し、使用量がリアルタイムで確認できる画面を用意してください。
Stripe Meter Billing を実装したアプリを App Store に提出する際は、App Store Review Guidelines の Section 3.1.1 に従い、デジタルサービスの購入には In-App Purchase を経由する必要がある点にも注意が必要です。Web アプリや B2B 利用など、対象外となるケースも多いので、事前に確認してください。
詳細な Stripe API の使い方についてはRork × Cloudflare Workers での REST API 構築ガイド も参考にしてください。また、Stripe サブスクリプションの基礎についてはRork × Stripe サブスクリプション実装 で詳しく解説しています。
運用が始まってから見る 3 つの数字
課金が動き始めたら、週次で見る数字を 3 つに絞っています。
一つ目はユーザーごとの原価と収益の比率 です。AI API の請求(OpenAI・Gemini・Anthropic のダッシュボード)と、Stripe のメーター集計から得られる従量収益を突き合わせます。私は「収益 1 に対して原価 0.4」を警戒ラインにしていて、これを超えて推移するようなら単価か無料枠の見直しを検討します。
// 今月の原価率を計算する(週次バッチで実行)
function costRatio ( aiApiCost : number , meteredRevenue : number ) {
const ratio = aiApiCost / meteredRevenue;
return {
ratio,
status: ratio < 0.4 ? 'healthy' : ratio < 0.6 ? 'warning' : 'critical' ,
} as const ;
}
二つ目は無料枠の消化率 です。無料枠の 80% に到達したユーザーにアプリ内で「今月 80 / 100 回を使用しました」と知らせると、月末の請求で驚かせることがなくなり、上位プランへの自然な導線にもなります。
三つ目は異常値の検知 です。平均の 10 倍を使うユーザーは、熱心なファンか、自動化による濫用かのどちらかです。Stripe の Billing Alerts を平均使用額の 3 倍あたりに張っておき、発火したら使用パターンを確認します。一日を通して分散しているなら人間、数分間に集中しているなら Bot を疑う、という見方をしています。
次のステップ
使用量課金の導入に興味を持った方は、まず Stripe のテスト環境で Meter を一つ作成してみることをお勧めします。Stripe ダッシュボードから 5 分で作れますし、テスト用のイベントを手動で送信して集計の挙動を確認できます。実際に動きが見えると、実装への不安がかなり減るはずです。
使用量課金は「ユーザーに公平、開発者に持続可能」なモデルです。AI 機能を搭載したアプリを運営するなら、早めに設計を考えておく価値があると思います。