
Cfo Advisor
- 438 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
cfo-advisor is a Claude Code skill that stress-tests unit economics, runway, pricing models, and financial scenarios before developers commit to a monetization or fundraising strategy.
About
cfo-advisor is a skill from borghei/claude-skills that applies CFO-level financial scrutiny to product and business decisions before engineering commits to a revenue model. The skill structures analysis of unit economics, burn rate, runway months, pricing tiers, margin assumptions, and scenario planning under optimistic, base, and downside cases. Developers and technical founders reach for cfo-advisor when SaaS pricing, usage-based billing, or fundraising narratives need numeric grounding before checkout or billing code ships. The skill produces decision-ready financial framing rather than accounting ledger entries. cfo-advisor suits teams validating whether a monetization plan survives realistic churn, CAC, and infrastructure cost assumptions instead of guessing at price points during implementation.
- Frames unit economics and margin scenarios
- Reviews pricing and revenue model tradeoffs
- Estimates runway and burn considerations
- Surfaces CFO-level financial risks early
- Supports investor-ready financial narratives
Cfo Advisor by the numbers
- 438 all-time installs (skills.sh)
- Ranked #230 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/borghei/claude-skills --skill cfo-advisorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 438 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
How do you stress-test SaaS unit economics and runway?
Stress-test unit economics, runway, pricing models, and financial scenarios before committing to a monetization or fundraising strategy.
Who is it for?
Technical founders and lead developers validating SaaS pricing, runway, and unit economics before building billing or fundraising decks.
Skip if: Teams needing tax filing, bookkeeping, or legal fundraising documents should skip cfo-advisor.
When should I use this skill?
The user asks to model runway, unit economics, pricing tiers, or financial scenarios before monetization or fundraising decisions.
What you get
Pricing scenario models, runway projections, unit economics breakdowns, and monetization decision summaries.
Files
CFO Advisor
The agent acts as a fractional CFO, providing financial strategy and operational finance guidance grounded in SaaS benchmarks, GAAP standards, and investor expectations.
Workflow
1. Establish financial baseline -- Collect current ARR, burn rate, cash balance, and headcount. Calculate runway in months. Validate that the data is recent (within 30 days). 2. Build unit economics -- Calculate CAC, LTV, CAC Payback, LTV:CAC ratio, NRR, and Burn Multiple using the formulas below. Flag any metric outside benchmark ranges. 3. Construct financial model -- Build a 3-year model following the Revenue Build and Expense Build structures. Document all key assumptions explicitly. 4. Design investor reporting -- Configure the Monthly Metrics Package template. Set up the Board Financial Presentation slide structure for quarterly use. 5. Set up cash management -- Build the 13-week cash flow forecast. Establish the monthly rolling forecast. Verify minimum 6-month runway is maintained. 6. Establish close cadence -- Implement the Month-End Timeline (Day 1-12). Assign owners to each quality checklist item. 7. Assess risk posture -- Review market, credit, and operational risk categories. Confirm insurance coverage is adequate for company stage.
SaaS Unit Economics
CAC = (Sales + Marketing Spend) / New Customers
CAC Payback = CAC / (ARPU x Gross Margin)
LTV = ARPU x Gross Margin x Customer Lifetime
LTV:CAC Ratio = LTV / CAC Target: > 3:1
Logo Retention = (Customers End - New) / Customers Start
Net Revenue Retention = (MRR End - Churn + Expansion) / MRR StartBurn Multiple
Burn Multiple = Net Burn / Net New ARR
< 1.0x Excellent efficiency
1.0-1.5x Good efficiency
1.5-2.0x Average
> 2.0x Needs improvementRule of 40
Rule of 40 = Revenue Growth % + Profit Margin %
> 40% Strong performance
20-40% Acceptable
< 20% Needs attentionMonthly Metrics Package
FINANCIAL HIGHLIGHTS
- Revenue: $X.XM (vs Plan: +/-Y%)
- Gross Margin: XX% (vs Plan: +/-Y%)
- Operating Loss: $X.XM (vs Plan: +/-Y%)
- Cash Balance: $X.XM
- Runway: XX months
REVENUE METRICS
- ARR: $X.XM (+Y% QoQ)
- Net New ARR: $XXK
- NRR: XXX%
- Logo Churn: X.X%
EFFICIENCY METRICS
- CAC: $X,XXX
- CAC Payback: XX months
- Burn Multiple: X.XxBoard Financial Presentation
1. Financial summary (1 slide) 2. Revenue performance (1-2 slides) 3. Expense breakdown (1 slide) 4. Cash flow and runway (1 slide) 5. Key metrics trends (1 slide) 6. Forecast outlook (1 slide)
Revenue Build (Financial Model)
1. Starting ARR / customers 2. New logo assumptions (by segment) 3. Expansion rate 4. Churn rate 5. Pricing changes 6. Segment mix
Expense Build (Financial Model)
1. Headcount plan (by department) 2. Comp and benefits 3. Contractors 4. Software / tools 5. Facilities 6. Marketing programs 7. Travel and events
Budget Categories
| Category | Line Items |
|---|---|
| Revenue | New business (by segment), expansion, renewals, professional services |
| Cost of Revenue | Hosting/infrastructure, support, PS delivery, payment processing |
| OpEx | Sales & Marketing, R&D, G&A |
Month-End Close Timeline
| Days | Activity |
|---|---|
| 1-3 | Transaction cutoff |
| 3-5 | Reconciliations |
| 5-7 | Accruals and adjustments |
| 7-10 | Management review |
| 10-12 | Final close |
Quality Checklist: Bank reconciliation, revenue recognition, expense accruals, prepaid amortization, deferred revenue, intercompany elimination, flux analysis.
Revenue Recognition (ASC 606)
1. Identify the contract 2. Identify performance obligations 3. Determine transaction price 4. Allocate price to obligations 5. Recognize revenue when satisfied
SaaS considerations: Subscription vs usage revenue, implementation services, professional services, multi-year contracts, discounts and credits.
Cash Management
13-Week Cash Flow: Week-by-week projections of all known inflows/outflows. Review weekly. Maintain minimum cash buffer.
Monthly Rolling Forecast: 12-month forward view covering revenue collection timing, payroll, vendor payments, debt service, and CapEx.
Treasury Principles: Maintain 6+ months runway, preserve capital, optimize yield on idle cash, follow investment policy.
Cash Preservation Levers (when extending runway): 1. Hiring freeze 2. Vendor renegotiation 3. Discretionary spend cuts 4. Payment term extension 5. Revenue acceleration 6. Bridge financing
Due Diligence Data Room Checklist
Financial data:
- [ ] 3 years historical financials
- [ ] Monthly P&L by segment
- [ ] Balance sheet and cash flow
- [ ] ARR/MRR cohort analysis
- [ ] Customer unit economics
- [ ] Revenue recognition policy
- [ ] AR aging
- [ ] AP summary
Projections:
- [ ] 3-5 year financial model
- [ ] Key assumptions documented
- [ ] Sensitivity analysis
- [ ] Use of funds breakdown
- [ ] Path to profitability
Financial Risk Categories
| Risk Type | Key Concerns |
|---|---|
| Market | Interest rate exposure, FX exposure, customer concentration |
| Credit | Customer creditworthiness, AR aging, bad debt reserves |
| Operational | Internal controls, fraud prevention, systems reliability |
Example: Series-A SaaS Financial Snapshot
A Series-A company ($3M ARR, 35 employees, $12M raised) preparing for Series B:
Unit Economics:
CAC: $22K | LTV: $88K | LTV:CAC: 4.0x | CAC Payback: 16 months
NRR: 115% | Logo Retention: 90% | Gross Margin: 78%
Burn:
Monthly burn: $350K | Net new ARR/month: $180K
Burn Multiple: 1.9x (average -- needs improvement for Series B)
Cash: $5.2M | Runway: 15 months
Rule of 40:
Revenue growth: 95% YoY | Profit margin: -40%
Score: 55% (strong)
Board recommendation: Raise in 6 months at current trajectory.
Target metrics for raise: Burn Multiple < 1.5x, NRR > 120%.Essential Insurance Policies
D&O, E&O, Cyber liability, General liability, Workers compensation, Key person insurance.
Scripts
# Unit economics calculator
python scripts/unit_economics.py --metrics data.csv
# Cash flow projector
python scripts/cash_forecast.py --actuals Q1.csv --assumptions model.yaml
# Financial model builder
python scripts/fin_model.py --template saas --output model.xlsx
# Investor metrics dashboard
python scripts/investor_metrics.py --period monthlyReferences
references/financial_modeling.md-- Model building guidereferences/saas_metrics.md-- SaaS metrics deep divereferences/accounting_policies.md-- Policy documentationreferences/audit_prep.md-- Audit readiness guide
---
Tool Reference
financial_health_scorer.py
Comprehensive SaaS financial health assessment: Rule of 40, burn multiple, LTV:CAC, CAC payback, NRR, magic number, and composite score with investor-readiness verdict.
# Run with demo data (Series A SaaS)
python scripts/financial_health_scorer.py
# Quick assessment with key metrics
python scripts/financial_health_scorer.py --arr 3000000 --revenue-growth 95 --profit-margin -40 --burn 350000 --cash 5200000 --nrr 115 --gross-margin 78 --headcount 35
# From JSON file
python scripts/financial_health_scorer.py --input financials.json
# JSON output
python scripts/financial_health_scorer.py --input financials.json --jsonburn_rate_calculator.py
Models burn rate, runway under 5 scenarios (current, hiring freeze, 10% cut, 20% cut, revenue acceleration), generates 13-week cash flow forecast, and identifies action triggers.
# Run with demo data
python scripts/burn_rate_calculator.py
# Quick calculation
python scripts/burn_rate_calculator.py --cash 5200000 --revenue 250000 --expenses 600000 --headcount 35
# JSON output
python scripts/burn_rate_calculator.py --jsonscenario_modeler.py
Three-scenario financial projection engine with probability weighting, sensitivity analysis, and decision triggers. Projects base, upside, and downside cases over 8 quarters.
# Run with demo data
python scripts/scenario_modeler.py
# Quick model from key inputs
python scripts/scenario_modeler.py --arr 3000000 --expenses 900000 --cash 5200000 --quarters 8
# From JSON with custom scenarios
python scripts/scenario_modeler.py --input scenarios.json
# JSON output
python scripts/scenario_modeler.py --json---
Troubleshooting
| Problem | Likely Cause | Fix |
|---|---|---|
| Burn multiple shows > 3.0x | Spending significantly outpaces net new ARR | Audit S&M efficiency; consider hiring freeze; validate pipeline conversion rates |
| Rule of 40 score below 20% | Growth has slowed without corresponding margin improvement | Either re-accelerate growth or cut costs to improve margins -- cannot stay in the middle |
| CAC payback exceeds 24 months | Sales cycle too long, ACV too low, or S&M spend too high | Segment CAC by channel; cut underperforming channels; raise ACV through pricing |
| LTV:CAC ratio below 2.0x | Customer lifetime too short (churn) or acquisition too expensive | Address churn first (higher ROI); then optimize CAC by channel |
| NRR below 100% | Contraction and churn exceed expansion revenue | Build expansion playbook; segment churning customers; invest in customer success |
| Financial model assumptions questioned by board | Assumptions not documented or unrealistic | Document every assumption explicitly; show sensitivity analysis for key variables |
| Month-end close takes 15+ days | Manual processes, missing reconciliations, or unclear ownership | Implement the Day 1-12 close timeline; assign owners to each checklist item |
---
Success Criteria
- Financial health composite score above 65/100 (measured quarterly via financial_health_scorer.py)
- Rule of 40 score maintained above 40% for Series B+ companies
- Burn multiple below 2.0x (below 1.5x for Series B readiness)
- CAC payback under 18 months (under 12 months for top-quartile performance)
- Month-end close completed within 12 business days with zero material adjustments
- Board financial presentation completed 48+ hours before every board meeting
- Cash runway maintained above 12 months at all times (above 18 months preferred)
---
Scope & Limitations
In Scope: SaaS unit economics, burn rate analysis, financial modeling, cash management, investor reporting, month-end close, revenue recognition (ASC 606), due diligence preparation, scenario modeling.
Out of Scope: Tax planning, legal entity structuring, audit execution, payroll processing, accounts payable/receivable operations, insurance procurement, equity cap table management.
Limitations: Financial health scorer uses industry benchmarks that may not apply to non-SaaS business models. Burn rate calculator uses linear/exponential approximations -- actual cash flows vary with billing cycles and payment timing. Scenario modeler provides directional guidance, not auditable financial projections.
---
Integration Points
| Skill | Integration |
|---|---|
ceo-advisor | Financial scenarios feed board strategy discussions |
board-deck-builder | Financial update section; all deck numbers validated through CFO tools |
cro-advisor | Revenue forecasting; pipeline-to-revenue conversion assumptions |
chro-advisor | Headcount budget modeling; fully-loaded cost calculations |
ciso-advisor | Compliance budget sizing against quantified risk exposure |
company-os | Financial metrics in the weekly scorecard |
chief-of-staff | Routes financial questions; synthesizes CFO + CEO perspectives |
#!/usr/bin/env python3
"""
Burn Rate Calculator - Models burn rate, runway, and cash-out scenarios.
Calculates gross burn, net burn, runway under multiple scenarios,
and generates a 13-week cash flow forecast with action triggers.
"""
import argparse
import json
import sys
from datetime import datetime, timedelta
def calculate_burn(data: dict) -> dict:
"""Calculate burn metrics and runway scenarios."""
cash = data.get("cash_balance", 0)
monthly_revenue = data.get("monthly_revenue", 0)
monthly_expenses = data.get("monthly_expenses", 0)
revenue_growth_pct = data.get("monthly_revenue_growth_pct", 3)
expense_growth_pct = data.get("monthly_expense_growth_pct", 2)
headcount = data.get("headcount", 0)
avg_salary = data.get("avg_fully_loaded_salary", 0)
non_people_expenses = data.get("non_people_monthly", 0)
# If detailed breakdown available, compute expenses
if headcount > 0 and avg_salary > 0:
people_cost = headcount * avg_salary
total_expenses = people_cost + non_people_expenses
else:
total_expenses = monthly_expenses
people_cost = total_expenses * 0.65 # typical SaaS split
gross_burn = total_expenses
net_burn = total_expenses - monthly_revenue
results = {
"timestamp": datetime.now().isoformat(),
"current_state": {
"cash_balance": cash,
"monthly_revenue": monthly_revenue,
"gross_burn": round(gross_burn),
"net_burn": round(net_burn),
"people_cost": round(people_cost),
"non_people_cost": round(total_expenses - people_cost),
"runway_months": round(cash / net_burn, 1) if net_burn > 0 else 999,
"gross_margin_pct": round((1 - (monthly_revenue * 0.25) / monthly_revenue) * 100, 1) if monthly_revenue > 0 else 0,
},
"scenarios": [],
"cash_forecast_13_week": [],
"action_triggers": [],
"preservation_levers": [],
}
# Scenario modeling
scenarios = [
{"name": "Current Trajectory", "rev_growth": revenue_growth_pct, "exp_growth": expense_growth_pct, "headcount_change": 0},
{"name": "Hiring Freeze", "rev_growth": revenue_growth_pct, "exp_growth": 0.5, "headcount_change": 0},
{"name": "10% Cost Cut", "rev_growth": revenue_growth_pct * 0.8, "exp_growth": -10, "headcount_change": -round(headcount * 0.1)},
{"name": "20% Cost Cut", "rev_growth": revenue_growth_pct * 0.6, "exp_growth": -20, "headcount_change": -round(headcount * 0.2)},
{"name": "Revenue Acceleration (+50%)", "rev_growth": revenue_growth_pct * 1.5, "exp_growth": expense_growth_pct * 1.2, "headcount_change": 0},
]
for sc in scenarios:
sc_rev = monthly_revenue
sc_exp = total_expenses
sc_cash = cash
months = 0
# Apply one-time cuts
if sc["exp_growth"] < 0:
sc_exp = total_expenses * (1 + sc["exp_growth"] / 100)
while sc_cash > 0 and months < 36:
sc_rev *= (1 + sc["rev_growth"] / 100)
if sc["exp_growth"] >= 0:
sc_exp *= (1 + sc["exp_growth"] / 100)
sc_cash -= (sc_exp - sc_rev)
months += 1
scenario_result = {
"name": sc["name"],
"runway_months": months if sc_cash <= 0 else 36,
"monthly_net_burn_adjusted": round(sc_exp - sc_rev),
"headcount_change": sc["headcount_change"],
"cash_out_date": (datetime.now() + timedelta(days=months * 30)).strftime("%Y-%m-%d") if sc_cash <= 0 else "36+ months",
"runway_extension_vs_current": 0,
}
results["scenarios"].append(scenario_result)
# Calculate extension vs current
base_runway = results["scenarios"][0]["runway_months"]
for sc in results["scenarios"]:
sc["runway_extension_vs_current"] = sc["runway_months"] - base_runway
# 13-week cash flow forecast
weekly_revenue = monthly_revenue / 4.33
weekly_payroll = people_cost / 4.33
weekly_vendors = (total_expenses - people_cost) / 4.33
forecast_cash = cash
for week in range(1, 14):
inflow = weekly_revenue * (1 + revenue_growth_pct / 100 * week / 52)
# Payroll typically biweekly
payroll = weekly_payroll if week % 2 == 0 else weekly_payroll * 0.1
vendors = weekly_vendors
total_out = payroll + vendors
net = inflow - total_out
forecast_cash += net
results["cash_forecast_13_week"].append({
"week": week,
"date": (datetime.now() + timedelta(weeks=week)).strftime("%Y-%m-%d"),
"inflow": round(inflow),
"payroll": round(payroll),
"vendors": round(vendors),
"total_outflow": round(total_out),
"net": round(net),
"closing_cash": round(forecast_cash),
})
# Action triggers
triggers = [
{"runway_threshold": 12, "action": "Begin fundraising process", "severity": "info"},
{"runway_threshold": 9, "action": "Hiring freeze on non-critical roles", "severity": "warning"},
{"runway_threshold": 6, "action": "Discretionary spend freeze + vendor renegotiation", "severity": "high"},
{"runway_threshold": 4, "action": "Headcount reduction plan + bridge financing", "severity": "critical"},
{"runway_threshold": 2, "action": "Emergency cost reduction + wind-down planning", "severity": "critical"},
]
current_runway = results["current_state"]["runway_months"]
for t in triggers:
t["triggered"] = current_runway <= t["runway_threshold"]
results["action_triggers"].append(t)
# Preservation levers
results["preservation_levers"] = [
{"lever": "Hiring freeze", "monthly_savings": round(avg_salary * 2), "runway_impact_months": round(avg_salary * 2 * 12 / net_burn, 1) if net_burn > 0 else 0},
{"lever": "Vendor renegotiation (15%)", "monthly_savings": round(non_people_expenses * 0.15), "runway_impact_months": round(non_people_expenses * 0.15 * 12 / net_burn, 1) if net_burn > 0 else 0},
{"lever": "Discretionary cuts", "monthly_savings": round(total_expenses * 0.05), "runway_impact_months": round(total_expenses * 0.05 * 12 / net_burn, 1) if net_burn > 0 else 0},
{"lever": "10% headcount reduction", "monthly_savings": round(people_cost * 0.10), "runway_impact_months": round(people_cost * 0.10 * 12 / net_burn, 1) if net_burn > 0 else 0},
{"lever": "Payment term extension (Net-60)", "monthly_savings": round(non_people_expenses * 0.08), "runway_impact_months": round(non_people_expenses * 0.08 * 12 / net_burn, 1) if net_burn > 0 else 0},
]
return results
def format_text(results: dict) -> str:
"""Format as human-readable report."""
cs = results["current_state"]
lines = [
"=" * 65,
"BURN RATE & RUNWAY ANALYSIS",
"=" * 65,
f"Date: {results['timestamp'][:10]}",
"",
"CURRENT STATE:",
f" Cash Balance: ${cs['cash_balance']:>12,.0f}",
f" Monthly Revenue: ${cs['monthly_revenue']:>12,.0f}",
f" Gross Burn: ${cs['gross_burn']:>12,.0f}",
f" Net Burn: ${cs['net_burn']:>12,.0f}",
f" Runway: {cs['runway_months']:>12.1f} months",
f" People Cost: ${cs['people_cost']:>12,.0f} ({cs['people_cost']/cs['gross_burn']*100:.0f}% of burn)" if cs['gross_burn'] > 0 else "",
"",
"SCENARIO ANALYSIS:",
f"{'Scenario':<30} {'Runway':>8} {'Extension':>10} {'Cash-Out Date':>14}",
"-" * 65,
]
for sc in results["scenarios"]:
ext = f"+{sc['runway_extension_vs_current']}" if sc["runway_extension_vs_current"] > 0 else str(sc["runway_extension_vs_current"])
lines.append(
f"{sc['name']:<30} {sc['runway_months']:>6} mo {ext:>9} mo {sc['cash_out_date']:>14}"
)
lines.extend(["", "ACTION TRIGGERS:"])
for t in results["action_triggers"]:
icon = "[!!]" if t["triggered"] else "[ ]"
lines.append(f" {icon} At {t['runway_threshold']} months: {t['action']} [{t['severity'].upper()}]")
lines.extend(["", "CASH PRESERVATION LEVERS:"])
for lev in results["preservation_levers"]:
lines.append(
f" {lev['lever']:<35} Saves ${lev['monthly_savings']:>8,.0f}/mo (+{lev['runway_impact_months']:.1f} mo runway)"
)
lines.extend(["", "13-WEEK CASH FORECAST (summary):"])
forecast = results["cash_forecast_13_week"]
for week_data in [forecast[0], forecast[3], forecast[7], forecast[12]]:
lines.append(
f" Week {week_data['week']:>2} ({week_data['date']}): "
f"In ${week_data['inflow']:>8,.0f} Out ${week_data['total_outflow']:>8,.0f} "
f"Balance ${week_data['closing_cash']:>10,.0f}"
)
lines.extend(["", "=" * 65])
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Calculate burn rate, runway, and scenarios")
parser.add_argument("--input", "-i", help="JSON file with financial data")
parser.add_argument("--cash", type=float, help="Current cash balance")
parser.add_argument("--revenue", type=float, help="Monthly revenue")
parser.add_argument("--expenses", type=float, help="Monthly expenses")
parser.add_argument("--headcount", type=int, help="Current headcount")
parser.add_argument("--avg-salary", type=float, help="Average fully-loaded monthly salary")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
if args.input:
with open(args.input) as f:
data = json.load(f)
elif args.cash:
data = {
"cash_balance": args.cash,
"monthly_revenue": args.revenue or 0,
"monthly_expenses": args.expenses or 0,
"headcount": args.headcount or 0,
"avg_fully_loaded_salary": args.avg_salary or 12000,
"non_people_monthly": (args.expenses or 0) * 0.35,
}
else:
data = {
"cash_balance": 5200000,
"monthly_revenue": 250000,
"monthly_expenses": 600000,
"monthly_revenue_growth_pct": 5,
"monthly_expense_growth_pct": 2,
"headcount": 35,
"avg_fully_loaded_salary": 11000,
"non_people_monthly": 215000,
}
results = calculate_burn(data)
if args.json:
print(json.dumps(results, indent=2))
else:
print(format_text(results))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Financial Health Scorer - Comprehensive SaaS financial health assessment.
Calculates Rule of 40, burn multiple, magic number, CAC payback, LTV:CAC,
NRR, and composite financial health score with investor-grade benchmarks.
"""
import argparse
import json
import sys
from datetime import datetime
BENCHMARKS = {
"rule_of_40": {"excellent": 60, "good": 40, "acceptable": 20, "poor": 0},
"burn_multiple": {"excellent": 1.0, "good": 1.5, "acceptable": 2.0, "poor": 3.0},
"ltv_cac_ratio": {"excellent": 5.0, "good": 3.0, "acceptable": 2.0, "poor": 1.0},
"cac_payback_months": {"excellent": 6, "good": 12, "acceptable": 18, "poor": 24},
"nrr_pct": {"excellent": 130, "good": 115, "acceptable": 100, "poor": 90},
"gross_margin_pct": {"excellent": 85, "good": 75, "acceptable": 65, "poor": 50},
"magic_number": {"excellent": 1.5, "good": 1.0, "acceptable": 0.75, "poor": 0.5},
"arr_per_employee": {"excellent": 250000, "good": 180000, "acceptable": 120000, "poor": 80000},
"runway_months": {"excellent": 24, "good": 18, "acceptable": 12, "poor": 6},
}
WEIGHTS = {
"rule_of_40": 0.15,
"burn_multiple": 0.15,
"ltv_cac_ratio": 0.12,
"cac_payback_months": 0.10,
"nrr_pct": 0.15,
"gross_margin_pct": 0.10,
"magic_number": 0.08,
"arr_per_employee": 0.07,
"runway_months": 0.08,
}
def score_metric(value: float, metric_name: str) -> dict:
"""Score a single metric against benchmarks."""
bench = BENCHMARKS.get(metric_name, {})
if not bench:
return {"score": 50, "rating": "N/A", "benchmark_note": "No benchmark available"}
# Determine if higher or lower is better
lower_is_better = metric_name in ["burn_multiple", "cac_payback_months"]
if lower_is_better:
if value <= bench["excellent"]:
score, rating = 100, "Excellent"
elif value <= bench["good"]:
pct = (bench["good"] - value) / (bench["good"] - bench["excellent"])
score, rating = 75 + pct * 25, "Good"
elif value <= bench["acceptable"]:
pct = (bench["acceptable"] - value) / (bench["acceptable"] - bench["good"])
score, rating = 50 + pct * 25, "Acceptable"
elif value <= bench["poor"]:
pct = (bench["poor"] - value) / (bench["poor"] - bench["acceptable"])
score, rating = 25 + pct * 25, "Below Average"
else:
score, rating = max(0, 25 * bench["poor"] / value), "Poor"
else:
if value >= bench["excellent"]:
score, rating = 100, "Excellent"
elif value >= bench["good"]:
pct = (value - bench["good"]) / (bench["excellent"] - bench["good"])
score, rating = 75 + pct * 25, "Good"
elif value >= bench["acceptable"]:
pct = (value - bench["acceptable"]) / (bench["good"] - bench["acceptable"])
score, rating = 50 + pct * 25, "Acceptable"
elif value >= bench["poor"]:
pct = (value - bench["poor"]) / (bench["acceptable"] - bench["poor"])
score, rating = 25 + pct * 25, "Below Average"
else:
score, rating = max(0, 25 * value / bench["poor"]) if bench["poor"] != 0 else 0, "Poor"
return {
"score": round(min(100, max(0, score)), 1),
"rating": rating,
"benchmark_excellent": bench["excellent"],
"benchmark_good": bench["good"],
}
def calculate_health(data: dict) -> dict:
"""Calculate comprehensive financial health score."""
results = {
"timestamp": datetime.now().isoformat(),
"company": data.get("company", "Company"),
"stage": data.get("stage", "Unknown"),
"composite_score": 0,
"composite_rating": "",
"metrics": {},
"alerts": [],
"investor_readiness": {},
"recommendations": [],
}
# Calculate derived metrics
arr = data.get("arr", 0)
revenue_growth_pct = data.get("revenue_growth_pct", 0)
profit_margin_pct = data.get("profit_margin_pct", 0)
net_burn = data.get("net_burn_monthly", 0)
net_new_arr = data.get("net_new_arr_quarterly", 0)
sales_marketing_spend = data.get("sales_marketing_spend_quarterly", 0)
new_customers = data.get("new_customers_quarterly", 0)
arpu_monthly = data.get("arpu_monthly", 0)
gross_margin_pct = data.get("gross_margin_pct", 0)
customer_lifetime_months = data.get("customer_lifetime_months", 36)
nrr_pct = data.get("nrr_pct", 100)
headcount = data.get("headcount", 1)
cash = data.get("cash_balance", 0)
prev_quarter_arr = data.get("prev_quarter_arr", 0)
# Rule of 40
rule_of_40 = revenue_growth_pct + profit_margin_pct
results["metrics"]["rule_of_40"] = {
"value": round(rule_of_40, 1),
"formula": f"{revenue_growth_pct}% growth + {profit_margin_pct}% margin",
**score_metric(rule_of_40, "rule_of_40"),
}
# Burn Multiple
burn_multiple = (net_burn * 3 / net_new_arr) if net_new_arr > 0 else 99
results["metrics"]["burn_multiple"] = {
"value": round(burn_multiple, 2),
"formula": f"${net_burn * 3:,.0f} quarterly burn / ${net_new_arr:,.0f} net new ARR",
**score_metric(burn_multiple, "burn_multiple"),
}
# LTV:CAC
cac = (sales_marketing_spend / new_customers) if new_customers > 0 else 0
ltv = arpu_monthly * (gross_margin_pct / 100) * customer_lifetime_months
ltv_cac = (ltv / cac) if cac > 0 else 0
results["metrics"]["ltv_cac_ratio"] = {
"value": round(ltv_cac, 1),
"cac": round(cac),
"ltv": round(ltv),
"formula": f"${ltv:,.0f} LTV / ${cac:,.0f} CAC",
**score_metric(ltv_cac, "ltv_cac_ratio"),
}
# CAC Payback
monthly_contribution = arpu_monthly * (gross_margin_pct / 100)
cac_payback = (cac / monthly_contribution) if monthly_contribution > 0 else 99
results["metrics"]["cac_payback_months"] = {
"value": round(cac_payback, 1),
"formula": f"${cac:,.0f} CAC / ${monthly_contribution:,.0f} monthly contribution",
**score_metric(cac_payback, "cac_payback_months"),
}
# NRR
results["metrics"]["nrr_pct"] = {
"value": round(nrr_pct, 1),
**score_metric(nrr_pct, "nrr_pct"),
}
# Gross Margin
results["metrics"]["gross_margin_pct"] = {
"value": round(gross_margin_pct, 1),
**score_metric(gross_margin_pct, "gross_margin_pct"),
}
# Magic Number
arr_growth_quarterly = arr - prev_quarter_arr if prev_quarter_arr > 0 else net_new_arr
magic_number = (arr_growth_quarterly / sales_marketing_spend) if sales_marketing_spend > 0 else 0
results["metrics"]["magic_number"] = {
"value": round(magic_number, 2),
"formula": f"${arr_growth_quarterly:,.0f} ARR growth / ${sales_marketing_spend:,.0f} S&M spend",
**score_metric(magic_number, "magic_number"),
}
# ARR per Employee
arr_per_emp = arr / headcount if headcount > 0 else 0
results["metrics"]["arr_per_employee"] = {
"value": round(arr_per_emp),
"formula": f"${arr:,.0f} ARR / {headcount} employees",
**score_metric(arr_per_emp, "arr_per_employee"),
}
# Runway
runway = (cash / net_burn) if net_burn > 0 else 999
results["metrics"]["runway_months"] = {
"value": round(runway, 1),
"formula": f"${cash:,.0f} cash / ${net_burn:,.0f} monthly burn",
**score_metric(runway, "runway_months"),
}
# Composite score
composite = 0
for metric_name, weight in WEIGHTS.items():
if metric_name in results["metrics"]:
composite += results["metrics"][metric_name]["score"] * weight
results["composite_score"] = round(composite, 1)
if composite >= 80:
results["composite_rating"] = "Strong"
elif composite >= 65:
results["composite_rating"] = "Healthy"
elif composite >= 50:
results["composite_rating"] = "Acceptable"
elif composite >= 35:
results["composite_rating"] = "Needs Attention"
else:
results["composite_rating"] = "Critical"
# Alerts
for name, metric in results["metrics"].items():
if metric["rating"] == "Poor":
results["alerts"].append(f"CRITICAL: {name} = {metric['value']} (rated Poor)")
elif metric["rating"] == "Below Average":
results["alerts"].append(f"WARNING: {name} = {metric['value']} (Below Average)")
if runway < 6:
results["alerts"].insert(0, f"URGENT: Runway is {runway:.0f} months. Extend immediately.")
# Investor readiness
investor_ready = sum(1 for m in results["metrics"].values() if m["rating"] in ["Excellent", "Good"])
total = len(results["metrics"])
results["investor_readiness"] = {
"metrics_at_benchmark": investor_ready,
"total_metrics": total,
"readiness_pct": round(investor_ready / total * 100) if total > 0 else 0,
"verdict": "Fundraise-ready" if investor_ready >= 6 else
"Near-ready (fix 1-2 metrics)" if investor_ready >= 4 else
"Not ready (improve fundamentals first)",
}
# Recommendations
poor_metrics = [(n, m) for n, m in results["metrics"].items() if m["rating"] in ["Poor", "Below Average"]]
poor_metrics.sort(key=lambda x: WEIGHTS.get(x[0], 0), reverse=True)
for name, metric in poor_metrics[:3]:
results["recommendations"].append(
f"Improve {name}: currently {metric['value']}, target {metric['benchmark_good']}+"
)
return results
def format_text(results: dict) -> str:
"""Format as human-readable report."""
lines = [
"=" * 65,
"FINANCIAL HEALTH SCORECARD",
"=" * 65,
f"Company: {results['company']} | Stage: {results['stage']}",
f"Date: {results['timestamp'][:10]}",
f"COMPOSITE SCORE: {results['composite_score']}/100 ({results['composite_rating']})",
"",
f"{'Metric':<22} {'Value':>10} {'Score':>7} {'Rating':<15}",
"-" * 65,
]
for name, m in results["metrics"].items():
val = m["value"]
if name in ["gross_margin_pct", "nrr_pct", "rule_of_40"]:
val_str = f"{val:.1f}%"
elif name in ["burn_multiple", "ltv_cac_ratio", "magic_number"]:
val_str = f"{val:.2f}x"
elif name in ["cac_payback_months", "runway_months"]:
val_str = f"{val:.0f} mo"
elif name == "arr_per_employee":
val_str = f"${val:,.0f}"
else:
val_str = f"{val}"
lines.append(f"{name:<22} {val_str:>10} {m['score']:>6.0f}/100 {m['rating']:<15}")
if results["alerts"]:
lines.extend(["", "ALERTS:"])
for a in results["alerts"]:
lines.append(f" {a}")
ir = results["investor_readiness"]
lines.extend([
"",
f"INVESTOR READINESS: {ir['readiness_pct']}% ({ir['metrics_at_benchmark']}/{ir['total_metrics']} metrics at benchmark)",
f" Verdict: {ir['verdict']}",
])
if results["recommendations"]:
lines.extend(["", "TOP RECOMMENDATIONS:"])
for r in results["recommendations"]:
lines.append(f" -> {r}")
lines.extend(["", "=" * 65])
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Calculate SaaS financial health score")
parser.add_argument("--input", "-i", help="JSON file with financial data")
parser.add_argument("--arr", type=float, help="Annual Recurring Revenue")
parser.add_argument("--revenue-growth", type=float, help="Revenue growth rate (%%)")
parser.add_argument("--profit-margin", type=float, help="Profit margin (%%)")
parser.add_argument("--burn", type=float, help="Monthly net burn")
parser.add_argument("--cash", type=float, help="Cash balance")
parser.add_argument("--nrr", type=float, help="Net Revenue Retention (%%)")
parser.add_argument("--gross-margin", type=float, help="Gross margin (%%)")
parser.add_argument("--headcount", type=int, help="Total headcount")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
if args.input:
with open(args.input) as f:
data = json.load(f)
elif args.arr:
data = {
"company": "Company",
"stage": "Growth",
"arr": args.arr,
"revenue_growth_pct": args.revenue_growth or 50,
"profit_margin_pct": args.profit_margin or -20,
"net_burn_monthly": args.burn or 0,
"net_new_arr_quarterly": args.arr * 0.08,
"sales_marketing_spend_quarterly": args.arr * 0.12,
"new_customers_quarterly": 25,
"arpu_monthly": args.arr / 12 / 100,
"gross_margin_pct": args.gross_margin or 75,
"nrr_pct": args.nrr or 110,
"headcount": args.headcount or 50,
"cash_balance": args.cash or 5000000,
}
else:
# Demo data
data = {
"company": "SaaSCo",
"stage": "Series A",
"arr": 3000000,
"prev_quarter_arr": 2700000,
"revenue_growth_pct": 95,
"profit_margin_pct": -40,
"net_burn_monthly": 350000,
"net_new_arr_quarterly": 540000,
"sales_marketing_spend_quarterly": 450000,
"new_customers_quarterly": 20,
"arpu_monthly": 2500,
"gross_margin_pct": 78,
"customer_lifetime_months": 36,
"nrr_pct": 115,
"headcount": 35,
"cash_balance": 5200000,
}
results = calculate_health(data)
if args.json:
print(json.dumps(results, indent=2))
else:
print(format_text(results))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Financial Scenario Modeler - Three-scenario financial projection engine.
Models base, upside, and downside scenarios with probability weighting.
Produces board-ready projections with sensitivity analysis and decision triggers.
"""
import argparse
import json
import math
import sys
from datetime import datetime
def project_scenario(base: dict, scenario: dict, quarters: int = 8) -> list:
"""Project financials for a scenario over N quarters."""
projections = []
arr = base.get("arr", 0)
expenses = base.get("quarterly_expenses", 0)
cash = base.get("cash_balance", 0)
growth_rate = scenario.get("quarterly_arr_growth_pct", 10) / 100
expense_growth = scenario.get("quarterly_expense_growth_pct", 5) / 100
gross_margin = scenario.get("gross_margin_pct", 75) / 100
churn_rate = scenario.get("quarterly_churn_pct", 2) / 100
for q in range(1, quarters + 1):
# Revenue dynamics
new_arr = arr * growth_rate
churned_arr = arr * churn_rate
net_new_arr = new_arr - churned_arr
arr += net_new_arr
quarterly_revenue = arr / 4
# Cost dynamics
expenses *= (1 + expense_growth)
gross_profit = quarterly_revenue * gross_margin
operating_income = gross_profit - expenses
free_cash_flow = operating_income * 0.85 # simplified for WC and capex
cash += free_cash_flow
# Derived metrics
burn_multiple = (-operating_income / net_new_arr) if net_new_arr > 0 and operating_income < 0 else 0
rule_of_40 = (growth_rate * 4 * 100) + (operating_income / quarterly_revenue * 100) if quarterly_revenue > 0 else 0
runway = (cash / (-operating_income)) if operating_income < 0 else 999
projections.append({
"quarter": q,
"arr": round(arr),
"quarterly_revenue": round(quarterly_revenue),
"net_new_arr": round(net_new_arr),
"gross_profit": round(gross_profit),
"gross_margin_pct": round(gross_margin * 100, 1),
"quarterly_expenses": round(expenses),
"operating_income": round(operating_income),
"operating_margin_pct": round(operating_income / quarterly_revenue * 100, 1) if quarterly_revenue > 0 else 0,
"free_cash_flow": round(free_cash_flow),
"cash_balance": round(cash),
"burn_multiple": round(burn_multiple, 2),
"rule_of_40": round(rule_of_40, 1),
"runway_months": round(runway * 3, 1) if runway < 999 else 999,
"arr_growth_yoy_pct": round((1 + growth_rate) ** 4 * 100 - 100, 1),
})
return projections
def model_scenarios(data: dict) -> dict:
"""Run full scenario analysis."""
base_financials = {
"arr": data.get("arr", 0),
"quarterly_expenses": data.get("quarterly_expenses", 0),
"cash_balance": data.get("cash_balance", 0),
}
scenarios_config = data.get("scenarios", {
"base": {
"name": "Base Case",
"probability": 0.50,
"quarterly_arr_growth_pct": 10,
"quarterly_expense_growth_pct": 5,
"gross_margin_pct": 75,
"quarterly_churn_pct": 2,
},
"upside": {
"name": "Upside",
"probability": 0.25,
"quarterly_arr_growth_pct": 15,
"quarterly_expense_growth_pct": 7,
"gross_margin_pct": 78,
"quarterly_churn_pct": 1.5,
},
"downside": {
"name": "Downside",
"probability": 0.25,
"quarterly_arr_growth_pct": 5,
"quarterly_expense_growth_pct": 3,
"gross_margin_pct": 72,
"quarterly_churn_pct": 3,
},
})
quarters = data.get("projection_quarters", 8)
results = {
"timestamp": datetime.now().isoformat(),
"company": data.get("company", "Company"),
"starting_arr": base_financials["arr"],
"starting_cash": base_financials["cash_balance"],
"projection_quarters": quarters,
"scenarios": {},
"probability_weighted": {},
"sensitivity": {},
"decision_triggers": [],
"board_summary": {},
}
# Project each scenario
for key, config in scenarios_config.items():
projections = project_scenario(base_financials, config, quarters)
final = projections[-1] if projections else {}
results["scenarios"][key] = {
"name": config["name"],
"probability": config["probability"],
"assumptions": {
"quarterly_arr_growth": f"{config['quarterly_arr_growth_pct']}%",
"quarterly_expense_growth": f"{config['quarterly_expense_growth_pct']}%",
"gross_margin": f"{config['gross_margin_pct']}%",
"quarterly_churn": f"{config['quarterly_churn_pct']}%",
},
"projections": projections,
"year_2_arr": final.get("arr", 0),
"year_2_operating_margin": final.get("operating_margin_pct", 0),
"year_2_cash": final.get("cash_balance", 0),
"year_2_rule_of_40": final.get("rule_of_40", 0),
"profitability_quarter": next(
(p["quarter"] for p in projections if p["operating_income"] > 0), None
),
}
# Probability-weighted outcomes
pw_arr = sum(
s["year_2_arr"] * s["probability"]
for s in results["scenarios"].values()
)
pw_cash = sum(
s["year_2_cash"] * s["probability"]
for s in results["scenarios"].values()
)
pw_margin = sum(
s["year_2_operating_margin"] * s["probability"]
for s in results["scenarios"].values()
)
results["probability_weighted"] = {
"expected_arr": round(pw_arr),
"expected_cash": round(pw_cash),
"expected_operating_margin": round(pw_margin, 1),
"arr_range": f"${min(s['year_2_arr'] for s in results['scenarios'].values()):,.0f} - ${max(s['year_2_arr'] for s in results['scenarios'].values()):,.0f}",
}
# Sensitivity analysis
base_arr = results["scenarios"].get("base", {}).get("year_2_arr", 0)
for var_name, var_range in [("growth", [-5, -2, 0, 2, 5]), ("churn", [-1, -0.5, 0, 0.5, 1]), ("margin", [-5, -2, 0, 2, 5])]:
sensitivity = []
for delta in var_range:
adj_config = dict(scenarios_config.get("base", {}))
if var_name == "growth":
adj_config["quarterly_arr_growth_pct"] += delta
elif var_name == "churn":
adj_config["quarterly_churn_pct"] += delta
elif var_name == "margin":
adj_config["gross_margin_pct"] += delta
proj = project_scenario(base_financials, adj_config, quarters)
final_arr = proj[-1]["arr"] if proj else 0
sensitivity.append({
"delta": f"{delta:+.1f}%",
"year_2_arr": round(final_arr),
"impact_pct": round((final_arr - base_arr) / base_arr * 100, 1) if base_arr > 0 else 0,
})
results["sensitivity"][var_name] = sensitivity
# Decision triggers
downside = results["scenarios"].get("downside", {})
downside_projs = downside.get("projections", [])
for p in downside_projs:
if p.get("runway_months", 999) < 9 and p["quarter"] <= 4:
results["decision_triggers"].append({
"quarter": p["quarter"],
"trigger": f"Downside runway drops to {p['runway_months']:.0f} months",
"action": "Initiate fundraising or cost reduction",
"severity": "critical",
})
break
if downside.get("profitability_quarter") is None:
results["decision_triggers"].append({
"quarter": "N/A",
"trigger": "Downside never reaches profitability in projection window",
"action": "Ensure fundraising plan covers downside scenario",
"severity": "high",
})
# Board summary
base_sc = results["scenarios"].get("base", {})
results["board_summary"] = {
"headline": f"Projected ARR: ${pw_arr:,.0f} (probability-weighted, {quarters}Q horizon)",
"base_case_arr": f"${base_sc.get('year_2_arr', 0):,.0f}",
"upside_arr": f"${results['scenarios'].get('upside', {}).get('year_2_arr', 0):,.0f}",
"downside_arr": f"${results['scenarios'].get('downside', {}).get('year_2_arr', 0):,.0f}",
"key_assumption": f"Base case: {scenarios_config.get('base', {}).get('quarterly_arr_growth_pct', 0)}% quarterly growth, {scenarios_config.get('base', {}).get('quarterly_churn_pct', 0)}% churn",
"key_risk": "Downside churn or slower growth depletes runway" if any(t["severity"] == "critical" for t in results["decision_triggers"]) else "No critical triggers in projection window",
}
return results
def format_text(results: dict) -> str:
"""Format as board-ready text."""
lines = [
"=" * 70,
"FINANCIAL SCENARIO MODEL",
"=" * 70,
f"Company: {results['company']}",
f"Starting ARR: ${results['starting_arr']:,.0f} | Cash: ${results['starting_cash']:,.0f}",
f"Horizon: {results['projection_quarters']} quarters",
"",
"SCENARIO COMPARISON (End of Projection):",
f"{'Scenario':<18} {'Prob':>5} {'ARR':>12} {'Op Margin':>10} {'Cash':>12} {'Ro40':>6} {'Profit Q':>9}",
"-" * 70,
]
for key, sc in results["scenarios"].items():
profit_q = f"Q{sc['profitability_quarter']}" if sc["profitability_quarter"] else "N/A"
lines.append(
f"{sc['name']:<18} {sc['probability']:>4.0%} ${sc['year_2_arr']:>10,.0f} "
f"{sc['year_2_operating_margin']:>9.1f}% ${sc['year_2_cash']:>10,.0f} "
f"{sc.get('year_2_rule_of_40', 0):>5.0f} {profit_q:>9}"
)
pw = results["probability_weighted"]
lines.extend([
"",
f"PROBABILITY-WEIGHTED: ARR ${pw['expected_arr']:,.0f} | "
f"Margin {pw['expected_operating_margin']:.1f}% | Cash ${pw['expected_cash']:,.0f}",
f"ARR Range: {pw['arr_range']}",
])
# Sensitivity
lines.extend(["", "SENSITIVITY ANALYSIS (impact on Year 2 ARR):"])
for var, data in results["sensitivity"].items():
impacts = " ".join(f"{d['delta']}:{d['impact_pct']:+.1f}%" for d in data)
lines.append(f" {var.title()}: {impacts}")
# Triggers
if results["decision_triggers"]:
lines.extend(["", "DECISION TRIGGERS:"])
for t in results["decision_triggers"]:
lines.append(f" [{t['severity'].upper()}] {t['trigger']} -> {t['action']}")
# Board summary
bs = results["board_summary"]
lines.extend([
"",
"BOARD SUMMARY:",
f" {bs['headline']}",
f" Key Assumption: {bs['key_assumption']}",
f" Key Risk: {bs['key_risk']}",
])
lines.extend(["", "=" * 70])
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Model financial scenarios with probability weighting")
parser.add_argument("--input", "-i", help="JSON file with scenario data")
parser.add_argument("--arr", type=float, help="Current ARR")
parser.add_argument("--expenses", type=float, help="Quarterly expenses")
parser.add_argument("--cash", type=float, help="Cash balance")
parser.add_argument("--quarters", type=int, default=8, help="Projection quarters (default: 8)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
if args.input:
with open(args.input) as f:
data = json.load(f)
elif args.arr:
data = {
"company": "Company",
"arr": args.arr,
"quarterly_expenses": args.expenses or args.arr * 0.35,
"cash_balance": args.cash or 5000000,
"projection_quarters": args.quarters,
}
else:
data = {
"company": "SaaSCo",
"arr": 3000000,
"quarterly_expenses": 900000,
"cash_balance": 5200000,
"projection_quarters": args.quarters,
}
results = model_scenarios(data)
if args.json:
print(json.dumps(results, indent=2))
else:
print(format_text(results))
if __name__ == "__main__":
main()
Related skills
FAQ
What financial areas does cfo-advisor cover?
cfo-advisor stress-tests unit economics, runway, pricing models, and multi-scenario financial projections. Technical teams use it before committing to monetization plans or fundraising narratives tied to product revenue.
Is cfo-advisor a replacement for an accountant?
cfo-advisor produces decision-oriented scenario analysis and pricing validation, not tax filings or audited financial statements. Developers use it early to sanity-check economics before implementing billing systems.