Billing for AI Services: Token, GPU, and API Accounting
Note: when an AI platform starts handling thousands of requests per second, billing becomes a bottleneck. LLM tokens are priced using an input/output model, GPU time is billed per second with a minimum charge, and API requests have rate limiting. A configuration error, and the platform could lose up to 10% of revenue. We build billing systems that synchronize these heterogeneous resources into a unified accounting model. Our engineers have 5+ years of experience developing billing for high-load AI services.
How We Ensure Accurate Accounting for Tokens and GPU Hours
The main challenge is combining multiple billing units into a single system. LLM tokens (input/output) are typically priced separately, with progressive discounts for large volumes. GPU hours are billed per second, with a minimum billing period—usually 10 seconds—to avoid microtransactions. API requests often have rate limits with burst quotas. All this must be merged into a consistent invoice that the customer understands. For each resource, we design a separate data model supporting tiered pricing, credits, and minimum charges. The final system uses PostgreSQL for OLTP operations and ClickHouse for analytical aggregations over arbitrary periods. We guarantee transparent accounting with detailed breakdowns by model, endpoint, and time slot.
from dataclasses import dataclass from enum import Enum from decimal import Decimal class BillingUnit(Enum): TOKEN = "token" # 1M tokens GPU_SECOND = "gpu_second" # GPU time REQUEST = "request" # Request GB_MONTH = "gb_month" # Storage @dataclass class PricingRule: resource: BillingUnit unit_price: Decimal tier_breaks: list # [(volume_threshold, discounted_price)] minimum_charge: Decimal = Decimal('0') PRICING = { "llm_input_token": PricingRule( resource=BillingUnit.TOKEN, unit_price=Decimal('0.000005'), # $5 per 1M tokens tier_breaks=[ (10_000_000, Decimal('0.000004')), # >10M → $4/1M (100_000_000, Decimal('0.000003')), # >100M → $3/1M ] ), "gpu_a100_second": PricingRule( resource=BillingUnit.GPU_SECOND, unit_price=Decimal('0.00089'), # ~$3.20/hour per A100 tier_breaks=[], minimum_charge=Decimal('0.01') # Minimum 10 seconds billing ), } | Resource | Unit of Measure | Accounting Features |
|---|---|---|
| LLM tokens | 1M tokens | Input vs output, tiered pricing |
| GPU hours | Second (minimum 10) | Ceiling rounding, GPU types |
| API requests | Each | Rate limiting, burst |
| Storage | GB per month | Considering replication |
Comparison: Real-Time Metering vs. Batch Billing
Instead of relying on hourly batching, real-time metering with Redis and Kafka reduces billing latency to 10 ms. This is 50 times faster than traditional approaches, which is especially important under high load where every request costs money. Such an architecture can reduce revenue loss up to 10% and improve budgeting accuracy.
Why Real-Time Metering Is Critical for AI Billing
Without real-time metering, customers can exceed their budget, and the platform can lose money due to billing delays. We use Redis for fast counters (current balance, rate limiting) and Kafka for auditing. P99 event write latency is under 10 ms. Our certified engineers ensure system fault tolerance.
Metering implementation includes two parallel flows: operational counters in Redis for instant limit control and a Kafka event queue for further aggregation. This ensures a balance between speed and reliability.
class UsageMeter: def __init__(self, redis_client, kafka_producer): self.redis = redis_client self.kafka = kafka_producer async def record_llm_usage(self, customer_id: str, model_id: str, input_tokens: int, output_tokens: int, request_id: str): # 1. Real-time counters in Redis (for rate limiting and balance checks) pipe = self.redis.pipeline() month_key = f"usage:{customer_id}:{self.current_month()}" pipe.hincrby(month_key, f"{model_id}:input_tokens", input_tokens) pipe.hincrby(month_key, f"{model_id}:output_tokens", output_tokens) pipe.expire(month_key, 60 * 60 * 24 * 40) # 40 days await pipe.execute() # 2. Detailed events to Kafka for auditing and billing await self.kafka.send("usage_events", { "event_type": "llm_inference", "customer_id": customer_id, "model_id": model_id, "request_id": request_id, "input_tokens": input_tokens, "output_tokens": output_tokens, "timestamp": datetime.utcnow().isoformat() }) async def record_gpu_job(self, customer_id: str, job_id: str, gpu_type: str, duration_seconds: float): # GPU billing with ceiling rounding to the second billable_seconds = max(ceil(duration_seconds), 10) # Minimum 10 sec await self.kafka.send("usage_events", { "event_type": "gpu_training", "customer_id": customer_id, "job_id": job_id, "gpu_type": gpu_type, "duration_seconds": billable_seconds, "timestamp": datetime.utcnow().isoformat() }) Apache Kafka is used as a reliable event broker, and Redis for low-latency counters. This combination has been tested in production with loads exceeding 10 thousand events per second.
How We Generate Invoices and Control Budgets
Invoices are built from aggregated events in ClickHouse. We support tiered pricing, credits, and discounts. Budget control includes alerts at thresholds and automatic blocking.
class InvoiceGenerator: async def generate_monthly_invoice(self, customer_id: str, billing_period: str) -> Invoice: # Aggregate usage events from ClickHouse usage = await self.clickhouse.query(""" SELECT model_id, sum(input_tokens) as total_input, sum(output_tokens) as total_output, sum(gpu_seconds) as total_gpu, count() as total_requests FROM usage_events WHERE customer_id = %(customer_id)s AND toYYYYMM(timestamp) = %(period)s GROUP BY model_id """, {"customer_id": customer_id, "period": billing_period}) line_items = [] total = Decimal('0') for row in usage: input_cost = self.compute_tiered_price( row['total_input'], PRICING['llm_input_token'] ) output_cost = self.compute_tiered_price( row['total_output'], PRICING['llm_output_token'] ) line_items.append({ 'description': f"{row['model_id']} - Input tokens", 'quantity': row['total_input'], 'unit': "1M tokens", 'unit_price': float(PRICING['llm_input_token'].unit_price * 1_000_000), 'amount': float(input_cost) }) total += input_cost + output_cost # Apply credits and discounts credits = await self.get_customer_credits(customer_id) final_amount = max(total - credits, Decimal('0')) invoice = Invoice( customer_id=customer_id, period=billing_period, line_items=line_items, subtotal=float(total), credits_applied=float(min(credits, total)), total=float(final_amount) ) # Send via Stripe if final_amount > 0: await self.stripe.create_invoice(customer_id, invoice) return invoice async def check_budget_alerts(customer_id: str): customer = await db.get_customer(customer_id) current_spend = await usage_meter.get_current_month_spend(customer_id) thresholds = [0.5, 0.8, 0.9, 1.0] # % of monthly budget for threshold in thresholds: alert_amount = customer.monthly_budget * threshold if current_spend >= alert_amount: alert_key = f"budget_alert:{customer_id}:{threshold}:{current_month()}" if not await redis.exists(alert_key): await send_budget_alert(customer, threshold, current_spend) await redis.setex(alert_key, 86400 * 31, "1") Example tariff configuration
Tariffs are defined in a YAML file: resources, tiered prices, minimum charges. Changing tariffs takes minutes without redeployment.Billing System Development Process
| Stage | Duration | Result |
|---|---|---|
| Tariff and metric analysis | 2–3 days | Data model and specification |
| Microservice design | 3–5 days | Architecture diagrams |
| Metering and billing implementation | 1–3 weeks | Working MVP |
| Payment gateway integration | 3–5 days | Test transactions |
| Testing (unit, load) | 5–7 days | Coverage report and P99 |
| Deployment and documentation | 2–3 days | Operations manual |
Deliverables include: full API documentation, access to source code, team training (2 hours), and one month of post-release support.
Our experience includes more than 50 completed projects in AI billing. The most common cause of revenue loss in practice is incorrect tiered pricing configuration when crossing price thresholds: without proper calculation, the platform charges less than it should. Another frequent issue is lack of edge case handling: canceled requests after inference starts, interrupted GPU jobs, and retry requests after timeout. Each of these scenarios requires explicit handling in billing logic. We verify the tariff model on synthetic data with boundary cases before launch. Contact us for a preliminary audit. We will assess the complexity of your tariffs and propose timelines.







