📦 Subscription Revenue Tracker — 技能工具

v1.1.0

SaaS and subscription business revenue intelligence. Track MRR/ARR, calculate churn rate, net revenue retention (NRR), customer lifetime value (LTV), cohort...

0· 137·1 当前·1 累计
下载技能包
License
MIT-0
最后更新
2026/3/19
0
安全扫描
VirusTotal
无害
查看报告
OpenClaw
安全
medium confidence
The skill's instructions and requirements are coherent with a subscription-revenue tool, but the SKILL.md includes examples that inline live API keys and doesn't advise safer credential handling — exercise caution when supplying secrets.
评估建议
This skill appears to do what it claims (compute MRR/ARR, churn, cohorts, and prepare GL entries), but it uses examples that inline secret keys. Before installing or using: (1) Never paste live secret keys into commands or source files — use environment variables or a secrets store and prefer restricted/read-only API keys with minimal scope. (2) Test with test/sandbox credentials (Stripe test keys) rather than sk_live keys. (3) Be careful when running CLI commands that may be saved in shell hist...
详细分析 ▾
用途与能力
The name/description (MRR/ARR, churn, cohorts, QBO journaling) matches the runtime instructions: Stripe/Chargebee API calls, CSV handling, and Python/CLI examples. Optional binaries and APIs listed in metadata (stripe, python3, jq, curl; Stripe/Chargebee/QBO) are appropriate for the stated purpose.
指令范围
Instructions stay within the domain of revenue/KPI computation and GL posting. However, examples show direct use of secret keys (e.g. sk_live_YOUR_KEY) embedded in curl and Python snippets; there is no guidance in the SKILL.md about minimizing exposure (use restricted/read-only keys, environment variables, or token-scoped credentials) or avoiding commit/logging of secrets.
安装机制
No install spec and no code files — instruction-only skill. This is lower-risk because nothing is downloaded or written to disk by the skill itself.
凭证需求
The skill does not declare required environment variables or a primary credential, but the runtime examples clearly expect Stripe/Chargebee API keys. This is proportionate to the purpose, but the lack of declared credential handling and the use of inline live-key examples increases the chance a user will accidentally expose sensitive credentials.
持久化与权限
always is false and the skill has no install or persistent components. Autonomous invocation is allowed (platform default) but there is no extra persistence or system-wide configuration requested.
安全有层次,运行前请审查代码。

License

MIT-0

可自由使用、修改和再分发,无需署名。

运行时依赖

无特殊依赖

版本

latestv1.1.02026/3/17

Fix display name from slug to proper title

无害

安装命令

点击复制
官方npx clawhub@latest install subscription-revenue-tracker
镜像加速npx clawhub@latest install subscription-revenue-tracker --registry https://cn.longxiaskill.com

技能文档

Track MRR/ARR, churn, NRR, cohort retention, and LTV for SaaS and subscription businesses. Produces investor-grade metrics and clean GL entries.


Core Metrics Defined

MetricFormulaWhy It Matters
MRRSum of all active recurring monthly revenuePulse of the business
ARRMRR × 12Annualized scale metric for investors
New MRRRevenue from new customers this monthGrowth engine
Expansion MRRUpgrades / upsells from existing customersEfficiency signal
Contraction MRRDowngrades from existing customersNegative signal
Churned MRRRevenue lost from cancellationsRetention health
Net New MRRNew + Expansion − Contraction − ChurnedNet growth
Gross Churn RateChurned MRR / Beginning MRRRevenue decay rate
Net Revenue Retention (NRR)(Beginning + Expansion − Contraction − Churned) / BeginningGrowth from existing base
LTVARPU / Gross Churn RateCustomer economic value
CACSales + Marketing Spend / New CustomersAcquisition cost
LTV:CACLTV / CACUnit economics health (target: >3x)
Payback PeriodCAC / (ARPU × Gross Margin)Months to recover acquisition cost

Workflows

1. Pull MRR from Stripe

# List all active subscriptions with their amounts
stripe subscriptions list \
  --status=active \
  --limit=100 \
  --expand[]=data.items.data \
  2>&1 | jq '
    .data[] | {
      id: .id,
      customer: .customer,
      status: .status,
      current_period_start: (.current_period_start | strftime("%Y-%m-%d")),
      mrr: (.items.data[0].price.unit_amount / 100  
            (if .items.data[0].price.recurring.interval == "year" then 1/12 else 1 end))
    }
  '

Get MRR summary via Stripe API (no CLI):

curl "https://api.stripe.com/v1/subscriptions?status=active&limit=100&expand[]=data.items.data" \
  -u sk_live_YOUR_KEY: | jq '
  [.data[] | 
    (.items.data[0].price.unit_amount / 100)  
    (if .items.data[0].price.recurring.interval == "year" then 1/12 else 1 end)
  ] | add
  '

Python: Full MRR waterfall from Stripe events:

import stripe
from datetime import datetime, timezone
from collections import defaultdict
from dateutil.relativedelta import relativedelta

stripe.api_key = "sk_live_YOUR_KEY"

def get_mrr_waterfall(year: int, month: int) -> dict: """ Calculate MRR waterfall for a given month. Returns: new, expansion, contraction, churned, net_new MRR. """ # Period boundaries period_start = datetime(year, month, 1, tzinfo=timezone.utc) period_end = period_start + relativedelta(months=1) prev_start = period_start - relativedelta(months=1)

# Get subscriptions active at start of period (denominator) beginning_subs = _get_active_subscriptions_at(prev_start) ending_subs = _get_active_subscriptions_at(period_end)

# Categorize by customer beginning_customers = {s.customer: _get_mrr(s) for s in beginning_subs} ending_customers = {s.customer: _get_mrr(s) for s in ending_subs}

new_mrr = 0.0 expansion_mrr = 0.0 contraction_mrr = 0.0 churned_mrr = 0.0

all_customers = set(beginning_customers) | set(ending_customers)

for cust_id in all_customers: begin_val = beginning_customers.get(cust_id, 0.0) end_val = ending_customers.get(cust_id, 0.0) delta = end_val - begin_val

if begin_val == 0 and end_val > 0: new_mrr += end_val elif begin_val > 0 and end_val == 0: churned_mrr += begin_val elif delta > 0: expansion_mrr += delta elif delta < 0: contraction_mrr += abs(delta)

beginning_mrr = sum(beginning_customers.values())

return { "period": f"{year}-{month:02d}", "beginning_mrr": beginning_mrr, "new_mrr": new_mrr, "expansion_mrr": expansion_mrr, "contraction_mrr": contraction_mrr, "churned_mrr": churned_mrr, "net_new_mrr": new_mrr + expansion_mrr - contraction_mrr - churned_mrr, "ending_mrr": beginning_mrr + new_mrr + expansion_mrr - contraction_mrr - churned_mrr, "gross_churn_rate": churned_mrr / beginning_mrr if beginning_mrr else 0, "nrr": (beginning_mrr + expansion_mrr - contraction_mrr - churned_mrr) / beginning_mrr if beginning_mrr else 0, }

def _get_mrr(subscription) -> float: """Extract normalized monthly value from a Stripe subscription.""" item = subscription.get("items", {}).get("data", [{}])[0] price = item.get("price", {}) amount = price.get("unit_amount", 0) / 100 qty = item.get("quantity", 1) interval = price.get("recurring", {}).get("interval", "month") if interval == "year": return (amount qty) / 12 elif interval == "week": return (amount qty) 4.333 return amount qty

def _get_active_subscriptions_at(timestamp: datetime) -> list: """Get subscriptions that were active at a given timestamp.""" ts = int(timestamp.timestamp()) subs = stripe.Subscription.list( status="all", created={"lte": ts}, limit=100 ) return [ s for s in subs.auto_paging_iter() if s.current_period_start <= ts <= (s.canceled_at or ts + 1) ]

2. Cohort Analysis

Track retention by signup cohort — the gold standard for understanding retention quality:

import pandas as pd
import numpy as np

def build_cohort_table(subscription_events: pd.DataFrame) -> pd.DataFrame: """ Build monthly cohort retention table. Input columns: customer_id, event_type (started/churned), event_month (YYYY-MM) Output: matrix of cohort × months_since_start → retention percentage Example output: cohort | M+0 | M+1 | M+2 | M+3 | M+6 | M+12 2025-01 | 100% | 87% | 79% | 74% | 65% | 54% 2025-02 | 100% | 91% | 83% | 78% | -- | -- """ # Assign cohort (month of first subscription) first_sub = (subscription_events[subscription_events.event_type == "started"] .groupby("customer_id")["event_month"] .min() .reset_index() .rename(columns={"event_month": "cohort"})) df = subscription_events.merge(first_sub, on="customer_id") df["cohort"] = pd.to_datetime(df["cohort"]) df["event_month"] = pd.to_datetime(df["event_month"]) df["months_since_start"] = ( (df["event_month"].dt.year - df["cohort"].dt.year) 12 + (df["event_month"].dt.month - df["cohort"].dt.month) ) # Active customers per cohort per month active = (df[df.event_type != "churned"] .groupby(["cohort", "months_since_start"])["customer_id"] .nunique() .reset_index() .rename(columns={"customer_id": "active_customers"})) cohort_table = active.pivot( index="cohort", columns="months_since_start", values="active_customers" ) # Normalize to cohort size (M+0 = 100%) cohort_sizes = cohort_table[0] retention_table = cohort_table.divide(cohort_sizes, axis=0) 100 return retention_table.round(1)

def average_retention_curve(cohort_table: pd.DataFrame, min_cohorts: int = 3) -> pd.Series: """ Compute average retention curve across cohorts with enough data. Used for LTV projection. """ # Only include cohorts with at least min_cohorts data points per period valid_cols = cohort_table.columns[cohort_table.notna().sum() >= min_cohorts] return cohort_table[valid_cols].mean()

3. LTV and Unit Economics

def calculate_ltv(arpu: float, gross_margin: float, monthly_churn_rate: float) -> dict:
    """
    Calculate Customer Lifetime Value and payback metrics.
    
    Args:
        arpu: Average Revenue Per User per month
        gross_margin: Gross margin % (0.0-1.0)
        monthly_churn_rate: Monthly revenue churn rate (0.0-1.0)
    
    Returns:
        LTV, gross profit LTV, and key benchmarks
    """
    if monthly_churn_rate <= 0:
        raise ValueError("Churn rate must be > 0 for LTV calculation")
    
    avg_customer_lifetime_months = 1 / monthly_churn_rate
    ltv_revenue = arpu  avg_customer_lifetime_months
    ltv_gross_profit = ltv_revenue  gross_margin
    
    return {
        "arpu_monthly": arpu,
        "arpu_annual": arpu  12,
        "monthly_churn_rate": monthly_churn_rate,
        "avg_lifetime_months": avg_customer_lifetime_months,
        "ltv_revenue": ltv_revenue,
        "ltv_gross_profit": ltv_gross_profit,
        "benchmarks": {
            "saas_target_ltv_cac": ">3x",
            "saas_target_payback": "<12 months",
            "saas_target_nrr": ">100%",
        }
    }

def payback_period(cac: float, arpu: float, gross_margin: float) -> dict: """ Calculate CAC payback period in months. Healthy SaaS: <12 months Great SaaS: <6 months Struggling: >18 months """ monthly_gross_profit_per_customer = arpu gross_margin if monthly_gross_profit_per_customer <= 0: return {"payback_months": float("inf"), "status": "never — negative gross margin"} months = cac / monthly_gross_profit_per_customer if months < 6: status = "excellent" elif months < 12: status = "healthy" elif months < 18: status = "acceptable" else: status = "concerning" return { "cac": cac, "arpu_monthly": arpu, "gross_margin": gross_margin, "payback_months": round(months, 1), "payback_status": status, }

4. CSV Import (Non-Stripe Businesses)

For businesses without Stripe — import from any billing system:

# Expected CSV format:
# customer_id, plan_name, mrr, start_date, end_date (blank if active), currency

import pandas as pd from datetime import datetime

def load_subscriptions_from_csv(path: str, as_of_date: str = None) -> pd.DataFrame: """ Load subscription data from a CSV export. Handles Chargebee, Recurly, Zuora, or manual exports. Required columns: customer_id, mrr, start_date Optional: end_date, plan_name, currency """ df = pd.read_csv(path) df["start_date"] = pd.to_datetime(df["start_date"]) if "end_date" in df.columns: df["end_date"] = pd.to_datetime(df["end_date"], errors="coerce") if as_of_date: cutoff = pd.Timestamp(as_of_date) # Active = started before cutoff AND (not ended OR ended after cutoff) df = df[ (df["start_date"] <= cutoff) & (df.get("end_date", pd.NaT).isna() | (df.get("end_date", pd.NaT) > cutoff)) ] # Standardize MRR (handle annual → monthly) if "billing_interval" in df.columns: df.loc[df.billing_interval == "annual", "mrr"] /= 12 return df

def compute_metrics_from_csv(csv_path: str, period: str) -> dict: """ Full metrics computation from CSV for a given YYYY-MM period. """ year, month = map(int, period.split("-")) # Current and previous month active subs period_end = datetime(year, month, 28) # safe month-end prev_period_end = datetime(year, month - 1 if month > 1 else 12, 28) current = load_subscriptions_from_csv(csv_path, period_end.strftime("%Y-%m-%d")) previous = load_subscriptions_from_csv(csv_path, prev_period_end.strftime("%Y-%m-%d")) curr_by_cust = current.groupby("customer_id")["mrr"].sum() prev_by_cust = previous.groupby("customer_id")["mrr"].sum() new_customers = curr_by_cust.index.difference(prev_by_cust.index) churned_customers = prev_by_cust.index.difference(curr_by_cust.index) existing = curr_by_cust.index.intersection(prev_by_cust.index) expansion = (curr_by_cust[existing] - prev_by_cust[existing]).clip(lower=0).sum() contraction = (prev_by_cust[existing] - curr_by_cust[existing]).clip(lower=0).sum() beginning_mrr = prev_by_cust.sum() return { "period": period, "customer_count": len(curr_by_cust), "mrr": curr_by_cust.sum(), "arr": curr_by_cust.sum() 12, "beginning_mrr": beginning_mrr, "new_mrr": curr_by_cust[new_customers].sum(), "expansion_mrr": expansion, "contraction_mrr": contraction, "churned_mrr": prev_by_cust[churned_customers].sum(), "net_new_mrr": curr_by_cust.sum() - beginning_mrr, "gross_churn_rate": prev_by_cust[churned_customers].sum() / beginning_mrr if beginning_mrr else 0, "nrr": (beginning_mrr + expansion - contraction - prev_by_cust[churned_customers].sum()) / beginning_mrr if beginning_mrr else 0, "arpu": curr_by_cust.mean(), }

5. Deferred Revenue GL Entries (QBO-Ready)

Subscription revenue must be recognized over the service period (ASC 606 / IFRS 15):

from datetime import date
from dateutil.relativedelta import relativedelta

def deferred_revenue_schedule( invoice_date: date, invoice_amount: float, service_start: date, service_end: date, description: str ) -> list[dict]: """ Generate monthly revenue recognition journal entries for an annual subscription. At invoice: Debit A/R, Credit Deferred Revenue Monthly: Debit Deferred Revenue, Credit Revenue Returns list of journal entries ready for QBO import. """ total_days = (service_end - service_start).days entries = [] # Initial: recognize deferred revenue liability entries.append({ "date": invoice_date.isoformat(), "type": "invoice_booking", "description": f"Book deferred revenue — {description}", "debit_account": "Accounts Receivable", "credit_account": "Deferred Revenue", "amount": invoice_amount, }) # Monthly recognition current_date = date(service_start.year, service_start.month, 1) while current_date <= service_end: # Days in this period period_end = min( date(current_date.year, current_date.month + 1, 1) - relativedelta(days=1), service_end ) period_days = (period_end - max(current_date, service_start)).days + 1 period_revenue = invoice_amount (period_days / total_days) entries.append({ "date": current_date.isoformat(), "type": "revenue_recognition", "description": f"Revenue recognition — {description} ({current_date.strftime('%b %Y')})", "debit_account": "Deferred Revenue", "credit_account": "Subscription Revenue", "amount": round(period_revenue, 2), "period_days": period_days, }) current_date = date(current_date.year, current_date.month, 1) + relativedelta(months=1) return entries

# Example: $12,000 annual subscription entries = deferred_revenue_schedule( invoice_date=date(2026, 1, 1), invoice_amount=12000.00, service_start=date(2026, 1, 1), service_end=date(2026, 12, 31), description="Acme Corp — Enterprise Plan" ) # → 13 entries: 1 booking + 12 monthly recognition of $1,000 each

6. Investor-Ready Output

def generate_investor_summary(metrics_history: list[dict]) -> dict:
    """
    Generate board/investor MRR summary from 12 months of metrics.
    
    Returns formatted dict suitable for pitch deck tables or Sheets export.
    """
    if len(metrics_history) < 2:
        raise ValueError("Need at least 2 months of data for growth calculations")
    
    latest = metrics_history[-1]
    prev_month = metrics_history[-2]
    twelve_months_ago = metrics_history[0] if len(metrics_history) >= 12 else None
    
    mom_growth = (latest["mrr"] - prev_month["mrr"]) / prev_month["mrr"] if prev_month["mrr"] else 0
    
    yoy_growth = None
    if twelve_months_ago and twelve_months_ago["mrr"]:
        yoy_growth = (latest["mrr"] - twelve_months_ago["mrr"]) / twelve_months_ago["mrr"]
    
    # Rule of 40: YoY Revenue Growth % + EBITDA Margin % >= 40 is healthy SaaS
    # (requires EBITDA margin input from financial model)
    
    return {
        "as_of": latest["period"],
        "mrr": f"${latest['mrr']:,.0f}",
        "arr": f"${latest['arr']:,.0f}",
        "mrr_mom_growth": f"{mom_growth:.1%}",
        "mrr_yoy_growth": f"{yoy_growth:.1%}" if yoy_growth is not None else "N/A",
        "nrr": f"{latest['nrr']:.1%}",
        "gross_churn": f"{latest['gross_churn_rate']:.2%}",
        "customer_count": latest["customer_count"],
        "arpu": f"${latest['arpu']:,.0f}",
        "new_mrr": f"${latest['new_mrr']:,.0f}",
        "expansion_mrr": f"${latest['expansion_mrr']:,.0f}",
        "churned_mrr": f"${latest['churned_mrr']:,.0f}",
        "net_new_mrr": f"${latest['net_new_mrr']:,.0f}",
        "benchmarks": {
            "nrr_status": "elite" if latest["nrr"] > 1.20 else "strong" if latest["nrr"] > 1.10 else "healthy" if latest["nrr"] > 1.00 else "concerning",
            "churn_status": "excellent" if latest["gross_churn_rate"] < 0.01 else "healthy" if latest["gross_churn_rate"] < 0.02 else "high",
        }
    }

Benchmark Reference

SaaS Health Benchmarks (2025)

MetricBest in ClassHealthyWatch
MoM MRR Growth>15%5-15%<5%
Gross Churn (monthly)<0.5%0.5-2%>2%
NRR>120%100-120%<100%
LTV:CAC>5x3-5x<3x
CAC Payback<6 mo6-12 mo>12 mo
Rule of 40>4020-40<20

Revenue Recognition (ASC 606)

  • Annual prepaid subscriptions: Recognize monthly (1/12 per month)
  • Multi-year contracts: Recognize over full term
  • Usage-based billing: Recognize as consumed
  • Setup fees: Recognize ratably with subscription unless distinct performance obligation

Integration with Other Skills

  • startup-financial-model: Feed MRR history and projections as revenue driver
  • kpi-alert-system: Alert when churn > threshold or NRR drops below 100%
  • qbo-automation: Import deferred revenue journal entries automatically
  • investor-memo-generator: Pull MRR waterfall data for Section 3 (Financial Performance)
  • report-generator: Generate monthly board report with cohort retention tables

Not For This Skill

  • Executing Stripe API write operations (refunds, subscription changes) — use a billing management skill
  • QBO data entry — use qbo-automation after generating journal entries here
  • Crypto/DeFi revenue — use defi-position-tracker
  • One-time product sales without subscription component — use financial-analysis-agent
  • Payroll or employee compensation — use payroll tools
  • Tax filing or 1099 generation — use crypto-tax-agent or consult a CPA
  • on-chain-payroll or qbo-to-tax-bridge — PTIN-backed Moltlaunch services, not ClawHub
数据来源ClawHub ↗ · 中文优化:龙虾技能库