What is Monthly Recurring Revenue (MRR) for AI Agent Subscriptions?
Monthly Recurring Revenue is the predictable, normalized revenue your AI agent product generates each month from active subscriptions. Unlike total revenue, which can spike with annual prepayments or one-time add-ons, MRR strips away noise and gives you a clean signal of your recurring revenue engine. For AI agent subscriptions — where customers pay a recurring fee to access an intelligent agent that performs tasks, generates insights, or automates workflows — MRR becomes the single most important metric on your dashboard.
MRR is calculated by summing the monthly value of every active subscription, normalized to a monthly cadence. If a customer pays $1,200 annually for your AI code review agent, their MRR contribution is $100. If they pay $50 monthly, it's simply $50. The formula looks straightforward:
MRR = Σ (normalized_monthly_value of each active subscription)
But in practice, MRR breaks into distinct components that tell a richer story:
- New MRR — revenue from brand-new subscribers acquired this month
- Expansion MRR — additional revenue from existing customers upgrading tiers, adding seats, or purchasing add-ons
- Contraction MRR — revenue lost when customers downgrade tiers or remove add-ons (but don't fully cancel)
- Churned MRR — revenue lost from customers who cancel entirely
- Reactivated MRR — revenue from previously churned customers who return
For an AI agent business, these components are critical because AI agent pricing models tend to be hybrid: flat monthly tiers, usage-based overage charges, per-seat licensing, or credits that renew monthly. Your MRR system must handle all of these gracefully.
Why MRR Matters for AI Agent Businesses
AI agent subscriptions live at the intersection of SaaS metrics and AI infrastructure costs. Every time a subscriber asks your agent to summarize a document, run a SQL query, or generate a marketing campaign, you consume compute — LLM tokens, GPU cycles, API calls to vector databases, or orchestrator execution time. Your gross margin depends on the ratio between MRR and these variable costs. Tracking MRR precisely lets you:
- Forecast infrastructure spend — When you know your MRR growth rate, you can project GPU and API budgets weeks ahead, negotiating reserved instances or committed-use discounts with cloud providers
- Calculate unit economics per agent type — Segment MRR by agent persona (e.g., "Code Review Agent" vs. "Customer Support Agent") and compare against their distinct inference costs to identify which agents are profitable
- Set CAC payback benchmarks — If you spend $300 acquiring a customer through content marketing or developer evangelism, you need to know how many months of MRR it takes to recoup that cost
- Drive investor conversations — VCs evaluating AI startups look at MRR growth rate, net dollar retention, and the MRR-to-burn ratio. A clean MRR data pipeline makes due diligence smoother
- Trigger automated actions — When expansion MRR spikes on a particular agent tier, you might auto-scale dedicated capacity or trigger a customer success workflow
Most importantly, MRR decouples revenue recognition from cash collection. In AI agent businesses, customers frequently prepay for credits or annual seats. MRR normalizes everything to a monthly heartbeat, so your growth charts reflect actual usage commitment rather than cash flow lumpiness.
How to Build an MRR System for AI Agent Subscriptions
Step 1: Model Your Subscription Entities
Start by defining the core database models. An AI agent subscription typically has a plan (with tiers), optional add-ons, and usage-based components. Here's a minimal schema using SQLAlchemy for a Python-based billing service:
from datetime import datetime, date
from decimal import Decimal
from sqlalchemy import Column, String, Integer, Numeric, Date, DateTime, ForeignKey, Enum
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
import enum
Base = declarative_base()
class PlanTier(enum.Enum):
STARTER = "starter"
PROFESSIONAL = "professional"
ENTERPRISE = "enterprise"
class SubscriptionStatus(enum.Enum):
ACTIVE = "active"
PAST_DUE = "past_due"
CANCELED = "canceled"
TRIALING = "trialing"
class Subscription(Base):
__tablename__ = "subscriptions"
id = Column(Integer, primary_key=True)
customer_id = Column(String(64), nullable=False, index=True)
plan_tier = Column(Enum(PlanTier), nullable=False)
status = Column(Enum(SubscriptionStatus), nullable=False)
monthly_base_price = Column(Numeric(10, 2), nullable=False)
seats = Column(Integer, default=1)
started_at = Column(Date, nullable=False)
canceled_at = Column(Date, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
addons = relationship("SubscriptionAddon", back_populates="subscription")
class AddonType(enum.Enum):
EXTRA_AGENT_INSTANCE = "extra_agent_instance"
PRIORITY_INFERENCE = "priority_inference"
CUSTOM_INTEGRATIONS = "custom_integrations"
ADVANCED_ANALYTICS = "advanced_analytics"
class SubscriptionAddon(Base):
__tablename__ = "subscription_addons"
id = Column(Integer, primary_key=True)
subscription_id = Column(Integer, ForeignKey("subscriptions.id"), nullable=False)
addon_type = Column(Enum(AddonType), nullable=False)
monthly_price = Column(Numeric(10, 2), nullable=False)
quantity = Column(Integer, default=1)
created_at = Column(DateTime, default=datetime.utcnow)
removed_at = Column(DateTime, nullable=True)
subscription = relationship("Subscription", back_populates="addons")
This schema separates the base subscription from add-ons so you can independently track expansion and contraction. The monthly_base_price stores the normalized monthly value of the chosen plan, while each add-on carries its own monthly_price. When a customer upgrades from Starter ($50/month) to Professional ($150/month), you record the new plan tier and update the monthly_base_price — and that delta becomes expansion MRR.
Step 2: Build the MRR Calculation Engine
With the models in place, build a service that calculates MRR for a given month. This service should be idempotent — you can run it multiple times for the same month and get the same result — which makes it safe for scheduled jobs and retroactive corrections:
from datetime import date, timedelta
from decimal import Decimal
from typing import Dict, List
from dataclasses import dataclass
from dateutil.relativedelta import relativedelta
@dataclass
class MRRBreakdown:
total_mrr: Decimal
new_mrr: Decimal
expansion_mrr: Decimal
contraction_mrr: Decimal
churned_mrr: Decimal
reactivated_mrr: Decimal
subscriber_count: int
class MRRCalculator:
def __init__(self, session):
self.session = session
def calculate_for_month(self, year: int, month: int) -> MRRBreakdown:
# Define the target month's start and end
month_start = date(year, month, 1)
# End is first day of next month
if month == 12:
month_end = date(year + 1, 1, 1)
else:
month_end = date(year, month + 1, 1)
# Also need previous month boundaries for comparison
prev_start = month_start - relativedelta(months=1)
prev_end = month_start
# Fetch all subscriptions that were active at any point during the target month
# A subscription contributes MRR if it was active on the last day of the month,
# but for change analysis we look at the entire month window.
# 1. Active subs at end of this month
active_this_month = self._get_active_subscriptions(as_of=month_end)
# 2. Active subs at end of previous month
active_prev_month = self._get_active_subscriptions(as_of=prev_end)
# Compute total MRR for this month from active subscriptions
total_mrr = sum(
self._sub_mrr(sub) for sub in active_this_month
)
# Identify new subscribers: active this month but NOT active last month,
# and their started_at falls within this month
prev_customer_ids = {sub.customer_id for sub in active_prev_month}
this_customer_ids = {sub.customer_id for sub in active_this_month}
new_ids = this_customer_ids - prev_customer_ids
new_mrr = sum(
self._sub_mrr(sub) for sub in active_this_month
if sub.customer_id in new_ids
and prev_start <= sub.started_at < month_end
)
# Reactivated: customers active this month, not active last month,
# but started_at is before this month (they returned after churning)
reactivated_ids = {
sub.customer_id for sub in active_this_month
if sub.customer_id in new_ids
and sub.started_at < prev_start
}
reactivated_mrr = sum(
self._sub_mrr(sub) for sub in active_this_month
if sub.customer_id in reactivated_ids
)
# Churned: were active last month but NOT active this month
churned_ids = prev_customer_ids - this_customer_ids
churned_mrr = sum(
self._sub_mrr(sub) for sub in active_prev_month
if sub.customer_id in churned_ids
)
# Expansion and contraction: customers active in both months
retained_ids = this_customer_ids & prev_customer_ids
expansion_mrr = Decimal("0.00")
contraction_mrr = Decimal("0.00")
for customer_id in retained_ids:
prev_sub = next(s for s in active_prev_month if s.customer_id == customer_id)
curr_sub = next(s for s in active_this_month if s.customer_id == customer_id)
prev_mrr = self._sub_mrr(prev_sub)
curr_mrr = self._sub_mrr(curr_sub)
delta = curr_mrr - prev_mrr
if delta > 0:
expansion_mrr += delta
elif delta < 0:
contraction_mrr += abs(delta)
return MRRBreakdown(
total_mrr=total_mrr,
new_mrr=new_mrr,
expansion_mrr=expansion_mrr,
contraction_mrr=contraction_mrr,
churned_mrr=churned_mrr,
reactivated_mrr=reactivated_mrr,
subscriber_count=len(active_this_month)
)
def _get_active_subscriptions(self, as_of: date):
"""Return subscriptions that are active as of the given date."""
return self.session.query(Subscription).filter(
Subscription.status.in_([SubscriptionStatus.ACTIVE, SubscriptionStatus.PAST_DUE]),
Subscription.started_at <= as_of,
(Subscription.canceled_at == None) | (Subscription.canceled_at > as_of)
).all()
def _sub_mrr(self, subscription: Subscription) -> Decimal:
"""Calculate the total monthly revenue for a single subscription."""
base = subscription.monthly_base_price * subscription.seats
addon_total = sum(
addon.monthly_price * addon.quantity
for addon in subscription.addons
if addon.removed_at is None
)
return Decimal(str(base)) + Decimal(str(addon_total))
The MRRCalculator uses a comparison approach between two month-end snapshots. This is more reliable than trying to replay events chronologically because it handles edge cases gracefully — a customer who cancels and resubscribes twice in the same month still ends up with the correct classification. The _sub_mrr method normalizes everything to a monthly value by summing the base plan (multiplied by seats) plus active add-ons.
Step 3: Persist MRR Snapshots for Time-Series Analysis
Running MRR calculations on the fly for arbitrary historical dates becomes expensive as your subscription count grows. Instead, materialize monthly snapshots into a dedicated table so you can query MRR trends instantly:
class MRRSnapshot(Base):
__tablename__ = "mrr_snapshots"
id = Column(Integer, primary_key=True)
year = Column(Integer, nullable=False)
month = Column(Integer, nullable=False)
total_mrr = Column(Numeric(12, 2), nullable=False)
new_mrr = Column(Numeric(12, 2), nullable=False)
expansion_mrr = Column(Numeric(12, 2), nullable=False)
contraction_mrr = Column(Numeric(12, 2), nullable=False)
churned_mrr = Column(Numeric(12, 2), nullable=False)
reactivated_mrr = Column(Numeric(12, 2), nullable=False)
subscriber_count = Column(Integer, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
__table_args__ = (
# One snapshot per month
UniqueConstraint("year", "month"),
)
def materialize_monthly_mrr(session, year: int, month: int):
calculator = MRRCalculator(session)
breakdown = calculator.calculate_for_month(year, month)
# Upsert the snapshot
existing = session.query(MRRSnapshot).filter_by(year=year, month=month).first()
if existing:
existing.total_mrr = breakdown.total_mrr
existing.new_mrr = breakdown.new_mrr
existing.expansion_mrr = breakdown.expansion_mrr
existing.contraction_mrr = breakdown.contraction_mrr
existing.churned_mrr = breakdown.churned_mrr
existing.reactivated_mrr = breakdown.reactivated_mrr
existing.subscriber_count = breakdown.subscriber_count
else:
session.add(MRRSnapshot(
year=year, month=month,
total_mrr=breakdown.total_mrr,
new_mrr=breakdown.new_mrr,
expansion_mrr=breakdown.expansion_mrr,
contraction_mrr=breakdown.contraction_mrr,
churned_mrr=breakdown.churned_mrr,
reactivated_mrr=breakdown.reactivated_mrr,
subscriber_count=breakdown.subscriber_count
))
session.commit()
Run materialize_monthly_mrr on the first day of each new month (for the just-completed month) via a cron job or a scheduled Celery task. For backfilling historical data, loop over past months in order. Because the calculator is idempotent, you can safely re-materialize any month to correct data quality issues without double-counting.
Step 4: Expose MRR via an API Endpoint
Your dashboard, forecasting models, and operational alerts will consume MRR data through an API. Here's a FastAPI endpoint that returns MRR for a date range with optional segmentation by plan tier or agent type:
from fastapi import FastAPI, Query
from datetime import date
from typing import Optional, List
from pydantic import BaseModel
app = FastAPI()
class MRRPoint(BaseModel):
year: int
month: int
total_mrr: float
new_mrr: float
expansion_mrr: float
contraction_mrr: float
churned_mrr: float
reactivated_mrr: float
subscriber_count: int
net_new_mrr: float # new + expansion - contraction - churned + reactivated
class MRRResponse(BaseModel):
points: List[MRRPoint]
average_growth_rate: Optional[float]
@app.get("/api/mrr", response_model=MRRResponse)
def get_mrr(
start_year: int = Query(..., ge=2020),
start_month: int = Query(..., ge=1, le=12),
end_year: int = Query(..., ge=2020),
end_month: int = Query(..., ge=1, le=12),
plan_tier: Optional[str] = Query(None),
session = Depends(get_session)
):
query = session.query(MRRSnapshot).filter(
(MRRSnapshot.year > start_year) |
((MRRSnapshot.year == start_year) & (MRRSnapshot.month >= start_month)),
(MRRSnapshot.year < end_year) |
((MRRSnapshot.year == end_year) & (MRRSnapshot.month <= end_month))
).order_by(MRRSnapshot.year, MRRSnapshot.month)
snapshots = query.all()
points = []
for snap in snapshots:
net_new = (
snap.new_mrr + snap.expansion_mrr
- snap.contraction_mrr - snap.churned_mrr
+ snap.reactivated_mrr
)
points.append(MRRPoint(
year=snap.year,
month=snap.month,
total_mrr=float(snap.total_mrr),
new_mrr=float(snap.new_mrr),
expansion_mrr=float(snap.expansion_mrr),
contraction_mrr=float(snap.contraction_mrr),
churned_mrr=float(snap.churned_mrr),
reactivated_mrr=float(snap.reactivated_mrr),
subscriber_count=snap.subscriber_count,
net_new_mrr=float(net_new)
))
# Calculate average month-over-month growth rate
if len(points) >= 2:
growth_rates = []
for i in range(1, len(points)):
prev = points[i-1].total_mrr
curr = points[i].total_mrr
if prev > 0:
growth_rates.append((curr - prev) / prev)
avg_growth = sum(growth_rates) / len(growth_rates) if growth_rates else None
else:
avg_growth = None
return MRRResponse(points=points, average_growth_rate=avg_growth)
This endpoint powers your internal dashboards. The net_new_mrr field gives you a single number that captures the net change in MRR each month — positive means you're growing, negative means you're shrinking. The average growth rate helps investors understand trajectory at a glance.
Step 5: Handle AI-Specific Pricing Nuances
AI agent subscriptions often include usage-based components — token consumption, inference minutes, or API call volumes — that sit outside the fixed subscription model. To incorporate these into MRR without distorting it, treat predictable overage patterns as recurring add-ons rather than one-time charges:
class UsageBasedComponent:
"""
Models a usage-based charge that recurs predictably.
For example, if a customer consistently uses 500k extra tokens
beyond their plan allowance, you can create an addon for the
estimated monthly overage rather than treating it as variable revenue.
"""
def estimate_monthly_overage(self, customer_id: str, lookback_months: int = 3):
"""Use historical usage to predict next month's overage."""
recent_usage = self._fetch_usage_history(customer_id, lookback_months)
if not recent_usage:
return Decimal("0.00")
avg_extra_tokens = sum(recent_usage) / len(recent_usage)
plan_allowance = self._get_plan_allowance(customer_id)
estimated_overage_tokens = max(0, avg_extra_tokens - plan_allowance)
# Convert tokens to dollars (example: $0.02 per 1k tokens)
overage_dollars = (estimated_overage_tokens / 1000) * Decimal("0.02")
return overage_dollars.quantize(Decimal("0.01"))
def sync_overage_addon(self, subscription_id: int):
"""Create or update an overage addon to reflect predicted usage."""
sub = self.session.query(Subscription).get(subscription_id)
estimated = self.estimate_monthly_overage(sub.customer_id)
existing_overage = next(
(a for a in sub.addons if a.addon_type == AddonType.EXTRA_AGENT_INSTANCE
and a.removed_at is None),
None
)
if estimated > 0:
if existing_overage:
existing_overage.monthly_price = estimated
else:
new_addon = SubscriptionAddon(
subscription_id=subscription_id,
addon_type=AddonType.EXTRA_AGENT_INSTANCE,
monthly_price=estimated,
quantity=1
)
self.session.add(new_addon)
self.session.commit()
elif existing_overage and estimated == 0:
existing_overage.removed_at = datetime.utcnow()
self.session.commit()
This approach smooths usage-based revenue into your MRR while keeping the actual billing flexible. When the monthly MRR snapshot runs, it picks up whatever overage add-on is active at month-end, giving you a conservative, predictable revenue figure. The actual invoice might differ slightly from the estimate, but the MRR system prioritizes predictability over penny-perfect accuracy.
Best Practices for MRR with AI Agent Subscriptions
Segment MRR by Agent Type Early
If your platform hosts multiple AI agents — say a code review agent, a documentation agent, and a deployment agent — tag every subscription with an agent_type at creation time. This lets you build per-agent MRR views that reveal which agents drive growth and which ones have high churn. An agent with high expansion MRR (lots of upgrades) but also high contraction MRR may have a pricing or value communication problem worth investigating.
Automate Alerts on MRR Anomalies
Wire your MRR snapshots into an anomaly detection system. A sudden drop in total MRR could indicate a billing outage, a bug that prevented subscription renewals, or a competitor launch. A simple z-score check on the daily MRR delta can catch these before customers notice:
def detect_mrr_anomaly(session, threshold: float = 2.0):
"""Check if the latest MRR change is statistically unusual."""
snapshots = session.query(MRRSnapshot).order_by(
MRRSnapshot.year.desc(), MRRSnapshot.month.desc()
).limit(13).all() # 12 months + current
if len(snapshots) < 6:
return False # Not enough data
deltas = []
for i in range(1, len(snapshots)):
prev_mrr = float(snapshots[i].total_mrr)
curr_mrr = float(snapshots[i-1].total_mrr)
deltas.append(curr_mrr - prev_mrr)
mean_delta = sum(deltas) / len(deltas)
std_delta = (sum((d - mean_delta) ** 2 for d in deltas) / len(deltas)) ** 0.5
if std_delta == 0:
return False
latest_delta = deltas[0]
z_score = (latest_delta - mean_delta) / std_delta
if abs(z_score) > threshold:
send_alert(f"MRR anomaly detected: z-score {z_score:.2f}, delta ${latest_delta:,.2f}")
return True
return False
Treat Trials as a Separate Pipeline, Not as MRR
AI agent trials are critical for conversion, but they are not MRR. Keep trial subscriptions in a dedicated status (TRIALING) and exclude them from all MRR calculations. When a trial converts to a paid plan, record it as new MRR in that month. This separation prevents inflated MRR numbers and gives you clean conversion funnel metrics to optimize.
Reconcile MRR Against Actual Cash Collections Monthly
MRR is an accrual-based metric, but your bank account runs on cash. Every month, reconcile your MRR snapshot against actual invoice payments collected. Build a reconciliation report that flags discrepancies:
class MRRReconciliation:
def reconcile(self, year: int, month: int) -> Dict:
snapshot = self.session.query(MRRSnapshot).filter_by(
year=year, month=month
).first()
cash_collected = self._get_cash_collected_for_month(year, month)
# Cash can diverge from MRR due to:
# - Annual prepayments (cash > MRR)
# - Failed payments / past due accounts (cash < MRR)
# - Timing differences (payment arrives early or late)
mrr_total = float(snapshot.total_mrr)
difference = cash_collected - mrr_total
pct_diff = (difference / mrr_total) * 100 if mrr_total > 0 else 0
return {
"mrr": mrr_total,
"cash_collected": cash_collected,
"difference": difference,
"percentage_difference": pct_diff,
"requires_investigation": abs(pct_diff) > 10
}
A persistent gap between MRR and cash suggests payment failures, billing bugs, or recognition timing issues — all of which directly impact your AI infrastructure budget planning.
Keep MRR Calculation Simple and Deterministic
It's tempting to build a sophisticated MRR system that handles every edge case with custom rules — prorated upgrades, mid-cycle plan changes, credit rollovers. Resist this. The best MRR systems use simple, deterministic rules that everyone in the company can reason about. When you need nuanced revenue analysis, build separate analytical models on top of your raw subscription events rather than complicating the core MRR pipeline. A clean, simple MRR number builds trust across engineering, finance, and leadership.
Version Your MRR Logic
As your AI agent pricing evolves — maybe you introduce a new enterprise tier or switch from per-seat to per-usage pricing — your MRR calculation logic will need to change. Version your MRRCalculator (e.g., v1, v2) and tag every materialized snapshot with the calculator version used. When you transition to a new version, backfill historical snapshots so you can always compare apples-to-apples across time periods.
Conclusion
Monthly Recurring Revenue is the gravitational center of your AI agent subscription business. It transforms a messy stream of upgrades, downgrades, cancellations, and prepayments into a clean, predictable signal that drives infrastructure planning, investor confidence, and operational alerts. By modeling subscriptions with clear entities, building an idempotent monthly calculator, materializing snapshots, and handling AI-specific pricing nuances like usage-based overages, you create a system that scales from your first ten customers to tens of thousands. The code patterns laid out here — SQLAlchemy models, a snapshot-based calculator, FastAPI endpoints, and anomaly detection — give you a production-ready foundation. Start with simple, deterministic rules, segment by agent type, reconcile against cash, and version your logic. With a solid MRR system in place, every other business decision about your AI agents becomes measurably better informed.