TL;DR — Babel’s engine has one formula: a leverage-weighted composite score. For each clause, it matches the extracted value to a band (via range, enum, or predicate), scores the band against who has power in the deal, and picks the highest-scoring band — with a deliberate tie-break toward “market.” Posture is classified by a ±0.2 threshold on the raw founder/investor scores. No Nash solver, no game tree — just weighted scoring that tilts with leverage and a fixed preference for consensus.

For links to all the posts in this series, see Babel.


The Term Sheet: What We’re Actually Reviewing

A term sheet is a non-binding summary of the economic, governance, and protective terms of a venture capital investment. Babel currently covers 15 clause types, each defined as a structured object in packages/batna/seed/bands.json:

# clause_key Title Value type Unit
1 exclusivity No-Shop / Exclusivity number days
2 preemptive_pro_rata Pre-emptive Right enum none, limited_next_round, full_ongoing
3 rofo Right of First Offer number days
4 rofr Right of First Refusal number days
5 tag_along Tag Along / Co-Sale enum none, pro_rata_only, full_on_threshold
6 drag_along Drag Along object predicate
7 founder_vesting Founder Vesting & Clawback object predicate
8 anti_dilution Anti-dilution enum none, narrow_wa, broad_wa, full_ratchet
9 reserved_matters Affirmative / Veto Rights enum narrow_list, market_list, broad_list
10 exit Exit Timeline & Mode number years
11 liquidation_preference Liquidation Preference enum 1x_np, 1x_p, >1x_np, >1x_p
12 board Board Composition enum founder_majority, balanced, investor_weighted
13 information_rights Information Rights enum quarterly_min, monthly_kpi_q_fin, monthly_all
14 option_pool ESOP Pool Size number percent
15 pay_to_play Pay-to-Play enum none, soft, hard

Each clause carries:

  • Bands — named buckets (typically 3–4) mapping a value or predicate to a founder_score and investor_score (both 0–1).
  • Rationale — one line per band so the copilot can explain why.
  • Trades — actionable swaps (“Shorter window ↔ weekly diligence updates”).
  • Links — related clause keys used to build the graph’s second-order edges.
  • Regex hints — patterns the extraction pipeline uses for clause detection.

Adding a new clause or updating “what market means” is editing JSON. No code changes required.


Institutionalising Tribal Knowledge

The real knowledge in VC deals lives in people’s heads and in precedent: “we usually do 30–45 days exclusivity,” “founders push for 1x non-participating,” “if they ask for full ratchet you give something else.” That’s tribal. It’s hard to onboard on, hard to compare across firms, and impossible to feed directly into software.

The first design choice was to write it down in one place and in a shape a program can use - shout out to Karn for doing most of the work here. That place is bands.json. For every clause we define exactly what the tribal knowledge is, encoded as ranges, enums, or predicates.

Example: Exclusivity. Three bands:

Band Range (days) Founder score Investor score Rationale
short [15, 30] 1.0 0.2 Founder flexibility
market [30, 45] 0.6 0.6 Balanced convention
long [45, 60] 0.2 1.0 Investor completeness

Example: Liquidation Preference. Four bands derived from two dimensions — multiple and participation — collapsed into an enum:

Enum value Meaning Founder score Investor score
1x_np 1× non-participating 1.0 0.3
1x_p 1× participating 0.5 0.7
>1x_np >1× non-participating 0.3 0.8
>1x_p >1× participating 0.1 1.0

The derivation logic in banding.py (_derive_value_for_band) combines the extracted liq_multiple and participation attributes into the correct enum:

# Simplified from banding.py
mult = attrs.get("liq_multiple", 1.0)
part = attrs.get("participation", "non-participating")
if mult <= 1.0:
    value = "1x_np" if "non" in part else "1x_p"
else:
    value = ">1x_np" if "non" in part else ">1x_p"

We also encode links between clauses and trades that span clauses. So the system doesn’t just say “this is investor-friendly”; it can say “if you give on exclusivity, you could ask for X on ROFR.”

All of that is pure data. No code in the band definitions — just ranges, enums, scores, and text. The experience of many deals gets collapsed into one auditable, versioned artifact that both the engine and the copilot read.


Leverage

Leverage is a pair of weights: { investor, founder } summing to 1. Default is { investor: 0.6, founder: 0.4 } — a slight tilt toward the investor, reflecting typical Series A dynamics (investor often has more alternatives and capital at risk). At 0.5 / 0.5 the recommendation is purely band-driven; as leverage moves away from balanced, the chosen band tilts toward the stronger party. In practice leverage can come from a questionnaire (runway, competing term sheets, round size) or be set per transaction. The math doesn’t care where the number came from.

The leverage pair is stored per document as leverage_json in the database, so every clause in the same term sheet shares the same leverage context. The default 0.6 / 0.4 split is baked into config.ts and band_map.py.


Band Matching

Band matching is clause-specific and supports three modes, implemented identically in engine.ts (frontend) and band_map.py (backend):

Range match (numeric attributes)

Value (v) falls in ([lo, hi]) — inclusive on both ends. So a boundary value (e.g. exclusivity at exactly 30 days) can match two bands — both short [15, 30] and market [30, 45]. When that happens, the tie-break rule below picks one band deterministically.

[ \text{match}(b, v) = \begin{cases} \text{true} & \text{if } b.\text{range}[0] \leq v \leq b.\text{range}[1] \ \text{false} & \text{otherwise} \end{cases} ]

Used for: exclusivity (days), rofo (days), rofr (days), exit (years), option_pool (percent).

Enum match (categorical attributes)

Exact string equality.

[ \text{match}(b, v) = \begin{cases} \text{true} & \text{if } b.\text{enum_match} = v \ \text{false} & \text{otherwise} \end{cases} ]

Used for: liquidation_preference, anti_dilution, tag_along, preemptive_pro_rata, reserved_matters, board, information_rights, pay_to_play.

Predicate match (complex attributes)

If neither range nor enum_match is present, the band always matches. The predicate string (e.g., "drag after ≥6y AND floor ≥2.0x AND qualified buyer") is descriptive; the backend’s _derive_value_for_band maps complex attribute objects to the correct band before matching.

Used for: drag_along, founder_vesting — clauses where the answer depends on multiple interacting fields (thresholds, timelines, floors, cliffs).


Composite Score

For a band (b) and leverage (\ell):

[ \text{score}(b, \ell) = b.\text{investor_score} \times \ell_{\text{inv}} + b.\text{founder_score} \times \ell_{\text{fdr}} ]

This is the only formula in the engine. A band that’s great for the investor (high investor_score) scores higher when investor leverage is high; a founder-friendly band scores higher when founder leverage is high. We’re not solving a full game; we’re weighted scoring so that “recommended” tilts with who has the upper hand.

Worked example — Exclusivity at 37 days

  • Matches market band: [30, 45]
  • founder_score = 0.6, investor_score = 0.6
  • With default leverage { inv: 0.6, fdr: 0.4 }:

[ \text{score} = 0.6 \times 0.6 + 0.6 \times 0.4 = 0.36 + 0.24 = 0.60 ]

Worked example — Exclusivity at 25 days with investor-heavy leverage

  • Matches short band: [15, 30]
  • founder_score = 1.0, investor_score = 0.2
  • With leverage { inv: 0.8, fdr: 0.2 }:

[ \text{score} = 0.2 \times 0.8 + 1.0 \times 0.2 = 0.16 + 0.20 = 0.36 ]

The low score signals that a founder-friendly outcome is unlikely given the leverage distribution. Same value, different leverage → different recommendation.

Worked example — Anti-dilution broad_wa with founder leverage

  • Matches broad_wa band via enum match ✓
  • founder_score = 0.4, investor_score = 0.8
  • With founder-leaning leverage { inv: 0.3, fdr: 0.7 }:

[ \text{score} = 0.8 \times 0.3 + 0.4 \times 0.7 = 0.24 + 0.28 = 0.52 ]

Moderate score — the investor-friendly band is tempered by founder leverage. Compare with default leverage:

[ \text{score}_{\text{default}} = 0.8 \times 0.6 + 0.4 \times 0.4 = 0.48 + 0.16 = 0.64 ]

Higher score at default leverage because the investor tilt aligns with the band’s investor-friendly nature.


Band Selection with Tie-Breaking

When multiple bands match (overlapping ranges, or predicate bands that always match):

  1. Filter all matching bands.
  2. Sort by composite score descending.
  3. Tie-break: if any matching band is named "market" (case-insensitive), prefer it.
  4. Return the top band.
// From engine.ts — pickBand()
const sorted = matches.sort((a, b) => compositeScore(b, lev) - compositeScore(a, lev));
const market = sorted.find(b => b.name.toLowerCase() === "market");
return market ?? sorted[0] ?? null;

The market preference is deliberate: when the math is close, we nudge toward consensus. That aligns with “consensus as a service” — the goal is one shared answer both sides can reason from, not to maximise either party’s score. Preferring the conventional band at ties reduces argument over edge cases and keeps the output legible. This is the system’s built-in bias: “when in doubt, recommend what’s conventional.”

Worked example — tie-break: Exclusivity at exactly 30 days matches both short [15, 30] and market [30, 45]. With balanced leverage { inv: 0.5, fdr: 0.5 }, short scores (0.2 \times 0.5 + 1.0 \times 0.5 = 0.6) and market scores (0.6 \times 0.5 + 0.6 \times 0.5 = 0.6). The tie-break selects market.

The Python implementation in band_map.py is identical:

def pick_band(bands, attrs, leverage):
    matches = [b for b in bands if _matches(b, attrs)]
    matches.sort(key=lambda b: composite_score(b, leverage), reverse=True)
    market = next((b for b in matches if b["name"].lower() == "market"), None)
    return market or (matches[0] if matches else None)

Posture / Tilt

From the chosen band’s raw scores (not the composite), we classify posture using a ±0.2 threshold:

[ \text{posture} = \begin{cases} \texttt{founder_friendly} & \text{if } f > i + 0.2
\texttt{investor_friendly} & \text{if } i > f + 0.2
\texttt{market} & \text{otherwise} \end{cases} ]

where (f = b.\text{founder_score}) and (i = b.\text{investor_score}).

Why 0.2? It’s a design choice to create a dead zone around balanced bands and avoid noisy posture flips. A band with founder_score = 0.6 and investor_score = 0.5 is market — the 0.1 difference isn’t enough to call it directional. But founder_score = 1.0 and investor_score = 0.2 is clearly founder_friendly (difference = 0.8 > 0.2). The value was chosen to separate “roughly balanced” from “clearly tilted” without being so large that most bands collapse to market.

Note that posture is computed from the band’s intrinsic scores, not the leverage-weighted composite. Leverage affects which band is chosen; posture reflects what that band means regardless of who has power.


The Objective: Consensus, Not Optimisation

Design principles

  1. Deterministic — Same clause, same leverage → same band and posture. No LLM in the loop for the band choice.
  2. Transparent — The user (and the LLM) see which band was chosen, the composite score, and why (rationale + tilt).
  3. Leverage-aware — The recommendation shifts with investor vs founder leverage so it’s useful in real negotiations.
  4. Actionable — Output includes trades so the next step (“give X, ask for Y”) is explicit.
  5. Auditable — Tribal knowledge is in bands.json, versioned in git. The math is a single weighted sum.

What “consensus” means

The consensus is: one band per clause, chosen by leverage-weighted score, with a fixed tie-break to “market.” That single recommendation is what we call consensus — not “everyone agrees,” but “the system has one clear, reproducible answer so both sides can reason from the same fact.”

Cross-clause coherence

The engine is clause-local: it resolves each clause to one band and one posture independently. Cross-clause logic lives in:

  • Trades — suggested swaps that span clause boundaries (“Limit pre-emptive right to next round ↔ higher cap”).
  • Links — the second-order edges in the graph that the copilot can traverse when a user asks “if I give on exclusivity, what should I ask for?”
  • Future: multi-clause solver — optimise across all clauses at once (e.g., “maximise founder score subject to investor acceptance constraint”). The architecture supports this; the current system deliberately keeps the local/global split clean.

What this is not

No Nash solver, no multi-dimensional optimisation, no game tree. Just “which band does this value hit?” and “among those, which has the highest leverage-weighted score, with market preferred when tied.” The simplicity is the point: the math is inspectable, the data is editable, and the LLM is never asked to invent norms.


Next: Agentic Pipeline for Document Review — decomposition pipeline, copilot, and LangGraph term-sheet generation.