Build a freemium image generator: end-to-end tutorial
Ship a freemium AI image generator with hard caps, upgrade prompts, real-time analytics, and a live "renders left" badge - using @vevee/sdk + your image provider of choice. Full code, ten-minute build.
Last updated: 2026-05-10
This guide builds a complete freemium image generator: free users get 20 renders/month, pro users get 500, hard cap on free with an upgrade prompt, live "X renders left" badge in the UI, and Stripe-driven plan upgrades.
The whole thing is about 80 lines of code. Most of the complexity that AI image apps drown in - counters, period resets, atomic enforcement, dashboards - is delegated to AIPricingLab.
Step-by-step
1. Set up plans and limit groups
In the AIPricingLab dashboard, create two plans on your app: plan_free (20 image.render / month, calendar-anchored) and plan_pro (500 image.render / month). Match rule on both: event_type = "image.render".
2. Install the SDK and initialize
Backend uses sk_live_, frontend uses pk_live_.
// server.ts
import { createClient } from "@vevee/sdk";
export const vevee = createClient({ apiKey: process.env.VEVEE_KEY! });
// browser.ts
import { createClient } from "@vevee/sdk";
export const aplPub = createClient({ apiKey: process.env.NEXT_PUBLIC_VEVEE_KEY! });3. Assign a free plan on signup
When a user signs up, give them the free plan. Idempotent - safe to call repeatedly.
await vevee.upsertSubscription({
userId: user.id,
planId: "plan_free",
});4. Gate image generation with reserve / commit / release
Standard pattern. Reserve before the Flux call; commit on success; release on error.
import { VeveeError } from "@vevee/sdk";
export async function generate(userId: string, prompt: string) {
const r = await vevee.reserve(userId, "image.render", 1, { model: "flux-pro" });
if (!r.allowed) {
return { error: "upgrade_required", reasons: r.reasons };
}
try {
const image = await callFluxPro(prompt);
await vevee.commit(r.reservationId!);
return { image };
} catch (err) {
await vevee.release(r.reservationId!);
throw err;
}
}5. Render a live "renders left" badge
Frontend uses the public key to read the user's own counters. No server roundtrip needed.
import { useEffect, useState } from "react";
import { aplPub } from "./browser";
export function RendersLeftBadge({ userId }: { userId: string }) {
const [remaining, setRemaining] = useState<number | null>(null);
useEffect(() => {
aplPub.usage(userId).then(u => {
const c = u.counters.find(x => x.label === "Renders");
setRemaining(c?.remaining ?? 0);
});
}, [userId]);
if (remaining === null) return null;
return (
<span>
{remaining > 0 ? `${remaining} renders left` : "Upgrade for more renders"}
</span>
);
}6. Catch limit_reached and show an upgrade modal
When the user hits their cap, the reserve call returns allowed=false. Render an upgrade prompt with a Stripe checkout button.
async function onGenerate(prompt: string) {
const res = await fetch("/api/generate", { method: "POST", body: JSON.stringify({ prompt }) });
const data = await res.json();
if (data.error === "upgrade_required") {
setShowUpgradeModal(true);
return;
}
setImage(data.image);
}7. On Stripe upgrade, switch the plan
In your Stripe webhook handler, on checkout.session.completed, switch the user to plan_pro. Idempotent - safe on retries.
// /api/stripe/webhook.ts
async function onCheckoutComplete(stripeUserId: string, internalUserId: string) {
await vevee.upsertSubscription({
userId: internalUserId,
planId: "plan_pro",
});
}Closing the cycling exploit
Without protection, a user could free-trial → upgrade → cancel → free-trial again to keep getting fresh quotas. Set onPlanChange: "block" on the renders limit group: when they downgrade back to free mid-period, their counter pre-fills to quota and stays there until the next period.
Adding upgrade nudges at 80%
Best-in-class freemium nudges users at ~80% of quota, not 100%. Configure threshold webhooks in the AIPricingLab dashboard at 0.8 of any limit group; wire the webhook to your in-app notification system.
Per-model sub-quotas
Want pro users to get 500 SD renders + 50 Flux Pro renders? Add a second limit group "premium_renders" with match rule { model: ["flux-pro", "imagen-3"] } and quota 50. One render event hits both groups.
Multi-tenant: same code, different plans
If you sell this image generator as a white-label SaaS, every tenant is a separate AIPricingLab workspace. Free / pro plans live per-workspace and inherit your limit-group definitions via templates.
Frequently asked questions
How long does this actually take to build end-to-end?
About ten minutes for the AIPricingLab integration. The Stripe checkout + webhook usually adds another 30 minutes. The hardest part is design / copy for the upgrade modal.
Can I do this without Stripe?
Yes - the free tier with hard caps works completely standalone. Add Stripe (or any other billing system) when you are ready to charge.
What if I want unlimited renders on pro?
Set the pro plan limit group quota to a very large number, or omit the limit group from plan_pro entirely. Either approach works.
How do I handle abuse where one user creates many accounts to stack free tiers?
AIPricingLab does not solve that - it is your auth/identity problem. Pair email-OTP signups with a dedup pass on phone number, GitHub OAuth, or a fingerprint signal. Common in practice.
Other guides
How to track OpenAI API usage by user (with quotas, in real time)
Step-by-step guide to tracking OpenAI API usage per end-user with real-time quotas. Pure-TypeScript pattern using @vevee/sdk. Concurrency-safe, provider-agnostic, ten-minute integration.
7 min · GuideHow to implement per-user rate limits in your AI app
Per-user rate limits for AI apps need atomic enforcement, plan awareness, and refundable reservations. Here is the pattern that works under load - using @vevee/sdk and ~30 lines of code.
9 min · GuideHow to implement usage-based pricing for an AI product
Decide between per-token, per-action, and hybrid pricing. Implement quotas, refunds, and Stripe meter sync. Step-by-step with code, from a developer who has shipped all three models.
6 min · GuideThe reserve / commit / release pattern: atomic AI quota enforcement
Why naive AI usage metering breaks under concurrency, and the only correct pattern that fixes it. Reserve / commit / release explained, with full TypeScript example and the failure modes it prevents.
8 min · GuideHow to charge AI app users by token usage (with refunds and live balance)
Step-by-step: charge users for actual token consumption with pre-paid credits, post-paid invoicing, and a live balance display. Atomic reservations, accurate refunds, and Stripe meter sync.