《独立开发者的商业指南·合同、应收与风险》13.1 合同条款矩阵

三份合同、八个条款、一张矩阵。本节把 LedgerLite 的 Net 15 与 12 个月费用上限、Tidecast 的平台月结与买断授权、Nord 的 Net 30 与合同金额 100% 上限放进同一张表,每格同时给出条款值和对应金额,再用十项评分解释 8、5、6 三个分数。

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

卷二前 12 章分别拆过合同的八个部分、责任上限、账期、坏账、保险、IP 与渠道。拆开讲有一个副作用:读者知道每个条款重要,却不知道在自己的合同里先看哪一条。收口要做的事,是把条款重新压回一张表,让每条条款旁边站着一个金额。

这一节搭一张 8 列 × 3 行的合同矩阵。列是条款维度,行是三个案例;每格不是一句描述,而是「条款值 + 它在本卷算出的金额」。矩阵搭完后,再用十项评分给三份合同打一个 0 到 10 的强度分,最后说明这张表怎么带进卷三。

为什么合同要压成矩阵

合同文本按时间顺序写:当事人、服务范围、价格、期限、责任、IP、数据、终止。这个顺序适合阅读,不适合比较。三家公司的合同放在一起时,真正要比的是同一个维度上的不同选择:账期是 Net 15 还是 Net 30,责任上限是 12 个月费用还是合同金额 100%。

矩阵的作用是强制对齐。它要求每一行回答同一个问题,每一格给出同一种东西。只要有一格写不出金额,就说明这条条款还没有被翻译成财务后果;在本书的口径里,算不出来的条款等于没有条款。

矩阵也不能只有条款值。Net 30 是一个术语,12,684 美元才是它对现金的占用。把两者放在同一格里,读者才能看到「同样的 Net 30,对不同生意是不同量级的压力」。这也是本节所有金额都来自前 12 章、而不是重新假设一套数字的原因。

三案例的八列矩阵

下表是矩阵的主体。每格第一行是条款值,第二行是对应金额;金额口径来自本卷已发布的章节,不是本节新造的。

条款LedgerLiteTidecastNord Studio
当事人新加坡 Pte Ltd;260 家订阅客户
$240,240 年收入
Wyoming LLC;Steam 占净得 74.7%
$138,171 净收入
英国 Ltd;两名合伙人各 50%
$154,320 年收入
账期Net 15;平均占用 $9,873
实际 DSO 18.2 天
平台月结 30 天;未结算 $8,597.25
90 天收入暴露 $19,079
Net 30;平均占用 $12,684
实际 DSO 26.6 天
责任上限12 个月费用;公司层面 $240,240
单份合同 $924
平台协议单方上限;按平台费 $44,215 计
卖方无谈判空间
合同金额 100%;单项目 $8,500
年咨询收入 $112,200
续约窗口年付 cohort 60 天窗口 $11,847
30 天窗口 $5,924
平台定义,无合同锁定
可锁定金额 $0
咨询续约率 70%;风险收入 $33,660
60 天额外窗口 $5,533
SLA99.5%;月度抵扣 10%
年上限 $24,024
平台可用性条款;停机一周 $2,657
12 月一周 $8,060
99.0%;产品线 5% 抵扣 $2,106
若含咨询线 $7,716
IP 归属自有代码与商标;AGPL 合规成本 $85,672
占 ARR 35.7%
音乐买断、不得转授权;3 年保底 $180,000
整体出售锚 $248,708
交付物归客户、产品归工作室
单合同交付物 $8,500
数据条款DPA + SCC;首年 $5,400
泄露自留 $10,000
客户数据在平台,无导出承诺
可保证金额 $0
无 DPA,泄露通知未约定
可保证金额 $0
终止提前 30 天;通知期收入 $5,924
占 SDE 4.5%
平台下架;剩余授权缺口 $38,000
Steam 占销量 80%
合伙人退出买断 $77,220
分 24 个月支付

这张表读起来密,但结构很清楚。LedgerLite 的每一格都有金额,因为订阅合同的条款可以被拆到客户和月份;Tidecast 有两格是 0,不是因为没有风险,而是因为平台条款不允许卖方锁定任何金额;Nord 的金额最大的一格不在合同里,在合伙人退出条款里。

每格金额从哪里来

矩阵里最容易被误读的是责任上限。LedgerLite 的公司层面上限是 $240,240,但单份合同只有 $924;这两个数字不矛盾,前者是 260 份合同加总后的风险预算,后者是任何一家客户能索赔的上限。比较三家时要用同一个层级:公司对公司,合同对合同。拿 $240,240 去比 Nord 单项目的 $8,500,会得出「LedgerLite 风险高 28 倍」的错误结论。

账期那一行的金额是平均现金占用,不是年末应收。LedgerLite 的 Net 15 对应 $9,873,Nord 的 Net 30 对应 $12,684,Tidecast 的平台月结 30 天对应 $8,597.25。年末应收是时点余额,平均占用是全年水平;矩阵用后者,因为条款影响的是全年现金,不是某一天的表。

续约窗口的金额是「窗口锁定的收入」,不是「续约概率」。LedgerLite 的年付 cohort 是 $72,072,60 天窗口锁 $11,847;这 $11,847 是合同期内已经确定的收入,不是明年能不能续的期望值。把锁定收入和续约概率混在一起,会把一个条款问题算成一个销售问题。

SLA 的金额是抵扣上限,不是停机损失。LedgerLite 的年度最大抵扣是 $24,024,Nord 的产品线是 $2,106;如果 Nord 把咨询线也算进 SLA 基数,抵扣会变成 $7,716。同一份 99.0% 的承诺,基数不同,金额差 3.7 倍。

IP 那一行,LedgerLite 放的是 AGPL 合规成本 $85,672,Tidecast 放的是三年授权保底 $180,000,Nord 放的是单合同交付物 $8,500。三个数字不是同一种东西,但它们回答同一个问题:IP 条款一旦出事,最坏要花多少钱才能把资产拿回来或换掉。

数据条款的两个 0 需要单独解释。Tidecast 的客户数据在平台,Nord 没有 DPA,矩阵给 0 不是说数据风险为零,而是说「合同没有给出可计算的保障金额」。0 在矩阵里是一个警告,不是一个安全值。

终止那一行,LedgerLite 是通知期内的收入 $5,924,Tidecast 是剩余授权缺口 $38,000,Nord 是合伙人买断 $77,220。三者的共同点是:终止条款的金额往往比责任上限更接近真实退出成本,因为它包含了过渡、替换和重新签约的时间。

条款强度评分:十项,满分 10

金额回答「最坏多少」,评分回答「条款写全了没有」。评分用十项检查,每项 1 分,满分 10 分。评分标准是教学口径,不是法律意见;它的用途是横向比较,不是判断合同是否有效。

#检查项LedgerLiteTidecastNord Studio
1签约主体与收款主体一致✓✓✓
2验收条件可判定✓✓✗
3账期与逾期后果明确✓✓✓
4责任上限有明确基数✓✗✓
5续约窗口双向对称✗✗✗
6SLA 有统计周期与抵扣基数✓✓✓
7IP 归属或授权范围明确✓✓✓
8数据条款含通知时限✓✗✗
9终止后的过渡与导出可计算✗✗✗
10保险要求与保额匹配✓✗✓
合计856

三个案例里有两项是共同失分:续约窗口双向对称、终止后的过渡与导出;数据条款则是 Tidecast 与 Nord 的共同失分。这不是巧合。独立开发者的合同通常由客户或平台提供,卖方只能在「接受」和「不签」之间选;能改的往往是金额和期限,改不了的是对称性和退出安排。

LedgerLite 拿 8 分,落在强档。它失掉的两项是续约窗口和终止过渡:年付客户有 60 天窗口,但窗口是单向的,客户可以走,卖方不能同等提前退出;终止后的数据导出也没有写进合同。这两项不影响它现在的收入,但会影响买方的尽调结论。

Tidecast 拿 5 分,落在弱档。它失掉的是责任上限、续约、数据、终止和保险。平台协议的责任上限由平台单方定义,卖方没有谈判空间;客户数据在平台,导出承诺为零;平台下架后的剩余授权缺口是 $38,000。这三项加起来,解释了为什么它的合同矩阵看起来「简单」,实际风险却不低。

Nord 拿 6 分,落在及格档。它失掉的是验收、续约、数据和终止。咨询合同的验收条件偏软,续约率 70% 是观察值而不是合同权利,数据条款缺失,合伙人退出只有口头约定。最后一项最贵:$77,220 的买断金额没有写进任何文件,退出时只能重新谈。

评分和金额要一起看。LedgerLite 的 8 分对应 $240,240 的责任上限和 $13,337 的未覆盖缺口;Tidecast 的 5 分对应 $180,000 的授权保底和 $232,430 的总缺口;Nord 的 6 分对应 $8,500 的单项目上限和 $27,462 的缺口。分数低的合同不一定金额大,但金额大的缺口通常出现在分数低的合同里。

矩阵怎么读:三组对照

第一组是责任上限与保险。LedgerLite 的上限 $240,240 由 $1M 网络安全险承接,单次最大损失 $280,947 下缺口只有 $10,000;Nord 的单项目上限 $8,500 由 $500K E&O 承接,专业责任缺口 $5,000;Tidecast 没有责任上限谈判空间,也没有保险,IP 缺口 $144,000、平台缺口 $88,430。三家的差别不在保额大小,而在「上限、保险、缺口」能不能连成一条线。责任上限的计算口径见 2.1 责任上限:最坏损失怎么算 ,保险与缺口的联动见 12.3 保险、备份与应急预案的联动 。

第二组是账期与应收。LedgerLite 的 Net 15 占用 $9,873,Nord 的 Net 30 占用 $12,684,Tidecast 的平台月结 30 天占用 $8,597.25。绝对值上 Nord 最高,但占 SDE 的比例是另一回事:LedgerLite 的应收是 SDE 的 9.2%,Tidecast 的平台未结算是 7.1%,Nord 是 32.8%。同样的 30 天,对 SDE 只有 $34,320 的 Nord 是三分之一个季度的 owner earnings。账期的完整成本见 3.1 账期的真实成本:Net 15、30、60 差多少 。

第三组是 IP 与终止。LedgerLite 的 AGPL 合规成本 $85,672 相当于一年 ARR 的 35.7%;Tidecast 的三年授权保底 $180,000 相当于整体出售锚 $248,708 的 72.4%;Nord 的合伙人买断 $77,220 相当于 4.5× SDE 估值 $154,440 的 50.0%。这三个比例说明,IP 和终止条款不是「出事才用」的备份条款,它们在正常退出时就会吃掉一大块交易对价。

矩阵还可以反过来用:先看哪一格的金额占 SDE 比例最高,再回去看那条条款。LedgerLite 最高的是责任上限(1.84× SDE),Tidecast 最高的是授权保底(1.49× SDE),Nord 最高的是合伙人买断(2.25× SDE)。三家的第一优先级因此不同:LedgerLite 先看保险,Tidecast 先看授权终止,Nord 先看合伙人退出。

写成代码:contract-matrix.ts

矩阵的代码只做三件事:固定八个条款的顺序、校验每格都有值和金额、用十项检查算强度分。它不判断条款是否合法,也不替读者谈判;它只保证三份合同被放在同一套坐标里。

buildContractMatrix 拒绝缺列的合同,因为缺一列就不是同一张表;scoreContract 要求恰好十项检查,少一项就报错,而不是按零分算——没检查不等于没风险。最弱案例按得分率排序,分数相同时保持输入顺序,保证多次运行结果一致。

// file: src/contract/contract-matrix.ts
// tier: L1
// verify: node --test test/contract/contract-matrix.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 ClauseKey =
  | "parties"
  | "payment"
  | "liability"
  | "renewal"
  | "sla"
  | "ip"
  | "data"
  | "termination";

export type ClauseCell = {
  value: string;
  amountCents: Cents;
};

export type ContractCase = {
  name: string;
  clauses: Record<ClauseKey, ClauseCell>;
  strengthChecks: boolean[];
};

export type MatrixClause = {
  key: ClauseKey;
  label: string;
  value: string;
  amountCents: Cents;
};

export type MatrixRow = {
  name: string;
  clauses: MatrixClause[];
  score: number;
  maxScore: number;
  scoreBps: Bps;
  grade: "strong" | "adequate" | "weak";
};

export type MatrixResult = {
  rows: MatrixRow[];
  weakestCase: string;
  weakestScoreBps: Bps;
};

export const CLAUSE_ORDER: ClauseKey[] = [
  "parties",
  "payment",
  "liability",
  "renewal",
  "sla",
  "ip",
  "data",
  "termination",
];

export const CLAUSE_LABELS: Record<ClauseKey, string> = {
  parties: "当事人",
  payment: "账期",
  liability: "责任上限",
  renewal: "续约窗口",
  sla: "SLA",
  ip: "IP 归属",
  data: "数据条款",
  termination: "终止",
};

/** Ten checks, one point each. The list is a teaching rubric, not a legal test. */
export const STRENGTH_ITEMS: string[] = [
  "签约主体与收款主体一致",
  "验收条件可判定",
  "账期与逾期后果明确",
  "责任上限有明确基数",
  "续约窗口双向对称",
  "SLA 有统计周期与抵扣基数",
  "IP 归属或授权范围明确",
  "数据条款含通知时限",
  "终止后的过渡与导出可计算",
  "保险要求与保额匹配",
];

export const STRENGTH_MAX = STRENGTH_ITEMS.length;

function validAmount(value: Cents): boolean {
  return Number.isInteger(value) && value >= 0;
}

function isClauseKey(value: string): value is ClauseKey {
  return (CLAUSE_ORDER as string[]).includes(value);
}

/**
 * Scores a contract on the ten-item rubric. Every item must be present as a
 * boolean; a missing item is a gap in the review, not a silent zero. The
 * grade lines are fixed teaching thresholds: 8 strong, 6 adequate, below 6
 * weak. They compare review completeness, not legal enforceability.
 */
export function scoreContract(
  checks: boolean[],
): Result<{ score: number; maxScore: number; scoreBps: Bps; grade: MatrixRow["grade"] }> {
  if (checks.length !== STRENGTH_MAX) {
    return fail(`expected ${STRENGTH_MAX} checks, got ${checks.length}`);
  }
  let score = 0;
  for (const check of checks) {
    if (typeof check !== "boolean") {
      return fail("every check must be a boolean");
    }
    if (check) {
      score += 1;
    }
  }
  const scoreBps = divide(score, STRENGTH_MAX);
  const grade: MatrixRow["grade"] =
    scoreBps >= 8000 ? "strong" : scoreBps >= 6000 ? "adequate" : "weak";
  return ok({ score, maxScore: STRENGTH_MAX, scoreBps, grade });
}

/**
 * Builds the eight-column matrix. Each cell carries both the clause value and
 * the amount that the clause puts at stake in the current chapter. The two
 * are deliberately separate: "Net 30" is a term, 12,684 dollars is its cash
 * consequence. The weakest case is the lowest score ratio, with input order
 * breaking ties so repeated runs agree.
 */
export function buildContractMatrix(cases: ContractCase[]): Result<MatrixResult> {
  if (cases.length === 0) {
    return fail("at least one contract case is required");
  }

  const rows: MatrixRow[] = [];
  for (const item of cases) {
    if (item.name.trim() === "") {
      return fail("case name cannot be empty");
    }
    const clauses: MatrixClause[] = [];
    for (const key of CLAUSE_ORDER) {
      const cell = item.clauses[key];
      if (cell === undefined) {
        return fail(`${item.name} is missing clause ${key}`);
      }
      if (!isClauseKey(key)) {
        return fail(`unknown clause ${key}`);
      }
      if (cell.value.trim() === "") {
        return fail(`${item.name} clause ${key} needs a value`);
      }
      if (!validAmount(cell.amountCents)) {
        return fail(`${item.name} clause ${key} needs a non-negative amount`);
      }
      clauses.push({
        key,
        label: CLAUSE_LABELS[key],
        value: cell.value,
        amountCents: cell.amountCents,
      });
    }

    const scored = scoreContract(item.strengthChecks);
    if (!scored.ok) {
      return scored;
    }
    rows.push({
      name: item.name,
      clauses,
      score: scored.value.score,
      maxScore: scored.value.maxScore,
      scoreBps: scored.value.scoreBps,
      grade: scored.value.grade,
    });
  }

  let weakestCase = rows[0].name;
  let weakestScoreBps = rows[0].scoreBps;
  for (const row of rows) {
    if (row.scoreBps < weakestScoreBps) {
      weakestCase = row.name;
      weakestScoreBps = row.scoreBps;
    }
  }

  return ok({ rows, weakestCase, weakestScoreBps });
}

把三份合同代进去,结果是 LedgerLite 8 分、Tidecast 5 分、Nord 6 分,最弱案例是 Tidecast。代码给出的 weakestCase 不是为了排名,而是为了提醒:下一节算风险敞口时,Tidecast 的 0 应收最容易被误读成「没有应收风险」。

小结

  • 合同矩阵把八个条款压成同一套坐标:当事人、账期、责任上限、续约、SLA、IP、数据、终止。
  • 每格必须同时有条款值和金额;只有条款值,读者知道术语,不知道后果。
  • LedgerLite 的矩阵最完整:Net 15 占用 $9,873,12 个月费用上限 $240,240,SLA 年抵扣上限 $24,024。
  • Tidecast 的矩阵最薄:应收为零,但平台未结算 $8,597.25,责任上限不可谈,3 年授权保底 $180,000。
  • Nord 的矩阵里金额最大的一格不在合同正文,在合伙人退出:买断 $77,220,分 24 个月。
  • 十项强度评分:LedgerLite 8 分、Tidecast 5 分、Nord 6 分;三家共同失分在续约对称性与终止过渡。
  • 矩阵的第一读法是「金额占 SDE 比例」,不是「条款数量」;比例最高的那一格决定先谈什么。
  • 代码只校验结构和算分,不提供法律意见;条款效力由适用法律决定。

阅读导航:上一节:12.3 保险、备份与应急预案的联动 · 下一节:13.2 应收与风险敞口 。

常见错误与边界

  1. 把矩阵当成合同清单。 矩阵只收八个会产生财务后果的维度;保密、通知、不可抗力等条款重要,但不在这张表里。
  2. 用公司层面上限比单份合同上限。 LedgerLite 的 $240,240 是 260 份合同加总,单份只有 $924;层级不同,比较没有意义。
  3. 把账期金额当年末应收。 矩阵用的是全年平均占用;年末余额是时点数,两者不能直接替换。
  4. 把续约窗口当续约概率。 窗口锁定的是合同期内的收入,不是明年续约的可能性。
  5. 把 SLA 抵扣当停机损失。 抵扣是收入折减,停机损失还包括支持成本和客户流失。
  6. 把 0 当成没有风险。 Tidecast 的数据条款和 Nord 的 DPA 都记 0,含义是「合同没有给出可计算保障」,不是「风险为零」。
  7. 只看得分不看金额。 8 分的合同也可能有 $13,337 的缺口;分数衡量写全程度,金额衡量最坏后果。
  8. 试图一次补齐十项。 三家的共同失分在续约与终止;先补这两项,比逐条修改格式有效。
  9. 把评分当法律意见。 评分是教学口径,不判断条款是否可执行;具体效力以适用法律和合同文本为准。
  10. 把矩阵做完就结束。 矩阵是下一节风险敞口的输入;条款金额不进入敞口表,就只是一张好看的对照表。

本节的边界是「条款值到金额的映射」:只覆盖八个条款维度、十项强度检查和三个案例,不展开合同解释、判例和法条。所有金额来自本卷前 12 章的教学设定;正式交易中的合同审查应由具备执业资格的专业人士完成。下一节会把矩阵里的金额转成应收、坏账准备、风险期望损失与未覆盖缺口,做一次横向对照。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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