mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
* fix(calculator): honest burn-multiple and runway labels, surface model assumptions - Burn Multiple now reports Graham's metric (net burn / net new ARR); the net burn / MRR ratio is reported separately as Burn to Revenue. The qualifier (efficient/healthy/warning/critical) is derived from the real burn multiple, so DEAD verdicts no longer print 'efficient'. - ALIVE verdicts no longer print a misleading 'Runway: 120 months' (projection cap); output now shows 'Projected cash-out' with 'none within the 10-year projection' when the company never runs out. - Model assumptions (fixed/variable burn split, variable burn ratio, growth decay, projection cap, safety buffer) are now surfaced in the human report and in JSON model_assumptions. - SKILL.md: fix dead paulgraham.com/default.html source URL to aord.html; update output-field docs and examples to real model output. - Add regression tests (tests/integration/test_default_alive.py). Fixes #272 Fixes #273 * docs(calculator): add When Not to Use boundary (validator requirement)
431 lines
18 KiB
Python
Executable File
431 lines
18 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Default Alive / Default Dead Calculator
|
|
|
|
Paul Graham's foundational startup diagnostic: given current revenue, burn rate,
|
|
cash on hand, and growth rate, determine whether a startup will reach
|
|
profitability before running out of money.
|
|
|
|
Usage:
|
|
python default-alive.py --monthly-revenue 50000 --monthly-burn 120000 --cash-on-hand 800000 --monthly-growth 8
|
|
python default-alive.py --monthly-revenue 30000 --monthly-burn 75000 --cash-on-hand 500000 --monthly-growth 10 --json
|
|
python default-alive.py --monthly-revenue 50000 --monthly-burn 120000 --cash-on-hand 800000 --monthly-growth 8 --verbose
|
|
|
|
See SKILL.md for full documentation and methodology.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Core calculation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
MAX_PROJECTION_MONTHS = 120 # 10 years — safety limit
|
|
SAFETY_BUFFER_MONTHS = 3 # months of runway required post-breakeven
|
|
|
|
|
|
def project_trajectory(
|
|
monthly_revenue: float,
|
|
monthly_burn: float,
|
|
cash_on_hand: float,
|
|
monthly_growth_pct: float,
|
|
growth_decay_pct: float = 0.5,
|
|
fixed_burn_pct: float = 70.0,
|
|
) -> dict:
|
|
"""Project month-by-month financial trajectory.
|
|
|
|
Parameters
|
|
----------
|
|
monthly_revenue : Current monthly recurring revenue (MRR)
|
|
monthly_burn : Total monthly operating expenses
|
|
cash_on_hand : Cash reserves
|
|
monthly_growth_pct : Month-over-month revenue growth rate (%)
|
|
growth_decay_pct : Monthly decay in growth rate (%) — models market saturation
|
|
fixed_burn_pct : Percentage of burn that is fixed (vs. variable with revenue)
|
|
|
|
Returns
|
|
-------
|
|
dict with trajectory, verdict, and diagnostic metrics
|
|
"""
|
|
growth_rate = monthly_growth_pct / 100.0
|
|
decay_rate = growth_decay_pct / 100.0
|
|
fixed_burn = monthly_burn * (fixed_burn_pct / 100.0)
|
|
variable_burn_ratio = (monthly_burn * (1 - fixed_burn_pct / 100.0)) / max(monthly_revenue, 1)
|
|
|
|
revenue = monthly_revenue
|
|
cash = cash_on_hand
|
|
peak_revenue = revenue
|
|
current_growth = growth_rate
|
|
|
|
trajectory = []
|
|
breakeven_month: int | None = None
|
|
cashout_month: int | None = None
|
|
|
|
for month in range(1, MAX_PROJECTION_MONTHS + 1):
|
|
# Revenue grows (or decays) at current growth rate
|
|
revenue = revenue * (1 + current_growth)
|
|
|
|
# Growth rate decays toward zero
|
|
current_growth = current_growth * (1 - decay_rate)
|
|
|
|
# Burn: fixed component + variable component
|
|
variable_burn = revenue * variable_burn_ratio
|
|
total_burn = fixed_burn + variable_burn
|
|
|
|
# Cash flow
|
|
net_cash = revenue - total_burn
|
|
cash += net_cash
|
|
|
|
# Track peak revenue for decay modeling
|
|
peak_revenue = max(peak_revenue, revenue)
|
|
|
|
entry = {
|
|
"month": month,
|
|
"revenue": round(revenue, 2),
|
|
"burn": round(total_burn, 2),
|
|
"net_cash_flow": round(net_cash, 2),
|
|
"cash": round(cash, 2),
|
|
"growth_rate_pct": round(current_growth * 100, 2),
|
|
"profitable": net_cash >= 0,
|
|
}
|
|
trajectory.append(entry)
|
|
|
|
# Track first breakeven month
|
|
if net_cash >= 0 and breakeven_month is None:
|
|
breakeven_month = month
|
|
|
|
# Track cash-out month
|
|
if cash <= 0 and cashout_month is None:
|
|
cashout_month = month
|
|
cash = 0 # floor at zero
|
|
|
|
# Stop if both conditions are met (or we've run out of cash with no hope)
|
|
if breakeven_month is not None and cashout_month is not None:
|
|
break
|
|
|
|
# If we've been unprofitable for 5 years and cash is gone, stop
|
|
if month > 60 and cash <= 0 and net_cash < 0:
|
|
if cashout_month is None:
|
|
cashout_month = month
|
|
break
|
|
|
|
# ------------------------------------------------------------------
|
|
# Diagnostics
|
|
# ------------------------------------------------------------------
|
|
|
|
runway_months = cashout_month if cashout_month else None # None = never runs out within projection
|
|
net_burn = monthly_burn - monthly_revenue
|
|
|
|
# Secondary diagnostic: net burn / MRR. This is NOT Graham's burn multiple.
|
|
burn_to_revenue_ratio = round(net_burn / max(monthly_revenue, 1), 2) if monthly_revenue > 0 else None
|
|
|
|
# Graham's burn multiple: net burn / net new ARR (annualized new recurring revenue added this month)
|
|
current_arr = monthly_revenue * 12
|
|
if monthly_growth_pct > 0 and monthly_revenue > 0:
|
|
next_arr = (monthly_revenue * (1 + monthly_growth_pct / 100)) * 12
|
|
net_new_arr = next_arr - current_arr
|
|
burn_multiple = round(net_burn / max(net_new_arr, 1), 2) if net_new_arr > 0 else None
|
|
else:
|
|
net_new_arr = 0
|
|
burn_multiple = None
|
|
|
|
# Verdict
|
|
if breakeven_month is not None and cashout_month is not None:
|
|
if breakeven_month < cashout_month:
|
|
post_breakeven_runway = cashout_month - breakeven_month
|
|
verdict = "ALIVE" if post_breakeven_runway >= SAFETY_BUFFER_MONTHS else "MARGINAL"
|
|
else:
|
|
verdict = "DEAD"
|
|
elif breakeven_month is not None and cashout_month is None:
|
|
verdict = "ALIVE"
|
|
elif cashout_month is not None and breakeven_month is None:
|
|
verdict = "DEAD"
|
|
else:
|
|
verdict = "MARGINAL"
|
|
|
|
# Levers
|
|
levers = []
|
|
if verdict == "DEAD" or verdict == "MARGINAL":
|
|
if monthly_growth_pct < 15:
|
|
levers.append({
|
|
"name": "accelerate-growth",
|
|
"description": "Increasing growth rate to 15%/month would reach breakeven sooner",
|
|
"impact": "high",
|
|
})
|
|
if monthly_burn > monthly_revenue * 2:
|
|
levers.append({
|
|
"name": "reduce-burn",
|
|
"description": "Burn is more than 2x revenue — cost reduction extends runway directly",
|
|
"impact": "high",
|
|
})
|
|
levers.append({
|
|
"name": "fundraising",
|
|
"description": "Default Dead means fundraising is existential, not optional",
|
|
"impact": "critical",
|
|
})
|
|
if monthly_revenue > 0:
|
|
levers.append({
|
|
"name": "pricing",
|
|
"description": "20% price increase with <5% churn impact could shift trajectory significantly",
|
|
"impact": "medium",
|
|
})
|
|
|
|
# Monthly gap
|
|
gap_to_breakeven = monthly_burn - monthly_revenue
|
|
months_of_gap = round(cash_on_hand / max(gap_to_breakeven, 1), 1) if gap_to_breakeven > 0 else None
|
|
|
|
# ------------------------------------------------------------------
|
|
# Assemble result
|
|
# ------------------------------------------------------------------
|
|
|
|
result = {
|
|
"inputs": {
|
|
"monthly_revenue": monthly_revenue,
|
|
"monthly_burn": monthly_burn,
|
|
"cash_on_hand": cash_on_hand,
|
|
"monthly_growth_pct": monthly_growth_pct,
|
|
},
|
|
"diagnostics": {
|
|
"net_monthly_burn": round(net_burn, 2),
|
|
"burn_multiple": burn_multiple,
|
|
"burn_to_revenue_ratio": burn_to_revenue_ratio,
|
|
"current_arr": round(current_arr, 2),
|
|
"net_new_arr": round(net_new_arr, 2),
|
|
"projected_cashout_month": runway_months,
|
|
"months_to_breakeven": breakeven_month,
|
|
"cashout_month": cashout_month,
|
|
"gap_to_breakeven": round(gap_to_breakeven, 2),
|
|
"months_of_gap_remaining": months_of_gap,
|
|
"months_projected": len(trajectory),
|
|
},
|
|
"model_assumptions": {
|
|
"fixed_burn_pct": fixed_burn_pct,
|
|
"variable_burn_ratio": round(variable_burn_ratio, 4),
|
|
"growth_decay_pct": growth_decay_pct,
|
|
"projection_cap_months": MAX_PROJECTION_MONTHS,
|
|
"safety_buffer_months": SAFETY_BUFFER_MONTHS,
|
|
},
|
|
"verdict": verdict,
|
|
"explanation": _generate_explanation(
|
|
verdict, runway_months, breakeven_month, cashout_month,
|
|
burn_multiple, burn_to_revenue_ratio, monthly_revenue, monthly_burn,
|
|
cash_on_hand,
|
|
),
|
|
"levers": levers,
|
|
"trajectory": trajectory if len(trajectory) <= 60 else trajectory[:60],
|
|
}
|
|
|
|
return result
|
|
|
|
|
|
def _generate_explanation(
|
|
verdict: str,
|
|
runway_months: int | None,
|
|
breakeven_month: int | None,
|
|
cashout_month: int | None,
|
|
burn_multiple: float | None,
|
|
burn_to_revenue_ratio: float | None,
|
|
monthly_revenue: float,
|
|
monthly_burn: float,
|
|
cash_on_hand: float,
|
|
) -> str:
|
|
"""Generate plain-English explanation of the verdict."""
|
|
lines = []
|
|
|
|
if verdict == "ALIVE":
|
|
lines.append("✅ DEFAULT ALIVE — You will reach profitability before running out of cash.")
|
|
if breakeven_month:
|
|
lines.append(f" Breakeven projected at month {breakeven_month}.")
|
|
elif verdict == "DEAD":
|
|
lines.append("❌ DEFAULT DEAD — You will run out of cash before reaching profitability.")
|
|
if cashout_month:
|
|
lines.append(f" Cash runs out at month {cashout_month}.")
|
|
if breakeven_month:
|
|
lines.append(f" Breakeven would require {breakeven_month} months — too late.")
|
|
else:
|
|
lines.append("⚠️ MARGINAL — Breakeven is possible but dangerously close to cash-out.")
|
|
if breakeven_month and cashout_month:
|
|
lines.append(f" Breakeven at month {breakeven_month}, cash-out at month {cashout_month}.")
|
|
lines.append(f" Only {cashout_month - breakeven_month} months of post-breakeven buffer (need {SAFETY_BUFFER_MONTHS}+).")
|
|
|
|
lines.append("")
|
|
if burn_multiple is not None:
|
|
qualifier = "efficient" if burn_multiple < 1 else "healthy" if burn_multiple < 2 else "warning" if burn_multiple < 3 else "critical"
|
|
lines.append(f" Burn Multiple: {burn_multiple}x ({qualifier}, net burn / net new ARR)")
|
|
if burn_to_revenue_ratio is not None:
|
|
lines.append(f" Burn to Revenue: {burn_to_revenue_ratio}x (net burn / MRR)")
|
|
if runway_months is None:
|
|
lines.append(" Projected cash-out: none within the 10-year projection")
|
|
else:
|
|
lines.append(f" Projected cash-out (model): month {runway_months}")
|
|
lines.append(f" Monthly gap: ${monthly_burn - monthly_revenue:,.0f}")
|
|
lines.append(f" Cash: ${cash_on_hand:,.0f}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def format_output(result: dict, verbose: bool) -> str:
|
|
"""Format the result as a human-readable report."""
|
|
lines = []
|
|
|
|
# Header
|
|
lines.append("=" * 60)
|
|
lines.append(" DEFAULT ALIVE / DEFAULT DEAD ANALYSIS")
|
|
lines.append("=" * 60)
|
|
lines.append("")
|
|
|
|
# Inputs
|
|
inputs = result["inputs"]
|
|
lines.append("── Inputs ──────────────────────────────────────────────")
|
|
lines.append(f" Monthly revenue: ${inputs['monthly_revenue']:>8,.0f}")
|
|
lines.append(f" Monthly burn: ${inputs['monthly_burn']:>8,.0f}")
|
|
lines.append(f" Cash on hand: ${inputs['cash_on_hand']:>8,.0f}")
|
|
lines.append(f" Monthly growth: {inputs['monthly_growth_pct']:>7.1f}%")
|
|
lines.append("")
|
|
|
|
# Verdict
|
|
diag = result["diagnostics"]
|
|
lines.append("── Verdict ─────────────────────────────────────────────")
|
|
lines.append(f" {result['verdict']}")
|
|
lines.append("")
|
|
lines.append(result["explanation"])
|
|
lines.append("")
|
|
|
|
# Diagnostics
|
|
lines.append("── Diagnostics ─────────────────────────────────────────")
|
|
lines.append(f" Net monthly burn: ${diag['net_monthly_burn']:>8,.0f}")
|
|
if diag["burn_multiple"] is not None:
|
|
lines.append(f" Burn Multiple: {diag['burn_multiple']:>8.2f}x (net burn ÷ net new ARR)")
|
|
if diag["burn_to_revenue_ratio"] is not None:
|
|
lines.append(f" Burn to Revenue: {diag['burn_to_revenue_ratio']:>8.2f}x (net burn ÷ MRR)")
|
|
lines.append(f" Current ARR: ${diag['current_arr']:>8,.0f}")
|
|
if diag["net_new_arr"]:
|
|
lines.append(f" Net new ARR/month: ${diag['net_new_arr']:>8,.0f}")
|
|
cashout = diag["projected_cashout_month"]
|
|
cashout_label = "none within 10y projection" if cashout is None else f"month {cashout}"
|
|
lines.append(f" Projected cash-out: {cashout_label:>8}")
|
|
lines.append(f" Months to breakeven: {diag['months_to_breakeven'] or 'never':>8}")
|
|
if diag["months_of_gap_remaining"]:
|
|
lines.append(f" Cash gap coverage: {diag['months_of_gap_remaining']:>8.1f} months at current spend")
|
|
lines.append("")
|
|
|
|
# Model assumptions
|
|
if "model_assumptions" in result:
|
|
a = result["model_assumptions"]
|
|
lines.append("── Model Assumptions ──────────────────────────────")
|
|
lines.append(f" Fixed burn: {a['fixed_burn_pct']:.0f}% of burn fixed; {100 - a['fixed_burn_pct']:.0f}% scales with revenue")
|
|
lines.append(f" Variable burn ratio: {a['variable_burn_ratio']:.2f} per $1 of revenue")
|
|
lines.append(f" Growth decay: {a['growth_decay_pct']:.1f}% per month")
|
|
lines.append(f" Projection cap: {a['projection_cap_months']} months | Safety buffer: {a['safety_buffer_months']} months")
|
|
lines.append("")
|
|
|
|
# Levers
|
|
if result["levers"]:
|
|
lines.append("── Levers ───────────────────────────────────────────────")
|
|
for lever in result["levers"]:
|
|
icon = {"high": "🔴", "medium": "🟡", "critical": "🚨"}.get(lever["impact"], "⚪")
|
|
lines.append(f" {icon} {lever['name']}: {lever['description']}")
|
|
lines.append("")
|
|
|
|
# Trajectory (verbose only)
|
|
if verbose and result.get("trajectory"):
|
|
lines.append("── Monthly Trajectory ───────────────────────────────────")
|
|
lines.append(f" {'Mo':>4} {'Revenue':>10} {'Burn':>10} {'Net Cash':>10} {'Cash':>12} {'Growth':>7} {'Prof?':>5}")
|
|
lines.append(" " + "-" * 60)
|
|
for entry in result["trajectory"][:36]: # First 3 years
|
|
flag = "✓" if entry["profitable"] else "✗"
|
|
lines.append(
|
|
f" {entry['month']:>4} "
|
|
f"${entry['revenue']:>8,.0f} "
|
|
f"${entry['burn']:>8,.0f} "
|
|
f"${entry['net_cash_flow']:>8,.0f} "
|
|
f"${entry['cash']:>10,.0f} "
|
|
f"{entry['growth_rate_pct']:>5.1f}% "
|
|
f"{flag:>4}"
|
|
)
|
|
if len(result["trajectory"]) > 36:
|
|
lines.append(f" ... ({len(result['trajectory']) - 36} more months projected)")
|
|
lines.append("")
|
|
|
|
lines.append("=" * 60)
|
|
lines.append(" Paul Graham's Default Alive/Dead Framework")
|
|
lines.append(" paulgraham.com/default.html")
|
|
lines.append("=" * 60)
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Default Alive / Default Dead Calculator",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python default-alive.py --monthly-revenue 50000 --monthly-burn 120000 --cash-on-hand 800000 --monthly-growth 8
|
|
python default-alive.py --monthly-revenue 30000 --monthly-burn 75000 --cash-on-hand 500000 --monthly-growth 10 --json
|
|
python default-alive.py --monthly-revenue 50000 --monthly-burn 120000 --cash-on-hand 800000 --monthly-growth 8 --verbose
|
|
""",
|
|
)
|
|
parser.add_argument("--monthly-revenue", type=float, required=True, help="Current monthly recurring revenue")
|
|
parser.add_argument("--monthly-burn", type=float, required=True, help="Total monthly operating expenses")
|
|
parser.add_argument("--cash-on-hand", type=float, required=True, help="Current cash reserves")
|
|
parser.add_argument("--monthly-growth", type=float, required=True, help="Month-over-month revenue growth rate (%)")
|
|
parser.add_argument("--growth-decay", type=float, default=0.5, help="Monthly growth deceleration (%%, default: 0.5)")
|
|
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
parser.add_argument("--verbose", action="store_true", help="Show monthly projection")
|
|
parser.add_argument("--dry-run", action="store_true", help="Validate inputs and show what would be computed")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Validate
|
|
if args.monthly_revenue < 0:
|
|
parser.error("monthly-revenue must be >= 0")
|
|
if args.monthly_burn <= 0:
|
|
parser.error("monthly-burn must be > 0")
|
|
if args.cash_on_hand < 0:
|
|
parser.error("cash-on-hand must be >= 0")
|
|
if args.monthly_growth < 0:
|
|
parser.error("monthly-growth must be >= 0")
|
|
if args.growth_decay < 0 or args.growth_decay > 10:
|
|
parser.error("growth-decay should be between 0 and 10")
|
|
|
|
if args.dry_run:
|
|
print(json.dumps({
|
|
"status": "valid",
|
|
"inputs": {
|
|
"monthly_revenue": args.monthly_revenue,
|
|
"monthly_burn": args.monthly_burn,
|
|
"cash_on_hand": args.cash_on_hand,
|
|
"monthly_growth": args.monthly_growth,
|
|
},
|
|
"message": "Inputs validated. Run without --dry-run to compute.",
|
|
}, indent=2))
|
|
return
|
|
|
|
result = project_trajectory(
|
|
monthly_revenue=args.monthly_revenue,
|
|
monthly_burn=args.monthly_burn,
|
|
cash_on_hand=args.cash_on_hand,
|
|
monthly_growth_pct=args.monthly_growth,
|
|
growth_decay_pct=args.growth_decay,
|
|
)
|
|
|
|
if args.json:
|
|
# Strip trajectory unless verbose requested it
|
|
output = result.copy()
|
|
if not args.verbose and "trajectory" in output:
|
|
output["trajectory"] = f"{len(result['trajectory'])} months projected (use --verbose to show)"
|
|
print(json.dumps(output, indent=2))
|
|
else:
|
|
print(format_output(result, verbose=args.verbose))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|