ANTIGRAVITY LABJP
Articles/Agents & Manager
Agents & Manager/2026-05-03Advanced

Building an Agent-Driven SaaS on Antigravity and Stripe — A Complete 2026 Implementation Guide

An end-to-end implementation guide for shipping an automation SaaS built on Antigravity's agent capabilities, Stripe, and Cloudflare Workers — covering task queuing, concurrency control, webhook handling, and the agent-specific edge cases (timeouts, partial failure, cost runaway) that always show up.

Antigravity322Stripe14Cloudflare Workers7SaaS10Implementation

In the companion roadmap I argued that agent-driven SaaS sells time savings, not features. This article is the implementation half: the actual code for shipping a billing, production-grade agent SaaS on Antigravity + Stripe + Cloudflare Workers.

The Antigravity Lab content automation system itself is built on a simpler version of the architecture you'll see here. Webhook failures, agent timeouts, runaway concurrency — these are all production scars I'm collapsing into reusable patterns.

The code targets Next.js 16 App Router on Cloudflare Workers via OpenNext, with Stripe v15, Anthropic SDK, and Cloudflare Queues. Agent execution is too slow to fit a synchronous HTTP handler — queue-based async execution is the standard structure.

Reference Architecture

  • Auth: NextAuth.js
  • Billing: Stripe Checkout (subscription + one-shot credits)
  • Authorization & credits: Stripe Webhook → Cloudflare KV
  • Task queue: Cloudflare Queues for async agent execution
  • Agent execution layer: Cloudflare Workers + Anthropic SDK
  • State store: D1 (task history and results)

The crucial architectural move: separate HTTP request handling from agent execution. When the user submits a form, return a task ID immediately and enqueue the work; a separate worker processes the queue. This keeps UX responsive even when individual agent runs take minutes.

Step 1: Stripe Product Design

Agent SaaS defaults to "flat monthly + per-task cap" pricing.

  • Pro Monthly (JPY): "Pro Agent Plan," ¥1,500/month, 10 tasks/month
  • Pro Monthly (USD): "Pro Agent Plan," $10/month, 10 tasks/month
  • Additional Credits (JPY/USD): ¥300 / $2 per credit

Step 2: Checkout Endpoint

Identical pattern to the Claude × Stripe guide, with agent-specific 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 ? 'Pro Agent Plan (Monthly)' : 'Additional Task Credit',
          description: isPro
            ? '10 agent task executions per month'
            : '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 is the field the webhook will use to update the user's credit balance in KV. Pro subscribers get 10/month auto-replenished; one-shot credit purchases simply increment.

Step 3: Webhook for Task Credit Management

// 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;
 
  // Idempotency
  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': {
      // Replenish credits at each renewal
      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 });
}

The invoice.payment_succeeded handler with billing_reason === 'subscription_cycle' is what makes monthly credit replenishment work. Pro subscribers get fresh credits at each renewal automatically — no manual intervention required.

Step 4: Task Submission API and Queue Enqueue

When a user submits a task, enqueue it and return immediately:

// 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();
 
  // Check credit balance
  const credits = parseInt(
    (await env.MEMBERSHIP_KV.get(`site:antigravity:credits:${email}`)) ?? '0',
    10
  );
  if (credits < 1) {
    return NextResponse.json(
      { error: 'no_credits', message: 'Out of task credits' },
      { status: 402 }
    );
  }
 
  // Concurrency check (max 3 in-flight)
  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: 'Too many concurrent tasks' },
      { status: 429 }
    );
  }
 
  const { taskType, prompt } = await req.json();
  const taskId = crypto.randomUUID();
 
  // Decrement credit, increment running counter
  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 });
 
  // Persist task record
  await env.DB.prepare(
    `INSERT INTO tasks (id, email, type, prompt, status, created_at) VALUES (?, ?, ?, ?, ?, ?)`
  ).bind(taskId, email, taskType, prompt, 'queued', Date.now()).run();
 
  // Enqueue
  await env.AGENT_QUEUE.send({ taskId, email, taskType, prompt });
 
  return NextResponse.json({ taskId, status: 'queued' });
}

Step 5: Queue Worker for Agent Execution

A separate worker consumes the queue and runs the agent:

// 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();
        // Refund credit on failure
        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-minute task timeout
    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();
}

Returning the user's credit on failure is the right default. Charging for a failed task once is a one-time annoyance; charging repeatedly across a flaky integration burns trust quickly.

Step 6: Timeout and Partial-Failure Handling

The two situations that always show up in agent SaaS production:

Timeouts. When the agent steps further than expected, you risk hitting Cloudflare's 30-second worker limit. The pattern above splits this into a soft 10-minute "task timeout" tracked in D1 — exceeded steps fail the task, refund the credit, and surface a clear message instead of a server error.

Partial failures. Multi-step tasks that die at step 7 of 10 should preserve the work from steps 1–6. Persist intermediate results as you go:

await env.DB.prepare(
  `INSERT INTO task_steps (task_id, step_index, result) VALUES (?, ?, ?)`
).bind(taskId, stepIndex, stepResult).run();

When a task fails, the user can be shown "we got this far before encountering an issue" — which is far better UX than "task failed, sorry."

Step 7: Preventing Cost Runaway

The most terrifying agent SaaS bug is an infinite-loop that quietly burns your monthly Anthropic budget. I've personally hit two variants in development — including one where the agent's plan included spawning subagents that recursively spawned more subagents. Without guardrails, this would have produced a billing event I'd rather not discuss.

Three required layers of defense:

  1. Max steps per taskMAX_STEPS = 20, fail-stop on overrun
  2. Max tokens per taskMAX_TOKENS = 200000, fail-stop on overrun
  3. Anthropic Console budget alerts — email at 80% of monthly budget
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++;
}

These three together make six-figure billing accidents structurally hard to produce. They are the most important code in the system.

Step 8: Cloudflare Edge Cache Awareness

Same as the other site implementations — paid users need cache bypass for membership-aware UI (project 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));
});

Pre-Launch Checklist

  • Stripe webhook URL points to production
  • STRIPE_WEBHOOK_SECRET and ANTHROPIC_API_KEY are live values
  • KV keys namespaced under site:antigravity:
  • Cloudflare Queues binding present in wrangler.toml
  • D1 tasks and task_steps tables created
  • Anthropic Console monthly budget alerts configured
  • Test mode walk-through: subscribe → first month → renewal → cancel
  • MAX_STEPS / MAX_TOKENS guardrails verified by deliberate overrun
  • cache-worker.js premium_token cookie bypass verified

Closing — Three Decisions That Make This Work

The architectural decisions specific to agent-driven SaaS, all of which separate this implementation from a generic API-wrapper SaaS:

  1. Queue-driven async execution — long-running tasks are decoupled from HTTP, so user experience stays snappy even when an agent run takes minutes
  2. Per-user concurrency control — capping in-flight tasks per user prevents API quota exhaustion and protects fair-use across the customer base
  3. Three-layer cost runaway protection — step cap, token cap, and budget alerts together turn the worst possible agent failure mode (six-figure surprise bill) into a contained error message

Get these three right and an Antigravity-built agent SaaS is structurally ready for production from day one. Combined with the pricing approach in the roadmap, you have a product that bills correctly and refuses to accidentally bankrupt you.

If you get stuck, the four sites I run on this stack — Antigravity Lab, Claude Lab, Gemini Lab, Rork Lab — are working references.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Agents & Manager2026-05-05
Building a Subscription AI Agent Service with AgentKit 2.0 — Stripe Billing to Monthly Revenue Design
Complete guide to building and monetizing a subscription-based AI agent service using AgentKit 2.0. Covers Stripe integration, multi-agent design, pricing strategy, and churn prevention — everything needed to reach stable monthly recurring revenue.
Agents & Manager2026-04-26
Building a Subscription SaaS on Antigravity Multi-Agent — Role Splits, Quotas, and Stripe Integration That Actually Stick
Antigravity multi-agent is fun to demo. Turning it into the core of a paying subscription SaaS is a different game. Here's the implementation playbook — agent role splits, idempotent task queues, Stripe metered billing, and retention — with working TypeScript code.
Agents & Manager2026-04-14
Build an AI Agent SaaS with Antigravity × AgentKit 2.0 × Stripe — A Complete Implementation Guide for Solo Developers
A complete guide to building a subscription-based AI agent SaaS using AgentKit 2.0 and Stripe with Antigravity. Covers architecture design, billing integration, rate limiting, Webhook idempotency, and production deployment — everything tutorials skip.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →