《独立开发者的商业指南·收入与财务工程》11.3 估值区间与谈判区间

点估值容易变成锚定。本节用悲观、基准、乐观三档把 LedgerLite 的 3.2× ARR 展开成 $672,672–$864,864 的区间,再叠加 70/30 现金与 earn-out、10% 托管和 $3,000 营运资金调整,算出约 $458,261–$765,768 的净得范围;Nord 则用 3.0×–4.5× SDE 给出 $102,960–$154,440 的折价区间。

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

11.1 说明了分母,11.2 拆开了驱动,但两者最终都要落到一个价格上。最容易犯的错误,是把价格写成一个点:LedgerLite 值 $768,768,Tidecast 值 $248,708,Nord 值 $154,440。点估值看起来精确,实际上把分母质量、驱动评分和交易结构的所有不确定性,压缩成了一个没有误差范围的数字。

这一节把点展开成区间。做法不是给三个数字取平均,而是用悲观、基准、乐观三档情景,分别对应不同的倍数假设,再看交易结构如何把 headline 价格转换成净得。区间不是犹豫,区间是把风险写进价格的方式。区间越宽,说明买卖双方对未来的分歧越大;分歧本身,就是谈判要处理的内容。

点估值为什么危险

点估值的第一個问题是锚定。一旦卖方说出 $768,768,后面的谈判就会围绕这个数字上下浮动,而不是回到「这门生意值多少」的问题。买方会说「太贵」,卖方会说「已经让了」,双方都在一个未经拆解的锚上做加减法。

点估值的第二个问题是隐藏假设。$768,768 来自 3.2× ARR,但 3.2× 本身包含了对续费、增长、毛利、集中度和创始人依赖的判断。这些判断在 11.2 里被拆成了五个驱动,在点估值里却被压缩成一个倍数。买方接受 3.2×,等于接受了这五个驱动的全部假设;卖方报出 3.2×,也等于承诺了这些假设。

点估值的第三个问题是忽略结构。$768,768 是 headline 价格,不是到手金额。LedgerLite 的交易结构是现金 70% + earn-out 30%,还有 10% 托管和 $3,000 营运资金下调。如果 earn-out 没拿到、托管被扣,实际净得会远低于 $768,768。把 headline 当净得,是谈判里最常见的误会之一。

视角点估值区间估值
表达方式一个数字低值、基准、高值
隐含假设被压缩进倍数每个情景对应一组假设
风险处理事后争论事前定价
交易结构容易忽略单独折算
谈判作用锚定划定可谈范围

区间估值不是把数字说得模糊,而是把「在什么假设下值多少」说清楚。一个诚实的区间,应该能让对方看到:低值对应哪些条件恶化,高值对应哪些条件兑现,基准值又依赖哪些假设。

三档情景:悲观、基准、乐观

三档情景的做法是:先选一个基准倍数,再给一个上下浮动幅度,得到悲观、基准、乐观三个倍数,最后乘到同一个分母上。浮动幅度不是行业统计,而是对驱动不确定性的教学假设。LedgerLite 的续费基数较稳,浮动可以窄一些;Tidecast 没有续费基数,浮动应该宽一些;Nord 依赖服务和合伙人,浮动也要宽。

案例分母悲观基准乐观低值基准值高值
LedgerLiteARR $240,2402.8×3.2×3.6×$672,672$768,768$864,864
Tidecast净收入 $138,1711.4×1.8×2.2×$193,439$248,708$303,976
Nord StudioSDE $34,3203.0×3.75×4.5×$102,960$128,700$154,440

这张表里的倍数都是教学情景,不是市场报价。LedgerLite 的 2.8×–3.6× 围绕 3.2× 上下浮动 0.4×;Tidecast 的 1.4×–2.2× 围绕 1.8× 上下浮动 0.4×;Nord 的 3.0×–4.5× 正好对应案例披露的折价区间,基准取中点 3.75×。

区间宽度用同一个公式复算:

区间宽度 = (高值 − 低值) ÷ 基准值

按这个公式,LedgerLite 的宽度是 (864,864 − 672,672) ÷ 768,768 = 25.0%;Tidecast 是 (303,976 − 193,439) ÷ 248,708 ≈ 44.4%;Nord 是 (154,440 − 102,960) ÷ 128,700 = 40.0%。三个宽度不同,不是随意设定的,而是反映了三个案例的风险结构不同。

案例低值基准值高值宽度宽度背后的主要不确定性
LedgerLite$672,672$768,768$864,86425.0%续费与扩张能否维持
Tidecast$193,439$248,708$303,97644.4%单部作品销量与下一部作品
Nord Studio$102,960$128,700$154,44040.0%服务收入与合伙人可转移性

三档情景不是三个独立的故事,而是同一组驱动的三种取值。LedgerLite 的低值对应续费变差、扩张放缓;高值对应 NRR 维持甚至提升。Tidecast 的低值对应销量回落、平台分成或退款恶化;高值对应长尾销售和续作预期。Nord 的低值对应服务收入无法转移、合伙人离开;高值对应客户关系和交付能力顺利交接。谈判时,双方应该争论的是「哪个情景更可能」,而不是在基准值上反复砍价。

LedgerLite:区间如何覆盖 earn-out 与托管

LedgerLite 的 headline 区间是 $672,672–$864,864。但案例给出的交易结构不是一次性现金支付,而是资产收购:现金 70%、earn-out 30%、托管 10%,再加上营运资金调整。结构条款见附录 B 第 7 节 ,这里只做净得折算。

以基准值 $768,768 为例:

项比例或金额说明
Headline 价格$768,7683.2× ARR
现金部分70% = $538,138成交时支付
减:托管10% = $76,877从现金中扣留 12 个月
成交时现金$461,261538,138 − 76,877
Earn-out30% = $230,63012 个月,ARR ≥95% 全额
营运资金调整−$3,000目标 $15,000,实际 $12,000
最好情况净得$765,768现金 + 托管 + earn-out − 调整
最差情况净得$458,261只有成交时现金 − 调整

这张表说明了两件事。第一,headline 和净得之间隔着 $3,000 到 $310,507 的差额:最好情况只比 headline 少 $3,000,最差情况少 $310,507。第二,earn-out 和托管不是「额外的钱」,而是 headline 里可能拿不到的部分。买方用这两个工具把未来的不确定性转回给卖方。

把三档 headline 都按同一结构折算,可以得到一个净得区间:

Headline 情景Headline成交时现金Earn-out托管最好净得最差净得
悲观$672,672$403,603$201,802$67,267$669,672$400,603
基准$768,768$461,261$230,630$76,877$765,768$458,261
乐观$864,864$518,919$259,459$86,486$861,864$515,919

表中的金额按整数美元四舍五入,比例仍是 70/30/10,营运资金调整固定为 −$3,000。可以看到,净得区间的下限 $400,603 和上限 $861,864 相差一倍多。这个差距不是估值模型算错了,而是交易结构把 headline 区间放大了:headline 越低,earn-out 和托管也按比例缩小,但「拿不到」的比例不变。

谈判时,卖方应该把「headline 区间」和「净得区间」分开谈。只谈 headline,容易接受一个看起来很高、但 earn-out 条件很严的价格;只谈净得,又可能忽略 earn-out 兑现后的上行空间。正确的做法是把三个情景都折算成净得,再看自己能不能接受最差情况。如果最差净得低于自己的底线,就应该调整结构,而不是继续在 headline 上争。

把区间与结构写成代码

下面的 range.ts 把三档情景和交易结构固化成两组函数。scenarioMultiples 围绕基准倍数生成悲观、基准、乐观三个倍数;scenarioRange 把三个倍数乘到同一个分母上,并返回区间宽度;netProceeds 把 headline 拆成成交时现金、托管、earn-out 和营运资金调整,分别计算最差、期望和最好净得。期望值使用显式概率,不使用「拍脑袋的折扣」。

// file: src/value/range.ts
// tier: L1
// verify: node --test test/value/range.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 ScenarioMultiples = {
  pessimisticBps: Bps;
  baseBps: Bps;
  optimisticBps: Bps;
};

export type ScenarioRange = {
  lowCents: Cents;
  baseCents: Cents;
  highCents: Cents;
  spreadBps: Bps;
  widthBps: Bps;
};

export type DealTerms = {
  cashShareBps: Bps;
  earnOutShareBps: Bps;
  escrowShareBps: Bps;
  earnOutProbabilityBps: Bps;
  escrowReleaseProbabilityBps: Bps;
  workingCapitalAdjustmentCents: Cents;
};

export type NetProceeds = {
  headlineCents: Cents;
  upfrontCents: Cents;
  escrowExpectedCents: Cents;
  earnOutExpectedCents: Cents;
  adjustmentCents: Cents;
  worstCents: Cents;
  expectedCents: Cents;
  bestCents: Cents;
  haircutBps: Bps;
};

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 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;
}

export function scenarioMultiples(
  baseBps: Bps,
  spreadBps: Bps,
): Result<ScenarioMultiples> {
  if (!Number.isInteger(baseBps) || baseBps <= 0) {
    return fail("base multiple must be a positive integer in basis points");
  }
  if (!Number.isInteger(spreadBps) || spreadBps < 0) {
    return fail("spread must be a non-negative integer in basis points");
  }
  if (baseBps - spreadBps <= 0) {
    return fail("pessimistic multiple must remain positive");
  }
  return ok({
    pessimisticBps: baseBps - spreadBps,
    baseBps,
    optimisticBps: baseBps + spreadBps,
  });
}

/**
 * Build a three-point band around a base multiple. Width is measured against
 * the base-case value so a wider band reads directly as a larger risk signal.
 */
export function scenarioRange(
  baseCents: Cents,
  baseMultipleBps: Bps,
  spreadBps: Bps,
): Result<ScenarioRange> {
  const amountError = checkAmount(baseCents, "base");
  if (amountError) {
    return fail(amountError);
  }
  const multiples = scenarioMultiples(baseMultipleBps, spreadBps);
  if (!multiples.ok) {
    return multiples;
  }
  const lowCents = applyRate(baseCents, multiples.value.pessimisticBps);
  const baseValueCents = applyRate(baseCents, multiples.value.baseBps);
  const highCents = applyRate(baseCents, multiples.value.optimisticBps);
  return ok({
    lowCents,
    baseCents: baseValueCents,
    highCents,
    spreadBps,
    widthBps: divide(highCents - lowCents, baseValueCents),
  });
}

function checkShares(terms: DealTerms): string | null {
  const cashError = checkBps(terms.cashShareBps, "cash share");
  if (cashError) {
    return cashError;
  }
  const earnOutError = checkBps(terms.earnOutShareBps, "earn-out share");
  if (earnOutError) {
    return earnOutError;
  }
  const escrowError = checkBps(terms.escrowShareBps, "escrow share");
  if (escrowError) {
    return escrowError;
  }
  if (terms.cashShareBps + terms.earnOutShareBps !== 10000) {
    return "cash plus earn-out shares must add up to 10000 bps";
  }
  if (terms.escrowShareBps > terms.cashShareBps) {
    return "escrow share cannot exceed the cash share";
  }
  const earnOutProbabilityError = checkBps(
    terms.earnOutProbabilityBps,
    "earn-out probability",
  );
  if (earnOutProbabilityError) {
    return earnOutProbabilityError;
  }
  const escrowProbabilityError = checkBps(
    terms.escrowReleaseProbabilityBps,
    "escrow release probability",
  );
  if (escrowProbabilityError) {
    return escrowProbabilityError;
  }
  if (!Number.isInteger(terms.workingCapitalAdjustmentCents)) {
    return "working capital adjustment must be an integer amount in cents";
  }
  return null;
}

/**
 * Earn-out and escrow are not additional money on top of the headline price;
 * they are parts of the headline that may not be paid. The expected value uses
 * explicit probabilities, while worst and best bracket the structural range.
 */
export function netProceeds(
  headlineCents: Cents,
  terms: DealTerms,
): Result<NetProceeds> {
  const amountError = checkAmount(headlineCents, "headline");
  if (amountError) {
    return fail(amountError);
  }
  const shareError = checkShares(terms);
  if (shareError) {
    return fail(shareError);
  }
  const cashCents = applyRate(headlineCents, terms.cashShareBps);
  const earnOutCents = applyRate(headlineCents, terms.earnOutShareBps);
  const escrowCents = applyRate(headlineCents, terms.escrowShareBps);
  const upfrontCents = cashCents - escrowCents;
  const escrowExpectedCents = applyRate(
    escrowCents,
    terms.escrowReleaseProbabilityBps,
  );
  const earnOutExpectedCents = applyRate(
    earnOutCents,
    terms.earnOutProbabilityBps,
  );
  const adjustmentCents = terms.workingCapitalAdjustmentCents;
  const worstCents = upfrontCents - adjustmentCents;
  const expectedCents =
    upfrontCents +
    escrowExpectedCents +
    earnOutExpectedCents -
    adjustmentCents;
  const bestCents = upfrontCents + escrowCents + earnOutCents - adjustmentCents;
  return ok({
    headlineCents,
    upfrontCents,
    escrowExpectedCents,
    earnOutExpectedCents,
    adjustmentCents,
    worstCents,
    expectedCents,
    bestCents,
    haircutBps: divide(headlineCents - expectedCents, headlineCents),
  });
}

代码里有两个口径需要注意。第一,scenarioRange 的宽度以基准值为分母,而不是以低值或高值为分母;这样同一套公式可以横向比较不同案例的区间宽度。第二,netProceeds 把托管视为从现金中扣留,而不是在 headline 之外额外加钱;worstCents 假设 earn-out 为零、托管不释放,bestCents 假设两者全额兑现,expectedCents 则用调用方给出的概率。概率不是事实,而是谈判前必须写下来的假设。

Nord:3.0×–4.5× SDE 的折价区间

Nord 的区间不是从 3.75× 上下浮动出来的,而是案例直接给出了两个锚:标准锚 4.5× SDE = $154,440,买方折价 3.0× SDE = $102,960。中点的 3.75× = $128,700 是为了做三档情景而取的教学基准,不是案例披露的价格。

情景倍数SDE估值买方视角
悲观3.0×$34,320$102,960服务收入无法转移,合伙人依赖未解决
基准3.75×$34,320$128,700部分服务关系可交接,但仍需绑定合伙人
乐观4.5×$34,320$154,440服务收入顺利转移,合伙人留任

Nord 的折价来源在 11.2 已经拆过:服务收入占 72.7%,咨询续约率 70%,前 3 家客户占 48%,两位合伙人各 50% 且无书面退出条款。这些因素不会因为谈判技巧而消失,只会因为交易结构而部分缓解。比如,买方可以要求两位合伙人留任 12–24 个月、签署竞业限制、把关键客户关系转成公司合同;这些安排能把估值从 3.0× 推向 3.75×,但很难直接推到 4.5×。

Nord 还有一条合伙人退出路径:一位合伙人退出,买断 50% × 4.5 × SDE = $77,220,分 24 个月支付。这是另一个交易,不是整体出售价格。它说明同一门生意在不同交易结构下会有不同的价格:整体出售给外部买方,得到的是 3.0×–4.5× 的区间;内部合伙人买断,得到的是 4.5× 的一半,而且分 24 个月支付,时间风险由退出方承担。谈判时不能把这两个价格混在一起。

交易价格支付方式风险承担方
整体出售(悲观)3.0× SDE = $102,960按并购协议买方承担服务流失风险
整体出售(乐观)4.5× SDE = $154,440按并购协议卖方需证明可转移性
合伙人退出50% × 4.5 × SDE = $77,220分 24 个月退出方承担时间与信用风险

把 Nord 的区间和 LedgerLite 的净得区间放在一起看,会发现一个反直觉的现象:Nord 的 headline 上限 $154,440 远低于 LedgerLite 的 headline 下限 $672,672,但 Nord 的区间宽度 40.0% 高于 LedgerLite 的 25.0%。绝对值小不代表风险小;区间宽度衡量的是相对不确定性,不是价格高低。

区间宽度就是风险信号

区间宽度不是模型的技术参数,而是谈判信息。宽度越大,说明买卖双方对未来的分歧越大,或者这门生意的关键假设越难验证。LedgerLite 的 25.0% 最窄,因为它的续费基数、流失率和毛利率都有可观测的数字;Tidecast 的 44.4% 最宽,因为它没有续费基数,销量又高度集中在发布期;Nord 的 40.0% 居中偏宽,因为服务收入和合伙人依赖都难以用报表验证。

案例区间宽度主要风险源谈判动作
LedgerLite25.0%续费与扩张用 GRR、NRR、月流失数据缩小分歧
Tidecast44.4%单部作品销量与续作把下一部作品的预算和销量假设写进条款
Nord Studio40.0%服务收入与合伙人用留任、竞业、客户合同转移来换倍数

宽度本身也有陷阱。第一,窄区间可能是假精确:如果分母选错了,比如把总收入当 ARR,区间再窄也没有意义。第二,宽区间不一定是坏事:如果宽度的来源可以被结构化,比如 earn-out 和托管,卖方可以用结构换取更高的 headline。第三,宽度会随着信息增加而收窄:尽调、客户访谈、代码审查都会让某些假设变得可验证,区间应该随信息更新,而不是一次定死。

谈判区间不是估值区间的简单复制。卖方有自己的底线,买方有自己的上限,两个区间可能重叠,也可能不重叠。重叠部分就是可成交区;不重叠时,正确的动作是回到驱动和结构,而不是强行取中点。比如 LedgerLite 的净得区间是 $400,603–$861,864,如果卖方的底线是 $500,000,买方的上限是 $700,000,那么 $500,000–$700,000 就是谈判区;如果卖方的底线是 $800,000,买方的上限是 $700,000,就没有重叠,应该重新谈结构或放弃交易。

区间类型用途不应该做什么
估值区间表达不同假设下的价值不要把三档平均成一个点
净得区间折算 earn-out、托管和调整不要把 headline 当净得
谈判区间找双方可接受的重叠不要在没有重叠时强行成交

区间估值最终要回答的不是「这门生意值多少钱」,而是「在什么条件下值多少,以及这些条件能不能被验证」。11.2 的五个驱动提供了验证清单,11.3 的三档情景提供了价格语言,交易结构则提供了把价格和条件绑定在一起的工具。三者合起来,才是一个可以谈判的估值。

小结

  • 点估值会把分母质量、驱动评分和交易结构压缩成一个数字,容易变成锚定。
  • 三档情景用悲观、基准、乐观三个倍数乘同一个分母;LedgerLite 是 2.8×–3.6× ARR,Tidecast 是 1.4×–2.2× 收入,Nord 是 3.0×–4.5× SDE。
  • 区间宽度 = (高值 − 低值) ÷ 基准值:LedgerLite 25.0%、Tidecast 44.4%、Nord 40.0%。
  • LedgerLite 的 headline $768,768 经过 70/30 现金与 earn-out、10% 托管和 −$3,000 营运资金调整后,净得区间约为 $458,261–$765,768;最差情况只有成交时现金。
  • Nord 的 3.0×–4.5× SDE 给出 $102,960–$154,440 的区间,折价来自服务收入 72.7%、前 3 家客户 48% 和两位合伙人的可转移性。
  • 区间宽度是风险信号:宽度越大,越应该回到驱动和结构,而不是强行取中点。

阅读导航:上一节:11.2 增长、毛利、集中度对倍数的影响 · 下一节:12.1 尽调清单:财务、客户、合同 。

常见错误与边界

  1. 把三档情景平均成一个点。 平均会抹掉悲观和乐观背后的假设;区间的作用是保留分歧,不是消除分歧。
  2. 把 headline 当净得。 LedgerLite 的 $768,768 在 earn-out 和托管全部兑现时净得 $765,768,在最差情况下只有 $458,261;两者不是一回事。
  3. 把 earn-out 当额外收入。 Earn-out 是 headline 的一部分,不是加在价格上的奖金;它只是把支付时点推后,并把风险转给卖方。
  4. 把托管当买方扣款。 托管是从现金中扣留、条件满足后释放,不是永久扣款;但卖方必须把「不释放」写进最差情景。
  5. 忽略营运资金调整。 LedgerLite 的目标营运资金 $15,000、实际 $12,000,下调 $3,000;金额不大,但它会直接减少净得。
  6. 把 Nord 的 3.0×–4.5× 当成市场区间。 这是案例披露的标准锚与折价锚,不是行业统计;中点的 3.75× 是教学基准。
  7. 把合伙人退出价当整体出售价。 $77,220 是 50% × 4.5 × SDE,分 24 个月支付,和整体出售的 $102,960–$154,440 不是同一交易。
  8. 用窄区间证明估值准确。 如果分母选错,窄区间只是精确地错;先确认分母,再看宽度。
  9. 把区间宽度当缺点。 宽度可以被结构化管理;earn-out、托管、留任协议都是把宽度变成条款的工具。
  10. 在没有重叠时强行取中点。 谈判区间不重叠,说明假设或结构有问题;此时应该回到驱动和尽调,而不是用数学平均掩盖分歧。

11.3 把估值从点变成区间,也为第 12 章的尽调做了铺垫:尽调的作用,就是把区间里那些「无法验证」的假设,变成「可以验证」的事实。区间不会因为尽调而消失,但会变窄,而且窄得有理有据。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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