* feat: C-Suite expansion — 8 new executive advisory roles Add COO, CPO, CMO, CFO, CRO, CISO, CHRO advisors and Executive Mentor. Expands C-level advisory from 2 to 10 roles with 74 total files. Each role includes: - SKILL.md (lean, <5KB, ~1200 tokens for context efficiency) - Reference docs (loaded on demand, not at startup) - Python analysis scripts (stdlib only, runnable CLI) Executive Mentor features /em: slash commands (challenge, board-prep, hard-call, stress-test, postmortem) with devil's advocate agent. 21 Python tools, 24 reference frameworks, 28,379 total lines. All SKILL.md files combined: ~17K tokens (8.5% of 200K context window). Badge: 88 → 116 skills * feat: C-Suite orchestration layer + 18 complementary skills ORCHESTRATION (new): - cs-onboard: Founder interview → company-context.md - chief-of-staff: Routing, synthesis, inter-agent orchestration - board-meeting: 6-phase multi-agent deliberation protocol - decision-logger: Two-layer memory (raw transcripts + approved decisions) - agent-protocol: Inter-agent invocation with loop prevention - context-engine: Company context loading + anonymization CROSS-CUTTING CAPABILITIES (new): - board-deck-builder: Board/investor update assembly - scenario-war-room: Cascading multi-variable what-if modeling - competitive-intel: Systematic competitor tracking + battlecards - org-health-diagnostic: Cross-functional health scoring (8 dimensions) - ma-playbook: M&A strategy (acquiring + being acquired) - intl-expansion: International market entry frameworks CULTURE & COLLABORATION (new): - culture-architect: Values → behaviors, culture code, health assessment - company-os: EOS/Scaling Up operating system selection + implementation - founder-coach: Founder development, delegation, blind spots - strategic-alignment: Strategy cascade, silo detection, alignment scoring - change-management: ADKAR-based change rollout framework - internal-narrative: One story across employees/investors/customers UPGRADES TO EXISTING ROLES: - All 10 roles get reasoning technique directives - All 10 roles get company-context.md integration - All 10 roles get board meeting isolation rules - CEO gets stage-adaptive temporal horizons (seed→C) Key design decisions: - Two-layer memory prevents hallucinated consensus from rejected ideas - Phase 2 isolation: agents think independently before cross-examination - Executive Mentor (The Critic) sees all perspectives, others don't - 25 Python tools total (stdlib only, no dependencies) 52 new files, 10 modified, 10,862 new lines. Total C-suite ecosystem: 134 files, 39,131 lines. * fix: connect all dots — Chief of Staff routes to all 28 skills - Added complementary skills registry to routing-matrix.md - Chief of Staff SKILL.md now lists all 28 skills in ecosystem - Added integration tables to scenario-war-room and competitive-intel - Badge: 116 → 134 skills - README: C-Level Advisory count 10 → 28 Quality audit passed: ✅ All 10 roles: company-context, reasoning, isolation, invocation ✅ All 6 phases in board meeting ✅ Two-layer memory with DO_NOT_RESURFACE ✅ Loop prevention (no self-invoke, max depth 2, no circular) ✅ All /em: commands present ✅ All complementary skills cross-reference roles ✅ Chief of Staff routes to every skill in ecosystem * refactor: CEO + CTO advisors upgraded to C-suite parity Both roles now match the structural standard of all new roles: - CEO: 11.7KB → 6.8KB SKILL.md (heavy content stays in references) - CTO: 10KB → 7.2KB SKILL.md (heavy content stays in references) Added to both: - Integration table (who they work with and when) - Key diagnostic questions - Structured metrics dashboard table - Consistent section ordering (Keywords → Quick Start → Responsibilities → Questions → Metrics → Red Flags → Integration → Reasoning → Context) CEO additions: - Stage-adaptive temporal horizons (seed=3m/6m/12m → B+=1y/3y/5y) - Cross-references to culture-architect and board-deck-builder CTO additions: - Key Questions section (7 diagnostic questions) - Structured metrics table (DORA + debt + team + architecture + cost) - Cross-references to all peer roles All 10 roles now pass structural parity: ✅ Keywords ✅ QuickStart ✅ Questions ✅ Metrics ✅ RedFlags ✅ Integration * feat: add proactive triggers + output artifacts to all 10 roles Every C-suite role now specifies: - Proactive Triggers: 'surface these without being asked' — context-driven early warnings that make advisors proactive, not reactive - Output Artifacts: concrete deliverables per request type (what you ask → what you get) CEO: runway alerts, board prep triggers, strategy review nudges CTO: deploy frequency monitoring, tech debt thresholds, bus factor flags COO: blocker detection, scaling threshold warnings, cadence gaps CPO: retention curve monitoring, portfolio dog detection, research gaps CMO: CAC trend monitoring, positioning gaps, budget staleness CFO: runway forecasting, burn multiple alerts, scenario planning gaps CRO: NRR monitoring, pipeline coverage, pricing review triggers CISO: audit overdue alerts, compliance gaps, vendor risk CHRO: retention risk, comp band gaps, org scaling thresholds Executive Mentor: board prep triggers, groupthink detection, hard call surfacing This transforms the C-suite from reactive advisors into proactive partners. * feat: User Communication Standard — structured output for all roles Defines 3 output formats in agent-protocol/SKILL.md: 1. Standard Output: Bottom Line → What → Why → How to Act → Risks → Your Decision 2. Proactive Alert: What I Noticed → Why It Matters → Action → Urgency (🔴🟡⚪) 3. Board Meeting: Decision Required → Perspectives → Agree/Disagree → Critic → Action Items 10 non-negotiable rules: - Bottom line first, always - Results and decisions only (no process narration) - What + Why + How for every finding - Actions have owners and deadlines ('we should consider' is banned) - Decisions framed as options with trade-offs - Founder is the highest authority — roles recommend, founder decides - Risks are concrete (if X → Y, costs $Z) - Max 5 bullets per section - No jargon without explanation - Silence over fabricated updates All 10 roles reference this standard. Chief of Staff enforces it as a quality gate. Board meeting Phase 4 uses the Board Meeting Output format. * feat: Internal Quality Loop — verification before delivery No role presents to the founder without passing verification: Step 1: Self-Verification (every role, every time) - Source attribution: where did each data point come from? - Assumption audit: [VERIFIED] vs [ASSUMED] tags on every finding - Confidence scoring: 🟢 high / 🟡 medium / 🔴 low per finding - Contradiction check against company-context + decision log - 'So what?' test: every finding needs a business consequence Step 2: Peer Verification (cross-functional) - Financial claims → CFO validates math - Revenue projections → CRO validates pipeline backing - Technical feasibility → CTO validates - People/hiring impact → CHRO validates - Skip for single-domain, low-stakes questions Step 3: Critic Pre-Screen (high-stakes only) - Irreversible decisions, >20% runway impact, strategy changes - Executive Mentor finds weakest point before founder sees it - Suspicious consensus triggers mandatory pre-screen Step 4: Course Correction (after founder feedback) - Approve → log + assign actions - Modify → re-verify changed parts - Reject → DO_NOT_RESURFACE + learn why - 30/60/90 day post-decision review Board meeting contributions now require self-verified format with confidence tags and source attribution on every finding. * fix: resolve PR review issues 1, 4, and minor observation Issue 1: c-level-advisor/CLAUDE.md — completely rewritten - Was: 2 skills (CEO, CTO only), dated Nov 2025 - Now: full 28-skill ecosystem map with architecture diagram, all roles/orchestration/cross-cutting/culture skills listed, design decisions, integration with other domains Issue 4: Root CLAUDE.md — updated all stale counts - 87 → 134 skills across all 3 references - C-Level: 2 → 33 (10 roles + 5 mentor commands + 18 complementary) - Tool count: 160+ → 185+ - Reference count: 200+ → 250+ Minor observation: Documented plugin.json convention - Explained in c-level-advisor/CLAUDE.md that only executive-mentor has plugin.json because only it has slash commands (/em: namespace) - Other skills are invoked by name through Chief of Staff or directly Also fixed: README.md 88+ → 134 in two places (first line + skills section) * fix: update all plugin/index registrations for 28-skill C-suite 1. c-level-advisor/.claude-plugin/plugin.json — v2.0.0 - Was: 2 skills, generic description - Now: all 28 skills listed with descriptions, all 25 scripts, namespace 'cs', full ecosystem description 2. .codex/skills-index.json — added 18 complementary skills - Was: 10 roles only - Now: 28 total c-level entries (10 roles + 6 orchestration + 6 cross-cutting + 6 culture) - Each with full description for skill discovery 3. .claude-plugin/marketplace.json — updated c-level-skills entry - Was: generic 2-skill description - Now: v2.0.0, full 28-skill ecosystem description, skills_count: 28, scripts_count: 25 * feat: add root SKILL.md for c-level-advisor ClawHub package --------- Co-authored-by: Leo <leo@openclaw.ai>
491 lines
18 KiB
Python
491 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Fundraising Model
|
|
==================
|
|
Cap table management, dilution modeling, and multi-round scenario planning.
|
|
Know exactly what you're giving up before you walk into any negotiation.
|
|
|
|
Covers:
|
|
- Cap table state at each round
|
|
- Dilution per shareholder per round
|
|
- Option pool shuffle impact
|
|
- Multi-round projections (Seed → A → B → C)
|
|
- Return scenarios at different exit valuations
|
|
|
|
Usage:
|
|
python fundraising_model.py
|
|
python fundraising_model.py --exit 150 # model at $150M exit
|
|
python fundraising_model.py --csv
|
|
|
|
Stdlib only. No dependencies.
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import io
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Data structures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@dataclass
|
|
class Shareholder:
|
|
"""A shareholder in the cap table."""
|
|
name: str
|
|
share_class: str # "common", "preferred", "option"
|
|
shares: float
|
|
invested: float = 0.0 # total cash invested
|
|
is_option_pool: bool = False
|
|
|
|
|
|
@dataclass
|
|
class RoundConfig:
|
|
"""Configuration for a financing round."""
|
|
name: str # e.g. "Series A"
|
|
pre_money_valuation: float
|
|
investment_amount: float
|
|
new_option_pool_pct: float = 0.0 # % of POST-money to allocate to new options
|
|
option_pool_pre_round: bool = True # True = pool created before round (dilutes founders)
|
|
lead_investor_name: str = "New Investor"
|
|
share_price_override: Optional[float] = None # if None, computed from valuation
|
|
|
|
|
|
@dataclass
|
|
class CapTableEntry:
|
|
"""A row in the cap table at a point in time."""
|
|
name: str
|
|
share_class: str
|
|
shares: float
|
|
pct_ownership: float
|
|
invested: float
|
|
is_option_pool: bool = False
|
|
|
|
|
|
@dataclass
|
|
class RoundResult:
|
|
"""Snapshot of cap table after a round closes."""
|
|
round_name: str
|
|
pre_money_valuation: float
|
|
investment_amount: float
|
|
post_money_valuation: float
|
|
price_per_share: float
|
|
new_shares_issued: float
|
|
option_pool_shares_created: float
|
|
total_shares: float
|
|
cap_table: list[CapTableEntry]
|
|
|
|
|
|
@dataclass
|
|
class ExitAnalysis:
|
|
"""Proceeds to each shareholder at an exit."""
|
|
exit_valuation: float
|
|
shareholder: str
|
|
shares: float
|
|
ownership_pct: float
|
|
proceeds_common: float # if all preferred converts to common
|
|
invested: float
|
|
moic: float # multiple on invested capital (for investors)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Core cap table engine
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class CapTable:
|
|
"""Manages a cap table through multiple rounds."""
|
|
|
|
def __init__(self):
|
|
self.shareholders: list[Shareholder] = []
|
|
self._total_shares: float = 0.0
|
|
|
|
def add_shareholder(self, sh: Shareholder) -> None:
|
|
self.shareholders.append(sh)
|
|
self._total_shares += sh.shares
|
|
|
|
def total_shares(self) -> float:
|
|
return sum(s.shares for s in self.shareholders)
|
|
|
|
def snapshot(self, label: str = "") -> list[CapTableEntry]:
|
|
total = self.total_shares()
|
|
return [
|
|
CapTableEntry(
|
|
name=s.name,
|
|
share_class=s.share_class,
|
|
shares=s.shares,
|
|
pct_ownership=s.shares / total if total > 0 else 0,
|
|
invested=s.invested,
|
|
is_option_pool=s.is_option_pool,
|
|
)
|
|
for s in self.shareholders
|
|
]
|
|
|
|
def execute_round(self, config: RoundConfig) -> RoundResult:
|
|
"""
|
|
Execute a financing round:
|
|
1. (Optional) Create option pool pre-round (dilutes existing shareholders)
|
|
2. Issue new shares to investor at round price
|
|
Returns a RoundResult with full cap table snapshot.
|
|
"""
|
|
current_total = self.total_shares()
|
|
|
|
# Step 1: Option pool shuffle (if pre-round)
|
|
option_pool_shares_created = 0.0
|
|
if config.new_option_pool_pct > 0 and config.option_pool_pre_round:
|
|
# Target: post-round option pool = new_option_pool_pct of total post-money shares
|
|
# Solve: pool_shares / (current_total + pool_shares + new_investor_shares) = target_pct
|
|
# This requires iteration because new_investor_shares also depends on pool_shares
|
|
# Simplification: create pool based on post-round total (slightly approximated)
|
|
target_post_round_pct = config.new_option_pool_pct
|
|
post_money = config.pre_money_valuation + config.investment_amount
|
|
|
|
# Estimate shares per dollar (price per share)
|
|
price_per_share = config.pre_money_valuation / current_total
|
|
new_investor_shares_estimate = config.investment_amount / price_per_share
|
|
|
|
# Pool shares needed so that pool / total_post = target_pct
|
|
total_post_estimate = current_total + new_investor_shares_estimate
|
|
pool_shares_needed = (target_post_round_pct * total_post_estimate) / (1 - target_post_round_pct)
|
|
|
|
# Check if existing pool is sufficient
|
|
existing_pool = next(
|
|
(s.shares for s in self.shareholders if s.is_option_pool), 0
|
|
)
|
|
additional_pool_needed = max(0, pool_shares_needed - existing_pool)
|
|
|
|
if additional_pool_needed > 0:
|
|
option_pool_shares_created = additional_pool_needed
|
|
# Add to existing pool or create new
|
|
pool_sh = next((s for s in self.shareholders if s.is_option_pool), None)
|
|
if pool_sh:
|
|
pool_sh.shares += additional_pool_needed
|
|
else:
|
|
self.shareholders.append(Shareholder(
|
|
name="Option Pool",
|
|
share_class="option",
|
|
shares=additional_pool_needed,
|
|
is_option_pool=True,
|
|
))
|
|
|
|
# Step 2: Price per share (after pool creation)
|
|
current_total_post_pool = self.total_shares()
|
|
if config.share_price_override:
|
|
price_per_share = config.share_price_override
|
|
else:
|
|
price_per_share = config.pre_money_valuation / current_total_post_pool
|
|
|
|
# Step 3: New shares for investor
|
|
new_shares = config.investment_amount / price_per_share
|
|
|
|
# Step 4: Add investor to cap table
|
|
self.shareholders.append(Shareholder(
|
|
name=config.lead_investor_name,
|
|
share_class="preferred",
|
|
shares=new_shares,
|
|
invested=config.investment_amount,
|
|
))
|
|
|
|
post_money = config.pre_money_valuation + config.investment_amount
|
|
total_post = self.total_shares()
|
|
|
|
return RoundResult(
|
|
round_name=config.name,
|
|
pre_money_valuation=config.pre_money_valuation,
|
|
investment_amount=config.investment_amount,
|
|
post_money_valuation=post_money,
|
|
price_per_share=price_per_share,
|
|
new_shares_issued=new_shares,
|
|
option_pool_shares_created=option_pool_shares_created,
|
|
total_shares=total_post,
|
|
cap_table=self.snapshot(),
|
|
)
|
|
|
|
def analyze_exit(self, exit_valuation: float) -> list[ExitAnalysis]:
|
|
"""
|
|
Simple exit analysis: all preferred converts to common, proceeds split pro-rata.
|
|
(Does not model liquidation preferences — see fundraising_playbook.md for that.)
|
|
"""
|
|
total = self.total_shares()
|
|
price_per_share = exit_valuation / total
|
|
results = []
|
|
for s in self.shareholders:
|
|
if s.is_option_pool:
|
|
continue # unissued options don't receive proceeds
|
|
proceeds = s.shares * price_per_share
|
|
moic = proceeds / s.invested if s.invested > 0 else 0.0
|
|
results.append(ExitAnalysis(
|
|
exit_valuation=exit_valuation,
|
|
shareholder=s.name,
|
|
shares=s.shares,
|
|
ownership_pct=s.shares / total,
|
|
proceeds_common=proceeds,
|
|
invested=s.invested,
|
|
moic=moic,
|
|
))
|
|
return sorted(results, key=lambda x: x.proceeds_common, reverse=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Reporting
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def fmt(value: float, prefix: str = "$") -> str:
|
|
if value == float("inf"):
|
|
return "∞"
|
|
if abs(value) >= 1_000_000:
|
|
return f"{prefix}{value/1_000_000:.2f}M"
|
|
if abs(value) >= 1_000:
|
|
return f"{prefix}{value/1_000:.0f}K"
|
|
return f"{prefix}{value:.2f}"
|
|
|
|
|
|
def print_round_result(result: RoundResult, prev_cap_table: Optional[list[CapTableEntry]] = None) -> None:
|
|
print(f"\n{'='*70}")
|
|
print(f" {result.round_name.upper()}")
|
|
print(f"{'='*70}")
|
|
print(f" Pre-money valuation: {fmt(result.pre_money_valuation)}")
|
|
print(f" Investment: {fmt(result.investment_amount)}")
|
|
print(f" Post-money valuation: {fmt(result.post_money_valuation)}")
|
|
print(f" Price per share: {fmt(result.price_per_share, '$')}")
|
|
print(f" New shares issued: {result.new_shares_issued:,.0f}")
|
|
if result.option_pool_shares_created > 0:
|
|
print(f" Option pool created: {result.option_pool_shares_created:,.0f} shares")
|
|
print(f" ⚠️ Pool created pre-round: dilutes existing shareholders, not new investor")
|
|
print(f" Total shares post: {result.total_shares:,.0f}")
|
|
|
|
print(f"\n {'Shareholder':<22} {'Shares':>12} {'Ownership':>10} {'Invested':>10} {'Δ Ownership':>12}")
|
|
print(" " + "-"*68)
|
|
|
|
prev_map = {e.name: e.pct_ownership for e in prev_cap_table} if prev_cap_table else {}
|
|
|
|
for entry in result.cap_table:
|
|
delta = ""
|
|
if entry.name in prev_map:
|
|
change = (entry.pct_ownership - prev_map[entry.name]) * 100
|
|
delta = f"{change:+.1f}pp"
|
|
elif not entry.is_option_pool:
|
|
delta = "new"
|
|
|
|
invested_str = fmt(entry.invested) if entry.invested > 0 else "-"
|
|
print(
|
|
f" {entry.name:<22} {entry.shares:>12,.0f} "
|
|
f"{entry.pct_ownership*100:>9.2f}% {invested_str:>10} {delta:>12}"
|
|
)
|
|
|
|
|
|
def print_exit_analysis(results: list[ExitAnalysis], exit_valuation: float) -> None:
|
|
print(f"\n{'='*70}")
|
|
print(f" EXIT ANALYSIS @ {fmt(exit_valuation)} (all preferred converts to common)")
|
|
print(f"{'='*70}")
|
|
print(f"\n {'Shareholder':<22} {'Ownership':>10} {'Proceeds':>12} {'Invested':>10} {'MOIC':>8}")
|
|
print(" " + "-"*65)
|
|
for r in results:
|
|
moic_str = f"{r.moic:.1f}x" if r.moic > 0 else "n/a"
|
|
invested_str = fmt(r.invested) if r.invested > 0 else "-"
|
|
print(
|
|
f" {r.shareholder:<22} {r.ownership_pct*100:>9.2f}% "
|
|
f"{fmt(r.proceeds_common):>12} {invested_str:>10} {moic_str:>8}"
|
|
)
|
|
print(f"\n Note: Does not model liquidation preferences.")
|
|
print(f" Participating preferred reduces founder proceeds in most real exits.")
|
|
print(f" See references/fundraising_playbook.md for full liquidation waterfall.")
|
|
|
|
|
|
def print_dilution_summary(rounds: list[RoundResult]) -> None:
|
|
print(f"\n{'='*70}")
|
|
print(f" DILUTION SUMMARY — FOUNDER PERSPECTIVE")
|
|
print(f"{'='*70}")
|
|
|
|
# Find all founders (common shareholders who aren't investors or option pool)
|
|
founder_names = []
|
|
for entry in rounds[0].cap_table:
|
|
if entry.share_class == "common" and not entry.is_option_pool:
|
|
founder_names.append(entry.name)
|
|
|
|
if not founder_names:
|
|
print(" No common shareholders found in initial cap table.")
|
|
return
|
|
|
|
header = f" {'Round':<16}" + "".join(f" {n:<16}" for n in founder_names) + f" {'Total Inv':>12}"
|
|
print(header)
|
|
print(" " + "-" * (16 + 18 * len(founder_names) + 14))
|
|
|
|
for result in rounds:
|
|
cap_map = {e.name: e for e in result.cap_table}
|
|
total_invested = sum(e.invested for e in result.cap_table if not e.is_option_pool)
|
|
row = f" {result.round_name:<16}"
|
|
for name in founder_names:
|
|
pct = cap_map[name].pct_ownership * 100 if name in cap_map else 0
|
|
row += f" {pct:>6.2f}% "
|
|
row += f" {fmt(total_invested):>12}"
|
|
print(row)
|
|
|
|
|
|
def export_csv_rounds(rounds: list[RoundResult]) -> str:
|
|
buf = io.StringIO()
|
|
writer = csv.writer(buf)
|
|
writer.writerow(["Round", "Shareholder", "Share Class", "Shares", "Ownership Pct",
|
|
"Invested", "Pre Money", "Post Money", "Price Per Share"])
|
|
for r in rounds:
|
|
for entry in r.cap_table:
|
|
writer.writerow([
|
|
r.round_name, entry.name, entry.share_class,
|
|
round(entry.shares, 0), round(entry.pct_ownership * 100, 4),
|
|
round(entry.invested, 2), round(r.pre_money_valuation, 0),
|
|
round(r.post_money_valuation, 0), round(r.price_per_share, 4),
|
|
])
|
|
return buf.getvalue()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sample data: typical two-founder Series A/B/C startup
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def build_sample_model() -> tuple[CapTable, list[RoundResult]]:
|
|
"""
|
|
Sample company:
|
|
- 2 founders, started with 10M shares each
|
|
- 1M shares for early advisor
|
|
- Raises Pre-seed → Seed → Series A → Series B → Series C
|
|
"""
|
|
cap = CapTable()
|
|
SHARES_PER_FOUNDER = 4_000_000
|
|
SHARES_ADVISOR = 200_000
|
|
|
|
# Founding state
|
|
cap.add_shareholder(Shareholder("Founder A (CEO)", "common", SHARES_PER_FOUNDER))
|
|
cap.add_shareholder(Shareholder("Founder B (CTO)", "common", SHARES_PER_FOUNDER))
|
|
cap.add_shareholder(Shareholder("Advisor", "common", SHARES_ADVISOR))
|
|
|
|
rounds: list[RoundResult] = []
|
|
prev_cap = cap.snapshot()
|
|
|
|
# Round 1: Pre-seed — $500K at $4.5M pre, 10% option pool created
|
|
r1 = cap.execute_round(RoundConfig(
|
|
name="Pre-seed",
|
|
pre_money_valuation=4_500_000,
|
|
investment_amount=500_000,
|
|
new_option_pool_pct=0.10,
|
|
option_pool_pre_round=True,
|
|
lead_investor_name="Angel Syndicate",
|
|
))
|
|
rounds.append(r1)
|
|
prev_r1 = r1.cap_table[:]
|
|
|
|
# Round 2: Seed — $2M at $9M pre, expand option pool to 12%
|
|
r2 = cap.execute_round(RoundConfig(
|
|
name="Seed",
|
|
pre_money_valuation=9_000_000,
|
|
investment_amount=2_000_000,
|
|
new_option_pool_pct=0.12,
|
|
option_pool_pre_round=True,
|
|
lead_investor_name="Seed Fund",
|
|
))
|
|
rounds.append(r2)
|
|
|
|
# Round 3: Series A — $12M at $38M pre, refresh option pool to 15%
|
|
r3 = cap.execute_round(RoundConfig(
|
|
name="Series A",
|
|
pre_money_valuation=38_000_000,
|
|
investment_amount=12_000_000,
|
|
new_option_pool_pct=0.15,
|
|
option_pool_pre_round=True,
|
|
lead_investor_name="Series A Fund",
|
|
))
|
|
rounds.append(r3)
|
|
|
|
# Round 4: Series B — $25M at $95M pre, refresh pool to 12%
|
|
r4 = cap.execute_round(RoundConfig(
|
|
name="Series B",
|
|
pre_money_valuation=95_000_000,
|
|
investment_amount=25_000_000,
|
|
new_option_pool_pct=0.12,
|
|
option_pool_pre_round=True,
|
|
lead_investor_name="Series B Fund",
|
|
))
|
|
rounds.append(r4)
|
|
|
|
# Round 5: Series C — $40M at $185M pre, refresh pool to 10%
|
|
r5 = cap.execute_round(RoundConfig(
|
|
name="Series C",
|
|
pre_money_valuation=185_000_000,
|
|
investment_amount=40_000_000,
|
|
new_option_pool_pct=0.10,
|
|
option_pool_pre_round=True,
|
|
lead_investor_name="Series C Fund",
|
|
))
|
|
rounds.append(r5)
|
|
|
|
return cap, rounds
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Fundraising Model — Cap Table & Dilution")
|
|
parser.add_argument("--exit", type=float, default=250.0,
|
|
help="Exit valuation in $M for return analysis (default: 250)")
|
|
parser.add_argument("--csv", action="store_true", help="Export round data as CSV to stdout")
|
|
args = parser.parse_args()
|
|
|
|
exit_valuation = args.exit * 1_000_000
|
|
|
|
print("\n" + "="*70)
|
|
print(" FUNDRAISING MODEL — CAP TABLE & DILUTION ANALYSIS")
|
|
print(" Sample Company: Two-founder SaaS startup")
|
|
print(" Pre-seed → Seed → Series A → Series B → Series C")
|
|
print("="*70)
|
|
|
|
cap, rounds = build_sample_model()
|
|
|
|
# Print each round
|
|
prev = None
|
|
for r in rounds:
|
|
print_round_result(r, prev)
|
|
prev = r.cap_table
|
|
|
|
# Dilution summary table
|
|
print_dilution_summary(rounds)
|
|
|
|
# Exit analysis at specified valuation
|
|
exit_results = cap.analyze_exit(exit_valuation)
|
|
print_exit_analysis(exit_results, exit_valuation)
|
|
|
|
# Also print at 2x and 5x for sensitivity
|
|
print("\n Exit Sensitivity — Founder A Proceeds:")
|
|
print(f" {'Exit Valuation':<20} {'Founder A %':>12} {'Founder A $':>14} {'MOIC':>8}")
|
|
print(" " + "-"*56)
|
|
for mult in [0.5, 1.0, 1.5, 2.0, 3.0, 5.0]:
|
|
val = rounds[-1].post_money_valuation * mult
|
|
ex = cap.analyze_exit(val)
|
|
founder_a = next((r for r in ex if r.shareholder == "Founder A (CEO)"), None)
|
|
if founder_a:
|
|
print(f" {fmt(val):<20} {founder_a.ownership_pct*100:>11.2f}% "
|
|
f"{fmt(founder_a.proceeds_common):>14} {'n/a':>8}")
|
|
|
|
print("\n Key Takeaways:")
|
|
final = rounds[-1].cap_table
|
|
total = sum(e.shares for e in final)
|
|
founder_a_final = next((e for e in final if e.name == "Founder A (CEO)"), None)
|
|
if founder_a_final:
|
|
print(f" Founder A final ownership: {founder_a_final.pct_ownership*100:.2f}%")
|
|
total_raised = sum(e.invested for e in final)
|
|
print(f" Total capital raised: {fmt(total_raised)}")
|
|
print(f" Total shares outstanding: {total:,.0f}")
|
|
print(f" Final post-money: {fmt(rounds[-1].post_money_valuation)}")
|
|
print("\n Run with --exit <$M> to model proceeds at different exit valuations.")
|
|
print(" Example: python fundraising_model.py --exit 500")
|
|
|
|
if args.csv:
|
|
print("\n\n--- CSV EXPORT ---\n")
|
|
sys.stdout.write(export_csv_rounds(rounds))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|