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:
- Active Developers - Unique developers who used CODITECT (monthly)
- Agent Invocations - Total AI agent calls (e.g., orchestrator, codi-documentation-writer)
- Command Executions - Total slash command uses (e.g., /git-sync, /analyze)
- Storage Usage - Total file storage (GB-months)
- 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:
- Event Sourcing for usage data (PostgreSQL append-only log)
- Daily Aggregation via Celery (sum usage per tenant per day)
- Stripe Usage Records API for billing (sync aggregated usage to Stripe)
- Real-Time Dashboard for customers (usage visibility)
- 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
Related ADRs
- 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)