《独立开发者的商业指南·资本与退出》9.3 交接期与知识转移

交接不是发一份文档,而是让买方在不打电话给创始人的情况下把生意跑起来。本节用 Nord 的 4 小时 × 60 天陪跑、40 小时文档与 60 小时买方 PM,算清 31,000 美元交接成本、10,296 美元托管释放节奏,并给出客户、供应商、运维与隐性决策四类知识的验收标准。

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

交割日之后,买方最容易低估的成本不是服务器迁移,而是「问人」。代码可以读,合同可以换主体,客户可以发通知;但「这个客户为什么享受 15% 折扣」「这个供应商为什么不能换付款主体」「这个功能为什么三年前没做」这类问题,只存在于创始人的脑子里。Nord 的两位合伙人既持有前 3 家客户 48% 的收入,又承担交付与运维;知识不在系统里,在两个人身上。

本节把交接拆成三件事:30/60/90 天的节奏、四类知识的清单、以及交接完成的可检验标准。它不写操作手册模板,也不提供法律意见;所有工时、比例与扣款都是教学假设。

30/60/90 天:把交接拆成三个阶段

交接期最常见的错误是「前两周密集开会,后两个月找不到人」。知识转移需要重复和反向演练,不是一次性的信息倾倒。把 90 天分成三段,每段有明确的交付物和验收动作,买方才能在托管释放前真正接手。

第 0–30 天是关系交接。创始人把前 3 家客户、3 家供应商和关键账号逐一介绍给买方;不是发一封介绍邮件,而是让买方独立完成一次沟通。客户要听到「以后由谁负责」,供应商要知道「付款主体变了以后找谁」,账号要确认所有权和恢复邮箱已经在买方名下。这个阶段的目标不是「认识」,而是「买方单独发邮件,对方会回」。

第 31–60 天是运维交接。买方在创始人旁听下完成一次部署、一次回滚、一次备份恢复和一次月结;创始人只回答问题,不接管键盘。Nord 的教学假设是创始人每天投入 4 小时、持续 60 天,共 240 小时;这 240 小时里,前 30 天偏关系,后 30 天偏运维和财务。

第 61–90 天是反向陪跑。买方主导,创始人只做复核;如果买方能在不打电话的情况下完成客户续约、部署回滚、月结和支持工单,交接才算进入验收。90 天不是硬性期限,而是「关系交接 30 天 + 运维交接 30 天 + 反向验证 30 天」的教学节奏;实际交易可以压缩,但压缩的通常是反向验证,而不是关系交接。

阶段天数主线交付物验收动作
关系交接0–30客户、供应商、账号客户地图、供应商清单、账号清单买方独立完成一次客户沟通
运维交接31–60部署、回滚、月结runbook、录屏、财务日历买方独立部署并回滚一次
反向验证61–90买方主导、创始人复核验收记录、问题清单连续两周无创始人介入

三个阶段里,最容易被跳过的是反向验证。创始人觉得「我已经讲过了」,买方觉得「我大概懂了」,双方都不愿意再花 30 天;结果交割后第一个月,买方遇到一个没演练过的故障,还是要打电话。反向验证不是形式,它是把「听过」变成「做过」的唯一环节。

知识清单:客户关系、供应商、运维手册、隐性决策

知识转移的清单不能只列「代码、文档、账号」,那是最容易交的部分。真正决定买方能不能独立运营的,是四类知识:客户关系、供应商关系、运维手册、隐性决策。前三类可以写成文档,第四类只能靠对话和复盘。

客户关系不只是联系人列表。买方要知道谁决策、谁付款、谁使用、续约节点在哪个月、历史折扣是多少、上一次投诉是什么、哪些承诺是创始人口头给的。Nord 的前 3 家客户占收入 48%,每一家的续约节点和折扣历史都直接影响现金流;买方如果只拿到一个邮箱,第一次续约就会出问题。

供应商关系不只是合同。买方要知道 3 家供应商分别提供什么、付款主体是谁、合同什么时候到期、有没有替代方案、换供应商要多久。Nord 的直接成本里有 18,000 美元外包和 6,000 美元工具;供应商金额不大,但断供会直接影响交付。清单里要写「谁负责续费、谁负责 escalation、备用供应商是谁」。

运维手册不只是部署命令。买方要能独立完成部署、回滚、监控、备份、恢复和事故处理;手册要覆盖「正常流程」和「出问题怎么办」两条线。最容易被忽略的是回滚:很多团队只写怎么上线,不写怎么撤;交割后第一次上线失败,买方才发现没有回滚脚本。

隐性决策是最难交的一类。为什么这个客户有例外价格、为什么这个功能一直没做、为什么这个技术债没有还、为什么某个供应商不能换,这些决策没有写在代码里,也没有写在合同里。交接时要用「决策日志 + 复盘会」把至少 10 个关键取舍讲清楚;买方不需要同意,但需要知道原因。

知识类别具体内容载体验收标准
客户关系决策链、续约节点、折扣历史、口头承诺客户地图、通话记录买方独立完成一次续约沟通
供应商合同、付款主体、到期日、替代方案供应商清单、合同摘要买方独立完成一次续费或下单
运维手册部署、回滚、监控、备份、事故处理runbook、录屏买方独立部署并回滚一次
隐性决策定价例外、技术债、未做功能、供应商选择决策日志、复盘会买方能解释三个关键取舍
财务开票、收款、账期、递延收入财务日历、对账表买方独立完成一次月结
账号域名、云、支付、代码仓库账号清单、恢复邮箱所有权与恢复邮箱在买方名下

四类知识里,客户关系和隐性决策最难量化,也最容易在交割后被低估。买方在尽调时能看到合同和代码,看不到「这个客户只认创始人」和「这个供应商是朋友介绍的」。交接清单的作用,是把这些看不见的东西变成可以验收的动作。

LedgerLite 的知识转移更像「产品交接」:一人 SaaS、260 家客户、月流失 1.5%,买方主要接手代码、支付通道和客户支持,客户关系不像 Nord 那样集中在一个人身上。Tidecast 则几乎没有组织知识可交:一人开发、平台发行,买方拿到的是代码、美术和发行权,账号本身还不能过户。

陪跑工时与买方扣款:31,000 美元怎么算

交接成本由三部分组成:创始人陪跑、文档整理、买方项目管理。创始人陪跑按「每天 4 小时 × 60 天 × 机会成本」算;机会成本用 Nord 的咨询费率 85 美元/小时,因为这段时间本来可以用于计费项目。文档整理按 40 小时计,买方 PM 按 60 小时、120 美元/小时计。

成本项计算金额
创始人陪跑4 小时 × 60 天 × 8520,400
文档整理40 小时 × 853,400
买方 PM60 小时 × 1207,200
合计31,000

31,000 美元占交易价 102,960 美元(3.0× SDE 的折价锚)的 30.1%,即 3,011 个基点。这个比例说明交接不是「交割后的杂事」,而是交易经济性的一部分:如果卖方承担,等于从净所得里再扣 31,000 美元;如果买方承担,买方会把这段成本提前反映在报价里。

按 60 天摊,每天的成本是 516.67 美元。创始人每天 4 小时看起来不多,但 240 小时相当于 Nord 每月 110 小时咨询产能的 2.2 个月;也就是说,交接期会占掉创始人两个多月的可计费时间。如果交割后创始人还要同时维护老客户和配合买方,实际投入通常高于 4 小时/天。

买方扣款是另一条线。教学假设买方把交易价的 10% 作为交接托管,金额是 10,296 美元;按 30/60/90 天三个节点释放,权重分别是 30%、40%、30%。这不是罚款,而是「没交完之前先不付完」的机制;卖方要把它当成现金流安排,而不是价格损失。

节点交付物权重释放金额
第 30 天客户、供应商与账号交接30%3,088.80
第 60 天部署、回滚与月结验收40%4,118.40
第 90 天反向验证与支持交接30%3,088.80
合计100%10,296.00

托管释放节奏要和验收标准绑定,而不是和日历绑定。第 60 天到了但买方还不能独立部署,释放就不应该发生;反过来,如果第 45 天就完成了全部验收,也没有必要等到第 90 天。节点是上限,不是自动触发器。

验收标准:什么时候算交接完成

交接完成的定义不是「卖方交完了」,而是「买方不需要卖方也能跑」。这个定义要落成可检验的标准,否则双方对「交完了」的理解会差很远。教学假设用四条硬标准:客户、运维、财务、支持,每条都要有独立完成的证据。

第一条是客户:买方独立完成前 3 家客户中至少一家的续约沟通,且客户明确知道后续联系人。第二条是运维:买方独立完成一次部署和一次回滚,不需要创始人在场。第三条是财务:买方独立完成一次月结,能对上递延收入和应收。第四条是支持:买方连续两周处理 90% 以上的 incoming 工单,创始人只做复核。

四条标准全部满足才算通过,不能用「80% 完成」放行。原因很简单:没通过的那一条,通常正是客户信任或现金流所在。买方可以接受文档不完美,但不能接受「客户只认创始人」或「月结对不上」;前者影响续约,后者影响估值。

验收项证据通过条件不通过时的处理
客户续约邮件、会议记录买方独立完成,客户确认联系人延长关系交接,暂缓释放
运维部署日志、回滚记录买方独立操作,无创始人介入补录屏与演练,暂缓释放
财务月结表、对账记录递延收入与应收对得上补财务日历与对账规则
支持工单系统记录连续两周 90% 独立处理延长反向陪跑

验收通过后,托管释放,交接期结束;但知识转移不一定结束。买方通常还会保留 30–60 天的「问询窗口」,创始人按小时计费或按约定提供有限支持。这个窗口要写进交易文件,而不是靠口头承诺;否则交割三个月后,买方还会因为一个老客户的问题打电话。

写成代码:knowledge-transfer.ts

下面的 knowledge-transfer.ts 把交接拆成成本和释放两条线。founderCostCents、documentationCostCents、buyerPmCostCents 相加是交接成本;holdbackCents 按里程碑权重拆成 releases,每个节点释放多少一目了然。acceptanceGate 则要求所有验收项都通过,避免「80% 完成」被当成完成。

// file: src/capital/knowledge-transfer.ts
// tier: L1
// verify: node --test test/capital/knowledge-transfer.test.ts

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

export type Milestone = {
  day: number;
  deliverable: string;
  weightBps: Bps;
};

export type HandoverInput = {
  founderHoursPerDay: number;
  handoverDays: number;
  founderRateCents: Cents;
  documentationHours: number;
  buyerPmHours: number;
  buyerPmRateCents: Cents;
  priceCents: Cents;
  holdbackBps: Bps;
  acceptanceDays: number;
  milestones: Milestone[];
};

export type MilestoneRelease = {
  day: number;
  deliverable: string;
  weightBps: Bps;
  releaseCents: Cents;
};

export type HandoverOutcome = {
  founderHours: number;
  founderCostCents: Cents;
  documentationCostCents: Cents;
  buyerPmCostCents: Cents;
  totalCostCents: Cents;
  totalCostBpsOfPrice: Bps;
  costPerHandoverDayCents: Cents;
  holdbackCents: Cents;
  finalReleaseCents: Cents;
  milestoneCount: number;
  milestoneWeightBps: Bps;
  releases: MilestoneRelease[];
};

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 handover is a cost with a release schedule. Founder shadowing, written
 * documentation and buyer-side project management all consume hours that have
 * an opportunity cost, while the buyer holds back part of the price until the
 * milestones are met. The model prices the handover, splits the holdback by
 * milestone weight and keeps the two numbers separate: handover cost is what
 * the seller spends, holdback is what the buyer has not yet paid. This is a
 * teaching model, not a legal or tax opinion.
 */
export function handoverPlan(input: HandoverInput): Result<HandoverOutcome> {
  if (
    !Number.isFinite(input.founderHoursPerDay) ||
    input.founderHoursPerDay <= 0 ||
    input.founderHoursPerDay > 24
  ) {
    return fail("founder hours per day must be between 0 and 24");
  }
  if (
    !Number.isInteger(input.handoverDays) ||
    input.handoverDays < 1 ||
    input.handoverDays > 365
  ) {
    return fail("handover days must be an integer between 1 and 365");
  }
  if (!Number.isInteger(input.founderRateCents) || input.founderRateCents <= 0) {
    return fail("founder rate must be a positive integer amount in cents");
  }
  if (
    !Number.isFinite(input.documentationHours) ||
    input.documentationHours < 0 ||
    input.documentationHours > 10000
  ) {
    return fail("documentation hours must be between 0 and 10000");
  }
  if (
    !Number.isFinite(input.buyerPmHours) ||
    input.buyerPmHours < 0 ||
    input.buyerPmHours > 10000
  ) {
    return fail("buyer PM hours must be between 0 and 10000");
  }
  if (
    !Number.isInteger(input.buyerPmRateCents) ||
    input.buyerPmRateCents <= 0
  ) {
    return fail("buyer PM rate must be a positive integer amount in cents");
  }
  if (!Number.isInteger(input.priceCents) || input.priceCents <= 0) {
    return fail("price must be a positive integer amount in cents");
  }
  const holdbackError = checkBps(input.holdbackBps, "holdback");
  if (holdbackError) {
    return fail(holdbackError);
  }
  if (
    !Number.isInteger(input.acceptanceDays) ||
    input.acceptanceDays < input.handoverDays ||
    input.acceptanceDays > 365
  ) {
    return fail("acceptance days must be an integer from handover to 365");
  }
  if (!Array.isArray(input.milestones) || input.milestones.length === 0) {
    return fail("at least one acceptance milestone is required");
  }

  let milestoneWeightBps = 0;
  for (const milestone of input.milestones) {
    if (
      !Number.isInteger(milestone.day) ||
      milestone.day < 1 ||
      milestone.day > input.acceptanceDays
    ) {
      return fail("milestone day must fall inside the acceptance window");
    }
    if (
      typeof milestone.deliverable !== "string" ||
      milestone.deliverable.trim() === ""
    ) {
      return fail("each milestone needs a deliverable label");
    }
    const weightError = checkBps(milestone.weightBps, "milestone weight");
    if (weightError) {
      return fail(weightError);
    }
    milestoneWeightBps += milestone.weightBps;
  }
  if (milestoneWeightBps !== 10000) {
    return fail("milestone weights must add up to 10000 bps");
  }

  const founderHours = input.founderHoursPerDay * input.handoverDays;
  const founderCostCents = Math.round(
    founderHours * input.founderRateCents,
  );
  const documentationCostCents = Math.round(
    input.documentationHours * input.founderRateCents,
  );
  const buyerPmCostCents = Math.round(
    input.buyerPmHours * input.buyerPmRateCents,
  );
  const totalCostCents =
    founderCostCents + documentationCostCents + buyerPmCostCents;
  const holdbackCents = applyRate(input.priceCents, input.holdbackBps);
  const releases = input.milestones.map((milestone) => ({
    day: milestone.day,
    deliverable: milestone.deliverable,
    weightBps: milestone.weightBps,
    releaseCents: applyRate(holdbackCents, milestone.weightBps),
  }));

  return ok({
    founderHours,
    founderCostCents,
    documentationCostCents,
    buyerPmCostCents,
    totalCostCents,
    totalCostBpsOfPrice: divide(totalCostCents, input.priceCents),
    costPerHandoverDayCents: Math.round(totalCostCents / input.handoverDays),
    holdbackCents,
    finalReleaseCents: input.priceCents - holdbackCents,
    milestoneCount: input.milestones.length,
    milestoneWeightBps,
    releases,
  });
}

export type AcceptanceCriterion = {
  key: string;
  met: boolean;
};

export type AcceptanceOutcome = {
  total: number;
  met: number;
  metBps: Bps;
  passed: boolean;
  missing: string[];
};

/**
 * A handover is complete when the buyer can run the business without the
 * seller, not when the seller has sent the last file. The gate counts explicit
 * criteria and requires all of them; a percentage above the threshold is not
 * enough, because the one missing criterion is usually the one that transfers
 * customer trust. The criteria are inputs so each deal can define its own.
 */
export function acceptanceGate(
  criteria: AcceptanceCriterion[],
  thresholdBps: Bps,
): Result<AcceptanceOutcome> {
  if (!Array.isArray(criteria) || criteria.length === 0) {
    return fail("at least one acceptance criterion is required");
  }
  const thresholdError = checkBps(thresholdBps, "acceptance threshold");
  if (thresholdError) {
    return fail(thresholdError);
  }
  const seen = new Set<string>();
  let met = 0;
  const missing: string[] = [];
  for (const criterion of criteria) {
    if (
      typeof criterion.key !== "string" ||
      criterion.key.trim() === ""
    ) {
      return fail("each acceptance criterion needs a key");
    }
    if (seen.has(criterion.key)) {
      return fail("acceptance criterion keys must be unique");
    }
    seen.add(criterion.key);
    if (criterion.met) {
      met += 1;
    } else {
      missing.push(criterion.key);
    }
  }
  const metBps = divide(met, criteria.length);
  return ok({
    total: criteria.length,
    met,
    metBps,
    passed: met === criteria.length && metBps >= thresholdBps,
    missing,
  });
}

代码把「成本」和「托管」分开,是因为它们不是同一笔钱:成本是卖方投入的时间与买方投入的管理,托管是买方还没付的价款。把两者相加会重复计算,把两者混为一谈会让卖方以为「托管释放了就等于交接赚钱了」。acceptanceGate 的 passed 要求 met === total,即使 metBps 超过阈值也不放行;这是刻意设计的,因为最后一项通常是最难交的客户信任。

把 Nord 代入:创始人 240 小时、成本 2,040,000 美分;文档 340,000 美分;买方 PM 720,000 美分;总成本 3,100,000 美分,占交易价 3,011 个基点;每天成本 51,667 美分;托管 1,029,600 美分,按 30/40/30 释放 308,880、411,840、308,880 美分;剩余价款 9,266,400 美分。四条验收标准全过,passed 才为真。

小结

  • 交接分三段:0–30 天关系、31–60 天运维、61–90 天反向验证;最容易省略的反向验证,决定买方能否独立跑。
  • 知识清单至少四类:客户关系、供应商、运维手册、隐性决策;前三类写文档,第四类靠决策日志和复盘。
  • Nord 的交接成本是 31,000 美元:创始人 240 小时 20,400、文档 3,400、买方 PM 7,200;占 102,960 美元交易价的 30.1%。
  • 托管按 10% 算是 10,296 美元,按 30/40/30 释放;节点是上限,不是自动触发器,验收没过就不释放。
  • 验收用四条硬标准:客户续约、部署回滚、月结对账、支持工单;全部通过才算完成,不能用 80% 放行。
  • 交接完成后保留 30–60 天问询窗口,写进交易文件;口头承诺在交割三个月后通常已经失效。
  • LedgerLite 偏产品交接,Tidecast 几乎没有组织知识可交;Nord 的知识在两个人脑子里,所以交接成本最高。

阅读导航:上一节:9.2 竞业、保密与留任 · 下一节:10.1 税后所得与分期 。

常见错误与边界

  1. 把交接当信息倾倒。 开两天会、发一个网盘链接不等于交接;买方要独立做过一次,才算学会。
  2. 只交代码和文档。 客户关系、供应商关系、隐性决策不在代码里;只交技术资产,交割后第一个续约就会出问题。
  3. 跳过反向验证。 创始人讲过了、买方听懂了,不等于买方能做;61–90 天的反向陪跑不能省。
  4. 按日历释放托管。 第 60 天到了但部署还没验收,释放就不应该发生;节点是上限,验收才是触发器。
  5. 用「80% 完成」放行。 没通过的那一项通常是客户信任或现金流;四条硬标准全部通过才算完成。
  6. 低估创始人时间。 4 小时 × 60 天是 240 小时,相当于 Nord 两个多月的咨询产能;交接期还要维护老客户时,实际投入更高。
  7. 买方 PM 不计成本。 买方要投入项目管理、验收和沟通;60 小时 × 120 美元是 7,200 美元,不是零。
  8. 交接期没有问询窗口。 交割三个月后买方还会因为老客户问题打电话;窗口要写进交易文件,按小时计费或约定上限。
  9. 把托管当罚款。 托管是「没交完先不付完」,不是价格扣减;卖方要把它排进现金流,而不是当成损失。
  10. 忽略知识转移对估值的影响。 买方在尽调时会把「知识集中在两个人身上」折价;交接计划越具体,折价越少。

本节的边界是「关键知识可清单化、陪跑工时可估算、验收标准可事先约定」的静态模型。它不覆盖跨境雇佣、竞业与留任的法律效力、税务处理,也不保证买方一定能把客户关系接过去;这些要把交易文件与交接计划交给律师和买方团队一起审阅。交接完成后,交易的钱才真正变成卖方的税后所得,见 10.1 税后所得与分期 ;代码与账号交割的技术细节见 8.3 代码、域名与数据交割 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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