《独立开发者的商业指南·收入与财务工程》10.1 五个核心指标与口径

每月只看五个指标:MRR 及净变化、毛利率、SDE 与 SDE 率、经营现金流、零收入 runway。本节用 LedgerLite 的月报说明每个指标的绿色阈值、异常信号与对应动作,再用 Tidecast 的买断脉冲和 Nord Studio 的服务混合说明同一张表为什么会变形。

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

仪表盘最常见的失败不是缺指标,而是指标太多。收入、客户数、转化率、客单价、毛利、费用、现金、应收、库存、runway 都放上去,最后每天只看最上面那个大数字。指标一多,阈值就没人记得,红灯也会被当成背景色。这一节只留五个,并给每个指标配一条阈值、一个异常信号和一个动作。

为什么只留五个

一个月度指标要同时满足三个条件:能从银行流水和三张表复算;有一个事先约定的阈值;越过阈值后有一个明确的人和时间去处理。三条缺一条,这个指标就会变成「看一眼,然后放回去」。

五个指标对应的是一条链,不是五张孤立的成绩单:

顺序指标回答的问题口径来源
1MRR 及净变化收入在长还是在漏第 2 章的单位经济、第 4 章的收入确认
2毛利率每收一元留下多少第 3 章的利润表
3SDE 与 SDE 率经营本身赚不赚钱第 3 章的利润表
4经营现金流利润有没有变成现金第 3 章的现金流量表
5runway现金还能撑多久第 8 章的 runway 与最低水位

这条链不能跳。MRR 增长但毛利率下滑,增长可能是在买收入;毛利率高但 SDE 率为负,费用结构有问题;SDE 为正但经营现金流为负,钱可能压在应收或税费里;现金转换率好但 runway 只有三个月,一次退款潮就会打断。五个数一起看,才能把增长、效率、现金、生存四件事分开。

本章不重复公式。第 2 章讲过单位经济,第 3 章讲过三张表,第 8 章讲过 runway 与最低水位;本章只回答一个问题:每月看哪五个,看到什么数值该做什么。公式的口径一旦变化,五个指标的读数就会一起变形,所以先统一口径,再谈阈值。

收入端两个:MRR 净变化与毛利率

MRR 净变化不是「这个月新签了多少」,而是期末 MRR 减期初 MRR。LedgerLite 当月从 19,280 走到 20,020,净增 740,增幅 3.8%。这 740 可以拆成四段:

桥接项金额说明
期初 MRR$19,280上月结转
新签+$78010 家 × $78
扩张+$310存量客户升级
收缩−$60降档
流失−$290约 1.5%
期末 MRR$20,020净增 +$740

毛新增是 +$1,090,毛流失是 −$350,净增 +$740。只看净增,会看不到 350 美元的漏损;只看新签,会看不到 290 美元的流失。四段桥接是月度必看的最小分解,少了任何一段,净增都只是结果,不是原因。

LedgerLite 的 NRR 是 102%,GRR 是 83%,月流失 1.5%。三个数放在一起才完整:GRR 83% 说明存量客户每年漏掉约 17%,NRR 102% 说明扩张刚好补上流失,净增长仍然依赖新签。如果某个月净增为正但 NRR 掉到 95% 以下,问题不是「这个月」,而是存量开始漏;这时继续加获客预算,只会往漏水的桶里加水。

毛利率是毛利除以收入。LedgerLite 年收入 240,240,变动成本 38,000(云 18,000、支付 12,000、支持 8,000),毛利 202,240,毛利率 84.2%。一个百分点是 240,240 × 1% = $2,402 的毛利,不是小数。毛利率的月度任务不是「算出 84.2%」,而是判断变动成本里哪一项在动:云随用量、支付随收入、支持随客户数,三者的驱动不同,动作也不同。

Tidecast 的 72.0% 不能直接和 84.2% 比。它的净得已经扣掉退款 4%、Steam 分成 30% 和官网支付费 5%,平台分成是收入的直接扣减,不是「毛利薄」。Nord Studio 的 84.4% 是咨询和产品两条线的混合值,外包和工具的直接成本边界与订阅 SaaS 不同。跨案例比毛利率,先问「哪些成本被算进了变动成本」,再问「这个毛利率能不能覆盖固定费用」。

利润与现金三个:SDE 率、现金转换率、runway

SDE 是毛利减运营费用。LedgerLite 的 SDE 130,240,SDE 率 130,240 ÷ 240,240 = 54.2%。这个数在创始人薪酬之前,扣掉 60,000 薪酬和 11,941 所得税,才是 58,299 的净利润。SDE 率回答「这门生意本身能产出多少现金」,不回答「创始人能拿多少」。

SDE 率的跨案例比较必须先统一口径。Nord Studio 的 SDE 34,320 ÷ 154,320 = 22.2%,看起来很低,但它的两人工资 96,000 已经从毛利里扣掉了;把这笔加回去,SDE 是 130,320,率是 84.4%,和 LedgerLite 的 54.2% 才是同一层。Tidecast 的 121,171 ÷ 138,171 = 87.7%,是买断制在扣掉营销、服务器和工具之后的经营剩余。三个 22.2%、54.2%、87.7% 放在一张表里,如果不注明「薪酬前/后」,会得出完全相反的结论。

现金转换率是经营现金流除以净利润。LedgerLite 的 64,299 ÷ 58,299 = 110.3%,高于 100%,原因是递延收入增加 8,000、应收增加 2,000:客户先付钱、后确认收入,现金比利润多。这个数低于 80% 通常意味着利润被应收或库存吃掉;连续两个月低于 50%,要先去查回款,而不是先怪利润表。现金转换率高于 100% 也不全是好事:递延收入是未来要交付的义务,不是免费的现金。

runway 用零收入口径:现金除以月固定成本,假设收入归零,只看现金能覆盖多久。LedgerLite 95,000 ÷ 11,000 = 8.6 个月;Tidecast 92,090 ÷ 1,417 = 65.0 个月;Nord 25,740 ÷ 10,000 = 2.6 个月。零收入口径是保守下限,不是预测;它故意忽略流入,用来回答「最坏情况下还有多少时间」。第 8 章还讲了按净烧钱率算的 runway 和最低水位,两者要一起看:水位回答「什么时候必须停」,runway 回答「停下来之前还有多久」。Tidecast 的 65.0 个月不含生活支出和续作预算,只能当上限,不能当安全垫。本节用年度数算比率,是因为案例事实表只给年度数;真实月度仪表盘要用当月数,年度数只用于校准口径。

阈值表:看到什么数值做什么

下面五个阈值是教学参数,不是行业标准。它们的作用是让「什么值得处理」在月初就约定好,而不是月底凭感觉。参数可以按公司规模和收入形态调整,但要写在预算说明里,不能等指标变红再改。

指标绿色阈值异常信号对应动作
MRR 净变化月净增 ≥ +1.0%,且 NRR ≥ 100%连续 2 个月净增为负;NRR < 95%;月流失 > 2.5%先拆四段桥接,再查流失客户,最后才动获客预算
毛利率订阅 ≥ 75%;买断净得 ≥ 65%;服务混合按直接成本边界设线单月下降 ≥ 3 个百分点;连续两季下降拆云、支付、支持/平台分成,先分一次性与结构性
SDE 与 SDE 率创始人薪酬前 ≥ 40%;服务混合薪酬后 ≥ 25%低于红线;连续 3 个月下降先加回创始人薪酬统一口径,再决定提价、降外包或减工具
经营现金流现金转换率 ≥ 80%连续 2 个月 < 50%;净利润为正而经营现金流为负查应收、递延收入、税费时点;先催收,再谈融资
runway订阅 ≥ 6 个月;买断/项目制 ≥ 12 个月< 3 个月;连续 3 个月下降冻结非必要支出、催收、预收年费;< 3 个月不做长期投入

这张表有三条使用规则。

第一,阈值是「或」,不是「且」。MRR 净增 0.8% 但 NRR 只有 94%,也算异常;毛利率只降了 2 个百分点但连续两季都降,也算异常。单一指标可以骗人,组合信号更难同时造假。

第二,绿灯不等于不用看。绿灯只说明「这个月不用单独开会」,不说明「这个数没有变化」。毛利率从 84.2% 走到 80.1% 仍在绿灯区,但一个月掉 4.1 个百分点,已经触发「单月下降 ≥ 3 个百分点」的异常信号;阈值是动作开关,不是健康证书。

第三,红灯只改动作,不改口径。看到 runway 2.6 个月,第一反应不能是把固定成本改小,让 runway 变长;口径一改,下个月就无法和上个月比。红灯时先冻结非必要支出、催收、预收年费,再考虑调整业务结构。口径的变更要单独记录,并重算历史三个月,否则仪表盘会失去可比性。

三案例同表与代码

把五个指标放到同一张表,会看到三种生意不能用同一把尺子:

指标LedgerLiteTidecastNord Studio
月度收入口径MRR $20,020月均净得 $11,514(年 138,171 ÷ 12)产品 MRR $3,510;咨询另计
毛利率84.2%72.0%(净得率)84.4%(混合)
SDE 率54.2%(薪酬前)87.7%(净得口径)22.2%(薪酬后);加回工资 84.4%
现金转换率110.3%100%(可支配现金口径)100%(可支配现金口径)
零收入 runway8.6 个月65.0 个月(不含生活支出)2.6 个月
机械状态绿灯黄灯:MRR 口径不适用红灯:SDE 率与 runway

Tidecast 的第一格不是 MRR。买断制的月收入是脉冲,12 月 2,400 份、1 月 480 份,用单月 MRR 会把季节噪声当成趋势;它的替代口径是滚动三个月净得。Nord 的第一格只是产品线 MRR 3,510,占总收入 27.3%,咨询 9,350 不在 MRR 里;把两条线合成一个「MRR」会掩盖服务续约率 70% 和产品月流失 3.0% 的差异。

表里的 100% 也要打折扣。Tidecast 和 Nord 没有单独披露经营现金流,只能用可支配现金除以同一笔可支配现金,得到的 100% 是恒等式,不是效率;真正的现金转换率要等现金流分类。这也是为什么同一张表要同时写口径和数值,只写数值会把「无法计算」伪装成「表现很好」。

机械状态只是提示,不是结论。Tidecast 的黄灯来自「MRR 口径不适用」,不是业务变差;Nord 的红灯同时来自 SDE 率 22.2% 和 runway 2.6 个月,这两个才是需要动作的信号。仪表盘的价值是让红灯先被看见,不是替人做判断。

下面的 dashboard.ts 把五个读数、阈值和红黄绿状态固化成一次计算。金额仍是整数美分,比率仍是基点;priorMrr 允许为 null,因为缺上期数据时应该显示「无法判断」,而不是编一个 0%。

// file: src/fin/dashboard.ts
// tier: L1
// verify: node --test test/fin/dashboard.test.ts

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

export type Status = "green" | "amber" | "red";

export type MetricKey =
  | "mrr_growth"
  | "gross_margin"
  | "sde_rate"
  | "cash_conversion"
  | "runway";

export type MetricReading = {
  key: MetricKey;
  label: string;
  value: number | null;
  unit: "bps" | "months";
  status: Status;
};

export type DashboardInput = {
  mrr: Cents;
  priorMrr: Cents | null;
  nrrBps?: Bps;
  grossMarginBps: Bps;
  priorGrossMarginBps: Bps;
  sde: Cents;
  revenue: Cents;
  operatingCashFlow: Cents;
  netIncome: Cents;
  cash: Cents;
  monthlyFixedCosts: Cents;
};

/** Teaching thresholds, not industry standards. Change them in one place. */
export const LIMITS = {
  mrrGreen: 100,
  mrrRed: -100,
  nrrGreen: 10000,
  nrrRed: 9500,
  marginGreen: 7500,
  marginRed: 6500,
  marginDropAmber: 300,
  marginDropRed: 500,
  sdeGreen: 4000,
  sdeRed: 2500,
  cashGreen: 8000,
  cashRed: 5000,
  runwayGreen: 6,
  runwayRed: 3,
} as const;

export type Dashboard = {
  readings: MetricReading[];
  green: number;
  amber: number;
  red: number;
  overall: Status;
};

function highStatus(value: number, green: number, red: number): Status {
  if (value >= green) return "green";
  if (value < red) return "red";
  return "amber";
}

function checkInput(input: DashboardInput): string | null {
  if (!Number.isInteger(input.mrr) || input.mrr < 0) {
    return "mrr must be a non-negative integer";
  }
  if (
    input.priorMrr !== null &&
    (!Number.isInteger(input.priorMrr) || input.priorMrr <= 0)
  ) {
    return "prior mrr must be null or a positive integer";
  }
  if (
    input.nrrBps !== undefined &&
    (!Number.isInteger(input.nrrBps) ||
      input.nrrBps < 0 ||
      input.nrrBps > 30000)
  ) {
    return "nrr must be between zero and 30000 bps";
  }
  if (
    !Number.isInteger(input.grossMarginBps) ||
    input.grossMarginBps < 0 ||
    input.grossMarginBps > 10000 ||
    !Number.isInteger(input.priorGrossMarginBps) ||
    input.priorGrossMarginBps < 0 ||
    input.priorGrossMarginBps > 10000
  ) {
    return "gross margins must be between zero and 10000 bps";
  }
  if (
    !Number.isInteger(input.sde) ||
    !Number.isInteger(input.revenue) ||
    input.revenue < 0 ||
    !Number.isInteger(input.operatingCashFlow) ||
    !Number.isInteger(input.netIncome)
  ) {
    return "sde, revenue, cash flow and net income must be integer cents";
  }
  if (!Number.isInteger(input.cash) || input.cash < 0) {
    return "cash must be a non-negative integer";
  }
  if (!Number.isInteger(input.monthlyFixedCosts) || input.monthlyFixedCosts <= 0) {
    return "monthly fixed costs must be a positive integer";
  }
  return null;
}

function mrrReading(input: DashboardInput): MetricReading {
  if (input.priorMrr === null) {
    return {
      key: "mrr_growth",
      label: "MRR 净变化",
      value: null,
      unit: "bps",
      status: "amber",
    };
  }
  const value = divide(input.mrr - input.priorMrr, input.priorMrr);
  let status: Status;
  if (
    value >= LIMITS.mrrGreen &&
    (input.nrrBps === undefined || input.nrrBps >= LIMITS.nrrGreen)
  ) {
    status = "green";
  } else if (
    value < LIMITS.mrrRed ||
    (input.nrrBps !== undefined && input.nrrBps < LIMITS.nrrRed)
  ) {
    status = "red";
  } else {
    status = "amber";
  }
  return { key: "mrr_growth", label: "MRR 净变化", value, unit: "bps", status };
}

function grossMarginReading(input: DashboardInput): MetricReading {
  let status = highStatus(
    input.grossMarginBps,
    LIMITS.marginGreen,
    LIMITS.marginRed,
  );
  const drop = input.priorGrossMarginBps - input.grossMarginBps;
  if (status === "green" && drop >= LIMITS.marginDropAmber) status = "amber";
  if (drop >= LIMITS.marginDropRed) status = "red";
  return {
    key: "gross_margin",
    label: "毛利率",
    value: input.grossMarginBps,
    unit: "bps",
    status,
  };
}

function sdeRateReading(input: DashboardInput): MetricReading {
  const value = divide(input.sde, input.revenue);
  return {
    key: "sde_rate",
    label: "SDE 率",
    value,
    unit: "bps",
    status: highStatus(value, LIMITS.sdeGreen, LIMITS.sdeRed),
  };
}

function cashConversionReading(input: DashboardInput): MetricReading {
  if (input.netIncome <= 0) {
    return {
      key: "cash_conversion",
      label: "现金转换率",
      value: null,
      unit: "bps",
      status: input.operatingCashFlow >= 0 ? "amber" : "red",
    };
  }
  const value = divide(input.operatingCashFlow, input.netIncome);
  return {
    key: "cash_conversion",
    label: "现金转换率",
    value,
    unit: "bps",
    status: highStatus(value, LIMITS.cashGreen, LIMITS.cashRed),
  };
}

function runwayReading(input: DashboardInput): MetricReading {
  const value = Math.round((input.cash / input.monthlyFixedCosts) * 10) / 10;
  return {
    key: "runway",
    label: "零收入 runway",
    value,
    unit: "months",
    status: highStatus(value, LIMITS.runwayGreen, LIMITS.runwayRed),
  };
}

/**
 * One red tile turns the whole month red. A five-tile dashboard is a short
 * priority list, not an average score.
 */
export function buildDashboard(input: DashboardInput): Result<Dashboard> {
  const inputError = checkInput(input);
  if (inputError) return fail(inputError);
  const readings = [
    mrrReading(input),
    grossMarginReading(input),
    sdeRateReading(input),
    cashConversionReading(input),
    runwayReading(input),
  ];
  const green = readings.filter((r) => r.status === "green").length;
  const amber = readings.filter((r) => r.status === "amber").length;
  const red = readings.filter((r) => r.status === "red").length;
  const overall: Status = red > 0 ? "red" : amber > 0 ? "amber" : "green";
  return ok({ readings, green, amber, red, overall });
}

代码有五个细节。第一,priorMrr 为 null 时 MRR 读数返回 amber 和 null,不伪造 0% 增长;缺数据本身就是异常信号。第二,NRR 是可选参数:买断制没有 NRR,硬填 100% 会让 MRR 永远绿灯。第三,毛利率除了看绝对值,还看相对上期的下降幅度;84.2% 掉到 80.1% 仍在 75% 的绿灯线之上,但 4.1 个百分点的单月下降会把它转成 amber;掉到 79.0% 才会因为 5.2 个百分点的下降转 red。第四,净利润为负时现金转换率没有意义,代码改成看经营现金流方向:为正 amber,为负 red,不拿负数去除以负数。第五,整体状态是「任一红灯即红」,不是取平均;五格三绿两红仍然是红灯,因为两个红格已经足够让这个月失焦。

小结

  • 月度只看五个:MRR 及净变化、毛利率、SDE 与 SDE 率、经营现金流、零收入 runway;五个指标对应增长、效率、现金、生存一条链。
  • 每个指标都要有阈值、异常信号和动作三件事;只有定义没有阈值,仪表盘就只是报表。
  • LedgerLite 当月 MRR 净增 $740、增幅 3.8%,四段桥接是 +$780 新签、+$310 扩张、−$60 收缩、−$290 流失;净增为正不代表没有漏损。
  • SDE 率跨案例比较前先统一创始人薪酬口径:Nord 的 22.2% 是薪酬后,加回 96,000 工资才是 84.4%。
  • 现金转换率高于 100% 可能来自递延收入,低于 80% 才要查回款;两种方向都要解释,不能只看数字好坏。
  • 零收入 runway 是保守下限:LedgerLite 8.6 个月、Tidecast 65.0 个月(不含生活支出)、Nord 2.6 个月;红灯时先改支出和回款,不改口径。

阅读导航:上一节:9.3 跨境预提、转让定价与资金回流 · 下一节:10.2 月度关账流程 。

常见错误与边界

  1. 指标越多越安心。 十五个指标没有阈值,等于没有指标;五个有阈值、有动作的指标,比一页 Excel 更有用。
  2. 只看 MRR 净增。 净增 +$740 背后可能是 +$1,090 毛新增和 −$350 毛流失;不拆桥接,就不知道增长是不是在盖漏损。
  3. 把 NRR 和 GRR 混着用。 GRR 83% 说明存量漏损,NRR 102% 说明扩张补上了漏损;两个数回答不同问题,不能互相替代。
  4. 跨案例直接比毛利率。 Tidecast 的 72.0% 含平台分成和退款,LedgerLite 的 84.2% 不含;分母不同,比出来的结论不可用。
  5. SDE 率不注明薪酬前/后。 Nord 的 22.2% 和 LedgerLite 的 54.2% 不在同一层;加回工资后 Nord 是 84.4%,结论会反转。
  6. 把现金转换率高于 100% 当成绩。 递延收入是未来交付义务,现金提前到账不等于利润变好;要同时看递延余额和交付进度。
  7. runway 用最乐观的收入假设。 零收入 runway 故意忽略流入,就是为了避免「下个月会更好」把红灯涂绿。
  8. 把 Tidecast 的 65.0 个月当安全垫。 它不含生活支出和续作预算,是一次脉冲后的上限,不是可持续的 runway。
  9. 红灯时改口径。 把固定成本改小让 runway 变长,会让历史三个月无法比较;口径变更要单独记录并重算。
  10. 机械状态代替判断。 仪表盘只负责把红灯排到前面;Tidecast 的黄灯是口径不适用,不是业务变差,Nord 的红灯才是要处理的问题。

本章的边界是「月度快照」:它假设三张表已经结平、口径没有变更、收入可以按 MRR 或等效月均归类。它不处理结账本身的对账差异,也不处理预算偏差的归因;前者是下一节的任务,后者要回到第 5 章的季度校准。五个指标不是越少越好,而是「少到每个月都能看完、每个都能追到动作」;当业务复杂到五个不够时,应该拆的是业务线,不是把仪表盘扩成三十格。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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