Reeve
Services API

Credits & Billing

Credit system implementation, Stripe integration, and tier-based usage metering.

Credits & Billing

The billing system combines prepaid credits for LLM usage with tier-based metering for platform features.

Credit Service

The CreditService class manages credit operations:

Balance Management

class CreditService:
    def get_balance(self, user_id: str) -> BalanceResponse:
        """Get current credit balance and metadata."""
        
    def purchase(self, user_id: str, amount: int) -> PurchaseResponse:
        """Create Stripe Checkout session for credit purchase."""
        
    def deduct(self, user_id: str, amount: float) -> DeductResponse:
        """Deduct credits after LLM usage (called by gateway)."""
        
    def deduct_batch(self, deductions: list) -> BatchResponse:
        """Batch deductions for efficiency (gateway flush)."""

Stripe Checkout Flow

User clicks "Buy Credits"
  → Frontend calls POST /api/credits/purchase { amount: 25 }
    → Services creates Stripe Checkout session
      → User redirected to Stripe
        → Payment succeeds
          → Stripe fires webhook: payment_intent.succeeded
            → Services credits the user's balance
              → User sees updated balance

Webhook Processing

@router.post("/api/credits/webhook")
async def stripe_webhook(request: Request):
    payload = await request.body()
    sig = request.headers.get("stripe-signature")
    
    event = stripe.Webhook.construct_event(
        payload, sig, settings.stripe_webhook_secret
    )
    
    if event["type"] == "payment_intent.succeeded":
        payment = event["data"]["object"]
        user_id = payment["metadata"]["user_id"]
        amount = payment["amount"] / 100  # cents to dollars
        
        credit_service.add_credits(user_id, amount)

Insufficient Credits

When credits run out, the deduction endpoint returns an error:

class InsufficientCreditsError(Exception):
    def __init__(self, balance: float, required: float):
        self.balance = balance
        self.required = required

The gateway handles this by notifying the user to purchase more credits or switch to BYOK.

Tier Metering

Quota Registry

23 features with per-tier limits defined in core/tier_quotas.py:

QUOTA_REGISTRY = {
    "ad_generations": {"free": 10, "pro": 100, "enterprise": None},
    "brand_analyses": {"free": 5, "pro": 50, "enterprise": None},
    "connector_syncs": {"free": 20, "pro": 500, "enterprise": None},
    "dashboard_refreshes": {"free": 50, "pro": 500, "enterprise": None},
    "goal_creations": {"free": 5, "pro": 50, "enterprise": None},
    "agent_creations": {"free": 3, "pro": 20, "enterprise": None},
    # ... more features
}

None means unlimited.

Tier Gate Middleware

FastAPI dependency that checks quotas before processing requests:

from api.core.tier_gate import require_tier

@router.post("/api/ads/generate")
async def generate_ad(
    user: AuthUser = Depends(get_current_user),
    _: bool = Depends(require_tier("ad_generations")),
):
    # Only reached if user has quota remaining
    ...

Usage Tracking

The tier_usage table tracks per-feature usage with rolling 30-day windows:

CREATE TABLE tier_usage (
    id SERIAL PRIMARY KEY,
    user_id VARCHAR NOT NULL,
    feature VARCHAR NOT NULL,
    count INTEGER DEFAULT 1,
    window_start TIMESTAMP NOT NULL,
    window_end TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

Usage Check

def check_quota(user_id: str, feature: str, db: Session) -> bool:
    tier = get_user_tier(user_id, db)
    limit = QUOTA_REGISTRY.get(feature, {}).get(tier)
    
    if limit is None:  # Unlimited
        return True
    
    usage = get_rolling_usage(user_id, feature, days=30, db=db)
    return usage < limit

API Endpoints

EndpointMethodAuthDescription
/api/credits/balanceGETUserGet credit balance
/api/credits/purchasePOSTUserStart Stripe checkout
/api/credits/deductPOSTServiceDeduct credits
/api/credits/deduct/batchPOSTServiceBatch deductions
/api/credits/historyGETUserTransaction history
/api/credits/webhookPOSTStripePayment webhooks
/api/tier/usageGETUserCurrent usage vs quotas
/api/tier/checkGETUserCheck specific feature quota

Credits and tier quotas are independent systems. Credits cover LLM costs. Tier quotas limit platform feature usage. A Pro user with BYOK still has tier quotas — they just don't use credits for LLM calls.

On this page