Eugene Kaizin · Case Studies · CS-002
CS-002 In Development Solo

A Typed Language for Business Models That Cannot Lie

Financial Modeling TypeScript DSL Atomic Design Compile-time Validation Constraint Solver Deterministic Simulation Open Source v2 architecture
Executive Summary
Domain
Financial modeling for decision-making under uncertainty
Core Problem
Financial models that look correct but cannot verify themselves
Architecture
TypeScript DSL with compile-time validation and deterministic solver
Key Mechanism
Constraints as build-time contracts — broken models fail before simulation
Demo Model
Coffee To Go: 3 SKUs, 2 staff, 24-month simulation, 3 scenarios
Status
v1 in production · v2 (Atomic Design architecture) in development
Problem Space

The Plausibility Trap

There is a specific failure mode in financial modeling more dangerous than an obvious error. It is a model that produces a number — a confident, formatted, plausible number — without checking whether that number is consistent with the assumptions it was built on. Call it the Plausibility Trap.

How it happens today

The Excel method
A founder builds a business plan in Google Sheets. Expenses are a flat line labeled "other: 50,000₽." Fixed and variable costs are not separated. When order volume grows from 10 to 70, variable costs stay flat. The model shows profit. The business runs at a loss. The error is invisible because Excel doesn't know what a variable cost is — it only knows what number is in the cell.
The template method
A downloaded business plan template. 80% of cells are irrelevant to this business. The remaining 20% are filled in by intuition. An inspector sees a formatted table — and no justification for a single number in it.
The vanilla AI method
ChatGPT produces a structured business plan. Revenue grows each month. Variable costs stay flat as volume scales. Margin in the model: 80%. Margin in the business: 12%. The model is internally inconsistent — and nothing in the generation process checks for that.

The cost of a model that cannot verify itself

A founder invests a 350,000₽ grant into a business where contribution margin is negative. The model never showed this because expenses were one line.

A plan for a social contract grant is rejected. The inspector couldn't trace a single number to a source. The model had no provenance.

A business is acquired for 8M₽. Six months later: 60% of revenue was one client who left. A PESSIMISTIC scenario simulation would have shown the risk before signing.

A founder plans 73 orders per month. Physically possible at 160 working hours at 3.3 hours per order: 48. The capacity breach was never modeled.

The common root: a model that accepts any input and produces output without verifying the internal consistency of its own assumptions.

Design Constraints

Boundaries established before implementation

Before writing a line of code, I established what a valid solution must satisfy. These constraints shaped every architectural decision that followed.

01 Every number must have a source. A hypothesis without evidence is a guess. Every input must carry provenance — where it came from, and how confident we are. A number without Evidence must not compile.
02 Broken unit economics must fail at build time, not at month 6. If contribution margin is negative, the model must refuse to run. Not warn. Refuse. The error must surface before simulation, not inside it.
03 Capacity is a physical constraint, not an assumption. A model that promises 73 orders when a founder can physically complete 48 is not conservative — it is wrong. Resource limits must be encoded and enforced.
04 The solver must find exact values, not approximations. "Approximately 50 orders" is not an answer when signing a procurement order. The breakeven threshold must be a precise number, found deterministically.
05 Every calculation step must be reproducible. An inspector, investor, or auditor must be able to trace every output number back to the assumption that produced it. Black-box results are not auditable results.
06 The architecture must be domain-agnostic. The primitives that model a coffee shop must be the same primitives that model a manufacturing line or an M&A acquisition. The engine must not know what "coffee" is.
Architecture Rationale

Why a DSL, not a tool

The standard answer to "Excel is bad for financial modeling" is a SaaS application — a form-based UI that guides the user through inputs and produces a report. That is the wrong abstraction for this problem.

A form-based tool encodes one model of how a business works. It handles the cases its designer anticipated. It fails on everything else. A DSL encodes the rules of financial modeling, not the models themselves. Users compose from primitives. The system validates composition, not content.

Approach Stores Validation Composition
Excel Numbers None None
SaaS tool One model Partial None
Vanilla LLM Plausible structure None None
BEPO SDK Rules Build time Primitives

The SDK is a TypeScript library. A model is code. Code compiles — or it doesn't. That property is the entire point.

Architecture

Atomic Design for Financial Modeling

BEPO v2 is organized as a five-level hierarchy where each level validates its own invariants before the next level can compose from it. A broken atom cannot become a valid molecule. A molecule with negative margin cannot become a valid organism. A model cannot simulate if it hasn't passed every level of validation below it.

Architecture Diagram / In Development
Three-layer constraint pipeline
Atoms
Rate, Money, Evidence
Molecules
ExpensePool, DriverChain
Organisms
Phase, RevenueModel
Templates
RetailPoint, SoloFounder
Pages
coffeeToGo.ts, carwash.ts
Interactive SVG diagram · Atomic Design component · Scheduled for v1.1
Level 1 — Atoms
Immutable Value Objects
Rate, Money, Duration, Evidence, Hypothesis. Invariant checked at construction: Rate.of(-1) throws. These are the irreducible primitives — every level above inherits their guarantees.
Level 2 — Molecules
Composed Atoms with Structural Validation
ExpensePool, DriverChain, RevenueStreamPool. Invariant: shares must sum to 1.0. At least one expense required. .build() returns T or throws — there are no warnings at the structural level, only mathematical validity or invalidity.
Level 3 — Organisms
Full Business Components with Domain Rules
Phase, Operations, RevenueModel, MarketingEngine. Invariant: contributionMargin > 0. Capacity coverage enforced. .build() returns BuildResult<T> — errors throw, risks go to warnings[].
Level 4 — Templates
Parameterized Business Patterns
SoloFounder, RetailPoint, ServiceWithMarketing. .configure(props) returns Builder. Builder.build() returns BuildResult<BusinessModel>. Builder.fork(overrides) returns new Builder from merged props — no cloning.
Level 5 — Pages
Concrete Models with Real Hypotheses
coffeeToGo.ts, carwash.ts. Data only. No business logic. The page declares hypotheses, composes templates, and runs simulation. Every level below has already been validated.
Validation Contract
Level Checks When Returns
AtomOwn invariant (≥ 0, not null)ConstructionT or throw
MoleculeStructural (sum = 1.0).build()T or throw
OrganismBusiness rule (margin > 0).build()BuildResult<T>
TemplateStructural completeness.build()BuildResult<T>

Molecules don't use the BuildResult envelope — there are no business warnings at the structural level, only mathematical validity or invalidity. Organisms and above produce warnings: SHORT_RUNWAY, LOW_CONFIDENCE, NO_MARKETING_BINDING. Domain objects stay clean. Diagnostics live in the envelope.

Key Mechanisms

Five mechanisms that make the system reliable

1

Evidence-Bound Hypotheses

Every input to a model is a Hypothesis. A Hypothesis carries: the value, SourceType (MARKET_RESEARCH, BENCHMARK, HISTORICAL_DATA, EXPERT_GUESS), Confidence (LOW, MEDIUM, HIGH), and optionally a URL or document reference.

A FixedDriver without evidence does not compile. The system cannot be given a number without being told where it came from. This is not metadata — it is part of the model's validity contract.

The reasoning trace output shows: "Traffic = 3 (constant, source: MARKET_RESEARCH, confidence: MEDIUM)." An inspector reviewing a grant application sees not just the number, but its provenance.

2

Unit Economics as a Compile Gate

If the contribution margin is negative — if variable cost per unit exceeds ticket size — the model must refuse to run. Not warn. Refuse. The error must surface before simulation, not inside it.

PhaseBuilder.build()
class PhaseBuilder { build(): BuildResult<Phase> { const phase = new Phase(this.expensePool, this.revenueModel, ...); // Business invariant — blocks simulation if (phase.calculateContributionMargin() <= 0) { throw new UnitEconomicsError( 'Contribution margin is negative: variable cost per unit exceeds ticket size.' ); } // Business risks — allow simulation, surface in warnings const warnings = this.scanForRisks(phase); return { value: phase, warnings }; } }
3

Deterministic Solver — Binary Search for Breakeven

The Solver finds the minimum driver value at which the business becomes viable — not through intuition, not through manual iteration, but through binary search with precision to 0.01. The answer to "how many orders do I need to break even" is not "around 50." It is "52.4 orders per day — the threshold below which cash crisis is inevitable, accounting for capacity constraints and the current expense structure."

4

Capacity-Aware Simulation

Resources — founder time, equipment, staff — have hard limits in hours. When demand exceeds physical supply, the simulation throws CapacityBreachError. This is not a warning. A model that promises output its resources cannot produce is not a conservative model — it is an incorrect one. The CapacityPool validates that for every month of the simulation, the sum of demandHours across all service units does not exceed supplyHours across all resources. If it does — the simulation stops.

5

Reasoning Trace

Every calculation step is recorded. The trace is not a debug log — it is a deliverable. An investor, an inspector, or an auditor receives not a number but a complete derivation of how every number in the model was produced.

reasoning_trace.log — month 3
Month 3: Traffic = RampUp(month=3, duration=6) × Seasonal(coefficient=1.1) = (3/6 × base(3)) × 1.1 = 1.5 × 3 × 1.1 = 4.95 orders/day Source: MARKET_RESEARCH · Confidence: MEDIUM
Architecture Decision

Unified Revenue Model

v1 had two parallel calculation paths inside Phase.calculateMonth(): the legacy funnel (traffic × conversion × ticket) and the operational model (CapacityPool). This was duplication — and a source of inconsistency when models grew complex.

v2 unifies them behind a single interface. Phase no longer knows which mode is active. It delegates to RevenueModel. The calculation path is an implementation detail of the revenue organism.

SimpleMode
traffic × conversion × ticketSize
For quick estimates, MVP models. The simplest valid revenue calculation — three multiplied drivers producing orders and revenue.
DetailedMode
RevenueStreamPool with SKU mix
For coffee shops, retail, multi-product businesses. Each SKU has a share of total orders, an individual price, and a per-unit variable cost. Shares must sum to 1.0.
CapacityMode
CapacityPool with Demand[]
For service businesses, manufacturing, carwashes. Revenue is bounded by physical capacity — demand hours per service unit cannot exceed supply hours across resources.
Architecture Decision

Marketing → Revenue Binding

In v1, marketing drivers were often attached as mutations to existing model state. In v2, all organisms are immutable. The binding is explicit and happens at build time.

Phase assembly
// 1. Build the marketing organism const { value: marketing } = MarketingEngine.create({ channels, budget }).build(); // 2. Extract the aggregated traffic driver — immutable const trafficDriver = marketing.getAggregatedTrafficDriver(); // 3. Pass explicitly into the revenue model const { value: revenue } = RevenueModel.create(SimpleMode) .withTraffic(trafficDriver) .withTicketSize(ticketPrice) .build();

If withTraffic() is not called on a Phase that includes a MarketingEngine, Phase.build() adds Warning: NO_MARKETING_BINDING to the envelope. The simulation still runs — but the operator knows the binding is missing.

Composition Pattern

Template Composition and Fork

Templates are factories — they take parameters and return a Builder. The Builder holds props, not assembled instances. fork() merges props and returns a new Builder. Nothing is cloned.

Three models. One source of truth. Every fork re-validates from atoms up.

scenario_fork.ts
// Base configuration const baseConfig = RetailPoint.configure({ name: "Coffee To Go — Base", menu: [ { name: "Espresso", share: 0.65, price: 150, cost: 45 }, { name: "Filter", share: 0.10, price: 120, cost: 30 }, { name: "Food", share: 0.25, price: 200, cost: 80 }, ], staff: [{ role: "Barista", count: 2, salary: 45000 }], location: { rent: 65000, utilities: 12000 }, taxRegime: new USN6Strategy(), }); const { value: baseModel, warnings } = baseConfig.build(); // Scenario fork — new Builder from merged props, no cloning const optimistic = baseConfig.fork({ menu: baseConfig.props.menu.map(sku => ({ ...sku, price: sku.price * 1.15 })), }).build(); const pessimistic = baseConfig.fork({ menu: baseConfig.props.menu.map(sku => ({ ...sku, price: sku.price * 0.85 })), }).build();
Demo

Coffee To Go — 3 Scenarios

The coffeeToGo.ts page demonstrates the full stack: template composition, SKU-mix revenue, 24-month simulation, and scenario analysis. All three scenarios built via .fork() from a single base configuration.

Input Hypotheses (LLM-generated, verifiable)
Daily traffic: 70 customers · BENCHMARK · MEDIUM
Conversion: 0.85 · BENCHMARK · MEDIUM
SKU mix: Espresso 65% / Filter 10% / Food 25%
Staff: 2 baristas at 45,000₽/month
Fixed expenses: Rent 65,000₽ · Utilities 12,000₽
CAPEX: 985,000₽ (equipment + fit-out)
Tax regime: USN 6%
Simulation: 24 months

BASE scenario — 24-month simulation

Month Orders Revenue Expenses Net Cumulative
11,488162,900₽198,000₽−35,100₽−35,100₽
61,785210,000₽178,000₽+32,000₽−62,400₽
121,890224,700₽183,000₽+41,700₽+89,700₽
241,890224,700₽185,000₽+39,700₽+462,900₽
solver_output.log
Minimum viable daily traffic: 54.2 customers Current plan: 70.0 customers Safety margin: +29% Below 54.2 → cash crisis inevitable given current expense structure.

Scenario Comparison

System Score Thermal Compatibility Trace Notes
BASE +462,900₽ Month 8 None 54.2/day Baseline configuration · 70 customers/day
OPTIMISTIC +631,200₽ Month 6 None 48.1/day +15% pricing across all SKUs · 70 customers/day
PESSIMISTIC +187,400₽ Month 14 Months 3–5 (89,000₽) 63.7/day −15% pricing · Crisis depth: −89,000₽ at month 5
Architecture Decision Record

ADR-001: The Atomic Design Decision

The v1 architecture had three layers: Primitives → Building Blocks → Engine. Functional, but without a reuse mechanism. Every simulation was written from scratch. Copy-paste was the only composition pattern.

The v2 decision: five-level Atomic Design, borrowed from UI component architecture and applied to financial modeling. The mapping is structurally sound because financial modeling is fractal: rates compose into expenses, expenses compose into pools, pools compose into phases, phases compose into business models. Each level is self-contained and self-validating.

Four open questions were resolved before v2 implementation:

Decision 01
Fork semantics — operate on Builder props, not class instances
Reason
Deep-cloning TypeScript class instances breaks prototypes and violates immutability. A fork re-assembles from configuration — the resulting object is always freshly validated from atoms up.
Tradeoff
Every fork re-runs full validation. Computationally more expensive than cloning, but correctness is the point — a cloned model that skipped validation would defeat the architecture.
Alternative rejected: Object.assign / structuredClone on assembled instances → prototype chain corruption, silent immutability violations
Decision 02
Build contract — BuildResult<T> envelope at Organism level and above
Reason
Domain objects must not carry diagnostic state. Atoms and Molecules throw on invariant violation — there are no warnings at the structural level. Organisms and Templates return { value, warnings } to separate business risks from structural errors.
Tradeoff
Two different return contracts (throw vs. envelope). But the boundary is clear: structural = throw, business risk = warning. This distinction is load-bearing — it tells the operator what kind of problem they have.
Alternative rejected: Unified Result<T, E> everywhere → structural errors become handle-able when they should be non-recoverable
Decision 03
Marketing → Revenue binding is explicit and immutable
Reason
MarketingEngine produces an immutable Driver. That Driver is explicitly passed to RevenueModel at Phase assembly time. No mutation. No implicit coupling. Missing binding → Warning in the envelope, not a silent fallback.
Tradeoff
Requires the model author to explicitly wire marketing output to revenue input. This is deliberate — implicit wiring is the source of the spreadsheet errors the system exists to prevent.
Alternative rejected: Implicit coupling via shared mutable state → the Excel pattern the architecture is designed to eliminate
Decision 04
Registry deferred — ES module imports provide better type safety
Reason
As a TypeScript library for developers, ES module imports provide superior type safety and tree-shaking compared to string-keyed registry access. Registry becomes relevant only if a no-code editor is added — at which point it is justified by concrete requirements.
Tradeoff
No runtime discoverability of templates. But discoverability for developers comes from IDE autocomplete on named exports — which is strictly better than string-keyed lookup.
Alternative rejected: BepoRegistry with string-keyed template lookup → worse type safety, no tree-shaking, premature abstraction without a consumer
What I Learned
The validation hierarchy is the architecture — not a feature on top
Each level inherits the validation of every level below it. A Template that composes broken Molecules cannot build. This is not defensive programming — it is the architecture. The property that makes the system reliable is not any individual check but the fact that no check can be bypassed from above. A broken atom poisons every structure that touches it.
BuildResult as an envelope keeps domain objects clean
In v1, warnings were fields on domain objects — Phase had a warnings[] array, which meant domain logic and diagnostic state were mixed. v2 separates them: domain objects carry only business data. Diagnostics live in the BuildResult envelope. The separation means domain methods never need to check "am I in a warning state?" before computing.
The engine must not know the domain — and that is the hardest design constraint
Driver.valueForMonth() returns a number. It does not know what "coffee" is. ExpensePool sums items. It does not know what "rent" is. Solver.findMinimum() runs binary search. It does not know what "breakeven" means. This agnosticism is deliberate and difficult — every time a convenience method was tempting ("getCoffeeShopMetrics()"), it was rejected. The engine that knows coffee is useless for manufacturing.
Replicability

The engine is the transferable asset

The SDK primitives have no domain knowledge. Driver.valueForMonth() returns a number — it does not know what "coffee" is. ExpensePool sums items — it does not know what "rent" is. Solver.findMinimum() runs binary search — it does not know what "breakeven" means. The difference between a coffee shop and any of these use cases is not the engine. It is the model. The engine is already built.

Due Diligence
Input seller claimed figures as Hypothesis entries with SourceType: HISTORICAL_DATA. Run PESSIMISTIC. Find the traffic level at which the business fails to service its debt. Buy with that number, not without it.
CFO Leak Detection
Decompose variable expenses to component level. Solver runs sensitivity analysis: if logistics drops by 30₽/order, breakeven shifts from 52 to 41 units. The CFO sees the value of optimization in the same units as the decision.
Investor Preparation
Three scenarios. Reasoning trace for every number. Solver output: survival threshold. The founder arrives at the meeting with a mathematical argument, not a presentation.
Multi-Phase Valuation
Three phases: current state → scaling → stabilization. Each phase has independent drivers, expenses, capacity. Cumulative cash flow across phases, with scenario variance on discount rate.
Engine Properties
Driver → returns a number, agnostic to what it drives
ExpensePool → sums Fixed, Variable, Percent items, agnostic to expense type
Solver → binary search on driver until totalProfit > 0
CapacityPool → validates demandHours ≤ supplyHours, agnostic to domain
ReasoningTrace → records every calculation step, produces auditable output
This is one of several projects I'm documenting
Available to walk through the architecture, the tradeoffs, and what this constraint-driven pattern could look like applied to your domain.