《独立开发者的商业指南·收入与财务工程》2.1 单位经济的四个数:ARPA、毛利、CAC、回本

单位经济不是四个孤立指标,而是一条从 ARPA 到 LTV/CAC 的算术链。本节用 LedgerLite 的 260 家客户、77 美元 ARPA、84.2% 毛利、140 美元 CAC 复算四个数,横向对照 Tidecast 的每份净得与 Nord Studio 的产品线,并给出可运行的 economics.ts。

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

单位经济要回答的问题很具体:多服务一个客户,这门生意是赚钱,还是先烧钱再指望以后赚回来?它不看总收入,也不看估值,只看「一个单位」的收入、成本、获客花费和回收速度。单位算不过来,规模越大亏得越快;单位算得过来,扩张才有意义。

这一节只拆四个数:ARPA、单客户毛利、CAC、回本月数。LTV 和 LTV/CAC 是两个组合比率,会在这里出现,但三种算法的差异留到下一节展开。主案例是 LedgerLite;Tidecast 和 Nord Studio 各用两段做横向对照,说明同一组指标换一种收入形态就会变形。

四个数看起来简单,真正的难点在口径:ARPA 的分母是付费客户,CAC 的分母是新签客户,两者不是同一批人;毛利的分母是收入,回本的分母是月毛利。口径一混,结论就会从「2.2 个月回本」变成「一年半回本」。

四个数各自的定义

先把定义写死。下面每个公式都只使用案例事实表里的原始数字,派生值保留整数美元或一位小数。

指标公式LedgerLite 代入结果
ARPAMRR ÷ 付费客户数$20,020 ÷ 260$77.00/月
单客户月毛利ARPA × 毛利率$77.00 × 84.2%$64.83/月
CAC获客支出 ÷ 新签客户数$16,800 ÷ 120$140.00
回本期CAC ÷ 单客户月毛利$140.00 ÷ $64.832.2 个月
LTV单客户月毛利 ÷ 月流失率$64.83 ÷ 1.5%$4,322
LTV/CACLTV ÷ CAC$4,322 ÷ $14030.9

这张表里有两个容易看漏的点。

第一,ARPA 用的是期末付费客户 260 家,CAC 用的是全年新签 120 家。前者是存量,后者是增量。把 120 家当成 ARPA 的分母,会得到 166.83 美元的假 ARPA;把 260 家当成 CAC 的分母,会得到 64.62 美元的假 CAC。两个错误方向相反,却都可能让单位经济「看起来还行」。

第二,回本期的分母是月毛利,不是月收入。用收入算,LedgerLite 的回本期是 140 ÷ 77 = 1.8 个月,比真实值快 0.4 个月。毛利率越低,这个偏差越大;到 50% 毛利时,用收入算会把回本期低估一半。

LTV 和 LTV/CAC 不是新的原始数,而是前四个数的组合。LTV 把「月毛利」和「流失率」乘成一个客户生命周期内的毛利总额;LTV/CAC 再把它和获客成本放在一起。组合比率方便比较,但也最容易掩盖口径差异,所以下一节会专门拆 LTV 的三种算法。

LedgerLite:把四个数算出来

LedgerLite 的收入侧很干净:260 家付费客户,ARPA 77 美元/月,MRR 20,020 美元,ARR 240,240 美元。月流失率 1.5%,年流失约 16.6%;GRR 83%,NRR 102%。年付客户 30%,月付 70%,年付折扣 15%。这些数字说明 ARPA 是一个混合平均值,不是价目表上的标价。

成本侧,年度变动成本 38,000 美元,其中云 18,000、支付 12,000、支持 8,000;毛利 202,240 美元,毛利率 84.2%。获客侧,年营销费用 16,800 美元,年新签 120 家,CAC 140 美元。把这三段接起来:

单客户月毛利 = $77.00 × 84.2% = $64.83
回本期 = $140.00 ÷ $64.83 = 2.16 → 2.2 个月
LTV = $64.83 ÷ 1.5% = $4,322.00
LTV/CAC = $4,322.00 ÷ $140.00 = 30.87 → 30.9

2.2 个月的回本期意味着:一个客户在第三个月初就已经把获客成本赚回来了。按年看,单客户年毛利是 64.83 × 12 = 777.96 美元,是 CAC 的 5.56 倍。即使这个客户只活一年,获客投入也有充足余量。

LTV 4,322 美元对应的是 1 ÷ 1.5% = 66.7 个月的期望寿命。这个数字看起来很长,但它建立在「月流失率长期保持 1.5%」的假设上;如果流失率升到 3%,LTV 会立刻掉到 2,161 美元,LTV/CAC 从 30.9 掉到 15.4。单位经济不是一张静态成绩单,而是一组对流失率高度敏感的估计。

这里还有一个舍入链要说明。案例事实表把单客户月毛利写成 64.8 美元,再用 64.8 ÷ 1.5% 得到 4,320 美元;代码保留整数美分,先得到 6,483 美分,再除以 1.5%,得到 4,322 美元。两者相差 2 美元,来自中间步骤的舍入,不是公式冲突。LTV/CAC 在两种口径下都四舍五入为 30.9,因为 4,320 ÷ 140 = 30.86,4,322 ÷ 140 = 30.87。

如果把视角换成「新增客户」而不是「平均客户」,还能看到另一个细节。MRR 桥里当月新签 10 家、新增 MRR 780 美元,平均新单价是 78 美元,略高于 77 美元的存量 ARPA。这不影响本节四个数的计算,但提醒我们:ARPA 是存量与增量的混合值,CAC 是增量口径,两者匹配时要意识到这个时间差。

三个案例的横向对照

把同一组公式套到三种收入形态上,会得到一张「看起来能比、其实要小心比」的表。

项LedgerLiteTidecastNord Studio
收入形态订阅买断服务 + 订阅
单位客户·月份客户·月(产品线)
ARPA / 单位净收入$77.00/月$14.39/份$39.00/月
毛利率口径84.2%72.0%(净得率)84.4%(混合)
单位毛利$64.83/月$14.39/份$32.92/月(近似)
CAC / 单位获客成本$140.00$1.25/份不可复算
回本期2.2 个月成交时不可复算
LTV$4,322$14.39(单次)$1,097(产品线)
LTV/CAC30.911.5不可复算

Tidecast 是买断制,单位不是「客户·月」而是「份」。9,600 份销量、138,171 美元净得,每份净收入 14.39 美元;营销 12,000 美元对应每份 1.25 美元,LTV/CAC 是 11.5。这里的「LTV」退化成单次净收入,因为没有续费;回本在成交时完成,平台月结 30 天只影响现金时点,不影响单位经济。它的 72.0% 是净得率,已经扣掉退款 4%、Steam 分成 30% 和官网支付费 5%,但没有扣营销、服务器和工具;若把后三项也扣掉,每份 SDE 是 121,171 ÷ 9,600 = 12.62 美元。拿 72.0% 去和 SaaS 的 84.2% 比,会把「平台分成」误读成「毛利薄」。

Nord Studio 是服务加产品的混合体。产品线 90 家、39 美元/月,ARPA 39 美元;公司整体毛利率 84.4%,用它近似产品线,单客户月毛利是 39 × 84.4% = 32.92 美元;产品月流失 3.0%,LTV 是 32.92 ÷ 3.0% = 1,097 美元。但案例事实表没有单列营销费用,直接成本只有外包 18,000 和工具 6,000,获客主要靠 55% 的转介绍和合伙人时间,所以 CAC 不可复算,回本与 LTV/CAC 也无法判定。这不是数据缺失的小问题,而是混合业务做单位经济的典型障碍:服务线的客户获取成本藏在人力里,产品线的获客又可能搭服务的便车。

三个案例放在一起,真正可比的只有「口径是否一致」,而不是「数值谁高谁低」。LedgerLite 的 30.9 和 Tidecast 的 11.5 都建立在「获客支出可以单独归集」上;Nord Studio 连这个前提都不满足。下一节会把 LTV 的三种算法拆开,说明为什么同一个 LedgerLite 可以算出 4,322、4,688 甚至 4,092 三个不同的 LTV。

四个数的边界

四个数能成立,依赖四个前提。任何一个不成立,数字就要打折扣。

ARPA 是平均值,不是价格。 LedgerLite 有 30% 年付客户享受 15% 折扣,70% 月付;ARPA 77 美元是两类客户的混合。如果年付比例从 30% 升到 50%,在标价不变的情况下 ARPA 会下降,因为年付折扣被更多客户使用。用 ARPA 做预测时,要同时跟踪客户结构。

毛利率的口径必须跟着业务走。 LedgerLite 的变动成本是云、支付、支持;Tidecast 的退款、平台分成、支付费是收入的直接扣减;Nord Studio 的直接成本是外包和工具。三者都叫「毛利」,但成本边界不同。跨案例比较毛利率,先问一句「哪些成本被算进了变动成本」。

CAC 只算现金获客支出,会低估一人公司的真实获客成本。 LedgerLite 的 140 美元只含 16,800 美元营销费用,不含创始人谈客户、写内容、做演示的时间。Tidecast 的 1.25 美元只含 12,000 美元营销,不含 14 个月开发换来的品牌与愿望单积累。时间成本不进 CAC,不代表它不存在;它只是没有出现在这张表里。

回本期是「毛利回收」口径,不是「现金回收」口径。 回本期回答「多久赚回获客成本」,不回答「账上什么时候有钱」。LedgerLite 平均账期 Net 15,Tidecast 平台月结 30 天,Nord Studio 账期 Net 30;这些时点差异会影响现金流,但不改变 2.2 个月这个单位经济结论。现金流口径要等第 3 章和第 8 章。

把这四个前提写进模型,比记住四个数字更重要。获客如何归因、渠道成本如何分摊,见 获客归因与复盘 ;收入结构如何组合,见 收入模型组合 。本节只负责把单位算清楚。

把四个数写成代码

下面的 economics.ts 把四个数和两个比率固化成函数。金额统一用整数美分,比率统一用基点(1% = 100 bps),每个函数返回 Result,输入不合法时给出明确错误,而不是返回 NaN。buildUnitEconomics 一次跑完整条链,适合直接喂给案例数据。

// file: src/unit/economics.ts
// tier: L1
// verify: node --test test/unit/economics.test.ts

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

export type UnitEconomicsInput = {
  mrrCents: Cents;
  customers: number;
  grossMarginBps: Bps;
  marketingCents: Cents;
  newCustomers: number;
  monthlyChurnBps: Bps;
};

export type UnitEconomics = {
  arpaCents: Cents;
  grossProfitPerCustomerCents: Cents;
  cacCents: Cents;
  paybackMonths: number;
  ltvCents: Cents;
  ltvCacRatioBps: Bps;
};

function isCents(value: number): boolean {
  return Number.isInteger(value) && value >= 0;
}

function isBps(value: number): boolean {
  return Number.isInteger(value) && value >= 0 && value <= 10000;
}

/** Monthly average revenue per account, in cents. */
export function arpa(mrrCents: Cents, customers: number): Result<Cents> {
  if (!isCents(mrrCents)) {
    return fail("MRR must be a non-negative integer number of cents");
  }
  if (!Number.isInteger(customers) || customers <= 0) {
    return fail("customers must be a positive integer");
  }
  return ok(Math.round(mrrCents / customers));
}

/** Gross profit contributed by one customer in one month, in cents. */
export function grossProfitPerCustomer(
  arpaCents: Cents,
  grossMarginBps: Bps,
): Result<Cents> {
  if (!isCents(arpaCents)) {
    return fail("ARPA must be a non-negative integer number of cents");
  }
  if (!isBps(grossMarginBps)) {
    return fail("gross margin must be an integer between 0 and 10000 bps");
  }
  return ok(Math.round((arpaCents * grossMarginBps) / 10000));
}

/** Customer acquisition cost: acquisition spend divided by new customers. */
export function cac(
  marketingCents: Cents,
  newCustomers: number,
): Result<Cents> {
  if (!isCents(marketingCents)) {
    return fail("marketing spend must be a non-negative integer number of cents");
  }
  if (!Number.isInteger(newCustomers) || newCustomers <= 0) {
    return fail("new customers must be a positive integer");
  }
  return ok(Math.round(marketingCents / newCustomers));
}

/**
 * Months of gross profit needed to recover CAC, rounded to one decimal.
 * Returns 0 when CAC is zero.
 */
export function paybackMonths(
  cacCents: Cents,
  monthlyGrossProfitCents: Cents,
): Result<number> {
  if (!isCents(cacCents)) {
    return fail("CAC must be a non-negative integer number of cents");
  }
  if (!isCents(monthlyGrossProfitCents)) {
    return fail("monthly gross profit must be a non-negative integer number of cents");
  }
  if (cacCents === 0) {
    return ok(0);
  }
  if (monthlyGrossProfitCents === 0) {
    return fail("monthly gross profit must be positive when CAC is positive");
  }
  return ok(Math.round((cacCents / monthlyGrossProfitCents) * 10) / 10);
}

/**
 * Gross-profit LTV under a constant monthly churn rate.
 * The formula is monthly gross profit divided by monthly churn.
 */
export function ltvFromGrossProfit(
  monthlyGrossProfitCents: Cents,
  monthlyChurnBps: Bps,
): Result<Cents> {
  if (!isCents(monthlyGrossProfitCents)) {
    return fail("monthly gross profit must be a non-negative integer number of cents");
  }
  if (!Number.isInteger(monthlyChurnBps) || monthlyChurnBps <= 0 || monthlyChurnBps > 10000) {
    return fail("monthly churn must be an integer between 1 and 10000 bps");
  }
  return ok(
    Math.round((monthlyGrossProfitCents * 10000) / monthlyChurnBps),
  );
}

/** LTV divided by CAC, expressed in basis points, so 3.0x is 30000 bps. */
export function ltvCacRatio(ltvCents: Cents, cacCents: Cents): Result<Bps> {
  if (!isCents(ltvCents)) {
    return fail("LTV must be a non-negative integer number of cents");
  }
  if (!isCents(cacCents) || cacCents === 0) {
    return fail("CAC must be a positive integer number of cents");
  }
  return ok(Math.round((ltvCents * 10000) / cacCents));
}

/** Compute the four numbers and the two headline ratios in one pass. */
export function buildUnitEconomics(
  input: UnitEconomicsInput,
): Result<UnitEconomics> {
  const arpaResult = arpa(input.mrrCents, input.customers);
  if (!arpaResult.ok) {
    return arpaResult;
  }
  const grossProfitResult = grossProfitPerCustomer(
    arpaResult.value,
    input.grossMarginBps,
  );
  if (!grossProfitResult.ok) {
    return grossProfitResult;
  }
  const cacResult = cac(input.marketingCents, input.newCustomers);
  if (!cacResult.ok) {
    return cacResult;
  }
  const paybackResult = paybackMonths(cacResult.value, grossProfitResult.value);
  if (!paybackResult.ok) {
    return paybackResult;
  }
  const ltvResult = ltvFromGrossProfit(
    grossProfitResult.value,
    input.monthlyChurnBps,
  );
  if (!ltvResult.ok) {
    return ltvResult;
  }
  const ratioResult = ltvCacRatio(ltvResult.value, cacResult.value);
  if (!ratioResult.ok) {
    return ratioResult;
  }
  return ok({
    arpaCents: arpaResult.value,
    grossProfitPerCustomerCents: grossProfitResult.value,
    cacCents: cacResult.value,
    paybackMonths: paybackResult.value,
    ltvCents: ltvResult.value,
    ltvCacRatioBps: ratioResult.value,
  });
}

代码里有三个设计选择值得说明。

第一,金额用整数美分,除法只在最后一步舍入。grossProfitPerCustomer 先把 7,700 × 8,420 ÷ 10,000 舍入成 6,483 美分,再由它算回本和 LTV;这样每个中间值都能和案例事实表对账,不会出现「不同函数给出不同小数」的情况。

第二,paybackMonths 返回一位小数,ltvCacRatio 返回基点。前者适合写「2.2 个月」,后者适合做阈值判断(2.3 节会用到 3.0x = 30,000 bps)。单位不同是刻意的:回本期是时间,比率是倍数,混在一起容易读错。

第三,buildUnitEconomics 在零流失率时返回失败,而不是返回 Infinity。零流失意味着 LTV 没有有限值,继续算下去只会得到一串无意义的数字;显式失败比隐式无穷更适合放进报表。输入校验也让「CAC 为 0」「客户数为 0」这类边界在测试里可复现。

小结

  • 单位经济的四个原始数是 ARPA、单客户月毛利、CAC、回本期;LTV 和 LTV/CAC 是组合比率。
  • LedgerLite:ARPA 77 美元/月,毛利率 84.2%,单客户月毛利 64.83 美元,CAC 140 美元,回本 2.2 个月,LTV 4,322 美元,LTV/CAC 30.9。
  • ARPA 的分母是 260 家付费客户,CAC 的分母是 120 家年新签;混用分母会得到假数。
  • 回本期要用月毛利做分母;用月收入会把 2.2 个月算成 1.8 个月。
  • Tidecast 的每份净收入是 14.39 美元,每份获客成本 1.25 美元,LTV/CAC 11.5;买断制没有续费,LTV 退化成单次净收入。
  • Nord Studio 的产品线 ARPA 39 美元、LTV 约 1,097 美元,但案例没有单列营销费用,CAC 与回本不可复算。
  • 四个数依赖四个前提:ARPA 是混合值、毛利率口径随业务变、CAC 不含创始人时间、回本期不等于现金回收期。
  • LTV 对流失率高度敏感:LedgerLite 的月流失从 1.5% 升到 3.0%,LTV 从 4,322 掉到 2,161。

阅读导航:上一节:1.3 生命周期转型:从服务到产品、从买断到订阅 · 下一节:2.2 LTV 的三种算法与适用边界 。

常见错误与边界

  1. 用同一批客户做 ARPA 和 CAC 的分母。 ARPA 用期末 260 家,CAC 用年新签 120 家;混用会同时算错两个数。
  2. 用收入代替毛利算回本期。 140 ÷ 77 = 1.8 个月是收入口径;140 ÷ 64.83 = 2.2 个月才是毛利口径。
  3. 把 ARPA 当成价目表价格。 77 美元混合了 70% 月付和 30% 年付(年付折扣 15%),客户结构变化会改变 ARPA。
  4. 跨案例直接比毛利率。 LedgerLite 的 84.2%、Tidecast 的 72.0%、Nord Studio 的 84.4% 成本边界不同,不能放在一条横线上排名。
  5. 忘记 CAC 不含创始人时间。 一人公司的营销费用只是获客成本的一部分;用 140 美元做决策时,要意识到真实投入更高。
  6. 把回本期当成现金回收期。 回本期是毛利口径;Net 15、平台月结 30 天、Net 30 都会让现金晚到。
  7. 用单月数据外推全年。 MRR 桥只描述当月;CAC 的 120 家是全年新签,两个窗口不同。
  8. 把 LTV 当成承诺值。 LTV 4,322 美元依赖 1.5% 的月流失率;流失率翻倍,LTV 减半。
  9. 在买断制里硬套订阅公式。 Tidecast 没有月流失,LTV 只能退化成单次净收入 14.39 美元;硬套「毛利 ÷ 流失率」会除以零或编造流失率。
  10. 在混合业务里假设 CAC 可单独归集。 Nord Studio 的获客靠 55% 转介绍和合伙人时间,营销费用没有单列;此时「不可复算」比拍一个数更诚实。

四个数是单位经济的地基,但不是全部。下一节会把 LTV 拆成三种算法,说明同一组原始数为什么会算出不同的生命周期价值;再下一节会把四个数变成五个失效信号,用来判断一组单位经济到底该继续、观察还是停下。第 3 章会把它们接进利润表,看单位经济如何变成年度利润。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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