/template recipe-metered-billing
Commercial License Required
MakerKit requires a commercial license. While Orchestre can help you build with it, you must obtain a valid license from MakerKit for commercial use.
Access: /template recipe-metered-billing or /t recipe-metered-billing
Purpose
Implements a sophisticated usage-based billing system where customers pay based on actual consumption of resources. This recipe creates infrastructure for tracking usage events, aggregating metrics, calculating charges, and integrating with Stripe's metered billing capabilities.
How It Actually Works
The recipe:
- Analyzes your product's usage patterns
- Creates usage tracking infrastructure
- Implements event ingestion pipelines
- Adds aggregation and reporting systems
- Integrates with Stripe metered billing
- Sets up real-time usage dashboards
Use Cases
- API Platforms: Charge per API call
- Cloud Services: Bill for compute hours
- Data Processing: Price by GB processed
- Communication Platforms: Cost per message/minute
- Storage Services: Charge for storage used
- Analytics Tools: Bill by events tracked
Examples
API Usage Billing
bash
/template recipe-metered-billing api-calls
# Implements API metering with:
# - Per-endpoint pricing
# - Rate tier discounts
# - Real-time usage tracking
# - Overage handlingStorage Metering
bash
/template recipe-metered-billing storage
# Creates storage billing with:
# - GB-hours calculation
# - Peak vs average pricing
# - Bandwidth tracking
# - Regional pricingCompute Time Billing
bash
/template recipe-metered-billing compute-hours
# Advanced compute billing with:
# - CPU/GPU hour tracking
# - Instance type pricing
# - Spot vs on-demand rates
# - Resource optimizationWhat Gets Created
Database Schema
sql
-- Usage events table (partitioned by month)
create table usage_events (
id uuid primary key default gen_random_uuid(),
team_id uuid references teams(id) on delete cascade,
user_id uuid references auth.users(id),
event_type text not null,
event_name text not null,
quantity decimal(20,6) not null,
unit text not null,
metadata jsonb default '{}',
idempotency_key text unique,
created_at timestamp with time zone default now()
) partition by range (created_at);
-- Aggregated usage
create table usage_aggregates (
id uuid primary key default gen_random_uuid(),
team_id uuid references teams(id) on delete cascade,
metric_name text not null,
period_start timestamp with time zone not null,
period_end timestamp with time zone not null,
total_usage decimal(20,6) not null,
unit text not null,
tier_breakdown jsonb default '{}',
created_at timestamp with time zone default now(),
unique(team_id, metric_name, period_start)
);
-- Pricing tiers
create table usage_pricing_tiers (
id uuid primary key default gen_random_uuid(),
metric_name text not null,
tier_start decimal(20,6) not null,
tier_end decimal(20,6),
price_per_unit decimal(10,6) not null,
currency text default 'usd',
effective_from timestamp with time zone default now(),
effective_to timestamp with time zone
);
-- Usage alerts
create table usage_alerts (
id uuid primary key default gen_random_uuid(),
team_id uuid references teams(id) on delete cascade,
metric_name text not null,
threshold_value decimal(20,6) not null,
threshold_type text check (threshold_type in ('soft', 'hard')),
action text not null,
is_active boolean default true,
last_triggered_at timestamp with time zone,
created_at timestamp with time zone default now()
);
-- Billing periods
create table metered_billing_periods (
id uuid primary key default gen_random_uuid(),
team_id uuid references teams(id) on delete cascade,
subscription_id text references subscriptions(id),
period_start timestamp with time zone not null,
period_end timestamp with time zone not null,
usage_records jsonb default '{}',
total_amount decimal(10,2),
status text default 'active',
stripe_invoice_id text,
created_at timestamp with time zone default now()
);API Routes
typescript
// app/api/usage/track/route.ts
POST /api/usage/track
- Ingest usage events
- Validate quantities
- Apply idempotency
- Queue for processing
// app/api/usage/report/route.ts
GET /api/usage/report
- Current period usage
- Historical trends
- Cost projections
- Tier breakdowns
// app/api/usage/aggregate/route.ts
POST /api/usage/aggregate
- Run aggregation jobs
- Calculate period totals
- Apply pricing tiers
- Update Stripe usage
// app/api/usage/alerts/route.ts
POST /api/usage/alerts
- Configure usage alerts
- Set thresholds
- Define actions
- Test notificationsReact Components
typescript
// components/billing/usage-dashboard.tsx
- Real-time usage metrics
- Cost accumulation
- Trend visualization
- Alert status
// components/billing/usage-details-table.tsx
- Detailed event log
- Filtering and search
- Export functionality
- Drill-down views
// components/billing/usage-forecast.tsx
- Projected costs
- Usage trends
- Budget tracking
- Optimization tips
// components/billing/usage-alerts-manager.tsx
- Alert configuration
- Threshold settings
- Notification preferences
- Alert historyHooks and Utilities
typescript
// hooks/use-metered-billing.ts
- useUsageTracking()
- useUsageMetrics()
- useUsageCosts()
- useUsageAlerts()
// lib/usage/tracker.ts
- Track usage events
- Batch processing
- Error handling
- Retry logic
// lib/usage/aggregator.ts
- Aggregate by period
- Apply pricing rules
- Calculate totals
- Generate reports
// lib/usage/stripe-sync.ts
- Sync to Stripe
- Create usage records
- Handle failures
- ReconciliationTechnical Details
High-Performance Event Ingestion
typescript
// Batched event processing
class UsageEventProcessor {
private queue: UsageEvent[] = [];
private batchSize = 1000;
private flushInterval = 5000; // 5 seconds
async track(event: UsageEvent) {
// Add idempotency
event.idempotencyKey = event.idempotencyKey || generateKey(event);
this.queue.push(event);
if (this.queue.length >= this.batchSize) {
await this.flush();
}
}
private async flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.batchSize);
try {
await this.processBatch(batch);
} catch (error) {
// Add to dead letter queue
await this.handleFailedBatch(batch, error);
}
}
private async processBatch(events: UsageEvent[]) {
// Bulk insert with conflict handling
await db.insert(usage_events)
.values(events)
.onConflict('idempotency_key')
.doNothing();
}
}Tiered Pricing Calculator
typescript
// Complex pricing tier logic
class TieredPricingCalculator {
async calculate(usage: number, metric: string): Promise<Cost> {
const tiers = await this.getTiers(metric);
let totalCost = 0;
let remainingUsage = usage;
const breakdown = [];
for (const tier of tiers) {
if (remainingUsage <= 0) break;
const tierUsage = Math.min(
remainingUsage,
(tier.end || Infinity) - tier.start
);
const tierCost = tierUsage * tier.pricePerUnit;
totalCost += tierCost;
remainingUsage -= tierUsage;
breakdown.push({
tier: tier.name,
usage: tierUsage,
rate: tier.pricePerUnit,
cost: tierCost,
});
}
return { total: totalCost, breakdown };
}
}Real-time Aggregation
typescript
// Streaming aggregation with time windows
class UsageAggregator {
async aggregateRealtime(teamId: string) {
// Use materialized views for performance
const currentUsage = await db.query(`
SELECT
metric_name,
SUM(quantity) as total_usage,
COUNT(*) as event_count,
MAX(created_at) as last_event
FROM usage_events
WHERE team_id = $1
AND created_at >= date_trunc('hour', CURRENT_TIMESTAMP)
GROUP BY metric_name
`, [teamId]);
// Update cache for dashboard
await cache.set(
`usage:realtime:${teamId}`,
currentUsage,
60 // 1 minute TTL
);
return currentUsage;
}
}Memory Evolution
The recipe creates comprehensive usage tracking memory:
markdown
## Metered Billing Configuration
### Tracked Metrics
- API Calls: $0.001 per request
- Tiers: 0-10K free, 10K-100K $0.001, 100K+ $0.0008
- Storage: $0.10 per GB-month
- Calculated daily, billed monthly
- Bandwidth: $0.05 per GB
- Ingress free, egress charged
### Aggregation Settings
- Real-time: 1-minute windows
- Hourly: For dashboards
- Daily: For reporting
- Monthly: For billing
### Alert Thresholds
- 80% of budget: Email notification
- 100% of budget: Slack alert + email
- 120% of budget: Service throttling
### Integration Status
- Stripe: Usage records synced hourly
- Webhooks: Real-time event ingestion
- Analytics: Grafana dashboards
- Monitoring: DataDog integrationBest Practices
Accurate Usage Tracking
- Use idempotency keys for all events
- Implement client-side event batching
- Add retry logic with exponential backoff
- Validate quantities before tracking
Cost Transparency
- Show real-time usage and costs
- Provide detailed breakdowns
- Offer usage forecasting
- Enable budget alerts
Performance Optimization
- Partition usage tables by time
- Use materialized views for aggregates
- Implement caching strategies
- Archive old usage data
Billing Accuracy
- Reconcile usage daily
- Audit Stripe usage records
- Handle timezone differences
- Support usage corrections
Integration Points
With API Gateway
typescript
// Automatic API usage tracking
const apiMiddleware = async (req, res, next) => {
const startTime = Date.now();
res.on('finish', async () => {
await trackUsage({
teamId: req.teamId,
metric: 'api_calls',
quantity: 1,
metadata: {
endpoint: req.path,
method: req.method,
statusCode: res.statusCode,
duration: Date.now() - startTime,
},
});
});
next();
};With Storage System
typescript
// Storage usage tracking
const storageTracker = {
async onUpload(teamId: string, file: File) {
await trackUsage({
teamId,
metric: 'storage_bytes',
quantity: file.size,
metadata: {
action: 'upload',
fileType: file.type,
},
});
},
async calculateDaily(teamId: string) {
const totalBytes = await getTotalStorage(teamId);
const gbHours = (totalBytes / 1e9) * 24;
await trackUsage({
teamId,
metric: 'storage_gb_hours',
quantity: gbHours,
});
},
};With Background Jobs
typescript
// Compute time tracking
const jobTracker = {
async trackJob(jobId: string, duration: number) {
const job = await getJob(jobId);
await trackUsage({
teamId: job.teamId,
metric: 'compute_seconds',
quantity: duration,
metadata: {
jobType: job.type,
resources: job.resources,
},
});
},
};Troubleshooting
Common Issues
Missing Usage Events
- Check idempotency conflicts
- Verify event batching
- Review error logs
- Test retry mechanism
Incorrect Aggregations
- Verify timezone handling
- Check aggregation windows
- Review pricing tier logic
- Validate calculations
Stripe Sync Failures
- Check API rate limits
- Verify usage record format
- Review webhook logs
- Test manual sync
Debug Helpers
typescript
// Usage debugging tools
const usageDebug = {
async traceEvent(eventId: string) {
const event = await getEvent(eventId);
const aggregations = await getAggregationsForEvent(eventId);
const stripeRecord = await getStripeRecord(eventId);
return {
event,
aggregations,
stripeRecord,
timeline: await getEventTimeline(eventId),
};
},
async validatePeriod(teamId: string, period: Date) {
const events = await getEventsForPeriod(teamId, period);
const calculated = await calculatePeriodUsage(events);
const reported = await getReportedUsage(teamId, period);
return {
eventCount: events.length,
calculated,
reported,
discrepancy: calculated - reported,
};
},
};Advanced Features
Predictive Scaling
typescript
// ML-based usage prediction
const usagePredictor = {
async predictNextPeriod(teamId: string) {
const history = await getUsageHistory(teamId, 90); // 90 days
const model = await loadPredictionModel();
return model.predict({
history,
seasonality: detectSeasonality(history),
trend: calculateTrend(history),
});
},
};Multi-Currency Support
typescript
// Handle different currencies
const currencyHandler = {
async convertUsageCost(amount: number, from: string, to: string) {
const rate = await getExchangeRate(from, to);
return {
original: { amount, currency: from },
converted: { amount: amount * rate, currency: to },
rate,
timestamp: new Date(),
};
},
};Related Commands
/template add-billing- Basic subscription billing/template add-analytics- Usage analytics/template add-webhooks- Event ingestion/template add-admin-dashboard- Usage monitoring
