04 — Billing & Credit Infrastructure
Ring: 1 (Launch Blocker) Dependency: R1-1 (Auth — can’t bill without knowing who the user is) Handbook: Ch. 68-84 (pricing & credit economy)
Problem
- No payment infrastructure exists.
- No usage limits — everyone can use everything without restriction.
- AI costs cannot be controlled (per user).
- Revenue mechanism is zero.
Decisions
D1: 4 Plans (2 active + 2 planned)
| Plan | Price | Target | Status |
|---|---|---|---|
| Free | $0 | Trial — WOW moment (discovery open, contact gated) | To be implemented (launch) |
| Pro | 39 annual) | Individual exporter | To be implemented (launch) |
| Team | 119 annual) | Export teams | Defined in DB, UI/billing later |
| Enterprise | Custom | Large companies, multi-org | Defined in DB, UI/billing later |
Only Free + Pro at launch. Team/Enterprise plan records are created in DB (no UI, manual upgrade).
D2: Payment Model — Hybrid (Subscription + Credit)
| Pack | Credits | Price |
|---|---|---|
| Small | 50 | ~$15 |
| Medium | 200 | ~$50 |
| Large | 1000 | ~$200 |
Prices to be finalized post-beta. Only the infrastructure is built now.
D3: Plan Limits
| Feature | Free | Pro | Team | Enterprise |
|---|---|---|---|---|
| Discoveries/mo | 5 | 50 | 200 | Unlimited |
| Companies/search | 10 | 25 | 25 | 50 |
| Contact reveals/mo | 0 | 100 | 500 | Unlimited |
| Saved leads | 10 | Unlimited | Unlimited | Unlimited |
| Org creation | 1 | 1 | 1 | Unlimited |
| Member count | 1 | 1 | 10 | Unlimited |
| Batch operations | ❌ | ❌ | ✅ | ✅ |
| API access | ❌ | ❌ | ❌ | ✅ |
| Auto-headhunt | ❌ | ✅ | ✅ | ✅ |
| AI company analysis | ❌ | ✅ | ✅ | ✅ |
| Follow-up reminders | ❌ | ✅ | ✅ | ✅ |
| Shared leads workspace | ❌ | ❌ | ✅ | ✅ |
| Multi-org | ❌ | ❌ | ❌ | ✅ |
These limits are stored in the export_ai_plan_limits table (NOT hardcoded).
Monthly Usage Cycle: Based on billing period (relative to the user’s subscription date). Example: subscribed March 15 → March 15 - April 14 is 1 period.usage_monthlytable uses aperiod_start DATEcolumn (matches the subscription’scurrent_period_start).checkUsage()reads the row for the active billing period; if none exists, assumes 0. pg_cron reset is UNNECESSARY — a new row is automatically created for each new period, previous periods remain as archive. Free plan users: registration date is used as the billing anchor.
D4: Credit Costs
| Operation | Credits | Handbook Ref |
|---|---|---|
| Discovery search | 1 | Ch. 76 |
| Contact reveal | 1 | Ch. 76 |
| Deep company analysis (enrichment) | 2 | Ch. 76 |
| Market intelligence report | 3 | Ch. 76 |
| Batch operation (per company) | 0.5 | — |
Usage within plan limits does not consume credits. Credits are only used when limits are exceeded.
D5: Beta Period
D6: Payment Provider Integration (Provider-Agnostic)
Recommended Provider: Lemon Squeezy- MoR (Merchant of Record): MoR service built on Stripe infrastructure. Tax/invoicing automatic, global sales from Turkey.
- Direct registration from Turkey: Lemon Squeezy accepts Turkey-based companies.
- Subscription + one-time support: Both monthly plan and credit pack purchases supported.
- Paddle: Alternative MoR option (similar features, larger ecosystem).
- US LLC + Stripe: Post-scale (if a US LLC is established). Stripe is not a MoR — tax/invoice responsibility falls on us.
- PayPal: Payment receiving has been closed in Turkey since 2016. Not usable.
- iyzico: Not suitable for global B2B — TRY-based, not a MoR, limited international card support.
Note: Decided: Lemon Squeezy. Provider-agnostic interfaces will be used in the codebase to keep switching cost low.
Data Model
New Tables
Architecture
Usage Check Flow
Payment Provider Webhook Flow
Provider Event Mapping:Idempotency: Every webhook event is saved to the
Generic Event Lemon Squeezy Event Paddle Event Stripe Event subscription.activated subscription_createdsubscription.activatedcheckout.session.completedsubscription.updated subscription_updatedsubscription.updatedcustomer.subscription.updatedsubscription.canceled subscription_cancelledsubscription.canceledcustomer.subscription.deletedsubscription.past_due subscription_payment_failedsubscription.past_dueinvoice.payment_failedtransaction.completed order_createdtransaction.completedpayment_intent.succeededtransaction.payment_failed order_refundedtransaction.payment_failedinvoice.payment_failedexport_ai_webhook_eventstable. If the sameprovider_event_idarrives again, it is not processed (duplicate protection).
Security Architecture
5-Layer Security
| Layer | Content | Detail |
|---|---|---|
| 1. Network | Webhook endpoint IP whitelist | Only the provider’s IP ranges are accepted (Lemon Squeezy/Paddle/Stripe IP list) |
| 2. API | Webhook signature verification | Every incoming webhook’s signature is verified. Invalid signature → 401. Mandatory — cannot be skipped. |
| 3. Application | Idempotency + event dedup | provider_event_id is UNIQUE in export_ai_webhook_events. Same event is not processed twice. |
| 4. Database | RLS + INSERT-only audit trail | credit_transactions table is INSERT-only — UPDATE/DELETE PROHIBITED. Immutable audit trail. |
| 5. Reconciliation | Periodic reconciliation | Weekly cron: subscription statuses in the provider are compared with DB; inconsistencies generate alerts. |
Atomic Credit Operations
Provider-Agnostic Design Principle
All provider-specific code is isolated underproviders/. The business logic layer (check-usage.ts,consume-credit.ts) imports no provider SDK. A provider change only affects the adapter file.
Subscription Status and Access Control
| Status | Login | New AI Operation | Behavior |
|---|---|---|---|
active | ✅ | ✅ | Normal usage |
trialing | ✅ | ✅ | Normal usage (beta) |
past_due | ✅ | ❌ | Login OK, new AI ops BLOCKED. Warning banner: “Payment failed, please update your payment method.” |
canceled | ✅ | ❌ | Read-only mode. No new operations, existing data remains accessible. |
paused | ✅ | ❌ | Login OK, new AI ops BLOCKED. Subscription paused. |
Page Access Blocking (CRITICAL):checkUsage()includes subscription status check. For statuses other thanactiveandtrialing, it returns\{ allowed: false, reason: 'subscription_inactive' \}.
- In
past_due/canceled/pausedstatus, access to AI pages (discovery, headhunt, batch operations) is BLOCKED viaproxy.tsor layout-level guard. - Even if the user types the URL directly, the page does not open — they are redirected to a billing warning page.
- Blocked pages:
/discovery,/operations, headhunt modal - Not blocked:
/companies,/contacts,/leads(read),/org/billing(update payment),/pricing - UI: Layout banner (non-dismissible) + disabled buttons + tooltip
Current Code Impact
New Files
| File | Content |
|---|---|
lib/billing/check-usage.ts | checkUsage(orgId, action) → allowed/remaining/creditCost |
lib/billing/consume-credit.ts | consumeCredit(orgId, amount, ref) → transaction record |
lib/billing/plan-limits.ts | getPlanLimits(plan) → limits object |
lib/billing/types.ts | IUsageCheck, ICreditTransaction, ISubscription, IPlanLimits |
app/api/usage/check/route.ts | GET — for frontend confirmation dialog |
lib/billing/providers/lemonsqueezy.ts | Lemon Squeezy provider adapter (ILemonSqueezyProvider) |
lib/billing/payment-provider.ts | IPaymentProvider interface + factory |
lib/billing/webhook-handler.ts | Generic webhook handler, provider dispatch |
app/api/webhooks/payment/route.ts | POST — Payment provider webhook handler |
app/pricing/page.tsx | Plan comparison + upgrade page |
app/org/billing/page.tsx | Org billing management (current plan, credit balance, invoice history) |
Files to Change
| File | Change |
|---|---|
| All AI-calling routes | Pre-operation checkUsage(), post-operation incrementUsage() + consumeCredit() |
components/Sidebar.tsx | Credit balance display |
export_ai_organizations | plan column already exists (added in 01-auth) |
Future Decisions
FD-1: Real Pricing (Post-Beta)
Pro: 79? Team: 199? Decision based on beta user behavior.
FD-2: Annual Billing (Ring 3+)
Annual payment discount (20%). Separate Price/Plan object in the payment provider.
FD-3: Usage-Based Billing (Ring 4+)
Metered billing via provider for usage-based invoicing. For Enterprise plans.
FD-4: Credit Expiry (Ring 3+)
Should purchased credits expire? (e.g. 12 months). Anti-hoarding.
Atomic Tasks
| # | Task | Ring | Size |
|---|---|---|---|
| BILL-0 | export_ai_webhook_events table + RLS + unique index (idempotency) | R1 | Migration |
| BILL-1 | export_ai_plan_limits table + 4 plan seed data | R1 | Migration |
| BILL-2 | export_ai_credit_wallets table + RLS | R1 | Migration |
| BILL-3 | export_ai_credit_transactions table + RLS + index (INSERT-only policy) | R1 | Migration |
| BILL-4 | export_ai_usage_monthly table + RLS + index | R1 | Migration |
| BILL-5 | export_ai_subscriptions table + RLS (provider-agnostic columns) | R1 | Migration |
| BILL-6 | lib/billing/types.ts — all interfaces (including IPaymentProvider) | R1 | Small |
| BILL-7 | lib/billing/plan-limits.ts — getPlanLimits() | R1 | Small |
| BILL-8 | lib/billing/check-usage.ts — checkUsage() (plan limit + credit check) | R1 | Medium |
| BILL-9 | lib/billing/consume-credit.ts — consumeCredit() + incrementUsage() (advisory lock) | R1 | Medium |
| BILL-10 | GET /api/usage/check — for frontend confirmation dialog | R1 | Small |
| BILL-11 | Usage check + consume integration in all AI routes | R1 | Large |
| BILL-12 | Sidebar credit balance display | R1 | Small |
| BILL-13 | Payment Provider Checkout integration (Pro plan purchase — Lemon Squeezy) | R1 | Large |
| BILL-14 | POST /api/webhooks/payment — provider-agnostic webhook handler | R1 | Medium |
| BILL-15 | /pricing page — plan comparison | R1 | Medium |
| BILL-16 | /org/billing page — billing management | R1 | Medium |
| BILL-17 | Credit pack purchase (payment provider one-time transaction) | R1 | Medium |
| BILL-18 | Beta config — Free plan limits (5 discoveries, 0 reveals, 10 leads). Updated per R0-8 decision. | R1 | Small |
| BILL-19b | Free tier limit change — DB migration (reveal 0, leads 10) + UI update | R1 | Small |
| BILL-21 | Failed headhunt credit check — do NOT consume if no results | R1 | Medium |
| BILL-22 | Headhunt hit rate analysis — measure 50-70% success rate, improvement plan | R2 | Medium |