This page documents exactly how dothemathbefore turns your assumptions into LTV, payback, and retention numbers — every formula, every default, and every lookup table it uses. All three business models (subscription, pay-as-you-go, transaction fee) share the same underlying engine; they only differ in how ARPU is derived before it's fed in.
Overview
A product is modeled as a cohort of customers moving through discrete billing cycles. Each cycle has a retention rate, a revenue per surviving customer, and a gross margin. The engine walks the cohort cycle by cycle — from cycle 1 up to a capped maximum lifetime — accumulating revenue and gross profit, then reports the running totals (LTV) alongside CAC-derived metrics like payback period.
Everything below reflects calcSubscriptionLTV() in src/lib/ltv.ts, which is the single calculation function used by all three business models — including the app's MCP server, so numbers are identical whether you use the UI or Claude/Cursor.
Billing cycles & time
Every rate in the model (churn, discount rate) is defined per billing cycle. A cycle is cycleLength × cycleUnit (e.g. "1 month" or "28 days"), converted to days using fixed day-counts per unit:
| Unit | Days |
|---|---|
| day | 1 |
| week | 7 |
| month | 30.4375 (365.25 / 12) |
| quarter | 91.3125 (365.25 / 4) |
| year | 365.25 |
365.25 (not 365) is used everywhere so leap years wash out on average instead of silently drifting the annual↔per-cycle conversions over a multi-year lifetime.
cycleDays = max(1, cycleLength) × daysPerUnit[cycleUnit]
cyclesPerYear = 365.25 / cycleDaysThe "max lifetime" you set (e.g. "2 years") is converted into a hard cap on the number of cycles the loop runs, rounded to the nearest whole cycle and capped at 3650 cycles regardless of input:
lifetimeDays = max(1, lifetimeLength) × daysPerUnit[lifetimeUnit]
maxLifeCycles = clamp(round(lifetimeDays / cycleDays), 1, 3650)Revenue per model
ARPU (average revenue per cycle, per active customer) is computed differently depending on the business model, then handed to the same engine:
Subscription
arpu = price per cycle (direct input)
Pay-as-you-go
arpu = purchaseVolumePerCycle × unitPrice
Transaction fee
revenuePerTxn = (takeRate% × avgTxnValue) + fixedFee
arpu = txnVolumePerCycle × revenuePerTxnFrom this point on, "ARPU" is treated identically regardless of model — a transaction-fee product's ARPU is just "revenue an active account generates per cycle," same as a subscription's price.
Retention & churn
Retention starts at 100% at the beginning of cycle 1. Each cycle's churn rate reduces retention going into the next cycle:
retention(1) = 1
retention(t) = retention(t-1) × (1 - churn(t)) for t > 1
churn(t) = churnOverrides[t] ?? churn (default per-cycle churn)
arpu(t) = arpuOverrides[t] ?? arpu
margin(t)= grossMarginOverrides[t] ?? grossMarginPer-cycle overrides (churn, ARPU, margin) let a specific cycle deviate from the flat default — this is how churn-curve shapes and the freemium override below are implemented, without changing the engine itself.
Trial / freemium
When the access model is "trial / freemium," cycle 1's ARPU is overridden to 0 (the free period), but churn still applies normally from cycle 1 onward:
arpuOverrides["1"] = 0 // only when accessModel === "trial"This means freemium customers who never convert are already modeled — they show up as customers who churn during the free cycle(s) and contribute zero revenue on the way out. The trial/direct choice also feeds the churn-curve recommendation (see below): trial access typically produces an early spike in churn as bad fits filter out fast.
Because of this, CAC should be defined consistently with the population the curve represents: if CAC is your cost per signup (free + paid), the model is already correct as-is. If your CAC is cost per converted, paying customer, you should not also let cycle 1 model conversion risk — that double-counts the drop-off your CAC has already priced in.
Margin, setup fee & expansion
Gross profit per cycle is revenue times that cycle's margin:
revenue(t) = arpu(t) × retention(t) × expansionMultiplier(t)
grossProfit(t) = revenue(t) × margin(t)
expansionMultiplier(1) = 1
expansionMultiplier(t) = expansionMultiplier(t-1) × (1 + expansionRate)expansionRate compounds ARPU cycle over cycle for surviving customers (e.g. seat or usage expansion in B2B) — positive expands, negative contracts. It defaults to 0 (flat ARPU aside from explicit overrides).
An optional one-time setup fee (e.g. a B2B onboarding fee) is collected before cycle 1, undiscounted, and seeds the cumulative totals instead of going through the per-cycle loop:
setupFeeGrossProfit = setupFee × setupFeeMargin (defaults to grossMargin)
revCum(0) = setupFee
gpCum(0) = setupFeeGrossProfit
gpCumDiscounted(0)= setupFeeGrossProfitLTV (gross, net, discounted)
Three running totals accumulate across cycles 1..maxLifeCycles, each starting from the setup-fee seed above:
LTV gross = revCum = setupFee + Σ revenue(t)
LTV effective = gpCum = setupFeeGrossProfit + Σ grossProfit(t)
LTV discounted = gpCumDiscounted = setupFeeGrossProfit + Σ grossProfit(t) / (1 + drCycle)^t"LTV effective" (net, non-discounted cumulative gross profit) is the headline LTV number used for the LTV:CAC ratio and payback period. "LTV discounted" applies the time-value-of-money adjustment described below.
A separate, closed-form theoretical LTV is also shown — the classic steady-state formula, ignoring the lifetime cap and any per-cycle overrides. It's a quick sanity check against the curve-based number above:
if churn > 0: ltvTheoretical = (arpu × margin) / churn
else: ltvTheoretical = arpu × margin × maxLifeCycles
avgLifeCycles = churn > 0 ? 1 / churn : ∞
avgLifeCappedCycles = min(avgLifeCycles, maxLifeCycles)Discounting
You set an annual discount rate; it's converted to a per-cycle rate using the cycle's actual length, so a 28-day and a 30-day cycle compound to the same annual rate:
drCycle = (1 + drAnnual) ^ (1 / cyclesPerYear) − 1Each cycle's gross profit is discounted back to present value by (1 + drCycle)^t before being added to the discounted cumulative total. The setup fee is not discounted — it lands at t = 0 by definition.
CAC, LTV:CAC & payback
CAC is a single input, compared against the cumulative totals above:
LTV:CAC ratio = gpCum / cac (∞ if cac = 0)
LTV:CAC ratio, discounted= gpCumDiscounted / cacPayback period is the fractional cycle at which cumulative gross profit first reaches CAC — found by linear interpolation within the cycle where the crossing happens, rather than snapping to the whole cycle:
// at the first cycle t where cumGrossProfit(t) ≥ cac:
fraction = (cac − cumGrossProfit(t-1)) / (cumGrossProfit(t) − cumGrossProfit(t-1))
paybackCycles = (t − 1) + fractionThe same interpolation is run twice — once on non-discounted cumulative gross profit, once on the discounted series — giving paybackCycles and paybackCyclesDiscounted. Payback is null if CAC is never recovered within the capped lifetime.
paybackShareOfLifetime = paybackCyclesDiscounted / avgLifeCappedCyclesThis last ratio answers "what fraction of a customer's expected lifetime does it take to earn back their acquisition cost" — useful for comparing paybook speed across products with very different cycle lengths.
When you don't know your CAC, onboarding suggests one as a fallback share of LTV (see defaults table below) rather than leaving it at zero.
Churn curve shapes
Instead of a flat churn rate, you can apply a shape across the cycles — implemented purely as per-cycle churn overrides that average out to your stated churn rate. Onboarding recommends a shape from a 2×2 matrix of the nature of the customer's need × how they first got in:
| Need | Access | Recommended curve | Why |
|---|---|---|---|
| Ongoing | Trial | U-shaped | Trial filters bad fits early; ongoing need means whoever stays, stays. |
| Ongoing | Direct | Flat | Self-selected at purchase, little structural reason to leave. |
| Phase (has an end) | Trial | U-shaped (sharper) | Trial filter up front, plus the phase's natural end soon after. |
| Phase (has an end) | Direct | Late-heavy | Convinced at purchase, but churn climbs near the phase's end. |
Crucially, none of this touches retention directly — it only decides what churn(t) is at each cycle. The retention curve in the chart is just the cumulative product from the Retention & churn section, applied to whatever per-cycle churn values this section produces.
Building the per-cycle churn values
A curve is built from up to two additive "tails" layered on top of a flat baseline, evaluated at each cycle's position p (0 = first cycle, 1 = last cycle):
baseline = 0.45 // late-heavy, always
baseline = need === "continuous" ? 0.4 : 0.45 // flat / u-shape
earlyTail(p) = amplitude(early) × (3.4 − baseline) × exp(−(p / width(early))²)
// u-shape's late tail — a mirrored bell peaking at the last cycle
lateTailBell(p) = amplitude(late) × (peak − baseline) × exp(−((1 − p) / width(late))²)
peak = need === "continuous" ? 1.8 : 4.2
// late-heavy's late tail — a power-curve ramp, not a bell
lateTailRamp(p) = amplitude(late) × (3.5 − baseline) × p ^ exponent(late)
raw(t) = baseline + earlyTail(p) + (curve === "late-heavy" ? lateTailRamp(p) : lateTailBell(p))The early tail is a bell curve centered on cycle 1 that decays fast — modeling a trial/quality filter spiking churn right at the start, then fading out. The late tail is either another bell centered on the last cycle (u-shape — a second, independent risk near the end) or a monotonic ramp (late-heavy — churn stays low, then climbs as customers approach a natural end point).
The two steepness sliders (0–1, 0.5 = default)
Each tail has its own slider, and each slider controls two things at once: how much of the tail shows up at all, and how narrow/abrupt it is once it does:
amplitude(s) = min(1, 2 × s) // 0 at s=0 (tail absent), 1 from s=0.5 onward
width(s) = 2 ^ (1 − 2 × s) // wider/gentler at low s, narrower/sharper at high s
exponent(s) = 2 ^ (2 × s) // late-heavy ramp only: 1 (linear) .. 4 (flat, then spikes)At steepness 0 a tail contributes nothing — it's not just small, it's completely absent from the shape. That's what lets both sliders at (0, 0) collapse any curve type to a perfectly flat line: with no tails, raw(t) is the same constant baseline at every cycle.
When you don't touch the sliders, each curve type resolves to its own untuned defaults:
| Curve | Early steepness | Late steepness |
|---|---|---|
| Flat | 0 (no tail) | 0 (no tail) |
| U-shaped | 0.5 | 0.5 |
| Late-heavy | 0 (no tail) | 0.5 |
Flat's defaults are both 0, so it never has any tails to begin with — which is why the engine skips generating a churn-override table for it entirely (see below) and just applies your flat churn rate directly on every cycle.
Normalizing back to your stated average
The raw(t) values above are shape only — they don't carry a real churn magnitude yet. They're divided by their own mean (so the mean becomes exactly 1), then scaled by the average churn you actually entered:
mean = average of raw(t) across all cycles
churn(t) = clamp(avgChurn × raw(t) / mean, 0, 1) // this fills churnOverrides[t]This is what guarantees the chosen shape never changes the number you typed in: whatever shape you pick, the mean of churn(t) across the curve is still your stated average churn — only its distribution across cycles changes. If both tails are absent (steepness 0 on each), the engine returns undefined instead of an override table, and churnAtCycle() falls back straight to the flat churn value on every cycle.
Sector benchmarks
When you don't know your churn rate, picking a sector suggests one — sourced as annual logo-churn (SaaS surveys / public benchmarks for B2C verticals; ChartMogul/ProfitWell/KeyBanc-style SMB/mid-market/enterprise splits for B2B) and converted to your billing cycle on the fly:
annualRetention = clamp(1 − annualChurn, 0, 1)
perCycleRetention= annualRetention ^ (1 / cyclesPerYear)
perCycleChurn = 1 − perCycleRetentionB2C sectors (segmented by vertical). Monthly churn is the annual figure converted with the formula above, at a 30.4375-day cycle (exactly 12 cycles/year):
| Sector | Annual churn | Monthly churn |
|---|---|---|
| SaaS consumer / app software B2C | 47.5% | 5.2% |
| Streaming video / SVOD | 50% | 5.6% |
| Streaming audio (music) | 12% | 1.1% |
| Fintech consumer / neobank | 59% | 7.2% |
| Fitness & wellness apps | 61% | 7.5% |
| EdTech B2C (online courses / skills) | 60% | 7.4% |
| Kids education apps | 59.9% | 7.3% |
| Gaming (mobile app subscription) | 62% | 7.7% |
| Subscription box (beauty, curation) | 70% | 9.5% |
| Meal kit | 72% | 10.1% |
| Telemedicine / telehealth | 70% | 9.5% |
| DTC supplements / wellness subscription | 60% | 7.4% |
| E-commerce DTC (non-subscription) | 69% | 9.3% |
| Food delivery | 63% | 8.0% |
| Dating app | 65% | 8.4% |
B2B sectors (segmented by customer segment, not vertical — deal size drives churn far more than industry at this level):
| Segment | Annual churn | Monthly churn |
|---|---|---|
| SaaS B2B — SMB / self-serve (low ticket, no sales team) | 25% | 2.4% |
| SaaS B2B — Mid-market (sales-assisted) | 13% | 1.2% |
| SaaS B2B — Enterprise (multi-year contracts) | 6% | 0.5% |
Default constants
Values the calculators fall back to when you skip a question, shown as editable placeholders rather than hidden assumptions:
| Constant | Value | Used for |
|---|---|---|
| Default annual discount rate | 3% | Discounted LTV, when not overridden |
| Typical churn — Subscription | 5% / cycle | Fallback before a sector is picked |
| Typical churn — PAYG / Transaction fee | 8% / cycle | Fallback before a sector is picked |
| CAC fallback share of LTV | 30% | Suggested CAC when you don't know your real one |
| Max lifetime cap (hard ceiling) | 3650 cycles | Upper bound on maxLifeCycles regardless of input |
This engine is also exposed over MCP, so Claude or Cursor can run the exact same calculations — see MCP setup.