1. Why AI SaaS Pricing Is Different
Traditional SaaS has a simple truth: once a user signs up, every additional login, page view, or API call is nearly free. Your cost per user is flat, so a $29/month plan with unlimited usage works fine.
AI SaaS is the opposite. Every prompt sent to GPT-4, Claude, or Gemini has a real, variable cost — and that cost scales with usage. A single power user running agent workflows can burn through $50 in LLM fees in a single afternoon. If you sold that user a $29 flat plan, you just lost money on them.
This is why AI SaaS development in Laravel needs a different billing strategy from day one. You need to meter usage, enforce limits, and tie revenue to actual consumption — without destroying the user experience in the process.
This guide walks through the full monetization playbook for a Laravel AI SaaS in 2026: pricing models, unit economics, a production-ready credit system, Stripe metered billing with Cashier, and the anti-abuse, dashboarding, and forecasting patterns that turn AI features into a sustainable business.
2. The Three AI SaaS Pricing Models
Almost every profitable AI SaaS in 2026 uses one of three pricing shapes — or a hybrid of two. Pick the one that matches your usage curve before you write a single line of billing code.
A. Flat Subscription + Soft Cap
A fixed monthly fee (e.g., $29, $99, $299) with a generous but capped usage allowance. Anything past the cap is throttled or queued, not charged. Great for productivity tools (writing assistants, email summarizers) where 95% of users are light users and you can absorb the cost.
B. Credit Packs (Pre-paid)
Users buy bundles of credits that are consumed per AI action. A "generate blog post" costs 5 credits, a "chat message" costs 1 credit. Credits refill monthly on a subscription or can be topped up on demand. This is the dominant model for AI copywriters, image generators, and voice tools.
C. Usage-Based (Metered) Billing
Pay per token, per API call, or per compute second — billed in arrears at month end. Used by developer-facing APIs and agentic products where usage varies wildly. Aligns cost with consumption perfectly but creates bill shock risk if not paired with budget alerts.
The 2026 Winner: Hybrid
Most successful Laravel AI SaaS products use a subscription + credits hybrid. The subscription gives users a monthly credit allowance (covering 90% of normal usage) and predictable revenue for you. Overage credits are sold as metered add-ons via Stripe. This caps bill shock, makes the pricing page easy to understand, and keeps your margins healthy on power users.
3. Calculating Your Unit Economics
Before you decide what to charge, you need to know what each action actually costs you. This is the step most Laravel AI SaaS founders skip — and why their margins are usually thinner than they realize.
The Per-Call Cost Formula
Cost per AI call
= (input_tokens × input_price_per_1M / 1,000,000)
+ (output_tokens × output_price_per_1M / 1,000,000)
+ (embedding_tokens × embedding_price / 1,000,000) // if RAG
+ infrastructure_overhead // queue, DB, storage
+ support_allocation // ~5–10% padding
Example: A Blog Post Generator
| Component | Tokens | Cost (USD) |
|---|---|---|
| System + user prompt (input) | 2,000 | $0.0050 |
| Generated article (output) | 3,500 | $0.0525 |
| Embedding for RAG context | 1,500 | $0.0002 |
| Queue + DB + storage | — | $0.0020 |
| Total per post | — | ~$0.06 |
If you charge 5 credits per post and sell 500 credits for $29, your marginal cost is $0.06 × 100 posts = $6. Gross margin on that plan is ~79% — healthy for a SaaS.
The Margin Rule of Thumb
Target a minimum 70% gross margin on your entry plan and 80%+ on higher tiers. Leave headroom for: model price increases, support costs, free-tier abuse, and failed generations you have to refund. In practice this means charge 4–6x your raw model cost — not 2x.
4. Building a Credit System in Laravel 12
A credit system needs three things: a balance per user/team, an immutable ledger of every movement, and atomic decrements so two parallel requests never double-spend. Here’s a production schema.
Migrations
// database/migrations/2026_04_16_000001_create_credit_balances_table.php
Schema::create('credit_balances', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->unsignedBigInteger('balance')->default(0); // whole credits
$table->unsignedBigInteger('lifetime_granted')->default(0);
$table->unsignedBigInteger('lifetime_consumed')->default(0);
$table->timestamp('resets_at')->nullable(); // when monthly allowance refills
$table->timestamps();
$table->unique('team_id');
});
// database/migrations/2026_04_16_000002_create_credit_ledger_table.php
Schema::create('credit_ledger', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('type'); // grant, consume, refund, topup, reset
$table->bigInteger('amount'); // signed: +grant, -consume
$table->bigInteger('balance_after');
$table->string('reason');
$table->json('meta')->nullable(); // model, tokens, request_id
$table->uuid('idempotency_key')->nullable()->unique();
$table->timestamps();
$table->index(['team_id', 'created_at']);
});
The CreditService
// app/Services/Billing/CreditService.php
namespace App\Services\Billing;
use App\Models\Team;
use App\Models\CreditBalance;
use App\Models\CreditLedger;
use Illuminate\Support\Facades\DB;
use App\Exceptions\InsufficientCreditsException;
class CreditService
{
public function consume(
Team $team,
int $amount,
string $reason,
array $meta = [],
?string $idempotencyKey = null,
): CreditLedger {
return DB::transaction(function () use ($team, $amount, $reason, $meta, $idempotencyKey) {
// Row-level lock prevents double-spend on concurrent requests
$balance = CreditBalance::where('team_id', $team->id)
->lockForUpdate()
->firstOrFail();
if ($balance->balance < $amount) {
throw new InsufficientCreditsException(
"Team {$team->id} has {$balance->balance}, needs {$amount}"
);
}
$balance->decrement('balance', $amount);
$balance->increment('lifetime_consumed', $amount);
return CreditLedger::create([
'team_id' => $team->id,
'user_id' => auth()->id(),
'type' => 'consume',
'amount' => -$amount,
'balance_after' => $balance->balance,
'reason' => $reason,
'meta' => $meta,
'idempotency_key' => $idempotencyKey,
]);
});
}
public function grant(Team $team, int $amount, string $reason, array $meta = []): CreditLedger
{
return DB::transaction(function () use ($team, $amount, $reason, $meta) {
$balance = CreditBalance::firstOrCreate(
['team_id' => $team->id],
['balance' => 0],
);
$balance->increment('balance', $amount);
$balance->increment('lifetime_granted', $amount);
return CreditLedger::create([
'team_id' => $team->id,
'type' => 'grant',
'amount' => $amount,
'balance_after' => $balance->balance,
'reason' => $reason,
'meta' => $meta,
]);
});
}
}
The lockForUpdate() plus DB::transaction() combination is what prevents race conditions: two parallel requests both trying to consume the last credit will serialize, and the second one will correctly fail with InsufficientCreditsException.
Why an Idempotency Key Matters
AI calls fail, time out, and retry. If your frontend fires "generate post" twice because the user double-clicked, you don’t want to charge them twice. Pass a UUID from the controller, store it on the ledger row, and the unique index makes the second attempt a no-op.
5. Token Metering & Real Usage Tracking
Credits are what users pay in. Tokens are what you pay in. You need to track both so you can reconcile costs against revenue, and so you can charge variable-cost actions (chat vs long-form generation vs agent workflows) accurately.
The ai_usage Table
Schema::create('ai_usage', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained();
$table->foreignId('user_id')->nullable()->constrained();
$table->string('feature'); // chat, blog_generator, agent_run
$table->string('provider'); // openai, anthropic, gemini
$table->string('model'); // gpt-4o-mini, claude-sonnet-4-6
$table->unsignedInteger('input_tokens');
$table->unsignedInteger('output_tokens');
$table->unsignedInteger('total_tokens');
$table->decimal('cost_usd', 10, 6); // raw provider cost
$table->unsignedInteger('credits_charged');
$table->unsignedInteger('latency_ms')->nullable();
$table->string('request_id')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['team_id', 'created_at']);
$table->index(['feature', 'created_at']);
});
Recording Usage After Every Call
// app/Services/AI/MeteredCompletion.php
public function complete(Team $team, string $feature, array $messages): string
{
$idempotencyKey = (string) Str::uuid();
// 1. Pre-check credits (section 8 covers this deeper)
if (! $this->hasBudget($team, $feature)) {
throw new InsufficientCreditsException();
}
// 2. Call the LLM
$start = microtime(true);
$response = $this->client->chat()->create([
'model' => 'gpt-4o-mini',
'messages' => $messages,
]);
$latency = (int) ((microtime(true) - $start) * 1000);
// 3. Calculate real cost
$inputTokens = $response->usage->promptTokens;
$outputTokens = $response->usage->completionTokens;
$costUsd = $this->priceCalculator->cost('gpt-4o-mini', $inputTokens, $outputTokens);
// 4. Map cost to credits (e.g., 1 credit = $0.002 in provider cost)
$credits = (int) ceil($costUsd / 0.002);
// 5. Atomically consume credits + record usage
DB::transaction(function () use (/* ... */) {
$this->credits->consume($team, $credits, "ai.{$feature}", [
'model' => 'gpt-4o-mini',
'tokens' => $inputTokens + $outputTokens,
], $idempotencyKey);
AiUsage::create([
'team_id' => $team->id,
'feature' => $feature,
'provider' => 'openai',
'model' => 'gpt-4o-mini',
'input_tokens' => $inputTokens,
'output_tokens' => $outputTokens,
'total_tokens' => $inputTokens + $outputTokens,
'cost_usd' => $costUsd,
'credits_charged' => $credits,
'latency_ms' => $latency,
'request_id' => $response->id,
]);
});
return $response->choices[0]->message->content;
}
Now every LLM call writes one row to ai_usage. At month-end you can compute real COGS, per-team margin, per-feature profitability, and per-model cost efficiency with a single SQL query.
6. Stripe Metered Billing with Cashier
For usage-based overages, Stripe’s metered billing (now called "usage-based pricing" in their Billing Meters API) is the cleanest integration. Cashier 15+ wraps it so you can report usage in one line.
Configure the Meter in Stripe
In your Stripe dashboard, create a Meter (e.g., ai_credits_overage) with event name ai.credit_consumed and aggregation sum. Attach it to a metered Price on your subscription product.
Reporting Usage from Laravel
// When a user exceeds their monthly allowance and opts into overages
public function consumeOverage(Team $team, int $credits): void
{
$team->subscription('default')->reportUsage($credits, now(), 'meter-ai-overage');
// Or, using the low-level Meter Events API directly:
$team->stripe()->billing->meterEvents->create([
'event_name' => 'ai.credit_consumed',
'payload' => [
'stripe_customer_id' => $team->stripe_id,
'value' => (string) $credits,
],
]);
}
Idempotent Batching for High Volume
Don’t report every single token in real time — Stripe accepts up to 10,000 events/sec per meter but you’ll burn API calls. Instead, queue a job that aggregates usage in 5-minute windows:
// app/Jobs/ReportAiUsageToStripe.php
public function handle(): void
{
$windows = AiUsage::query()
->selectRaw('team_id, SUM(credits_charged) as credits')
->where('created_at', '>=', now()->subMinutes(5))
->where('reported_to_stripe', false)
->groupBy('team_id')
->get();
foreach ($windows as $w) {
$team = Team::find($w->team_id);
if ($team->hasOverageEnabled() && $w->credits > 0) {
$this->overage->consumeOverage($team, $w->credits);
}
}
AiUsage::where('created_at', '>=', now()->subMinutes(5))
->update(['reported_to_stripe' => true]);
}
Schedule it every 5 minutes in routes/console.php. Add an exponential-backoff retry so a transient Stripe 500 doesn’t lose usage data.
7. Plan Limits & Quota Enforcement
Credits handle the variable cost. But you also want feature gates (Pro gets GPT-4, Free gets GPT-4o-mini), concurrency caps (Free = 1 request at a time), and rate limits (Starter = 100 messages/day). Put all three in a single Plan config.
// config/plans.php
return [
'free' => [
'monthly_credits' => 100,
'models' => ['gpt-4o-mini'],
'max_context_tokens' => 8_000,
'concurrent_requests' => 1,
'rate_limit_per_hour' => 20,
'features' => ['chat'],
],
'pro' => [
'monthly_credits' => 2_500,
'models' => ['gpt-4o-mini', 'gpt-4o', 'claude-sonnet-4-6'],
'max_context_tokens' => 200_000,
'concurrent_requests' => 5,
'rate_limit_per_hour' => 500,
'features' => ['chat', 'agents', 'rag', 'vision'],
],
'team' => [
'monthly_credits' => 15_000,
'models' => ['*'],
'max_context_tokens' => 1_000_000,
'concurrent_requests' => 20,
'rate_limit_per_hour' => 5_000,
'features' => ['*'],
'overage_enabled' => true,
],
];
A Simple PlanGate
class PlanGate
{
public function can(Team $team, string $feature): bool
{
$plan = config("plans.{$team->plan}");
return in_array('*', $plan['features']) || in_array($feature, $plan['features']);
}
public function allowsModel(Team $team, string $model): bool
{
$plan = config("plans.{$team->plan}");
return in_array('*', $plan['models']) || in_array($model, $plan['models']);
}
public function hitRateLimit(Team $team): bool
{
$plan = config("plans.{$team->plan}");
return RateLimiter::tooManyAttempts("ai:{$team->id}", $plan['rate_limit_per_hour']);
}
}
Wrap LLM calls in a single middleware that runs the gate, the rate limiter, and the credit check together — one failure path, one error response, one place to change the rules.
9. Usage Dashboard with Livewire
Users who can’t see what they’re consuming will churn the first time they get a bill they didn’t expect. A live usage panel is not optional — it’s a retention feature.
// app/Livewire/Billing/UsagePanel.php
namespace App\Livewire\Billing;
use Livewire\Component;
use Livewire\Attributes\Computed;
class UsagePanel extends Component
{
#[Computed]
public function summary(): array
{
$team = auth()->user()->currentTeam;
$plan = config("plans.{$team->plan}");
return [
'balance' => $team->creditBalance->balance,
'monthly_allowance' => $plan['monthly_credits'],
'used_this_cycle' => $this->consumedThisCycle($team),
'resets_at' => $team->creditBalance->resets_at,
'top_features' => $this->topFeatures($team),
'projected_overage' => $this->projectOverage($team),
];
}
private function projectOverage(Team $team): int
{
$daysElapsed = now()->diffInDays($team->creditBalance->cycle_started_at) ?: 1;
$daysInCycle = 30;
$used = $this->consumedThisCycle($team);
$projected = (int) ($used / $daysElapsed * $daysInCycle);
return max(0, $projected - config("plans.{$team->plan}.monthly_credits"));
}
public function render() { return view('livewire.billing.usage-panel'); }
}
Poll it every 10 seconds with wire:poll.10s so balances update in real time as the user runs AI actions in another tab. Show a yellow warning at 80% usage and a red banner at 100% with a one-click upgrade CTA.
10. Handling Over-Limit Users Gracefully
When a user hits their limit, what happens next determines whether they upgrade or churn. Three patterns, from worst to best:
- Hard block — the feature just errors. Users feel cheated and leave.
- Silent overage — you keep serving requests and bill at month-end. Causes bill shock and chargebacks.
- Interactive upgrade prompt — the call is paused, the user sees a modal with two clear choices: top up credits ($X for Y credits) or upgrade plan. One click resumes the action.
The third pattern is what you want. Implement it by throwing a domain exception and catching it in a Livewire event listener that opens the upgrade modal.
try {
$output = $aiService->complete($team, 'chat', $messages);
$this->addMessage($output);
} catch (InsufficientCreditsException $e) {
$this->dispatch('open-upgrade-modal', [
'needed' => $e->creditsNeeded,
'balance' => $e->currentBalance,
'suggested_plan' => $this->suggestUpgrade($team),
]);
}
If the user tops up, resume the message from where it failed by storing a pending message ID and replaying it after the webhook confirms payment. This turns a potential churn event into a conversion event.
11. Multi-Model Routing for Margin Control
Not every AI task needs the most expensive model. Routing simple requests to cheaper models — and reserving premium models for jobs that actually need them — is the single highest-leverage thing you can do for AI SaaS margins.
A Three-Tier Router
class ModelRouter
{
public function pickModel(string $feature, int $inputTokens, string $plan): string
{
// Free plan -> always cheap model
if ($plan === 'free') {
return 'gpt-4o-mini';
}
// Simple tasks -> cheap even on paid plans
if (in_array($feature, ['classify', 'title_generator', 'short_summary'])) {
return 'gpt-4o-mini';
}
// Long context or agentic -> premium only when needed
if ($inputTokens > 32_000 || $feature === 'agent_run') {
return 'claude-sonnet-4-6';
}
return 'gpt-4o';
}
}
Combined with retrieval-augmented generation to keep context tight, this routing layer typically cuts token costs by 40–60% with zero user-visible quality change.
Prompt Caching
Anthropic’s prompt caching and OpenAI’s cached input pricing both drop per-token cost by ~50–90% for repeated system prompts. If you run agents or long chat sessions, add a cache marker to your system prompt and watch input costs collapse.
12. Fair Use, Abuse & Anti-Fraud
Free tiers and generous trials are a magnet for bots, fraud signups, and users who burn credits faster than you can process the chargebacks. These controls have paid for themselves on every AI SaaS we’ve shipped:
- Email domain blocklist — reject disposable email providers on signup.
- Stripe Radar rules — enable the "block elevated risk" rule; it catches most card testing early.
- Per-IP concurrent signup limits — 3 accounts per IP per day is plenty for legit households.
- Output-size caps per feature — a "chat message" that returns 50k tokens is almost certainly abuse or a jailbreak.
- Daily cost ceilings per team — hard-cap total USD spend per team even on paid plans, with an alert email before you throttle.
See the Laravel 12 security best practices guide for the full rate-limiting, 2FA, and audit-logging patterns these checks sit on top of.
13. Refunds, Credit Top-Ups & Rollovers
Three policy decisions that deserve explicit answers before your first paying customer asks:
Should Credits Roll Over?
Our recommendation: subscription credits do not roll over (use-it-or-lose-it each cycle), but purchased top-up credits never expire. This keeps the base subscription predictable for you while giving users confidence to buy extra packs without feeling pressured.
Refund Logic for Failed Generations
public function refundForFailure(CreditLedger $original, string $reason): CreditLedger
{
if ($original->type !== 'consume') {
throw new \InvalidArgumentException('Only consume entries can be refunded');
}
// Idempotent: prevent double refunds
if (CreditLedger::where('meta->refund_of', $original->id)->exists()) {
return CreditLedger::where('meta->refund_of', $original->id)->first();
}
return $this->credits->grant(
$original->team,
abs($original->amount),
"refund.{$reason}",
['refund_of' => $original->id],
);
}
Auto-refund when: the LLM returns an empty response, your safety filter blocks the output, or a timeout kills the request. Don’t auto-refund when the user simply doesn’t like the output — that’s a quality problem, not a billing one.
Top-Up Flow
Use a single-price Stripe Checkout Session for top-ups (not a subscription). On checkout.session.completed, grant the credits with a ledger type = 'topup' entry and mark it non-expiring. Pair it with a price ladder: $10/500 credits, $45/2500 credits, $180/12500 credits — larger packs sell better than they look.
14. Production Checklist
Before you turn on paid AI features for real users, confirm every item below:
- Pricing model chosen — flat, credits, metered, or hybrid; documented on the pricing page.
- Unit economics verified — gross margin ≥ 70% on the entry plan at forecasted usage.
- Credit ledger is immutable — every movement is one append-only row; no updates, no deletes.
- Atomic decrements —
DB::transaction()+lockForUpdate()on every consume. - Idempotency keys — every user-triggered consume carries a UUID.
- Real token usage logged —
ai_usagecaptures input/output/cost on every call. - Pre-authorization check — no LLM call happens before a budget verification.
- Failure refunds automated — empty responses, timeouts, and safety-blocked outputs auto-refund.
- Stripe metered usage reported in 5-min batches — with retries and a
reported_to_stripeflag. - Usage dashboard live — balance, projected overage, and top features visible to users.
- Warning + upgrade UX wired — 80% yellow, 100% red, modal with one-click upgrade.
- Plan gates enforced — feature, model, context, and concurrency checked in one middleware.
- Multi-model router active — cheap models handle simple tasks.
- Prompt caching enabled — long system prompts are cached across requests.
- Anti-abuse in place — email blocklist, Stripe Radar, daily team cost ceiling.
- Admin reconciliation — monthly dashboard comparing Stripe revenue vs provider invoices vs logged
cost_usd. - Refund policy documented — clearly written for support, linked from the billing page.
15. Conclusion
Monetizing a Laravel AI SaaS isn’t about picking a number and hoping it covers your OpenAI bill. It’s about building a system where every AI call has a measurable cost, every user has a visible allowance, and every overage becomes a revenue event instead of a margin leak.
The pattern that works in 2026 is the hybrid model: a predictable monthly subscription that grants credits, a token meter that tracks real cost, Stripe usage-based billing for overages, and a multi-model router that picks the cheapest model that can do the job. Wrap it all in atomic transactions, an immutable ledger, and a dashboard users actually look at — and you have AI SaaS billing that scales from your first user to your ten-thousandth without a rewrite.
LaraSpeed ships with the billing foundation already wired. Stripe Cashier, team-scoped credits, an immutable ledger, usage tracking, plan gates, a Livewire usage dashboard, and the pre-authorization pattern are all there out of the box. You skip the weeks of plumbing and start charging for your AI features on day one.
Ship your Laravel AI SaaS with billing that actually makes money
LaraSpeed is the production-ready Laravel 12 SaaS starter kit with Stripe subscriptions, a credit system, AI features, multi-tenancy, and a full admin panel — all wired together so you can focus on your product, not the plumbing.
Get LaraSpeed — Starting at $49