◉ANTIGRAVITY LABEN
記事一覧/Agents & Manager
◈ Agents & Manager/2026-04-27上級

Antigravity エージェントを商品にして売る90日設計図 — 関門と撤退ラインを先に決める

AgentKit 2.0 で作ったエージェントを個人開発者が商品にするまでの90日を、作業ではなく関門で区切って設計します。特定 SDK に依存しない実行ラッパー、D1 の利用上限ガード、署名検証つき Stripe Webhook、損益分岐の計算式まで、動くコードと一緒にまとめました。

AgentKit 2.013Antigravity386AI エージェント8個人開発108Stripe14商品化

決済ページまで作り終えた夜、Webhook のログを開いて手が止まったことがあります。Stripe のダッシュボードには「成功」と出ているのに、自分のデータベースには権限が付いていませんでした。原因は、metadata のキー名を一か所だけ planType と plan_type で書き分けていたことでした。

エージェントそのものは、その時点でもう動いていました。それでも「売れる状態」には届いていなかったのです。動くことと売れることのあいだには、価格、決済、権限付与、上限管理、止め方といった、地味な層が何枚も挟まっています。

前編のAgentKit 2.0 で作ったAIエージェントを売る5つの収益化パスでは、配布チャネルの選び方をお話ししました。この記事はその続きとして、商品として完成させ、最初の課金を受け取るまでの90日を、関門つきの設計図にします。

作業を重ねる前に、止める条件を決めておきます。 90日の設計は、この一文に集約しています。

90日を「作業」ではなく「関門」で区切る

ロードマップというと、日付ごとのタスク一覧になりがちです。ところが個人開発では、タスクが終わっても次へ進んでよいとは限りません。進むか、直すか、やめるかを決める地点を、日付の代わりに置きます。

以下の基準は、私が自分の判断用に置いている叩き台です。数字は皆さまの商品に合わせて書き換えてください。

関門時期の目安通過の条件通過できないとき
G1 商品定義Day 155つの問いに1ページで答えられる/想定ユーザー1人に実際に話を聞けた実装に入らず、定義を書き直す
G2 原価Day 301回の実行あたりの原価を実測できている/上限ガードが動いているモデルの選び方か処理の分割を見直す
G3 決済Day 60テスト決済から権限付与までが署名検証つきで通る/二重配信でも二重付与にならない公開せず、Webhook を直す
G4 初回課金Day 75知人以外から1件でも課金がある商品ページか価格を見直す
G5 撤退判断Day 90損益分岐に対する現在地を数字で言えるピボットか撤退を決める

関門を置くと、「あと少し作り込めば売れるかもしれない」という気持ちに歯止めがかかります。胃が重くなる夜ほど、機能を足したくなるものです。

Phase 1(Day 1〜15)— 商品として定義する

最初の2週間は、コードを書かずに商品定義だけを進めます。技術から入ると、買う理由を後付けで探すことになり、たいてい行き詰まります。

5つの問いを1ページに収める

  • 誰の、何を解決するのか:たとえば「個人のEC運営者が、商品ページの英訳に毎月かけている時間を短縮する」と書けるところまで具体にします
  • 既存の選択肢と何が違うのか:汎用のチャット AI や既存の翻訳ツールでは足りない理由を、1文で説明します
  • エージェントが行うステップ:入力、処理、出力の各段階を書き出します
  • 失敗したときの振る舞い:想定外の入力、途中停止、外部 API の失敗を一つずつ想定します
  • 価格の根拠:解決する時間、相手にとっての時間の価値、原価と手数料から逆算します

この1ページが書けないなら、それは商品としてまだ形になっていない、というだけのことです。落ち込む必要はなく、書けるまで質問を重ねる段階だと受け止めています。

最初はシングルエージェントから

マルチエージェント構成は魅力的ですが、原因の切り分け、ユーザーへの説明、原価の見積もりが、台数に応じて重くなります。最初の商品は1つの役割に絞り、売れ筋が見えてから分割しても遅くありません。

Phase 2(Day 16〜30)— 壊れにくさと原価を作り込む

この期間のテーマは、動かすことではなく、壊れにくく、止められることです。

特定 SDK に依存しない実行ラッパー

エージェントの呼び出し部分は、将来 SDK やモデルが変わっても差し替えられるよう、関数として抜き出しておきます。以下は、タイムアウトと再試行だけを担当する薄いラッパーです。

// lib/run-with-guards.ts — 呼び出し先(SDK・API)は call として注入する
type AgentCall = (
  input: string,
  signal: AbortSignal,
) => Promise<{ text: string; tokensIn: number; tokensOut: number }>;
 
function isRetryable(e: unknown): boolean {
  if (e instanceof DOMException && e.name === 'AbortError') return true;
  const status = (e as { status?: number })?.status;
  return status === 429 || (status !== undefined && status >= 500);
}
 
export async function runWithGuards(
  call: AgentCall,
  input: string,
  opts: { timeoutMs?: number; maxAttempts?: number } = {},
) {
  const timeoutMs = opts.timeoutMs ?? 60_000;
  const maxAttempts = opts.maxAttempts ?? 3;
  let lastError: unknown;
 
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), timeoutMs);
    try {
      return await call(input, ctrl.signal);
    } catch (e) {
      lastError = e;
      if (!isRetryable(e) || attempt === maxAttempts) break;
      // 指数バックオフ + ジッター(同時リトライの集中を避ける)
      const wait = 2 ** attempt * 500 + Math.random() * 250;
      await new Promise((r) => setTimeout(r, wait));
    } finally {
      clearTimeout(timer);
    }
  }
  throw lastError;
}

再試行の回数と待ち時間は、そのまま原価と体感速度に跳ね返ります。設計の考え方はエージェントの再試行バジェット設計で詳しく扱っています。

1日あたりの利用上限は D1 で数える

無料枠や月額プランを置くなら、上限のカウントはサーバー側で行います。Cloudflare KV は書き込み同士の整合性が保証されないため、連続リクエストで上限を超えてしまうことがあります。回数の集計には、SQL で原子的に更新できる D1 を使います。

CREATE TABLE IF NOT EXISTS usage (
  user_id TEXT NOT NULL,
  day     TEXT NOT NULL,
  runs    INTEGER NOT NULL,
  PRIMARY KEY (user_id, day)
);
// 上限内なら +1 して true、上限に達していれば false を返す
export async function consumeQuota(
  db: D1Database,
  userId: string,
  dailyLimit: number,
): Promise<boolean> {
  const day = new Date().toISOString().slice(0, 10);
  const row = await db
    .prepare(
      `INSERT INTO usage (user_id, day, runs) VALUES (?1, ?2, 1)
       ON CONFLICT(user_id, day) DO UPDATE SET runs = runs + 1
       WHERE runs < ?3
       RETURNING runs`,
    )
    .bind(userId, day, dailyLimit)
    .first<{ runs: number }>();
  return row !== null;
}

ON CONFLICT … DO UPDATE … WHERE の条件が偽になると、更新も RETURNING の行も生まれません。その「行が返らない」ことを、上限到達の合図として使っています。

原価は「1回あたり」で実測する

モデル名と単価は更新が頻繁なため、この記事では固定の値を置きません。代わりに、実行のたびに入力トークンと出力トークンを記録し、自分の使うモデルの最新の料金表から1回あたりの原価を計算します。ここで出た数字が、後述する損益分岐の入力になります。

簡単な入力には安価なモデル、長文や込み入った指示だけ上位のモデル、という振り分けは有効です。ただし、振り分けの基準は入力文字数だけでは足りません。品質を見ながら調整する対象として、環境変数に逃がしておくとよいでしょう。

const MODEL_LIGHT = process.env.MODEL_LIGHT!; // 安価・高速なモデルの ID
const MODEL_HEAVY = process.env.MODEL_HEAVY!; // 品質重視のモデルの ID
 
export function pickModel(input: string, needsReasoning: boolean) {
  if (needsReasoning || input.length > 5000) return MODEL_HEAVY;
  return MODEL_LIGHT;
}

並列でエージェントを動かすときの上限の考え方は、並列エージェントの割当バジェット設計にまとめています。

Phase 3(Day 31〜45)— 配布の入口を絞る

前編の5つの配布パスから、最初は1つか2つに絞ります。私は、自社サイトでの直接販売を軸にして、マーケットプレイスは入口の一つとして試す構成をすすめています。理由は単純で、価格、顧客情報、返金の扱いを自分で決められるのが自社サイトだけだからです。

マーケットプレイスへの出品は、手数料、審査、価格の自由度、顧客情報の扱いが、場所によって大きく異なります。出品の前に、最新の規約と手数料表を必ずご自身で確認してください。この記事では特定のマーケットプレイスの条件を前提にしていません。

商品ページに最低限置くものは、次の5点です。

  • 「誰の何を解決するか」を1文で書いたヒーロー部
  • 実際にエージェントが動く30秒前後のデモ
  • 入力、実行、結果の3ステップ
  • 料金表(プランの違いが一目で分かるもの)
  • 開発者自身がつまずいた点に絞った、1〜2問の FAQ

マーケットプレイスと自社サイトで価格を変える場合は、どちらかが一方を食わない設計にします。流入用の入口は価格を抑え、継続利用の機能は自社サイトの月額に置く、といった線引きです。

Phase 4(Day 46〜60)— Stripe の決済と権限付与

ここが、冒頭でつまずいた層です。Stripe Checkout でセッションを作り、決済完了を Webhook で受け取って権限を付けます。

プランは「使われ方」に合わせる

エージェントは「1回使えば結果が出る」商品になりやすく、その場合は月額だけでなく、1回利用や買い切りを併売すると選びやすくなります。金額は、手元の原価と手数料から逆算したうえで、次のような3層から試す方法があります。

プラン想定する使い方価格の置き方
1回利用試したい人、単発の用事原価の数倍。手数料を引いても黒字になる額
月額定期的に使う個人上限回数から最悪月の原価を逆算して決める
買い切り購読を避けたい人利用上限と更新方針を明記したうえで設定する

Checkout セッションの作成

// app/api/checkout/route.ts
import Stripe from 'stripe';
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
 
const PLANS = {
  oneshot:  { mode: 'payment',      amount: 480,  name: '1回利用' },
  monthly:  { mode: 'subscription', amount: 980,  name: '月額 Pro' },
  lifetime: { mode: 'payment',      amount: 4800, name: '買い切り' },
} as const;
 
export async function createCheckout(
  plan: keyof typeof PLANS,
  userId: string,
  agentId: string,
  origin: string,
) {
  const p = PLANS[plan];
  return stripe.checkout.sessions.create({
    mode: p.mode,
    line_items: [
      {
        quantity: 1,
        price_data: {
          currency: 'jpy', // JPY は最小単位が円なので 480 は 480 円
          unit_amount: p.amount,
          product_data: { name: p.name },
          ...(p.mode === 'subscription'
            ? { recurring: { interval: 'month' as const } }
            : {}),
        },
      },
    ],
    client_reference_id: userId,
    metadata: { plan, agent_id: agentId }, // キー名は Webhook 側と一字一句そろえる
    success_url: `${origin}/agent/${agentId}?checkout=success`,
    cancel_url: `${origin}/agent/${agentId}`,
  });
}

ユーザー ID は client_reference_id に、商品とプランは metadata に入れています。キー名は定数にして、作る側と受ける側で同じ定義を読むようにすると、冒頭のような書き分けは起こりません。

署名を検証し、二重配信に耐える Webhook

Webhook は、署名を検証せずに受けると誰でも「決済完了」を偽装できます。また、Stripe は同じイベントを複数回届けることがあるため、権限付与は冪等でなければなりません。

// app/api/stripe/webhook/route.ts
import Stripe from 'stripe';
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
 
export async function POST(req: Request) {
  const body = await req.text(); // 署名検証には生の本文が必要
  const sig = req.headers.get('stripe-signature');
  if (!sig) return new Response('missing signature', { status: 400 });
 
  let event: Stripe.Event;
  try {
    // Workers / Edge 環境では constructEventAsync を使う
    event = await stripe.webhooks.constructEventAsync(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
  } catch {
    return new Response('invalid signature', { status: 400 });
  }
 
  if (event.type === 'checkout.session.completed') {
    const s = event.data.object as Stripe.Checkout.Session;
    const userId = s.client_reference_id;
    const plan = s.metadata?.plan;
    const agentId = s.metadata?.agent_id;
 
    if (!userId || !plan || !agentId) {
      console.error('webhook metadata missing', event.id);
      return new Response('missing metadata', { status: 400 }); // 再配信させて気づく
    }
    if (s.payment_status !== 'paid') {
      return new Response('not paid yet', { status: 200 }); // 非同期決済は別イベントで付与
    }
 
    // event.id を主キーにした処理済みテーブルへ INSERT し、重複なら何もしない
    await grantOnce({ eventId: event.id, userId, agentId, plan });
  }
  return new Response('ok', { status: 200 });
}

grantOnce の中身は、処理済みイベントの ID を主キーに持つテーブルへの INSERT(重複時は無視)と、権限付与を同じトランザクションにまとめるだけで足ります。課金額の集計がずれたときの調べ方は、メータリング台帳と Stripe のズレの記録に整理しました。決済まわり全体の構成は、SaaS の Stripe フルスタック構成も併せてご覧ください。

Phase 5(Day 61〜75)— ローンチ前の点検

公開の前に、次の項目を一つずつ潰します。

  • 商品ページの冒頭が、ユーザーの困りごとから始まっていること
  • デモが30秒前後で、価値が伝わること
  • 本番モードで少額の決済を実際に通し、返金まで動作を確認済みであること
  • 解約の導線があり、解約後の権限の扱いが説明されていること
  • 決済完了、解約、エラー時のメールが届くこと
  • 実行ログが保存され、ユーザーが履歴を見られること
  • 問い合わせ先が明記されていること
  • 特定商取引法に基づく表記、プライバシーポリシー、利用規約の公開

告知は、X への投稿、技術記事、既存のフォロワーへの案内など、自分が普段使っている場所から始めます。投稿は「開発で詰まったこと」「個人でやるからこその工夫」「価格を決めた理由」の順に並べると、読み手が自分の状況と重ねやすくなります。初日の反応は運の要素が大きいため、そこで判断を下さず、関門 G4 の基準で見ることをおすすめします。

Phase 6(Day 76〜90)— 運用と、撤退ライン

毎日見る3つの数字

  • 新規ユーザー数(自社サイトと外部チャネルを分けて記録)
  • 転換率(訪問者のうち課金に至った割合)
  • エラー率(想定どおりに完了しなかった実行の割合)

それぞれの警戒ラインは、ローンチ前に自分で決めて書き留めておきます。ラインを後から動かせるようにしておくと、数字が悪い日に、都合のよい解釈をしてしまうからです。

損益分岐を計算式で持つ

撤退ラインを「気持ち」ではなく計算式にします。1人あたりの月次の粗利と、固定費の2つがあれば、損益分岐の人数が出ます。

粗利(1人・月)= 月額 ×(1 − 決済手数料率)− 月の平均実行回数 × 1回あたりの原価

損益分岐の人数 = 月の固定費 ÷ 粗利(切り上げ)

以下は、置いた値を明示した試算です。月額 980 円、決済手数料率 4%、1回あたり原価 4 円、固定費(ホスティング、ドメイン、監視)を月 3,000 円と仮定しています。この値は説明のための仮置きで、実際の数字はご自身の請求書から入れてください。

利用の重さ月の実行回数粗利(1人・月)損益分岐の人数
軽い20回約 861 円4人
標準40回約 781 円4人
重い150回約 341 円9人

重いユーザーが増えると、損益分岐の人数が倍以上に伸びることが見て取れます。月額プランに1日あたりの上限を置いたのは、この「最悪月」を数字で押さえるためでした。上限が決まっていれば、粗利が最も薄い月でも、赤字の幅を事前に計算できます。

撤退のライン

私が個人開発で置いている撤退の条件は、次の三つのうち、いずれかが3か月続いたときです。

  • 月の純利益が、固定費を下回ること
  • 運用にかかる時間が、本業や制作の時間を圧迫し続けること
  • 損益分岐の人数に対する現在地が、3か月たっても半分に届かないこと

数字を決めておく意味は、サンクコストに引きずられないためです。撤退は失敗ではなく、次の商品に時間を渡すための判断だと考えています。

ローンチ後に見直す三つのこと

  • 説明できる範囲に絞る:商品名、説明、価格のすべてが3秒で伝わる状態を保ちます。複雑なマルチエージェント構成は、内部の設計として使い、ユーザーには1つの役割として見せます
  • ユーザーと話す場所を持つ:メールでも、小さなコミュニティでも構いません。汎用エージェントとの競合が起きたとき、頼りになるのは機能の差よりも、困りごとを聞いてきた蓄積でした
  • モデル指定を定期的に見直す:新しいモデルは、原価や品質が変わることがあります。見直しの周期は、請求書の傾向を見て決めるのが確実です

90日は「最短ルートの一例」にすぎません。本業のある方は倍の期間をかけても問題ありません。

まずは今夜、5つの問いを1ページに書き出してみてください。書き終えたら、G1 の基準で、その1ページを一度だけ読み返していただければと思います。

シェア

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

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

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

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

関連記事

◈ Agents & Manager2026-04-14
Antigravity × AgentKit 2.0 × Stripe でつくるAIエージェント SaaS — 個人開発者が本番運用まで到達する実装ガイド
AgentKit 2.0 と Stripe を組み合わせ、サブスクリプション収益を得られる AI エージェント SaaS を Antigravity で構築する手順をまとめました。アーキテクチャ設計から課金・Webhook の冪等性、同時実行スロットが漏れる Redis 実装の穴まで、個人開発で実際につまずいた箇所を中心に整理しています。
◈ Agents & Manager2026-03-19
Antigravity で Next.js + Supabase + Stripe のサブスク基盤を組む — 任せた範囲と手で書いた範囲
Antigravity のエージェントに Next.js + Supabase の土台を組ませ、Stripe の Checkout と Webhook だけは手で書いた記録です。署名検証・冪等性・metadata の受け渡しなど、任せると壊れやすい箇所を実装コードで示します。
◈ Agents & Manager2026-10-16
エージェントに .zip を渡す前に、.git を除いて 29 MB を 15 MB にし、秘密情報の警告 28 件を見分けました
Antigravity アプリ v2.23.0 で .zip を添付できるようになりました。自分のリポジトリを実際に固めて .git の重さと秘密情報検査の誤検知を数え、除外・検査・マニフェストを一度に行う Python スクリプトの全文を残します。