以下情境为教学假设,人物与数字不代表经核实的真实案例。
转型通常从一个很朴素的念头开始:服务太累,想做成产品;买断收入不稳,想改成订阅。念头本身没有错,错在把它当成一个「切换开关」——仿佛某一天关掉旧收入,打开新收入,生意就完成了升级。
真实的转型是一条交叉曲线:旧收入在下降,新收入在上升,两条线在某个时点交叉。交叉之前,企业要靠现金储备补差额;交叉之后,才谈得上「转型成功」。这一节把这条曲线算出来,用的仍然是案例事实表里的数字。
《独立开发者的商业指南》关心的转型主要有两条:
| 路径 | 起点 | 终点 | 现金变化 | 主要风险 |
|---|---|---|---|---|
| 服务转产品 | 按小时或项目收费 | 订阅或买断产品 | 从「交付即收款」变成「先投入后回收」 | 产品起量慢,服务收入先降 |
| 买断转订阅 | 一次性售卖 | 持续订阅 | 从「上线集中收款」变成「按月分摊」 | 首年现金下降,老客户不续费 |
Nord Studio 是服务转产品的样本:咨询 110 小时/月、85 美元/小时,月收入 9,350 美元;产品 90 家、39 美元/月,月收入 3,510 美元。咨询占 72.7%,产品占 27.3%。转型的目标不是「把咨询砍掉」,而是让产品 MRR 长到能替代咨询收入,同时不让现金断掉。
Tidecast 是买断转订阅的潜在样本,但它现在还没有订阅收入。它的 9,600 份销量、19.99 美元标价、138,171 美元净得,都是一次性收入;退款率 4%、Steam 分成 30%、Q4 占销量 38%。这些数字说明它的现金集中在上线期和假日季,改成订阅会把现金往后摊,先要熬过一段低收入期。
两条路径的算术不同,但判断框架相同:替代率、现金安全垫、组织能力。下面先用 Nord Studio 把三个阈值算清楚,再回到 Tidecast 的差异。
服务转产品:替代服务收入需要多少客户
先算目标。Nord Studio 的咨询月收入是:
咨询月收入 = 110 小时 × $85/小时 = $9,350
产品单价是 39 美元/月。如果只要求产品 MRR 替代咨询月收入,所需客户数是:
所需客户数 = ceil($9,350 ÷ $39) = ceil(239.74) = 240 家
当前已有 90 家产品客户,所以还差 150 家。接下来要算「多久能长到 240 家」。这里需要一个月度增长假设:产品月流失 3.0%,每月毛新增 15 家(这是教学参数,不是案例事实表数字)。客户数的递推是:
C(t) = C(t−1) − round(C(t−1) × 3.0%) + 15
MRR(t) = C(t) × $39
代入后,关键月份的客户数与 MRR 如下:
| 月份 | 客户数 | 产品 MRR | 与咨询月收入的距离 |
|---|---|---|---|
| 0 | 90 | $3,510 | −$5,840 |
| 3 | 126 | $4,914 | −$4,436 |
| 6 | 159 | $6,201 | −$3,149 |
| 9 | 189 | $7,371 | −$1,979 |
| 12 | 216 | $8,424 | −$926 |
| 15 | 241 | $9,399 | +$49 |
第 15 个月,产品 MRR 达到 9,399 美元,首次超过咨询月收入 9,350 美元。注意这里的目标只是「替代咨询收入」,不是「替代总收入」。Nord Studio 当前总收入是 9,350 + 3,510 = 12,860 美元;如果咨询归零、产品只长到 9,399 美元,总收入会比现在少 3,461 美元。
如果目标是保持总收入不变,产品 MRR 需要达到 12,860 美元,所需客户数是 ceil(12,860 ÷ 39) = 330 家。按同样的流失和新增假设,达到 330 家需要 29 个月,第 29 个月是 331 家、MRR 12,909 美元。
两个目标对应两个时间尺度:替代服务线 15 个月,替代总收入 29 个月。转型计划必须先说清楚是哪一个,否则「转型完成」会变成一个没有定义的词。
转型期现金缺口
产品增长的同时,咨询不会一直停在 110 小时。假设咨询在 12 个月内线性退出:
咨询月收入(t) = $9,350 × max(0, 12 − t) ÷ 12
第 1 个月还有 11/12 的咨询收入,第 12 个月归零。把咨询收入和产品 MRR 相加,再与当前总收入 12,860 美元比较:
月缺口(t) = max(0, $12,860 − 咨询收入(t) − 产品 MRR(t))
累计缺口 = Σ 月缺口(t)
关键月份的缺口如下。金额单位为美元,保留两位小数;合计与缺口由精确值相减。
| 月份 | 咨询收入 | 产品 MRR | 合计收入 | 月缺口 |
|---|---|---|---|---|
| 1 | $8,570.83 | $3,978.00 | $12,548.83 | $311.17 |
| 3 | $7,012.50 | $4,914.00 | $11,926.50 | $933.50 |
| 6 | $4,675.00 | $6,201.00 | $10,876.00 | $1,984.00 |
| 9 | $2,337.50 | $7,371.00 | $9,708.50 | $3,151.50 |
| 12 | $0.00 | $8,424.00 | $8,424.00 | $4,436.00 |
| 15 | $0.00 | $9,399.00 | $9,399.00 | $3,461.00 |
缺口先扩大、后收窄:咨询退出速度快于产品补位速度,第 12 个月咨询归零、缺口达到 4,436 美元的峰值;第 13 个月起产品继续增长,缺口才开始下降。15 个月的累计缺口是 38,398 美元,平均每月 2,560 美元。
Nord Studio 的可支配现金是 25,740 美元(税后)。按平均缺口算:
缺口 runway = floor($25,740 ÷ $2,560) = 10 个月
资金缺口 = $38,398 − $25,740 = $12,658
10 个月的 runway 小于 15 个月的替代周期,转型会在第 10 个月左右耗尽现金,距离完成还差约 12,658 美元。如果按「完全没有收入」的绝对口径算,月度现金支出按两位合伙人工资 96,000 美元/年加直接成本 24,000 美元/年估算,合计 120,000 美元/年,即 10,000 美元/月;25,740 ÷ 10,000 = 2.57,只能撑 2 个完整月。绝对 runway 更短,但它假设收入瞬间归零,适合做压力测试,不适合描述正常转型。
两种 runway 的差距说明:转型的风险不是「产品做不出来」,而是「产品做出来的速度慢于现金消耗的速度」。要补上 12,658 美元的缺口,要么加快产品增长,要么延后咨询退出,要么先留出更多现金。
三个阈值
把上面的算术收成三个阈值。它们不是精确的开关,而是转型前必须回答的三个问题。
替代率
替代率 = 产品 MRR ÷ 服务月收入。Nord Studio 在第 15 个月达到 9,399 ÷ 9,350 = 100.5%。如果目标是保持总收入,替代率要按总收入算:12,860 ÷ 9,350 = 137.5%,也就是产品 MRR 要达到服务收入的 1.375 倍。
替代率低于 100% 时,转型还在「补缺口」阶段;达到 100% 只是替代了服务线,达到 137.5% 才替代了总收入。把这两个数写进计划,比写「产品收入超过服务收入」更不容易自欺。
现金安全垫
累计缺口 38,398 美元是转型的现金成本。安全垫至少覆盖这个数;更保守的做法是再加 20% 缓冲,即 38,398 × 1.2 = 46,078 美元;或者用「缺口 + 3 个月固定支出」= 38,398 + 30,000 = 68,398 美元。
Nord Studio 现在只有 25,740 美元,低于任何一个口径。转型前要补的现金是 12,658 美元(覆盖缺口)、20,338 美元(覆盖 20% 缓冲)或 42,658 美元(覆盖缺口加 3 个月固定成本)。这三个数字都可以从事实表复算,不是拍脑袋的「多留点钱」。
组织能力
前两个阈值是财务的,第三个是人的。Nord Studio 有两位合伙人,咨询和产品都要人:谁负责产品增长,谁继续交付咨询?如果两个人同时转产品,咨询收入会掉得比模型更快;如果一个人守咨询、一个人做产品,产品增长可能慢于每月 15 家。
招聘第三人是另一条路,但成本是 55,000 美元/年;案例事实表给出的覆盖条件是新增收入至少 75,000 美元,才能覆盖工资与税。把这条约束放回模型:如果第三人在产品 MRR 达到 9,350 美元之前加入,现金缺口会再增加 55,000 美元/年,runway 会更短。组织能力不是「有没有人」,而是「新增的人能不能在现金耗尽前产生足够收入」。
服务产品化的通用条件,包括标准化、可复用资产和交付边界,见 服务产品化的条件 ;服务线的退出节奏见 服务收尾 。本节只算财务后果,不重复操作步骤。
买断转订阅的不同之处
Tidecast 的转型方向不是服务转产品,而是买断转订阅。它的现金结构和 Nord Studio 相反:买断是上线期集中收款,订阅是把同样的收入摊到未来。转型的第一个后果是首年现金下降,因为原本一次性收到的 19.99 美元,变成了每月几美元的订阅。
Tidecast 的数字里,有几个约束不能忽略:
- 9,600 份销量、19.99 美元标价,净得 138,171 美元;退款率 4%、Steam 分成 30%、官网支付费 5%。
- Q4 占全年销量 38%,12 月 2,400 份、1 月 480 份;订阅化会抹平季节峰值,也会削掉上线期的大额现金。
- 买断玩家中有多少愿意转订阅,案例事实表没有给出;这个数不能拍脑袋补,必须用真实转化率验证。
因此,Tidecast 的转型不应该从「把买断改成订阅」开始,而应该从「哪些内容适合持续付费」开始:续作、资料片、关卡包、授权,都是比整体转订阅更贴近现有资产的路径。案例事实表给的 IP 授权报价是保底 60,000 美元/年 + 8% 净收入,3 年最低保证 180,000 美元;这条路径不要求把 9,600 个买断玩家转成订阅用户,而是把 IP 授权给能持续运营的第三方。
如果一定要转订阅,至少要回答三个问题:现有买断用户中愿意续费的比例是多少?订阅内容的生产成本是多少?首年现金下降后,现金储备能撑多久?这三个问题没有案例数字支撑时,任何模型都只是假设。订阅生命周期的操作细节见 订阅生命周期 。
把转型写成代码
下面的 transition.ts 把三个量固化成函数:replacementMrr 算替代服务收入所需客户数与月数,transitionGap 算转型期累计现金缺口,runwayMonths 算现金能撑几个完整月。客户数按整数递推,金额用整数美分。
// file: src/fin/transition.ts
// tier: L1
// verify: node --test test/fin/transition.test.ts
import { type Cents, sub } from "../core/money.ts";
import { type Bps } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";
export type ReplacementInput = {
targetMrrCents: Cents;
productPriceCents: Cents;
currentCustomers: number;
monthlyChurnBps: Bps;
grossAddsPerMonth: number;
maxMonths?: number;
};
export type Replacement = {
targetMrrCents: Cents;
customersNeeded: number;
monthsToReplace: number;
finalCustomers: number;
finalMrrCents: Cents;
series: Cents[];
};
export type GapPoint = {
month: number;
serviceCents: Cents;
productCents: Cents;
totalCents: Cents;
gapCents: Cents;
};
export type GapResult = {
points: GapPoint[];
totalGapCents: Cents;
averageGapCents: Cents;
months: number;
};
function validate(input: ReplacementInput): Result<number> {
if (!Number.isInteger(input.targetMrrCents) || input.targetMrrCents <= 0) {
return fail("target MRR must be a positive integer number of cents");
}
if (!Number.isInteger(input.productPriceCents) || input.productPriceCents <= 0) {
return fail("product price must be a positive integer number of cents");
}
if (!Number.isInteger(input.currentCustomers) || input.currentCustomers < 0) {
return fail("current customers must be a non-negative integer");
}
if (
!Number.isInteger(input.monthlyChurnBps) ||
input.monthlyChurnBps < 0 ||
input.monthlyChurnBps > 10000
) {
return fail("monthly churn must be an integer between 0 and 10000 bps");
}
if (
!Number.isInteger(input.grossAddsPerMonth) ||
input.grossAddsPerMonth < 0
) {
return fail("gross adds must be a non-negative integer");
}
const maxMonths = input.maxMonths ?? 120;
if (!Number.isInteger(maxMonths) || maxMonths <= 0) {
return fail("maxMonths must be a positive integer");
}
return ok(maxMonths);
}
/**
* Customers needed so that product MRR reaches targetMrrCents, and the number
* of months to get there assuming a fixed monthly gross-add count and churn.
*/
export function replacementMrr(input: ReplacementInput): Result<Replacement> {
const checked = validate(input);
if (!checked.ok) {
return checked;
}
const customersNeeded = Math.ceil(
input.targetMrrCents / input.productPriceCents,
);
let customers = input.currentCustomers;
let mrr = customers * input.productPriceCents;
if (mrr >= input.targetMrrCents) {
return ok({
targetMrrCents: input.targetMrrCents,
customersNeeded,
monthsToReplace: 0,
finalCustomers: customers,
finalMrrCents: mrr,
series: [],
});
}
const series: Cents[] = [];
for (let month = 1; month <= checked.value; month++) {
const churned = Math.round(
(customers * input.monthlyChurnBps) / 10000,
);
customers = Math.max(0, customers - churned + input.grossAddsPerMonth);
mrr = customers * input.productPriceCents;
series.push(mrr);
if (mrr >= input.targetMrrCents) {
return ok({
targetMrrCents: input.targetMrrCents,
customersNeeded,
monthsToReplace: month,
finalCustomers: customers,
finalMrrCents: mrr,
series,
});
}
}
return fail(`target not reached within ${checked.value} months`);
}
/**
* Monthly cash gap while service revenue runs off linearly and product MRR
* follows productSeries. gap = max(0, baseline - service - product).
*/
export function transitionGap(
baselineMonthlyCents: Cents,
serviceMonthlyCents: Cents,
serviceRunoffMonths: number,
productSeries: Cents[],
): Result<GapResult> {
if (!Number.isInteger(baselineMonthlyCents) || baselineMonthlyCents <= 0) {
return fail("baseline revenue must be a positive integer number of cents");
}
if (!Number.isInteger(serviceMonthlyCents) || serviceMonthlyCents < 0) {
return fail("service revenue must be a non-negative integer number of cents");
}
if (
!Number.isInteger(serviceRunoffMonths) ||
serviceRunoffMonths <= 0
) {
return fail("service runoff must be a positive number of months");
}
if (productSeries.length === 0) {
return fail("product series must not be empty");
}
for (const mrr of productSeries) {
if (!Number.isInteger(mrr) || mrr < 0) {
return fail("product series values must be non-negative integers");
}
}
const points: GapPoint[] = productSeries.map((productCents, index) => {
const month = index + 1;
const remaining = Math.max(0, serviceRunoffMonths - month);
const serviceCents = Math.round(
(serviceMonthlyCents * remaining) / serviceRunoffMonths,
);
const totalCents = serviceCents + productCents;
return {
month,
serviceCents,
productCents,
totalCents,
gapCents: Math.max(0, sub(baselineMonthlyCents, totalCents)),
};
});
const totalGapCents = points.reduce((acc, point) => acc + point.gapCents, 0);
return ok({
points,
totalGapCents,
averageGapCents: Math.round(totalGapCents / points.length),
months: points.length,
});
}
/** Full months of cash at a constant monthly gap; Infinity when the gap is zero. */
export function runwayMonths(
cashCents: Cents,
monthlyGapCents: Cents,
): Result<number> {
if (!Number.isInteger(cashCents) || cashCents < 0) {
return fail("cash must be a non-negative integer number of cents");
}
if (!Number.isInteger(monthlyGapCents) || monthlyGapCents < 0) {
return fail("monthly gap must be a non-negative integer number of cents");
}
if (monthlyGapCents === 0) {
return ok(Number.POSITIVE_INFINITY);
}
return ok(Math.floor(cashCents / monthlyGapCents));
}
代码里的增长假设是显式参数:grossAddsPerMonth 和 monthlyChurnBps。这两个数不是案例事实,而是计划输入;换一组假设,月数和缺口都会变。把假设放在参数里,而不是写死在公式里,是为了让读者能一眼看到「结论依赖什么」。
transitionGap 的咨询退出也是显式参数 serviceRunoffMonths。案例里用 12 个月,是因为它和产品 15 个月的替代周期接近;如果咨询退出更快,缺口会更大,runway 会更短。代码不替读者选择退出速度,只负责把选择后的现金后果算出来。
runwayMonths 用平均缺口做分母,返回完整月数;当缺口为 0 时返回 Infinity,表示现金不会被缺口消耗。它不处理收入波动、税期和应收账期,这些会在第 8 章的压力测试里补齐,相关方法见 8.3 runway 与现金底线
。
小结
- 转型是一条交叉曲线:旧收入下降、新收入上升,交叉前靠现金补差额。
- Nord Studio 替代咨询月收入 9,350 美元需要 240 家产品客户;从 90 家、每月净增约 12 家出发,第 15 个月达到 241 家、MRR 9,399 美元。
- 如果要保持总收入 12,860 美元不变,需要 330 家客户,按同样假设要 29 个月。
- 咨询 12 个月线性退出、产品 15 个月替代,累计现金缺口 38,398 美元,平均每月 2,560 美元。
- 可支配现金 25,740 美元只能支撑约 10 个月的缺口,距离 15 个月转型还差 12,658 美元。
- 三个阈值:替代率 100%(替代服务线)或 137.5%(替代总收入)、现金安全垫覆盖累计缺口、组织能力能支撑产品增长。
阅读导航:上一节:1.2 收入模型组合:主收入与第二收入线 · 下一节:2.1 单位经济的四个数:ARPA、毛利、CAC、回本 。
常见错误与边界
- 把「替代服务收入」当成「替代总收入」。 Nord Studio 产品 MRR 达到 9,350 美元只是替代咨询线;总收入仍是 12,860 美元,产品要到 12,860 美元才持平。
- 忽略咨询退出的速度。 咨询 12 个月线性退出时累计缺口 38,398 美元;退出越快,缺口越大,runway 越短。
- 用绝对 runway 描述正常转型。 25,740 美元按 10,000 美元/月固定成本只能撑 2 个月,但转型期仍有收入;绝对 runway 是压力测试,不是计划值。
- 把增长假设当事实。 每月毛新增 15 家是教学参数,不是案例事实;换成 10 家,替代月数会显著拉长。
- 忘记流失。 产品月流失 3.0%,每月新增 15 家中有约 3 家只是在补流失;净增约 12 家,不是 15 家。
- 现金安全垫只算一个月。 累计缺口 38,398 美元是 15 个月的总量;只留一个月缺口会在第 2 个月就断粮。
- 在产品 MRR 不足时招第三人。 第三人成本 55,000 美元/年,需要新增收入至少 75,000 美元才能覆盖;提前招聘会扩大缺口。
- 把买断转订阅当成开关。 Tidecast 的 9,600 份销量、Q4 38% 的季节性会在订阅化后消失;首年现金下降,必须有储备。
- 用未经验证的转化率建模。 买断玩家转订阅的比例不在案例事实表里;没有真实数据时,任何「转化率 10%」都是假设,不是预测。
- 把转型完成定义成「产品上线」。 上线只是起点;替代率、现金缺口和组织能力都过关,才叫完成转型。
转型不是一次性的战略选择,而是一段有明确现金成本的过渡期。第 2 章会把单位经济拆成四个数,帮助判断产品线是否值得继续投入;第 8 章会把 runway 和压力测试做成月度模型。本节的任务,是把「转型要花多少钱、要多久」从感觉变成可复算的数字。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。