《独立开发者的商业指南·收入与财务工程》11.1 三种估值口径:ARR 倍数、SDE 倍数、DCF

三个案例给出三个锚:LedgerLite 3.2× ARR = $768,768、Tidecast 1.8× 收入 = $248,708、Nord 4.5× SDE = $154,440。本节把 ARR、收入、SDE 三种倍数放回各自的分母,说明它们分别适用于什么主体、为什么不能混用,并用 DCF 做一次交叉校验。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

三个案例的估值锚经常被放在同一张表里:LedgerLite 是 3.2× ARR = $768,768,Tidecast 是 1.8× 收入 = $248,708,Nord Studio 是 4.5× SDE = $154,440。三个数字并列时,最自然的读法是「LedgerLite 最贵、Tidecast 最便宜、Nord 居中」。这个读法在数学上就不成立,因为三个倍数的分母不是同一种东西。

倍数是一个比率,不是一个价格标签。3.2× 只有在说明「3.2 倍的什么」之后才有意义;离开分母,3.2 和 4.5 只是两个小数。这一节先把三种分母拆开,再讨论各自适用于什么主体,最后用 DCF 做一次反向校验。4.3 已经演示过把三个锚换算成收入倍数之后的排序变化,本节不重复那套换算,而是回答更前面的问题:什么时候应该用哪一种分母。

案例披露锚分母分母金额估值金额
LedgerLite3.2× ARR年度经常性收入$240,240$768,768
Tidecast1.8× 收入净收入$138,171$248,708
Nord Studio4.5× SDE卖方自由现金流$34,320$154,440
Nord Studio(折价)3.0× SDE卖方自由现金流$34,320$102,960

分母决定倍数,倍数不决定价格

把同一门生意放到三种分母上,倍数的绝对值会立刻失去可比性。先看 LedgerLite:它只有订阅收入,所以 ARR 和收入恰好都是 $240,240。用同一个 3.2× 去乘,ARR 和收入都得到 $768,768;但用 3.2× 去乘 SDE $130,240,只能得到 $416,768。三个结果都来自同一个 3.2×,价格却相差 $352,000。

分母LedgerLite 金额3.2× 的结果这个结果回答的问题
ARR$240,240$768,768买方为续费基数付多少钱
收入$240,240$768,768买方为当前收入流付多少钱
SDE$130,240$416,768买方为可支配现金付多少钱

Nord Studio 更能说明问题。它的产品订阅年化收入是 90 家 × $39 × 12 = $42,120,总收入是 $154,320,SDE 是 $34,320。同一个 4.5× 乘下去,三种分母给出三个完全不同的世界:

分母Nord 金额4.5× 的结果读法
产品 ARR$42,120$189,540只为可续费的产品线定价
总收入$154,320$694,440把 4.5× 用在以服务为主的收入上
SDE$34,320$154,440案例披露的锚

$694,440 不是 Nord 的真实报价,它只是「4.5× 总收入」这个算式的结果。Nord 的服务收入占 72.7%,续约率 70%,把这部分收入按 ARR 式的 4.5× 定价,等于假设咨询服务像订阅一样可以自动续费。这个假设不成立,所以这个数字只能用来演示误用,不能拿来谈判。

倍数的定义只有一个:

估值 = 分母 × 倍数
倍数 = 估值 ÷ 分母

这个式子看起来简单,但它要求分母和倍数必须来自同一套口径。3.2× ARR 里的 3.2 是「每一美元年度经常性收入值 3.2 美元」;3.2× SDE 里的 3.2 是「每一美元卖方自由现金流值 3.2 美元」。两个 3.2 只是数字碰巧相同,经济含义完全不同。混用的本质,是把「每单位续费基数」的价格套到「每单位现金」上,或者反过来。

把三种口径写成代码

下面的 multiple.ts 把三种分母固化成三个入口:quoteArr 只接受完全经常性的基数,quoteRevenue 允许混合收入但返回持续性警告,quoteSde 强制要求说明所有者薪酬是否已扣除。compareQuotes 在不同分母、不同薪酬口径或不同经常性占比之间直接拒绝比较,toRevenueMultiple 则把任意报价换算成收入倍数,供跨口径核对。

// file: src/value/multiple.ts
// tier: L1
// verify: node --test test/value/multiple.test.ts

import { type Cents } from "../core/money.ts";
import { type Bps, applyRate, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";

export type MultipleBase = "arr" | "revenue" | "sde";

export type MultipleQuote = {
  base: MultipleBase;
  multipleBps: Bps;
  baseCents: Cents;
  valueCents: Cents;
  recurringShareBps: Bps;
  ownerSalaryDeducted: boolean | null;
  warnings: string[];
};

function checkAmount(amount: Cents, label: string): string | null {
  if (!Number.isInteger(amount) || amount <= 0) {
    return `${label} must be a positive integer amount in cents`;
  }
  return null;
}

function checkMultiple(multipleBps: Bps): string | null {
  if (!Number.isInteger(multipleBps) || multipleBps <= 0) {
    return "multiple must be a positive integer in basis points";
  }
  return null;
}

function checkShare(shareBps: Bps): string | null {
  if (!Number.isInteger(shareBps) || shareBps < 0 || shareBps > 10000) {
    return "recurring share must be between 0 and 10000 bps";
  }
  return null;
}

/**
 * ARR is by definition fully recurring. Passing total revenue as ARR is the
 * most common valuation mistake, so a partial recurring share is rejected.
 */
export function quoteArr(
  arrCents: Cents,
  multipleBps: Bps,
  recurringShareBps: Bps = 10000,
): Result<MultipleQuote> {
  const amountError = checkAmount(arrCents, "arr");
  if (amountError) {
    return fail(amountError);
  }
  const multipleError = checkMultiple(multipleBps);
  if (multipleError) {
    return fail(multipleError);
  }
  const shareError = checkShare(recurringShareBps);
  if (shareError) {
    return fail(shareError);
  }
  if (recurringShareBps !== 10000) {
    return fail("arr base must be fully recurring; pass only the recurring amount");
  }
  return ok({
    base: "arr",
    multipleBps,
    baseCents: arrCents,
    valueCents: applyRate(arrCents, multipleBps),
    recurringShareBps,
    ownerSalaryDeducted: null,
    warnings: [],
  });
}

/**
 * Revenue may mix recurring and one-time income. The quote is valid, but the
 * warnings keep the caller from describing it as recurring or as ARR.
 */
export function quoteRevenue(
  revenueCents: Cents,
  multipleBps: Bps,
  recurringShareBps: Bps = 0,
): Result<MultipleQuote> {
  const amountError = checkAmount(revenueCents, "revenue");
  if (amountError) {
    return fail(amountError);
  }
  const multipleError = checkMultiple(multipleBps);
  if (multipleError) {
    return fail(multipleError);
  }
  const shareError = checkShare(recurringShareBps);
  if (shareError) {
    return fail(shareError);
  }
  const warnings: string[] = [];
  if (recurringShareBps < 10000) {
    warnings.push("revenue base mixes recurring and one-time income");
  }
  if (recurringShareBps === 0) {
    warnings.push("no recurring share: do not label this multiple as ARR");
  }
  return ok({
    base: "revenue",
    multipleBps,
    baseCents: revenueCents,
    valueCents: applyRate(revenueCents, multipleBps),
    recurringShareBps,
    ownerSalaryDeducted: null,
    warnings,
  });
}

/**
 * SDE only means the same thing when owner compensation has been handled the
 * same way. The flag is mandatory; a false value is allowed but warned.
 */
export function quoteSde(
  sdeCents: Cents,
  multipleBps: Bps,
  ownerSalaryDeducted: boolean,
): Result<MultipleQuote> {
  const amountError = checkAmount(sdeCents, "sde");
  if (amountError) {
    return fail(amountError);
  }
  const multipleError = checkMultiple(multipleBps);
  if (multipleError) {
    return fail(multipleError);
  }
  if (typeof ownerSalaryDeducted !== "boolean") {
    return fail("sde quote must state whether owner salary was deducted");
  }
  const warnings: string[] = [];
  if (!ownerSalaryDeducted) {
    warnings.push(
      "owner salary not deducted; this sde is not comparable to a post-salary sde",
    );
  }
  return ok({
    base: "sde",
    multipleBps,
    baseCents: sdeCents,
    valueCents: applyRate(sdeCents, multipleBps),
    recurringShareBps: 0,
    ownerSalaryDeducted,
    warnings,
  });
}

/** Recover the multiple from a price and a base, for checking a quoted anchor. */
export function impliedMultiple(
  valueCents: Cents,
  baseCents: Cents,
): Result<Bps> {
  const valueError = checkAmount(valueCents, "valuation");
  if (valueError) {
    return fail(valueError);
  }
  const baseError = checkAmount(baseCents, "base");
  if (baseError) {
    return fail(baseError);
  }
  return ok(divide(valueCents, baseCents));
}

/**
 * Comparing two multiples is only meaningful when their denominators and
 * adjustments match. Different bases are a hard failure, not a warning.
 */
export function compareQuotes(
  first: MultipleQuote,
  second: MultipleQuote,
): Result<{ differenceBps: Bps }> {
  if (first.base !== second.base) {
    return fail(
      `cannot compare ${first.base} multiple with ${second.base} multiple`,
    );
  }
  if (
    first.base === "sde" &&
    first.ownerSalaryDeducted !== second.ownerSalaryDeducted
  ) {
    return fail("sde quotes differ on owner salary treatment");
  }
  if (
    first.base === "revenue" &&
    first.recurringShareBps !== second.recurringShareBps
  ) {
    return fail("revenue quotes differ on recurring share; normalize the base first");
  }
  return ok({ differenceBps: first.multipleBps - second.multipleBps });
}

/** Convert any quote into a revenue multiple so the denominators match. */
export function toRevenueMultiple(
  quote: MultipleQuote,
  revenueCents: Cents,
): Result<Bps> {
  const revenueError = checkAmount(revenueCents, "revenue");
  if (revenueError) {
    return fail(revenueError);
  }
  return ok(divide(quote.valueCents, revenueCents));
}

代码里最值得注意的不是乘法,而是三个校验。第一,quoteArr 拒绝把带有非经常性收入的总额当作 ARR;这条校验拦住的正是「用总收入乘 ARR 倍数」的常见错误。第二,quoteRevenue 不拒绝混合收入,但会返回警告,因为收入倍数本身可以用于买断制,只是不能暗示持续性。第三,quoteSde 把「是否扣所有者薪酬」变成必填参数;没有这个参数,两个 SDE 倍数可能一个含老板工资、一个不含,数字上能比,经济上不能比。

ARR 倍数:只对经常性收入有意义

ARR 的定义是年度经常性收入:

MRR = 付费客户数 × 每户月收入
ARR = MRR × 12

LedgerLite 有 260 家付费客户,ARPA 是 $77/月,所以 MRR = 260 × 77 = $20,020,ARR = 20,020 × 12 = $240,240。3.2× ARR 就是 3.2 × 240,240 = $768,768。

ARR 倍数之所以存在,是因为买方买的不是「今年收到多少钱」,而是「明年还能收到多少钱」。LedgerLite 的月流失率是 1.5%,年流失约 16.6%,GRR 83%,NRR 102%。这组数字告诉买方:现有客户群每年会漏掉一部分,但扩张收入略高于流失,续费基数大致能稳住。3.2× 买的是这个续费基数,以及维护这个基数所需的代码、合同和客户关系。

ARR 倍数的适用边界很窄:主体必须有经常性收入。Tidecast 是买断制游戏,没有订阅,ARR 为零,用 ARR 倍数给它定价没有分母。Nord 只有产品订阅那一部分可以叫 ARR:90 家 × $39 × 12 = $42,120。如果要用 ARR 倍数,只能乘这 $42,120,不能乘 $154,320。乘出来的 $189,540 也只覆盖产品线,不覆盖占 72.7% 的咨询服务。

案例可计入 ARR 的部分金额能否用 ARR 倍数
LedgerLite全部订阅收入$240,240可以
Tidecast无经常性收入$0不可以
Nord Studio仅产品订阅$42,120只能用于产品线

还有一个容易忽略的边界:ARR 不是现金。LedgerLite 有 30% 年付客户,年末递延收入 $22,000,占 ARR 的 9.2%。买方在尽调时会把递延收入视为尚未履行的服务义务,可能从价格中扣除;反过来,预收现金又让卖方在交割时更有底气。ARR 倍数定的是「续费基数的价格」,最终到手金额还要经过现金、负债和营运资金调整,这条桥在 4.1 节已经拆过,这里不展开。

收入倍数:买断制可用,但不能暗示持续性

Tidecast 的 1.8× 用的是净收入,不是标价收入。标价收入是 $19.99 × 9,600 份 = $191,904;扣掉 4% 退款、Steam 30% 分成和官网 5% 支付费之后,净得是 $138,171。1.8 × 138,171 = $248,707.8,四舍五入为 $248,708。

项金额说明
标价收入$191,90419.99 × 9,600
退款−$7,6764%
平台分成−$44,215Steam 30%
支付费−$1,842官网 5%
净收入$138,171净得率 72.0%

收入倍数对买断制是可用的,因为买方确实在买一条当前收入流。但它和 ARR 倍数回答的问题不同:ARR 倍数问「续费基数值多少」,收入倍数问「这条收入流值多少」。Tidecast 没有续费基数,买家付 1.8× 之后,必须自己承担下一部作品的风险。续作预算 $120,000、预计首年销量 7,000 份,这些是买方的追加投入,不在 1.8× 里。

收入倍数最大的风险是被用来暗示持续性。把 1.8× 写进一页纸,很容易让读者以为「明年还会有 $138,171」。实际上,Tidecast 的销量高度集中:12 月 2,400 份,是月均 800 份的 3 倍;Q4 占全年销量 38%。这是一条有季节性的、以单部作品为载体的收入流,不是一条会自动续期的收入流。倍数本身不包含「下一部作品」的承诺。

Nord 的例子说明误用收入倍数会把价格推到什么程度。Nord 的总收入是 $154,320,其中咨询 $112,200、产品订阅 $42,120。用 4.5× 乘总收入得到 $694,440,相当于为一项续约率 70%、依赖两位合伙人的服务业务支付近 4.5 倍年收入。这个价格没有任何案例事实支持;它只是提醒:收入倍数可以用,但必须先问这条收入明年还在不在。

分母持续性来自哪里买方在为什么付费典型误用
ARR订阅续费与扩张续费基数把一次性收入算进 ARR
收入当前作品或合同当前收入流把收入倍数说成可持续
SDE现有经营现金流可支配现金不说明所有者薪酬

SDE 倍数:先问有没有扣所有者薪酬

SDE 是卖方自由现金流,常用定义是:

SDE = 毛利 − 运营费用

它衡量的是「买方接手后,在支付管理人员工资之前能拿到的现金」。三个案例的 SDE 算法一致,但所有者薪酬的处理并不一致:

案例毛利运营费用或工资SDE所有者薪酬处理
LedgerLite$202,240运营费用 $72,000$130,240创始人薪酬 $60,000 单独列示
Tidecast$138,171(净得)营销、服务器、工具 $17,000$121,171一人经营,未单列工资
Nord Studio$130,320两人工资 $96,000$34,320两位合伙人工资已扣除

LedgerLite 的 SDE 是 202,240 − 72,000 = $130,240,创始人薪酬 $60,000 在 SDE 之后才扣,税前利润是 $70,240。Nord 的 SDE 是 130,320 − 96,000 = $34,320,两位合伙人的工资已经包含在 $96,000 里。Tidecast 没有单独列工资,SDE 是 138,171 − 12,000 − 3,000 − 2,000 = $121,171。

这三个 SDE 不能直接放在一起比,因为「所有者薪酬是否已扣」不同。LedgerLite 的 SDE 是「扣掉创始人工资之前」的现金;Nord 的 SDE 是「扣掉两位合伙人工资之后」的现金;Tidecast 的 SDE 没有工资行,买方接手后如果雇人运营,还要再扣一笔市场工资。倍数相同,含义不同。

案例给出的锚是 4.5× SDE = $154,440,折价后 3.0× SDE = $102,960。这里的 4.5× 和 3.0× 都建立在 Nord 已扣两人工资的 SDE 上。如果换成一个未扣所有者薪酬的 SDE,同样的 4.5× 会高估买方可支配现金,因为买方还要支付替代管理者的工资。SDE 倍数适合利润薄、所有者薪酬影响大的小生意,但前提是双方对「扣没扣」有一致的定义。

DCF 只做校验,不做报价

DCF 的公式不复杂:

估值 = Σ 未来自由现金流 ÷ (1 + 折现率)^t + 终值 ÷ (1 + 折现率)^n

复杂的是三个输入:未来现金流预测、折现率、终值假设。对一人公司来说,这三项都不稳定。预测期越长,假设越多;折现率要反映关键人依赖、客户集中和平台风险;终值往往是总估值里最大的一块,而终值又最依赖「几年后还能不能继续经营」这种很难验证的判断。

所以本章不把 DCF 当作报价工具,而把它当作校验工具。看到 3.2× ARR,可以反推:买方要拿到这个回报,需要 LedgerLite 的续费基数保持多稳?案例给的月流失 1.5%、GRR 83%、NRR 102% 能不能支撑?看到 1.8× 收入,可以反推:买方是否已经把下一部作品的成功概率算进去?看到 4.5× SDE,可以反推:Nord 的服务收入能不能脱离两位合伙人独立存在?如果答案是否定的,DCF 会把倍数里的乐观假设暴露出来,而不会自动给出一个更高的价格。

DCF 和倍数不是两套互相替代的方法,而是两种不同的提问方式。倍数问「同类资产最近按什么价格成交」,DCF 问「这门生意未来能产生多少现金」。前者依赖可比交易,后者依赖经营假设。独立开发者通常没有足够多的可比交易,所以倍数只能作为入口;真正决定价格的,还是 11.2 要拆的五个驱动,以及 11.3 要做的区间。

小结

  • 倍数 = 估值 ÷ 分母。离开分母,3.2、1.8、4.5 只是小数,不能直接比大小。
  • 同一门生意换分母,价格会变:LedgerLite 的 3.2× 在 ARR 和收入上是 $768,768,在 SDE 上是 $416,768;Nord 的 4.5× 在产品 ARR 上是 $189,540,在总收入上是 $694,440,在 SDE 上是 $154,440。
  • ARR 倍数只对经常性收入有意义;Tidecast 没有 ARR,Nord 只有产品线的 $42,120 可以计入。
  • 收入倍数对买断制可用,但它买的是当前收入流,不承诺明年还有;Tidecast 的 1.8× 净收入 = $248,708。
  • SDE 倍数必须先说明所有者薪酬是否扣除;Nord 的 4.5× = $154,440、折价 3.0× = $102,960 都建立在已扣两人工资的 SDE 上。
  • DCF 在本书里只做校验:用未来现金流反推倍数隐含的留存、增长和可转移性假设。

阅读导航:上一节:10.3 复盘会与决策记录 · 下一节:11.2 增长、毛利、集中度对倍数的影响 。

常见错误与边界

  1. 把 3.2、1.8、4.5 直接比大小。 三个倍数的分母分别是 ARR、净收入和 SDE;不换分母就比大小,等于拿长度比重量。
  2. 把总收入当 ARR。 Nord 的总收入是 $154,320,可计入 ARR 的只有产品订阅 $42,120;用总收入乘 4.5× 会得到 $694,440 这种没有经济含义的数字。
  3. 把 ARR 当现金。 LedgerLite 年末递延收入 $22,000,占 ARR 9.2%;ARR 是续费基数,不是已经到手的现金。
  4. 把净收入当标价收入。 Tidecast 的 1.8× 用的是扣退款、平台分成和支付费后的 $138,171,不是标价 $191,904。
  5. 用收入倍数暗示持续性。 收入倍数可以给买断制定价,但 1.8× 不包含下一部作品的承诺;Tidecast 的 Q4 占全年销量 38%,季节性本身就是风险。
  6. 不说明 SDE 是否扣所有者薪酬。 LedgerLite 的 SDE 在创始人工资之前,Nord 的 SDE 在两人工资之后;两个 4.5× 不是同一种 4.5×。
  7. 把 SDE 当净利润。 SDE 还没扣买方接手后的管理人员工资和所得税;4.5× SDE 不等于 4.5× 净利润。
  8. 只看倍数不看绝对值。 4.5× 一个 $34,320 的 SDE 只有 $154,440;1.8× 一个 $138,171 的净收入是 $248,708。
  9. 把 DCF 当精确答案。 一人公司的终值假设占比很高,折现率稍变,结果就会大幅变化;DCF 适合校验,不适合单独报价。
  10. 忽略交易结构。 资产收购、股权收购、earn-out、托管和营运资金调整都会改变最终到手金额;本节只比较「估值 ÷ 分母」这一层,结构条款见 11.3 与附录 B 第 7 节 。

11.1 只回答「用哪个分母」。11.2 继续往下拆:同一个分母上,为什么有的生意能拿到更高倍数,有的只能拿到折价。五个驱动会给出可观测的答案,11.3 再把它们变成区间。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查