Skip to content

Free Claude skill · Equity and cap tables · v1.0

Cap table modeling skill for Claude

Give Claude your cap table, your SAFEs and convertible notes and the round you expect, and it builds the cap table after the round: what each SAFE and note converts into, the option pool top-up, the price per share, and every holder's shares and fully diluted percentage before and after. It also shows the effective pre-money: how much of the headline valuation your existing shares are actually priced at.

By the StartupReader editorial desk · Updated 2026-11-08 · MIT licence · Works on claude.ai, Claude Code and the Claude API

At a glance2026

Cap table modeling

instrument types it converts
3
network calls
0
MIT licence
Free
Cap table modeling at a glance: instrument types it converts: 3; network calls: 0; MIT licence: Free.

What it does

  • Converts post-money SAFEs, pre-money SAFEs and convertible notes at the lowest of the cap price, the discount price and the round price, and says which one applied.
  • Adds a convertible note's simple interest to the amount that converts, and flags a note that is past its maturity date.
  • Tops up the option pool to the investors' post-money target, counting any unallocated pool you already have.
  • Works out the round's price per share and issues the new investors' shares.
  • Shows every holder's shares and fully diluted percentage before and after, plus the founders' and the SAFE and note holders' combined stakes.
  • Splits the headline pre-money into what your existing shares are priced at, what the pool takes and what the conversions take.
  • Flags MFN clauses where a later SAFE or note got better terms, and reruns what-ifs: a different valuation, a smaller pool, or conversions outside the pre-money.

Ask Claude things like

  • “Here's our cap table and our SAFEs. What do we each own after a $4M seed at $16M pre?”
  • “Does our angel SAFE convert at the cap or the discount if the round is priced at $10M?”
  • “How much is the 10% option pool really costing the founders?”
  • “We have a pre-money SAFE and a post-money SAFE. Why do they convert differently?”

What you give it

  • Your current cap table: founders, investors, granted options and the unallocated pool, as a spreadsheet, a screenshot or pasted numbers.
  • Every SAFE and convertible note, or just their terms: amount, cap, discount, MFN clause, and for notes the interest rate and dates.
  • The round: pre-money valuation, amount raised, option pool target and expected closing date. A range works if it isn't priced yet.

What you get back

  • A headline: the founders' combined stake before and after, the price per share and the effective pre-money.
  • The full before-and-after table, holder by holder, in shares and percentages.
  • How each SAFE and note converts, and which term applied.
  • Where the headline pre-money went: your existing shares, the pool top-up and the conversions.
  • The what-ifs you ask for, and the assumptions to confirm with your lawyer.

Why post-money and pre-money SAFEs convert differently

A post-money SAFE's cap is divided by the company's share count including every SAFE and note as they convert, so the holder's percentage is fixed when they sign. In the example below, $750,000 of SAFEs at a $7.5M post-money cap convert into 1,234,283 shares: exactly 10% of the 10,000,000 existing shares plus the 2,342,832 conversion shares. However many SAFEs follow, each one comes out of the founders' share, not out of the earlier SAFE holders'.

A pre-money SAFE or a convertible note divides its cap by the shares that exist before the conversions, so its holder's percentage isn't fixed: every further SAFE or note dilutes that holder as well as the founders. Documents define these share counts in different ways, so the skill tells Claude to read the definition in each SAFE or note, set the model to match, and list the assumption in its answer.

Why the price, the pool and the conversions are solved together

In a priced round with SAFEs, every number depends on the others. The price per share is the pre-money divided by a share count that includes the pool top-up and the converted SAFEs. A SAFE or note converting at a discount needs that price. And the pool top-up is a percentage of the company after everything has been issued. Spreadsheets handle this with circular references, and it is easy to get wrong by hand.

The skill's script solves them together: it repeats the calculation until the share counts stop changing, then rounds each conversion down to whole shares. Claude reads the documents and writes the input file; the script does the arithmetic, and Claude is told to copy every figure from its output. In the simple case it agrees with StartupReader's SAFE conversion calculator: a $500K SAFE at a $5M post-money cap, converting in a $5M round at a $20M pre-money, ends at 8%, with the founders at 72%.

What the effective pre-money tells you

The headline pre-money pays for three things: your existing shares, the new option pool and the shares the SAFEs and notes convert into. In the example, a $16M pre-money values the existing holders' shares at $11,922,165. The pool top-up takes $1,284,670 and the conversions take $2,793,165.

That breakdown is what to negotiate with. A pool sized to a real hiring plan, or SAFEs and notes priced outside the pre-money so they dilute the new investors too, changes how much of the headline is really yours. The skill runs those what-ifs when you ask, alongside a lower or higher round valuation to show where a cap stops applying and a discount takes over.

  • Today's holders: round price x the shares that exist before the round.
  • Pool top-up: the shares added so the unallocated pool reaches its target.
  • Conversions: the shares every SAFE and note converts into.

Worked example

Fernhollow Labs, a made-up climate-software startup, is raising a $4M seed at a $16M pre-money with a 10% post-money option pool. Its founders, Asha and Leo, hold 4,500,000 shares each; employees hold 400,000 granted options and 600,000 more sit unallocated in the pool. Earlier it raised an uncapped $100,000 advisor SAFE with an MFN clause, $750,000 of post-money SAFEs at a $7.5M cap, a $150,000 pre-money SAFE at a $6M cap from an accelerator, and a $500,000 bridge note at 8% simple interest with an $8M cap and a 20% discount, issued a year before the round closes.

The founder asks

Here's our cap table and our SAFEs and note (attached). What do Leo and I own after the seed, and what if it's priced at $10M pre instead?

Claude reads the skill, then runs its script with the figures from the documents. These are the script's real outputs:

$ python scripts/cap_table.py assets/example-cap-table.json
Cap table model: Fernhollow Labs (fictional), Seed (amounts in USD)Round: 4,000,000 new money at a 16,000,000 pre-money (20,000,000 post-money)Unallocated pool target after the round: 10.0% of the fully diluted company Conversions (each converts at the lowest of its cap price, discount price and the round price)  Instrument        Type               Converting  Cap price  Disc. price  Converts at       Shares  Advisor SAFE      post-money SAFE       100,000          -            -  round price       83,877  Angel SAFEs       post-money SAFE       750,000     0.6076            -  cap            1,234,283  Accelerator SAFE  pre-money SAFE        150,000     0.5416            -  cap              276,938  Bridge note       convertible note      540,000     0.7222       0.9538  cap              747,734  Bridge note: 500,000 principal + 40,000 simple interest (8.0% a year for 1.00 years) Round price per share: 1.1922  (16,000,000 pre-money / 13,420,382 pre-money fully diluted shares)   Holder            Shares before  % before   Shares after   % after  Asha (founder)        4,500,000    45.00%      4,500,000    26.82%  Leo (founder)         4,500,000    45.00%      4,500,000    26.82%  Granted options         400,000     4.00%        400,000     2.38%  Unallocated pool        600,000     6.00%      1,677,548    10.00%  Advisor SAFE                  -         -         83,877     0.50%  Angel SAFEs                   -         -      1,234,283     7.36%  Accelerator SAFE              -         -        276,938     1.65%  Bridge note                   -         -        747,734     4.46%  Seed investors                -         -      3,355,095    20.00%  Total                10,000,000   100.00%     16,775,475   100.00%Founders together: 9,000,000 shares, 90.00% before, 53.65% afterSAFEs and notes together: 2,342,832 shares, 13.97% after Effective pre-money for today's holders: 11,922,165  (headline 16,000,000)  Headline 16,000,000 = today's holders 11,922,165 + pool top-up 1,284,670 + SAFE and note conversions 2,793,165Post-money at the round price: 20,000,000 Notes  - Advisor SAFE has an MFN clause, and later instruments have better terms: Angel SAFEs (cap 7,500,000, post-money SAFE), Accelerator SAFE (cap 6,000,000, pre-money SAFE), Bridge note (cap 8,000,000, convertible note; discount 20%). If the clause lets the holder adopt them, re-run with those terms.
$ python scripts/cap_table.py assets/example-cap-table.json --pre-money 10000000 --raise 2500000
Cap table model: Fernhollow Labs (fictional), Seed (amounts in USD)Round: 2,500,000 new money at a 10,000,000 pre-money (12,500,000 post-money)Unallocated pool target after the round: 10.0% of the fully diluted company Conversions (each converts at the lowest of its cap price, discount price and the round price)  Instrument        Type               Converting  Cap price  Disc. price  Converts at       Shares  Advisor SAFE      post-money SAFE       100,000          -            -  round price      137,152  Angel SAFEs       post-money SAFE       750,000     0.5952            -  cap            1,260,088  Accelerator SAFE  pre-money SAFE        150,000     0.5398            -  cap              277,860  Bridge note       convertible note      540,000     0.7198       0.5833  discount         925,782  Bridge note: 500,000 principal + 40,000 simple interest (8.0% a year for 1.00 years) Round price per share: 0.7291  (10,000,000 pre-money / 13,715,296 pre-money fully diluted shares)   Holder            Shares before  % before   Shares after   % after  Asha (founder)        4,500,000    45.00%      4,500,000    26.25%  Leo (founder)         4,500,000    45.00%      4,500,000    26.25%  Granted options         400,000     4.00%        400,000     2.33%  Unallocated pool        600,000     6.00%      1,714,412    10.00%  Advisor SAFE                  -         -        137,152     0.80%  Angel SAFEs                   -         -      1,260,088     7.35%  Accelerator SAFE              -         -        277,860     1.62%  Bridge note                   -         -        925,782     5.40%  Seed investors                -         -      3,428,824    20.00%  Total                10,000,000   100.00%     17,144,118   100.00%Founders together: 9,000,000 shares, 90.00% before, 52.50% afterSAFEs and notes together: 2,600,882 shares, 15.17% after Effective pre-money for today's holders: 7,291,129  (headline 10,000,000)  Headline 10,000,000 = today's holders 7,291,129 + pool top-up 812,532 + SAFE and note conversions 1,896,339Post-money at the round price: 12,500,000 Notes  - Advisor SAFE has an MFN clause, and later instruments have better terms: Angel SAFEs (cap 7,500,000, post-money SAFE), Accelerator SAFE (cap 6,000,000, pre-money SAFE), Bridge note (cap 8,000,000, convertible note; discount 20%). If the clause lets the holder adopt them, re-run with those terms.

The answer, in the skill's format (abridged):

Cap table: Fernhollow Labs, seed

The headline

You and Leo go from 90.00% to 53.65% of the company, 26.82% each. The seed is priced at $1.1922 a share, and the $16M pre-money values today's holders at $11,922,165: the pool top-up takes $1,284,670 and the SAFE and note conversions $2,793,165.

Before and after (excerpt)

Holder Shares before % before Shares after % after
Asha (founder) 4,500,000 45.00% 4,500,000 26.82%
Leo (founder) 4,500,000 45.00% 4,500,000 26.82%
Unallocated pool 600,000 6.00% 1,677,548 10.00%
Seed investors - - 3,355,095 20.00%
Total 10,000,000 100.00% 16,775,475 100.00%

How each SAFE and note converts

Instrument Converting amount Converts at Shares % after
Advisor SAFE $100,000 round price 83,877 0.50%
Angel SAFEs $750,000 cap 1,234,283 7.36%
Accelerator SAFE $150,000 cap 276,938 1.65%
Bridge note $540,000 cap 747,734 4.46%

Together they take 13.97%. The advisor SAFE is uncapped, so it converts at the round price, but its MFN clause may let it adopt a later cap. Check the wording before closing.

What-ifs

At a $10M pre-money raising $2.5M, the bridge note converts at its 20% discount instead of its cap, the SAFEs and note take 15.17%, and the founders hold 52.50%.

Check with your lawyer

  • The post-money SAFEs' stakes are fixed before the pool top-up, so the top-up dilutes them too. Confirm against their definition of company capitalisation.
  • The note's interest is simple, for exactly one year: $40,000.

This is a model to check against your lawyers' closing cap table, not legal or tax advice.

A made-up company and numbers. Your answer depends on your documents, and Claude's wording varies from run to run; the figures come from the script.

Install it

The skill is one folder. Download it once and add it wherever you use Claude; skills don't sync between claude.ai, Claude Code and the API, so each needs its own copy.

claude.ai and the Claude apps

  1. Download the ZIP and keep it zipped.
  2. In Claude, open Settings > Capabilities and turn on Code execution and file creation (called Create and edit files in the newer settings layout).
  3. Open Customize > Skills, click +, choose Create skill, then Upload a skill, and pick the ZIP.
  4. Leave the skill switched on. Claude uses it when your request matches its description; you don't have to name it.

On Team and Enterprise plans an owner first turns on code execution and skills under Organization settings > Plugins & skills. Anthropic's help centre has the details for each plan.

Claude Code

Unzip it into your personal skills folder to use it in every project, or into .claude/skills/ in a repository to share it with your team. Claude picks it up automatically; you can also run it by typing /cap-table-modeling.

Terminal
mkdir -p ~/.claude/skills
unzip cap-table-modeling.zip -d ~/.claude/skills/

Skills with a script need Python 3 on your computer.

Claude API

Upload the unzipped folder once; the response includes the skill's id. Uploaded skills are shared across your API workspace.

Terminal
curl -X POST "https://api.anthropic.com/v1/skills" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2026-10-21" \
  -F "files[]=@cap-table-modeling/SKILL.md;filename=cap-table-modeling/SKILL.md" \
  -F "files[]=@cap-table-modeling/assets/example-cap-table.json;filename=cap-table-modeling/assets/example-cap-table.json" \
  -F "files[]=@cap-table-modeling/references/conversion-rules.md;filename=cap-table-modeling/references/conversion-rules.md" \
  -F "files[]=@cap-table-modeling/scripts/cap_table.py;filename=cap-table-modeling/scripts/cap_table.py"

Then list it in a request's container, with the code execution tool that skills run in (up to 20 skills per request):

Terminal
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2026-10-21" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-5-5",
    "max_tokens": 4096,
    "container": {
      "skills": [{ "type": "custom", "skill_id": "YOUR_SKILL_ID", "version": "latest" }]
    },
    "tools": [{ "type": "code_execution_20250825", "name": "code_execution" }],
    "messages": [{ "role": "user", "content": "..." }]
  }'

From Anthropic's skills API guide. Skills on the API run with no network access, which is why these skills' scripts use only Python's standard library.

What's in the skill

Every file, exactly as it is in the download. Claude reads SKILL.md when a request matches, opens the other files only when a step needs them, and runs scripts without loading their code. Read them before you install: a skill can direct Claude to run code, so only add skills you trust.

Folder
cap-table-modeling/
  SKILL.md
  assets/example-cap-table.json
  references/conversion-rules.md
  scripts/cap_table.py
SKILL.md114 lines
---
name: cap-table-modeling
description: Builds a startup cap table from the founders' shares, options, SAFEs and convertible notes and models the next priced round. Converts post-money and pre-money SAFEs (valuation cap and discount, MFN clauses flagged) and convertible notes (principal plus simple interest, cap and discount), tops up the option pool to the investors' post-money target, issues the new investors' shares, and prints a before-and-after table by holder with share counts and fully diluted percentages, the price per share and the effective pre-money valuation. Use when a founder asks who will own what after a seed or Series A round, how much a SAFE or note converts into, how much dilution to expect, what the option pool shuffle costs, whether a cap or a discount applies, or shares a cap table, SAFE, convertible note or term sheet and wants a pro forma cap table.
license: MIT
compatibility: Works on claude.ai, Claude Code and the Claude API. The script needs Python 3.8+ (standard library only) and no network access.
metadata:
  author: StartupReader
  version: "1.0"
  homepage: https://startupreader.com/claude-skills/cap-table-modeling
---

# Cap table modelling

Turn the founders' cap table and their SAFEs and notes into a pro forma cap table for the next priced round: who owns what before and after, the price per share and the effective pre-money. The script does every calculation; your job is to read the documents correctly, get the inputs right and explain the result. The output is a model to check the lawyers' closing cap table against, not legal or tax advice. Say so once, at the end.

## Workflow

Copy this checklist and tick it off as you go:

```
- [ ] 1. Collect the holders, the pool, every SAFE and note, and the round terms
- [ ] 2. Write the cap table JSON
- [ ] 3. Run scripts/cap_table.py and fix any error it reports
- [ ] 4. Run the what-ifs the founders care about
- [ ] 5. Write the answer in the output format below
- [ ] 6. Check every figure against the script output
```

### 1. Collect the inputs

- **Holders**: everyone with issued shares, plus granted options (vested or not) as their own row. Fully diluted means all of them count.
- **Unallocated pool**: option shares reserved but not yet granted.
- **Every SAFE and note**: type (post-money SAFE, pre-money SAFE or note), amount, cap, discount, MFN clause, and for notes the interest rate, issue date and maturity. [references/conversion-rules.md](references/conversion-rules.md) explains each field and what to look for in the documents.
- **The round**: headline pre-money, new money, the pool target (unallocated pool after the round as a share of the fully diluted company) and the expected closing date.

Never guess a cap, a discount, or whether a SAFE is pre-money or post-money: ask. If the founders only have percentages, use 10,000,000 shares split by those percentages and say the share counts are illustrative. If the round is not priced yet, ask for the range they expect and model its ends.

### 2. Write the JSON

Copy [assets/example-cap-table.json](assets/example-cap-table.json) and replace its contents. Mark each founder with `"founder": true` so the script prints their combined stake. Fractions are decimals (0.2 for 20%), dates are YYYY-MM-DD, every name is unique, and the file uses one currency. The script rejects unknown fields, so a typo fails loudly.

### 3. Run the model

```bash
python scripts/cap_table.py cap-table.json
```

If it exits with an error, fix the input it names and run it again. Do not estimate instead: the price, pool top-up and conversions depend on each other, and the script solves them together.

### 4. Run the what-ifs

```bash
# A lower or higher round: shows where a cap stops applying or a discount takes over
python scripts/cap_table.py cap-table.json --pre-money 10000000 --raise 2500000

# A pool sized to the hiring plan instead of the term sheet's target
python scripts/cap_table.py cap-table.json --pool-target 0.08

# Conversions priced outside the pre-money, so they dilute the new investors too
python scripts/cap_table.py cap-table.json --conversions-outside-pre-money
```

Pick the what-ifs that answer the founders' question; two or three is usually enough. If the script notes an MFN clause with better later terms and the clause applies, copy those terms into that instrument, rerun, and show both results.

### 5. Output format

```markdown
# Cap table: [Company], [round]

## The headline
Two or three sentences: the founders' combined stake before and after, the price per share, and the effective pre-money against the headline.

## Before and after
| Holder | Shares before | % before | Shares after | % after |
|---|---|---|---|---|
Every row from the script, totals included.

## How each SAFE and note converts
| Instrument | Converting amount | Converts at | Shares | % after |
|---|---|---|---|---|
One sentence for any instrument that converts at its discount or the round price instead of its cap, and for any MFN clause.

## Where the headline pre-money went
The script's breakdown: today's holders, the pool top-up and the conversions, each in currency.

## What-ifs
One line per extra run: what changed and the founders' combined stake after.

## Check with your lawyer
The assumptions this model made (which share count each cap divides into, simple interest, MFN not applied) and the terms to confirm.
```

### 6. Check before sending

- Every number appears in the script output or the documents. Percentages are copied, never recomputed.
- Pre-money and post-money are never mixed up. A post-money SAFE's cap is a post-money figure; a priced round's headline is usually pre-money.
- Each instrument's type matches its document, and the answer says which share count each cap divided into.
- The answer ends with: "This is a model to check against your lawyers' closing cap table, not legal or tax advice."

## How the script models a round

- Round price = pre-money / pre-money fully diluted shares. By default that count includes the pool top-up and every conversion, so the new investors own exactly raise / post-money and today's holders pay for the pool and the conversions.
- Each SAFE or note converts at the lowest of its cap price, its discount price and the round price: one of them, never both cap and discount.
- A post-money SAFE's stake is fixed at amount / cap of the pre-round shares plus all conversions, so later SAFEs dilute the founders, not it. A pre-money SAFE or note divides its cap by the pre-round shares plus the pool top-up, so each further SAFE or note dilutes it too.
- The pool top-up brings the unallocated pool up to the target, counting any existing unallocated pool.
- Effective pre-money = round price x today's fully diluted shares: the headline minus what the pool top-up and the conversions take.

## Rules

- Explain dilution in shares as well as percentages: nobody's share count falls in a round, but everyone's percentage does.
- When several SAFEs or notes stack up, show their combined stake after the round. That total is what usually surprises founders.
- For Indian rounds, read the Indian section of [references/conversion-rules.md](references/conversion-rules.md) before modelling CCPS, an iSAFE or a convertible note.
- Do not tell the founders whether to accept a round or a cap. Show what each option does to their stake.
assets/example-cap-table.json24 lines
{
  "company": "Fernhollow Labs (fictional)",
  "currency": "USD",
  "holders": [
    { "name": "Asha (founder)", "shares": 4500000, "founder": true },
    { "name": "Leo (founder)", "shares": 4500000, "founder": true },
    { "name": "Granted options", "shares": 400000 }
  ],
  "unallocated_pool": 600000,
  "instruments": [
    { "name": "Advisor SAFE", "type": "post-money-safe", "amount": 100000, "mfn": true, "issued": "2024-09-01" },
    { "name": "Angel SAFEs", "type": "post-money-safe", "amount": 750000, "cap": 7500000, "issued": "2025-02-01" },
    { "name": "Accelerator SAFE", "type": "pre-money-safe", "amount": 150000, "cap": 6000000, "issued": "2025-04-01" },
    { "name": "Bridge note", "type": "note", "amount": 500000, "cap": 8000000, "discount": 0.2, "interest_rate": 0.08, "issued": "2025-11-01", "maturity": "2027-11-01" }
  ],
  "round": {
    "name": "Seed",
    "pre_money": 16000000,
    "raise": 4000000,
    "pool_target": 0.10,
    "closing": "2026-11-01"
  }
}
references/conversion-rules.md82 lines
# Conversion rules and the input file

## Contents
- The JSON file, field by field
- How the round is priced
- How each SAFE and note converts
- Which share count a cap divides into
- MFN, interest and maturity
- What to read in the documents
- Indian rounds

## The JSON file, field by field

| Field | Meaning |
|---|---|
| `company`, `currency` | Labels for the output only. |
| `holders` | `[{"name", "shares", "founder"}]`: founders, angels and earlier priced investors with issued shares, and granted options (vested or not), each as its own row. Set `"founder": true` on founders to get their combined stake. |
| `unallocated_pool` | Option pool shares reserved but not yet granted. Default 0. |
| `instruments[].type` | `post-money-safe`, `pre-money-safe` or `note`. |
| `instruments[].amount` | Purchase amount (SAFE) or principal (note). |
| `instruments[].cap` | Valuation cap. Omit or 0 for uncapped. |
| `instruments[].discount` | Discount as a fraction: 0.2 for 20% off the round price. Omit or 0 for none. |
| `instruments[].mfn` | `true` if it has a most-favoured-nation clause. |
| `instruments[].interest_rate` | Notes only: yearly simple interest as a fraction, e.g. 0.08. |
| `instruments[].years` or `issued` | Notes only: years of interest, or the issue date (YYYY-MM-DD) with `round.closing`. `issued` also orders instruments for the MFN check. |
| `instruments[].maturity` | Notes only, optional: flagged if the round closes after it. |
| `instruments[].cap_includes_conversions`, `cap_includes_pool_top_up` | Override the share count the cap divides into (see below). |
| `round.name`, `pre_money`, `raise` | The priced round: headline pre-money and new money. |
| `round.pool_target` | Unallocated pool after the round as a fraction of the fully diluted company, e.g. 0.10. Omit for no top-up. |
| `round.closing` | Closing date, for note interest and maturity. |
| `round.investor_name` | Label for the new investors. Default "<round name> investors". |
| `round.conversions_in_pre_money` | Default `true`. `false` when the term sheet prices the round on today's shares and the pool only. |

The script rejects unknown fields, so a typo fails loudly instead of being ignored.

## How the round is priced

- Price per share = pre-money valuation / pre-money fully diluted shares.
- By default the pre-money share count includes today's holders, granted options, the unallocated pool, the pool top-up and every SAFE and note conversion. The new investors then own exactly raise / post-money, and the pool and conversions are paid for by today's holders. This matches StartupReader's SAFE conversion and dilution calculators, where the round's pre-money includes the SAFE.
- With `conversions_in_pre_money: false` (or `--conversions-outside-pre-money`), conversions are extra shares on top, so they dilute the new investors as well.
- Pool top-up: shares are added before the round until the unallocated pool equals `pool_target` of the fully diluted company after the round. An existing unallocated pool counts toward the target; if it is already above, nothing is added.
- Effective pre-money = round price x today's fully diluted shares. It is what today's holders' stake is priced at once the pool top-up and conversions are taken out of the headline.

## How each SAFE and note converts

Each instrument converts at the lowest of three prices, never more than one of them:
1. Cap price = cap / the share count its document defines (next section).
2. Discount price = round price x (1 - discount).
3. The round price, for an uncapped instrument with no discount or when neither term helps.

Shares = converting amount / that price, rounded down. The converting amount is the purchase amount for a SAFE and principal plus accrued interest for a note.

The price, the pool top-up and the conversions depend on each other, so the script solves them together. Do not try to reproduce the result by hand; rerun the script after any change.

## Which share count a cap divides into

| Type | Default count | Effect |
|---|---|---|
| `post-money-safe` | Today's fully diluted shares plus the shares issued on all SAFE and note conversions; not the round's pool top-up | The holder's stake is fixed at amount / cap of that count. More SAFEs dilute the founders, not this holder. The pool top-up and new money then dilute it like everyone else. |
| `pre-money-safe` | Today's fully diluted shares plus the pool top-up; not the conversions | The stake is not fixed: every further SAFE or note dilutes this holder as well as the founders. |
| `note` | Same as a pre-money SAFE | As for a pre-money SAFE. |

These are the script's defaults. Documents differ, so read the definition of the share count the cap is divided by (often called company capitalisation or fully diluted capitalisation) and set `cap_includes_conversions` and `cap_includes_pool_top_up` to match. State in the answer which count each instrument used.

## MFN, interest and maturity

- MFN (most-favoured-nation): the holder can take better terms the company later gives other investors in similar instruments. The script lists later instruments with a lower cap or a bigger discount but never applies them. If the clause applies, copy the better terms into that instrument, rerun, and show both results.
- Interest: simple interest, principal x rate x years, with years = days from `issued` to `round.closing` / 365. If a note compounds or uses another day count, say that the converting amount is an estimate.
- Maturity: a note has a date by which it should convert or be repaid, and one that reaches it without a plan hands the noteholders leverage. The script flags a closing after maturity; tell the founders to read what the note says happens then.

## What to read in the documents

- SAFE: the purchase amount; whether it is a post-money or pre-money SAFE (the title and the cap definition say); the cap; the discount. Some documents state the price paid instead of the discount: a "discount rate" of 80% means a 20% discount, so enter 0.2. Check for an MFN clause and any side letter, such as pro-rata rights.
- Convertible note: principal, interest rate and whether it compounds, issue date, maturity date and what happens at maturity, cap, discount, and the share count the cap divides into.
- The term sheet: pre-money or post-money valuation, the new money, the pool target and whether it is measured on the post-money company, and whether the pre-money includes the SAFEs and notes.

If a term is missing or ambiguous, ask. Never fill in a typical cap or discount.

## Indian rounds

Indian companies usually raise early money through compulsorily convertible preference shares (CCPS), convertible notes (open to DPIIT-recognised startups under specific rules) or templates such as the iSAFE, which is structured as CCPS. If the instrument's conversion formula works like a cap and discount on the next round's price, model it as the matching type and say so. For foreign investors, foreign-exchange rules generally require the conversion formula to be fixed when the instrument is issued and the conversion price not to fall below the fair value at that time; flag this for the lawyer rather than modelling it.
scripts/cap_table.py368 lines
#!/usr/bin/env python3
"""Cap table model: convert SAFEs and notes, top up the option pool, issue a priced round.

Standard library only, so it runs in the Claude API sandbox (no network, no
package installs), on claude.ai and in Claude Code.

Usage:
  python scripts/cap_table.py assets/example-cap-table.json
  python scripts/cap_table.py cap-table.json --pre-money 12000000 --raise 3000000 --pool-target 0.12
  python scripts/cap_table.py cap-table.json --conversions-outside-pre-money

The JSON file (field by field in references/conversion-rules.md):
  holders           [{"name": "Asha (founder)", "shares": 4500000}, ...]
                    everyone with issued shares or granted options today
  unallocated_pool  shares reserved for future grants and not yet granted
  instruments       [{"name", "type", "amount", "cap", "discount", "mfn", ...}]
                    type: post-money-safe, pre-money-safe or note; notes also
                    take "interest_rate" and "years" or "issued" (YYYY-MM-DD)
  round             {"name", "pre_money", "raise", "pool_target", "closing"}

Fractions are decimals: 0.2 for a 20% discount, 0.10 for a 10% pool.
Amounts are in one currency unit; shares are counts. A simplified model for
checking the lawyers' closing cap table, not legal advice.
"""
import argparse
import datetime
import json
import math
import sys
from decimal import ROUND_HALF_UP, Decimal

DAYS_PER_YEAR = 365  # simple interest accrues by actual days over a 365-day year
TOLERANCE = 1e-14  # relative change in share counts at which the conversion maths has settled
MAX_STEPS = 200000  # iterations allowed before deciding the inputs cannot add up
ROUNDING_GUARD = 1e-3  # shares: keeps floating-point noise (999,999.9999) from losing a whole share

# type: (label, cap divides into a count that includes conversion shares, that includes the pool top-up)
TYPES = {
    "post-money-safe": ("post-money SAFE", True, False),
    "pre-money-safe": ("pre-money SAFE", False, True),
    "note": ("convertible note", False, True),
}

TOP_KEYS = {"company", "currency", "holders", "unallocated_pool", "instruments", "round"}
HOLDER_KEYS = {"name", "shares", "founder"}
INSTRUMENT_KEYS = {
    "name", "type", "amount", "cap", "discount", "mfn", "interest_rate", "years", "issued", "maturity",
    "cap_includes_conversions", "cap_includes_pool_top_up",
}
ROUND_KEYS = {"name", "pre_money", "raise", "pool_target", "closing", "investor_name", "conversions_in_pre_money"}


def fail(message):
    print(f"Error: {message}", file=sys.stderr)
    sys.exit(2)


def money(x):
    return f"{x:,.0f}"


def pct(x, places=2):
    """Halves rounded up like the site's calculators (31.125 -> 31.13)."""
    return f"{Decimal(x * 100).quantize(Decimal(1).scaleb(-places), rounding=ROUND_HALF_UP)}%"


def check_keys(obj, allowed, where):
    if not isinstance(obj, dict):
        fail(f"{where} must be a JSON object")
    unknown = sorted(set(obj) - allowed)
    if unknown:
        fail(f"{where} has unknown field(s): {', '.join(unknown)}. Allowed: {', '.join(sorted(allowed))}")


def number(obj, key, where, default=None, positive=False, below=None):
    value = obj.get(key, default)
    if value is None:
        if default is None:
            fail(f"{where} needs \"{key}\"")
        return default
    if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
        fail(f"{where}: \"{key}\" must be a number, not {json.dumps(value)}")
    if value < 0 or (positive and value == 0):
        fail(f"{where}: \"{key}\" must be {'above' if positive else 'at least'} 0")
    if below is not None and value >= below:
        fail(f"{where}: \"{key}\" must be below {below:g} (write fractions as decimals, e.g. 0.2 for 20%)")
    return float(value)


def flag(obj, key, where, default):
    value = obj.get(key, default)
    if not isinstance(value, bool):
        fail(f"{where}: \"{key}\" must be true or false")
    return value


def date(obj, key, where):
    value = obj.get(key)
    if value is None:
        return None
    try:
        return datetime.date.fromisoformat(value)
    except (TypeError, ValueError):
        fail(f"{where}: \"{key}\" must be a date written YYYY-MM-DD, not {json.dumps(value)}")


def load(path, args):
    try:
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        fail(f"{path} not found")
    except json.JSONDecodeError as e:
        fail(f"{path} is not valid JSON: {e.msg} at line {e.lineno}")
    check_keys(data, TOP_KEYS, "the file")

    holders = data.get("holders")
    if not isinstance(holders, list) or not holders:
        fail("\"holders\" must be a non-empty list of {\"name\", \"shares\"}")
    names = set()

    def new_name(obj, where):
        name = obj.get("name")
        if not isinstance(name, str) or not name.strip():
            fail(f"{where} needs a \"name\"")
        if name in names:
            fail(f"the name \"{name}\" is used twice; every holder, instrument and investor needs its own")
        names.add(name)
        return name

    rows = []
    for i, h in enumerate(holders, 1):
        check_keys(h, HOLDER_KEYS, f"holder {i}")
        name = new_name(h, f"holder {i}")
        rows.append((name, number(h, "shares", f"holder \"{name}\"", positive=True), flag(h, "founder", f"holder \"{name}\"", False)))
    pool = number(data, "unallocated_pool", "the file", default=0.0)

    rnd = data.get("round")
    check_keys(rnd, ROUND_KEYS, "\"round\"")
    r = {
        "name": rnd.get("name", "Priced round"),
        "pre": args.pre_money if args.pre_money is not None else number(rnd, "pre_money", "\"round\"", positive=True),
        "raise": args.raise_ if args.raise_ is not None else number(rnd, "raise", "\"round\"", positive=True),
        "pool_target": args.pool_target if args.pool_target is not None else number(rnd, "pool_target", "\"round\"", default=0.0, below=1),
        "closing": date(rnd, "closing", "\"round\""),
        "in_pre": False if args.conversions_outside_pre_money else flag(rnd, "conversions_in_pre_money", "\"round\"", True),
    }
    if not isinstance(r["name"], str) or not r["name"].strip():
        fail("\"round\": \"name\" must be text")
    if r["pre"] <= 0 or r["raise"] <= 0:
        fail("the round's pre-money and raise must both be above 0")
    if not 0 <= r["pool_target"] < 1:
        fail("the pool target must be a fraction between 0 and 1, e.g. 0.10 for 10%")
    investor_share = r["raise"] / (r["pre"] + r["raise"])
    if r["pool_target"] >= 1 - investor_share:
        fail(f"a {pct(r['pool_target'], 1)} pool target leaves nothing for existing holders: the new investors already take {pct(investor_share, 1)}")
    r["investors"] = rnd.get("investor_name", f"{r['name']} investors")
    if not isinstance(r["investors"], str):
        fail("\"round\": \"investor_name\" must be text")
    new_name({"name": r["investors"]}, "\"round\"")
    new_name({"name": "Unallocated pool"}, "the pool")

    instruments = []
    for i, x in enumerate(data.get("instruments") or [], 1):
        check_keys(x, INSTRUMENT_KEYS, f"instrument {i}")
        name = new_name(x, f"instrument {i}")
        where = f"instrument \"{name}\""
        kind = x.get("type")
        if kind not in TYPES:
            fail(f"{where}: \"type\" must be one of {', '.join(TYPES)}")
        label, with_conversions, with_pool = TYPES[kind]
        inst = {
            "order": i,
            "name": name,
            "type": kind,
            "label": label,
            "amount": number(x, "amount", where, positive=True),
            "cap": number(x, "cap", where, default=0.0),
            "discount": number(x, "discount", where, default=0.0, below=1),
            "mfn": flag(x, "mfn", where, False),
            "issued": date(x, "issued", where),
            "maturity": date(x, "maturity", where),
            "with_conversions": flag(x, "cap_includes_conversions", where, with_conversions),
            "with_pool": flag(x, "cap_includes_pool_top_up", where, with_pool),
            "interest": 0.0,
            "years": 0.0,
        }
        rate = number(x, "interest_rate", where, default=0.0, below=1)
        if kind != "note" and (rate or "years" in x):
            fail(f"{where}: only notes carry interest; a SAFE has no interest rate")
        if rate:
            if "years" in x:
                years = number(x, "years", where)
            elif inst["issued"] and r["closing"]:
                years = (r["closing"] - inst["issued"]).days / DAYS_PER_YEAR
                if years < 0:
                    fail(f"{where} is dated after the round's closing date")
            else:
                fail(f"{where}: to add interest, give \"years\", or \"issued\" here and \"closing\" in the round")
            inst["years"] = years
            inst["interest"] = inst["amount"] * rate * years
        inst["rate"] = rate
        inst["converting"] = inst["amount"] + inst["interest"]
        instruments.append(inst)
    return data, rows, pool, instruments, r


def solve(existing, pool, instruments, r):
    """Fixed point of the round: the price depends on the conversion and pool
    shares, and they depend on the price. Every quantity rises with the others,
    so iterating from zero settles on the answer or keeps growing if the
    conversions, pool and new money claim 100% or more of the company."""
    top_up = 0.0
    conv = [0.0] * len(instruments)
    for _ in range(MAX_STEPS):
        converted = sum(conv)
        price = r["pre"] / (existing + top_up + (converted if r["in_pre"] else 0.0))
        new_conv, detail = [], []
        for inst in instruments:
            base = existing + (top_up if inst["with_pool"] else 0.0) + (converted if inst["with_conversions"] else 0.0)
            cap_price = inst["cap"] / base if inst["cap"] else None
            disc_price = price * (1 - inst["discount"]) if inst["discount"] else None
            used = min(p for p in (cap_price, disc_price, price) if p is not None)
            how = "cap" if used == cap_price else ("round price" if used == price else "discount")
            new_conv.append(inst["converting"] / used)
            detail.append((cap_price, disc_price, used, how))
        total = existing + top_up + sum(new_conv) + r["raise"] / price
        new_top_up = max(0.0, r["pool_target"] * total - pool)
        change = max([abs(a - b) for a, b in zip(new_conv, conv)] + [abs(new_top_up - top_up)])
        conv, top_up = new_conv, new_top_up
        if change <= TOLERANCE * total:
            return price, top_up, conv, detail
        if total > existing * 1e9:
            break
    fail("the SAFEs, notes, pool target and new money add up to 100% or more of the company; check the caps, amounts and pool target")


def later(a, b):
    """True if instrument b was issued after a: by date when both have one, else by order in the file."""
    if a["issued"] and b["issued"]:
        return b["issued"] > a["issued"]
    return b["order"] > a["order"]


def run(args):
    data, rows, pool, instruments, r = load(args.file, args)
    existing = sum(s for _, s, _ in rows) + pool
    price, top_up, conv, detail = solve(existing, pool, instruments, r)

    top_up_shares = math.floor(top_up + 0.5)
    conv_shares = [math.floor(c + ROUNDING_GUARD) for c in conv]
    investor_shares = math.floor(r["raise"] / price + ROUNDING_GUARD)
    total_after = existing + top_up_shares + sum(conv_shares) + investor_shares
    company = data.get("company", "Company")
    currency = f" (amounts in {data['currency']})" if data.get("currency") else ""

    print(f"Cap table model: {company}, {r['name']}{currency}")
    print(f"Round: {money(r['raise'])} new money at a {money(r['pre'])} pre-money ({money(r['pre'] + r['raise'])} post-money)")
    if r["pool_target"]:
        print(f"Unallocated pool target after the round: {pct(r['pool_target'], 1)} of the fully diluted company")
    else:
        print("No option pool target: the pool is not topped up")
    if not r["in_pre"]:
        print("SAFE and note conversions sit outside the pre-money, so they dilute the new investors too")

    if instruments:
        w = max(len("Instrument"), *(len(i["name"]) for i in instruments))
        print()
        print("Conversions (each converts at the lowest of its cap price, discount price and the round price)")
        print(f"  {'Instrument':<{w}}  {'Type':<16}  {'Converting':>11}  {'Cap price':>9}  {'Disc. price':>11}  {'Converts at':<11}  {'Shares':>11}")
        for inst, (cap_price, disc_price, used, how), shares in zip(instruments, detail, conv_shares):
            cap_s = f"{cap_price:.4f}" if cap_price is not None else "-"
            disc_s = f"{disc_price:.4f}" if disc_price is not None else "-"
            print(f"  {inst['name']:<{w}}  {inst['label']:<16}  {money(inst['converting']):>11}  {cap_s:>9}  {disc_s:>11}  {how:<11}  {money(shares):>11}")
        for inst in instruments:
            if inst["interest"]:
                print(f"  {inst['name']}: {money(inst['amount'])} principal + {money(inst['interest'])} simple interest"
                      f" ({pct(inst['rate'], 1)} a year for {inst['years']:.2f} years)")

    pre_count = existing + top_up + (sum(conv) if r["in_pre"] else 0.0)
    print()
    print(f"Round price per share: {price:.4f}  ({money(r['pre'])} pre-money / {money(pre_count)} pre-money fully diluted shares)")

    w = max(len("Holder"), len("Unallocated pool"), len(r["investors"]), *(len(n) for n, _, _ in rows), *(len(i["name"]) for i in instruments))
    print()
    print(f"  {'Holder':<{w}}  {'Shares before':>13}  {'% before':>8}  {'Shares after':>13}  {'% after':>8}")
    table = [(n, s, s) for n, s, _ in rows]
    if pool or top_up_shares:
        table.append(("Unallocated pool", pool, pool + top_up_shares))
    table += [(i["name"], None, s) for i, s in zip(instruments, conv_shares)]
    table.append((r["investors"], None, investor_shares))
    for name, before, after in table:
        b = f"{money(before):>13}  {pct(before / existing):>8}" if before is not None else f"{'-':>13}  {'-':>8}"
        print(f"  {name:<{w}}  {b}  {money(after):>13}  {pct(after / total_after):>8}")
    print(f"  {'Total':<{w}}  {money(existing):>13}  {pct(1):>8}  {money(total_after):>13}  {pct(1):>8}")
    founder_shares = sum(s for _, s, is_founder in rows if is_founder)
    if founder_shares:
        print(f"Founders together: {money(founder_shares)} shares, {pct(founder_shares / existing)} before, {pct(founder_shares / total_after)} after")
    if instruments:
        print(f"SAFEs and notes together: {money(sum(conv_shares))} shares, {pct(sum(conv_shares) / total_after)} after")

    effective = round(price * existing)
    pool_value = round(price * top_up)
    print()
    print(f"Effective pre-money for today's holders: {money(effective)}  (headline {money(r['pre'])})")
    conv_value = r["pre"] - effective - pool_value if r["in_pre"] else 0
    if not r["in_pre"]:
        pool_value = r["pre"] - effective
    parts = [f"today's holders {money(effective)}"]
    if pool_value:
        parts.append(f"pool top-up {money(pool_value)}")
    if conv_value:
        parts.append(f"SAFE and note conversions {money(conv_value)}")
    if len(parts) > 1:
        print(f"  Headline {money(r['pre'])} = {' + '.join(parts)}")
    print(f"Post-money at the round price: {money(price * (existing + top_up + sum(conv)) + r['raise'])}")

    notes = []
    if r["pool_target"] and top_up == 0:
        notes.append(f"The unallocated pool is already at or above {pct(r['pool_target'], 1)} of the company after the round, so nothing is added.")
    for inst in instruments:
        if inst["maturity"] and r["closing"] and r["closing"] > inst["maturity"]:
            notes.append(f"{inst['name']} reached maturity on {inst['maturity'].isoformat()}, before the closing: check what the note says happens at maturity.")
        if not inst["mfn"]:
            continue
        better = []
        for other in instruments:
            if other is inst or not later(inst, other):
                continue
            terms = []
            if other["cap"] and (not inst["cap"] or other["cap"] < inst["cap"]):
                terms.append(f"cap {money(other['cap'])}, {other['label']}")
            if other["discount"] > inst["discount"]:
                terms.append(f"discount {pct(other['discount'], 0)}")
            if terms:
                better.append(f"{other['name']} ({'; '.join(terms)})")
        if better:
            notes.append(f"{inst['name']} has an MFN clause, and later instruments have better terms: {', '.join(better)}."
                         " If the clause lets the holder adopt them, re-run with those terms.")
        else:
            notes.append(f"{inst['name']} has an MFN clause; no later instrument in the file has better terms.")
    if notes:
        print()
        print("Notes")
        for n in notes:
            print(f"  - {n}")


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("file", help="cap table JSON file")
    parser.add_argument("--pre-money", type=float, help="override the round's pre-money valuation")
    parser.add_argument("--raise", dest="raise_", metavar="RAISE", type=float, help="override the new money raised")
    parser.add_argument("--pool-target", type=float, help="override the unallocated pool target, a fraction of post-money")
    parser.add_argument("--conversions-outside-pre-money", action="store_true",
                        help="price the round on today's shares and the pool only, so conversions dilute the new investors too")
    args = parser.parse_args()
    for value, label in ((args.pre_money, "--pre-money"), (args.raise_, "--raise")):
        if value is not None and not (math.isfinite(value) and value > 0):
            fail(f"{label} must be a number above 0")
    if args.pool_target is not None and not (math.isfinite(args.pool_target) and 0 <= args.pool_target < 1):
        fail("--pool-target must be a fraction between 0 and 1, e.g. 0.10 for 10%")
    run(args)


if __name__ == "__main__":
    main()

Limits

  • It's a model, not the closing cap table. The round's lawyers produce the exact conversion schedule; use this to check theirs.
  • It models one priced round at a time, with one group of new investors. It doesn't model liquidation preferences or anti-dilution: the term sheet review skill covers those.
  • SAFEs and notes define the share count their cap divides into in different ways. The skill uses a default for each type, tells Claude to read the definition, and lists its assumptions; check them.
  • Notes are modelled with simple interest. If a note compounds or counts days differently, treat its converting amount as an estimate.
  • It flags MFN clauses but doesn't apply them automatically, because whether one applies depends on its wording.
  • For Indian rounds it can model CCPS or an iSAFE that converts like a SAFE, but it flags foreign-exchange pricing rules for your lawyer rather than modelling them.

Questions

Can it handle post-money and pre-money SAFEs in the same cap table?

Yes. Each converts on its own rule: a post-money SAFE's percentage is fixed against the share count including all conversions, while a pre-money SAFE or note divides its cap by the shares before them. The script solves them together with the convertible notes, the pool top-up and the new money.

Why doesn't my SAFE get both the cap and the discount?

Because it converts at whichever gives the investor the lower price, not both. The skill shows the cap price, the discount price and the round price side by side, so you can see which one applied and how close the others were.

What if our round isn't priced yet?

Give it the range you expect. It models each end, which shows where a cap stops applying or a discount takes over, and how much the founders' stake moves across the range. That is worth doing before agreeing a cap on the next SAFE.

Do I need a spreadsheet or a particular file format?

No. Paste your numbers, attach a cap table export or a screenshot, or share the SAFE documents. Claude writes the input file the script reads, starting from the worked example included with the skill, and asks for anything missing instead of guessing a cap or discount.

Will the result match what our lawyers produce?

It should be close, but documents differ in how they define the share count a cap divides into, how interest accrues and how shares are rounded. The skill lists the assumptions it made so you can compare them with the closing cap table line by line.

Does it work for Indian rounds with CCPS or an iSAFE?

It can model an instrument whose conversion works like a cap and discount on the next round's price, and says so. For foreign investors, foreign-exchange pricing rules can limit conversion prices; it flags that for your lawyer rather than modelling it.

Learn what the skill checks

The skill is built from the same rules and formulas as these StartupReader pages, so you can follow its reasoning step by step.

Pairs well with

The weekly brief

More on cap table modeling, once a week

Who raised, who launched and the founder terms worth knowing, in one short email a week.

The StartupReader app

Startup news in 60-second swipes

Funding, launches and founder programs, each on one card with the key numbers. Free, with read-aloud and auto-scroll.

Get it on Google PlayComing soon

Building something? List your startup free.

Written and tested by the StartupReader editorial desk with AI assistance; the example outputs are the skill's scripts run on the made-up figures shown. StartupReader is not affiliated with Anthropic. The skill gives general information, not legal, tax or financial advice. Spotted something wrong? Tell us.