このSDKが持つmeteredModel() というラッパーを streamText() の model に差し込むだけで、input/output の token 数が Stripe Meter Events として送られます。
この記事ではローカルのチャット UI で会話するたびに、Stripe ダッシュボードのメーター画面に input/output のレコードが届くところまでを試してみます。
Stripe ダッシュボードで Meter を作る
事前準備として Stripe ダッシュボードで Meter を 1 つ作っておきます。Stripe ダッシュボードの Billing → Meters → 新規作成と進み、 Event name に token-billing-tokens を指定します。
なぜこの名前は固定である必要があります。これは @stripe/ai-sdk@0.1.1 の実装(dist/meter/meter-event-logging.js)が event_name をハードコードでこの値にしているためです。受け側のメーターが同名で存在しない場合、 Stripe 側でメーターイベントを受け付けてくれません。
集計方法は Sum、 Customer mapping は stripe_customer_id、 Value setting は value フィールドを参照する設定にします。これで meteredModel() から飛んでくるイベントが受け取れる状態になります。
プロジェクトを作る
Vercel AI SDK は v6 系で API が大きく変わっています。StripeのAI SDKはV5を想定している様子で、useChat() の返り値が違う・ UIMessage の型が合わないといった詰まり方をしました。そこでライブラリのバージョンをv5系に固定します。念のためデモに使ったバージョンでのインストールでコマンドをメモしておきました。
npx create-next-app@latest demo-ai-chat-with-stripe --typescript --eslint --app
cd demo-ai-chat-with-stripe
npm install ai@5 @ai-sdk/react@2.0.154 @ai-sdk/anthropic@2.0.70 @stripe/ai-sdk@0.1.1 zod@4.3.6
続いてプロジェクト直下に .env を作ります。
ANTHROPIC_API_KEY=your_anthropic_api_key
STRIPE_SECRET_KEY=your_stripe_secret_key
Vercel AI SDK の公式 quickstart は .env.local と AI_GATEWAY_API_KEY を使う構成ですが、今回は Anthropic と Stripe の直接連携なので、扱うキーが少し変わります。
チャット UI を作る
アプリのセットアップができたので、app/chat.tsx を作成します。
'use client';
import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
export default function Chat() {
const [input, setInput] = useState('');
const { messages, sendMessage } = useChat();
return (
<div className="mx-auto flex w-full max-w-md flex-col py-24">
{messages.map(message => (
<div key={message.id} className="whitespace-pre-wrap">
{message.role === 'user' ? 'User: ' : 'AI: '}
{message.parts.map((part, i) => {
switch (part.type) {
case 'text':
return <div key={`${message.id}-${i}`}>{part.text}</div>;
default:
return null;
}
})}
</div>
))}
<form
onSubmit={e => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ text: input });
setInput('');
}}
>
<input
className="fixed bottom-0 mb-8 w-full max-w-md rounded border border-zinc-300 p-2 shadow-xl dark:border-zinc-800 dark:bg-zinc-900"
value={input}
placeholder="Say something..."
onChange={e => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}
app/page.tsx はこのコンポーネントを置くだけにします。これはチャットコンポーネントがクライアント側で実行される想定のため、SSRまたはSSGされるPageコンポーネント側との依存を減らすためです。
import Chat from './chat';
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col px-16 py-32 dark:bg-black">
<Chat />
</main>
</div>
);
}
トークン計測処理をRoute Handler に差し込む
続いてuseChatが利用する生成AI呼び出しエンドポイントを作ります。app/api/chat/route.ts を作成しましょう。APIのパスは変更可能ですが、useChatのデフォルトエンドポイントにあわせておきます。
import { streamText, UIMessage, convertToModelMessages } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { meteredModel } from '@stripe/ai-sdk/meter';
export async function POST(req: Request) {
const stripeApiKey = process.env.STRIPE_SECRET_KEY;
if (!stripeApiKey) {
return new Response('Missing STRIPE_SECRET_KEY', { status: 500 });
}
const { messages }: { messages: UIMessage[] } = await req.json();
const model = meteredModel(
anthropic('claude-sonnet-4-7'),
stripeApiKey,
'cus_replace_with_your_customer_id',
);
const result = streamText({
model,
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
AI SDKのデモと少し異なる部分があります。これはStripeのAI SDKを使ってトークン課金の仕組みを追加したためです。meteredModel() を使わない場合の streamText() はこうなっています。
const result = streamText({
model: anthropic('claude-sonnet-4-7'),
messages: convertToModelMessages(messages),
});
meteredModel() を使う場合は、 anthropic('claude-sonnet-4-7') をラップして streamText に渡します。これだけでAIが読み書きしたトークン数を計測し、Stripeの従量課金に反映させています。
const model = meteredModel(
anthropic('claude-sonnet-4-5'),
stripeApiKey,
'cus_replace_with_your_customer_id',
);
const result = streamText({ model, messages: convertToModelMessages(messages) });
Modelの選択画面だけが変わるため、streamText() の使い方は変わりません。 そのため、AI SDKで実装したチャットアプリへの請求系追加がよりシンプルに行えます。
meteredModel() が Stripe に送るデータ
@stripe/ai-sdk@0.1.1 の dist/meter/meter-event-logging.js を覗くと、 Stripe Meter Events API へ送られているペイロードの形が見えます。
{
"event_name": "token-billing-tokens",
"timestamp": "2026-05-05T01:23:45.000Z",
"payload": {
"stripe_customer_id": "cus_xxxxx",
"value": "543",
"model": "anthropic/claude-sonnet-4-5",
"token_type": "input"
}
}
把握しておくと役に立つ性質が 2 つあります。
input と output で 2 回送られる
1 回の streamText() 呼び出しに対して、 token_type: "input" と token_type: "output" の 2 つのメーターイベントが別々に送信されます。 Stripe ダッシュボードの Meter Events を見ると、 1 回のチャット応答ごとに 2 レコードが並ぶ形です。入力と出力で価格を変えたい場合は、 token_type の値で課金プランの分岐を組むことになります。
Fire-and-forget で送られる
メーターイベントの送信は await されません。送信に失敗してもチャット応答自体はブロックされず、エラーは console.error に出るだけです。
ネットワークが落ちている、 Stripe のキーが無効、メーター名が一致していない、といった状況でも UI 上は普通にチャットが動いてしまいます。メータリングが届いていることを Stripe ダッシュボード側でも能動的に確認しないと、課金漏れには気付けません。
動かす
npm run dev
http://localhost:3000 を開いて何か話しかけ、 AI から応答が返ってくることを確認します。
応答が返ったら Stripe ダッシュボードの Billing → Meters → token-billing-tokens を開き、 Recent events に 2 レコード( input と output)が増えているかを確認します。サーバーコンソールに Error sending meter events to Stripe: というログが出ている場合は、メーター名の不一致か API キーの問題です。
このサンプルを本番に持っていくときに変えるところ
サンプルでは 'cus_replace_with_your_customer_id' をハードコードしているので、誰がチャットしても全部同じ Customer に計上されます。本番では meteredModel() の第 3 引数を、ログインユーザーから DB 経由で解決した値に差し替えます。
const user = await getCurrentUser(req);
const customerId = await db.getStripeCustomerId(user.id);
const model = meteredModel(
anthropic('claude-sonnet-4-5'),
stripeApiKey,
customerId,
);
サンプルから本番に移す上で構造的に変更が必要なのは、第 3 引数の解決方法だけです。認証や DB の実装は別ですが、課金帰属の仕組みはこのまま動かせます。
まとめ
LLMトークンの課金を簡単にできる機能自体も非公開プレビューでローンチされています。しかし2026年5月時点ではまだ申請が必要なため、手早く決済を開始したい場合、こちらの方法もご検討ください。ただしVercel AI SDKのバージョン互換性に少し難があり、v6系が使えない点に注意が必要です。