Private financial software · single owner

Land investment
accounting that
reconciles to the cent.

Plots are rarely paid for by one person. Plot Ledger records who funded what, works out the ownership that follows from it, and splits every sale across investors on their own agreements — down to the last cent, with nothing left to work out by hand.

tests on the money engine
101tests on the money engine
float operations on money
0float operations on money
tables, fully migrated
12tables, fully migrated
account that can sign in
1account that can sign in

The problem

A spreadsheet cannot hold this shape

One plot can be funded by several people at once, each on different terms, each splitting their profit differently between reinvestment and cash. Every sale means recalculating all of it, and a single missed reinvestment leaves the capital figures wrong from then on.

Who put money in, and how much?

What share of this plot does each of them own?

What did the plot make when it sold?

Who gets how much of that?

How much goes back to work, and how much is paid out?

What is free to invest again right now?

Ownership follows from what each position actually paid, so a jointly bought plot needs no special handling.

Reinvested profit grows the pools automatically — capital never stays frozen at the opening figure.

Cash paid out is profit only. Returned principal is never mistaken for a payout.

Losses fall on whoever provided the capital. The owner takes a share of profit, not of loss.

How a sale resolves

Profit falls through three splits

Each stage divides what the one above it produced. Every stage adds back to its parent exactly, which is what makes the final row trustworthy.

1 · what the plot made

Sale price less buy price

$18,000.00

divided by how much each position put in

2 · each position's part

Owner capital

$9,000.00

50% of the plot

Investor A

$4,500.00

25%

Investor B

$4,500.00

25%

each position split on its own agreement

3 · owner and investor shares

Owner total

$13,050.00

Investors total

$4,950.00

each share split between reinvestment and cash

4 · where it ends up

Back into owner pool

$9,787.50

Owner cash

$3,262.50

Back into investor pools

$3,465.00

Cash paid to investors

$1,485.00

Worked example

One plot, three agreements, one table

An $80,000 plot sold for $98,000. Every figure below came out of the real calculation engine; the amounts are illustrative.

Buy price

$80,000.00

Sale price

$98,000.00

Plot profit

$18,000.00

PositionInvestedOwnsIts partOwner gets→ reinvested→ cashInvestor gets→ reinvested→ cash
Owner capitalall profit retained · 75% reinvested$40,000.0050%$9,000.00$9,000.00$6,750.00$2,250.00
Investor A50 / 50 · they reinvest 70%$20,000.0025%$4,500.00$2,250.00$1,687.50$562.50$2,250.00$1,575.00$675.00
Investor B40 / 60 · they reinvest 70%$20,000.0025%$4,500.00$1,800.00$1,350.00$450.00$2,700.00$1,890.00$810.00
Total$80,000.00100%$18,000.00$13,050.00$9,787.50$3,262.50$4,950.00$3,465.00$1,485.00
$13,050.00 + $4,950.00 = $18,000.00$9,787.50 + $3,262.50 = $13,050.00$3,465.00 + $1,485.00 = $4,950.00every cent accounted for

The engine

Two rules keep the arithmetic honest

The calculation engine is pure and database-free: it takes what a plot cost, what it sold for and who funded it, and returns where every cent went. Nothing else in the codebase does arithmetic on money.

finance/allocation.ts
// Give every position the floor of its exact share,
// then hand out the leftover cents, largest remainder first.
for (let i = 0; i < weightsCents.length; i += 1) {
  const exactNumerator = magnitude * weightsCents[i]!;
  const share = exactNumerator / totalWeight;

  base.push(share);
  distributed += share;
  remainders.push({ index: i, remainder: exactNumerator % totalWeight });
}

let leftover = magnitude - distributed;

// Largest remainder first; ties resolved by original order,
// so the same inputs always give the same answer.
remainders.sort((a, b) =>
  a.remainder === b.remainder
    ? a.index - b.index
    : a.remainder > b.remainder ? -1 : 1,
);

for (const entry of remainders) {
  if (leftover <= 0n) break;
  base[entry.index] = base[entry.index]! + 1n;
  leftover -= 1n;
}

Splitting a total by weight almost always leaves a remainder. Floors alone lose those cents; rounding each share independently can invent them. The leftover is handed out one cent at a time, largest fractional remainder first, so the parts always sum to the original — and ties break on position, so the result never changes between runs.

finance/sale-calculator.ts
// Round one side, subtract for the other,
// so no cent can appear or vanish.
const ownerProfitCents =
  multiplyCentsByBps(positionProfitCents, ownerProfitBps);
const investorProfitCents =
  positionProfitCents - ownerProfitCents;

const ownerReinvestCents =
  multiplyCentsByBps(ownerProfitCents, ownerReinvestBps);
const ownerPersonalCashCents =
  ownerProfitCents - ownerReinvestCents;

Within a share, only one side is ever rounded. The other is whatever is left after subtracting it. Two independently rounded halves can drift a cent apart from the whole they came from; a half and a remainder cannot.

Engineering

The parts that matter when it is real money

No floating point, anywhere

Money is an integer count of cents in a bigint, stored in BIGINT columns. Percentages are integer basis points, so 68.2% is the number 6820. A cent cannot be lost to binary rounding because no float ever touches the ledger.

Splits that reconcile exactly

Profit is divided by the largest remainder method, so the parts always add back to the whole. Within a share, one side is rounded and the other derived by subtraction — a cent can never appear or vanish between them.

Invariants that run in production

Every distribution is verified before it can be written, and the database enforces the same balances with CHECK constraints. A split that does not reconcile is physically unstorable.

History that cannot move

The profit agreement is copied onto the funding row at the moment a plot is funded. Renegotiate later and past plots keep the terms they were funded on. A finalised sale is a snapshot, never recomputed.

Balances are derived, not stored

There are no mutable balance columns to drift out of sync. Every pool, every available figure and every payout is recomputed from immutable capital transactions, funding rows and sale snapshots.

Single owner, checked three times

Middleware, every page and every server action independently verify the session and scope each query to its owner. Financial writes take row locks, so two requests can never both finalise one sale.

Built with

A deliberately ordinary stack

  • Next.js

    App Router, Server Components

  • TypeScript

    strict, no implicit any

  • PostgreSQL

    hosted on Neon

  • Drizzle ORM

    typed schema and migrations

  • Auth.js

    credentials, bcrypt, owner-only

  • Tailwind CSS

    with a small component kit

  • Zod

    one schema, client and server

  • Vitest

    101 tests on the money engine

Statements, not spreadsheet dumps

Portfolio, plot and investor statements are laid out for A4, repeat their table headers across pages, and never split a plot's figures over two sheets.

Built for a phone as much as a desk

Installable, with every table becoming a readable card rather than a sideways scroll, and the four screens used most reachable from the bottom bar.