e (e.g., 0.05 for 5%)
}
export interface Scenario {
id: string;
drivers: Record<string, Driver>;
metadata: Record<string, string>;
}
export interface MonthlyState {
month: number;
date: string;
users: number;
mrr: Decimal;
cogs: Decimal;
opex: Decimal;
burnRate: Decimal;
cashBalance: Decimal;
runway: number;
}
#### 2. Implement the Engine
The engine manages the time-step iteration and state transitions.
```typescript
// engine/financial-engine.ts
import { Decimal } from 'decimal.js';
import { Scenario, MonthlyState, Driver } from '../types/financials';
export class FinancialEngine {
private initialCash: Decimal;
constructor(initialCash: number) {
this.initialCash = new Decimal(initialCash);
}
public simulate(scenario: Scenario, months: number): MonthlyState[] {
const history: MonthlyState[] = [];
let currentState: MonthlyState = this.initializeState(scenario);
for (let m = 1; m <= months; m++) {
currentState = this.advanceMonth(currentState, scenario, m);
history.push({ ...currentState, month: m });
}
return history;
}
private initializeState(scenario: Scenario): MonthlyState {
const drivers = scenario.drivers;
return {
month: 0,
date: new Date().toISOString(),
users: drivers['active_users']?.value || 0,
mrr: new Decimal(drivers['mrr']?.value || 0),
cogs: new Decimal(0),
opex: new Decimal(0),
burnRate: new Decimal(0),
cashBalance: this.initialCash,
runway: 0,
};
}
private advanceMonth(
prev: MonthlyState,
scenario: Scenario,
month: number
): MonthlyState {
const drivers = scenario.drivers;
// Calculate growth with compounding
const userGrowth = drivers['user_growth']?.growthRate || 0;
const newUsers = prev.users * (1 + userGrowth);
// Calculate MRR based on ARPU or direct growth
const mrrGrowth = drivers['mrr_growth']?.growthRate || 0;
const newMrr = prev.mrr.mul(1 + mrrGrowth);
// COGS and OPEX calculations
const cogs = newMrr.mul(drivers['cogs_percentage']?.value || 0);
const opex = new Decimal(drivers['fixed_opex']?.value || 0)
.add(newUsers * (drivers['variable_opex_per_user']?.value || 0));
// Burn Rate
const burnRate = opex.sub(newMrr);
const newCash = prev.cashBalance.sub(burnRate);
// Runway
const runway = burnRate.gt(0)
? newCash.div(burnRate).toNumber()
: Infinity;
return {
month,
date: this.advanceDate(prev.date),
users: newUsers,
mrr: newMrr,
cogs,
opex,
burnRate,
cashBalance: newCash,
runway,
};
}
private advanceDate(isoDate: string): string {
const d = new Date(isoDate);
d.setMonth(d.getMonth() + 1);
return d.toISOString();
}
}
3. Unit Economics Validation
Unit economics must be validated via unit tests to ensure mathematical correctness.
// tests/unit-economics.test.ts
import { describe, it, expect } from 'vitest';
import { calculateLTV } from '../utils/unit-economics';
import { Decimal } from 'decimal.js';
describe('Unit Economics', () => {
it('should calculate LTV correctly with churn', () => {
// LTV = ARPU / Churn Rate
const arpu = 50;
const churn = 0.05; // 5% monthly
const expectedLTV = 1000;
const ltv = calculateLTV(arpu, churn);
expect(ltv.toNumber()).toBeCloseTo(expectedLTV, 2);
});
it('should throw error if churn is zero or negative', () => {
expect(() => calculateLTV(50, 0)).toThrow();
expect(() => calculateLTV(50, -0.01)).toThrow();
});
});
4. Sensitivity Analysis Module
Generate multiple scenarios programmatically to analyze sensitivity.
// analysis/sensitivity.ts
import { FinancialEngine } from '../engine/financial-engine';
import { Scenario, Driver } from '../types/financials';
export function runSensitivityAnalysis(
engine: FinancialEngine,
baseScenario: Scenario,
variable: string,
range: number[]
) {
const results = range.map((val) => {
const modifiedScenario: Scenario = {
...baseScenario,
id: `${baseScenario.id}_sensitivity_${variable}_${val}`,
drivers: {
...baseScenario.drivers,
[variable]: {
...baseScenario.drivers[variable],
value: val,
},
},
};
const projection = engine.simulate(modifiedScenario, 24);
const finalRunway = projection[projection.length - 1].runway;
return { variable, value: val, runwayMonths: finalRunway };
});
return results;
}
Pitfall Guide
1. Floating-Point Precision Errors
Mistake: Using native JavaScript numbers for currency.
Explanation: IEEE 754 floating-point arithmetic leads to precision loss (e.g., 0.1 + 0.2 !== 0.3). Over 36 months, this compounds, causing cash balance discrepancies that trigger false runway alerts.
Best Practice: Always use decimal.js or big.js for monetary values. Store values as integers (cents) if avoiding libraries.
2. Circular Dependencies
Mistake: Revenue depends on marketing spend, which depends on revenue.
Explanation: Spreadsheets often create circular references. In code, this causes infinite loops or stack overflows if not handled.
Best Practice: Resolve circularities by decoupling. Use a fixed budget allocation rule (e.g., "Marketing is 20% of prior month's revenue") or implement a solver that iterates to convergence for the current period before advancing.
3. Ignoring Seasonality and Lag Effects
Mistake: Assuming linear growth or immediate impact of drivers.
Explanation: CAC often lags; spend in Month 1 yields users in Month 2. Revenue recognition may lag billing.
Best Practice: Implement delay buffers in the engine. Use arrays to track spend over time and apply conversion functions with lags.
4. Hardcoding Magic Numbers
Mistake: Embedding assumptions directly in calculation logic.
Explanation: Makes the model brittle and hard to audit. Changing churn requires hunting through logic.
Best Practice: Centralize all assumptions in the Scenario object. The engine should be pure logic driven entirely by injected parameters.
5. Neglecting Unit Test Coverage
Mistake: Treating the model as "math that works" without tests.
Explanation: Financial logic is business logic. A bug in the burn rate calculation can mislead the entire company.
Best Practice: Write unit tests for every driver function and integration tests for the engine. Verify edge cases: zero cash, negative growth, infinite runway.
6. Over-Engineering Early Models
Mistake: Building a complex microservice architecture for a seed-stage model.
Explanation: Adds unnecessary overhead when the primary need is speed and flexibility.
Best Practice: Start with a modular TypeScript script. Promote to a service only when multiple teams need programmatic access or when simulation volume requires scaling.
7. Static Validation
Mistake: Validating the model once at creation.
Explanation: Market conditions change. A model validated in Q1 may be invalid in Q3.
Best Practice: Implement regression tests that compare model outputs against actuals periodically. Alert when variance exceeds thresholds.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Pre-Seed / Ideation | Simple TypeScript Script | Rapid iteration, low overhead, easy to share via Git. | Low (Dev hours) |
| Seed / Series A | Modular Library + Tests | Auditability, investor readiness, scalable assumptions. | Medium (Architecture effort) |
| Growth / Series B+ | Data Warehouse Integration | Automate driver ingestion from analytics tools, real-time updates. | High (Infra + Dev) |
| Complex Unit Economics | Monte Carlo Simulation Engine | Quantify risk, handle stochastic variables (churn, conversion). | Medium (Algorithm complexity) |
| Multi-Entity / International | Distributed Modeling Service | Handle currency conversion, tax jurisdictions, inter-company flows. | High (System complexity) |
Configuration Template
Use this template to initialize a standardized model project.
// config/model.config.ts
import { Scenario, Driver } from '../types/financials';
export const DEFAULT_BASELINE: Scenario = {
id: 'baseline_v1',
metadata: {
version: '1.0.0',
author: 'cto@startup.com',
date: '2024-05-20',
description: 'Base case assuming 10% MoM growth',
},
drivers: {
active_users: { name: 'Active Users', value: 1000 },
mrr: { name: 'MRR', value: 15000 },
user_growth: { name: 'User Growth', value: 0, growthRate: 0.10 },
mrr_growth: { name: 'MRR Growth', value: 0, growthRate: 0.12 },
cogs_percentage: { name: 'COGS %', value: 0.25 },
fixed_opex: { name: 'Fixed OPEX', value: 25000 },
variable_opex_per_user: { name: 'Var OPEX/User', value: 2.50 },
churn_rate: { name: 'Churn Rate', value: 0.04 },
cac: { name: 'CAC', value: 150 },
arpu: { name: 'ARPU', value: 15 },
},
};
export const SCENARIOS: Record<string, Scenario> = {
baseline: DEFAULT_BASELINE,
aggressive: {
...DEFAULT_BASELINE,
id: 'aggressive_v1',
metadata: { ...DEFAULT_BASELINE.metadata, description: 'Aggressive growth, higher burn' },
drivers: {
...DEFAULT_BASELINE.drivers,
user_growth: { ...DEFAULT_BASELINE.drivers.user_growth, growthRate: 0.20 },
fixed_opex: { ...DEFAULT_BASELINE.drivers.fixed_opex, value: 40000 },
},
},
conservative: {
...DEFAULT_BASELINE,
id: 'conservative_v1',
metadata: { ...DEFAULT_BASELINE.metadata, description: 'Conservative growth, efficiency focus' },
drivers: {
...DEFAULT_BASELINE.drivers,
user_growth: { ...DEFAULT_BASELINE.drivers.user_growth, growthRate: 0.05 },
fixed_opex: { ...DEFAULT_BASELINE.drivers.fixed_opex, value: 20000 },
},
},
};
Quick Start Guide
-
Initialize Project:
mkdir startup-model && cd startup-model
npm init -y
npm install typescript decimal.js vitest
npx tsc --init
-
Create Structure:
mkdir -p src/{types,engine,utils,tests} config
-
Add Core Files:
Copy the types/financials.ts, engine/financial-engine.ts, and config/model.config.ts from this article into the respective directories.
-
Run Simulation:
Create src/index.ts:
import { FinancialEngine } from './engine/financial-engine';
import { SCENARIOS } from '../config/model.config';
const engine = new FinancialEngine(500000); // $500k initial cash
const projection = engine.simulate(SCENARIOS.baseline, 24);
console.log('Final Runway (Months):', projection[23].runway);
console.log('Cash Balance:', projection[23].cashBalance.toString());
Run with npx ts-node src/index.ts.
-
Validate with Tests:
Add unit tests for drivers and run npx vitest. Ensure all tests pass before committing. Integrate with CI to prevent regressions.