Build Portfolio Coach

KINDAback 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 household tracker whose hard problems are deriving positions from a transaction ledger you can reconcile, never displaying a valuation without its source and age, and keeping a model review on the describing side of a regulated line.

estimated effort multi-daythe files for this build are in the project pack

RuntimeNext.js with TypeScript and SQLite, bound to localhostLedgerTransactions are the record; positions are derived and rebuildableMoneyInteger minor units with a currency code, and every rate stored with its dateRulesA dated tax rules file with a source and a review date, checked at render timeModelOne call that describes and asks questions, with a refusal check on the output

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 tracker locally.

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

    Verify node --version prints v22 or higher

  2. have readyfree

    Why Reconciliation is the only thing that proves the ledger is right, and it needs a figure produced by somebody other than your own code.

    Get it Download transaction history and a valuation statement from the same date for two accounts.

    Verify Derived positions match the statement to the penny

  3. have readyfree

    Why Every figure on the planning page comes from it, and a figure with no source and no review date is a number that will be quietly wrong.

    Get it Read the current published thresholds on the revenue authority's own pages, record the value, the tax year, the source link and the date you checked, and set a review date. open ↗

    Verify Every row has a source, a checked date and a review date, and a test asserts it

  4. decidefree

    Why A tracker with a model attached drifts towards advice one helpful feature at a time, and the boundary has to exist before the first review is generated.

    Get it Write down that no output names an instrument to buy, sell, hold or switch, that no segment-based suggestion is produced, and what the output check refuses.

    Verify The adversarial prompt test enforces the same list the README states

  5. API keypennies per review

    Why The review is the only part that leaves the machine, and it should be the only part with a spend limit and a provider-side ceiling.

    Get it Create a key with your preferred provider and set a spend limit in their console.

    Verify A review runs and the payload snapshot test passes

Data model

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

- people(id, name, relationship) and accounts(id, person_id, wrapper, provider, currency, opened_on)
- transactions(id, account_id, at, kind, instrument_id, units, price_minor, price_currency, fee_minor, fx_rate, fx_rate_date, source_row) · the record everything else is derived from
- instruments(id, symbol, name, currency, kind, identifier_kind, identifier) · the identifier kind matters because the same fund answers to several
- prices(instrument_id, as_of, price_minor, currency, source) · a price with no source and no date is not shown
- positions_cache(account_id, instrument_id, as_of, units, book_cost_minor) · derived, rebuildable, never authoritative
- assumptions(id, name, value, unit, set_by, set_at) · every projection input, attributed to the person who typed it
- tax_rules(id, jurisdiction, name, value_minor, tax_year, source_url, checked_on, review_by) · the planning page refuses once the review date has passed
- reviews(id, at, model_id, prompt_version, input_summary_json, output_text, refusals_json) · what was sent and what came back, because 'the app told me to' needs an answer
- There is no table for a recommendation, a target allocation or a suggested trade. There is nowhere to put one, and that absence is the design.

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/portfolio.dbThe ledger, the prices, the assumptions and the reviews. Back it up properly.
RULES_PATHrequired./data/rulesDated tax rules files. Updates add rows; nothing is edited in place.
PRICE_STALE_HOURSrequired48Past this a price is marked stale and its age appears on the headline.
PRICE_FEED_KEYsecretoptionalpk_...redactedOptional. With it absent the application values everything from manual prices.
MODEL_API_KEYsecretoptionalsk-...redactedThe review. The only outbound call, and the only thing with a spend ceiling.
MODEL_MONTHLY_CEILINGrequired2.00Reviews stop at this figure. The tracker carries on without them.
EXPORT_PASSPHRASEsecretrequireda-long-passphrase-redactedEncrypts the export. Losing it loses the export, not the database.
BASE_CURRENCYrequiredGBPEverything is converted into this for totals, with the rate and its date stored.

The build, in order

  1. A ledger you can reconcile, not a list of holdings

    Positions are derived from transactions and can be checked against a real statement.

    1. Everything else is a view and should be deletable without loss.

      Files src/db/schema.sqlsrc/positions.ts

    2. A pretty number nobody has checked is worse than no number.

      Files src/app/reconcile/page.tsxtests/reconcile.test.ts

    3. The rate on the day is part of the transaction, not a lookup.

      Files src/import/map.ts

    done when · tick each as it passes
    watch out
    • Letting someone type today's holdings and calling it a portfolio leaves no cost basis, no history and nothing to check against anything, so every gain figure is a guess and a double import goes unnoticed for a year. Store transactions, derive positions, and put a reconciliation screen in front of the dashboard so a mismatch is visible on the day it appears rather than at the next tax year.
  2. No valuation without its source and its age

    Every figure on the screen can be traced to a price, a source and a time.

    1. A price without provenance is a rumour with two decimal places.

      Files src/prices/store.ts

    2. It is the one number that tells you whether to trust the others.

      Files src/app/page.tsx

    3. Manual prices are the floor, not the fallback.

      Files src/prices/manual.tstests/no-feed.test.ts

    done when · tick each as it passes
    watch out
    • Falling back to the last known price when a feed fails makes a portfolio valued on Friday look as though it was valued this morning, and the first time anybody notices is when a figure somebody acted on turns out to be a fortnight old. Stamp every price with its source and time, mark stale ones visibly, and put the age of the oldest price next to the headline total rather than behind a hover.
  3. Projections made only of assumptions somebody typed

    The chart cannot be drawn until every input has been supplied and attributed.

    1. An unowned number becomes everyone's number.

      Files src/projection/inputs.ts

    2. A single line is a forecast and you do not have one.

      Files src/projection/chart.tsx

    3. So the screenshot arrives with its own footnotes.

      Files src/projection/assumptions.tstests/projection.test.ts

    done when · tick each as it passes
    watch out
    • Shipping a default growth rate means the chart somebody screenshots and shows their partner was produced by a number your application chose, and every conversation afterwards is about a figure nobody in the room decided. Ship none: leave the projection page empty until the assumptions are entered, attribute each one, and carry them on the face of the chart rather than in a panel underneath.
  4. A planning page that expires

    Every tax figure has a source, a checked date and a review date, and the page stops rather than ages.

    1. The figure and its expiry are one fact.

      Files data/rules/uk-2026.jsonsrc/rules.ts

    2. Showing nothing is honest; showing last year's threshold is not.

      Files src/app/planning/page.tsxtests/expiry.test.ts

    3. A disclaimer in the footer is a disclaimer nobody reads.

      Files src/components/TaxFigure.tsx

    done when · tick each as it passes
    watch out
    • Hard-coding a threshold that has been frozen for years feels safe precisely because it has not moved, and then a Finance Act extends the freeze again and your figure is right for the wrong reason until the year it silently is not. Put every figure in a dated file with its source, let the page refuse once the review date passes, and make updating the file a new row rather than an edit so an old estimate can still be explained.
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.