《独立开发者的商业指南·资本与退出》7.2 现金、票据与 earn-out

现金、卖方票据与 earn-out 是三种不同确定性的钱。本节把 Tidecast 的 248,708 美元与 LedgerLite 的 768,768 美元都拆成 70% 现金加 30% earn-out,按 12% 教学折现率算到手时间与现值,并演示计量口径从 95% 变成 94% 时卖方少拿多少。

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

报价是交易里最响的数字,也是最少说明问题的数字。同样是 248,708 美元,全部现金交割和 70% 现金加 30% earn-out,卖方在交割日拿到的钱差 74,612 美元;如果 earn-out 的计量口径在一年后把收入算低一点,最终到手还会再少一截。对价结构不是付款方式的细节,它决定了卖方承担多少未来风险、买方保留多少调整价格的空间。

这一节把对价拆成三种工具:现金、卖方票据和 earn-out。现金是交割日就能花的钱;卖方票据是卖方借钱给买方,按月收回本金和利息;earn-out 是卖方把一部分价格押在未来的经营指标上。三种工具的确定性、时间和风险不同,不能用一个「总价」比较。所有折现率、利率、期限和里程碑参数都是教学假设,不构成投资、法律或税务意见。

三种对价,三种确定性

工具何时拿到确定性主要风险卖方要问
现金交割日最高交割条件、营运资金调整交割日净额是多少
卖方票据交割后按月或到期中买方违约、清偿顺序、担保利率、期限、有无担保
earn-out里程碑达成后最低计量口径、经营控制、支付上限谁控制指标、怎么算、封顶多少

现金的确定性最高,但不是「报价乘以现金比例」这么简单。交割日实际到账要扣掉托管、营运资金调整和交割费用;托管通常从现金里切出来,比例和期限见 7.3 陈述保证、赔偿与托管 。本节先把托管放在一边,只看现金、票据和 earn-out 三者的时间价值。

卖方票据的本质是信贷。卖方把一部分价格借给买方,换取利息和分期还款;如果买方违约,卖方要从担保、清偿顺序和谈判里找回损失。票据的利率高于银行贷款,不是因为买方慷慨,而是因为卖方承担了信用风险。票据的期限越长、担保越弱,卖方越应该用更高的折现率看它。

earn-out 的本质是期权。卖方放弃一部分确定的价格,换取未来经营达标后的补偿;买方用这部分价格对冲「买贵了」的风险。earn-out 的争议通常不在比例,而在计量:收入按什么口径算、退款和折扣怎么处理、买方能不能通过削减营销让指标刚好差一点、支付有没有上限。比例是谈判桌上的数字,计量是交割后一年的战场。

70/30 的到手时间与现值

先看最简单的结构:70% 现金,30% earn-out,12 个月后支付,折现率按 12% 年化教学假设。Tidecast 的案例事实表只给出 248,708 美元的整体出售锚,没有规定对价结构;为了横向比较,这里给它套用同一组 70/30 教学假设。LedgerLite 的案例结构本来就是 70% 现金加 30% earn-out,12 个月,ARR 不低于基期 95% 时全额支付。

案例报价交割日现金12 个月 earn-out全额到手时间现值时间价值折损
Tidecast248,708174,09674,612第 12 个月240,3108,398
LedgerLite768,768538,138230,630第 12 个月742,81025,958

Tidecast 的 74,612 美元 earn-out 按 12% 折现到交割日,现值是 66,215 美元;加上 174,096 美元现金,总现值 240,310 美元,比报价少 8,398 美元,约 3.4%。LedgerLite 的 230,630 美元 earn-out 现值是 204,673 美元;加上 538,138 美元现金,总现值 742,810 美元,比报价少 25,958 美元,同样是 3.4%。

两个案例的百分比折损一样,因为结构、期限和折现率一样;绝对金额不同,因为报价不同。这就是为什么「30% earn-out」不能直接和「30% 折扣」比较:前者是时间价值加履约风险,后者是价格直接下调。如果 earn-out 最终没有全额支付,卖方的损失会远大于 3.4%。

到手时间也要分开看。现金在交割日到账;earn-out 在第 12 个月支付,前提是指标达标;如果指标不达标,支付可能延后、减少或归零。报价上的「全额到手时间」是第 12 个月,但这是最好情况,不是承诺。卖方在排现金流时,应该把 earn-out 当成第 12 个月才可能到账的钱,而不是交割日就能用的钱。

卖方票据:利率与期限

卖方票据把一部分价格变成债权。它比 earn-out 确定,因为还款义务不依赖经营指标;它又比现金不确定,因为买方可能违约。票据的利率、期限和担保决定它值多少。一个常见的教学结构是 60% 现金、15% 卖方票据、25% earn-out;票据年利率 8%,期限 24 个月,本金按月等额摊还,利息按未偿余额计算。

以 LedgerLite 的 768,768 美元为例,现金是 461,261 美元,票据本金 115,315 美元,earn-out 目标 192,192 美元。票据 24 个月、8% 年利率,总利息 9,610 美元;第一个月还款 5,574 美元,最后一个月 4,837 美元。按 12% 折现率,票据现值 110,900 美元,低于本金 115,315 美元;整个对价的现值是 742,722 美元,名义总额 778,378 美元,时间价值折损 35,656 美元。

组成金额时间现值说明
现金461,261交割日461,261未扣托管与调整
卖方票据本金115,31524 个月110,9008% 年利率,按月摊还
卖方票据利息9,61024 个月含在上行按未偿余额计算
earn-out192,19212 个月170,56195% 指标达标时全额
合计778,378742,722名义总额高于报价

这张表说明两件事。第一,票据利率低于卖方折现率时,票据的现值低于本金;8% 的票面利率覆盖不了 12% 的机会成本,卖方拿到的利息不足以补偿时间和信用风险。第二,名义总额 778,378 美元高于报价 768,768 美元,因为票据利息和 earn-out 被加进了「总价」;但现值仍然低于报价。名义总额和现值不是一回事,谈判时要把两者分开。

票据的期限也会改变现值。24 个月比 12 个月多一年时间价值,但也多一年信用暴露;如果买方在第 18 个月违约,卖方能收回多少取决于担保和清偿顺序。票据如果有资产抵押、母公司担保或银行保函,风险会低一些;如果是无担保的普通债权,卖方应该用更高的折现率,或者要求更短的期限。票据的利率不是越高越好:过高的利率会把买方的现金流压紧,反而提高违约概率。

earn-out 的计量争议

earn-out 的支付通常由一个或几个指标触发。LedgerLite 的案例是 ARR 不低于基期 95% 时全额支付 30%;Tidecast 的教学版本可以用首 12 个月净得不低于基期 90% 作为指标。指标本身只是一句话,真正决定支付的是计量口径:收入按总额还是净额、退款和折扣算不算、年付客户怎么确认、汇率用哪一天、关联方收入算不算、买方的营销和定价决策算不算。

为了把争议量化,假设 earn-out 采用线性支付:指标低于 80% 不支付,达到 95% 支付 100%,中间按比例。LedgerLite 的 230,630 美元 earn-out 在不同指标下的支付如下。

实际 ARR / 基期支付比例支付金额比全额少
95.0%100.0%230,6300
94.0%93.3%215,24715,383
90.0%66.7%153,76176,869
80.0%0%0230,630

指标从 95% 掉到 94%,支付比例从 100% 掉到 93.3%,卖方少拿 15,383 美元。这个差距不是来自收入真的少了 1%,而是来自「95% 是全额支付的阈值」这个结构:阈值附近,1 个百分点的指标变化会被放大成 6.7 个百分点的支付变化。如果合同写的是「达到 95% 才支付,否则为零」,94.9% 和 94.0% 的结果一样,都是零;线性支付至少让卖方在阈值以下还能拿一部分。

Tidecast 的教学版本更能说明口径问题。earn-out 是 74,612 美元,指标是首 12 个月净得不低于 138,171 美元的 90%,即 124,354 美元;低于 80% 不支付,达到 90% 支付 100%。如果买方把退款、平台分成或促销折扣算进「净得」的方式和卖方不同,实际指标从 90% 掉到 88%,支付比例变成 80%,卖方拿到 59,690 美元,少拿 14,922 美元。收入只差 2 个百分点,支付差 20 个百分点。

计量争议的根源是控制权。交割后,买方控制定价、营销、产品路线和收入确认;卖方只能看报表。如果 earn-out 写得太粗,买方可以通过削减营销、推迟新版本或改变打包方式,让指标刚好落在阈值以下;如果写得太细,双方会在每个月的报表上吵架。可执行的 earn-out 通常要写清四件事:指标定义、计算期间、买方的经营承诺、争议解决方式。指标定义包括收入口径、退款、折扣、汇率和关联方;经营承诺包括预算、营销投入和产品维护;争议解决包括审计权和专家裁定。

写成代码:earn-out.ts

下面的 earn-out.ts 把三种对价写成一个函数。earnOutPayout 处理 earn-out 的地板、阈值和上限:低于地板支付零,达到阈值支付上限,中间按线性比例。priceConsideration 把报价拆成现金、票据和 earn-out,计算票据利息、每月还款、各组成部分的现值和全额到手时间。

// file: src/capital/earn-out.ts
// tier: L1
// verify: node --test test/capital/earn-out.test.ts

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

export type EarnOutTerms = {
  floorBps: Bps;
  thresholdBps: Bps;
  capBps: Bps;
};

export type EarnOutPayout = {
  payoutBps: Bps;
  payoutCents: Cents;
  atCap: boolean;
  belowFloor: boolean;
};

export type ConsiderationInput = {
  priceCents: Cents;
  cashBps: Bps;
  noteBps: Bps;
  earnOutBps: Bps;
  noteAnnualRateBps: Bps;
  noteTermMonths: number;
  earnOutTermMonths: number;
  annualDiscountRateBps: Bps;
  earnOut: EarnOutTerms;
  earnOutActualBps: Bps;
};

export type Consideration = {
  priceCents: Cents;
  cashCents: Cents;
  notePrincipalCents: Cents;
  earnOutTargetCents: Cents;
  noteInterestCents: Cents;
  earnOutPayoutCents: Cents;
  nominalCents: Cents;
  presentValueCents: Cents;
  cashPresentValueCents: Cents;
  notePresentValueCents: Cents;
  earnOutPresentValueCents: Cents;
  timeValueDiscountCents: Cents;
  monthsToFullPayment: number;
  noteFirstPaymentCents: Cents;
  noteLastPaymentCents: Cents;
};

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

/**
 * Earn-out payout follows a linear ramp: nothing below the floor, the full cap
 * at the threshold, and a proportional share in between. Real contracts often
 * use a cliff instead of a ramp; the model makes the measurement dispute
 * visible either way. Floor, threshold and cap are teaching assumptions.
 */
export function earnOutPayout(
  targetCents: Cents,
  terms: EarnOutTerms,
  actualBps: Bps,
): Result<EarnOutPayout> {
  if (!Number.isInteger(targetCents) || targetCents < 0) {
    return fail("earn-out target must be a non-negative integer amount");
  }
  if (
    !validBps(terms.floorBps) ||
    !validBps(terms.thresholdBps) ||
    !validBps(terms.capBps)
  ) {
    return fail("earn-out terms must be between 0 and 10000 basis points");
  }
  if (terms.floorBps >= terms.thresholdBps) {
    return fail("earn-out floor must be below the threshold");
  }
  if (!Number.isInteger(actualBps) || actualBps < 0) {
    return fail("actual metric must be a non-negative integer in basis points");
  }

  if (actualBps <= terms.floorBps) {
    return ok({
      payoutBps: 0,
      payoutCents: 0,
      atCap: false,
      belowFloor: true,
    });
  }
  if (actualBps >= terms.thresholdBps) {
    return ok({
      payoutBps: terms.capBps,
      payoutCents: applyRate(targetCents, terms.capBps),
      atCap: true,
      belowFloor: false,
    });
  }

  const payoutBps = Math.round(
    (terms.capBps * (actualBps - terms.floorBps)) /
      (terms.thresholdBps - terms.floorBps),
  );
  return ok({
    payoutBps,
    payoutCents: applyRate(targetCents, payoutBps),
    atCap: false,
    belowFloor: false,
  });
}

/**
 * Splits the headline price into cash at close, a seller note and an earn-out.
 * The note amortizes principal in equal monthly installments and pays interest
 * on the outstanding balance. Present value discounts every monthly payment at
 * the teaching annual rate. This is arithmetic, not a valuation opinion.
 */
export function priceConsideration(
  input: ConsiderationInput,
): Result<Consideration> {
  if (!Number.isInteger(input.priceCents) || input.priceCents <= 0) {
    return fail("price must be a positive integer amount in cents");
  }
  if (
    !validBps(input.cashBps) ||
    !validBps(input.noteBps) ||
    !validBps(input.earnOutBps)
  ) {
    return fail("consideration shares must be between 0 and 10000 bps");
  }
  if (input.cashBps + input.noteBps + input.earnOutBps !== 10000) {
    return fail("cash, note and earn-out shares must add up to 100%");
  }
  if (!validBps(input.noteAnnualRateBps)) {
    return fail("note rate must be between 0 and 10000 bps");
  }
  if (!validBps(input.annualDiscountRateBps)) {
    return fail("discount rate must be between 0 and 10000 bps");
  }
  if (
    !Number.isInteger(input.noteTermMonths) ||
    input.noteTermMonths <= 0
  ) {
    return fail("note term must be a positive integer month count");
  }
  if (
    !Number.isInteger(input.earnOutTermMonths) ||
    input.earnOutTermMonths <= 0
  ) {
    return fail("earn-out term must be a positive integer month count");
  }

  const cashCents = applyRate(input.priceCents, input.cashBps);
  const notePrincipalCents = applyRate(input.priceCents, input.noteBps);
  const earnOutTargetCents = applyRate(input.priceCents, input.earnOutBps);

  const payout = earnOutPayout(
    earnOutTargetCents,
    input.earnOut,
    input.earnOutActualBps,
  );
  if (!payout.ok) {
    return fail(payout.error);
  }

  const monthlyPrincipal = Math.floor(
    notePrincipalCents / input.noteTermMonths,
  );
  const lastPrincipal =
    notePrincipalCents - monthlyPrincipal * (input.noteTermMonths - 1);
  const monthlyNoteRate = input.noteAnnualRateBps / 10000 / 12;
  const monthlyDiscountRate = input.annualDiscountRateBps / 10000 / 12;

  let outstanding = notePrincipalCents;
  let noteInterestCents = 0;
  let notePresentValueCents = 0;
  let firstPaymentCents = 0;
  let lastPaymentCents = 0;

  for (let month = 1; month <= input.noteTermMonths; month += 1) {
    const principal =
      month < input.noteTermMonths ? monthlyPrincipal : lastPrincipal;
    const interest = Math.round(outstanding * monthlyNoteRate);
    const payment = principal + interest;
    if (month === 1) {
      firstPaymentCents = payment;
    }
    lastPaymentCents = payment;
    noteInterestCents += interest;
    notePresentValueCents +=
      payment / Math.pow(1 + monthlyDiscountRate, month);
    outstanding -= principal;
  }

  const earnOutPresentValueCents = Math.round(
    payout.value.payoutCents /
      Math.pow(1 + monthlyDiscountRate, input.earnOutTermMonths),
  );
  const roundedNotePresentValueCents = Math.round(notePresentValueCents);
  const presentValueCents =
    cashCents + roundedNotePresentValueCents + earnOutPresentValueCents;
  const nominalCents =
    cashCents +
    notePrincipalCents +
    noteInterestCents +
    payout.value.payoutCents;

  return ok({
    priceCents: input.priceCents,
    cashCents,
    notePrincipalCents,
    earnOutTargetCents,
    noteInterestCents,
    earnOutPayoutCents: payout.value.payoutCents,
    nominalCents,
    presentValueCents,
    cashPresentValueCents: cashCents,
    notePresentValueCents: roundedNotePresentValueCents,
    earnOutPresentValueCents,
    timeValueDiscountCents: nominalCents - presentValueCents,
    monthsToFullPayment: Math.max(
      input.noteTermMonths,
      input.earnOutTermMonths,
    ),
    noteFirstPaymentCents: firstPaymentCents,
    noteLastPaymentCents: lastPaymentCents,
  });
}

把 Tidecast 的 70/30 代进去,现金 174,096 美元,earn-out 74,612 美元,现值 240,310 美元,时间价值折损 8,398 美元,全额到手在第 12 个月。LedgerLite 的 70/30 是现金 538,138 美元、earn-out 230,630 美元、现值 742,810 美元、折损 25,958 美元。60/15/25 的票据结构是现金 461,261 美元、票据本金 115,315 美元、earn-out 192,192 美元;票据利息 9,610 美元,现值合计 742,722 美元,名义总额 778,378 美元,全额到手在第 24 个月。

代码里的折现率、票据利率、期限和 earn-out 参数都是输入,不是写死的行业标准。读者可以用自己的机会成本替换 12%,用自己的票据利率替换 8%,看现值怎么变。代码只做算术,不做信用判断;票据能不能收回、earn-out 能不能达标,仍然要看买方的信用、合同条款和经营控制。

小结

  • 报价不等于到手:现金、卖方票据和 earn-out 的确定性、时间和风险不同,要分开算。
  • Tidecast 的 70/30 结构是现金 174,096 美元加 12 个月 earn-out 74,612 美元,按 12% 折现现值 240,310 美元,折损 8,398 美元。
  • LedgerLite 的 70/30 结构是现金 538,138 美元加 12 个月 earn-out 230,630 美元,现值 742,810 美元,折损 25,958 美元。
  • 卖方票据是信贷:60/15/25 结构下,115,315 美元票据、8% 年利率、24 个月,总利息 9,610 美元,现值 110,900 美元。
  • 名义总额 778,378 美元高于报价 768,768 美元,但现值只有 742,722 美元;名义和现值要分开看。
  • earn-out 的争议在计量:LedgerLite 的指标从 95% 掉到 94%,支付从 230,630 美元降到 215,247 美元,少拿 15,383 美元。
  • Tidecast 的指标从 90% 掉到 88%,支付从 74,612 美元降到 59,690 美元,少拿 14,922 美元。
  • 折现率、票据利率、期限和里程碑参数都是教学假设;代码只做算术,不做信用判断。

阅读导航:上一节:7.1 资产收购与股权收购 · 下一节:7.3 陈述保证、赔偿与托管 。

常见错误与边界

  1. 把报价当到手。 70% 现金加 30% earn-out 的报价,交割日只拿到 70%;剩下的 30% 要等指标达标,还不一定全额。
  2. 把 earn-out 当折扣。 earn-out 的 30% 不是价格下调 30%,而是时间价值加履约风险;折现后的损失可能远大于或小于 30%。
  3. 忽略票据的信用风险。 票据利率高于银行贷款,是因为卖方承担信用风险;无担保、无清偿优先级的票据不能按面值算。
  4. 用名义总额比较结构。 票据利息会把名义总额推高,但现值可能更低;谈判要看现值,不是看总价。
  5. 把票据利率定得过高。 利率越高,买方现金流越紧,违约概率越高;票据的利率要和买方的偿付能力一起看。
  6. earn-out 只写比例,不写口径。 收入按总额还是净额、退款和折扣怎么算、汇率用哪一天,都会改变支付。
  7. 忽略经营控制。 交割后买方控制定价和营销;没有经营承诺和审计权,earn-out 可以被「管理」到阈值以下。
  8. 把阈值当成线性。 如果合同是「达到才支付」,94.9% 和 94.0% 都可能是零;线性支付和 cliff 支付的差别要在谈判时写清。
  9. 忘记托管。 托管通常从现金里切出来,比例和期限会影响交割日净额;本节把托管留到 7.3,不代表它可以忽略。
  10. 把模型当信用判断。 代码只算现值,不算买方会不会违约、指标会不会达标;这些要靠合同条款和尽调。

本节的边界是「现金、卖方票据与 earn-out 的对价结构」:只讨论比例、利率、期限、里程碑、计量口径、支付上限和现值,不展开托管、赔偿上限或交割清单。所有折现率、利率、期限和里程碑参数都是教学假设,不构成投资、法律或税务意见。托管与赔偿见 7.3 陈述保证、赔偿与托管 ,交割资金流见 8.1 交割清单与资金流 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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