Skip to content

/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:

  1. Analyzes your product's usage patterns
  2. Creates usage tracking infrastructure
  3. Implements event ingestion pipelines
  4. Adds aggregation and reporting systems
  5. Integrates with Stripe metered billing
  6. 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 handling

Storage Metering

bash
/template recipe-metered-billing storage
# Creates storage billing with:
# - GB-hours calculation
# - Peak vs average pricing
# - Bandwidth tracking
# - Regional pricing

Compute 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 optimization

What 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 notifications

React 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 history

Hooks 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
- Reconciliation

Technical 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 integration

Best 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(),
    };
  },
};
  • /template add-billing - Basic subscription billing
  • /template add-analytics - Usage analytics
  • /template add-webhooks - Event ingestion
  • /template add-admin-dashboard - Usage monitoring

Built with ❤️ for the AI Coding community, by Praney Behl