Solo developers building AI SaaS products on Gemini API and generating consistent recurring revenue — in 2026, this has become a realistic and relatively well-understood career path. Model quality has risen to the point where AI features carry real user value, while infrastructure costs have dropped to near-zero thanks to Cloudflare Workers and similar edge platforms.
This guide covers the complete arc: revenue model design, TypeScript implementation, Stripe billing integration, Cloudflare Workers deployment, cost optimization, and user acquisition. All code in this guide is production-quality and directly usable.
Revenue Model Design — The Math Behind $650/Month
Before writing a single line of code, get the numbers clear. Here are three practical pricing structures for a Gemini-powered SaaS:
Scenario A: Freemium + Monthly Pro
- Free: 50 requests/month (enough to experience real value)
- Pro: $7/month (unlimited usage)
- Users needed: ~93 Pro subscribers for $650/month
Scenario B: Free Trial + Lifetime License
- 14-day free trial with full features
- Lifetime license: $67 one-time
- Sales needed: ~10/month for $670/month
Scenario C: Credit-Based (pay-per-use)
- 1,000 credits: $3.50
- Each AI action: 1–10 credits
- 500 MAU × $1.30 average spend = $650/month
The Gemini API cost picture: Running primarily on Gemini 3.1 Flash, the average cost per request is $0.0001–$0.001. At 10,000 monthly requests, API cost is $1–$10 — a few percent of revenue at most.
Architecture for a Maintainable, Scalable AI SaaS
This stack is designed to be operated solo while handling growth comfortably:
Frontend: Next.js 16 (App Router) + TypeScript
Hosting: Cloudflare Workers (OpenNext)
Database: Cloudflare D1 (SQLite) or Neon Postgres
Auth: Clerk or NextAuth.js v5
Billing: Stripe (Subscriptions + Payment Links)
AI: Google Gemini API (3.1 Flash + Pro)
KV Store: Cloudflare KV (sessions, cache, rate limiting)
Email: Resend or SendGrid
This configuration supports thousands of monthly users on free-tier or very low-cost infrastructure. It scales automatically without architectural changes.
Core Gemini API Integration
The AI layer that delivers your product's value:
// src/lib/gemini.ts — Gemini API client
import { GoogleGenerativeAI, GenerativeModel } from "@google/generative-ai";
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
interface GeminiRequestConfig {
model?: "gemini-3-1-flash" | "gemini-3-1-pro";
temperature?: number;
maxOutputTokens?: number;
}
export class GeminiClient {
private flashModel: GenerativeModel;
private proModel: GenerativeModel;
constructor() {
// Flash: fast and cheap — right for most requests
this.flashModel = genAI.getGenerativeModel({
model: "gemini-3-1-flash",
generationConfig: {
temperature: 0.7,
maxOutputTokens: 2048,
}
});
// Pro: higher accuracy — reserve for complex tasks
this.proModel = genAI.getGenerativeModel({
model: "gemini-3-1-pro",
generationConfig: {
temperature: 0.3,
maxOutputTokens: 8192,
}
});
}
async generateContent(
prompt: string,
config: GeminiRequestConfig = {}
): Promise<{
text: string;
tokensUsed: number;
model: string;
}> {
const model = config.model === "gemini-3-1-pro"
? this.proModel
: this.flashModel;
const result = await model.generateContent(prompt);
const response = result.response;
return {
text: response.text(),
tokensUsed: response.usageMetadata?.totalTokenCount ?? 0,
model: config.model ?? "gemini-3-1-flash"
};
}
async generateWithContext(
systemPrompt: string,
userMessage: string,
history: Array<{ role: "user" | "model"; text: string }>
): Promise<string> {
const chat = this.flashModel.startChat({
history: history.map(h => ({
role: h.role,
parts: [{ text: h.text }]
})),
generationConfig: { temperature: 0.7 }
});
const result = await chat.sendMessage(
`${systemPrompt}\n\nUser: ${userMessage}`
);
return result.response.text();
}
}
export const geminiClient = new GeminiClient();Auth and Plan State Management
// src/lib/user-plan.ts — User plan management via Cloudflare KV
import { getCloudflareContext } from "@opennextjs/cloudflare";
export type PlanType = "free" | "pro" | "premium";
export interface UserPlan {
userId: string;
plan: PlanType;
requestsUsed: number;
requestLimit: number;
resetAt: string;
}
const PLAN_LIMITS: Record<PlanType, number> = {
free: 50,
pro: -1, // unlimited
premium: -1 // unlimited
};
export async function getUserPlan(userId: string): Promise<UserPlan> {
const { env } = getCloudflareContext();
const kv = env.KV_STORE;
const planData = await kv.get<UserPlan>(`plan:${userId}`, "json");
if (!planData) {
const defaultPlan: UserPlan = {
userId,
plan: "free",
requestsUsed: 0,
requestLimit: PLAN_LIMITS.free,
resetAt: getNextMonthReset()
};
await kv.put(`plan:${userId}`, JSON.stringify(defaultPlan), {
expirationTtl: 86400 * 35
});
return defaultPlan;
}
// Monthly reset
if (new Date(planData.resetAt) < new Date()) {
planData.requestsUsed = 0;
planData.resetAt = getNextMonthReset();
await kv.put(`plan:${userId}`, JSON.stringify(planData));
}
return planData;
}
export async function checkAndIncrementUsage(
userId: string
): Promise<{ allowed: boolean; remaining: number }> {
const plan = await getUserPlan(userId);
if (plan.requestLimit === -1) {
await incrementUsage(userId, plan);
return { allowed: true, remaining: -1 };
}
if (plan.requestsUsed >= plan.requestLimit) {
return { allowed: false, remaining: 0 };
}
await incrementUsage(userId, plan);
return {
allowed: true,
remaining: plan.requestLimit - plan.requestsUsed - 1
};
}
async function incrementUsage(userId: string, plan: UserPlan): Promise<void> {
const { env } = getCloudflareContext();
plan.requestsUsed += 1;
await env.KV_STORE.put(`plan:${userId}`, JSON.stringify(plan));
}
function getNextMonthReset(): string {
const next = new Date();
next.setMonth(next.getMonth() + 1);
next.setDate(1);
next.setHours(0, 0, 0, 0);
return next.toISOString();
}AI API Endpoint
// src/app/api/ai/generate/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { geminiClient } from "@/lib/gemini";
import { checkAndIncrementUsage } from "@/lib/user-plan";
export const runtime = "edge";
export async function POST(req: NextRequest): Promise<NextResponse> {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { allowed, remaining } = await checkAndIncrementUsage(userId);
if (!allowed) {
return NextResponse.json(
{
error: "Monthly limit reached",
message: "Upgrade to Pro for unlimited access.",
upgradeUrl: "/pricing"
},
{
status: 429,
headers: {
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": new Date(
Date.now() + 30 * 24 * 60 * 60 * 1000
).toISOString()
}
}
);
}
const body = await req.json();
const { prompt, type = "general" } = body;
if (!prompt || typeof prompt !== "string") {
return NextResponse.json({ error: "Invalid prompt" }, { status: 400 });
}
const systemPrompts: Record<string, string> = {
general: "You are a helpful AI assistant.",
writing: "You are a professional writer. Produce clear, compelling prose.",
analysis: "You are a data analyst. Be logical and objective in your analysis.",
code: "You are a senior engineer. Provide high-quality code with clear explanations."
};
try {
const result = await geminiClient.generateContent(
`${systemPrompts[type] ?? systemPrompts.general}\n\n${prompt}`,
{ model: "gemini-3-1-flash" }
);
return NextResponse.json({
text: result.text,
tokensUsed: result.tokensUsed,
remaining
});
} catch (error) {
console.error("Gemini API error:", error);
return NextResponse.json(
{ error: "AI generation failed" },
{ status: 500 }
);
}
}Stripe Billing — Complete Implementation
// src/app/api/checkout/route.ts — Create Stripe Checkout session
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-01-27.acacia"
});
const PRODUCTS = {
pro_monthly: {
name: "Gemini SaaS Pro Plan",
description: "Unlimited AI feature access. Billed monthly.",
priceUsd: 700, // cents
interval: "month" as const
},
premium_lifetime: {
name: "Gemini SaaS Premium (Lifetime)",
description: "One-time payment for permanent Pro access.",
priceUsd: 6700, // cents
interval: null
}
};
export async function POST(req: NextRequest): Promise<NextResponse> {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { planId } = await req.json();
const product = PRODUCTS[planId as keyof typeof PRODUCTS];
if (!product) {
return NextResponse.json({ error: "Invalid plan" }, { status: 400 });
}
const lineItem: Stripe.Checkout.SessionCreateParams.LineItem = {
quantity: 1,
price_data: {
currency: "usd",
unit_amount: product.priceUsd,
product_data: {
name: product.name,
description: product.description,
images: [`${process.env.NEXT_PUBLIC_BASE_URL}/images/stripe-product.png`]
},
...(product.interval ? {
recurring: { interval: product.interval }
} : {})
}
};
const session = await stripe.checkout.sessions.create({
mode: product.interval ? "subscription" : "payment",
line_items: [lineItem],
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/api/verify-session?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
metadata: {
user_id: userId,
plan_type: planId
}
});
return NextResponse.json({ url: session.url });
}// src/app/api/webhooks/stripe/route.ts — Webhook handler
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { getCloudflareContext } from "@opennextjs/cloudflare";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-01-27.acacia"
});
export async function POST(req: NextRequest): Promise<NextResponse> {
const body = await req.text();
const sig = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
// Cloudflare Workers requires the async version
event = await stripe.webhooks.constructEventAsync(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
const { env } = getCloudflareContext();
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.CheckoutSession;
const userId = session.metadata?.user_id;
const planType = session.metadata?.plan_type;
if (userId && planType) {
await upgradePlan(env.KV_STORE, userId, planType);
}
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
const userId = subscription.metadata?.user_id;
if (userId) {
await downgradePlan(env.KV_STORE, userId);
}
break;
}
}
return NextResponse.json({ received: true });
}
async function upgradePlan(
kv: KVNamespace,
userId: string,
planType: string
): Promise<void> {
const planKey = `plan:${userId}`;
const existing = await kv.get(planKey, "json") as any ?? {};
const newPlan = planType.includes("premium") ? "premium" : "pro";
await kv.put(planKey, JSON.stringify({
...existing,
plan: newPlan,
requestLimit: -1,
updatedAt: new Date().toISOString()
}));
}
async function downgradePlan(kv: KVNamespace, userId: string): Promise<void> {
const planKey = `plan:${userId}`;
const existing = await kv.get(planKey, "json") as any ?? {};
await kv.put(planKey, JSON.stringify({
...existing,
plan: "free",
requestLimit: 50
}));
}Cost Optimization — Protecting Your Margin
// src/lib/cache.ts — Response caching to eliminate duplicate API costs
import { getCloudflareContext } from "@opennextjs/cloudflare";
import crypto from "crypto";
export async function getCachedResponse(
prompt: string,
ttlSeconds: number = 3600
): Promise<string | null> {
const { env } = getCloudflareContext();
const hash = crypto.createHash("sha256").update(prompt).digest("hex");
return await env.KV_STORE.get(`cache:${hash}`);
}
export async function setCachedResponse(
prompt: string,
response: string,
ttlSeconds: number = 3600
): Promise<void> {
const { env } = getCloudflareContext();
const hash = crypto.createHash("sha256").update(prompt).digest("hex");
await env.KV_STORE.put(`cache:${hash}`, response, {
expirationTtl: ttlSeconds
});
}
// Automatic model selection based on task type
export function selectOptimalModel(
taskType: string
): "gemini-3-1-flash" | "gemini-3-1-pro" {
const proTasks = ["complex_analysis", "code_review", "legal", "financial"];
return proTasks.includes(taskType) ? "gemini-3-1-pro" : "gemini-3-1-flash";
}Caching identical or near-identical prompts eliminates redundant API calls. For FAQ-style responses and templated report generation, cache hit rates of 30–50% are achievable, meaningfully reducing cost. Routing tasks through Flash vs. Pro based on complexity cuts overall API spend by 40–60% compared to using Pro for everything.
User Acquisition — The Path to 93 Pro Subscribers
Three channels with proven results for AI SaaS products:
Product Hunt launch: PH concentrates motivated early adopters who specifically look for new AI tools. A well-prepared launch — email list, community warm-up, and concentrated Day 1 voting — can drive hundreds to thousands of signups in a single day. This is typically the highest-leverage single event in a new SaaS's early life.
SEO content marketing: Users looking for AI tools search for terms like "best AI writing tool" or "Gemini API tutorial." Publishing regular tutorials and use-case articles builds sustainable organic traffic without ongoing ad spend. The compounding nature of SEO means the investment gets more efficient over time.
X (Twitter) product demos: Sharing genuinely interesting outputs your AI generates — especially things that surprise or delight — produces organic virality. "Here's what this AI tool did" posts consistently outperform text-only announcements in engagement.
Wrapping Up
Building a revenue-generating AI SaaS on Gemini API is a realistic path for solo developers in 2026 — not a theoretical one. The combination of capable AI infrastructure, near-zero edge hosting costs, and mature billing tooling means the main variable is your ability to identify a problem worth solving and build something users will pay for.
What this guide covered: revenue model design and the math behind reaching $650/month; Gemini API client, authentication, and rate limiting in TypeScript; complete Stripe Checkout and Webhook implementation; cost optimization via caching and model selection; and user acquisition channels with real traction.
The best time to start is before the market gets more crowded. Start small, validate with real users, and iterate.