Sky Yoo

Product engineering · 2026 · shipped

Mainspring

A local-first financial-independence planner with a deterministic Rust engine.

Mainspring — main view
MAINSPRING · MAIN VIEW

You wind it up — and it drives everything. A mainspring is wound once and releases that energy steadily to turn every hand on the face. This is a financial-independence planner built the same way — one deterministic engine, running on your machine, driving every number in the plan off a single tick. Accounts, balances, and assumptions stay on-device, with no account to create and nothing leaving by default. Built for planning that still has to be right years from now, not just today.

The instrument

Mainspring — A projection you can trust
PLATE I — A PROJECTION YOU CAN TRUST

A projection you can trust

Deterministic net-worth projections and a Monte Carlo success rate, computed on your machine in milliseconds — coast number, FI date, and what today's spending costs the plan.

Mainspring — Tune the instrument
PLATE II — TUNE THE INSTRUMENT

Tune the instrument

Set return and volatility assumptions directly, or start from a preset — three-fund, all-weather, a glidepath that de-risks near the date. Every number recalculates as you change one.

Mainspring — Rebalance without selling
PLATE III — REBALANCE WITHOUT SELLING

Rebalance without selling

See how far each holding has drifted from target, and how long steering new contributions there takes to close the gap — no selling, no capital-gains tax. Modeled from your own lots and cached prices.

Mainspring — Every dollar, accounted for
PLATE IV — EVERY DOLLAR, ACCOUNTED FOR

Every dollar, accounted for

Log spending against bills and goals and see whether you're in the black — how much of your take-home is working, and how much is spare.

The movement

The engineering underneath

A planner is only as good as its arithmetic. Most of the work lives below the dial — in spending precision where it matters, keeping the simulation honest, and leaving the whole of it yours.

Exact decimal where it counts, floats where they don't

Every balance, contribution, and rate is a Money value object over arbitrary-precision decimals — HALF_UP, forty digits, persisted as NUMERIC(18,4) so a figure round-trips through storage without ever touching a JS float. Its one constructor refuses floats outright; a lint rule enforces it across the codebase. The split is deliberate: exact decimal governs the ledger, where a drifted cent is a bug; the Monte Carlo kernel runs in f64, where you're sampling a distribution and decimal "exactness" would be a fiction.

money.ts
export class Money {
  private readonly d: Decimal;
  private constructor(d: Decimal) { this.d = d; }

  static of(value: string): Money {
    if (typeof (value as unknown) !== "string") {
      throw new TypeError("Money.of expects a decimal string, never a float.");
    }
    const d = new MoneyDecimal(value);
    if (!d.isFinite()) {
      throw new RangeError(`Money.of received a non-finite value: ${value}`);
    }
    return new Money(d);
  }

  add(other: Money): Money {
    return new Money(this.d.plus(other.d));
  }

  toString(): string {
    return this.d.toFixed(MONEY_SCALE, MoneyDecimal.ROUND_HALF_UP);
  }
}

A deterministic core, sealed off from the shell

The projection engine is one dependency-light Rust crate — serde and nothing else — that knows nothing about Svelte or Tauri: assumptions in, forecast out, no clock, no I/O, no global state. A seeded RNG makes every run reproducible, so the same inputs give the same numbers every time and the math can be unit-tested on its own — including a zero-volatility path checked against the closed-form compounding formula. One source, two targets: native today, WASM next.

The Monte Carlo models the risk averages hide

Thousands of paths run year by year, straight through the withdrawal phase, drawn from geometric Brownian motion — or bootstrapped from historical returns — rather than compounded from a single average. The ordering matters: a plan that clears on mean returns can still fail on a bad run of early years — sequence-of-returns risk — and only path-wise simulation surfaces it.

Local-first is the architecture, not a tagline

Your data lives on your machine — Postgres compiled into the webview (PGlite) over IndexedDB — so by default there is no server to be the source of truth; the client is the system of record. That removes a whole class of machinery: no accounts, no sync-conflict resolution, no network on the hot path. Market quotes and an optional AI copilot are the only things that reach out, both opt-in, with any API key kept in the OS keychain rather than the database.

Specification
Core
Deterministic Rust kernel — pure, serde-only
Money
Exact-decimal ledger (decimal.js) — never floats
Simulation
Monte Carlo — GBM or bootstrapped returns
Storage
Local Postgres (PGlite) over IndexedDB
Privacy
Nothing leaves the device by default
Shell
Tauri + Svelte over the kernel
Verification
Seeded RNG — zero-volatility path checked against the closed-form compounding formula