Skip to main content

ADR-015: Usage-Based Metering

Status: Accepted Date: 2025-11-30 Deciders: Architecture Team, Product Team, Billing Team Tags: metering, usage-based-billing, stripe, enterprise, scalability


Context

Enterprise Tier Pricing Challenge

CODITECT Enterprise tier requires fair, transparent pricing based on actual usage rather than fixed seat counts. Enterprise customers have unpredictable usage patterns:

Usage Variability:

  • Seasonal Projects: High usage during product launches, low during planning phases
  • Team Size Fluctuation: Teams scale up for major releases, scale down during maintenance
  • Feature Adoption: Usage increases as teams discover more agents and commands

Current Fixed Pricing Problems:

  • Overpayment: Enterprise pays for 100 seats but only uses 60 on average (40% waste)
  • Underpayment: Rapidly growing teams exceed seat limits, causing license denials
  • Billing Disputes: "We didn't use 100 seats every month, why are we charged for all 100?"

Real-World Enterprise Scenario

Acme Corp - Enterprise License (100 Seats, $5,000/month Fixed)

Month 1 (Product Launch):
- 95 developers active
- 15,000 agent invocations
- 50,000 CLI commands
- Utilization: 95% ✅ Fair price

Month 2 (Post-Launch Lull):
- 40 developers active
- 6,000 agent invocations
- 20,000 CLI commands
- Utilization: 40% ❌ Overpaying $3,000

Month 3 (Rapid Growth):
- 120 developers active (exceeded license!)
- License denials for 20 developers
- Support tickets: "Need more seats ASAP"
- Revenue loss: 20 seats * $50 = $1,000/month

Annual Impact:
- 6 months overpayment: $3,000 * 6 = $18,000
- 6 months underpayment: $1,000 * 6 = $6,000
- Net customer dissatisfaction: HIGH

Business Requirements

Fair Pricing Goals:

  • Pay for What You Use: Charge only for actual usage, not provisioned capacity
  • Transparent Metering: Real-time usage dashboards visible to customers
  • Predictable Billing: Usage caps and alerts to prevent bill shock
  • Scalable Revenue: Revenue grows with customer success (usage increase = value delivered)

Usage Metrics to Track:

  1. Active Developers - Unique developers who used CODITECT (monthly)
  2. Agent Invocations - Total AI agent calls (e.g., orchestrator, codi-documentation-writer)
  3. Command Executions - Total slash command uses (e.g., /git-sync, /analyze)
  4. Storage Usage - Total file storage (GB-months)
  5. API Calls - License validation API requests

Enterprise Pricing Model:

  • Base Fee: $1,000/month (includes 20 active developers)
  • Additional Developers: $40/developer/month
  • Agent Invocations: $0.01 per invocation (bulk discounts available)
  • Commands: $0.001 per command
  • Storage: $0.10 per GB-month

Stripe Usage Records Integration

Stripe supports usage-based billing via Usage Records API:

Stripe Subscription (Enterprise)
├─ Base Fee: $1,000/month (fixed)
└─ Metered Components:
├─ Active Developers (tiered pricing)
├─ Agent Invocations (per-unit pricing)
├─ Commands (per-unit pricing)
└─ Storage (per-GB pricing)

Decision

We will implement usage-based metering with:

  1. Event Sourcing for usage data (PostgreSQL append-only log)
  2. Daily Aggregation via Celery (sum usage per tenant per day)
  3. Stripe Usage Records API for billing (sync aggregated usage to Stripe)
  4. Real-Time Dashboard for customers (usage visibility)
  5. Usage Caps and Alerts (prevent bill shock)

Usage Metering Architecture

┌────────────────────────────────────────────────────────────────┐
│ Usage Metering Flow │
└────────────────────────────────────────────────────────────────┘

Developer Uses CODITECT

│ Invoke orchestrator agent
│ Execute /git-sync command

┌───────────────────────┐
│ CODITECT Client SDK │
│ (.coditect/scripts) │
│ │
│ usage_tracker.py │
│ track_usage() │
└───────┬───────────────┘

│ POST /api/v1/usage/events
│ {
│ "event_type": "agent_invocation",
│ "metadata": {"agent": "orchestrator"},
│ "quantity": 1
│ }

┌───────────────────────┐
│ Usage API (FastAPI) │
│ │
│ POST /usage/events │
│ → Validate │
│ → Write to DB │
└───────┬───────────────┘

│ INSERT INTO usage_events

┌───────────────────────┐
│ PostgreSQL │
│ (Event Sourcing) │
│ │
│ usage_events table │
│ (append-only) │
└───────┬───────────────┘

│ Daily Celery task (01:00 UTC)

┌───────────────────────┐
│ Aggregation Task │
│ │
│ 1. GROUP BY tenant │
│ 2. SUM quantities │
│ 3. Insert summary │
└───────┬───────────────┘

│ INSERT INTO usage_aggregates

┌───────────────────────┐
│ usage_aggregates │
│ (daily summaries) │
│ │
│ tenant_id, date, │
│ metric, quantity │
└───────┬───────────────┘

│ Stripe sync (end of billing cycle)

┌───────────────────────┐
│ Stripe Usage Records │
│ API │
│ │
│ POST /v1/ │
│ subscription_items/ │
│ {item_id}/ │
│ usage_records │
└───────┬───────────────┘

│ Stripe generates invoice

┌───────────────────────┐
│ Stripe Invoice │
│ │
│ Base: $1,000 │
│ + Developers: $800 │
│ + Agents: $150 │
│ + Commands: $20 │
│ = Total: $1,970 │
└───────────────────────┘

Event Sourcing Pattern

Event Stream (Immutable Log)

├─► Event 1: agent_invocation (orchestrator) at 09:15:23
├─► Event 2: command_execution (/git-sync) at 09:15:45
├─► Event 3: agent_invocation (codi-devops-engineer) at 09:16:10
├─► Event 4: storage_usage (1.5 GB) at 09:17:00
├─► ...
├─► Event 10,000: command_execution (/analyze) at 23:59:58


Daily Aggregation (01:00 UTC)

├─► Tenant A: 1,500 agent invocations
├─► Tenant A: 5,000 commands
├─► Tenant A: 45.2 GB-hours storage


Monthly Billing (End of Cycle)

├─► Sum daily aggregates for billing period
├─► Sync to Stripe Usage Records API
├─► Stripe generates invoice

└─► Customer receives itemized bill

Implementation

1. Database Schema

File: backend/usage/models.py

from django.db import models
from django.utils import timezone
import uuid


class UsageEvent(models.Model):
"""
Immutable usage event log (event sourcing pattern).

Every usage action creates an event. Never update or delete events.
"""

class EventType(models.TextChoices):
AGENT_INVOCATION = 'agent_invocation', 'Agent Invocation'
COMMAND_EXECUTION = 'command_execution', 'Command Execution'
STORAGE_USAGE = 'storage_usage', 'Storage Usage'
API_CALL = 'api_call', 'API Call'
ACTIVE_DEVELOPER = 'active_developer', 'Active Developer'

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey('licenses.Tenant', on_delete=models.CASCADE)
license = models.ForeignKey('licenses.License', on_delete=models.CASCADE)

event_type = models.CharField(max_length=50, choices=EventType.choices)
timestamp = models.DateTimeField(default=timezone.now, db_index=True)

# Event-specific metadata
metadata = models.JSONField(default=dict)
# Examples:
# - agent_invocation: {"agent": "orchestrator", "duration_ms": 1500}
# - command_execution: {"command": "/git-sync", "success": true}
# - storage_usage: {"gb": 1.5, "project_root": "/home/user/project"}

quantity = models.DecimalField(max_digits=10, decimal_places=4, default=1.0)
# - agent_invocation: 1
# - storage_usage: 1.5 (GB)
# - api_call: 1

# For tracking unique developers
developer_email = models.EmailField(null=True, blank=True, db_index=True)

class Meta:
db_table = 'usage_events'
ordering = ['-timestamp']
indexes = [
models.Index(fields=['tenant', 'timestamp']),
models.Index(fields=['event_type', 'timestamp']),
models.Index(fields=['developer_email', 'timestamp']),
]

# Partition by month for performance (PostgreSQL 12+)
# partitions = {'timestamp': 'monthly'}

def __str__(self):
return f"{self.event_type} - {self.tenant.name} at {self.timestamp}"


class UsageAggregate(models.Model):
"""
Daily usage aggregates per tenant.

Computed via Celery task (aggregate_daily_usage).
"""

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey('licenses.Tenant', on_delete=models.CASCADE)
license = models.ForeignKey('licenses.License', on_delete=models.CASCADE)

date = models.DateField(db_index=True)
metric = models.CharField(max_length=50) # 'agent_invocations', 'commands', etc.

quantity = models.DecimalField(max_digits=10, decimal_places=4)
unique_developers = models.IntegerField(default=0) # For active_developers metric

# Stripe sync tracking
synced_to_stripe = models.BooleanField(default=False)
stripe_usage_record_id = models.CharField(max_length=255, null=True, blank=True)
synced_at = models.DateTimeField(null=True, blank=True)

class Meta:
db_table = 'usage_aggregates'
ordering = ['-date']
unique_together = [['tenant', 'date', 'metric']]
indexes = [
models.Index(fields=['tenant', 'date']),
models.Index(fields=['synced_to_stripe', 'date']),
]

def __str__(self):
return f"{self.metric}: {self.quantity} for {self.tenant.name} on {self.date}"

2. Usage Tracking Endpoint

File: backend/usage/views.py

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from django.utils import timezone

from .models import UsageEvent
from licenses.models import License


@api_view(['POST'])
def track_usage(request):
"""
Track usage event from CODITECT client.

Request Body:
{
"license_key": "LIC-...",
"event_type": "agent_invocation",
"metadata": {"agent": "orchestrator", "duration_ms": 1500},
"quantity": 1,
"developer_email": "dev@company.com"
}

Response:
{
"event_id": "uuid",
"tracked": true
}

Rate Limiting:
- 10,000 requests/hour per tenant
- Batching recommended for high-volume usage

Returns:
201: Event tracked successfully
400: Invalid request
404: License not found
429: Rate limit exceeded
"""
license_key = request.data.get('license_key')
event_type = request.data.get('event_type')
metadata = request.data.get('metadata', {})
quantity = request.data.get('quantity', 1)
developer_email = request.data.get('developer_email')

# Validate required fields
if not license_key or not event_type:
return Response(
{'error': 'license_key and event_type required'},
status=status.HTTP_400_BAD_REQUEST
)

# Get license and tenant
try:
license_obj = License.objects.select_related('tenant').get(
license_key=license_key
)
except License.DoesNotExist:
return Response(
{'error': 'Invalid license key'},
status=status.HTTP_404_NOT_FOUND
)

# Create usage event
event = UsageEvent.objects.create(
tenant=license_obj.tenant,
license=license_obj,
event_type=event_type,
metadata=metadata,
quantity=quantity,
developer_email=developer_email
)

return Response({
'event_id': str(event.id),
'tracked': True,
'timestamp': event.timestamp
}, status=status.HTTP_201_CREATED)


@api_view(['GET'])
def get_usage_summary(request):
"""
Get usage summary for current billing period.

Query Params:
?license_key=LIC-...
&start_date=2025-11-01
&end_date=2025-11-30

Response:
{
"billing_period": {"start": "2025-11-01", "end": "2025-11-30"},
"summary": {
"agent_invocations": 15000,
"command_executions": 50000,
"storage_gb_hours": 1260.5,
"active_developers": 45,
"api_calls": 125000
},
"projected_cost": 1970.00
}
"""
from .billing import calculate_projected_cost

license_key = request.GET.get('license_key')
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')

# Implementation for summary calculation
# ... (query UsageAggregate for date range)

summary = {
'agent_invocations': 15000,
'command_executions': 50000,
'storage_gb_hours': 1260.5,
'active_developers': 45,
'api_calls': 125000
}

projected_cost = calculate_projected_cost(summary)

return Response({
'billing_period': {'start': start_date, 'end': end_date},
'summary': summary,
'projected_cost': projected_cost
})

3. Daily Aggregation Task

File: backend/usage/tasks.py

from celery import shared_task
from django.utils import timezone
from django.db.models import Sum, Count, Q
from datetime import timedelta
import logging

from .models import UsageEvent, UsageAggregate
from licenses.models import License

logger = logging.getLogger(__name__)


@shared_task(
bind=True,
name='usage.tasks.aggregate_daily_usage',
soft_time_limit=600,
time_limit=660
)
def aggregate_daily_usage(self, date=None):
"""
Aggregate usage events into daily summaries.

Schedule: Daily at 01:00 UTC (configured in celery.py)

Args:
date: Date to aggregate (default: yesterday)

Workflow:
1. Get all usage events for date
2. Group by tenant and metric
3. SUM quantities
4. Count unique developers
5. Insert into UsageAggregate table

Returns:
Dict with aggregation statistics
"""
try:
# Default to yesterday
if date is None:
date = (timezone.now() - timedelta(days=1)).date()

logger.info(f"Starting usage aggregation for {date}")

stats = {
'date': str(date),
'tenants_processed': 0,
'aggregates_created': 0
}

# Get all licenses with usage on this date
licenses = License.objects.filter(
tenant__usage_events__timestamp__date=date
).distinct()

for license_obj in licenses:
tenant = license_obj.tenant
stats['tenants_processed'] += 1

# Aggregate agent invocations
agent_count = UsageEvent.objects.filter(
tenant=tenant,
timestamp__date=date,
event_type=UsageEvent.EventType.AGENT_INVOCATION
).aggregate(total=Sum('quantity'))['total'] or 0

if agent_count > 0:
UsageAggregate.objects.update_or_create(
tenant=tenant,
license=license_obj,
date=date,
metric='agent_invocations',
defaults={'quantity': agent_count}
)
stats['aggregates_created'] += 1

# Aggregate command executions
command_count = UsageEvent.objects.filter(
tenant=tenant,
timestamp__date=date,
event_type=UsageEvent.EventType.COMMAND_EXECUTION
).aggregate(total=Sum('quantity'))['total'] or 0

if command_count > 0:
UsageAggregate.objects.update_or_create(
tenant=tenant,
license=license_obj,
date=date,
metric='command_executions',
defaults={'quantity': command_count}
)
stats['aggregates_created'] += 1

# Aggregate storage usage (average GB for the day)
storage_avg = UsageEvent.objects.filter(
tenant=tenant,
timestamp__date=date,
event_type=UsageEvent.EventType.STORAGE_USAGE
).aggregate(avg_gb=Sum('quantity'))['avg_gb'] or 0

if storage_avg > 0:
UsageAggregate.objects.update_or_create(
tenant=tenant,
license=license_obj,
date=date,
metric='storage_gb_hours',
defaults={'quantity': storage_avg * 24} # GB * hours in day
)
stats['aggregates_created'] += 1

# Count unique active developers
unique_devs = UsageEvent.objects.filter(
tenant=tenant,
timestamp__date=date,
developer_email__isnull=False
).values('developer_email').distinct().count()

if unique_devs > 0:
UsageAggregate.objects.update_or_create(
tenant=tenant,
license=license_obj,
date=date,
metric='active_developers',
defaults={
'quantity': unique_devs,
'unique_developers': unique_devs
}
)
stats['aggregates_created'] += 1

logger.info(
f"Aggregated usage for {tenant.name}",
extra={
'tenant_id': str(tenant.id),
'date': str(date),
'agent_invocations': agent_count,
'command_executions': command_count,
'active_developers': unique_devs
}
)

logger.info(
f"Usage aggregation completed",
extra=stats
)

return stats

except Exception as e:
logger.error(
f"Error in usage aggregation",
extra={'date': str(date), 'error': str(e)},
exc_info=True
)
return {'error': str(e)}

4. Stripe Usage Records Sync

File: backend/usage/stripe_sync.py

import stripe
from django.conf import settings
from django.utils import timezone
from datetime import timedelta
import logging

from .models import UsageAggregate
from licenses.models import License

logger = logging.getLogger(__name__)

stripe.api_key = settings.STRIPE_SECRET_KEY


class StripeUsageSync:
"""
Sync usage aggregates to Stripe Usage Records API.

Stripe Billing Cycle:
- Usage accumulated throughout billing period
- Invoice generated at end of period
- Usage records must be submitted before invoice finalization
"""

def sync_usage_to_stripe(self, tenant, billing_period_start, billing_period_end):
"""
Sync usage for tenant to Stripe.

Args:
tenant: Tenant object
billing_period_start: Start date of billing period
billing_period_end: End date of billing period

Workflow:
1. Get license with Stripe subscription
2. Get usage aggregates for billing period
3. Sum quantities by metric
4. Submit usage records to Stripe
5. Mark aggregates as synced

Returns:
Dict with sync statistics
"""
try:
# Get license with Stripe subscription
license_obj = License.objects.get(
tenant=tenant,
stripe_subscription_id__isnull=False,
status=License.Status.ACTIVE
)

subscription_id = license_obj.stripe_subscription_id

# Get subscription items for usage-based pricing
subscription = stripe.Subscription.retrieve(subscription_id)

# Map metrics to Stripe subscription items
metric_item_map = self._get_metric_item_map(subscription)

# Get usage aggregates for billing period
aggregates = UsageAggregate.objects.filter(
tenant=tenant,
date__gte=billing_period_start,
date__lte=billing_period_end,
synced_to_stripe=False
)

# Group by metric and sum quantities
usage_by_metric = {}
for agg in aggregates:
if agg.metric not in usage_by_metric:
usage_by_metric[agg.metric] = 0
usage_by_metric[agg.metric] += float(agg.quantity)

# Submit usage records to Stripe
stats = {
'tenant_id': str(tenant.id),
'billing_period': f"{billing_period_start} to {billing_period_end}",
'metrics_synced': 0,
'total_usage': {}
}

for metric, quantity in usage_by_metric.items():
subscription_item_id = metric_item_map.get(metric)

if not subscription_item_id:
logger.warning(
f"No Stripe subscription item for metric: {metric}",
extra={'tenant_id': str(tenant.id)}
)
continue

# Create usage record in Stripe
usage_record = stripe.SubscriptionItem.create_usage_record(
subscription_item_id,
quantity=int(quantity),
timestamp=int(timezone.now().timestamp()),
action='set' # 'set' replaces, 'increment' adds
)

# Mark aggregates as synced
UsageAggregate.objects.filter(
tenant=tenant,
date__gte=billing_period_start,
date__lte=billing_period_end,
metric=metric
).update(
synced_to_stripe=True,
stripe_usage_record_id=usage_record.id,
synced_at=timezone.now()
)

stats['metrics_synced'] += 1
stats['total_usage'][metric] = quantity

logger.info(
f"Synced {metric} usage to Stripe",
extra={
'tenant_id': str(tenant.id),
'metric': metric,
'quantity': quantity,
'usage_record_id': usage_record.id
}
)

return stats

except License.DoesNotExist:
logger.error(
f"No active license with Stripe subscription",
extra={'tenant_id': str(tenant.id)}
)
return {'error': 'No active subscription'}

except stripe.error.StripeError as e:
logger.error(
f"Stripe API error during usage sync",
extra={'tenant_id': str(tenant.id), 'error': str(e)},
exc_info=True
)
return {'error': str(e)}

def _get_metric_item_map(self, subscription):
"""
Map CODITECT metrics to Stripe subscription items.

Args:
subscription: Stripe Subscription object

Returns:
Dict mapping metric name -> subscription_item_id

Example:
{
'agent_invocations': 'si_abc123',
'command_executions': 'si_def456',
'storage_gb_hours': 'si_ghi789',
'active_developers': 'si_jkl012'
}
"""
metric_item_map = {}

for item in subscription['items']['data']:
price_metadata = item['price']['metadata']
metric_type = price_metadata.get('metric_type')

if metric_type:
metric_item_map[metric_type] = item['id']

return metric_item_map

5. Usage Dashboard (Customer-Facing)

File: backend/usage/dashboard.py

from django.db.models import Sum
from datetime import timedelta
from django.utils import timezone

from .models import UsageAggregate
from .billing import calculate_projected_cost


class UsageDashboard:
"""
Customer-facing usage dashboard data.
"""

def get_current_period_usage(self, tenant):
"""
Get usage for current billing period.

Returns:
{
'period_start': '2025-11-01',
'period_end': '2025-11-30',
'days_elapsed': 15,
'days_remaining': 15,
'usage': {
'agent_invocations': {
'total': 15000,
'daily_average': 1000,
'projected_month': 30000
},
'active_developers': {
'total': 45,
'included': 20,
'billable': 25,
'cost': 1000.00
}
},
'projected_cost': 1970.00,
'cost_breakdown': {
'base_fee': 1000.00,
'additional_developers': 1000.00,
'agent_invocations': 150.00,
'command_executions': 20.00
}
}
"""
# Get current billing period dates
now = timezone.now()
period_start = now.replace(day=1).date()

# Get aggregates for current period
aggregates = UsageAggregate.objects.filter(
tenant=tenant,
date__gte=period_start,
date__lte=now.date()
).values('metric').annotate(total=Sum('quantity'))

# Calculate usage summary
usage_summary = {}
for agg in aggregates:
metric = agg['metric']
total = float(agg['total'])

usage_summary[metric] = {
'total': total,
'daily_average': total / (now.day),
'projected_month': (total / now.day) * 30
}

# Calculate projected cost
projected_cost = calculate_projected_cost(usage_summary)

return {
'period_start': period_start,
'period_end': now.date(),
'days_elapsed': now.day,
'days_remaining': 30 - now.day,
'usage': usage_summary,
'projected_cost': projected_cost
}

6. Usage Caps and Alerts

File: backend/usage/caps.py

from django.core.mail import send_mail
from django.conf import settings
import logging

from .models import UsageAggregate
from .billing import calculate_projected_cost

logger = logging.getLogger(__name__)


class UsageCaps:
"""
Monitor usage and enforce caps to prevent bill shock.
"""

def check_usage_cap(self, tenant):
"""
Check if tenant is approaching usage cap.

Caps:
- 80% of budget: Warning email
- 90% of budget: Alert email
- 100% of budget: Soft cap (throttle)
- 110% of budget: Hard cap (block)

Returns:
{
'cap_exceeded': False,
'usage_percent': 75.5,
'projected_cost': 1970.00,
'budget': 2500.00
}
"""
# Get tenant's usage budget
budget = tenant.usage_budget or 5000.00 # Default $5K

# Get current usage
dashboard = UsageDashboard()
current_usage = dashboard.get_current_period_usage(tenant)

projected_cost = current_usage['projected_cost']
usage_percent = (projected_cost / budget) * 100

# Check thresholds
if usage_percent >= 100:
# Hard cap - block additional usage
self._send_hard_cap_email(tenant, projected_cost, budget)
return {
'cap_exceeded': True,
'usage_percent': usage_percent,
'projected_cost': projected_cost,
'budget': budget,
'action': 'blocked'
}

elif usage_percent >= 90:
# Soft cap - throttle usage
self._send_soft_cap_email(tenant, projected_cost, budget)
return {
'cap_exceeded': False,
'usage_percent': usage_percent,
'projected_cost': projected_cost,
'budget': budget,
'action': 'throttled'
}

elif usage_percent >= 80:
# Warning - approaching cap
self._send_warning_email(tenant, projected_cost, budget)
return {
'cap_exceeded': False,
'usage_percent': usage_percent,
'projected_cost': projected_cost,
'budget': budget,
'action': 'warning'
}

return {
'cap_exceeded': False,
'usage_percent': usage_percent,
'projected_cost': projected_cost,
'budget': budget,
'action': 'normal'
}

def _send_warning_email(self, tenant, projected_cost, budget):
"""Send 80% usage warning email."""
send_mail(
subject="CODITECT Usage Alert: 80% of Budget",
message=f"Your projected monthly cost is ${projected_cost:.2f}, which is 80% of your ${budget:.2f} budget.",
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[tenant.billing_email]
)

def _send_soft_cap_email(self, tenant, projected_cost, budget):
"""Send 90% usage alert email."""
send_mail(
subject="CODITECT Usage Alert: 90% of Budget - Usage Throttled",
message=f"Your projected monthly cost is ${projected_cost:.2f}, which is 90% of your ${budget:.2f} budget. Usage is now throttled.",
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[tenant.billing_email]
)

def _send_hard_cap_email(self, tenant, projected_cost, budget):
"""Send 100% usage hard cap email."""
send_mail(
subject="CODITECT Usage Alert: Budget Exceeded - Usage Blocked",
message=f"Your projected monthly cost is ${projected_cost:.2f}, which exceeds your ${budget:.2f} budget. Additional usage has been blocked.",
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[tenant.billing_email]
)

Consequences

Positive

Fair Usage-Based Pricing

  • Customers pay only for actual usage
  • Revenue scales with customer success
  • Transparent, predictable billing

Revenue Growth

  • Usage growth = revenue growth (aligned incentives)
  • Average Enterprise customer: $2,500/month (vs. $1,000 fixed)
  • 150% revenue increase from usage billing

Customer Transparency

  • Real-time usage dashboard
  • Projected cost visibility
  • No surprise bills (usage caps)

Scalability

  • Event sourcing handles millions of events
  • Daily aggregation reduces query load
  • Stripe Usage Records API handles billing

Anti-Bill-Shock

  • Usage caps at 80%, 90%, 100%
  • Email alerts at thresholds
  • Throttling and blocking prevent overages

Negative

⚠️ Implementation Complexity

  • Event sourcing architecture
  • Daily aggregation jobs
  • Stripe Usage Records integration
  • 3x development time vs. fixed pricing

⚠️ Billing Unpredictability

  • Monthly bill varies by usage
  • Customers may prefer fixed pricing
  • Mitigation: Usage caps provide ceiling

⚠️ Support Overhead

  • Usage dispute resolution
  • "Why was I charged for X?" questions
  • Mitigation: Detailed usage logs, dashboard

  • ADR-013: Stripe Integration for Billing
  • ADR-012: License Expiration and Renewal
  • ADR-010: Feature Gating Matrix

References


Last Updated: 2025-11-30 Owner: Billing Team, Engineering Team Review Cycle: Quarterly (optimize based on usage patterns)