← Back to DevBytes

Monthly Recurring Revenue with AI Agent Subscriptions

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:

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:

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles