ANTIGRAVITY LABEN
記事一覧/Agents & Manager
Agents & Manager/2026-05-03上級

Antigravity Agents × Stripeで自動化SaaSを月額課金化する実装ガイド — 2026年版

Antigravityのエージェント機能・Stripe・Cloudflare Workersを組み合わせて自動化SaaSを月額課金化する完全実装ガイドです。タスクキューイング・並列実行制御・Webhook・KVアクセス制御まで本番で動く形で解説します。

Antigravity338Stripe14Cloudflare Workers7SaaS10実装2

前編のAntigravityで個人SaaSを立ち上げる収益化ロードマップで書いた通り、エージェント駆動のSaaSは「時間節約価値」を販売する設計が肝です。本記事はその実装編で、Antigravityのエージェント機能・Stripe決済・Cloudflare Workers を組み合わせた月額課金SaaSを構築する完全な手順をまとめます。

私が運営するAntigravity Labのコンテンツ自動更新システムも、ここで紹介するパターンの簡易版で動いています。本番でWebhookが失敗したとき・エージェントタスクがタイムアウトしたとき・並列実行が暴走したときの対処を、実コード付きで書き残します。

コードはNext.js 16 (App Router) + Cloudflare Workers (OpenNext) + Stripe v15 + Anthropic SDK + Cloudflare Queues を前提にしています。エージェント実行は時間がかかるため、HTTP同期処理ではなくキューベースの非同期構成を取るのがエージェント型SaaSの定石です。

全体アーキテクチャ

最終構成は次の通りです。

  • 認証: NextAuth.js
  • 決済: Stripe Checkout(月額サブスクと単発購入両対応)
  • アクセス制御: Stripe Webhook → Cloudflare KV
  • タスクキュー: Cloudflare Queues(エージェント実行を非同期化)
  • エージェント実行層: Cloudflare Workers + Anthropic SDK
  • 状態管理: D1(タスク履歴・結果保存)

エージェント型SaaSのキモは、HTTPリクエストとエージェント実行を分離することです。ユーザーがフォームを送信したら、即座に「タスクID」を返してジョブをキューに積み、別Workerがバックグラウンドで実行する構成にします。これにより、エージェント処理が長時間化してもユーザー体験は損なわれません。

ステップ1: Stripe側のプロダクト設計

エージェント型SaaSは「月額固定 + タスク数上限」が基本モデルです。

  • Pro月額(JPY): 商品名「Pro Agent Plan」、価格 ¥1,500/月、月10タスクまで
  • Pro月額(USD): 商品名「Pro Agent Plan」、価格 $10/月、月10タスクまで
  • タスク追加(JPY/USD): 価格 ¥300 / $2、1タスク分のクレジット

Stripe Dashboardで作成した price_id を wrangler.toml に登録します。

ステップ2: Checkoutセッション生成API

Claude × Stripeガイドと同じパターンですが、エージェント型では metadata にタスク数情報も入れます。

// app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
 
export const runtime = 'edge';
 
export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.email) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
  }
 
  const { plan, locale } = await req.json();
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: '2025-10-31',
  });
 
  const isJa = locale === 'ja';
  const isPro = plan === 'pro';
 
  const params: Stripe.Checkout.SessionCreateParams = {
    mode: isPro ? 'subscription' : 'payment',
    success_url: `${process.env.SITE_URL}/${locale}/thanks?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.SITE_URL}/${locale}/membership`,
    customer_email: session.user.email,
    line_items: [{
      price_data: {
        currency: isJa ? 'jpy' : 'usd',
        recurring: isPro ? { interval: 'month' } : undefined,
        unit_amount: isPro ? (isJa ? 1500 : 1000) : (isJa ? 300 : 200),
        product_data: {
          name: isPro
            ? (isJa ? 'Pro Agent Plan(月額)' : 'Pro Agent Plan (Monthly)')
            : (isJa ? 'タスク追加クレジット' : 'Additional Task Credit'),
          description: isPro
            ? (isJa ? '月10タスクのエージェント実行が可能' : '10 agent task executions per month')
            : (isJa ? '1タスク分の追加クレジット' : 'One additional task credit'),
          images: [`${process.env.SITE_URL}/images/stripe-product.png`],
        },
      },
      quantity: 1,
    }],
    metadata: {
      plan_type: plan,
      task_credits: isPro ? '10' : '1',
      user_email: session.user.email,
    },
  };
 
  const checkoutSession = await stripe.checkout.sessions.create(params);
  return NextResponse.json({ url: checkoutSession.url });
}

metadata.task_credits はWebhookでKVに反映する際に使います。Pro契約者には毎月10タスクのクレジットを付与し、追加クレジットを購入したらその分を加算する設計です。

ステップ3: Stripe Webhookでタスククレジット管理

// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getCloudflareContext } from '@opennextjs/cloudflare';
 
export const runtime = 'edge';
 
export async function POST(req: NextRequest) {
  const sig = req.headers.get('stripe-signature');
  const body = await req.text();
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: '2025-10-31',
  });
 
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body, sig!, process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (e) {
    return NextResponse.json({ error: 'invalid signature' }, { status: 400 });
  }
 
  const { env } = getCloudflareContext();
  const kv = env.MEMBERSHIP_KV;
 
  // 冪等性
  const eventKey = `site:antigravity:webhook:${event.id}`;
  if (await kv.get(eventKey)) {
    return NextResponse.json({ received: true, duplicate: true });
  }
  await kv.put(eventKey, '1', { expirationTtl: 86400 * 7 });
 
  switch (event.type) {
    case 'checkout.session.completed': {
      const s = event.data.object as Stripe.Checkout.Session;
      const planType = s.metadata?.plan_type;
      const email = s.customer_email ?? s.metadata?.user_email;
      const taskCredits = parseInt(s.metadata?.task_credits ?? '0', 10);
      if (!email) break;
 
      if (planType === 'pro') {
        const subscription = await stripe.subscriptions.retrieve(s.subscription as string);
        await kv.put(
          `site:antigravity:member:${email}`,
          JSON.stringify({
            plan: 'pro',
            current_period_end: subscription.current_period_end,
            subscription_id: subscription.id,
          }),
          { expirationTtl: subscription.current_period_end - Math.floor(Date.now() / 1000) + 86400 * 3 }
        );
        // 月初のタスククレジットを付与
        await addTaskCredits(kv, email, 10);
      } else if (planType === 'credits') {
        // 追加クレジット購入
        await addTaskCredits(kv, email, taskCredits);
      }
      break;
    }
    case 'invoice.payment_succeeded': {
      // 月次更新時にタスククレジットを再付与
      const invoice = event.data.object as Stripe.Invoice;
      if (invoice.billing_reason === 'subscription_cycle') {
        const customer = await stripe.customers.retrieve(invoice.customer as string);
        if ('email' in customer && customer.email) {
          await addTaskCredits(kv, customer.email, 10);
        }
      }
      break;
    }
    case 'customer.subscription.deleted': {
      const sub = event.data.object as Stripe.Subscription;
      const customer = await stripe.customers.retrieve(sub.customer as string);
      if ('email' in customer && customer.email) {
        await kv.delete(`site:antigravity:member:${customer.email}`);
      }
      break;
    }
  }
  return NextResponse.json({ received: true });
}
 
async function addTaskCredits(kv: any, email: string, amount: number) {
  const key = `site:antigravity:credits:${email}`;
  const current = parseInt((await kv.get(key)) ?? '0', 10);
  await kv.put(key, String(current + amount), { expirationTtl: 86400 * 365 });
}

invoice.payment_succeeded(billing_reason=subscription_cycle)で月次更新時にクレジットを再付与する点がエージェント型SaaSの特徴的な設計です。これにより、サブスク継続中は毎月10タスクが自動補充されます。

ステップ4: タスク投入APIとキュー処理

ユーザーがタスク実行を要求したら、即座にキューに積んで非同期処理にします。

// app/api/tasks/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { getCloudflareContext } from '@opennextjs/cloudflare';
 
export const runtime = 'edge';
 
export async function POST(req: NextRequest) {
  const session = await getServerSession(authOptions);
  if (!session?.user?.email) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
  }
 
  const email = session.user.email;
  const { env } = getCloudflareContext();
 
  // クレジット残高確認
  const credits = parseInt(
    (await env.MEMBERSHIP_KV.get(`site:antigravity:credits:${email}`)) ?? '0',
    10
  );
  if (credits < 1) {
    return NextResponse.json(
      { error: 'no_credits', message: 'タスククレジットが不足しています' },
      { status: 402 }
    );
  }
 
  // 同時実行数チェック(最大3並列)
  const runningKey = `site:antigravity:running:${email}`;
  const running = parseInt((await env.MEMBERSHIP_KV.get(runningKey)) ?? '0', 10);
  if (running >= 3) {
    return NextResponse.json(
      { error: 'too_many_concurrent', message: '同時実行可能な数を超えています' },
      { status: 429 }
    );
  }
 
  const { taskType, prompt } = await req.json();
  const taskId = crypto.randomUUID();
 
  // クレジット消費 & 実行カウンタ増加
  await env.MEMBERSHIP_KV.put(
    `site:antigravity:credits:${email}`,
    String(credits - 1),
    { expirationTtl: 86400 * 365 }
  );
  await env.MEMBERSHIP_KV.put(runningKey, String(running + 1), { expirationTtl: 600 });
 
  // D1にタスクレコード作成
  await env.DB.prepare(
    `INSERT INTO tasks (id, email, type, prompt, status, created_at) VALUES (?, ?, ?, ?, ?, ?)`
  ).bind(taskId, email, taskType, prompt, 'queued', Date.now()).run();
 
  // Cloudflare Queueに投入
  await env.AGENT_QUEUE.send({ taskId, email, taskType, prompt });
 
  return NextResponse.json({ taskId, status: 'queued' });
}

ステップ5: キューワーカーでエージェント実行

別Workerでエージェントタスクを実際に処理します。

// workers/agent-worker.ts
import Anthropic from '@anthropic-ai/sdk';
 
export default {
  async queue(batch: MessageBatch, env: Env): Promise<void> {
    for (const message of batch.messages) {
      const { taskId, email, taskType, prompt } = message.body as any;
      try {
        await processAgentTask(taskId, email, taskType, prompt, env);
      } catch (e: any) {
        await env.DB.prepare(
          `UPDATE tasks SET status = 'failed', error = ?, completed_at = ? WHERE id = ?`
        ).bind(e.message, Date.now(), taskId).run();
        // クレジット返却
        await refundCredit(env, email);
      } finally {
        await decrementRunning(env, email);
      }
    }
  },
};
 
async function processAgentTask(
  taskId: string,
  email: string,
  taskType: string,
  prompt: string,
  env: Env
): Promise<void> {
  await env.DB.prepare(
    `UPDATE tasks SET status = 'running', started_at = ? WHERE id = ?`
  ).bind(Date.now(), taskId).run();
 
  const anthropic = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
 
  // エージェント本体(簡易版): 複数ステップを順次実行
  const steps = await planSteps(anthropic, taskType, prompt);
  const results: string[] = [];
 
  for (const step of steps) {
    const stepResult = await executeStep(anthropic, step);
    results.push(stepResult);
 
    // タスクタイムアウト(10分)
    const task = await env.DB.prepare(
      `SELECT started_at FROM tasks WHERE id = ?`
    ).bind(taskId).first();
    if (Date.now() - (task?.started_at as number) > 10 * 60 * 1000) {
      throw new Error('Task timeout exceeded');
    }
  }
 
  const finalReport = await generateReport(anthropic, results);
 
  await env.DB.prepare(
    `UPDATE tasks SET status = 'completed', result = ?, completed_at = ? WHERE id = ?`
  ).bind(finalReport, Date.now(), taskId).run();
}

ステップ6: タスクタイムアウトと部分失敗の対処

エージェントタスクで最も詰まるのが、長時間実行と部分失敗です。

タイムアウト対策 — エージェントが想定以上にステップを踏むと、Cloudflare Workerの実行時間上限(30秒)に達してしまいます。これを回避するため、上のコードでは10分のタスクタイムアウトを別途設定し、超過したらクレジットを返却して失敗にしています。

部分失敗対策 — 多段階タスクの途中でステップが失敗した場合、それまでの成果を保存しておく点が肝心です。各ステップ完了時にD1へ中間結果を保存し、失敗時にユーザーに「途中までの結果」を提供できる設計にします。

// 各ステップ後に中間結果を保存
await env.DB.prepare(
  `INSERT INTO task_steps (task_id, step_index, result) VALUES (?, ?, ?)`
).bind(taskId, stepIndex, stepResult).run();

ステップ7: コスト爆発の予防

エージェント型SaaSで最も恐ろしいのが、無限ループバグによるトークン消費の暴走です。私自身、開発中に「エージェントが計画の中でさらに別エージェントを呼び出して、再帰的にAPIを呼び続ける」というバグに2回ハマっています。

予防策として、次の3層を必ず入れてください。

  1. タスクごとの最大ステップ数制限MAX_STEPS = 20 のような上限を設定し、超過したら強制終了
  2. タスクごとの最大トークン消費上限MAX_TOKENS = 200000 で計測し、超過したら停止
  3. Anthropic Console側の月間予算アラート — 個人開発者の場合、月予算の80%到達でメール通知
let totalTokens = 0;
const MAX_TOKENS = 200000;
const MAX_STEPS = 20;
let stepCount = 0;
 
for (const step of steps) {
  if (stepCount >= MAX_STEPS) throw new Error('Max steps exceeded');
  if (totalTokens >= MAX_TOKENS) throw new Error('Token budget exceeded');
 
  const result = await executeStep(anthropic, step);
  totalTokens += result.usage.input_tokens + result.usage.output_tokens;
  stepCount++;
}

これらのガードを入れておけば、エージェントの暴走で月額数十万円の請求が来る事故を防げます。

ステップ8: Cloudflareエッジキャッシュ対策

他サイトと同じく、課金状態ユーザーへの正確な配信のためのキャッシュバイパスが必要です(STUMBLING_POINTS #73)。

addEventListener('fetch', event => {
  const req = event.request;
  const cookie = req.headers.get('cookie') || '';
  if (cookie.includes('premium_token')) {
    event.respondWith(fetch(req));
    return;
  }
  event.respondWith(handleCachedRequest(event));
});

本番投入前のチェックリスト

  • Stripe Webhookエンドポイントが本番URLを指している
  • STRIPE_WEBHOOK_SECRET ANTHROPIC_API_KEY が本番モードのものに切り替わっている
  • KVのkey prefixが site:antigravity: で他サイトと衝突しないことを確認
  • Cloudflare Queues の binding が wrangler.toml に追加されている
  • D1の tasks task_steps テーブルが作成済み
  • Anthropic Consoleで月間予算アラートを設定
  • Stripe Test Modeでサブスク作成 → 月次更新 → 解約の3パスを通している
  • タスクのMAX_STEPS/MAX_TOKENS ガードが効くことを意図的に発火させて確認
  • cache-worker.jspremium_token Cookieバイパスが効いている

全体を振り返って — エージェント型SaaSの3つの差別化ポイント

この実装ガイドの中で、エージェント型SaaSならではの設計判断は次の3つです。

  1. キュー駆動の非同期実行 — HTTPと切り離して長時間タスクを安定運用する
  2. 同時実行数制御 — 1ユーザーあたりの並列度を制限し、API quota枯渇を防ぐ
  3. 3層のコスト爆発予防 — ステップ数・トークン上限・予算アラートで暴走をブロック

これらをきちんと実装すれば、Antigravityで作るエージェント型SaaSは、本番運用に耐える商用プロダクトとして立ち上がります。前編で書いた価格設計と組み合わせれば、初日から赤字にならず、毎月安定した課金が成立する構成になります。

実装で詰まったら、Antigravity Lab・Claude Lab・Gemini Lab・Rork Labのリポジトリ群が同じ構成で動いている実例として参考になるはずです。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Agents & Manager2026-05-05
AgentKit 2.0 でサブスク型AIエージェントサービスを作る — Stripe課金から月次収益設計まで
AgentKit 2.0を使ったサブスクリプション型AIエージェントサービスの構築方法を完全解説。Stripe課金統合・マルチエージェント設計・プライシング戦略・解約防止まで、月次収益を安定させる実装ガイドです。
Agents & Manager2026-04-26
Antigravity マルチエージェントで継続課金 SaaS を作る — エージェント分担と購読収益の実装パターン
Antigravity のマルチエージェントを継続課金 SaaS の中核に据えるための実装と運用パターン。役割分担・タスクキュー・Stripe サブスク連携・コスト制御まで、動くコードと判断軸を体系的にまとめました。
Agents & Manager2026-04-14
Antigravity × AgentKit 2.0 × Stripe でつくるAIエージェント SaaS — 個人開発者が本番運用まで到達する実装ガイド
AgentKit 2.0 と Stripe を組み合わせ、サブスクリプション収益を得られるAIエージェント SaaS を Antigravity で構築する完全ガイド。アーキテクチャ設計から本番デプロイまで、個人開発者が実際につまずく落とし穴と解決策を詳解します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →