Build Auritrack

KINDAreplaces $3/mosaves $36/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A personal ledger whose hard problems are representing money so totals never drift, turning a loose sentence or a messy statement into rows without letting a model commit anything on its own, and keeping a correction trail that explains where every figure came from.

estimated effort weekendthe files for this build are in the project pack

RuntimeNode 22 with Express, server-rendered pages, bound to the loopback addressStorageSQLite with amounts stored as integer minor units and a currency column beside eachExtractionOne model call per input with a strict schema, producing a proposal and never a rowImportA staging table with per-row confidence, reviewed and committed as one batchReportingArithmetic in minor units throughout, formatted only at the last moment

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the local application.

    Get it nodejs.org or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. have readyfree

    Why The exponent per currency is the fact the whole money representation rests on, and guessing it is how a schema ends up wrong for the dinar and the yen at the same time.

    Get it Download the current published list and seed the currencies table from it, recording the publication date.

    Verify The seeded table gives the yen an exponent of zero and the Kuwaiti dinar an exponent of three

  3. API keypay per call; a statement import is the expensive operation and the README should say roughly how much

    Why Extraction from a sentence and from statement rows both need validated JSON, and this is the only part that costs money.

    Get it Create a key and set a provider-side spend limit.

    Verify One sentence produces a proposal that validates against the schema

  4. have readyfree

    Why Every import assumption you make will be wrong for the second bank, and finding that out on day one is much cheaper than finding out in April.

    Get it Download PDF or CSV statements from two accounts you hold.

    Verify Both stage rows with page and line indices recorded

  5. decidefree

    Why Whether foreign transactions are converted at all, and with which stored rate, changes the schema and every report, and deciding it late means recomputing history.

    Get it Write down the base currency and the rate source, and put both in configuration.

    Verify A foreign transaction without a stored rate is refused rather than estimated

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- currencies(code, exponent, symbol, source_note) · seeded from the published list, with the exponent stored rather than assumed
- transactions(id, occurred_on, amount_minor, currency, payee, category_id, note, source_kind, source_ref, created_at) · amount_minor is an integer and there is no decimal column anywhere
- proposals(id, source_kind, raw_input_excerpt, model_id, prompt_version, extracted_json, confidence, created_at, resolved_at, resolution) · what the model said, kept whether or not it was accepted
- corrections(id, proposal_id, field, model_value, user_value, at) · the only record of how wrong the extraction usually is
- import_batches(id, filename, file_checksum, page_count, rows_staged, rows_committed, state, created_at) · a batch is committed whole or not at all
- staged_rows(id, batch_id, page, line_index, parsed_json, confidence, fingerprint, duplicate_of, decision)
- budgets(id, category_id, month, limit_minor, currency) · in minor units, like everything else
- rates(from_currency, to_currency, rate_ppm, as_of_date, source) · stored per conversion, so last month's report does not change when the market does

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/ledger.dbThe whole ledger. This is the backup target and the only copy.
BASE_CURRENCYrequiredGBPReports total in this. Foreign amounts need a stored rate.
USER_TIMEZONErequiredEurope/LondonAn IANA zone name. Relative dates resolve against this and today's date in it.
MODEL_API_KEYsecretoptionalsk-...redactedExtraction only. The ledger works fully without it through manual entry.
MODEL_MONTHLY_CEILINGoptional5.00Extraction stops here; manual entry and reports continue.
IMPORT_CONFIDENCE_FLOORrequired0.7Staged rows below this must be resolved by hand before a batch can commit.
BACKUP_DIRrequired./backupsNightly copies with a retention count. Restored on a schedule, not on faith.

The build, in order

  1. A ledger that cannot drift

    Every amount is an integer count of minor units in a named currency, and the arithmetic is tested at the awkward cases.

    1. Two decimal places is an assumption, not a fact.

      Files migrations/001_currencies.sqlsrc/money/currencies.ts

    2. Nothing between those two points sees a decimal.

      Files src/money/parse.tstests/money.test.ts

    3. The ledger has to be usable when the key expires.

      Files src/app/transactions/new.tsx

    done when · tick each as it passes
    watch out
    • Storing amounts as a decimal number because the database offers one, then summing them in the application layer, reintroduces binary floating point at exactly the step where it matters, and the symptom is a category total that disagrees with the sum of its rows by a penny in a way nobody can reproduce. Keep integers end to end, and write a test that sums ten thousand random amounts and compares against an independently computed integer total.
  2. A sentence becomes a proposal, not a row

    Typed input produces something you confirm, with ambiguity surfaced as a question rather than resolved by guessing.

    1. Never let a model infer what day it is.

      Files src/extract/prompt.tstests/relative-dates.test.ts

    2. The schema asks which fields it was unsure about.

      Files src/extract/schema.tssrc/app/confirm.tsx

    3. It is the only measurement of how wrong extraction is for you.

      Files src/extract/corrections.ts

    done when · tick each as it passes
    watch out
    • Letting the model resolve 'yesterday' without being told today's date and the user's zone produces an entry a day out whenever the server clock is in a different zone or the request crosses midnight, and a ledger that is quietly a day out is worse than one that is obviously broken. Send today's date in the user's zone as part of the input, and have the schema return a date rather than a relative phrase.
  3. Statement import that commits whole or not at all

    A statement becomes a reviewed batch with per-row confidence, and a partial commit is impossible.

    1. Contents alone cannot tell two real coffees apart.

      Files src/import/fingerprint.tstests/duplicate-coffees.test.ts

    2. A silently discarded row is undiscoverable later.

      Files src/import/duplicates.ts

    3. Half an import is the worst state this ledger can reach.

      Files src/import/commit.tstests/atomic-import.test.ts

    done when · tick each as it passes
    watch out
    • Deduplicating on date, amount and payee collapses two genuinely separate identical purchases on the same day into one, and because the discarded row never existed in your database you will never find out, which turns reconciliation against the bank into an unwinnable argument. Fingerprint on the source file, page and line so every statement row is distinct, flag matches as suspected duplicates for a human, and never drop a row silently.
  4. Budgets and reports that do not move

    A report of last month shows the same numbers next year, including across currencies.

    1. A historical figure should never move.

      Files src/money/convert.tstests/stable-history.test.ts

    2. Declining to show a number beats inventing one.

      Files src/reports/aggregate.ts

    3. Two implementations means two answers and one argument.

      Files src/reports/aggregate.tssrc/app/reports/page.tsx

    done when · tick each as it passes
    watch out
    • Converting foreign transactions with today's rate at report time means last March's total changes every time you open the page, and a budget that was met in March quietly becomes a budget that was missed, which destroys the only thing a report is for. Store the rate and the date used on the transaction at the moment it is recorded, and treat a missing rate as a figure you decline to show rather than one you estimate.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more personal finance.