《独立开发者的商业指南·收入与财务工程》12.1 尽调清单:财务、客户、合同

买方尽调不是查账,而是验证收入明年还在不在。本节以 Nord Studio 的服务收入与合伙人依赖为反面,以 LedgerLite 的 MRR 桥与三方对账为正面,按收入、成本、现金、合同、税务、主体六组列出具体问题、应备材料与缺失后果,并给出可运行的清单完成度模型。

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

财务尽调(due diligence)不是审计的简化版,而是买方在签约前回答三个问题:账上的收入是不是真的、明年还在不在、换一个所有者还能不能收。审计看的是报表是否符合准则,尽调看的是这笔交易值不值得做。两者的证据要求不同:审计可以抽样,尽调会追到每一份合同、每一笔回款和每一个平台结算周期。

Nord Studio 是这一章的主案例。它的年收入是 154,320 美元,其中咨询服务 112,200 美元、产品订阅 42,120 美元,服务收入占 72.7%;前 3 家客户合计 48%;两位合伙人各持 50%,没有期权池,也没有书面退出条款。这些数字本身不构成欺诈,但它们决定了买方买到的不是一条稳定的收入曲线,而是几份可能随人走的合同。LedgerLite 作为对照:260 家付费客户,MRR 20,020 美元,MRR 桥逐月可还原,递延收入 22,000 美元与订阅系统一致,应收 12,000 美元约等于 18 天收入。

Tidecast 只在平台与 IP 条款处出现,因为它的问题不是财务报表,而是渠道和授权。

买方到底在验证什么

买方不会按你的科目表提问,而是按风险提问。把问题归到四层,清单才不会变成一张无休止的问卷。

验证层买方的问题需要的证据Nord Studio 的答案LedgerLite 的答案
真实性这笔收入真的发生过吗合同、发票、银行回款、平台结算单咨询按小时计费,需工时单与回款对应订阅系统导出与银行入账逐月对上
可持续性明年还会重复吗MRR 桥、续约率、流失率、客户集中度咨询续约率 70%,产品月流失 3.0%月流失 1.5%,NRR 102%
可转移性换老板后客户还付钱吗合同转让条款、交付依赖、IP 归属服务依赖两位合伙人,合同可转移性差标准订阅协议,交付不依赖个人
合规性有没有未披露的义务税表、DPA、担保、诉讼、关联交易英国 Ltd,VAT 20%,E&O 500K新加坡 Pte Ltd,GST 9%,网络安全险 1M

四层里,真实性最容易补,可持续性需要时间,可转移性最难改,合规性最容易在交割前爆雷。买方通常先做真实性核对,再花最多时间在可持续性和可转移性上。经营者准备材料时,也应该按这个顺序分配精力,而不是把三年银行流水扫描完就以为尽调结束了。

Nord 的困境在第三层。它的收入是真实的,咨询合同也真实存在,但客户关系跟着两位合伙人走;合同里如果没有转让条款,买方交割后还需要逐家取得客户同意。买方会把这种不确定性直接折进价格,或者要求把一部分对价放进托管和 earn-out。LedgerLite 的优势也不是增长更快,而是它的收入可以被一个不认识的买方接手。

收入与成本:先证明钱是真的,再证明钱会留下

收入组的问题最多,因为收入是估值的分子。下面四问几乎每个买方都会问,区别只是问得多细。

组内问题买方会要什么你手上应有什么缺了会怎样
收入构成按产品线、按月、按合同类型的收入明细12 个月收入明细表 + 发票清单一次性收入被当成经常性收入,倍数被下调
经常性收入MRR 桥:期初 + 新签 + 扩张 − 收缩 − 流失 = 期末月度 MRR 表与订阅系统导出买方自己重算,任何对不上都按最保守口径处理
客户集中前十大客户名单、合同到期日、续约沟通记录客户收入表 + 合同台账买方假设最坏情况,集中度直接进入折价
递延收入递延余额、确认政策、与系统余额的调节表订阅系统导出 + 会计政策说明收入确认被重述,历史可比性归零

LedgerLite 的 MRR 桥是一个可以直接拿给买方看的样板:期初 19,280 美元,新签 780 美元(10 家 × 78 美元),扩张 310 美元,收缩 60 美元,流失 290 美元,期末 20,020 美元。五项能相加,买方就能验证每一个月的变动来自哪里。Nord 没有订阅式的 MRR 桥,因为它的收入大部分来自按小时计费的咨询;它需要的是另一种桥:期初在手合同 + 新签工时 − 完成工时 − 流失客户 = 期末在手合同。形式不同,逻辑一样。

成本组的问题少一些,但更容易出现口径争议。

组内问题买方会要什么你手上应有什么缺了会怎样
变动成本云、支付、支持等直接成本的归集与分摊供应商账单 + 分摊表毛利率被高估,SDE 虚高
外包与工具外包合同、发票、订阅记录合同台账 + 付款记录未披露的长期承诺变成交割后负债
所有者薪酬工资、分红、个人费用与公司费用的边界薪酬说明 + 银行流水SDE 不可比,买方会按自己的口径重算
一次性费用设备、预付、资本化项目的处理固定资产表 + 摊销政策当期利润被美化,后续期间被压低

Nord 的 SDE 是 34,320 美元,计算方式是毛利 130,320 美元减去两位合伙人的工资 96,000 美元。LedgerLite 的 SDE 是 130,240 美元,已经扣除了 60,000 美元的创始人薪酬。把这两个数直接放在一起比较会误导:一个是两人团队扣完工资后的剩余,另一个是一人公司扣完创始人薪酬后的剩余。买方一定会先拉平口径,再谈倍数。准备材料时主动写清「SDE 是否已扣所有者薪酬」,比等买方来问更有利。

现金与合同:六组问题里的第三、第四组

利润表可以修饰,银行流水很难。现金组的核心不是余额,而是每一笔余额能不能解释。

组内问题买方会要什么你手上应有什么缺了会怎样
银行流水最近 12 个月全部账户的对账单银行原始 PDF + 账户清单账外收入或股东借款无法解释
应收账龄按客户、按账期的应收明细与催收记录账龄表 + 催收邮件逾期被低估,营运资金调整对你不利
递延与预收未履约义务的余额与履约计划订阅系统导出 + 合同条款交割后仍需提供服务,买方要求预留现金
借款与担保贷款、股东借款、对外担保、租赁借款合同 + 还款计划未披露负债直接减少股权价值

LedgerLite 的应收是 12,000 美元,约等于 18 天收入,逾期 30 天以上占应收 6%,坏账准备 1%。这组数字不大,但每一项都能在账龄表和银行流水里找到对应。Nord 的应收占年收入 7.3%,逾期率 8%,坏账准备 2%;账期是 Net 30,比 LedgerLite 的 Net 15 更长。账期越长,交割前的应收越容易变成争议:买方会问哪些发票已经催过、哪些客户习惯性拖延、逾期部分是否已经计入收入。

合同组是 Nord 最薄的一环。

组内问题买方会要什么你手上应有什么缺了会怎样
客户合同完整签署版、期限、续约、终止、转让条款合同台账 + 签署版 PDF无法确认交割后收入是否继续
平台与渠道平台协议、结算单、分成与退款条款平台后台导出 + 协议渠道政策变化无法量化
供应商合同云、支付、外包的期限与退出条款合同 + 账单 + 迁移方案关键依赖无法替换,交割后成本上升
IP 与授权商标、代码、素材、开源许可证、外包转让IP 清单 + 转让协议买方买到的资产存在权属瑕疵

LedgerLite 的合同风险相对可控:企业订阅协议与 DPA 是标准文本,责任上限是 12 个月费用,SLA 99.5% 对应 10% 抵扣,云占关键依赖 60%、支付占 25%,都有替代方案。Nord 的合同是咨询合同与产品条款,责任上限是合同金额的 100%,SLA 99.0% 对应 5% 抵扣,E&O 保险 500K 美元;转介绍占获客的 55%。真正的难点不是条款本身,而是「合同能不能转给买方」。如果合同没有 assignment 或 change-of-control 条款,买方需要逐家取得同意,任何一家拒绝都会让收入预测失效。

Tidecast 在这一组提供另一个极端:Steam 占销量 80%,平台协议决定结算、退款和分成;音乐是买断授权,不得转授权。买方如果计划整体收购,会先确认平台协议是否允许主体变更;如果计划做 IP 授权,则会先确认音乐授权能不能覆盖新的使用方式。平台与 IP 的问题不属于财务模型,但会直接改变交易结构。

税务与主体:最后两组最容易被拖延

税务问题通常不会让交易立刻失败,但会让交割推迟,而推迟本身有成本。

组内问题买方会要什么你手上应有什么缺了会怎样
所得税最近三年的申报表与完税凭证税局回执 + 财务报表申报收入与账面收入不一致,需要重建
流转税GST、VAT、销售税的注册与申报注册证明 + 申报记录漏报期间被要求补税与滞纳金
跨境预提税、OSS、常设机构判断跨境合同 + 预提凭证跨境付款被双重征税或扣缴
税费计提期末应交税费与递延税项税务计提表交割后税负由买方承担,价格被下调

LedgerLite 的所得税示例是 17% × 70,240 = 11,941 美元,GST 9%,海外客户需要判断 GST 注册门槛。Nord 是英国 Ltd,VAT 20%,公司税 25%,示例税负 8,580 美元。Tidecast 是美国 Wyoming LLC,销售税由平台代收,跨境需要 W-8BEN 与协定预提,示例税负 29,081 美元。这些税率与金额是教学设定,真实交易必须按当期法规核对,不能把这里的数字当成税务结论。

主体组的问题看起来最行政,却经常决定交易能不能签字。

组内问题买方会要什么你手上应有什么缺了会怎样
股权结构股东名册、股权比例、期权池、退出条款公司章程 + 股东协议交割需要全体股东同意,任何一人可阻断
主体存续注册证、年检、良好存续证明公司注册处文件主体异常导致合同与账户无法变更
关联方关联交易清单、资金往来、担保关联方台账 + 银行流水未披露关联交易变成陈述与保证的违约
诉讼与担保诉讼、仲裁、对外担保、知识产权争议法务清单 + 律师函交割后出现索赔,托管被扣

Nord 的股权结构是两位合伙人各 50%,没有期权池,没有书面退出条款。这个结构对买方意味着两件事:第一,任何股权交易都需要两人一致同意;第二,如果其中一人退出,另一半的买断价格没有合同依据。案例事实表给出的退出报价是 50% × 4.5 × SDE = 77,220 美元,分 24 个月支付;这个数字是教学示例,不是估值结论。买方看到这样的结构,会把「合伙人继续留任」写进交易条件,而不是假设它自然发生。

把清单变成可交付物

清单只有落成文件和责任人才有意义。一个可交付的尽调项应该满足三个条件:指向一份具体文件、能由一个责任人解释、在买家提问前就已经在数据室里。口头说明、截图和「我回头找一下」都不算完成。

下面的 dd.ts 把六组问题固化成模型。createChecklist 负责校验分组、编号和证据提示;completion 接收已完成项,返回总体完成度、必备项完成度、每组统计和缺口清单;缺口按「必备优先、编号其次」排序,方便直接生成待办。金额和比率不进入这个模型,它只回答「材料齐不齐」。

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

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

export type DdGroup =
  | "revenue"
  | "cost"
  | "cash"
  | "contracts"
  | "tax"
  | "entity";

export const DD_GROUPS: DdGroup[] = [
  "revenue",
  "cost",
  "cash",
  "contracts",
  "tax",
  "entity",
];

export type DdItem = {
  id: string;
  group: DdGroup;
  label: string;
  evidence: string;
  required: boolean;
};

export type GroupCompletion = {
  group: DdGroup;
  total: number;
  done: number;
  requiredTotal: number;
  requiredDone: number;
  completionBps: Bps;
};

export type DdCompletion = {
  total: number;
  done: number;
  requiredTotal: number;
  requiredDone: number;
  completionBps: Bps;
  requiredBps: Bps;
  groups: GroupCompletion[];
  gaps: DdItem[];
};

function isGroup(value: string): value is DdGroup {
  return (DD_GROUPS as string[]).includes(value);
}

export function createChecklist(items: DdItem[]): Result<DdItem[]> {
  if (items.length === 0) {
    return fail("checklist must not be empty");
  }
  const seen = new Set<string>();
  const copy: DdItem[] = [];
  for (const item of items) {
    if (item.id.trim() === "") {
      return fail("item id must not be empty");
    }
    if (seen.has(item.id)) {
      return fail(`duplicate item id: ${item.id}`);
    }
    if (!isGroup(item.group)) {
      return fail(`unknown group: ${item.group}`);
    }
    if (item.label.trim() === "") {
      return fail(`item ${item.id} needs a label`);
    }
    if (item.evidence.trim() === "") {
      return fail(`item ${item.id} needs an evidence hint`);
    }
    seen.add(item.id);
    copy.push({ ...item });
  }
  return ok(copy);
}

export function completion(
  items: DdItem[],
  doneIds: string[],
): Result<DdCompletion> {
  const checked = createChecklist(items);
  if (!checked.ok) {
    return checked;
  }
  const done = new Set(doneIds);
  const known = new Set(checked.value.map((item) => item.id));
  for (const id of done) {
    if (!known.has(id)) {
      return fail(`unknown item id: ${id}`);
    }
  }
  const groups = DD_GROUPS.map((group): GroupCompletion => {
    const inGroup = checked.value.filter((item) => item.group === group);
    const doneInGroup = inGroup.filter((item) => done.has(item.id));
    const required = inGroup.filter((item) => item.required);
    return {
      group,
      total: inGroup.length,
      done: doneInGroup.length,
      requiredTotal: required.length,
      requiredDone: required.filter((item) => done.has(item.id)).length,
      completionBps: divide(doneInGroup.length, inGroup.length),
    };
  });
  const required = checked.value.filter((item) => item.required);
  const requiredDone = required.filter((item) => done.has(item.id)).length;
  const gaps = checked.value
    .filter((item) => !done.has(item.id))
    .sort(
      (a, b) =>
        Number(b.required) - Number(a.required) || a.id.localeCompare(b.id),
    );
  return ok({
    total: checked.value.length,
    done: checked.value.filter((item) => done.has(item.id)).length,
    requiredTotal: required.length,
    requiredDone,
    completionBps: divide(
      checked.value.filter((item) => done.has(item.id)).length,
      checked.value.length,
    ),
    requiredBps: divide(requiredDone, required.length),
    groups,
    gaps,
  });
}

/** Gaps for one group, keeping the required-first order of `completion`. */
export function groupGaps(result: DdCompletion, group: DdGroup): DdItem[] {
  return result.gaps.filter((item) => item.group === group);
}

代码有三个刻意的边界。第一,createChecklist 会拒绝重复编号和空证据提示,因为尽调清单最常见的失败不是漏项,而是同一份材料被两个编号重复引用,最后没人知道哪份是正式版。第二,completion 遇到未知的完成项会报错而不是忽略,避免把拼错的编号当成「已完成」。第三,gaps 把必备项排在前面,因为买方不会平均看待所有缺口:一份缺失的合同转让条款,比一份缺失的市场宣传册重要得多。

使用顺序建议是:先跑一遍必备项,把缺口分给具体的人;再看每组的完成度,找出系统性短板;最后才看总完成率。Nord 即使收入组完成度很高,只要合同组缺转让条款,整体仍然不具备交割条件。完成率是进度指标,不是质量指标。

小结

  • 尽调验证四件事:收入真实、收入可持续、收入可转移、义务已披露。
  • 收入组先给 MRR 桥或等价的手工合同桥;对不上,买方会按最保守口径重算。
  • 成本组先拉平所有者薪酬口径;Nord 的 SDE 34,320 与 LedgerLite 的 130,240 不能直接比较。
  • 现金组用银行流水和账龄表证明每一笔余额;应收逾期与递延收入是交割前最常见的调整项。
  • 合同组的关键不是条款多漂亮,而是能不能转给买方;没有转让条款,收入预测随时失效。
  • 税务组按当期法规核对,案例税率只是教学设定;主体组先解决股权与关联方,否则交易无法签字。
  • 清单的完成标准是「有文件、有责任人、可复算」,不是「负责人说没问题」。

阅读导航:上一节:11.3 估值区间与谈判区间 · 下一节:12.2 数据室与三年对账 。

常见错误与边界

  1. 把尽调当成审计。 审计抽样看报表,尽调追到合同与回款;只准备报表,买方仍然会要原始凭证。
  2. 只准备最近一年。 买方通常要三年,至少覆盖一个完整的经济周期;只有一年数据,趋势无法判断。
  3. 用截图代替系统导出。 截图无法筛选、无法复算,也无法证明完整性;能导出的就用原始文件。
  4. 把一次性收入写进 MRR。 年度大单除以十二会破坏 MRR 桥,买方一眼就能看出期末对不上。
  5. 忽略合同转让条款。 收入真实不等于收入可转移;Nord 的问题正是后者,而不是前者。
  6. SDE 口径不写清。 是否扣除所有者薪酬、是否加回一次性费用,必须写在材料首页,不能等买方猜。
  7. 税务数字直接引用本文。 税率、门槛和申报义务随司法辖区与年度变化,案例数字只用于演示结构。
  8. 把清单交给一个人做完。 收入、合同、税务、主体分属不同责任人;一个人代答所有问题,缺口一定藏在细节里。

清单的作用不是让买方少问问题,而是让每个问题都有一个可核查的答案。下一节把这些答案放进一个买方能自己浏览的数据室,并说明三年对账怎么做。

再看 集中度风险 与 现金流量表 会更容易理解:前者解释为什么客户名单是尽调的第一份材料,后者解释为什么银行流水比利润表更难修饰。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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