以下情境为教学假设,人物与数字不代表经核实的真实案例。
交割清单上,域名、代码、账号都能列成条目;合同不行。合同不是一件可以装箱的资产,而是一段关系:对手方愿不愿意继续、在什么主体下继续、按什么条款继续。买方付的钱里,很大一部分买的不是服务器,而是这段关系能否在交割日之后原样存在。Nord 的年收入 154,320 美元里,72.7% 来自咨询服务,前 3 家客户占 48%;这些数字在估值表上是收入,在交割日是 105 份要处理的合同。
本节只回答一个问题:把合同从卖方手里交到买方手里,要花多少小时、多少钱、暴露多少收入。它不提供法律意见,也不判断某一条款在某个司法辖区是否有效;所有工时、比例与同意成本都是教学假设。
两种交易结构,两套承继规则
股权收购买的是主体。买方买下持有合同的公司的股份,合同对手方在法律上没有变化:劳动合同继续由原雇主履行,供应商继续向原公司开票,客户继续向原公司付款。承继是默认结果,不需要逐份重签。买方要担心的不是「合同要不要换」,而是「合同里有没有控制权变更条款」。
资产收购买的是资产。买方新设主体,把代码、客户关系、商标、设备买过去,但合同不会自动跟着走。每一份合同都要处理:能转让的签转让协议,不能转让的取得对方同意,对方不同意的重新签,重签也签不下来的,这份收入就留在卖方那里。资产收购的尽调清单里,合同数量不是背景信息,而是工作量。
| 维度 | 股权收购 | 资产收购 |
|---|---|---|
| 合同主体 | 不变 | 变为买方新主体 |
| 承继方式 | 默认承继 | 逐份转让、同意或重签 |
| 员工合同 | 随主体继续 | 通常要新雇主重新签约 |
| 供应商合同 | 随主体继续 | 常含禁止转让条款,需同意 |
| 客户合同 | 随主体继续 | 按合同转让条款逐份处理 |
| 主要风险 | 控制权变更触发终止 | 同意拿不到、重签期客户流失 |
这张表解释了一个常见误判:卖方以为「股权收购更省事」,买方却可能更愿意做资产收购,因为资产收购可以把某些负债留在旧主体。结构选择不是谁更先进,而是谁承担哪一类成本和风险。合同承继成本只是其中一项,却经常是交割后第一个月最直观的一项。
三类合同:劳动、供应商、客户
Nord 只有两名合伙人,没有正式雇员,但有劳动合同或合伙人协议;外包与工具供应商构成第二类;咨询合同和产品订阅构成第三类。合同份数的教学假设是:2 份劳动/合伙人协议、3 份供应商合同、10 份咨询合同、90 份产品订阅;90 家产品客户来自案例事实表,10 份咨询合同按 110 小时/月、每客户每月 11 小时反推。三类合同的承继逻辑不同,不能用一张「合同总数」的表格糊过去。
劳动合同承继的不只是工资,还有工龄、假期、通知期和可能的补偿。股权收购下,雇主主体不变,合同通常继续;资产收购下,新雇主需要重新签约,员工可以选择不签。对两人合伙的 Nord,这个问题更尖锐:合伙人既是股东又是劳动者,买方买的如果是资产,两位合伙人要不要变成买方的员工,是一个留任问题,不只是合同问题。
供应商合同承继的是连续性和价格。Nord 的直接成本里有 18,000 美元外包和 6,000 美元工具;供应商合同通常写着「未经书面同意不得转让」,有的还写着「控制权变更视为转让」。工具类供应商可能按账号绑定,换主体要重新验证付款方式;外包供应商则可能因为换了付款方而要求重谈价格。供应商合同金额不大,但断供的后果不小。
客户合同承继的是收入和信任。Nord 的 10 份咨询合同按项目或按月签,平均每份年收入 11,220 美元;90 家产品客户按月付 39 美元,平均每份年收入 468 美元。咨询合同金额大、数量少、条款重;产品订阅金额小、数量多、条款标准化。两类合同的承继工作量不能按同一套流程估。
| 合同类型 | 数量 | 单份年收入 | 承继难点 | 谁最可能拖慢 |
|---|---|---|---|---|
| 劳动/合伙人 | 2 | 不适用 | 工龄、留任、角色 | 合伙人本人 |
| 供应商 | 3 | 不适用 | 禁转条款、付款主体 | 供应商财务与法务 |
| 咨询客户 | 10 | 11,220 | 人手、交付责任、同意 | 客户采购与业务负责人 |
| 产品客户 | 90 | 468 | 条款通知、支付授权 | 客户管理员 |
Nord 的合同集中度让第三类更危险:前 3 家客户占收入 48%,如果这 3 家都在 10 份咨询合同里,那么咨询合同的单份平均收入会被拉高,重签时任何一家掉队都会直接改变收入结构。数量最多的 90 家产品客户反而好处理,因为单份金额小、条款标准化,适合批量通知。
重签工作量:105 份合同、85 小时、9,725 美元
把重签工作量拆成「合同数 × 单份工时 × 时薪」,可以得到一个可检验的预算。Nord 的教学假设是:每份劳动/合伙人合同 2 小时,每份供应商合同 2 小时,每份咨询合同 3 小时,每份产品订阅 0.5 小时;创始人时薪按咨询费率 85 美元计。
| 合同组 | 数量 | 单份工时 | 总工时 | 按 85 美元计 |
|---|---|---|---|---|
| 劳动/合伙人 | 2 | 2.0 | 4.0 | 340 |
| 供应商 | 3 | 2.0 | 6.0 | 510 |
| 咨询客户 | 10 | 3.0 | 30.0 | 2,550 |
| 产品客户 | 90 | 0.5 | 45.0 | 3,825 |
| 合计 | 105 | 85.0 | 7,225 |
85 小时按每天 4 小时计,是 21 个工作日;按 Nord 每月 110 小时的咨询产能计,是 0.77 个月。也就是说,仅重签合同就会吃掉创始人近一个月的可计费时间,还不包括谈判、等待对方回签和客户因为换主体而提出的额外问题。
除了重签,还有同意成本。10 份咨询合同里有「未经书面同意不得转让」的条款,教学假设每份同意的法律与沟通成本是 250 美元,合计 2,500 美元。资产收购下,合同承继总成本是 7,225 + 2,500 = 9,725 美元,占年收入 154,320 美元的 6.3%,即 630 个基点。
股权收购下,重签工时为 0,只剩同意成本 2,500 美元,占年收入的 1.6%,即 162 个基点。两种结构的差额是 7,225 美元、468 个基点。这个差额不是「股权收购更便宜」的结论,而是「资产收购把合同工作显性化」的度量:买方若坚持资产收购,这 9,725 美元要么由卖方承担,要么从价格里扣。
控制权变更条款:合同在交割日自动终止
承继不等于安全。很多合同里有一条控制权变更条款:当一方的大部分股权、投票权或实际控制人发生变化时,另一方有权终止合同,或者要求重新谈判。股权收购下主体没变,但控制权变了,这条款照样触发。卖方在尽调表上写「合同随主体自动承继」,买方法务会追问一句:「控制权变更条款查了吗?」
Nord 的教学假设是:10 份咨询合同里 60% 含控制权变更终止权,90 份产品订阅里 20% 含同类条款。按数量算,暴露合同是 6 + 18 = 24 份;按收入算,咨询部分 67,320 美元、产品部分 8,424 美元,合计 75,744 美元,占年收入 49.1%。
| 合同组 | 数量 | 含控制权变更比例 | 暴露合同 | 暴露收入 |
|---|---|---|---|---|
| 咨询客户 | 10 | 60% | 6 | 67,320 |
| 产品客户 | 90 | 20% | 18 | 8,424 |
| 合计 | 100 | 24 | 75,744 |
49.1% 的收入暴露不是「一定会丢」,而是「交割日之后对方有权重新选择」。买方会把这份不确定性写进价格调整、托管或 earn-out 条件;卖方则要在交割前拿到续签或放弃终止权的确认函。确认函拿得越晚,买方扣得越多。
转让条款和控制权变更条款是两件事。转让条款管的是「合同能不能换主体」,控制权变更条款管的是「主体没换但老板换了,对方能不能走」。资产收购要先过转让关,再过控制权变更关;股权收购只过第二关,但第二关往往被忽略,因为卖方以为「主体没变就没事」。
LedgerLite 的 260 份订阅合同是另一种量级:股权收购下合同随新加坡主体继续,资产收购则要逐份换主体。260 份按每份 0.2 小时的教学假设计是 52 小时,按 85 美元计是 4,420 美元;这还没算需要单独同意的企业合同。Tidecast 则提醒另一个边界:Steam 账号不是合同,平台不允许过户,买方拿到的是代码与发行权,不是店铺本身。
写成代码:contract-assignment.ts
下面的 contract-assignment.ts 把承继拆成四个量:要重签的合同数、重签工时与成本、同意成本、控制权变更暴露的收入。资产收购下 contractsToRepaper 等于合同总数,股权收购下为 0;两种结构都计算 atRiskRevenueCents,因为控制权变更条款不随结构消失。
// file: src/capital/contract-assignment.ts
// tier: L1
// verify: node --test test/capital/contract-assignment.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 DealStructure = "stock" | "asset";
export type ContractKind = "employment" | "supplier" | "customer";
export const CONTRACT_KINDS: ContractKind[] = [
"employment",
"supplier",
"customer",
];
export type ContractGroup = {
kind: ContractKind;
count: number;
annualRevenueCents: Cents;
reSignHours: number;
consentRequired: boolean;
consentCostCents: Cents;
changeOfControlBps: Bps;
};
export type AssignmentInput = {
structure: DealStructure;
hourlyRateCents: Cents;
groups: ContractGroup[];
};
export type AssignmentOutcome = {
structure: DealStructure;
automaticSuccession: boolean;
contractCount: number;
contractsToRepaper: number;
contractsWithConsent: number;
reSignHours: number;
reSignCostCents: Cents;
consentCostCents: Cents;
totalCostCents: Cents;
annualRevenueCents: Cents;
totalCostBpsOfRevenue: Bps;
atRiskContracts: number;
atRiskRevenueCents: Cents;
atRiskRevenueBps: Bps;
};
function isKind(value: string): value is ContractKind {
return (CONTRACT_KINDS as string[]).includes(value);
}
function checkAmount(value: Cents, label: string): string | null {
if (!Number.isInteger(value) || value < 0) {
return `${label} must be a non-negative integer amount in cents`;
}
return null;
}
function checkBps(value: Bps, label: string): string | null {
if (!Number.isInteger(value) || value < 0 || value > 10000) {
return `${label} must be between 0 and 10000 bps`;
}
return null;
}
/**
* A stock deal changes the owner of the contracting entity, so an employment,
* supplier or customer contract continues with the same legal party. An asset
* deal moves the contract to a new entity, so every contract must be re-papered
* or transferred with consent. Separately, a change-of-control clause gives the
* counterparty a termination right even when the entity itself survives. This
* model counts both costs and the revenue exposed to that right. It is a
* teaching model, not legal advice.
*/
export function assessAssignment(
input: AssignmentInput,
): Result<AssignmentOutcome> {
if (input.structure !== "stock" && input.structure !== "asset") {
return fail("structure must be stock or asset");
}
if (!Number.isInteger(input.hourlyRateCents) || input.hourlyRateCents <= 0) {
return fail("hourly rate must be a positive integer amount in cents");
}
if (!Array.isArray(input.groups) || input.groups.length === 0) {
return fail("at least one contract group is required");
}
let contractCount = 0;
let contractsWithConsent = 0;
let reSignHours = 0;
let reSignCostCents = 0;
let consentCostCents = 0;
let annualRevenueCents = 0;
let atRiskContracts = 0;
let atRiskRevenueCents = 0;
for (const group of input.groups) {
if (!isKind(group.kind)) {
return fail("contract kind must be employment, supplier or customer");
}
if (!Number.isInteger(group.count) || group.count < 0) {
return fail("contract count must be a non-negative integer");
}
if (
!Number.isFinite(group.reSignHours) ||
group.reSignHours < 0 ||
group.reSignHours > 1000
) {
return fail("re-sign hours must be between 0 and 1000 per contract");
}
for (const [value, label] of [
[group.annualRevenueCents, "annual revenue"],
[group.consentCostCents, "consent cost"],
] as Array<[Cents, string]>) {
const error = checkAmount(value, label);
if (error) {
return fail(error);
}
}
const bpsError = checkBps(
group.changeOfControlBps,
"change-of-control probability",
);
if (bpsError) {
return fail(bpsError);
}
contractCount += group.count;
annualRevenueCents += group.count * group.annualRevenueCents;
if (group.consentRequired) {
contractsWithConsent += group.count;
consentCostCents += group.count * group.consentCostCents;
}
if (input.structure === "asset") {
reSignHours += group.count * group.reSignHours;
}
atRiskContracts += Math.round(
(group.count * group.changeOfControlBps) / 10000,
);
atRiskRevenueCents += Math.round(
(group.count * group.annualRevenueCents * group.changeOfControlBps) /
10000,
);
}
if (contractCount === 0) {
return fail("contract groups must describe at least one contract");
}
if (input.structure === "asset") {
reSignCostCents = Math.round(reSignHours * input.hourlyRateCents);
}
const totalCostCents = reSignCostCents + consentCostCents;
return ok({
structure: input.structure,
automaticSuccession: input.structure === "stock",
contractCount,
contractsToRepaper: input.structure === "asset" ? contractCount : 0,
contractsWithConsent,
reSignHours,
reSignCostCents,
consentCostCents,
totalCostCents,
annualRevenueCents,
totalCostBpsOfRevenue: divide(totalCostCents, annualRevenueCents),
atRiskContracts,
atRiskRevenueCents,
atRiskRevenueBps: divide(atRiskRevenueCents, annualRevenueCents),
});
}
export type AssignmentComparison = {
cheaper: DealStructure | "equal";
deltaCostCents: Cents;
deltaCostBpsOfRevenue: Bps;
sameAtRiskRevenue: boolean;
};
/**
* Compares the two structures on contract work only. It deliberately ignores
* price, tax basis, entity setup and the buyer's appetite for inherited
* liabilities, because those belong to the deal-structure model. The point of
* this comparison is narrow: how much does it cost to keep the same revenue
* attached to a contracting party after closing?
*/
export function compareAssignment(
asset: AssignmentOutcome,
stock: AssignmentOutcome,
): Result<AssignmentComparison> {
if (asset.structure !== "asset" || stock.structure !== "stock") {
return fail("expected one asset outcome and one stock outcome");
}
if (asset.contractCount !== stock.contractCount) {
return fail("both structures must describe the same contract base");
}
if (asset.annualRevenueCents !== stock.annualRevenueCents) {
return fail("both structures must describe the same revenue base");
}
const deltaCostCents = asset.totalCostCents - stock.totalCostCents;
const cheaper =
deltaCostCents < 0 ? "asset" : deltaCostCents > 0 ? "stock" : "equal";
return ok({
cheaper,
deltaCostCents,
deltaCostBpsOfRevenue: divide(
Math.abs(deltaCostCents),
asset.annualRevenueCents,
),
sameAtRiskRevenue:
asset.atRiskRevenueCents === stock.atRiskRevenueCents,
});
}
代码把「重签」和「控制权变更」分开,是因为它们的成本性质不同:重签是确定要花的工时,控制权变更是概率性的收入暴露。totalCostCents 只加确定成本,atRiskRevenueCents 单独列出,避免把 75,744 美元的暴露当成 75,744 美元的损失。compareAssignment 只比较合同工作,不比较价格、税基和负债承接;那些属于交易结构模型,见 7.1 资产收购与股权收购
。
把 Nord 代入资产收购:105 份合同、重签 85 小时、重签成本 722,500 美分、同意成本 250,000 美分、总成本 972,500 美分,占年收入 630 个基点;控制权变更暴露 24 份合同、7,574,400 美分,占 4,908 个基点。换成股权收购:重签 0 小时、总成本 250,000 美分、占 162 个基点,暴露收入不变。差额 722,500 美分、468 个基点,就是资产收购多出来的合同工作。
小结
- 股权收购买主体,合同默认承继;资产收购买资产,合同要逐份转让、取得同意或重签。
- 三类合同要分开算:劳动/合伙人合同看工龄与角色,供应商合同看禁转与付款主体,客户合同看金额与条款。
- Nord 的 105 份合同按教学工时算是 85 小时、7,225 美元;加 2,500 美元同意成本,资产收购承继成本 9,725 美元,占年收入 6.3%。
- 股权收购下重签工时为 0,只剩 2,500 美元同意成本;两种结构差额 7,225 美元、468 个基点。
- 控制权变更条款不随结构消失:Nord 教学假设下 24 份合同、75,744 美元收入暴露,占年收入 49.1%。
- 转让条款管「能不能换主体」,控制权变更条款管「主体没换但老板换了能不能走」,资产收购要过两关,股权收购只过第二关。
- LedgerLite 的 260 份订阅合同在股权收购下随主体继续,资产收购则要逐份换主体;Tidecast 的 Steam 账号不是合同,不能按合同承继处理。
阅读导航:上一节:8.3 代码、域名与数据交割 · 下一节:9.2 竞业、保密与留任 。
常见错误与边界
- 把「股权收购」当成「合同不用管」。 主体不变只解决承继,不解决控制权变更条款;后者照样能让对方在交割日选择离开。
- 把合同总数当工作量。 105 份合同里,90 份产品订阅每份 0.5 小时,10 份咨询每份 3 小时;按平均工时乘总数会低估咨询合同。
- 忽略同意成本。 重签工时之外,还有征求同意、等待回签、解释交易的时间;Nord 的 10 份咨询合同按 250 美元一份算是 2,500 美元。
- 把控制权变更暴露当损失。 75,744 美元是「对方有权重新选择」的收入,不是已经丢掉的收入;它影响价格与托管,不等于当期亏损。
- 资产收购不查供应商合同。 供应商金额小,但禁转条款和付款主体变更足以让服务在交割后中断。
- 劳动合同只换主体、不换角色。 合伙人从股东变成买方员工,涉及职责、汇报线和留任期;只改合同抬头,人还是会走。
- 用卖方时薪估买方成本。 85 美元是 Nord 的咨询费率,买方法务、采购和 PM 的时薪不同;同一份合同在两边的成本不一样。
- 交割后才发现合同转不过去。 转让同意要在交割前拿到,至少要在托管释放前拿到;交割后再谈,卖方已经失去议价位置。
- 把平台账号写进合同承继清单。 Steam 等平台账号不是合同,平台规则可能直接禁止转让;它属于账号与授权交割,不是合同转让。
- 只看前 3 家客户。 前 3 家占 48%,但剩下 52% 分布在 97 份合同里;大客户要一家一家谈,小客户要批量处理,两套流程都要有。
本节的边界是「合同可分类、单份工时可按组估算、控制权变更概率可假设」的静态模型。它不判断某一条款是否有效,也不覆盖跨境劳动法、集体协商、供应商独家条款和平台规则;这些要把具体合同交给律师审阅。合同承继完成后,下一节处理人的问题:竞业、保密与留任,见 9.2 竞业、保密与留任 ;客户迁移的操作层见 8.2 客户、合同与账号迁移 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。