《独立开发者的商业指南·收入与财务工程》11.2 增长、毛利、集中度对倍数的影响

同一个分母上,为什么 LedgerLite 的 3.2× ARR 能站住,而 Nord 的 4.5× SDE 会被买方压到 3.0× = $102,960?本节把倍数拆成经常性收入占比、增长率、毛利率、客户集中度和创始人依赖五个驱动,给出每个驱动的观测指标、改善方向与倍数影响,并把评分固化成可复算的调整系数。

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

11.1 解决了「用哪个分母」:ARR、收入、SDE 各自回答不同问题,不能混用。但同一个分母上,倍数仍然会差很多。LedgerLite 的 3.2× ARR 能站住,Nord 的 4.5× SDE 却被买方压到 3.0× = $102,960;Tidecast 的 1.8× 收入既没有续费基数,也没有稳定的下一部作品承诺。分母相同或相近,价格却不同,差别来自倍数背后的驱动。

这一节把倍数拆成五个驱动:经常性收入占比、增长率、毛利率、客户集中度、创始人依赖。每个驱动都回答三个问题:看什么指标、往哪个方向改善、对倍数是加分还是减分。五个驱动不是一张行业倍数表,而是一套可复算的相对关系;所有评分都是教学设定,不是市场统计。

五个驱动,一张评分表

先把五个驱动放在一张表里。这张表只给方向和观测口径,不给「行业平均倍数」,因为独立开发者的交易样本太少,任何所谓行业区间都很容易变成伪精确。

驱动观测指标改善方向对倍数的影响方向
经常性收入占比ARR ÷ 总收入、续费率、NRR把服务转成订阅、提高年付比例占比越高,倍数越高
增长率MRR 净增、NRR、续约率、客户数净增稳定新增、扩张收入、降低流失增长越稳,倍数越高
毛利率毛利 ÷ 收入、变动成本率降云成本、提价、调整收入结构毛利率越高,倍数越高
客户集中度最大客户、前五大、前三大、HHI分散客户、降低单点依赖集中度越高,倍数越低
创始人依赖关键流程由谁处理、交接文档、团队文档化、外包、雇人、产品化依赖越低,倍数越高

这五个驱动不是并列的。经常性收入占比决定分母的质量,增长率决定这个分母明年会不会变大,毛利率决定每增长一元收入能留下多少,客户集中度和创始人依赖决定买方接手后能不能维持。前三个更像「这门生意好不好」,后两个更像「这门生意能不能交给别人」。

三个案例在五个驱动上的事实并不完整,而且口径不同。能直接用的数字只有这些:

驱动LedgerLiteTidecastNord Studio
经常性收入占比100%0%27.3%
增长相关事实NRR 102%、GRR 83%、月流失 1.5%单部作品、Q4 占销量 38%咨询续约 70%、产品月流失 3.0%
毛利率或净得率84.2%72.0% 净得率84.4%
客户或渠道集中度前五 31%、最大 9%Steam 80% 销量、平台 100%前 3 家 48%
创始人依赖1 人 + 外包1 人 + 外包美术,IP 可授权2 名合伙人,服务关系为主

这张表里没有「增长率」的统一数字,因为三个案例的增长机制根本不同:LedgerLite 看的是续费与扩张,Tidecast 看的是单部作品的销量曲线,Nord 看的是咨询续约和产品流失。用同一个增长率去横比,会把三种不同的东西压成一个数字。

经常性收入占比:分母的质量

经常性收入占比回答的是「这个分母明年还在不在」。LedgerLite 是 100% 订阅,ARR $240,240 全部来自 260 家付费客户;Tidecast 是 0% 经常性,$138,171 净收入来自 9,600 份买断销量;Nord 是 27.3% 产品订阅,$42,120 来自 90 家客户,其余 $112,200 是咨询。

案例经常性收入一次性或服务收入占比倍数锚
LedgerLite$240,240$0100%3.2× ARR = $768,768
Tidecast$0$138,1710%1.8× 收入 = $248,708
Nord Studio$42,120$112,20027.3%4.5× SDE = $154,440

改善方向很清楚:把可重复的服务产品化,把月付转成年付,把一次性交付转成订阅。但改善不是免费的。Nord 要把咨询转成产品,需要先把服务流程标准化,再承担产品研发成本;Tidecast 要做订阅,等于改变商业模式,不是把 $19.99 改成 $9.99/月那么简单。

对倍数的影响方向是正向的:经常性占比越高,买方越能把今年的收入外推到明年,倍数越高。但这不是线性的。4.3 用 2.5×–4.0× 和 1.0×–2.0× 两组教学区间做过线性插值,那是一个可复算的基准,不是市场报价。真实交易里,占比从 0 到 27.3% 带来的倍数提升,可能小于从 27.3% 到 100% 带来的提升,因为前者还没有证明续费能力。

Nord 的 27.3% 是一个典型的中间态:产品订阅有续费,但只占四分之一;咨询续约率 70%,意味着每年有三成服务收入要重新赢回来。买方看到这个结构,不会把 $154,320 全部当作经常性收入,也不会只用 $42,120 定价,而是会用 SDE 分母,再根据服务占比打折。这就是 4.5× 被压到 3.0× 的第一个原因。

增长率与毛利率:一个看方向,一个看留存

增长率决定分母会不会变大,毛利率决定变大的部分能留下多少。两个指标必须一起看:高增长低毛利,增长越快现金越紧;低增长高毛利,现金流稳但估值缺少想象空间。

先看增长。LedgerLite 的 NRR 是 102%,GRR 是 83%,月流失 1.5%。这三个数字放在一起,说明现有客户每年会流失一部分,但扩张收入略高于流失,净收入还在增长。Tidecast 没有续费基数,增长来自单部作品销量:年销 9,600 份,12 月 2,400 份,是月均 800 份的 3 倍,Q4 占全年 38%。这是发布期的脉冲,不是持续增长。Nord 的咨询续约率 70%,产品月流失 3.0%,两条线都在漏,增长要靠不断获新客。

案例增长观测增长质量对倍数的含义
LedgerLiteNRR 102%、GRR 83%、月流失 1.5%扩张略高于流失续费基数大致稳住,倍数有支撑
Tidecast年销 9,600 份、Q4 占 38%发布期脉冲,无续费买方要另算下一部作品的风险
Nord Studio咨询续约 70%、产品月流失 3.0%两条线都在漏服务收入需要持续获客,倍数被压

再看毛利率。LedgerLite 的毛利率是 84.2%,毛利 $202,240;Nord 是 84.4%,毛利 $130,320;Tidecast 的净得率是 72.0%,因为平台分成、退款和支付费先扣掉了 28%。毛利率高不代表估值一定高,但它决定了每一元新增收入能留下多少。LedgerLite 每多 $1 收入,约 $0.84 可以用于覆盖运营费用和利润;Tidecast 每多 $1 标价收入,先被渠道拿走约 $0.28。

改善毛利率的方向包括:降低云成本、提高价格、把低毛利的外包或支付成本转出去、改变收入结构。LedgerLite 的变动成本是 $38,000,其中云 $18,000、支付 $12,000、支持 $8,000;Nord 的直接成本是外包 $18,000、工具 $6,000;Tidecast 的主要漏损是 Steam 30% 分成和 4% 退款。这些数字都能从事实表复算,不需要额外假设。

对倍数的影响方向:增长率越高、增长越稳,倍数越高;毛利率越高,倍数越高。但两个驱动会互相抵消。Tidecast 的净得率 72.0% 不低,可增长是脉冲式的,买方不会因为一年卖得好就给一个可持续的倍数。Nord 的毛利率 84.4% 很高,但服务收入续约率只有 70%,高毛利也换不来高倍数。LedgerLite 的两个数字都不错,所以它的 3.2× ARR 在三案例里最稳。

客户集中度与创始人依赖:两个折价源

客户集中度和创始人依赖是买方最常用的两个折价理由。它们不影响「今年赚多少」,但影响「明年还能不能赚」。

客户集中度看的是收入分布。LedgerLite 前五家客户占 31%,最大一家 9%,长尾较厚;Tidecast 的 Steam 渠道占销量 80%、净收入约 74.7%,平台依赖接近 100%;Nord 前 3 家客户占 48%,服务收入占 72.7%。三个案例的集中类型不同:LedgerLite 是客户集中但分散度尚可,Tidecast 是渠道集中,Nord 是客户与服务线双重集中。7.2 已经把「客户集中」和「渠道集中」的口径拆开,这里直接沿用那个区分。

案例集中度事实风险类型对倍数的影响
LedgerLite前五 31%、最大 9%客户集中,但长尾厚小幅折价
TidecastSteam 80% 销量、平台 100%渠道集中平台规则变化会直接冲击收入
Nord Studio前 3 家 48%、服务 72.7%客户与服务线双重集中折价的主要来源之一

创始人依赖看的是「这门生意离了创始人还能不能转」。LedgerLite 是一人公司,但代码、合同、外包流程相对可交接;Tidecast 是一人开发,但 IP 组合可以授权或整体出售,代码、美术、音乐、商标构成可转移资产;Nord 是两位合伙人,服务收入依赖个人关系和交付能力,而且两位合伙人各 50%、无书面退出条款,交接难度最高。

案例创始人依赖可转移性对倍数的影响
LedgerLite1 人 + 外包,关键流程仍由创始人掌握中等需要交接计划和过渡期
Tidecast1 人 + 外包美术,IP 可授权较高(但音乐授权不可转授权)资产可卖,续作能力仍依赖创始人
Nord Studio2 名合伙人,服务关系为主较低买方必须绑定合伙人或重建团队

改善方向:客户集中度靠分散获客、增加长尾客户、降低单一大客户或单一平台的占比;创始人依赖靠文档化、标准化交付、培养第二负责人、把关键关系从个人转成合同和流程。这两个改善都需要时间,而且会先增加成本、后体现效果,所以买方通常不会为「正在改善」付全价。

对倍数的影响方向:集中度越高,倍数越低;创始人依赖越高,倍数越低。Tidecast 的可转移性其实不差,IP 可以授权,但平台集中度接近 100%,两个驱动一正一负;Nord 的两个驱动都偏负,所以它的 4.5× 锚被压到 3.0×;LedgerLite 的集中度可控、依赖中等,折价最小。

把评分变成调整系数

为了让五个驱动可以复算,本节用一套教学评分:每个驱动 0–100 分,权重分别是经常性收入占比 25%、增长率 20%、毛利率 20%、客户集中度 20%、创始人依赖 15%。权重之和是 100%。评分和权重都是教学参数,不是市场统计,也不代表任何行业标准。

三个案例的评分如下。评分依据只使用事实表里已有的数字和定性描述;没有数字的驱动,按相对位置给分,并在表后说明理由。

驱动LedgerLiteTidecastNord Studio
经常性收入占比100027
增长率653530
毛利率847284
客户集中度702025
创始人依赖607520
加权得分77.836.6537.55

评分理由:LedgerLite 的经常性占比 100%,直接给 100;增长率 65 反映 NRR 102% 略高于流失、但不是高速扩张;毛利率 84 对应 84.2%;客户集中度 70 对应前五 31%、最大 9%;创始人依赖 60 对应一人公司但流程可交接。Tidecast 的经常性占比 0;增长率 35 反映单部作品的脉冲式销量;毛利率 72 对应净得率 72.0%;客户集中度 20 对应 Steam 80%;创始人依赖 75 是因为 IP 可授权、可整体出售,但续作仍依赖创始人。Nord 的经常性占比 27;增长率 30 对应咨询续约 70%、产品月流失 3.0%;毛利率 84 对应 84.4%;客户集中度 25 对应前 3 家 48%;创始人依赖 20 对应两位合伙人、服务关系为主、无书面退出条款。

把加权得分转成倍数调整系数,用一个教学映射:以 50 分为中性,每高或低 1 分,倍数调整 80 个基点,也就是 0.8%。得分 100 对应 +40%,得分 0 对应 −40%。这个映射只用来演示方向,不是定价公式。

案例加权得分调整系数以 3.2× 为统一基数的结果
LedgerLite77.81.2224×3.9117×
Tidecast36.650.8932×2.8582×
Nord Studio37.550.9004×2.8813×

这张表最后一行要特别小心:它把三个案例都放到 3.2× 这个统一基数上,只是为了比较驱动带来的相对差异,不是说 Tidecast 可以用 3.2× ARR,也不是说 Nord 应该用 3.2× 的某个分母。Tidecast 没有 ARR,Nord 的 4.5× SDE 是另一个分母;跨分母套用系数,会犯 11.1 已经指出的错误。

五个驱动里,最弱的一项通常决定买方的折价叙事。LedgerLite 最弱的是创始人依赖 60 分,买方会要求交接计划和过渡期;Tidecast 最弱的是经常性收入 0 分,买方会把下一部作品的风险单独计价;Nord 最弱的是创始人依赖 20 分,其次是客户集中度 25 分,这正好对应它从 4.5× 被压到 3.0× 的原因。改善倍数最有效的动作,往往不是把最强项做得更强,而是把最弱项补到及格线。

把五驱动写成评分函数

下面的 drivers.ts 把评分、权重、加权得分和调整系数固化成函数。validateProfile 拒绝 0–100 之外的分数;weightedScoreBps 用基点表示加权得分;driverContributions 拆出每个驱动对总分的贡献;weakestDriver 找出买方最可能用来压价的那一项;applyDriverAdjustment 把系数乘回一个基准倍数。所有金额和比率仍然沿用整数与基点口径,不引入浮点漂移。

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

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

export type DriverKey =
  | "recurringShare"
  | "growth"
  | "grossMargin"
  | "concentration"
  | "transferability";

export type DriverProfile = {
  recurringShare: number;
  growth: number;
  grossMargin: number;
  concentration: number;
  transferability: number;
};

export type DriverContribution = {
  key: DriverKey;
  score: number;
  weightedBps: number;
};

export type DriverAdjustment = {
  scoreBps: Bps;
  coefficientBps: Bps;
  baseMultipleBps: Bps;
  adjustedMultipleBps: Bps;
};

export const DRIVER_KEYS: DriverKey[] = [
  "recurringShare",
  "growth",
  "grossMargin",
  "concentration",
  "transferability",
];

/** Teaching weights, not observed market weights. They sum to 10000 bps. */
export const DRIVER_WEIGHTS_BPS: Record<DriverKey, Bps> = {
  recurringShare: 2500,
  growth: 2000,
  grossMargin: 2000,
  concentration: 2000,
  transferability: 1500,
};

export const NEUTRAL_SCORE = 50;

/**
 * Teaching mapping: every point above or below the neutral score moves the
 * multiple by 80 bps, capped by the 0-100 score range at roughly +/-40%.
 */
export const SCORE_TO_COEFFICIENT_BPS = 80;

const MIN_SCORE = 0;
const MAX_SCORE = 100;

function checkScore(value: number): string | null {
  if (!Number.isInteger(value) || value < MIN_SCORE || value > MAX_SCORE) {
    return "driver scores must be integers between 0 and 100";
  }
  return null;
}

export function validateProfile(profile: DriverProfile): Result<DriverProfile> {
  for (const key of DRIVER_KEYS) {
    const error = checkScore(profile[key]);
    if (error) {
      return fail(error);
    }
  }
  return ok(profile);
}

/** Weighted score expressed in basis points of the 0-100 scale. */
export function weightedScoreBps(profile: DriverProfile): Result<Bps> {
  const validation = validateProfile(profile);
  if (!validation.ok) {
    return validation;
  }
  let total = 0;
  for (const key of DRIVER_KEYS) {
    total += profile[key] * DRIVER_WEIGHTS_BPS[key];
  }
  return ok(Math.round(total / 100));
}

export function driverContributions(
  profile: DriverProfile,
): Result<DriverContribution[]> {
  const validation = validateProfile(profile);
  if (!validation.ok) {
    return validation;
  }
  return ok(
    DRIVER_KEYS.map((key) => ({
      key,
      score: profile[key],
      weightedBps: Math.round((profile[key] * DRIVER_WEIGHTS_BPS[key]) / 100),
    })),
  );
}

/** The lowest-scoring driver is the one a buyer will use to argue a discount. */
export function weakestDriver(
  profile: DriverProfile,
): Result<{ key: DriverKey; score: number }> {
  const validation = validateProfile(profile);
  if (!validation.ok) {
    return validation;
  }
  let weakestKey = DRIVER_KEYS[0];
  let weakestScore = profile[weakestKey];
  for (const key of DRIVER_KEYS) {
    if (profile[key] < weakestScore) {
      weakestKey = key;
      weakestScore = profile[key];
    }
  }
  return ok({ key: weakestKey, score: weakestScore });
}

/**
 * Convert the weighted score into a multiplier on the base multiple.
 * 10000 bps means no adjustment; above means a premium, below a discount.
 */
export function coefficientBps(profile: DriverProfile): Result<Bps> {
  const score = weightedScoreBps(profile);
  if (!score.ok) {
    return score;
  }
  const pointsAboveNeutral = score.value / 100 - NEUTRAL_SCORE;
  return ok(10000 + Math.round(pointsAboveNeutral * SCORE_TO_COEFFICIENT_BPS));
}

export function applyDriverAdjustment(
  baseMultipleBps: Bps,
  profile: DriverProfile,
): Result<DriverAdjustment> {
  if (!Number.isInteger(baseMultipleBps) || baseMultipleBps <= 0) {
    return fail("base multiple must be a positive integer in basis points");
  }
  const score = weightedScoreBps(profile);
  if (!score.ok) {
    return score;
  }
  const coefficient = coefficientBps(profile);
  if (!coefficient.ok) {
    return coefficient;
  }
  return ok({
    scoreBps: score.value,
    coefficientBps: coefficient.value,
    baseMultipleBps,
    adjustedMultipleBps: Math.round(
      (baseMultipleBps * coefficient.value) / 10000,
    ),
  });
}

代码里有一个刻意的设计:coefficientBps 不直接返回小数倍数,而是返回基点。10000 个基点表示不调整,12224 表示上浮 22.24%,8932 表示下调 10.68%。这样调完之后仍然可以用整数运算,也便于在文章和表格里复算。另一个设计是 weakestDriver 的并列处理:按 DRIVER_KEYS 的顺序取第一个最低分,保证同样的输入每次得到同样的输出。

小结

  • 倍数不是由分母单独决定的;同一个分母上,五个驱动决定溢价还是折价。
  • 五个驱动是:经常性收入占比、增长率、毛利率、客户集中度、创始人依赖。
  • 每个驱动都要回答三件事:看什么指标、往哪个方向改善、对倍数是加分还是减分。
  • LedgerLite 的加权得分 77.8,调整系数 1.2224×;Tidecast 36.65,0.8932×;Nord 37.55,0.9004×。评分和权重都是教学参数。
  • 最弱项通常决定折价叙事:LedgerLite 是创始人依赖,Tidecast 是经常性收入,Nord 是创始人依赖与客户集中度。
  • 改善倍数最有效的动作,是把最弱项补到及格线,而不是把最强项做得更强。

阅读导航:上一节:11.1 三种估值口径:ARR 倍数、SDE 倍数、DCF · 下一节:11.3 估值区间与谈判区间 。

常见错误与边界

  1. 把五个驱动当成行业公式。 25%/20%/20%/20%/15% 的权重和 0–100 评分都是教学参数,不是市场统计;真实交易没有这么整齐。
  2. 用同一个增长率横比三个案例。 LedgerLite 看 NRR 和流失,Tidecast 看单部作品销量,Nord 看咨询续约和产品流失;三种增长机制不能压成一个数字。
  3. 把高毛利率当成高倍数。 Nord 的毛利率 84.4%,但服务续约率 70%、前 3 家客户 48%,高毛利换不来高倍数。
  4. 把平台集中度当客户集中度。 Tidecast 的 Steam 80% 是渠道集中,不是客户集中;改善动作是分散渠道,不是分散客户。
  5. 把 IP 可转移当成业务可转移。 Tidecast 的 IP 可以授权或出售,但续作仍依赖创始人;音乐授权还不可转授权。
  6. 忽略创始人依赖的双面性。 一人公司决策快,但买方接手后要补管理能力;两位合伙人看似有分工,但没有书面退出条款,交接风险更高。
  7. 只补最强项。 把 LedgerLite 的经常性占比从 100% 再往上加没有空间;真正影响折价的是创始人依赖 60 分。
  8. 把调整系数直接乘到任意分母上。 系数是在同一个分母内部比较驱动差异用的;跨分母套用会重犯 11.1 的错误。
  9. 把「正在改善」按已完成定价。 分散客户、降低创始人依赖都需要时间,买方通常只为已经发生的变化付钱。
  10. 忘了交易结构。 驱动评分影响的是倍数区间,不是最终到手金额;earn-out、托管和营运资金调整会在 11.3 单独处理。

11.2 把倍数的「为什么」拆成了五个可观测的驱动。11.3 要把这些驱动变成三档情景:乐观、基准、悲观,并给出一个区间而不是一个点。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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