以下情境为教学假设,人物与数字不代表经核实的真实案例。
技术尽调不是代码评审。代码写得好不好,买方可以自己看;尽调要回答的是另一组问题:这些代码是谁的、能不能转给买方、用了什么许可证、依赖谁、账号在谁手里、数据能不能合法带走。五个问题里只要有一个答案不清楚,买方买到的就不是一项可以自由使用的资产,而是一份需要补签字、补授权或补钱的权利。
Nord Studio 是本节的主案例。它是英国 Ltd,两位合伙人各持 50%,代码、域名、云账号和客户数据都可能分散在两个人名下;买方按 3.0× SDE 给出的锚是 102,960 美元。LedgerLite 做对照:一人公司,260 家订阅客户,核心服务里有一段 AGPL 组件,合规成本是 85,672 美元,重写替代是 20,400 美元。Tidecast 提供资产型案例:美术外包占资产 30%,重制预算 25,200 美元,音乐是买断授权且不得转授权。
五个检查:从权属到可转移性
五个检查按「能不能拥有、能不能用、能不能带走」排列。代码归属回答拥有;开源许可证和第三方依赖回答使用;域名账号和数据合规回答带走。买方不会把五项平均看待:权属不清可能让交易直接中止,许可证问题通常可以用钱解决,依赖和账号问题决定交割后能不能继续运营。
| 检查 | 买方要看 | 典型瑕疵 | 量化后果 |
|---|---|---|---|
| 代码归属 | 创始人、员工、外包的转让协议 | 代码在个人名下、外包未转让 | 补转让或重制;Tidecast 美术 30,240 |
| 开源许可证 | SBOM、依赖清单、许可证扫描 | AGPL 进入 SaaS | 合规 85,672 或重写 20,400 |
| 第三方依赖 | 云、支付、平台的合同与退出条款 | 支付 25%、云 60%、Steam 80% | 依赖中断期的收入暴露 |
| 域名与账号 | WHOIS、注册商、云根账号、平台账号 | 账号在个人或前员工名下 | 交割无法完成,可能中止交易 |
| 数据合规 | DPA、数据地图、子处理者、跨境安排 | 无 DPA、跨境无依据 | 首年 DPA/SCC 5,400;更高是客户流失 |
五项里,代码归属和账号是「开关型」问题:有就是有,没有就是没有,不能用价格完全补偿。许可证和依赖是「成本型」问题:可以算出一个修复金额,再决定折价还是托管。数据合规介于两者之间:补 DPA 有明确成本,但客户是否接受、监管是否追问,往往要交割后才知道。
代码归属与开源许可证
代码归属的第一个问题是「谁写的」。Nord 的两位合伙人在公司成立前后都写过代码;如果早期代码没有通过书面转让进入 Ltd,公司拥有的可能只是一份使用许可,而不是所有权。买方收购的是公司资产,代码不在公司名下,资产表就缺一块。补一份转让协议的成本不高,但它是交割条件;如果某位合伙人已经离开或不愿意签,买方只能要求重写或把价格压到能覆盖重写。
外包代码和美术是第二个问题。Tidecast 的美术外包占资产工作量 30%,按 84,000 美元的资产重置基数计算,重制预算是 84,000 × 30% = 25,200 美元;按教学假设加上 20% 的管理加成是 30,240 美元。如果外包方已经持有权利,回购还要付溢价,教学假设 50% 到 100%,区间是 45,360 到 60,480 美元。合同里加一句转让条款的成本接近于零,事后回购的成本是五位数美元,而且还要看外包方是否愿意配合。
开源许可证是第三个问题,也是 LedgerLite 的主要风险。AGPL 的触发点不是「有没有修改」,而是「有没有向网络用户提供服务」;订阅 SaaS 正好落在这个触发点上。卷二已经算过三条路径:继续合规的成本是 85,672 美元,重写替代组件是 20,400 美元,购买商业许可的教学报价是 30,000 美元。合规与重写的差额是 65,272 美元,相当于 768,768 美元价格锚的 8.5%。许可证是否触发取决于版本、修改方式和提供服务的形式,本节只做商业口径的保守估算,不是法律结论。
| 路径 | 成本 | 占 768,768 | 说明 |
|---|---|---|---|
| 继续用 AGPL,合规 | 85,672 | 11.1% | 工程 13,600 + 三年竞争损失 72,072 |
| 重写替代组件 | 20,400 | 2.7% | 240 小时 × 85 美元,一次性为主 |
| 购买商业许可 | 30,000 | 3.9% | 教学假设报价,可能含年度支持 |
| 合规与重写差额 | 65,272 | 8.5% | 谈判区间,不是必然折价 |
依赖、账号与数据合规
第三方依赖不是「用了谁的 API」这么简单。买方要知道合同期限、终止条件、数据导出能力、价格调整机制和替代方案。LedgerLite 的支付依赖占 25%,按 240,240 美元计算是 60,060 美元收入;如果支付方终止服务且短期无法替换,按 3.2× ARR 计算的价格暴露是 60,060 × 3.2 = 192,192 美元,正好是 768,768 美元的 25%。云依赖占 60%,但云的替代性通常比支付高;Tidecast 的 Steam 占销量 80%,平台协议决定结算、退款和账号转让;Nord 的转介绍占获客 55%,依赖的是合伙人的关系,不是一纸合同。
域名与账号是最容易被当成行政细节的一项。域名注册商、DNS、云根账号、代码仓库、应用商店、支付后台、企业邮箱,任何一项在创始人个人名下,交割时都要多一次转移;如果在前员工或外包方名下,买方可能直接失去控制权。Nord 的两位合伙人如果一人控制云账号、另一人控制域名,任何一方不配合都能让交割停摆。买方的处理方式通常不是折价,而是把账号转移写成交割条件,或者在托管里留一笔直到转移完成。
数据合规要看三件事:有没有 DPA、数据流向哪里、客户能不能要求删除或导出。LedgerLite 的 DPA 年维护成本是 3,600 美元,首年 SCC 是 1,800 美元,合计 5,400 美元,占 768,768 美元的 0.7%;这笔钱不大,但如果买方继承的是没有 DPA 的数据处理关系,修复成本只是起点,客户流失和监管问询才是大头。Nord 的咨询项目要确认交付物和客户数据归谁、项目结束后是否删除;Tidecast 要确认玩家数据、分析工具和年龄分级。数据合规的边界是:本节不判断是否违法,只把可见的修复成本放进价格模型。
修复成本如何变成价格与托管
IP 瑕疵的价格处理有两条路:卖方在交割前修好,或者买方在价格里扣掉修复成本。前者不改价格,但会增加卖方的现金支出和时间;后者把修复责任转移给买方,通常按修复成本直接扣减。如果修复结果在交割时还不能确认,双方会把钱放进托管:先按修复成本加一个缓冲留存,修复完成或观察期结束后释放。下面的 10% 标准托管是案例设定的教学比例,不是行业统一标准。
| 案例 | 瑕疵与路径 | 修复成本 | 占价格 | 标准 10% 托管 | 所需托管(+20%) | 缺口 |
|---|---|---|---|---|---|---|
| LedgerLite | AGPL,重写 | 20,400 | 2.7% | 76,877 | 24,480 | 0 |
| LedgerLite | AGPL,继续合规 | 85,672 | 11.1% | 76,877 | 102,806 | 25,930 |
| Tidecast | 美术重制 | 30,240 | 12.2% | 24,871 | 36,288 | 11,417 |
| Tidecast | 美术回购上限 | 60,480 | 24.3% | 24,871 | 72,576 | 47,705 |
这张表最重要的一行是 Tidecast 的美术重制:修复成本 30,240 美元,占 248,708 美元价格的 12.2%,而标准 10% 托管只有 24,871 美元,缺口 11,417 美元。也就是说,10% 托管覆盖不了这项瑕疵;买方要么把托管提高到 36,288 美元(14.6%),要么在价格里先扣 30,240 美元。LedgerLite 的 AGPL 重写只占 2.7%,10% 托管足够覆盖;但如果卖方选择继续合规,成本 85,672 美元占 11.1%,托管需要提高到 102,806 美元(13.4%),标准 10% 不够。
修复成本和托管金额的对应关系可以写成一句话:所需托管 = 修复成本 ×(1 + 缓冲比例),再和标准托管比较;差额就是买方要多留的钱,或者要从价格里先扣的钱。缓冲比例是教学假设的 20%,用来覆盖修复超支和观察期风险;真实交易可能用 10% 到 30%,取决于瑕疵的可验证程度。托管不是折价:如果修复完成、没有索赔,钱会退回卖方;折价不会退回。卖方在选择「自己修」还是「让买方扣」时,要比较的是现金时间和托管释放条件,而不只是金额。
把修复写成代码:tech-ip-dd.ts
下面的 tech-ip-dd.ts 把两条路径写成函数。compareRemediation 接收每个路径的一次性成本、年度成本和年数,按总成本排序,返回最便宜路径和最贵路径的差额;escrowPlan 接收价格、标准托管比例、修复成本和缓冲比例,返回标准托管、所需托管、缺口和所需托管占价格的基点。金额用整数美分,比例用基点。
// file: src/capital/tech-ip-dd.ts
// tier: L1
// verify: node --test test/capital/tech-ip-dd.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 RemediationPath =
| "rewrite"
| "commercial-license"
| "compliance"
| "rebuy"
| "rebuy-premium"
| "fix";
export type RemediationOption = {
path: RemediationPath;
oneTimeCents: Cents;
annualCents: Cents;
years: number;
};
export type PathTotal = {
path: RemediationPath;
totalCents: Cents;
};
export type PathComparison = {
rows: PathTotal[];
cheapest: RemediationPath;
cheapestCents: Cents;
spreadCents: Cents;
};
export type EscrowInput = {
priceCents: Cents;
standardEscrowBps: Bps;
remediationCents: Cents;
bufferBps: Bps;
};
export type EscrowPlan = {
standardEscrowCents: Cents;
requiredEscrowCents: Cents;
shortfallCents: Cents;
requiredBps: Bps;
covered: boolean;
};
function isPath(value: string): value is RemediationPath {
return (
value === "rewrite" ||
value === "commercial-license" ||
value === "compliance" ||
value === "rebuy" ||
value === "rebuy-premium" ||
value === "fix"
);
}
function validCost(value: Cents): boolean {
return Number.isInteger(value) && value >= 0;
}
function validBps(value: Bps): boolean {
return Number.isInteger(value) && value >= 0 && value <= 10000;
}
function validYears(value: number): boolean {
return Number.isInteger(value) && value >= 0;
}
/**
* Paths are compared on total cost, not on headline quote. A rewrite is
* one-time; compliance usually carries an annual competition loss. Sorting is
* ascending, and ties keep the input order so the seller can present the
* preferred path first.
*/
export function compareRemediation(
options: RemediationOption[],
): Result<PathComparison> {
if (options.length === 0) {
return fail("at least one remediation path is required");
}
const seen = new Set<RemediationPath>();
const rows: { row: PathTotal; index: number }[] = [];
for (const option of options) {
if (!isPath(option.path)) {
return fail(`unknown remediation path ${option.path}`);
}
if (seen.has(option.path)) {
return fail(`duplicate remediation path ${option.path}`);
}
if (!validCost(option.oneTimeCents)) {
return fail(`${option.path} one-time cost must be a non-negative integer`);
}
if (!validCost(option.annualCents)) {
return fail(`${option.path} annual cost must be a non-negative integer`);
}
if (!validYears(option.years)) {
return fail(`${option.path} years must be a non-negative integer`);
}
seen.add(option.path);
rows.push({
row: {
path: option.path,
totalCents: option.oneTimeCents + option.annualCents * option.years,
},
index: rows.length,
});
}
const ordered = rows
.sort(
(a, b) => a.row.totalCents - b.row.totalCents || a.index - b.index,
)
.map((entry) => entry.row);
const cheapest = ordered[0];
const mostExpensive = ordered[ordered.length - 1];
return ok({
rows: ordered,
cheapest: cheapest.path,
cheapestCents: cheapest.totalCents,
spreadCents: mostExpensive.totalCents - cheapest.totalCents,
});
}
/**
* The standard escrow is a percentage of price. Cure cost can exceed it, so
* the required escrow is the cure cost plus a buffer. The return value shows
* both the shortfall and the escrow as a share of price, which is the number
* that belongs in the term sheet.
*/
export function escrowPlan(input: EscrowInput): Result<EscrowPlan> {
if (!Number.isInteger(input.priceCents) || input.priceCents <= 0) {
return fail("price must be a positive integer amount");
}
if (!validBps(input.standardEscrowBps)) {
return fail("standard escrow must be between 0 and 100%");
}
if (!validCost(input.remediationCents)) {
return fail("remediation cost must be a non-negative integer");
}
if (!validBps(input.bufferBps)) {
return fail("escrow buffer must be between 0 and 100%");
}
const standardEscrowCents = Math.round(
(input.priceCents * input.standardEscrowBps) / 10000,
);
const requiredEscrowCents = Math.round(
(input.remediationCents * (10000 + input.bufferBps)) / 10000,
);
return ok({
standardEscrowCents,
requiredEscrowCents,
shortfallCents: Math.max(0, requiredEscrowCents - standardEscrowCents),
requiredBps: divide(requiredEscrowCents, input.priceCents),
covered: requiredEscrowCents <= standardEscrowCents,
});
}
把 LedgerLite 代进去:合规路径是 1,360,000 + 2,402,400 × 3 = 8,567,200 美分,重写是 2,040,000 美分,商业许可是 3,000,000 美分;最便宜是重写,差额 6,527,200 美分,对应 65,272 美元。Tidecast 的美术重制是 3,024,000 美分,回购下限 4,536,000 美分,上限 6,048,000 美分;最便宜是重制,差额 3,024,000 美分,对应 30,240 美元。
托管函数对 LedgerLite 重写返回标准 7,687,680 美分、所需 2,448,000 美分、缺口 0、所需 318 个基点;对合规路径返回所需 10,280,640 美分、缺口 2,592,960 美分、所需 1337 个基点;对 Tidecast 重制返回标准 2,487,078 美分、所需 3,628,800 美分、缺口 1,141,722 美分、所需 1459 个基点。这些数字和上面的表逐行对应。
代码的边界是「已识别的修复路径」。它不判断许可证是否触发,不替代转让协议,也不计算客户流失;它只把修复成本、缓冲和托管放在同一张表上,让卖方知道「修」和「扣」哪一个更贵。权属、账号和数据合规里那些不能用钱完全补偿的部分,仍然要写进交割条件。
小结
- 技术 IP 尽调做五个检查:代码归属、开源许可证、第三方依赖、域名与账号、数据合规。
- 代码归属是开关型问题:Nord 的早期代码如果没有转让给 Ltd,补协议是交割条件,不是折价项。
- LedgerLite 的 AGPL 合规成本是 85,672 美元(11.1%),重写是 20,400 美元(2.7%),差额 65,272 美元。
- Tidecast 的美术重制是 30,240 美元(12.2%),回购区间 45,360 到 60,480 美元(18.2% 到 24.3%)。
- 标准 10% 托管覆盖不了 Tidecast 的美术重制:缺口 11,417 美元,所需托管 36,288 美元(14.6%)。
- AGPL 继续合规的所需托管是 102,806 美元(13.4%),标准 10% 托管缺口 25,930 美元。
- 所需托管 = 修复成本 ×(1 + 缓冲比例);托管可能退回,折价不会。
- 许可证是否触发、数据是否合规,都要按实际架构和当期法规核对;案例数字只用于演示结构。
阅读导航:上一节:6.2 客户与合同尽调 · 下一节:7.1 资产收购与股权收购 。
常见错误与边界
- 把技术尽调当成代码评审。 代码质量影响未来成本,权属和许可证影响交易能不能成立;两者不是一回事。
- 以为代码是谁写的就是谁的。 在很多法域,创始人个人写的代码如果没有转让,公司可能只有使用权;买方买到的资产表里就没有这块。
- 把外包付款当成权利转让。 付款买的是服务,不是著作权;Tidecast 的美术重制 30,240 美元就是缺口的价格。
- 忽略 AGPL 的触发点。 订阅 SaaS 向网络用户提供服务,可能直接触发 AGPL;合规成本 85,672 美元不是许可证费。
- 只用标准 10% 托管。 Tidecast 的美术修复占价格 12.2%,10% 托管不够;所需托管要按修复成本加缓冲算。
- 把托管当成折价。 托管完成修复后会退回;折价不会。两者对卖方的现金流影响完全不同。
- 把账号当成行政细节。 域名、云账号、平台账号不在公司名下,交割可能直接停摆;这不是钱能完全解决的问题。
- 用案例税率或法律结论。 许可证是否触发、数据跨境是否合规,取决于实际架构和当期法规;本节只做商业估算。
本节的边界是「技术与 IP 瑕疵如何变成价格与托管」:不展开客户集中度、财务尽调和交割迁移,也不对许可证或数据合规给法律意见。开源许可证的商业边界见 7.2 开源许可证的商业边界 ,IP 归属与转让见 7.3 IP 归属、转让与员工发明 ,账号与合同迁移见 8.1 交割清单与资金流 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。