Skip to main content

Billing System

Aragora's billing system provides user authentication, subscription management, usage tracking, and Stripe integration for monetization.

Overview

The billing module consists of four components:

ModulePurpose
models.pyData models for User, Organization, Subscription
jwt_auth.pyJWT token authentication
stripe_client.pyStripe API integration
usage.pyUsage tracking and cost calculation

Cost Visibility

Aragora exposes a cost visibility API and dashboard for tracking spend and budgets. See COST_VISIBILITY.md for endpoints and UI details.

Quick Start

Enable Billing

Set the required environment variables:

# JWT Authentication
export ARAGORA_JWT_SECRET="your-secure-secret-key"
export ARAGORA_JWT_EXPIRY_HOURS=24

# Stripe Integration (optional for paid tiers)
export STRIPE_SECRET_KEY="sk_test_xxx"
export STRIPE_WEBHOOK_SECRET="whsec_xxx"
export STRIPE_PRICE_STARTER="price_xxx"
export STRIPE_PRICE_PROFESSIONAL="price_xxx"
export STRIPE_PRICE_ENTERPRISE="price_xxx"

Subscription Tiers

TierDebates/MonthUsersAPI AccessPrice
FREE101No$0
STARTER502No$99/mo
PROFESSIONAL20010Yes$299/mo
ENTERPRISEUnlimitedUnlimitedYes$999/mo

Tier Features

from aragora.billing.models import SubscriptionTier, TIER_LIMITS

# Get limits for a tier
limits = TIER_LIMITS[SubscriptionTier.PROFESSIONAL]
print(limits.debates_per_month) # 200
print(limits.api_access) # True
print(limits.all_agents) # True

Authentication

JWT Tokens

Create and validate JWT tokens for user sessions:

from aragora.billing.jwt_auth import (
create_access_token,
create_refresh_token,
validate_access_token,
create_token_pair,
)

# Create access token (24-hour default)
token = create_access_token(
user_id="user_123",
email="user@example.com",
org_id="org_456",
role="admin",
)

# Create token pair (access + refresh)
pair = create_token_pair(
user_id="user_123",
email="user@example.com",
org_id="org_456",
)
print(pair.access_token) # JWT for API calls
print(pair.refresh_token) # JWT for token refresh
print(pair.expires_in) # Seconds until expiry

# Validate token
payload = validate_access_token(token)
if payload:
print(payload.user_id) # "user_123"
print(payload.email) # "user@example.com"
print(payload.is_expired) # False

API Key Authentication

Users can generate API keys for programmatic access:

from aragora.billing.models import User

user = User(email="dev@example.com")
api_key = user.generate_api_key() # Returns "ara_xxxxx..."

# Validate API key format
# Keys must start with "ara_" and be at least 15 characters
if api_key.startswith("ara_") and len(api_key) >= 15:
# Valid format (production should validate against database)
pass

Request Authentication

Extract authentication from HTTP requests:

from aragora.billing.jwt_auth import extract_user_from_request

# In a request handler
context = extract_user_from_request(handler)

if context.authenticated:
print(f"User: {context.user_id}")
print(f"Role: {context.role}")
print(f"Token type: {context.token_type}") # "access" or "api_key"

if context.is_admin:
# Allow admin operations
pass

Token Revocation

Revoke tokens to immediately invalidate user sessions:

from aragora.billing.jwt_auth import (
TokenBlacklist,
revoke_token,
is_token_revoked,
)

# Get the singleton blacklist instance
blacklist = TokenBlacklist.get_instance()

# Revoke a specific token (by JTI claim)
revoke_token(token_jti="abc123", reason="user_logout")

# Check if a token is revoked
if is_token_revoked(token_jti="abc123"):
print("Token has been revoked")

# Revoke all tokens for a user (on password change, account compromise)
blacklist.revoke_all_for_user(user_id="user_123", reason="password_changed")

# Cleanup expired entries (automatic, but can be triggered manually)
blacklist.cleanup()

Revocation API Endpoints

# Logout - revokes current access token
POST /api/auth/logout
Authorization: Bearer <access_token>

# Revoke specific token
POST /api/auth/revoke
Authorization: Bearer <access_token>
Content-Type: application/json

{
"token": "<token_to_revoke>",
"reason": "manual_revocation"
}

# Token refresh (automatically revokes old refresh token)
POST /api/auth/refresh
Content-Type: application/json

{
"refresh_token": "<refresh_token>"
}

Response (revoke):

{
"success": true,
"message": "Token revoked successfully"
}

Notifications & Payment Recovery

Aragora can send billing notifications via SMTP or a webhook. Configure these variables in your deployment environment:

VariableDescriptionDefault
ARAGORA_SMTP_HOSTSMTP server host-
ARAGORA_SMTP_PORTSMTP server port587
ARAGORA_SMTP_USERSMTP username-
ARAGORA_SMTP_PASSWORDSMTP password-
ARAGORA_SMTP_FROMFrom email addressbilling@aragora.ai
ARAGORA_NOTIFICATION_WEBHOOKWebhook URL for billing notifications-
ARAGORA_PAYMENT_GRACE_DAYSDays before downgrade after payment failure10

If you enable SMTP, ensure the credentials are stored in your secret manager and that outbound SMTP is allowed from your deployment network.

User Management

Creating Users

from aragora.billing.models import User

# Create user
user = User(email="new@example.com", name="New User")
user.set_password("secure_password")

# User ID is auto-generated
print(user.id) # UUID string

# Verify password
if user.verify_password("secure_password"):
print("Password correct")

Serialization

# Safe serialization (excludes sensitive data)
data = user.to_dict()
# {"id": "...", "email": "...", "has_api_key": True, ...}

# Include sensitive data (for admin views)
data = user.to_dict(include_sensitive=True)
# Includes api_key field

# Restore from dict
restored = User.from_dict(data)

Organization Management

Creating Organizations

from aragora.billing.models import Organization, SubscriptionTier, generate_slug

# Create organization
org = Organization(
name="Acme Corp",
slug=generate_slug("Acme Corp"), # "acme-corp"
tier=SubscriptionTier.PROFESSIONAL,
owner_id="user_123",
)

# Check limits
print(org.limits.debates_per_month) # 200
print(org.debates_remaining) # 200 (at start of month)
print(org.is_at_limit) # False

Usage Tracking

# Increment debate count
if org.increment_debates():
print("Debate started")
else:
print("At limit - upgrade required")

# Reset monthly usage (call at billing cycle)
org.reset_monthly_usage()

Usage Tracking

Track token usage and costs per user/organization:

from aragora.billing.usage import (
UsageTracker,
UsageEvent,
UsageEventType,
calculate_token_cost,
)

# Initialize tracker (stored under ARAGORA_DATA_DIR)
tracker = UsageTracker("usage.db")

# Record a debate event
tracker.record_event(
org_id="org_123",
user_id="user_456",
event_type=UsageEventType.DEBATE,
provider="anthropic",
model="claude-sonnet-4",
tokens_in=1500,
tokens_out=800,
)

# Calculate token cost
cost = calculate_token_cost(
provider="anthropic",
model="claude-sonnet-4",
tokens_in=1500,
tokens_out=800,
)
print(f"Cost: $\{cost\}") # Based on provider pricing

# Get usage summary
summary = tracker.get_usage_summary(
org_id="org_123",
start_date=datetime.now() - timedelta(days=30),
)
print(summary["total_debates"])
print(summary["total_tokens"])
print(summary["total_cost"])

Provider Pricing

Pricing per 1M tokens (as of Jan 2025):

ProviderModelInputOutput
AnthropicClaude Opus 4$15.00$75.00
AnthropicClaude Sonnet 4$3.00$15.00
OpenAIGPT-4o$2.50$10.00
OpenAIGPT-4o Mini$0.15$0.60
GoogleGemini Pro$1.25$5.00
DeepSeekDeepSeek V3$0.14$0.28
OpenRouterDefault$2.00$8.00

Stripe Integration

Setup

  1. Create products and prices in Stripe Dashboard
  2. Set environment variables with price IDs
  3. Configure webhook endpoint

Creating Checkout Sessions

from aragora.billing.stripe_client import (
create_checkout_session,
StripeConfigError,
)
from aragora.billing.models import SubscriptionTier

try:
session = create_checkout_session(
customer_email="user@example.com",
tier=SubscriptionTier.PROFESSIONAL,
success_url="https://app.example.com/success",
cancel_url="https://app.example.com/cancel",
metadata={"org_id": "org_123"},
)
# Redirect user to session.url
except StripeConfigError as e:
print(f"Stripe not configured: \{e\}")

Handling Webhooks

from aragora.billing.stripe_client import (
verify_webhook_signature,
WebhookEvent,
)

def handle_stripe_webhook(request):
payload = request.body
signature = request.headers.get("Stripe-Signature")

# Verify signature
event = verify_webhook_signature(payload, signature)

if event.type == "checkout.session.completed":
# Upgrade organization tier
org_id = event.data.metadata.get("org_id")
# Update organization...

elif event.type == "customer.subscription.deleted":
# Downgrade to free tier
pass

Managing Subscriptions

from aragora.billing.stripe_client import (
get_subscription,
cancel_subscription,
update_subscription,
)

# Get subscription details
sub = get_subscription(subscription_id)
print(sub.status) # "active", "canceled", etc.
print(sub.current_period_end)

# Cancel at period end
cancel_subscription(subscription_id, at_period_end=True)

# Upgrade/downgrade
update_subscription(
subscription_id,
new_price_id="price_enterprise_xxx",
)

Environment Variables

VariableDescriptionRequired
ARAGORA_JWT_SECRETSecret key for JWT signingYes (required for auth; required in prod)
ARAGORA_JWT_EXPIRY_HOURSAccess token expiry (default: 24)No
ARAGORA_REFRESH_TOKEN_EXPIRY_DAYSRefresh token expiry (default: 30)No
STRIPE_SECRET_KEYStripe API secret keyFor paid tiers
STRIPE_WEBHOOK_SECRETWebhook signing secretFor webhooks
STRIPE_PRICE_STARTERStripe price ID for StarterFor paid tiers
STRIPE_PRICE_PROFESSIONALStripe price ID for ProFor paid tiers
STRIPE_PRICE_ENTERPRISEStripe price ID for EnterpriseFor paid tiers

Security Considerations

  1. JWT Secret: Always set ARAGORA_JWT_SECRET in production. Auto-generated secrets are invalidated on restart.

  2. Password Hashing: Passwords are hashed using SHA-256 with unique salts. Never store plain text passwords.

  3. API Keys: Keys are prefixed with ara_ for easy identification. Implement database validation for production.

  4. Stripe Webhooks: Always verify webhook signatures to prevent spoofing.

  5. Token Expiry: Access tokens expire after 24 hours by default. Use refresh tokens for long-lived sessions.

Database Schema

The usage tracker creates these tables:

-- Usage events
CREATE TABLE usage_events (
id TEXT PRIMARY KEY,
org_id TEXT NOT NULL,
user_id TEXT,
event_type TEXT NOT NULL,
provider TEXT,
model TEXT,
tokens_in INTEGER DEFAULT 0,
tokens_out INTEGER DEFAULT 0,
cost_usd REAL DEFAULT 0,
metadata TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Monthly summaries (materialized for performance)
CREATE TABLE usage_monthly (
org_id TEXT NOT NULL,
month TEXT NOT NULL, -- YYYY-MM format
debates INTEGER DEFAULT 0,
api_calls INTEGER DEFAULT 0,
total_tokens INTEGER DEFAULT 0,
total_cost REAL DEFAULT 0,
PRIMARY KEY (org_id, month)
);

See Also