Designing a Credit-Based Billing System with Stripe for an AI SaaS
How we designed a credit system that meters AI usage alongside Stripe subscriptions, and the billing bugs that taught us expensive lessons.
Pure subscription pricing doesn’t work for AI products. A user who generates 10 posts per month and one who generates 1,000 have radically different costs to serve. Flat-rate pricing either overcharges light users or subsidizes heavy ones.
We needed a hybrid: Stripe subscriptions for the base plan, plus a credit system that meters actual AI usage. Here’s how we built it, and the billing bugs that cost us real money before we fixed them.
Why Credits?
AI operations have variable costs. Text generation with a frontier model costs more than a lighter model. Image generation costs more than text. Web scraping, deep research, and content processing all have different price points. A pure subscription would mean pricing for the worst case and losing margins on light users. Credits let us price proportionally to actual usage while keeping the UX simple — users see a credit balance, not a complex metering dashboard.
The Billing Architecture

Three layers work together:
Layer 1: Stripe Subscriptions
Stripe handles payment processing, plan management, and recurring billing. Each plan includes a base credit allowance that scales with the tier. Higher tiers get more credits, reflecting higher expected usage.
Layer 2: Credit Management
A custom system tracks credit balances, usage, and monthly refreshes. Each organization has a credit limit tied to their billing plan, tracking monthly allocation, current usage, and any bonus credits from purchased add-ons. A periodic job checks which organizations need their monthly credit refresh and resets their usage counters.
Layer 3: Feature Gating
Not all features are credit-based. Some are plan-gated entirely. Content stream limits, image studio access, and approval workflows are controlled by feature flags tied to the billing plan. This gives us granular control over what each tier can access.
Pre-Validation: The Critical Pattern
The most important billing pattern we implemented: check credits before starting expensive operations.
The flow is: validate → deduct → execute → refund on failure.
Why not deduct only on success? Because between validation and execution, another request could consume the remaining credits, leading to negative balances. Optimistic deduction with refund-on-failure is safer. We check the available balance, deduct the cost, run the operation, and refund if it fails.
Stripe Webhook Hardening
Stripe communicates subscription changes via webhooks. Getting this wrong means users paying but not getting access, or losing access while still paying.
Key hardening measures:
- Signature verification: every webhook is verified against the signing secret
- Idempotency: we store processed event IDs to handle duplicate deliveries
- Separate rate limits: webhook endpoints get higher limits than user-facing endpoints
- No internal error details: webhook responses never expose stack traces or internal state
The Bugs That Cost Us Money
Bug 1: Credit Leak on Failed Generation
Content generation fails sometimes — API timeouts, rate limits, malformed responses. Our initial implementation deducted credits before generation but didn’t refund on failure. Users lost credits for posts that were never created. The fix: wrap every billable operation in error handling that refunds credits on any failure.
Bug 2: Cache Staleness After Operations
The dashboard shows the current credit balance. After generating content, the displayed balance didn’t update because the response was cached. Users saw their old balance, thought they had credits, and queued more generations that failed at the pre-validation step. The fix: invalidate the balance cache after every credit-modifying operation.
Bug 3: Multi-Tenant Billing Isolation
Billing is scoped to organizations, but our early queries didn’t always enforce that scope. A query to check credit balance could theoretically return another organization’s data if the user had access to multiple organizations. The fix: enforce organization-level filtering on every billing query, with automated tests to verify it.
Bug 4: Stripe API Version Mismatch
Our server-side SDK and webhook handler were on different Stripe API versions, causing signature verification to fail intermittently. The fix: pin the API version explicitly and keep it consistent.
Auto Top-Up
For heavy users who don’t want to worry about running out mid-campaign, we built auto top-up. When credits drop below a configurable threshold, the system automatically purchases a credit add-on via Stripe and adds the corresponding bonus credits to the account.
What the bugs taught us
Deduct credits before execution and refund on failure. The alternative — deducting only on success — sounds cleaner but creates race conditions where users overdraw their balance between validation and execution.
Cache invalidation is a billing concern. We had users seeing stale balances, thinking they had credits, queuing more generations, and hitting validation failures. That’s a support ticket every time.
Test multi-tenant isolation in billing specifically. A billing leak — one org seeing another’s balance — is worse than a data leak because it’s a trust violation. Pin your Stripe API version explicitly; version drift between your SDK and webhook handler causes signature verification failures that are maddening to debug.
Log every credit transaction. When a user disputes their balance, an audit trail is the difference between a 5-minute resolution and an hour of detective work.
Series: Building Pengion Pilot
This post is part of a series on the technical challenges we hit building Pengion Pilot. If you haven’t already, start with the first post covering the full architecture and tech stack.
- How We Built an AI SaaS from First Commit to Production
- Migrating from Clerk to Better Auth
- Multi-Tenancy in NestJS
- AI Content Generation Pipeline
- Credit-Based Billing with Stripe ← you are here
- Content Streams
- Full-Stack Type Safety
- SaaS Security Lessons
- Background Jobs and Workers
Each post covers actual decisions and bugs we hit. If you’re building a SaaS, hopefully some of this is useful.