opncrafter

Building AI SaaS: From Wrapper to Business

Dec 30, 2025 • 25 min read

"GPT Wrapper" is thrown around as an insult — implying that any product built on top of an AI API is inherently valueless. Yet Jasper.ai raised $125M. Copy.ai crossed $10M ARR. PhotoRoom (AI photo editor) reached hundreds of millions in valuation. All of them started as thin layers on top of OpenAI's API. The question isn't whether wrapping an API is legitimate — it's whether you've built something people are willing to pay for persistently, regardless of what happens to the underlying model.

1. The Moat Question: What Actually Defends AI Products

🔄 Workflow Moat

The AI capability is generic, but the workflow integration is specific. "AI copywriting" is generic. "AI ad copy generator that auto-syncs to your Shopify product catalog and A/B tests in Meta Ads Manager" is a workflow moat. Users can't replicate this workflow easily with ChatGPT.

📊 Data Moat

Every correction a user makes to AI output is training signal. Save it. Fine-tune a smaller model (Llama 3.1-8B) on the corrections. After 10,000 edits, your model produces outputs that need 40% fewer corrections than raw GPT-4o — that's a genuine quality advantage.

🔗 Integrations Moat

Connecting to 50+ tools (Salesforce, HubSpot, Notion, Google Workspace) takes months of auth work, edge case handling, and API maintenance. Competitors launching a "similar" product inherit all that integration debt. This is why Zapier is worth billions.

🎓 Proprietary Prompts

Finely-tuned system prompts for specific industries (legal, medical, real estate) represent weeks of iteration. A legal brief prompt that understands citation formats, jurisdiction conventions, and avoids hallucinating case law is genuinely differentiated from a generic "write a legal document" prompt.

2. The Micro-SaaS Tech Stack (Ship in a Weekend)

# The goal: validate before you over-engineer
# This stack ships in a weekend and scales to $100K ARR without infrastructure changes

npx create-next-app@latest my-ai-saas --typescript --app --src-dir
cd my-ai-saas

npm install ai @ai-sdk/openai stripe @supabase/supabase-js @upstash/ratelimit @upstash/redis

# STACK DECISIONS:
# Next.js App Router: Server Actions handle payments + AI (no separate backend)
# Supabase: Postgres + pgvector (for RAG) + Auth + Row Level Security — free tier generous
# Stripe: Checkout handles all payment complexity (SCA, taxes, international cards)
# Upstash Redis: Serverless Redis for rate limiting (pay-per-request, no always-on cost)
# Vercel: Deploy Next.js for free — SSR + streaming + serverless functions included

3. Stripe Integration: Subscriptions + Credits

// app/api/checkout/route.ts — Stripe checkout session
import Stripe from 'stripe';
import { auth } from '@/lib/auth';  // Your auth (Clerk, Supabase, NextAuth)

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
    const { userId } = await auth();
    if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 });

    const { planId } = await req.json();

    // Option A: Subscription (monthly/annual recurring)
    const subscriptionSession = await stripe.checkout.sessions.create({
        payment_method_types: ['card'],
        line_items: [{
            price: planId,  // price_XYZ123 from Stripe Dashboard
            quantity: 1,
        }],
        mode: 'subscription',
        success_url: process.env.NEXT_PUBLIC_URL + '/dashboard?upgraded=true',
                        cancel_url: process.env.NEXT_PUBLIC_URL + '/pricing',
                        metadata: {userId},  // CRITICAL: Links payment to your user
                        client_reference_id: userId,
                        allow_promotion_codes: true,  // Let users apply coupon codes
    });

                        // Option B: Credit packs (one-time purchase, good for variable-cost AI)
                        const creditPackSession = await stripe.checkout.sessions.create({
                            payment_method_types: ['card'],
                        line_items: [{price: 'price_credits_500', quantity: 1 }],
                        mode: 'payment',  // One-time, not recurring
                        success_url: process.env.NEXT_PUBLIC_URL + '/dashboard?credits=added',
                        cancel_url: process.env.NEXT_PUBLIC_URL + '/pricing',
                        metadata: {userId, creditAmount: '500' },
    });

                        return Response.json({url: subscriptionSession.url });
}

                        // app/api/webhooks/stripe/route.ts — Webhook handler (THE CRITICAL PART)
                        // NEVER unlock features from the success_url redirect — users can manipulate it
                        // ALWAYS use webhooks to detect successful payments

                        import {headers} from 'next/headers';

                        export async function POST(req: Request) {
    const body = await req.text();
                        const signature = headers().get('stripe-signature')!;

                        let event;
                        try {
                            // Verify the webhook signature — rejects fake events
                            event = stripe.webhooks.constructEvent(
                                body,
                                signature,
                                process.env.STRIPE_WEBHOOK_SECRET!
                            );
    } catch (err) {
                            console.error('Webhook signature verification failed:', err);
                        return Response.json({error: 'Invalid signature' }, {status: 400 });
    }

                        switch (event.type) {
        case 'checkout.session.completed': {
            const session = event.data.object;
                        const userId = session.metadata?.userId;
                        const mode = session.mode;

                        if (mode === 'subscription') {
                            // Upgrade user to Pro plan in your database
                            await db.user.update({
                                where: { id: userId },
                                data: { plan: 'pro', stripeCustomerId: session.customer as string },
                            });
            } else if (mode === 'payment') {
                // Add credits to user account
                const credits = parseInt(session.metadata?.creditAmount ?? '0');
                        await db.user.update({
                            where: {id: userId },
                        data: {credits: {increment: credits } },
                });
            }
                        break;
        }

                        case 'customer.subscription.deleted': {
            // User cancelled — downgrade to free plan
            const subscription = event.data.object;
                        await db.user.update({
                            where: {stripeCustomerId: subscription.customer as string },
                        data: {plan: 'free', credits: 0 },
            });
                        break;
        }
    }

                        return Response.json({received: true });
}

4. Token Cost Management: Rate Limiting + Model Cascade

// app/api/generate/route.ts — AI endpoint with full cost protection

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';

const ratelimit = new Ratelimit({
    redis: Redis.fromEnv(),
    limiter: Ratelimit.slidingWindow(10, '10s'),  // 10 requests per 10 seconds per user
    analytics: true,  // Track usage in Upstash dashboard
});

export async function POST(req: Request) {
    const { userId, plan } = await auth();

    // 1. Rate limit check (runs in <5ms from Redis)
    const { success, limit, remaining } = await ratelimit.limit(userId);
    if (!success) {
        return Response.json(
            { error: 'Rate limit exceeded. Please wait a moment.' },
            { status: 429, headers: { 'X-RateLimit-Remaining': remaining.toString() } }
        );
    }

    // 2. Credit check (for credit-based plans)
    const user = await db.user.findUnique({ where: { id: userId } });
    if (user?.plan === 'credits' && user.credits <= 0) {
        return Response.json({ error: 'No credits remaining. Purchase more to continue.' }, { status: 402 });
    }

    const { prompt, complexity } = await req.json();

    // 3. Model Cascade: route to cheapest model that can handle the task
    // LLM-as-router: quick classification of request complexity
    const complexityScore = complexity ?? await classifyComplexity(prompt);

    const model = (() => {
        if (complexityScore < 3) return openai('gpt-4o-mini');   // Simple: $0.15/1M tokens
        if (complexityScore < 7) return openai('gpt-4o');         // Medium: $2.50/1M tokens
        return openai('o1-mini');                                  // Complex reasoning: $3/1M
    })();

    // 4. Deduct credit BEFORE generation (prevents free rides on errors)
    if (user?.plan === 'credits') {
        await db.user.update({ where: { id: userId }, data: { credits: { decrement: 1 } } });
    }

    // 5. Stream the response
    const result = await streamText({
        model,
        prompt,
        maxTokens: plan === 'free' ? 500 : 4000,  // Token cap by plan
    });

    return result.toDataStreamResponse();
}

async function classifyComplexity(prompt: string): Promise<number> {
    // Quick, cheap classification call (gpt-4o-mini ~$0.0001)
    const result = await generateText({
        model: openai('gpt-4o-mini'),
        prompt: `Rate the complexity of this task 1-10: "${prompt.substring(0, 200)}". Output ONLY the number.`,
        maxTokens: 5,
    });
    return parseInt(result.text.trim()) || 5;
}

Frequently Asked Questions

Should I charge subscriptions or credits for an AI SaaS?

It depends on usage variance. Subscriptions work when usage is predictable — $20/month for 100 generations maps cleanly to cost. Credits work when heavy users would destroy margin on a flat-fee plan: a user who generates 1,000 images a month at $0.04/image would cost you $40 on a $20 subscription. Credits also create natural upgrade paths (user runs out, wants more) and reduce churn risk (credits don't expire, so cancellation means losing value). Many successful AI SaaS products use a hybrid: a subscription plan covering baseline usage + credit add-ons for heavy users. Always set both a rate limit (requests per minute) and a max monthly cap to prevent any single user from bankrupting your OpenAI bill.

What happens when OpenAI releases a model that's better than my fine-tuned one?

This is the commodity model risk all AI SaaS faces. The answer is the data flywheel: the value isn't in the model you're using today — it's in the proprietary dataset of user corrections, preferences, and edge cases you've collected. When GPT-5 launches, you re-train your fine-tune on the same dataset with the new base model. You now have GPT-5 + your proprietary data, while a competitor starting fresh only has GPT-5. This is why capturing user feedback, edits, and ratings aggressively from day one is the most important architectural decision: user_correction rows in your database are your competitive moat, not your smart prompts.

Ready to Build?

Don't over-engineer it. Ship in a weekend, charge from day one, and let the market tell you if you've found a real problem.

Serverless Deployment Guide →

Continue Reading

👨‍💻
Written by

Vivek

AI Engineer

Full-stack AI engineer with 4+ years building LLM-powered products, autonomous agents, and RAG pipelines. I've shipped AI features to production for startups and worked hands-on with GPT-4o, LangChain, LlamaIndex, and the Vercel AI SDK. I started OpnCrafter to share everything I wish I had when learning — no fluff, just working code and real-world context.

GPT-4oLangChainNext.jsVector DBsRAGVercel AI SDK