TypeScript library for modeling startup cap table ownership across funding events — SAFE note conversions, priced rounds, option pools, and dilution.
Used by the 1984 Ventures Cap Table Worksheet, a free web tool for founders.
npm install @1984vc/cap-tableimport {
fitConversion,
buildPricedRoundCapTable,
CapTableRowType,
CommonRowType,
} from "@1984vc/cap-table";
// 1. Define your current shareholders
const founders = [
{ name: "Founder 1", shares: 4_500_000, type: CapTableRowType.Common, commonType: CommonRowType.Shareholder },
{ name: "Founder 2", shares: 4_500_000, type: CapTableRowType.Common, commonType: CommonRowType.Shareholder },
{ name: "Options Pool", shares: 1_000_000, type: CapTableRowType.Common, commonType: CommonRowType.UnusedOptions },
];
// 2. Define outstanding SAFEs
const safes = [
{ name: "Seed SAFE", investment: 1_000_000, cap: 10_000_000, discount: 0, conversionType: "post", type: CapTableRowType.Safe },
];
// 3. Solve for share counts at a priced Series A
const conversion = fitConversion(
12_000_000, // pre-money valuation
9_000_000, // total common shares (founders + issued options)
safes,
1_000_000, // unused options
0.10, // target options pool percentage post-round
[2_000_000], // series A investment amounts
);
// 4. Build the full cap table
const { common, safes: safeRows, series, refreshedOptionsPool, total } =
buildPricedRoundCapTable(conversion, [...founders, ...safes, {
name: "Lead Investor",
investment: 2_000_000,
type: CapTableRowType.Series,
round: 0,
}]);conversionType |
Description |
|---|---|
"pre" |
Pre-money SAFE — converts on pre-money valuation |
"post" |
Post-money SAFE — converts on post-money (YC standard) |
"mfn" |
MFN (Most Favored Nation) — no cap, gets lowest subsequent cap |
"yc7p" |
YC 7% post-money — guarantees 7% ownership |
"ycmfn" |
YC MFN variant (legacy) |
SAFEs with sideLetters: ["mfn"] automatically receive the lowest cap of any subsequent capped SAFE via populateSafeCaps().
Share counts and price-per-share (PPS) are rounded to match legal standards. The default strategy floors shares and rounds PPS to 5 decimal places. Override via RoundingStrategy:
const strategy = {
roundDownShares: true, // floor shares (default)
roundPPSPlaces: 5, // PPS decimal places (default)
};Some rows can't be fully calculated and are flagged rather than crashed:
"tbd"— Needs more info (e.g. uncapped SAFE before a priced round)"caveat"— Calculated with assumptions (e.g. MFN cap assigned)"error"— Invalid input (e.g. investment ≥ cap)
Calculates ownership percentages for existing shareholders with no round.
import { buildExistingShareholderCapTable } from "@1984vc/cap-table";
const rows = buildExistingShareholderCapTable(founders);
// rows[0].ownershipPct === 0.45Estimates SAFE ownership before a priced round is known. Marks uncapped SAFEs as "tbd".
const { common, safes, total } = buildEstimatedPreRoundCapTable([
...founders,
...safes,
]);Returns { common: CommonCapTableRow[], safes: SafeCapTableRow[], total: TotalCapTableRow }.
Builds the pre-round view using a solved BestFit from fitConversion().
const { common, safes, total } = buildPreRoundCapTable(conversion, stakeHolders);Builds the full cap table including the new priced round and refreshed options pool.
const { common, safes, series, refreshedOptionsPool, total, error } =
buildPricedRoundCapTable(conversion, stakeHolders);fitConversion(preMoneyValuation, commonShares, safes, unusedOptions, targetOptionsPct, seriesInvestments, roundingStrategy?)
Iteratively solves for share counts at a priced round, accounting for SAFE conversions and option pool refresh.
const conversion = fitConversion(
12_000_000, // pre-money valuation
9_000_000, // common shares outstanding (excluding unused options)
safes, // SAFENote[]
1_000_000, // unused options
0.10, // target option pool % post-round
[2_000_000], // one entry per series investor (or total)
);Returns a BestFit object:
{
pps: number // series round price per share
ppss: number[] // per-SAFE conversion price
convertedSafeShares: number
seriesShares: number
preMoneyShares: number // fully diluted pre-money share count
postMoneyShares: number
newSharesIssued: number
totalShares: number
additionalOptions: number // new options beyond existing pool
totalOptions: number
totalInvested: number
totalSeriesInvestment: number
roundingStrategy: RoundingStrategy
}Applies MFN logic — assigns each uncapped MFN SAFE the lowest cap from subsequent capped SAFEs.
const processedSafes = populateSafeCaps(rawSafes);Returns the effective conversion price per share for a single SAFE.
Validates SAFE inputs. Returns a CapTableOwnershipError or undefined.
import { stringToNumber, formatUSDWithCommas, shortenedUSD } from "@1984vc/cap-table";
stringToNumber("$1.5M") // → 1_500_000
stringToNumber("1,000,000") // → 1_000_000
formatUSDWithCommas(1_234_567) // → "$1,234,567"
shortenedUSD(1_500_000) // → "$1.5M"
shortenedUSD(50_000) // → "$50K"type CommonStockholder = {
id?: string;
name: string;
shares: number;
type: CapTableRowType.Common;
commonType: CommonRowType.Shareholder | CommonRowType.UnusedOptions;
};
type SAFENote = {
id?: string;
name?: string;
investment: number;
cap: number;
discount: number;
type: CapTableRowType.Safe;
conversionType: "pre" | "post" | "mfn" | "yc7p" | "ycmfn";
sideLetters?: ("mfn" | "pro-rata")[];
};
type SeriesInvestor = {
id?: string;
name?: string;
investment: number;
type: CapTableRowType.Series;
round: number; // 0-indexed round number
};
type StakeHolder = CommonStockholder | SAFENote | SeriesInvestor;enum CapTableRowType {
Common = "common",
Safe = "safe",
Series = "series",
Total = "total",
RefreshedOptions = "refreshedOptions",
}
enum CommonRowType {
Shareholder = "shareholder",
UnusedOptions = "unusedOptions",
}npm install
npm run build # tsup → dist/
npm test # vitestMIT — 1984 Ventures