以下情境为教学假设,人物与数字不代表经核实的真实案例。
财务尽调(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 数据室与三年对账 。
常见错误与边界
- 把尽调当成审计。 审计抽样看报表,尽调追到合同与回款;只准备报表,买方仍然会要原始凭证。
- 只准备最近一年。 买方通常要三年,至少覆盖一个完整的经济周期;只有一年数据,趋势无法判断。
- 用截图代替系统导出。 截图无法筛选、无法复算,也无法证明完整性;能导出的就用原始文件。
- 把一次性收入写进 MRR。 年度大单除以十二会破坏 MRR 桥,买方一眼就能看出期末对不上。
- 忽略合同转让条款。 收入真实不等于收入可转移;Nord 的问题正是后者,而不是前者。
- SDE 口径不写清。 是否扣除所有者薪酬、是否加回一次性费用,必须写在材料首页,不能等买方猜。
- 税务数字直接引用本文。 税率、门槛和申报义务随司法辖区与年度变化,案例数字只用于演示结构。
- 把清单交给一个人做完。 收入、合同、税务、主体分属不同责任人;一个人代答所有问题,缺口一定藏在细节里。
清单的作用不是让买方少问问题,而是让每个问题都有一个可核查的答案。下一节把这些答案放进一个买方能自己浏览的数据室,并说明三年对账怎么做。
再看 集中度风险 与 现金流量表 会更容易理解:前者解释为什么客户名单是尽调的第一份材料,后者解释为什么银行流水比利润表更难修饰。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。