《独立开发者的商业指南·资本与退出》5.1 ARR/SDE/EBITDA 倍数

三种倍数不是三个价格,而是三个分母。本节用 LedgerLite 的 3.2× ARR = $768,768、Tidecast 的 1.8× 收入 = $248,708、Nord 的 4.5× SDE = $154,440,拆开 ARR、SDE、EBITDA 的适用边界,并给出倍数选择决策表。

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

第 4 章讲买方怎么算,这一节讲买方按哪个分母算。卷一 11.1 三种估值口径 已经把 ARR、收入、SDE 三种口径的定义拆过;本节不重复定义,而是把它们放回交易现场,回答三个更具体的问题:这门生意该用哪个基数、同一个倍数换基数会差多少钱、什么情况下 EBITDA 才真的可用。

三个案例的锚经常被放在同一张表里:LedgerLite 是 3.2× ARR = $768,768,Tidecast 是 1.8× 收入 = $248,708,Nord 是 4.5× SDE = $154,440,折价后 3.0× SDE = $102,960。把它们并排看,最自然的读法是「LedgerLite 最贵、Tidecast 最便宜、Nord 居中」。这个读法在数学上不成立,因为三个倍数的分母不是同一种东西。

三种倍数,三种买家问题

倍数是一个比率,不是一个价格标签。3.2× 只有在说明「3.2 倍的什么」之后才有意义;离开分母,3.2、1.8、4.5 只是三个小数。三种常用倍数回答的是三个不同的问题:

倍数分母买家实际在问适用主体典型误用
ARR年度经常性收入续费基数值多少订阅、续费可预测把总收入当 ARR
收入净收入当前收入流值多少买断制、项目制暗示明年还有同样的收入
SDE卖方自由现金流接手后能拿走多少现金一人或小团队盈利资产不说明所有者薪酬
EBITDA息税折旧摊销前利润规范化经营利润值多少有规模、职业经理人经营在一人公司上直接套

ARR 和收入是顶线,SDE 和 EBITDA 是利润线。顶线倍数和利润线倍数不能直接比大小,因为一个问「收入值多少钱」,另一个问「利润值多少钱」。同一个 3.2× 乘在 ARR 上是 $768,768,乘在 SDE 上只有 $416,768;乘在 EBITDA 上只有 $234,368。数字都来自同一个 3.2×,经济含义却完全不同。

ARR 只对经常性收入有意义。LedgerLite 有 260 家客户,ARPA $77/月,MRR $20,020,ARR $240,240,全部是订阅收入,所以 ARR 和收入恰好相等。Tidecast 是买断制,ARR 为零;Nord 只有产品订阅 90 家 × $39 × 12 = $42,120 可以计入 ARR,咨询服务 $112,200 不能计入。

EBITDA 的适用边界最容易被忽略。它不是「比 SDE 更高级」的口径,而是「所有者薪酬已经不再重要」的口径。当一门生意由职业经理人经营、所有者不参与日常、折旧摊销和利息有规模时,EBITDA 才能把不同资本结构的公司放在同一把尺子上。对一人公司,所有者薪酬往往是最重要的成本项;直接用 EBITDA,等于把老板的工资当成不存在。

同一门生意,换基数差多少

先看 LedgerLite。它的 ARR 和收入都是 $240,240,SDE 是 $130,240。EBITDA 需要一步一步推:SDE $130,240 − 创始人薪酬 $60,000 = 税前利润 $70,240;再加回折旧 $3,000(教学假设:固定资产 $3,000 在一年内折旧完毕,且没有利息费用),得到 EBITDA $73,240。

基数金额3.2× 的结果与 ARR 结果的差
ARR$240,240$768,768$0
收入$240,240$768,768$0
SDE$130,240$416,768−$352,000
EBITDA$73,240$234,368−$534,400

同一个 3.2×,最高和最低相差 $534,400。这不是市场对 LedgerLite 的看法变了,而是分母变了。3.2× 只说明「每单位基数付 3.2 美元」,不说明基数是哪一条线。案例没有给出 EBITDA 倍数锚,所以这里的 3.2× EBITDA 只用于演示基数效应,不是报价。

Tidecast 的案例锚是 1.8× 净收入。净收入 $138,171 已经扣掉退款、Steam 30% 分成和官网 5% 支付费;SDE $121,171 又扣掉营销、服务器和工具 $17,000。

基数金额1.8× 的结果与收入结果的差
收入$138,171$248,708$0
SDE$121,171$218,108−$30,600

差 $30,600,约为收入结果的 12.3%。案例选收入而不是 SDE,是因为买断制游戏的买方通常按当前作品的收入流定价;SDE 会把卖方自己的营销和运营选择带进价格,而这些选择买方未必会重复。1.8× 收入买的是这一部作品,不包含下一部作品的承诺。

Nord 的案例锚是 4.5× SDE = $154,440,折价锚是 3.0× SDE = $102,960。它的产品订阅年化收入是 90 × $39 × 12 = $42,120,总收入 $154,320,SDE $34,320。

基数金额4.5× 的结果与 SDE 结果的差
产品 ARR$42,120$189,540+$35,100
SDE$34,320$154,440$0
总收入$154,320$694,440+$540,000

$694,440 不是 Nord 的报价,而是「把 4.5× 套到以服务为主的收入上」这个错误的产物。服务收入占 72.7%、咨询续约率 70%,它不像订阅那样自动续期;用 ARR 式的倍数乘它,等于假设咨询服务可以像订阅一样续费。案例的折价锚 3.0× SDE = $102,960 才是买方在服务收入难以转移时愿意付的价格。

倍数选择决策表

倍数选择可以写成一个决策顺序。先问收入结构,再问所有者角色,最后才问倍数高低。顺序很重要:先选倍数再找分母,几乎一定会选错。

顺序判断问题如果为是如果为否
1收入是否至少有 90% 是经常性收入用 ARR进入第 2 步
2是否是买断制、项目制,没有续费基数用收入进入第 3 步
3所有者是否亲自经营,薪酬是否显著影响利润用 SDE进入第 4 步
4是否有职业经理人,EBITDA 为正且已规范化用 EBITDA用 SDE 或收入

90% 是教学阈值,不是行业标准。它的作用是把「大部分收入可续费」和「少数收入可续费」分开。低于 90% 的订阅生意,应该先把经常性部分拆出来,再决定用 ARR 还是 SDE。LedgerLite 的经常性收入是 100%,所以直接落在 ARR;Nord 的产品订阅只占 27.3%,所以落在 SDE。

这张表也解释了为什么同一门生意会得到不同报价。战略买方可能按 ARR 出价,财务买方按 EBITDA 或 SDE,个人买家按 SDE 或收入。LedgerLite 的 3.2× ARR 是 $768,768;如果个人买家按 4.5× SDE,只得到 $586,080。两个价格都合理,因为它们回答的问题不同:前者买续费基数,后者买可支配现金。

决策表的输出不是价格,而是基数。基数确定之后,倍数才有讨论的意义。接下来要问的是:这个基数上的 3.2×、1.8× 或 4.5× 是否合理,而不是继续在基数之间来回换。

把选择逻辑写成代码

下面的 multiples.ts 把决策顺序写成 selectBase:订阅且经常性 ≥90% 选 ARR;买断制选收入;所有者经营选 SDE;职业经理人且 EBITDA 为正选 EBITDA。compareBases 把同一个倍数套到所有正基数上,用 recommended 标出决策表选中的那个,用 spreadBps 量化「换基数」的价差。

// file: src/capital/multiples.ts
// tier: L1
// verify: node --test test/capital/multiples.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" | "ebitda";
export type BusinessModel =
  | "subscription"
  | "transactional"
  | "service"
  | "company";

export const BASE_ORDER: MultipleBase[] = ["arr", "revenue", "sde", "ebitda"];

export type BusinessProfile = {
  model: BusinessModel;
  annualRevenueCents: Cents;
  arrCents: Cents;
  sdeCents: Cents;
  ebitdaCents: Cents;
  recurringShareBps: Bps;
  ownerOperated: boolean;
  ownerSalaryCents: Cents;
  profitable: boolean;
};

export type BaseSelection = {
  base: MultipleBase;
  amountCents: Cents;
  warnings: string[];
};

export type BaseQuote = {
  base: MultipleBase;
  baseCents: Cents;
  multipleBps: Bps;
  valueCents: Cents;
  recommended: boolean;
};

export type BaseComparison = {
  selection: BaseSelection;
  quotes: BaseQuote[];
  lowCents: Cents;
  highCents: Cents;
  spreadCents: Cents;
  spreadBps: Bps;
};

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

function checkBps(value: Bps, label: string): string | null {
  if (!Number.isInteger(value) || value < 0 || value > 10000) {
    return `${label} must be between 0 and 10000 bps`;
  }
  return null;
}

function checkProfile(profile: BusinessProfile): string | null {
  const models: BusinessModel[] = [
    "subscription",
    "transactional",
    "service",
    "company",
  ];
  if (!models.includes(profile.model)) {
    return "unknown business model";
  }
  const amounts: Array<[Cents, string]> = [
    [profile.annualRevenueCents, "annual revenue"],
    [profile.arrCents, "arr"],
    [profile.ownerSalaryCents, "owner salary"],
  ];
  for (const [value, label] of amounts) {
    const error = checkAmount(value, label);
    if (error) {
      return error;
    }
  }
  if (profile.arrCents > profile.annualRevenueCents) {
    return "arr cannot exceed annual revenue";
  }
  if (!Number.isInteger(profile.sdeCents)) {
    return "sde must be an integer amount in cents";
  }
  if (!Number.isInteger(profile.ebitdaCents)) {
    return "ebitda must be an integer amount in cents";
  }
  const shareError = checkBps(profile.recurringShareBps, "recurring share");
  if (shareError) {
    return shareError;
  }
  if (
    typeof profile.ownerOperated !== "boolean" ||
    typeof profile.profitable !== "boolean"
  ) {
    return "owner-operated and profitable flags must be booleans";
  }
  return null;
}

/**
 * Pick the base before picking the multiple. ARR needs recurring revenue, SDE
 * needs an owner-operated profit base, EBITDA needs professional management,
 * and a transactional product falls back to revenue.
 */
export function selectBase(
  profile: BusinessProfile,
): Result<BaseSelection> {
  const error = checkProfile(profile);
  if (error) {
    return fail(error);
  }
  const warnings: string[] = [];
  if (profile.model === "subscription" && profile.recurringShareBps < 9000) {
    warnings.push(
      "recurring share below 90%: normalize the recurring amount before using ARR",
    );
  }
  if (
    profile.model === "subscription" &&
    profile.recurringShareBps >= 9000 &&
    profile.arrCents > 0
  ) {
    if (profile.arrCents !== profile.annualRevenueCents) {
      warnings.push(
        "arr is lower than total revenue; quote only the recurring portion",
      );
    }
    return ok({ base: "arr", amountCents: profile.arrCents, warnings });
  }
  if (profile.model === "transactional" && profile.annualRevenueCents > 0) {
    warnings.push(
      "revenue multiple buys the current title, not a renewal base",
    );
    return ok({
      base: "revenue",
      amountCents: profile.annualRevenueCents,
      warnings,
    });
  }
  if (
    profile.model === "company" &&
    profile.profitable &&
    profile.ebitdaCents > 0
  ) {
    warnings.push("normalize one-off items before applying an EBITDA multiple");
    return ok({ base: "ebitda", amountCents: profile.ebitdaCents, warnings });
  }
  if (profile.ownerOperated && profile.sdeCents > 0) {
    if (profile.ownerSalaryCents <= 0) {
      warnings.push("owner salary is not stated; sde may not be comparable");
    }
    if (profile.ebitdaCents > 0) {
      warnings.push(
        "ebitda needs an owner-salary normalization before comparison",
      );
    }
    return ok({ base: "sde", amountCents: profile.sdeCents, warnings });
  }

  if (profile.profitable && profile.ebitdaCents > 0) {
    warnings.push("normalize one-off items before applying an EBITDA multiple");
    return ok({ base: "ebitda", amountCents: profile.ebitdaCents, warnings });
  }

  if (profile.sdeCents > 0) {
    return ok({ base: "sde", amountCents: profile.sdeCents, warnings });
  }

  if (profile.annualRevenueCents > 0) {
    warnings.push("revenue multiple does not imply recurrence or profitability");
    return ok({
      base: "revenue",
      amountCents: profile.annualRevenueCents,
      warnings,
    });
  }

  return fail("no positive base available");
}

/** Apply a multiple to one base; the caller is responsible for base choice. */
export function quoteBase(
  base: MultipleBase,
  baseCents: Cents,
  multipleBps: Bps,
): Result<BaseQuote> {
  if (!BASE_ORDER.includes(base)) {
    return fail("unknown multiple base");
  }
  if (!Number.isInteger(baseCents) || baseCents <= 0) {
    return fail("base must be a positive integer amount in cents");
  }
  if (!Number.isInteger(multipleBps) || multipleBps <= 0) {
    return fail("multiple must be a positive integer in basis points");
  }
  return ok({
    base,
    baseCents,
    multipleBps,
    valueCents: applyRate(baseCents, multipleBps),
    recommended: false,
  });
}

/**
 * Apply the same multiple to every positive base. This is a diagnostic, not a
 * recommendation: it shows how far the price moves when the denominator
 * changes while the headline multiple stays fixed.
 */
export function compareBases(
  profile: BusinessProfile,
  multipleBps: Bps,
): Result<BaseComparison> {
  const selection = selectBase(profile);
  if (!selection.ok) {
    return selection;
  }

  const quotes: BaseQuote[] = [];
  for (const base of BASE_ORDER) {
    const amount =
      base === "arr"
        ? profile.arrCents
        : base === "revenue"
          ? profile.annualRevenueCents
          : base === "sde"
            ? profile.sdeCents
            : profile.ebitdaCents;
    if (amount <= 0) {
      continue;
    }
    const quote = quoteBase(base, amount, multipleBps);
    if (!quote.ok) {
      return quote;
    }
    quotes.push({
      ...quote.value,
      recommended: base === selection.value.base,
    });
  }

  if (quotes.length === 0) {
    return fail("no positive base available");
  }

  let lowCents = quotes[0].valueCents;
  let highCents = quotes[0].valueCents;
  for (const quote of quotes) {
    lowCents = Math.min(lowCents, quote.valueCents);
    highCents = Math.max(highCents, quote.valueCents);
  }

  return ok({
    selection: selection.value,
    quotes,
    lowCents,
    highCents,
    spreadCents: highCents - lowCents,
    spreadBps: divide(highCents - lowCents, lowCents),
  });
}

代进三个案例:LedgerLite 选中 ARR,3.2× 得到 $768,768;Tidecast 选中收入,1.8× 得到 $248,708;Nord 选中 SDE,4.5× 得到 $154,440。compareBases 还会给出价差:LedgerLite 在 3.2× 下最高与最低相差 $534,400,是最低值的 228.0%;Nord 在 4.5× 下相差 $540,000,是最低值的 349.7%;Tidecast 在 1.8× 下相差 $30,600,是最低值的 14.0%。价差越大,说明分母选择越关键。

代码不判断倍数高低,只判断基数是否配得上这门生意。倍数本身仍要由增长、毛利、集中度和买方类型决定。5.2 会换一个方向:不看同类交易,直接看未来现金流,用 DCF 做一次独立校验。

小结

  • 三种倍数回答三个问题:ARR 买续费基数,SDE 买可支配现金,EBITDA 买规范化经营利润;收入倍数买当前收入流。
  • 顶线倍数和利润线倍数不能直接比大小;同一个 3.2× 乘在 LedgerLite 的 ARR、SDE、EBITDA 上,分别得到 $768,768、$416,768、$234,368。
  • Tidecast 的 1.8× 收入 = $248,708;改用 SDE 只有 $218,108,差 $30,600,因为买断制买方买的是当前作品,不是卖方运营选择。
  • Nord 的 4.5× SDE = $154,440;把 4.5× 套到总收入上得到 $694,440,这不是报价,是分母错误的产物。
  • 倍数选择按顺序问:经常性收入是否 ≥90%、是否买断制、所有者是否亲自经营、EBITDA 是否已规范化。
  • 同一门生意换基数,价差可以很大:LedgerLite 的 3.2× 下相差 $534,400,Nord 的 4.5× 下相差 $540,000,Tidecast 的 1.8× 下相差 $30,600。
  • 基数确定之后,倍数才有讨论意义;先选倍数再找分母,几乎一定会选错。

阅读导航:上一节:4.3 交易流程与时间表 · 下一节:5.2 DCF 与情景加权 。

常见错误与边界

  1. 把 3.2、1.8、4.5 直接比大小。 三个倍数的分母分别是 ARR、净收入和 SDE;不换分母就比大小,等于拿长度比重量。
  2. 把总收入当 ARR。 Nord 的总收入是 $154,320,可计入 ARR 的只有产品订阅 $42,120;用总收入乘 4.5× 会得到 $694,440 这种没有经济含义的数字。
  3. 对买断制用 ARR。 Tidecast 没有经常性收入,ARR 为零;它只能按收入或 SDE 定价,不能按 ARR 定价。
  4. 对一人公司直接套 EBITDA。 EBITDA 假设所有者薪酬已经不再重要;LedgerLite 的创始人薪酬是 $60,000,不先规范化就套 EBITDA,会把最主要的成本项当成不存在。
  5. SDE 不说明所有者薪酬是否扣除。 LedgerLite 的 SDE 在创始人工资之前,Nord 的 SDE 在两位合伙人工资之后;两个 4.5× 不是同一种 4.5×。
  6. 用收入倍数暗示持续性。 1.8× 收入可以给买断制定价,但不包含下一部作品的承诺;Tidecast 的 Q4 占全年销量 38%,季节性本身就是风险。
  7. 把战略买方的倍数套给个人买家。 3.2× ARR 是战略买方的锚;个人买家可能只按 4.5× SDE 出价,得到 $586,080。倍数不同,分母也不同。
  8. 把 ARR 当现金。 LedgerLite 年末递延收入 $22,000,占 ARR 的 9.2%;ARR 是续费基数,不是已经到手的现金。
  9. 把 EBITDA 当净利润。 EBITDA 还没扣利息、税、折旧和资本开支;它比 SDE 更「规范化」,但不等于股东能拿走的钱。
  10. 只选基数,不看倍数质量。 基数对了,倍数仍可能过高或过低;增长、毛利、集中度和买方类型会决定同一基数上的倍数区间。

5.1 只回答「用哪个基数」。5.2 换一个方向:不看同类交易,直接看未来现金流,用 DCF 做一次独立校验,并处理 DCF 与倍数差 30% 以上的情况。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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