# AI Billing & Credits System — Implementation Plan

> Detailed plan for implementing a flexible AI billing system with three modes: Platform Subscription, Pay-Per-Use Credits, and BYOK (Bring Your Own Key).

**Status:** ✅ COMPLETE (All 7 Phases)
**Estimated Effort:** 14-17 engineering days
**Dependencies:** License server API additions

---

## Implementation Progress

| Phase | Status | Notes |
|-------|--------|-------|
| Phase 1: Database Schema | ✅ Complete | Migration 147 (Predict) + Prisma schema (License Server) |
| Phase 2: Core Services | ✅ Complete | `ai-credit-service.ts`, `byok-service.ts`, `ai-billing-router.ts` |
| Phase 3: LLM Integration | ✅ Complete | `client-with-billing.ts` wrapper |
| Phase 4: API Endpoints | ✅ Complete | 18 routes: credits, BYOK, billing config, org limits, admin |
| Phase 5: License Server Integration | ✅ Complete | Credit service, portal routes, webhooks, Predict client |
| Phase 6: UI Implementation | ✅ Complete | Settings pages + Admin page |
| Phase 7: License Server Wiring | ✅ Complete | Customer mapping, JWT auth, direct Stripe fallback |

### Completed Migrations

**Predict (SQL):**
- `db/migrations/147_ai_billing_credits.sql` — Full schema with 9 tables + helper functions
- `db/migrations/148_license_server_customers.sql` — Customer mapping + webhook tracking

**License Server (Prisma):**
- `prisma/schema.prisma` — Added 5 models: `CreditBalance`, `CreditTransaction`, `CreditPackage`, `AiModelPricing`, `CreditSettings`

### Completed Services

**AI Credit Service** (`packages/be/src/billing/services/ai-credit-service.ts`):
- `getOrCreateCreditBalance()` — Get user's credit balance
- `checkCredits()` — Check if user has sufficient credits
- `reserveCredits()` — Reserve credits before LLM call
- `finalizeUsage()` — Deduct actual cost after successful call
- `releaseReservation()` — Release on failure
- `calculateCost()` / `estimateCost()` — Cost calculation using model pricing
- `checkOrgLimits()` — Org owner spending limits
- `syncBalanceFromLicenseServer()` — Webhook handler for sync

**BYOK Service** (`packages/be/src/billing/services/byok-service.ts`):
- `addBYOKKey()` — Add/update encrypted API key
- `getBYOKKeys()` — List user's keys (masked)
- `getDecryptedKey()` — Get key for API call (internal only)
- `removeBYOKKey()` — Delete a key
- `revalidateBYOKKey()` — Re-test key with provider
- `validateProviderKey()` — Test key with OpenAI/Anthropic

**AI Billing Router** (`packages/be/src/billing/services/ai-billing-router.ts`):
- `resolveBillingContext()` — Determine billing mode + validate
- `recordBilling()` — Record usage after LLM call
- `handleBillingFailure()` — Release reservations on error
- `getBillingStatus()` — Get user's full billing status

**LLM Billing Wrapper** (`packages/be/src/algorithms/llm/client-with-billing.ts`):
- `llmCallWithBilling()` — Wrapped llmCall with billing integration
- `canMakeAICall()` — Pre-check if user can make a call

### Completed API Handlers (Phase 4)

**Credits Handlers** (`packages/be/src/api/predict/v1/credits/handlers.ts`):
- `handleGetBalance` — GET /credits
- `handleGetTransactions` — GET /credits/transactions
- `handleGetPackages` — GET /credits/packages
- `handlePurchaseCredits` — POST /credits/purchase
- `handleGetAutoRefill` — GET /credits/auto-refill
- `handleUpdateAutoRefill` — PUT /credits/auto-refill
- `handleEstimateCost` — POST /credits/estimate

**BYOK Handlers** (`packages/be/src/api/predict/v1/byok/handlers.ts`):
- `handleGetBYOKKeys` — GET /byok
- `handleAddBYOKKey` — POST /byok
- `handleUpdateBYOKKey` — PUT /byok/[provider]
- `handleDeleteBYOKKey` — DELETE /byok/[provider]
- `handleValidateBYOKKey` — POST /byok/[provider]/validate
- `handleTestBYOKKey` — POST /byok/[provider]/test

**AI Billing Handlers** (`packages/be/src/api/predict/v1/ai-billing/handlers.ts`):
- `handleGetBillingConfig` — GET /ai-billing
- `handleUpdateBillingConfig` — PUT /ai-billing
- `handleGetUsage` — GET /ai-billing/usage

**Org AI Limits Handlers** (`packages/be/src/api/predict/v1/org/ai-limits/handlers.ts`):
- `handleGetOrgLimits` — GET /org/ai-limits
- `handleUpdateOrgLimits` — PUT /org/ai-limits
- `handleGetOrgUsage` — GET /org/ai-limits/usage
- `handleGetMemberUsage` — GET /org/ai-limits/member/[userId]
- `handleUpdateMemberLimit` — PUT /org/ai-limits/member/[userId]

**Admin AI Billing Handlers** (`packages/be/src/api/predict/v1/admin/ai-billing/handlers.ts`):
- `handleGetPricing` — GET /admin/ai-billing/pricing
- `handleCreatePricing` — POST /admin/ai-billing/pricing
- `handleUpdatePricing` — PUT /admin/ai-billing/pricing/[model]
- `handleDeletePricing` — DELETE /admin/ai-billing/pricing/[model]
- `handleGetPackages` — GET /admin/ai-billing/packages
- `handleCreatePackage` — POST /admin/ai-billing/packages
- `handleUpdatePackage` — PUT /admin/ai-billing/packages/[id]
- `handleDeletePackage` — DELETE /admin/ai-billing/packages/[id]
- `handleGetSettings` — GET /admin/ai-billing/settings
- `handleUpdateSettings` — PUT /admin/ai-billing/settings
- `handleAdjustCredits` — POST /admin/ai-billing/credits/[userId]/adjust

---

## 1. Executive Summary

This plan introduces a **three-tier AI billing model** that gives users flexibility in how they pay for AI features:

| Mode | How It Works | Best For |
|------|--------------|----------|
| **Platform Subscription** | AI included in Free/Pro/Enterprise plan | Most users |
| **Pay-Per-Use Credits** | Top up credits, pay only for what you use | Variable usage, cost control |
| **BYOK (Bring Your Own Key)** | User provides their own API keys | Enterprise, compliance, volume discounts |

Users can switch between modes or combine them (e.g., subscription + BYOK for overflow).

---

## 2. Current State

### 2.1 Existing Infrastructure

| Component | Location | Status |
|-----------|----------|--------|
| Subscription billing | `packages/be/src/billing/` | ✅ Working |
| Usage metering | `billing.usage_meters`, `billing.usage_events` | ✅ Working |
| AI usage logging | `predict.ai_usage_log`, `predict.ai_usage_daily` | ✅ Working |
| Trading credits | `trading.user_credits`, `trading.credit_transactions` | ✅ Working (reusable pattern) |
| BYOK (org-level) | `predict.organization_ai_settings` | ✅ Working |
| API key management | `auth.api_keys`, `auth.service_accounts` | ✅ Working |
| License server | `packages/be/src/integrations/license-server/` | ✅ Connected |
| LLM client | `packages/be/src/algorithms/llm/client.ts` | ✅ Working |

### 2.2 Current Limitations

1. **No per-user BYOK** — BYOK is org-level only, not individual user
2. **No credit system** — Users can't top up and pay-per-use
3. **No user API keys for AI** — API keys exist but not linked to AI billing
4. **Subscription-only billing** — Can't mix subscription + credits

---

## 3. Target Architecture

### 3.1 Three Billing Modes

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                         AI BILLING MODE SELECTION                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  User selects their AI billing mode at /settings/ai-billing                 │
│                                                                              │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐             │
│  │    PLATFORM     │  │    CREDITS      │  │      BYOK       │             │
│  │  SUBSCRIPTION   │  │  (Pay-Per-Use)  │  │ (Bring Own Key) │             │
│  ├─────────────────┤  ├─────────────────┤  ├─────────────────┤             │
│  │ • Included in   │  │ • Top up any    │  │ • Add your own  │             │
│  │   plan quota    │  │   amount        │  │   API keys      │             │
│  │ • Free: 10K tok │  │ • Pay only for  │  │ • Billed direct │             │
│  │ • Pro: 500K tok │  │   what you use  │  │   to your acct  │             │
│  │ • Ent: 5M tok   │  │ • No expiry     │  │ • Unlimited     │             │
│  │ • Resets monthly│  │ • Auto-refill   │  │ • Full control  │             │
│  └─────────────────┘  └─────────────────┘  └─────────────────┘             │
│         │                     │                     │                       │
│         └──────────┬──────────┴──────────┬──────────┘                       │
│                    │                     │                                  │
│                    ▼                     ▼                                  │
│           ┌──────────────────────────────────────────┐                     │
│           │           AI BILLING ROUTER              │                     │
│           │  Determines which key/balance to use     │                     │
│           │  per request based on user's config      │                     │
│           └──────────────────────────────────────────┘                     │
│                              │                                              │
│                              ▼                                              │
│           ┌──────────────────────────────────────────┐                     │
│           │              LLM CLIENT                  │                     │
│           │  Makes API call with resolved key        │                     │
│           │  Logs usage to ai_usage_log              │                     │
│           └──────────────────────────────────────────┘                     │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

### 3.2 Billing Mode Fallback Chain

When a user makes an AI request, the system checks billing modes in order:

```
1. BYOK (if configured and enabled)
   └── Use user's own API key
   └── Bill to their provider account
   └── No platform tracking of cost

2. Credits (if enabled and balance > 0)
   └── Check sufficient balance
   └── Reserve credits
   └── Make call
   └── Consume actual credits

3. Platform Subscription (default)
   └── Check subscription tier limits
   └── Use platform API key
   └── Count against monthly quota

4. Error: No valid billing mode
   └── Return 402 Payment Required
```

### 3.3 User Configuration Options

Users configure their AI billing at `/settings/ai-billing`:

```typescript
interface UserAIBillingConfig {
  // Primary billing mode
  primaryMode: 'subscription' | 'credits' | 'byok';

  // Fallback if primary exhausted/fails
  fallbackMode: 'subscription' | 'credits' | 'byok' | 'none';

  // Credit settings
  credits: {
    autoRefillEnabled: boolean;
    autoRefillAmount: number;        // Credits to purchase
    autoRefillTrigger: number;       // Trigger when below this
    lowBalanceAlert: number;         // Email alert threshold
  };

  // BYOK settings (per provider)
  byok: {
    anthropic: {
      enabled: boolean;
      keyId: string;                 // Reference to encrypted key
    };
    openai: {
      enabled: boolean;
      keyId: string;
    };
    preferredProvider: 'anthropic' | 'openai' | 'auto';
  };

  // Spend controls
  limits: {
    dailySpendLimit: number | null;  // In credits/dollars
    monthlySpendLimit: number | null;
    perRequestLimit: number | null;  // Max tokens per request
  };
}
```

---

## 4. Database Schema

### 4.1 Migration Files (Implemented)

| System | File | Status |
|--------|------|--------|
| **Predict** | `db/migrations/147_ai_billing_credits.sql` | ✅ Created |
| **License Server** | `prisma/schema.prisma` (CreditBalance, etc.) | ✅ Created |

### 4.2 Predict Tables (Migration 147)

```sql
-- =============================================================================
-- Migration 147: AI Billing System — Credits & User BYOK
-- =============================================================================

BEGIN;

-- -----------------------------------------------------------------------------
-- 4.1.1 User AI Billing Configuration
-- -----------------------------------------------------------------------------
CREATE TABLE billing.user_ai_billing (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE UNIQUE,

    -- Billing mode selection
    primary_mode VARCHAR(20) NOT NULL DEFAULT 'subscription',
    fallback_mode VARCHAR(20) DEFAULT 'none',

    -- Spend limits
    daily_spend_limit_cents INTEGER,           -- NULL = unlimited
    monthly_spend_limit_cents INTEGER,
    per_request_token_limit INTEGER,

    -- Current period spend tracking
    current_day_spend_cents INTEGER DEFAULT 0,
    current_month_spend_cents INTEGER DEFAULT 0,
    last_spend_reset_at TIMESTAMPTZ DEFAULT NOW(),

    -- Metadata
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    CONSTRAINT chk_primary_mode CHECK (primary_mode IN ('subscription', 'credits', 'byok')),
    CONSTRAINT chk_fallback_mode CHECK (fallback_mode IN ('subscription', 'credits', 'byok', 'none'))
);

-- -----------------------------------------------------------------------------
-- 4.1.2 AI Credits Balance (per user)
-- -----------------------------------------------------------------------------
CREATE TABLE billing.ai_credits (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE UNIQUE,

    -- Balance (in cents, 1 credit = $0.01)
    available_cents BIGINT NOT NULL DEFAULT 0,      -- Available balance in cents
    reserved_cents BIGINT NOT NULL DEFAULT 0,       -- Reserved for in-flight calls

    -- Lifetime stats
    total_purchased_cents BIGINT NOT NULL DEFAULT 0,
    total_consumed_cents BIGINT NOT NULL DEFAULT 0,
    total_bonus_cents BIGINT NOT NULL DEFAULT 0,
    total_refunded_cents BIGINT NOT NULL DEFAULT 0,

    -- Auto-refill settings
    auto_refill_enabled BOOLEAN DEFAULT FALSE,
    auto_refill_amount_cents INTEGER,               -- Amount to purchase
    auto_refill_trigger_cents INTEGER,              -- Trigger when below this
    auto_refill_payment_method_id VARCHAR(100),     -- Stripe payment method
    auto_refill_max_count INTEGER DEFAULT 3,        -- Max refills before re-auth required
    auto_refill_current_count INTEGER DEFAULT 0,    -- Current refill count since last auth
    auto_refill_last_auth_at TIMESTAMPTZ,           -- When user last authorized auto-refill

    -- Alerts
    low_balance_alert_cents INTEGER DEFAULT 500,    -- $5 default
    low_balance_alerted_at TIMESTAMPTZ,             -- Prevent spam

    -- License server sync
    license_server_customer_id VARCHAR(100),
    last_sync_at TIMESTAMPTZ,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    CONSTRAINT chk_available_non_negative CHECK (available_cents >= 0),
    CONSTRAINT chk_reserved_non_negative CHECK (reserved_cents >= 0)
);

-- -----------------------------------------------------------------------------
-- 4.1.3 AI Credit Transactions (audit log)
-- -----------------------------------------------------------------------------
CREATE TABLE billing.ai_credit_transactions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    credit_id UUID NOT NULL REFERENCES billing.ai_credits(id) ON DELETE CASCADE,

    -- Transaction details
    transaction_type VARCHAR(30) NOT NULL,
    amount_cents BIGINT NOT NULL,                   -- Positive = add, Negative = deduct
    balance_before_cents BIGINT NOT NULL,
    balance_after_cents BIGINT NOT NULL,

    -- LLM call attribution (for consumption transactions)
    llm_call_id UUID,                               -- Reference to ai_usage_log
    model VARCHAR(100),
    provider VARCHAR(50),
    input_tokens INTEGER,
    output_tokens INTEGER,

    -- Payment attribution (for purchase transactions)
    stripe_payment_intent_id VARCHAR(100),
    stripe_charge_id VARCHAR(100),
    license_server_transaction_id VARCHAR(100),

    -- Metadata
    description TEXT,
    metadata JSONB DEFAULT '{}',
    idempotency_key VARCHAR(100) UNIQUE,           -- Prevent duplicate transactions

    created_at TIMESTAMPTZ DEFAULT NOW(),

    CONSTRAINT chk_transaction_type CHECK (transaction_type IN (
        'purchase',           -- User bought credits
        'consumption',        -- Used for AI call
        'reservation',        -- Reserved for pending call
        'release',           -- Released unused reservation
        'bonus',             -- Promotional credits
        'refund',            -- Refund issued
        'adjustment',        -- Admin adjustment
        'auto_refill',       -- Auto-refill purchase
        'expiry'             -- Credits expired (if we add expiry)
    ))
);

-- -----------------------------------------------------------------------------
-- 4.1.4 Credit Packages (purchasable bundles) — ADMIN MANAGED
-- -----------------------------------------------------------------------------
-- NOTE: Packages are created/edited by super_admin via /admin/ai-billing/packages
CREATE TABLE billing.credit_packages (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Package details
    name VARCHAR(100) NOT NULL,
    description TEXT,
    credit_amount_cents BIGINT NOT NULL,            -- Credits received
    price_cents INTEGER NOT NULL,                   -- Price to pay
    currency VARCHAR(3) DEFAULT 'USD',

    -- Bonus
    bonus_cents BIGINT DEFAULT 0,
    bonus_description TEXT,

    -- Stripe integration
    stripe_price_id VARCHAR(100),
    stripe_product_id VARCHAR(100),

    -- Display
    is_active BOOLEAN DEFAULT TRUE,
    is_featured BOOLEAN DEFAULT FALSE,
    sort_order INTEGER DEFAULT 0,
    badge TEXT,                                     -- e.g., "Best Value", "Most Popular"

    -- Validity
    valid_from TIMESTAMPTZ DEFAULT NOW(),
    valid_until TIMESTAMPTZ,

    -- Admin tracking
    created_by UUID REFERENCES auth.users(id),
    updated_by UUID REFERENCES auth.users(id),

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    -- Minimum top-up is $5 (500 cents)
    CONSTRAINT chk_min_price CHECK (price_cents >= 500)
);

-- Seed default packages (1 cent = 1 credit for simplicity)
-- Super_admin can modify these via /admin/ai-billing/packages
INSERT INTO billing.credit_packages (name, description, credit_amount_cents, price_cents, bonus_cents, sort_order, badge) VALUES
    ('Starter', '500 credits (~1,250 AI responses)', 500, 500, 0, 1, NULL),
    ('Basic', '2,000 credits (~5,000 AI responses)', 2000, 2000, 0, 2, NULL),
    ('Popular', '5,000 credits (~12,500 AI responses)', 5000, 5000, 500, 3, 'Most Popular'),
    ('Pro', '10,000 credits (~25,000 AI responses)', 10000, 10000, 1500, 4, 'Best Value'),
    ('Business', '25,000 credits (~62,500 AI responses)', 25000, 25000, 5000, 5, NULL),
    ('Enterprise', '100,000 credits (~250,000 AI responses)', 100000, 100000, 25000, 6, NULL);

-- -----------------------------------------------------------------------------
-- 4.1.4b Organization AI Spending Limits — SET BY ORG OWNER
-- -----------------------------------------------------------------------------
-- Org owners can set default spend limits for their org members
CREATE TABLE billing.org_ai_limits (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    organization_id UUID NOT NULL REFERENCES predict.organizations(id) ON DELETE CASCADE UNIQUE,

    -- Default limits for org members (cents)
    default_daily_limit_cents INTEGER DEFAULT 1000,     -- $10 default per user per day
    default_monthly_limit_cents INTEGER,                -- NULL = no monthly limit
    default_per_request_limit_cents INTEGER,            -- NULL = no per-request limit

    -- Org-wide limits
    org_daily_limit_cents INTEGER,                      -- Total org spend per day
    org_monthly_limit_cents INTEGER,                    -- Total org spend per month

    -- Can members override their limits?
    allow_member_override BOOLEAN DEFAULT FALSE,
    max_member_daily_limit_cents INTEGER,               -- Max a member can set

    -- Who can configure BYOK
    byok_allowed_roles VARCHAR(20)[] DEFAULT ARRAY['owner', 'admin'],

    -- Tracking
    current_day_spend_cents INTEGER DEFAULT 0,
    current_month_spend_cents INTEGER DEFAULT 0,
    last_reset_at TIMESTAMPTZ DEFAULT NOW(),

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- -----------------------------------------------------------------------------
-- 4.1.5 User BYOK Keys (per-user, extends org-level)
-- -----------------------------------------------------------------------------
CREATE TABLE billing.user_byok_keys (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    -- Provider
    provider VARCHAR(50) NOT NULL,                  -- 'anthropic', 'openai'

    -- Encrypted key (AES-256-GCM)
    key_encrypted TEXT NOT NULL,
    key_last4 VARCHAR(4) NOT NULL,                  -- Last 4 chars for display
    key_prefix VARCHAR(10),                         -- e.g., 'sk-ant-' for identification

    -- Status
    is_enabled BOOLEAN DEFAULT TRUE,
    is_validated BOOLEAN DEFAULT FALSE,
    last_validated_at TIMESTAMPTZ,
    validation_error TEXT,

    -- Usage stats
    total_requests INTEGER DEFAULT 0,
    total_input_tokens BIGINT DEFAULT 0,
    total_output_tokens BIGINT DEFAULT 0,
    last_used_at TIMESTAMPTZ,

    -- Metadata
    label VARCHAR(100),                             -- User-provided name
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    CONSTRAINT uq_user_provider UNIQUE (user_id, provider),
    CONSTRAINT chk_provider CHECK (provider IN ('anthropic', 'openai'))
);

-- -----------------------------------------------------------------------------
-- 4.1.6 AI Model Pricing (cost calculation) — SUPER_ADMIN MANAGED
-- -----------------------------------------------------------------------------
-- NOTE: Pricing is managed by super_admin via /admin/ai-billing/pricing
CREATE TABLE billing.ai_model_pricing (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    provider VARCHAR(50) NOT NULL,
    model VARCHAR(100) NOT NULL,

    -- Pricing (cents per 1K tokens) — we use cents for integer math
    input_cost_per_1k_cents NUMERIC(10,4) NOT NULL,
    output_cost_per_1k_cents NUMERIC(10,4) NOT NULL,

    -- Platform markup (optional, super_admin configurable)
    markup_percent NUMERIC(5,2) DEFAULT 0,

    -- Display name for UI
    display_name VARCHAR(100),

    -- Fallback flag
    is_default BOOLEAN DEFAULT FALSE,
    is_active BOOLEAN DEFAULT TRUE,

    -- Effective dates (for price changes)
    effective_from TIMESTAMPTZ DEFAULT NOW(),
    effective_until TIMESTAMPTZ,

    -- Admin tracking
    created_by UUID REFERENCES auth.users(id),
    updated_by UUID REFERENCES auth.users(id),

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    CONSTRAINT uq_model_pricing UNIQUE (provider, model, effective_from)
);

-- Seed current pricing (as of 2026-04, in cents per 1K tokens)
-- Super_admin can modify these via /admin/ai-billing/pricing
INSERT INTO billing.ai_model_pricing (provider, model, display_name, input_cost_per_1k_cents, output_cost_per_1k_cents, markup_percent) VALUES
    -- Anthropic Claude 4
    ('anthropic', 'claude-sonnet-4-20250514', 'Claude Sonnet 4', 0.33, 1.65, 10),
    ('anthropic', 'claude-haiku-4-5-20251001', 'Claude Haiku 4.5', 0.11, 0.55, 10),
    ('anthropic', 'claude-opus-4-5', 'Claude Opus 4.5', 1.65, 8.25, 10),
    -- OpenAI
    ('openai', 'gpt-4o', 'GPT-4o', 0.275, 1.10, 10),
    ('openai', 'gpt-4o-mini', 'GPT-4o Mini', 0.0165, 0.066, 10),
    ('openai', 'o1', 'o1', 1.65, 6.60, 10),
    ('openai', 'o3-mini', 'o3-mini', 0.121, 0.484, 10),
    -- Defaults (fallback pricing)
    ('anthropic', 'default', 'Unknown Anthropic', 0.33, 1.65, 10),
    ('openai', 'default', 'Unknown OpenAI', 0.275, 1.10, 10);

UPDATE billing.ai_model_pricing SET is_default = TRUE WHERE model = 'default';

-- -----------------------------------------------------------------------------
-- 4.1.6b Platform AI Billing Settings — SUPER_ADMIN ONLY
-- -----------------------------------------------------------------------------
-- Global settings for AI billing, managed by super_admin
CREATE TABLE billing.ai_billing_settings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Minimum top-up amount (cents)
    min_topup_cents INTEGER DEFAULT 500,                -- $5 minimum

    -- Default auto-refill limits
    default_auto_refill_max_count INTEGER DEFAULT 3,    -- Max refills before re-auth

    -- Default org limits
    default_org_daily_limit_cents INTEGER DEFAULT 1000, -- $10 default
    default_org_monthly_limit_cents INTEGER,

    -- Credit expiry (NULL = never expires)
    credit_expiry_days INTEGER,

    -- Platform markup (global override)
    global_markup_percent NUMERIC(5,2) DEFAULT 10,

    -- Feature flags
    credits_enabled BOOLEAN DEFAULT TRUE,
    byok_enabled BOOLEAN DEFAULT TRUE,
    auto_refill_enabled BOOLEAN DEFAULT TRUE,

    -- Updated by
    updated_by UUID REFERENCES auth.users(id),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    -- Singleton pattern
    CONSTRAINT singleton CHECK (id = '00000000-0000-0000-0000-000000000001'::uuid)
);

-- Insert singleton settings row
INSERT INTO billing.ai_billing_settings (id)
VALUES ('00000000-0000-0000-0000-000000000001');

-- -----------------------------------------------------------------------------
-- 4.1.7 User API Keys (extended for credit billing)
-- -----------------------------------------------------------------------------
ALTER TABLE auth.api_keys
ADD COLUMN IF NOT EXISTS billing_mode VARCHAR(20) DEFAULT 'subscription',
ADD COLUMN IF NOT EXISTS credit_account_id UUID REFERENCES billing.ai_credits(id),
ADD COLUMN IF NOT EXISTS daily_spend_limit_cents INTEGER,
ADD COLUMN IF NOT EXISTS monthly_spend_limit_cents INTEGER,
ADD COLUMN IF NOT EXISTS current_day_spend_cents INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS current_month_spend_cents INTEGER DEFAULT 0;

ALTER TABLE auth.api_keys
ADD CONSTRAINT chk_api_key_billing_mode
CHECK (billing_mode IN ('subscription', 'credits', 'byok'));

-- -----------------------------------------------------------------------------
-- 4.1.8 Indexes
-- -----------------------------------------------------------------------------
CREATE INDEX idx_user_ai_billing_user ON billing.user_ai_billing(user_id);
CREATE INDEX idx_ai_credits_user ON billing.ai_credits(user_id);
CREATE INDEX idx_ai_credits_low_balance ON billing.ai_credits(available_cents)
    WHERE available_cents < low_balance_alert_cents;
CREATE INDEX idx_ai_credit_tx_user ON billing.ai_credit_transactions(user_id);
CREATE INDEX idx_ai_credit_tx_created ON billing.ai_credit_transactions(created_at DESC);
CREATE INDEX idx_ai_credit_tx_type ON billing.ai_credit_transactions(transaction_type);
CREATE INDEX idx_ai_credit_tx_idempotency ON billing.ai_credit_transactions(idempotency_key);
CREATE INDEX idx_user_byok_user ON billing.user_byok_keys(user_id);
CREATE INDEX idx_user_byok_provider ON billing.user_byok_keys(user_id, provider) WHERE is_enabled;
CREATE INDEX idx_credit_packages_active ON billing.credit_packages(is_active, sort_order);
CREATE INDEX idx_model_pricing_lookup ON billing.ai_model_pricing(provider, model, effective_from DESC);

COMMIT;
```

### 4.2 Schema Relationships

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           DATABASE SCHEMA                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  auth.users                                                                  │
│      │                                                                       │
│      ├──────┬──────────────┬────────────────┬────────────────┐              │
│      │      │              │                │                │              │
│      ▼      ▼              ▼                ▼                ▼              │
│  billing.   billing.    billing.        auth.          billing.             │
│  user_ai_   ai_credits  user_byok_      api_keys       ai_credit_           │
│  billing                keys             (extended)    transactions          │
│      │          │           │                │              │               │
│      │          │           │                │              │               │
│      │          └───────────┼────────────────┘              │               │
│      │                      │                               │               │
│      │                      ▼                               │               │
│      │            billing.ai_model_pricing                  │               │
│      │                                                      │               │
│      └──────────────────────────────────────────────────────┘               │
│                              │                                               │
│                              ▼                                               │
│                    predict.ai_usage_log                                     │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

---

## 5. API Endpoints

### 5.1 AI Billing Configuration

```
GET    /api/predict/v1/ai-billing
       → Get user's AI billing configuration and balances

PUT    /api/predict/v1/ai-billing
       → Update billing mode, limits, fallback settings
       Body: { primaryMode, fallbackMode, limits }

GET    /api/predict/v1/ai-billing/usage
       → Get usage breakdown by mode, model, day
       Query: ?days=30&groupBy=model|day|mode
```

### 5.2 Credits Management

```
GET    /api/predict/v1/credits
       → Get credit balance and stats
       Response: { available, reserved, total, autoRefill }

GET    /api/predict/v1/credits/transactions
       → Get transaction history
       Query: ?limit=50&offset=0&type=consumption|purchase|all

GET    /api/predict/v1/credits/packages
       → List available credit packages
       Response: { packages: [...] }

POST   /api/predict/v1/credits/purchase
       → Create Stripe checkout for credits
       Body: { packageId, successUrl, cancelUrl }
       Response: { checkoutUrl, sessionId }

POST   /api/predict/v1/credits/purchase/direct
       → Purchase with saved payment method
       Body: { packageId, paymentMethodId }
       Response: { transaction, newBalance }

PUT    /api/predict/v1/credits/auto-refill
       → Configure auto-refill settings
       Body: { enabled, amount, trigger, paymentMethodId }

GET    /api/predict/v1/credits/estimate
       → Estimate cost for a hypothetical request
       Query: ?model=claude-sonnet-4&inputTokens=1000&outputTokens=2000
       Response: { costCents, breakdown }
```

### 5.3 BYOK Key Management

```
GET    /api/predict/v1/byok
       → List user's BYOK keys
       Response: { keys: [{ provider, last4, enabled, stats }] }

POST   /api/predict/v1/byok
       → Add a new BYOK key
       Body: { provider, apiKey, label }
       Response: { keyId, validated }

PUT    /api/predict/v1/byok/:provider
       → Update BYOK key settings
       Body: { enabled, label }

DELETE /api/predict/v1/byok/:provider
       → Remove BYOK key

POST   /api/predict/v1/byok/:provider/validate
       → Re-validate BYOK key
       Response: { valid, error? }

POST   /api/predict/v1/byok/:provider/test
       → Test BYOK key with a minimal API call
       Response: { success, latencyMs, model }
```

### 5.4 API Key Management (Extended)

```
GET    /api/predict/v1/api-keys
       → List user's API keys with billing info

POST   /api/predict/v1/api-keys
       → Create API key with billing mode
       Body: { name, billingMode, creditAccountId?, limits? }
       Response: { apiKey, rawKey (once) }

POST   /api/predict/v1/api-keys/auto-provision
       → Auto-provision API key linked to credits
       Response: { apiKey, rawKey (once) }

PATCH  /api/predict/v1/api-keys/:id
       → Update key settings
       Body: { name?, billingMode?, limits? }

DELETE /api/predict/v1/api-keys/:id
       → Revoke API key
```

### 5.5 Webhooks (License Server)

```
POST   /api/predict/v1/webhooks/license-server/credits
       Events:
       - credits.purchased → Add credits to user
       - credits.refunded → Deduct credits
       - credits.auto_refill.success → Record auto-refill
       - credits.auto_refill.failed → Alert user
```

### 5.6 Super Admin Endpoints (Pricing & Package Management)

```
# Model Pricing Management (super_admin only)
GET    /api/predict/v1/admin/ai-billing/pricing
       → List all model pricing configurations

POST   /api/predict/v1/admin/ai-billing/pricing
       → Add new model pricing
       Body: { provider, model, displayName, inputCostPer1k, outputCostPer1k, markupPercent }

PUT    /api/predict/v1/admin/ai-billing/pricing/:id
       → Update model pricing
       Body: { inputCostPer1k?, outputCostPer1k?, markupPercent?, isActive? }

DELETE /api/predict/v1/admin/ai-billing/pricing/:id
       → Deactivate model pricing (soft delete)

# Credit Package Management (super_admin only)
GET    /api/predict/v1/admin/ai-billing/packages
       → List all credit packages

POST   /api/predict/v1/admin/ai-billing/packages
       → Create new credit package
       Body: { name, description, creditAmountCents, priceCents, bonusCents, badge }

PUT    /api/predict/v1/admin/ai-billing/packages/:id
       → Update credit package
       Body: { name?, priceCents?, bonusCents?, isActive?, badge? }

DELETE /api/predict/v1/admin/ai-billing/packages/:id
       → Deactivate package (soft delete)

# Global AI Billing Settings (super_admin only)
GET    /api/predict/v1/admin/ai-billing/settings
       → Get global AI billing settings

PUT    /api/predict/v1/admin/ai-billing/settings
       → Update global settings
       Body: { minTopupCents, defaultAutoRefillMaxCount, globalMarkupPercent, ... }

# Credit Adjustments (super_admin only)
POST   /api/predict/v1/admin/ai-billing/credits/:userId/adjust
       → Manually adjust user's credits
       Body: { amountCents, reason }
```

### 5.7 Org Owner Endpoints (Org Limits)

```
# Org AI Limits Management (org owner/admin only)
GET    /api/predict/v1/org/ai-limits
       → Get org's AI spending limits

PUT    /api/predict/v1/org/ai-limits
       → Update org AI limits
       Body: {
         defaultDailyLimitCents,      -- Default for members ($10 default)
         defaultMonthlyLimitCents,
         orgDailyLimitCents,          -- Total org limit
         orgMonthlyLimitCents,
         allowMemberOverride,         -- Can members raise their own limits?
         maxMemberDailyLimitCents,    -- Max a member can set if override allowed
         byokAllowedRoles             -- ['owner', 'admin'] or ['owner', 'admin', 'member']
       }

GET    /api/predict/v1/org/ai-usage
       → Get org's AI usage breakdown by member

POST   /api/predict/v1/org/ai-limits/member/:userId
       → Set specific limit for a member (override default)
       Body: { dailyLimitCents, monthlyLimitCents }
```

---

## 6. Service Implementation

### 6.1 AI Billing Router Service

The central service that routes AI requests to the appropriate billing mode.

```typescript
// packages/be/src/billing/services/ai-billing-router.ts

export interface BillingContext {
  mode: 'subscription' | 'credits' | 'byok';
  userId: string;
  organizationId?: string;

  // Resolved values
  apiKey?: string;           // For BYOK
  creditAccountId?: string;  // For credits
  provider: string;
  model: string;

  // Limits
  remainingTokens?: number;
  remainingCredits?: number;
}

export interface BillingResult {
  success: boolean;
  mode: 'subscription' | 'credits' | 'byok';
  cost?: {
    inputTokens: number;
    outputTokens: number;
    totalCostCents: number;
  };
  newBalance?: number;
  error?: string;
}

/**
 * Resolve billing context before making an LLM call
 */
export async function resolveBillingContext(
  userId: string,
  estimatedInputTokens: number,
  estimatedOutputTokens: number,
  preferredModel?: string,
  preferredProvider?: string
): Promise<BillingContext> {
  // 1. Get user's billing config
  const config = await getUserBillingConfig(userId);

  // 2. Try primary mode
  const primaryContext = await tryBillingMode(
    config.primaryMode,
    userId,
    estimatedInputTokens,
    estimatedOutputTokens,
    preferredModel,
    preferredProvider
  );

  if (primaryContext.available) {
    return primaryContext.context;
  }

  // 3. Try fallback mode
  if (config.fallbackMode !== 'none') {
    const fallbackContext = await tryBillingMode(
      config.fallbackMode,
      userId,
      estimatedInputTokens,
      estimatedOutputTokens,
      preferredModel,
      preferredProvider
    );

    if (fallbackContext.available) {
      return fallbackContext.context;
    }
  }

  // 4. No valid billing mode
  throw new InsufficientBillingError(
    'No valid billing mode available. ' +
    'Please add credits, configure BYOK, or upgrade your subscription.'
  );
}

/**
 * Record billing after successful LLM call
 */
export async function recordBilling(
  context: BillingContext,
  usage: { inputTokens: number; outputTokens: number },
  llmCallId: string
): Promise<BillingResult> {
  switch (context.mode) {
    case 'subscription':
      return recordSubscriptionUsage(context, usage, llmCallId);

    case 'credits':
      return recordCreditConsumption(context, usage, llmCallId);

    case 'byok':
      return recordBYOKUsage(context, usage, llmCallId);
  }
}
```

### 6.2 Credit Service

```typescript
// packages/be/src/billing/services/ai-credit-service.ts

export interface CreditBalance {
  available: number;        // Available cents
  reserved: number;         // Reserved for pending calls
  effective: number;        // available - reserved
  totalPurchased: number;
  totalConsumed: number;
  autoRefill: {
    enabled: boolean;
    amount: number;
    trigger: number;
  };
}

export interface ReservationResult {
  success: boolean;
  reservationId: string;
  amountReserved: number;
  newAvailable: number;
  error?: string;
}

/**
 * Get or create credit account for user
 */
export async function getOrCreateCreditAccount(userId: string): Promise<CreditBalance>;

/**
 * Check if user has sufficient credits
 */
export async function checkCredits(
  userId: string,
  estimatedCostCents: number
): Promise<{ sufficient: boolean; available: number; required: number }>;

/**
 * Reserve credits before LLM call (prevents overdraft)
 */
export async function reserveCredits(
  userId: string,
  estimatedCostCents: number,
  idempotencyKey: string
): Promise<ReservationResult>;

/**
 * Consume credits after successful LLM call
 */
export async function consumeCredits(
  userId: string,
  reservationId: string,
  actualCostCents: number,
  usage: { inputTokens: number; outputTokens: number; model: string; provider: string },
  llmCallId: string
): Promise<{ newBalance: number; transaction: CreditTransaction }>;

/**
 * Release reservation if LLM call fails
 */
export async function releaseReservation(
  userId: string,
  reservationId: string
): Promise<void>;

/**
 * Add credits from purchase
 */
export async function addPurchasedCredits(
  userId: string,
  amountCents: number,
  bonusCents: number,
  payment: {
    stripePaymentIntentId?: string;
    licenseServerTransactionId?: string;
  },
  idempotencyKey: string
): Promise<{ newBalance: number; transaction: CreditTransaction }>;

/**
 * Calculate cost for tokens
 */
export async function calculateCost(
  inputTokens: number,
  outputTokens: number,
  model: string,
  provider: string
): Promise<{ costCents: number; breakdown: { input: number; output: number; markup: number } }>;

/**
 * Process auto-refill if needed
 */
export async function checkAndProcessAutoRefill(userId: string): Promise<boolean>;
```

### 6.3 BYOK Service

```typescript
// packages/be/src/billing/services/byok-service.ts

export interface BYOKKey {
  id: string;
  provider: 'anthropic' | 'openai';
  last4: string;
  label: string;
  enabled: boolean;
  validated: boolean;
  stats: {
    totalRequests: number;
    totalInputTokens: number;
    totalOutputTokens: number;
    lastUsedAt: string | null;
  };
}

/**
 * Add BYOK key (validates before saving)
 */
export async function addBYOKKey(
  userId: string,
  provider: 'anthropic' | 'openai',
  apiKey: string,
  label?: string
): Promise<{ keyId: string; validated: boolean }>;

/**
 * Get decrypted BYOK key for API call
 */
export async function getDecryptedKey(
  userId: string,
  provider: 'anthropic' | 'openai'
): Promise<string | null>;

/**
 * Validate BYOK key with provider
 */
export async function validateBYOKKey(
  provider: 'anthropic' | 'openai',
  apiKey: string
): Promise<{ valid: boolean; error?: string }>;

/**
 * Record BYOK usage (for stats, no billing)
 */
export async function recordBYOKUsage(
  userId: string,
  provider: 'anthropic' | 'openai',
  usage: { inputTokens: number; outputTokens: number }
): Promise<void>;
```

### 6.4 Credit-Aware LLM Wrapper

```typescript
// packages/be/src/algorithms/llm/client-with-billing.ts

import { llmCall, type LLMCallInput, type LLMCallResult } from './client';
import { resolveBillingContext, recordBilling } from '@/billing/services/ai-billing-router';

export class InsufficientCreditsError extends Error {
  constructor(
    message: string,
    public required: number,
    public available: number
  ) {
    super(message);
    this.name = 'InsufficientCreditsError';
  }
}

export interface LLMCallWithBillingOptions {
  userId: string;
  organizationId?: string;
  bypassBilling?: boolean;  // For admin/testing
  preferredMode?: 'subscription' | 'credits' | 'byok';
}

/**
 * Make LLM call with billing integration
 */
export async function llmCallWithBilling<T>(
  input: LLMCallInput<T>,
  options: LLMCallWithBillingOptions
): Promise<LLMCallResult<T> & { billing: BillingResult }> {
  const { userId, bypassBilling } = options;

  // 1. Bypass billing for admin/testing
  if (bypassBilling) {
    const result = await llmCall(input);
    return {
      ...result,
      billing: { success: true, mode: 'subscription' },
    };
  }

  // 2. Estimate tokens
  const estimatedInputTokens = Math.ceil(
    (input.systemPrompt.length + input.userPrompt.length) / 4
  );
  const estimatedOutputTokens = input.options?.maxTokens || 2000;

  // 3. Resolve billing context (checks balance, limits, etc.)
  const billingContext = await resolveBillingContext(
    userId,
    estimatedInputTokens,
    estimatedOutputTokens,
    input.options?.modelOverride,
    undefined  // preferredProvider
  );

  // 4. For credits mode, reserve credits
  let reservationId: string | undefined;
  if (billingContext.mode === 'credits') {
    const estimatedCost = await calculateCost(
      estimatedInputTokens,
      estimatedOutputTokens,
      billingContext.model,
      billingContext.provider
    );

    const reservation = await reserveCredits(
      userId,
      estimatedCost.costCents,
      `llm:${Date.now()}:${Math.random().toString(36).slice(2)}`
    );

    if (!reservation.success) {
      throw new InsufficientCreditsError(
        'Insufficient credits',
        estimatedCost.costCents,
        reservation.newAvailable || 0
      );
    }

    reservationId = reservation.reservationId;
  }

  // 5. For BYOK mode, inject user's API key
  let modifiedInput = input;
  if (billingContext.mode === 'byok' && billingContext.apiKey) {
    // The LLM client will use this key instead of platform key
    modifiedInput = {
      ...input,
      _byokKey: billingContext.apiKey,
      _byokProvider: billingContext.provider,
    } as any;
  }

  try {
    // 6. Make LLM call
    const result = await llmCall(modifiedInput);

    // 7. Generate call ID for logging
    const llmCallId = crypto.randomUUID();

    // 8. Record billing
    const billingResult = await recordBilling(
      billingContext,
      {
        inputTokens: result.usage.inputTokens || 0,
        outputTokens: result.usage.outputTokens || 0,
      },
      llmCallId
    );

    // 9. Log to ai_usage_log
    await logAIUsage({
      id: llmCallId,
      userId,
      model: result.model,
      provider: result.provider,
      billingMode: billingContext.mode,
      inputTokens: result.usage.inputTokens,
      outputTokens: result.usage.outputTokens,
      costCents: billingResult.cost?.totalCostCents,
      latencyMs: result.latencyMs,
    });

    return {
      ...result,
      billing: billingResult,
    };

  } catch (error) {
    // 10. Release reservation on failure
    if (reservationId) {
      await releaseReservation(userId, reservationId);
    }
    throw error;
  }
}
```

---

## 7. UI Components

### 7.1 New Pages

| Route | Purpose |
|-------|---------|
| `/settings/ai-billing` | Main AI billing configuration |
| `/settings/ai-billing/credits` | Credit management, purchase, history |
| `/settings/ai-billing/byok` | BYOK key management |
| `/settings/ai-billing/usage` | Usage analytics and breakdown |
| `/settings/ai-billing/api-keys` | API key management with billing |

### 7.2 AI Billing Dashboard (`/settings/ai-billing`)

```tsx
// apps/web/src/app/settings/ai-billing/page.tsx

export default function AIBillingPage() {
  return (
    <SettingsLayout>
      <h1>AI Billing</h1>

      {/* Billing Mode Selector */}
      <BillingModeSelector
        modes={[
          {
            id: 'subscription',
            title: 'Platform Subscription',
            description: 'Use AI included in your plan',
            status: { used: 45000, limit: 500000 },
          },
          {
            id: 'credits',
            title: 'Pay-Per-Use Credits',
            description: 'Top up and pay only for what you use',
            balance: 2500,
          },
          {
            id: 'byok',
            title: 'Bring Your Own Key',
            description: 'Use your own API keys',
            configured: { anthropic: true, openai: false },
          },
        ]}
      />

      {/* Quick Stats */}
      <UsageOverview
        todaySpend={125}
        monthSpend={3450}
        topModels={['claude-sonnet-4', 'claude-haiku-4.5']}
      />

      {/* Spend Limits */}
      <SpendLimitsCard
        daily={{ current: 125, limit: 500 }}
        monthly={{ current: 3450, limit: 10000 }}
      />

      {/* Recent Activity */}
      <RecentActivityTable limit={10} />
    </SettingsLayout>
  );
}
```

### 7.3 Credits Page (`/settings/ai-billing/credits`)

```tsx
// apps/web/src/app/settings/ai-billing/credits/page.tsx

export default function CreditsPage() {
  return (
    <SettingsLayout>
      <h1>AI Credits</h1>

      {/* Balance Card */}
      <CreditBalanceCard
        available={2500}
        reserved={50}
        lowBalanceAlert={500}
      />

      {/* Purchase Packages */}
      <CreditPackagesGrid
        packages={packages}
        onPurchase={handlePurchase}
      />

      {/* Auto-Refill Settings */}
      <AutoRefillSettings
        enabled={true}
        amount={5000}
        trigger={500}
        paymentMethod="**** 4242"
      />

      {/* Transaction History */}
      <TransactionHistory
        transactions={transactions}
        pagination={pagination}
      />
    </SettingsLayout>
  );
}
```

### 7.4 BYOK Page (`/settings/ai-billing/byok`)

```tsx
// apps/web/src/app/settings/ai-billing/byok/page.tsx

export default function BYOKPage() {
  return (
    <SettingsLayout>
      <h1>Bring Your Own Keys</h1>

      <Alert variant="info">
        Use your own API keys to have charges billed directly to your
        OpenAI or Anthropic account. Perfect for high-volume users with
        existing API agreements.
      </Alert>

      {/* Provider Cards */}
      <BYOKProviderCard
        provider="anthropic"
        configured={true}
        last4="...abc1"
        status="validated"
        stats={{ requests: 1250, tokens: 2500000 }}
      />

      <BYOKProviderCard
        provider="openai"
        configured={false}
      />

      {/* Add Key Modal */}
      <AddBYOKKeyModal
        provider={selectedProvider}
        onSubmit={handleAddKey}
      />
    </SettingsLayout>
  );
}
```

---

## 8. License Server Integration

**License Server Location:** `/Users/justin/Documents/GitHub/LicenseServer`

**Current Stack:**
- Express.js + TypeScript
- PostgreSQL + Prisma ORM
- Stripe integration (subscriptions, one-time, metered billing)
- JWT authentication

### 8.1 License Server Database Additions

Add to `prisma/schema.prisma`:

```prisma
// =============================================================================
// CREDIT BALANCE SYSTEM
// =============================================================================

// Credit balance per customer
model CreditBalance {
  id              String    @id @default(uuid())
  customerId      String    @unique
  customer        Customer  @relation(fields: [customerId], references: [id], onDelete: Cascade)

  // Balance (in cents, 1 cent = 1 credit)
  availableCents  Int       @default(0)
  reservedCents   Int       @default(0)    // Reserved for in-flight operations

  // Lifetime stats
  totalPurchased  Int       @default(0)
  totalConsumed   Int       @default(0)
  totalBonus      Int       @default(0)
  totalRefunded   Int       @default(0)

  // Auto-refill settings
  autoRefillEnabled       Boolean   @default(false)
  autoRefillAmountCents   Int?      // Amount to purchase
  autoRefillTriggerCents  Int?      // Trigger when below this
  autoRefillMaxCount      Int       @default(3)      // Max refills before re-auth
  autoRefillCurrentCount  Int       @default(0)      // Current count since last auth
  autoRefillLastAuthAt    DateTime?
  autoRefillPaymentMethodId String? // Stripe payment method

  // Low balance alert
  lowBalanceAlertCents    Int       @default(500)    // $5 default
  lowBalanceAlertedAt     DateTime?

  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt

  transactions    CreditTransaction[]
}

// All credit movements (audit log)
model CreditTransaction {
  id              String    @id @default(uuid())
  creditBalanceId String
  creditBalance   CreditBalance @relation(fields: [creditBalanceId], references: [id], onDelete: Cascade)

  // Transaction details
  type            CreditTransactionType
  amountCents     Int       // Positive = add, negative = deduct
  balanceBefore   Int
  balanceAfter    Int

  // Purchase attribution
  stripePaymentIntentId String?
  stripeChargeId        String?
  creditPackageId       String?
  creditPackage         CreditPackage? @relation(fields: [creditPackageId], references: [id])

  // Consumption attribution (from Predict)
  externalCallId  String?   // LLM call ID from Predict
  model           String?
  provider        String?
  inputTokens     Int?
  outputTokens    Int?

  // Metadata
  description     String?
  metadata        Json?
  idempotencyKey  String?   @unique

  createdAt       DateTime  @default(now())
}

enum CreditTransactionType {
  PURCHASE
  CONSUMPTION
  RESERVATION
  RELEASE
  BONUS
  REFUND
  ADJUSTMENT
  AUTO_REFILL
  EXPIRY
}

// Credit packages (admin-managed)
model CreditPackage {
  id              String    @id @default(uuid())

  // Package details
  name            String
  description     String?
  creditAmountCents Int     // Credits received
  priceCents      Int       // Price to pay
  currency        String    @default("USD")

  // Bonus
  bonusCents      Int       @default(0)
  bonusDescription String?

  // Stripe integration
  stripeProductId String?
  stripePriceId   String?

  // Display
  isActive        Boolean   @default(true)
  isFeatured      Boolean   @default(false)
  sortOrder       Int       @default(0)
  badge           String?   // "Best Value", "Most Popular"

  // Validity
  validFrom       DateTime  @default(now())
  validUntil      DateTime?

  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt

  transactions    CreditTransaction[]
}

// Global credit settings (singleton)
model CreditSettings {
  id              String    @id @default("settings")

  // Minimum purchase
  minPurchaseCents Int      @default(500)   // $5 minimum

  // Auto-refill defaults
  defaultAutoRefillMaxCount Int @default(3)

  // Credit expiry (NULL = never)
  creditExpiryDays Int?

  // Feature flags
  creditsEnabled    Boolean @default(true)
  autoRefillEnabled Boolean @default(true)

  updatedAt       DateTime  @updatedAt
}

// Add relation to existing Customer model
model Customer {
  // ... existing fields ...
  creditBalance   CreditBalance?
}
```

### 8.2 License Server Service Implementation

Create `src/services/credit.service.ts`:

```typescript
// src/services/credit.service.ts

import { prisma } from '../config/database';
import { CreditTransactionType, Prisma } from '@prisma/client';
import { stripe } from './payment.service';
import { v4 as uuid } from 'uuid';

export interface CreditBalanceResponse {
  available: number;
  reserved: number;
  effective: number;
  lifetime: {
    purchased: number;
    consumed: number;
    bonus: number;
    refunded: number;
  };
  autoRefill: {
    enabled: boolean;
    amount: number | null;
    trigger: number | null;
    maxCount: number;
    currentCount: number;
    requiresReauth: boolean;
  };
}

/**
 * Get or create credit balance for customer
 */
export async function getOrCreateCreditBalance(customerId: string): Promise<CreditBalanceResponse> {
  let balance = await prisma.creditBalance.findUnique({
    where: { customerId },
  });

  if (!balance) {
    balance = await prisma.creditBalance.create({
      data: { customerId },
    });
  }

  return {
    available: balance.availableCents,
    reserved: balance.reservedCents,
    effective: balance.availableCents - balance.reservedCents,
    lifetime: {
      purchased: balance.totalPurchased,
      consumed: balance.totalConsumed,
      bonus: balance.totalBonus,
      refunded: balance.totalRefunded,
    },
    autoRefill: {
      enabled: balance.autoRefillEnabled,
      amount: balance.autoRefillAmountCents,
      trigger: balance.autoRefillTriggerCents,
      maxCount: balance.autoRefillMaxCount,
      currentCount: balance.autoRefillCurrentCount,
      requiresReauth: balance.autoRefillCurrentCount >= balance.autoRefillMaxCount,
    },
  };
}

/**
 * Reserve credits for an operation (prevents overdraft)
 */
export async function reserveCredits(
  customerId: string,
  amountCents: number,
  idempotencyKey: string
): Promise<{ success: boolean; reservationId: string; available: number }> {
  return prisma.$transaction(async (tx) => {
    const balance = await tx.creditBalance.findUnique({
      where: { customerId },
    });

    if (!balance || balance.availableCents - balance.reservedCents < amountCents) {
      return {
        success: false,
        reservationId: '',
        available: balance?.availableCents ?? 0,
      };
    }

    // Create reservation transaction
    const reservationId = uuid();
    await tx.creditTransaction.create({
      data: {
        creditBalanceId: balance.id,
        type: CreditTransactionType.RESERVATION,
        amountCents: -amountCents,
        balanceBefore: balance.availableCents,
        balanceAfter: balance.availableCents,
        idempotencyKey,
        metadata: { reservationId },
      },
    });

    // Update reserved amount
    await tx.creditBalance.update({
      where: { id: balance.id },
      data: { reservedCents: balance.reservedCents + amountCents },
    });

    return {
      success: true,
      reservationId,
      available: balance.availableCents - balance.reservedCents - amountCents,
    };
  });
}

/**
 * Consume credits (converts reservation to consumption)
 */
export async function consumeCredits(
  customerId: string,
  reservationId: string,
  actualAmountCents: number,
  usage: {
    externalCallId?: string;
    model?: string;
    provider?: string;
    inputTokens?: number;
    outputTokens?: number;
  }
): Promise<{ newBalance: number }> {
  return prisma.$transaction(async (tx) => {
    const balance = await tx.creditBalance.findUnique({
      where: { customerId },
    });

    if (!balance) throw new Error('Credit balance not found');

    // Find reservation
    const reservation = await tx.creditTransaction.findFirst({
      where: {
        creditBalanceId: balance.id,
        type: CreditTransactionType.RESERVATION,
        metadata: { path: ['reservationId'], equals: reservationId },
      },
    });

    const reservedAmount = reservation ? Math.abs(reservation.amountCents) : actualAmountCents;

    // Create consumption transaction
    await tx.creditTransaction.create({
      data: {
        creditBalanceId: balance.id,
        type: CreditTransactionType.CONSUMPTION,
        amountCents: -actualAmountCents,
        balanceBefore: balance.availableCents,
        balanceAfter: balance.availableCents - actualAmountCents,
        externalCallId: usage.externalCallId,
        model: usage.model,
        provider: usage.provider,
        inputTokens: usage.inputTokens,
        outputTokens: usage.outputTokens,
      },
    });

    // Update balance
    const updated = await tx.creditBalance.update({
      where: { id: balance.id },
      data: {
        availableCents: balance.availableCents - actualAmountCents,
        reservedCents: Math.max(0, balance.reservedCents - reservedAmount),
        totalConsumed: balance.totalConsumed + actualAmountCents,
      },
    });

    // Check if auto-refill needed
    await checkAutoRefill(tx, updated);

    return { newBalance: updated.availableCents };
  });
}

/**
 * Add credits from purchase
 */
export async function addPurchasedCredits(
  customerId: string,
  amountCents: number,
  bonusCents: number,
  payment: {
    stripePaymentIntentId?: string;
    stripeChargeId?: string;
    packageId?: string;
  },
  idempotencyKey: string
): Promise<{ newBalance: number }> {
  return prisma.$transaction(async (tx) => {
    let balance = await tx.creditBalance.findUnique({
      where: { customerId },
    });

    if (!balance) {
      balance = await tx.creditBalance.create({
        data: { customerId },
      });
    }

    const totalAmount = amountCents + bonusCents;

    // Create purchase transaction
    await tx.creditTransaction.create({
      data: {
        creditBalanceId: balance.id,
        type: CreditTransactionType.PURCHASE,
        amountCents: amountCents,
        balanceBefore: balance.availableCents,
        balanceAfter: balance.availableCents + amountCents,
        stripePaymentIntentId: payment.stripePaymentIntentId,
        stripeChargeId: payment.stripeChargeId,
        creditPackageId: payment.packageId,
        idempotencyKey,
      },
    });

    // Create bonus transaction if applicable
    if (bonusCents > 0) {
      await tx.creditTransaction.create({
        data: {
          creditBalanceId: balance.id,
          type: CreditTransactionType.BONUS,
          amountCents: bonusCents,
          balanceBefore: balance.availableCents + amountCents,
          balanceAfter: balance.availableCents + totalAmount,
          description: 'Purchase bonus',
        },
      });
    }

    // Update balance
    const updated = await tx.creditBalance.update({
      where: { id: balance.id },
      data: {
        availableCents: balance.availableCents + totalAmount,
        totalPurchased: balance.totalPurchased + amountCents,
        totalBonus: balance.totalBonus + bonusCents,
      },
    });

    return { newBalance: updated.availableCents };
  });
}

/**
 * Create Stripe checkout session for credit purchase
 */
export async function createCreditCheckoutSession(
  customerId: string,
  packageId: string,
  successUrl: string,
  cancelUrl: string
): Promise<{ checkoutUrl: string; sessionId: string }> {
  const customer = await prisma.customer.findUnique({
    where: { id: customerId },
  });

  if (!customer) throw new Error('Customer not found');

  const pkg = await prisma.creditPackage.findUnique({
    where: { id: packageId, isActive: true },
  });

  if (!pkg) throw new Error('Credit package not found');

  // Create Stripe checkout session
  const session = await stripe.checkout.sessions.create({
    customer: customer.stripeCustomerId || undefined,
    customer_email: !customer.stripeCustomerId ? customer.email : undefined,
    mode: 'payment',
    line_items: [{
      price_data: {
        currency: pkg.currency.toLowerCase(),
        unit_amount: pkg.priceCents,
        product_data: {
          name: pkg.name,
          description: pkg.description || undefined,
        },
      },
      quantity: 1,
    }],
    success_url: successUrl,
    cancel_url: cancelUrl,
    metadata: {
      type: 'credit_purchase',
      customerId,
      packageId,
      creditAmount: pkg.creditAmountCents.toString(),
      bonusAmount: pkg.bonusCents.toString(),
    },
  });

  return {
    checkoutUrl: session.url!,
    sessionId: session.id,
  };
}

/**
 * Check and process auto-refill if needed
 */
async function checkAutoRefill(
  tx: Prisma.TransactionClient,
  balance: any
): Promise<boolean> {
  if (!balance.autoRefillEnabled) return false;
  if (!balance.autoRefillTriggerCents) return false;
  if (!balance.autoRefillAmountCents) return false;
  if (!balance.autoRefillPaymentMethodId) return false;

  // Check if below trigger
  if (balance.availableCents > balance.autoRefillTriggerCents) return false;

  // Check if max refills reached (requires re-authorization)
  if (balance.autoRefillCurrentCount >= balance.autoRefillMaxCount) {
    // TODO: Send notification to user to re-authorize
    return false;
  }

  // TODO: Trigger auto-refill via Stripe PaymentIntent
  // This should be handled asynchronously to avoid blocking

  return true;
}
```

### 8.3 License Server API Routes

Add to `src/routes/portal.routes.ts`:

```typescript
// Credit Balance
router.get('/credits', authenticate, async (req, res) => {
  const balance = await creditService.getOrCreateCreditBalance(req.user.id);
  res.json({ success: true, data: balance });
});

// Credit Packages
router.get('/credits/packages', async (req, res) => {
  const packages = await prisma.creditPackage.findMany({
    where: { isActive: true },
    orderBy: { sortOrder: 'asc' },
  });
  res.json({ success: true, data: packages });
});

// Purchase Credits (Stripe Checkout)
router.post('/credits/checkout', authenticate, async (req, res) => {
  const { packageId, successUrl, cancelUrl } = req.body;
  const session = await creditService.createCreditCheckoutSession(
    req.user.id,
    packageId,
    successUrl,
    cancelUrl
  );
  res.json({ success: true, data: session });
});

// Credit Transaction History
router.get('/credits/transactions', authenticate, async (req, res) => {
  const { limit = 50, offset = 0, type } = req.query;
  const transactions = await prisma.creditTransaction.findMany({
    where: {
      creditBalance: { customerId: req.user.id },
      ...(type && type !== 'all' ? { type: type as any } : {}),
    },
    orderBy: { createdAt: 'desc' },
    take: Number(limit),
    skip: Number(offset),
  });
  res.json({ success: true, data: transactions });
});

// Configure Auto-Refill
router.put('/credits/auto-refill', authenticate, async (req, res) => {
  const { enabled, amountCents, triggerCents, paymentMethodId } = req.body;
  const balance = await prisma.creditBalance.update({
    where: { customerId: req.user.id },
    data: {
      autoRefillEnabled: enabled,
      autoRefillAmountCents: amountCents,
      autoRefillTriggerCents: triggerCents,
      autoRefillPaymentMethodId: paymentMethodId,
      autoRefillCurrentCount: 0, // Reset count on re-configuration
      autoRefillLastAuthAt: new Date(),
    },
  });
  res.json({ success: true, data: balance });
});

// Reserve Credits (called from Predict)
router.post('/credits/reserve', authenticate, async (req, res) => {
  const { amountCents, idempotencyKey } = req.body;
  const result = await creditService.reserveCredits(
    req.user.id,
    amountCents,
    idempotencyKey
  );
  res.json({ success: true, data: result });
});

// Consume Credits (called from Predict)
router.post('/credits/consume', authenticate, async (req, res) => {
  const { reservationId, amountCents, usage } = req.body;
  const result = await creditService.consumeCredits(
    req.user.id,
    reservationId,
    amountCents,
    usage
  );
  res.json({ success: true, data: result });
});

// Release Reservation (called from Predict on failure)
router.post('/credits/release', authenticate, async (req, res) => {
  const { reservationId } = req.body;
  await creditService.releaseReservation(req.user.id, reservationId);
  res.json({ success: true });
});
```

### 8.4 Required License Server Endpoints Summary

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/portal/credits` | GET | Get credit balance |
| `/api/portal/credits/packages` | GET | List credit packages |
| `/api/portal/credits/checkout` | POST | Create Stripe checkout |
| `/api/portal/credits/transactions` | GET | Transaction history |
| `/api/portal/credits/auto-refill` | PUT | Configure auto-refill |
| `/api/portal/credits/reserve` | POST | Reserve credits (from Predict) |
| `/api/portal/credits/consume` | POST | Consume credits (from Predict) |
| `/api/portal/credits/release` | POST | Release reservation (from Predict) |
| `/api/admin/credits/packages` | CRUD | Manage credit packages |
| `/api/admin/credits/pricing` | CRUD | Manage model pricing |
| `/api/admin/credits/settings` | GET/PUT | Global settings |
| `/api/admin/credits/adjust` | POST | Adjust user credits |

### 8.5 Stripe Webhook Handler Addition

Add to `src/routes/webhooks.ts`:

```typescript
case 'checkout.session.completed': {
  const session = event.data.object as Stripe.Checkout.Session;

  // Check if this is a credit purchase
  if (session.metadata?.type === 'credit_purchase') {
    const { customerId, packageId, creditAmount, bonusAmount } = session.metadata;

    await creditService.addPurchasedCredits(
      customerId,
      parseInt(creditAmount),
      parseInt(bonusAmount),
      {
        stripePaymentIntentId: session.payment_intent as string,
        packageId,
      },
      `checkout:${session.id}`
    );

    // Send webhook to Predict
    await sendWebhook('credits.purchased', {
      customerId,
      amount: parseInt(creditAmount),
      bonus: parseInt(bonusAmount),
      transactionId: session.id,
      stripePaymentIntentId: session.payment_intent,
    });
  }
  break;
}

case 'charge.refunded': {
  const charge = event.data.object as Stripe.Charge;

  // Check if this was a credit purchase
  const transaction = await prisma.creditTransaction.findFirst({
    where: { stripeChargeId: charge.id },
  });

  if (transaction) {
    // Deduct the refunded credits
    await creditService.refundCredits(
      transaction.creditBalance.customerId,
      charge.amount_refunded,
      charge.id
    );

    // Send webhook to Predict
    await sendWebhook('credits.refunded', {
      customerId: transaction.creditBalance.customerId,
      amount: charge.amount_refunded,
      transactionId: charge.id,
    });
  }
  break;
}
```

```yaml
# Credit Balance
GET /api/portal/credits/balance
  → { balance: number, reserved: number }

# Credit Packages
GET /api/portal/credits/packages
  → { packages: CreditPackage[] }

# Purchase Credits (Stripe Checkout)
POST /api/portal/credits/checkout
  Body: { packageId, successUrl, cancelUrl }
  → { checkoutUrl, sessionId }

# Purchase Credits (Direct with saved payment method)
POST /api/portal/credits/purchase
  Body: { packageId, paymentMethodId }
  → { transaction, newBalance }

# Auto-Refill Configuration
PUT /api/portal/credits/auto-refill
  Body: { enabled, amount, trigger, paymentMethodId }
  → { success }

# Transaction History
GET /api/portal/credits/transactions
  Query: ?limit=50&offset=0
  → { transactions: Transaction[], total: number }
```

### 8.2 Webhook Events from License Server

```yaml
credits.purchased:
  # Sent after successful Stripe payment
  data:
    customerId: string
    customerEmail: string
    amount: number          # Credits purchased
    bonus: number           # Bonus credits
    transactionId: string
    stripePaymentIntentId: string

credits.refunded:
  # Sent when credits are refunded
  data:
    customerId: string
    amount: number
    reason: string
    transactionId: string

credits.auto_refill.triggered:
  # Sent when auto-refill is triggered
  data:
    customerId: string
    amount: number
    trigger: number
    status: 'pending' | 'success' | 'failed'

credits.auto_refill.failed:
  # Sent when auto-refill payment fails
  data:
    customerId: string
    amount: number
    error: string
    retryAt: string?
```

### 8.3 Sync Strategy

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                    CREDIT SYNC STRATEGY                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  PRIMARY: Webhooks (real-time)                                              │
│  ───────────────────────────────                                            │
│  • License server sends webhook on purchase/refund                          │
│  • Predict updates local balance immediately                                │
│  • No polling required for normal operations                                │
│                                                                              │
│  SECONDARY: Periodic Sync (hourly)                                          │
│  ─────────────────────────────────                                          │
│  • Scheduler job compares local vs license server                           │
│  • Reconciles any drift (missed webhooks, etc.)                            │
│  • Logs discrepancies for investigation                                     │
│                                                                              │
│  TERTIARY: On-Demand Sync                                                   │
│  ─────────────────────────────                                              │
│  • User can trigger manual sync from UI                                     │
│  • Called on login if last_sync > 24h                                       │
│  • Called before purchase to ensure accurate balance                        │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
```

---

## 9. Implementation Phases

### Phase 1: Database & Core Services (5-6 days)

| Task | Effort | Dependencies |
|------|--------|--------------|
| Create migration (all tables) | 1 day | None |
| Implement `ai-credit-service.ts` | 2 days | Migration |
| Implement `byok-service.ts` | 1 day | Migration |
| Implement `ai-billing-router.ts` | 1 day | Credit + BYOK services |
| Implement model pricing service | 0.5 day | Migration |
| Unit tests | 0.5 day | Services |

### Phase 2: LLM Integration (2-3 days)

| Task | Effort | Dependencies |
|------|--------|--------------|
| Modify `llmCall` for BYOK key injection | 0.5 day | BYOK service |
| Implement `llmCallWithBilling` wrapper | 1 day | Router service |
| Update all LLM call sites to use wrapper | 1 day | Wrapper |
| Integration tests | 0.5 day | All above |

### Phase 3: API Endpoints (2-3 days)

| Task | Effort | Dependencies |
|------|--------|--------------|
| Credits endpoints (balance, transactions, packages) | 1 day | Credit service |
| BYOK endpoints (add, remove, validate) | 0.5 day | BYOK service |
| Billing config endpoints | 0.5 day | Router service |
| API key endpoints (extended) | 0.5 day | Services |
| Webhook handler for license server | 0.5 day | Credit service |

### Phase 4: License Server Integration (2-3 days)

| Task | Effort | Dependencies |
|------|--------|--------------|
| Coordinate with license server team | 0.5 day | None |
| Implement credit purchase flow | 1 day | License server ready |
| Implement auto-refill flow | 0.5 day | Purchase flow |
| Implement sync mechanism | 0.5 day | Credit service |
| End-to-end testing | 0.5 day | All above |

### Phase 5: UI Implementation (3-4 days)

| Task | Effort | Dependencies |
|------|--------|--------------|
| AI Billing dashboard page | 1 day | API endpoints |
| Credits page (balance, purchase, history) | 1 day | API endpoints |
| BYOK page (key management) | 0.5 day | API endpoints |
| Usage analytics page | 0.5 day | API endpoints |
| Components (cards, modals, tables) | 1 day | Pages |

### Phase 6: Polish & Launch (2 days)

| Task | Effort | Dependencies |
|------|--------|--------------|
| Error handling & edge cases | 0.5 day | All |
| Admin tools (credit adjustments) | 0.5 day | All |
| Documentation | 0.5 day | All |
| Monitoring & alerts setup | 0.5 day | All |

**Total: 16-21 days**

---

## 10. Migration Strategy

### 10.1 Existing Users

No migration needed for existing users — current users are non-paying beta users.
New billing modes will be available immediately upon deployment.

### 10.2 Feature Flags

```typescript
const AI_BILLING_FLAGS = {
  // Enable credits system
  CREDITS_ENABLED: true,

  // Enable user-level BYOK (vs org-level only)
  USER_BYOK_ENABLED: true,

  // Enable auto-refill
  AUTO_REFILL_ENABLED: true,

  // Minimum credit purchase (cents)
  MIN_CREDIT_PURCHASE: 500,

  // Maximum credit balance (cents) - fraud prevention
  MAX_CREDIT_BALANCE: 1000000,
};
```

### 10.3 Rollout Plan

1. **Week 1:** Deploy to staging, internal testing
2. **Week 2:** Beta with 10 selected users
3. **Week 3:** Gradual rollout (10% → 50% → 100%)
4. **Week 4:** Full GA, deprecate org-level-only BYOK

---

## 11. Monitoring & Alerts

### 11.1 Key Metrics

| Metric | Alert Threshold |
|--------|-----------------|
| Credit purchase failures | > 5% failure rate |
| BYOK validation failures | > 10% failure rate |
| Billing router errors | Any errors |
| Auto-refill failures | Any failures |
| Credit sync drift | > $10 difference |
| Low balance users | For proactive outreach |

### 11.2 Dashboards

- **Credits Overview:** Total credits sold, consumed, refunded
- **BYOK Adoption:** % users with BYOK, usage by provider
- **Billing Mode Distribution:** Subscription vs Credits vs BYOK
- **Revenue:** Credit purchases by day/week/month

---

## 12. Security Considerations

### 12.1 BYOK Key Security

- **Encryption:** AES-256-GCM with per-user salt
- **Storage:** Only encrypted form in DB; plaintext only in memory during API call
- **Access:** User can only access their own keys
- **Validation:** Keys validated on add and periodically re-validated
- **Audit:** All key operations logged

### 12.2 Credit Security

- **Idempotency:** All transactions have idempotency keys
- **Double-spend prevention:** Reservation system prevents overdraft
- **Audit trail:** Every credit movement logged with before/after
- **Fraud limits:** Max balance, max single purchase, velocity limits
- **Reconciliation:** Periodic sync with license server

### 12.3 API Key Security

- **Hashing:** Keys hashed with SHA-256 + salt
- **Rate limiting:** Per-key rate limits
- **Spend limits:** Per-key daily/monthly spend caps
- **Revocation:** Immediate revocation capability

---

## 13. Cost Analysis

### 13.1 Platform Costs

| Component | Cost | Notes |
|-----------|------|-------|
| Stripe fees | 2.9% + $0.30 per transaction | On credit purchases |
| License server | Existing infrastructure | No additional |
| Database storage | ~$0.10/GB/month | Minimal impact |
| API calls to providers | Pass-through | Covered by credits/BYOK |

### 13.2 Pricing Strategy

**Credit Exchange Rate:** 1 cent = 1 credit (simple mental math)

**Example Pricing:**
- Claude Sonnet 4: ~0.4 credits per average response
- Claude Haiku 4.5: ~0.04 credits per average response
- GPT-4o: ~0.35 credits per average response

**Markup:** 10% over raw API costs (already in pricing table)

**Packages:**
- $5 → 500 credits (no bonus)
- $20 → 2,000 credits (no bonus)
- $50 → 5,500 credits (10% bonus)
- $100 → 11,500 credits (15% bonus)
- $250 → 30,000 credits (20% bonus)
- $1,000 → 125,000 credits (25% bonus)

---

## 14. Key Files Reference

| File | Purpose | Status |
|------|---------|--------|
| `db/migrations/147_ai_billing_credits.sql` | Database schema (Predict) | ✅ Created |
| `/Users/justin/Documents/GitHub/LicenseServer/prisma/schema.prisma` | Database schema (License Server) | ✅ Updated |
| `packages/be/src/billing/services/ai-credit-service.ts` | Credit operations | ✅ Created |
| `packages/be/src/billing/services/byok-service.ts` | BYOK key management | ✅ Created |
| `packages/be/src/billing/services/ai-billing-router.ts` | Billing mode routing | ✅ Created |
| `packages/be/src/algorithms/llm/client-with-billing.ts` | Billing-aware LLM wrapper | ✅ Created |
| `apps/web/src/app/settings/ai-billing/` | UI pages | ⏳ Pending |
| `apps/web/src/app/api/predict/v1/credits/` | API routes | ⏳ Pending |
| `apps/web/src/app/api/predict/v1/byok/` | API routes | ⏳ Pending |

---

## 15. Open Questions

1. **Credit expiry:** Should credits expire after N months? (Recommended: No expiry)

2. **Refund policy:** Under what conditions can credits be refunded?

3. **Org vs User credits:** Should orgs have shared credit pools, or strictly per-user?

4. **BYOK for Enterprise only?** Or available to all tiers?

5. **Minimum balance for auto-refill?** Prevent micro-transactions?

6. **Grace period:** Allow small negative balance for in-flight requests?

---

## 16. Success Criteria

| Metric | Target |
|--------|--------|
| Credit purchase conversion | > 5% of free users |
| BYOK adoption (Enterprise) | > 30% |
| Billing errors | < 0.1% |
| Auto-refill success rate | > 99% |
| Time to first credit purchase | < 5 minutes |
| Support tickets (billing) | < 1% of users |

---

## Appendix A: API Response Examples

### Get Credits Balance

```json
GET /api/predict/v1/credits

{
  "available": 2500,
  "reserved": 50,
  "effective": 2450,
  "lifetime": {
    "purchased": 10000,
    "consumed": 7500,
    "bonus": 1000,
    "refunded": 0
  },
  "autoRefill": {
    "enabled": true,
    "amount": 5000,
    "trigger": 500,
    "paymentMethod": {
      "type": "card",
      "last4": "4242",
      "brand": "visa"
    }
  },
  "limits": {
    "daily": { "current": 125, "limit": 500 },
    "monthly": { "current": 3450, "limit": null }
  }
}
```

### Get AI Billing Config

```json
GET /api/predict/v1/ai-billing

{
  "primaryMode": "credits",
  "fallbackMode": "subscription",
  "subscription": {
    "plan": "pro",
    "used": 45000,
    "limit": 500000,
    "resetsAt": "2026-05-01T00:00:00Z"
  },
  "credits": {
    "available": 2500,
    "enabled": true
  },
  "byok": {
    "anthropic": { "configured": true, "enabled": true, "last4": "abc1" },
    "openai": { "configured": false, "enabled": false }
  },
  "limits": {
    "dailySpend": 500,
    "monthlySpend": null,
    "perRequest": 10000
  }
}
```

### Credit Transaction

```json
{
  "id": "ctx_abc123",
  "type": "consumption",
  "amount": -4,
  "balanceBefore": 2504,
  "balanceAfter": 2500,
  "details": {
    "model": "claude-sonnet-4-20250514",
    "provider": "anthropic",
    "inputTokens": 1250,
    "outputTokens": 450,
    "llmCallId": "llm_xyz789"
  },
  "createdAt": "2026-04-30T10:15:30Z"
}
```

---

## Phase 7: License Server Customer Wiring + Direct Stripe Fallback

> Added 2026-04-30 to complete the end-to-end credit purchase flow.

### Overview

Phase 7 addresses the final wiring needed to make credit purchases work:

1. **Customer Mapping** — Maps Predict users to License Server customers
2. **JWT Authentication** — Generates signed tokens for License Server API calls
3. **Direct Stripe Fallback** — Enables credit purchases even without License Server
4. **Webhook Handling** — Processes purchase confirmations from both providers

### Architecture

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                         CREDIT PURCHASE FLOW                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  User clicks "Buy Credits" in /settings/ai-billing/credits                 │
│                              │                                              │
│                              ▼                                              │
│  ┌──────────────────────────────────────────────────────────────────┐      │
│  │                  handleCreateCreditCheckout                       │      │
│  │  Checks: isLicenseServerConfigured() || isStripeConfigured()     │      │
│  └──────────────────────────────────────────────────────────────────┘      │
│                              │                                              │
│              ┌───────────────┴───────────────┐                              │
│              │                               │                              │
│              ▼                               ▼                              │
│  ┌────────────────────────┐    ┌────────────────────────┐                  │
│  │   LICENSE SERVER PATH  │    │   DIRECT STRIPE PATH   │                  │
│  ├────────────────────────┤    ├────────────────────────┤                  │
│  │ 1. Get/create customer │    │ 1. Get credit package  │                  │
│  │    mapping             │    │    from local DB       │                  │
│  │ 2. Generate JWT token  │    │ 2. Create Stripe       │                  │
│  │ 3. Call License Server │    │    checkout session    │                  │
│  │    /credits/checkout   │    │ 3. Return checkout URL │                  │
│  │ 4. Return checkout URL │    │                        │                  │
│  └────────────────────────┘    └────────────────────────┘                  │
│              │                               │                              │
│              ▼                               ▼                              │
│  ┌────────────────────────────────────────────────────────────────────┐    │
│  │                    USER COMPLETES STRIPE PAYMENT                    │    │
│  └────────────────────────────────────────────────────────────────────┘    │
│              │                               │                              │
│              ▼                               ▼                              │
│  ┌────────────────────────┐    ┌────────────────────────┐                  │
│  │  LICENSE SERVER        │    │  STRIPE WEBHOOK        │                  │
│  │  Webhook:              │    │  checkout.session      │                  │
│  │  credits.purchased     │    │  .completed            │                  │
│  ├────────────────────────┤    ├────────────────────────┤                  │
│  │ Calls Predict webhook: │    │ Extracts metadata:     │                  │
│  │ /webhooks/license-     │    │ - user_id              │                  │
│  │ server                 │    │ - credit_cents         │                  │
│  │                        │    │ - bonus_cents          │                  │
│  └────────────────────────┘    └────────────────────────┘                  │
│              │                               │                              │
│              └───────────────┬───────────────┘                              │
│                              ▼                                              │
│  ┌──────────────────────────────────────────────────────────────────┐      │
│  │                    addPurchasedCredits()                          │      │
│  │  Updates billing.ai_credits balance + creates transaction log    │      │
│  └──────────────────────────────────────────────────────────────────┘      │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

### Database Schema (Migration 148)

```sql
-- Customer mapping table
CREATE TABLE billing.license_server_customers (
  user_id UUID PRIMARY KEY REFERENCES auth.users(id),
  license_server_customer_id VARCHAR(255) NOT NULL UNIQUE,
  license_server_stripe_id VARCHAR(255),
  synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Webhook idempotency tracking
CREATE TABLE billing.license_server_webhook_events (
  id VARCHAR(255) PRIMARY KEY,
  event_type VARCHAR(100) NOT NULL,
  user_id UUID REFERENCES auth.users(id),
  payload JSONB,
  processed BOOLEAN NOT NULL DEFAULT FALSE,
  processing_error TEXT,
  processed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Additional columns on existing tables
ALTER TABLE billing.ai_credits
  ADD COLUMN license_server_customer_id VARCHAR(255),
  ADD COLUMN total_refunded_cents INTEGER DEFAULT 0;

ALTER TABLE billing.ai_credit_transactions
  ADD COLUMN external_id VARCHAR(255),
  ADD COLUMN metadata JSONB,
  ADD COLUMN license_server_transaction_id VARCHAR(255);
```

### Customer Service

**Location:** `packages/be/src/integrations/license-server/customer-service.ts`

```typescript
// Get or create customer mapping
async function getOrCreateLicenseServerCustomer(userId: string): Promise<CustomerMapping>

// Generate JWT token for License Server API
function generateLicenseServerToken(userId: string, customerId: string): string

// Convenience wrapper
async function getLicenseServerAuthToken(userId: string): Promise<string>

// Balance sync from webhooks
async function syncCreditBalanceFromLicenseServer(userId: string, balance: {...}): Promise<void>

// Transaction recording
async function recordLicenseServerTransaction(userId: string, txn: {...}): Promise<void>
```

### Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `LICENSE_SERVER_URL` | For LS path | Base URL of License Server |
| `LICENSE_SERVER_API_KEY` | For LS path | API key for server-to-server calls |
| `LICENSE_SERVER_JWT_SECRET` | For LS path | Shared secret for JWT signing |
| `LICENSE_SERVER_WEBHOOK_SECRET` | For LS path | Webhook signature verification |
| `STRIPE_SECRET_KEY` | For direct path | Stripe secret key |
| `STRIPE_WEBHOOK_SECRET` | For direct path | Stripe webhook signature secret |

### Webhook Endpoints

| Endpoint | Source | Events |
|----------|--------|--------|
| `/api/predict/v1/webhooks/license-server` | License Server | `credits.purchased`, `credits.refunded`, `credits.balance_sync`, `credits.auto_refill.*` |
| `/api/predict/v1/billing/webhooks/license-server` | License Server | `checkout.completed`, `subscription.*`, `license.*` |
| `/api/predict/v1/billing/webhooks/stripe` | Stripe | `checkout.session.completed`, `customer.subscription.*`, `invoice.*` |

### Deployment Checklist

1. **Apply migration 148:**
   ```bash
   cat db/migrations/148_license_server_customers.sql | ssh -i ~/.ssh/agencio-predict-sg.pem \
     ec2-user@54.255.100.122 "docker exec -i predict-postgres psql -U postgres -d predict_db"
   ```

2. **Set environment variables in Vercel:**
   ```bash
   # For License Server path
   echo "https://licensing.agencio.cloud" | npx vercel env add LICENSE_SERVER_URL production --yes
   echo "your-api-key" | npx vercel env add LICENSE_SERVER_API_KEY production --yes
   echo "your-jwt-secret" | npx vercel env add LICENSE_SERVER_JWT_SECRET production --yes
   echo "your-webhook-secret" | npx vercel env add LICENSE_SERVER_WEBHOOK_SECRET production --yes

   # For direct Stripe path (already configured via Stripe billing)
   # STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET should already be set
   ```

3. **Configure License Server webhook (if using LS path):**
   - URL: `https://predict.agencio.cloud/api/predict/v1/webhooks/license-server`
   - Events: `credits.purchased`, `credits.refunded`, `credits.balance_sync`
   - Secret: Must match `LICENSE_SERVER_WEBHOOK_SECRET`

4. **Configure Stripe webhook (if using direct path):**
   - URL: `https://predict.agencio.cloud/api/predict/v1/billing/webhooks/stripe`
   - Events: `checkout.session.completed` (add to existing webhook)
   - Secret: Already configured as `STRIPE_WEBHOOK_SECRET`

5. **Deploy:**
   ```bash
   npx vercel --prod --yes
   ```

### Testing

**Test License Server path:**
1. Set all `LICENSE_SERVER_*` env vars
2. Navigate to `/settings/ai-billing/credits`
3. Click "Buy" on any package
4. Complete Stripe checkout
5. Verify credits appear in balance

**Test direct Stripe path:**
1. Remove/unset `LICENSE_SERVER_URL`
2. Ensure `STRIPE_SECRET_KEY` is set
3. Navigate to `/settings/ai-billing/credits`
4. Click "Buy" on any package
5. Complete Stripe checkout
6. Verify credits appear in balance
