Vibecode Mortgage Coach
track this build4 phases, 12 steps, beginner friendly0%The math here is not exotic: amortization schedules, PMI drop-off, points breakeven, net cost after N years, rent versus buy. An agent can build a clean side-by-side scenario comparator with charts and a printable one-pager in a single session, and for a person modeling their own refinance that is genuinely enough. What you cannot one-shot is the part that makes it a business tool: pulling live loan scenarios out of the LOS or pricing engine, compliance-reviewable output your employer will actually let you send to a borrower, and the delivery layer that tracks whether the client opened the presentation. If you are a licensed originator, your DIY version is a scratchpad, not something you put in front of a client. If you are the borrower trying to sanity check what your lender sent you, the DIY version is arguably better because you control the assumptions.
You are building a lean indie version of Mortgage Coach. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Mortgage Coach · indie build A loan comparison builder whose entire value is that the numbers are correct and their assumptions are visible: amortisation computed exactly, every scenario stating its inputs, and an output that cannot be mistaken for the regulated disclosure it is not. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Arithmetic | Exact decimal maths with a documented rounding policy | money compounding over thirty years amplifies every floating point error | | Amortisation | A generated schedule per scenario, retained | the schedule is the evidence behind every summary figure | | Assumptions | Stored per scenario and rendered on the output | a comparison whose assumptions are hidden is not a comparison | | Rates | Entered by hand with the date and source recorded | a stale rate presented as current is the most likely way this misleads | | Output | A document clearly distinct from any regulated disclosure | borrowers cannot be expected to tell them apart, so you must | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the calculation engine and the document renderer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An exact decimal arithmetic library** · free - Why: Three hundred and sixty periods of floating point produces a closing balance that is not zero, and a borrower checking your numbers will find it. - Get it: Any well-tested arbitrary precision decimal library for your runtime. - Verify: A thirty year schedule closes at exactly zero - [ ] **Published amortisation figures to verify against** · free - Why: Your own calculation cannot verify itself, and the whole value of this tool is that the numbers hold up. - Get it: Take a published schedule from a regulator or a bank for a stated principal, rate and term, and use it as a test fixture. - Verify: Your schedule matches the published one to the cent - [ ] **What you will and will not call things** · free to decide; compliance advice if this is commercial - Why: Using a regulated term for a figure computed differently is the most serious thing this build can get wrong, and the decision belongs before the first calculation. - Get it: Decide whether you will implement the regulation's own calculation exactly. If not, choose different names for your figures and write down what each includes. - Verify: A written note names every displayed figure and states exactly what it includes - [ ] **A copy of the prescribed disclosure forms** · free - Why: Phase 5 requires the output to be visually distinct from them, which you cannot check without having them side by side. - Get it: Download the official sample forms from the regulator. - Verify: Your output and the official form are distinguishable at a glance ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Producing any regulated disclosure. - Live lender rate feeds. - Underwriting and eligibility assessment. - Advice or recommendation. - Integrations with loan origination systems, pricing engines and CRMs, so every scenario is typed in by hand - Compliance-vetted presentation output and audit trail, which is the entire reason a regulated lender licenses this instead of using a spreadsheet - Borrower engagement tracking: who opened the presentation, how long they looked, which option they clicked - Polished branded video and mobile presentation formats that agents and borrowers are used to receiving - Someone else maintaining the edge cases: ARMs, buydowns, MI structures, state-specific cost lines If one of those is essential to you, that is the reason to keep paying for Mortgage Coach, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Mortgage Coach The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me side-by-side loan comparisons that a borrower can actually check to replace Mortgage Coach. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool produces a document a loan officer shows a borrower who is about to make the largest financial decision of their life. Two things follow. First, the arithmetic has to be right and checkable. If you display a figure called an annual percentage rate it must be computed the way the regulation defines it, which in the United States is Regulation Z Appendix J, and it is not the interest rate. If you are not going to implement that calculation exactly, do not use the term. Call it something else and say what it includes. Second, this is not a regulated disclosure and must never look like one. The Loan Estimate is a specific document with a prescribed form and a three business day timing rule from application. A comparison presentation that borrows its layout or its language invites a borrower to treat it as the official one. State on every output what it is, what it is not, and that the terms are illustrative until a lender issues the real disclosure. Every assumption behind every number is shown on the document itself, because a comparison whose assumptions are hidden is a sales tool rather than an analysis. ### Stack (fixed, do not substitute) - Arithmetic: Exact decimal maths with a documented rounding policy · money compounding over thirty years amplifies every floating point error - Amortisation: A generated schedule per scenario, retained · the schedule is the evidence behind every summary figure - Assumptions: Stored per scenario and rendered on the output · a comparison whose assumptions are hidden is not a comparison - Rates: Entered by hand with the date and source recorded · a stale rate presented as current is the most likely way this misleads - Output: A document clearly distinct from any regulated disclosure · borrowers cannot be expected to tell them apart, so you must ### Data model (create this before Phase 1) - scenarios(id, comparison_id, label, principal_minor, rate_bps, term_months, product_kind, points_bps, fees_json, escrow_json) - assumptions(scenario_id, key, value, source) · tax rate, insurance, appreciation, inflation, anything that moves a number - schedules(scenario_id, period, payment_minor, interest_minor, principal_minor, balance_minor) · generated, retained, checkable - rate_quotes(id, scenario_id, rate_bps, quoted_on, source, expires_on) · a rate has a date or it is not a rate - comparisons(id, borrower_ref, created_by, created_at, published_at, disclaimer_version) - outputs(id, comparison_id, path, rendered_at, inputs_hash) · so a document can be reproduced exactly from its inputs - Every displayed figure is derived from a retained schedule, so there is no summary number that cannot be traced to a row. ### Phase 1 · Amortisation that survives being checked Build: Generate an amortisation schedule from principal, rate and term using exact decimal arithmetic with a stated rounding policy. Verify against an independent source. Handle the final payment absorbing rounding rather than leaving a stray balance. Trap: Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. Done when: A thirty year schedule matches an independent calculator to the cent, asserted in a test against published figures. The final balance is exactly zero. The rounding policy is stated on the output. No floating point type appears in any money path, verified by inspecting the types. Do not build yet: No comparisons, assumptions or output documents. ### Phase 2 · Products that are not simple loans Build: Support adjustable rates with their caps and adjustment schedule, interest-only periods, and temporary buydowns. Model the worst case for an adjustable rate alongside the expected case, and show both. Trap: Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. Done when: An adjustable rate scenario shows initial, expected and cap-limited worst case payments together, with the worst case not suppressible. Interest-only periods compute correctly and the payment step at their end is shown. A temporary buydown shows the payment in each year including after it ends. Caps are applied per adjustment and over the life of the loan. Do not build yet: No comparison or output yet. ### Phase 3 · Comparing scenarios on the same assumptions Build: Compare scenarios over a chosen horizon, showing total interest, total cost and remaining balance. Require assumptions to match across scenarios or flag the difference prominently. Compute break-even where one has higher costs and a lower rate. Trap: Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. Done when: Two scenarios with differing assumptions cannot be compared without the differences being displayed prominently, tested by crafting such a pair. Break-even is computed and stated where costs and rate trade off. The horizon is the same across scenarios and is stated. Total cost figures reconcile to the retained schedules. Do not build yet: No output document or rate handling yet. ### Phase 4 · Rates have dates Build: Record every rate with the date it was quoted, its source and an expiry. Mark a scenario built on an expired rate. Refuse to publish a comparison whose rates have expired without an explicit acknowledgement. Trap: A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. Done when: Every rate carries a quote date, source and expiry. A comparison built on an expired rate is visibly marked wherever it appears. Publishing an expired comparison requires an explicit acknowledgement. Reopening an old comparison shows its age prominently rather than presenting it as current. Do not build yet: No output document yet. ### Phase 5 · A document that is honest about what it is Build: Render a comparison document that states what it is and is not, shows every assumption, and is visually distinct from the prescribed disclosure forms. Make it reproducible from stored inputs. Include the schedules as an appendix a borrower can check. Trap: Borrowing the layout or the language of the official disclosure makes the document feel authoritative in exactly the way it should not, and a borrower will reasonably treat it as the real thing. Make it look like what it is, state the distinction on the first page, and include the schedules so the figures can actually be verified. Done when: The output states on its first page what it is, what it is not, and that terms are illustrative until a lender issues the disclosure. It is visually distinct from the prescribed forms, checked side by side. Every assumption appears on the document. Re-rendering from stored inputs produces an identical document, verified by checksum. Do not build yet: Nothing after this. ### Out of scope (and why) - Producing any regulated disclosure. Those have prescribed forms, timing rules and liability, and generating one from an unlicensed tool is not something to attempt. - Live rate feeds from lenders, which is a licensing and integration business rather than a feature. - Underwriting or eligibility assessment, which decides whether someone can have a loan at all. - Advice. This computes and presents; it does not recommend, and the output should not read as though it does. ### README must contain - That any figure called an annual percentage rate must be computed as the regulation defines it, and if you have not implemented that, do not use the term - That the output is not a regulated disclosure and says so on its first page - That adjustable rate scenarios always show the cap-limited worst case - That every rate carries a date and expired comparisons are visibly stale - That every assumption is printed on the document itself ===== AGENTS.md ===== # Agent instructions · Mortgage Coach indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Exact decimal maths with a documented rounding policy, A generated schedule per scenario, retained, Stored per scenario and rendered on the output, Entered by hand with the date and source recorded, A document clearly distinct from any regulated disclosure. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. - Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. - Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. - A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. ===== BUILD_PLAN.md ===== # Build plan · Mortgage Coach A loan comparison builder whose entire value is that the numbers are correct and their assumptions are visible: amortisation computed exactly, every scenario stating its inputs, and an output that cannot be mistaken for the regulated disclosure it is not. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Amortisation that survives being checked Your schedule matches an independent calculation to the cent. ### Steps 1. Use exact decimal arithmetic Three hundred and sixty periods of floating point is a visible error. Files: `src/money.ts` 2. Verify against published figures An independent source, in the test suite. Your own maths is not a check. Files: `tests/amortisation.test.ts` 3. Absorb rounding in the final payment And assert the closing balance is exactly zero. Files: `src/amortise.ts` ### Done when - [ ] A thirty year schedule matches an independent calculator - [ ] The final balance is exactly zero - [ ] The rounding policy is stated on the output - [ ] No floating point appears in a money path ### Watch out - Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. ## Phase 2 · Products that are not simple loans Adjustable rates, interest-only periods and buydowns compute correctly. ### Steps 1. Model caps per adjustment and lifetime Both. Applying only one produces a worst case that is wrong. Files: `src/arm.ts`, `src/arm.test.ts` 2. Always show the worst case Not as an option. Suppressing it is the misleading default. Files: `src/scenarios.ts` 3. Show the payment step at every transition The end of an interest-only or buydown period is what surprises people. Files: `src/steps.ts` ### Done when - [ ] Adjustable scenarios show initial, expected and worst case - [ ] The worst case cannot be suppressed - [ ] Interest-only and buydown transitions are shown - [ ] Caps apply per adjustment and over the life ### Watch out - Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. ## Phase 3 · Comparing scenarios on the same assumptions Two loans are compared fairly or the comparison is refused. ### Steps 1. Enforce a common horizon Different horizons produce any answer you like. Files: `src/compare.ts` 2. Surface assumption differences prominently At the top, not in a footnote. This is where comparisons mislead. Files: `src/compare.ts` 3. Compute break-even explicitly Points against rate is the actual question. Answer it with a number. Files: `src/breakeven.ts`, `src/breakeven.test.ts` ### Done when - [ ] Differing assumptions are displayed prominently - [ ] Break-even is computed and stated - [ ] The horizon is common and stated - [ ] Totals reconcile to the retained schedules ### Watch out - Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. ## Phase 4 · Rates have dates No figure is presented as current when it is not. ### Steps 1. Attach a date and source to every rate A rate without a date is not information. Files: `src/rates.ts` 2. Expire and mark visibly In the document, not only in the editor. Files: `src/rates.ts` 3. Require acknowledgement to publish stale Possible, deliberate and recorded. Files: `src/publish.ts` ### Done when - [ ] Every rate has a date, source and expiry - [ ] Expired scenarios are marked wherever shown - [ ] Publishing expired requires acknowledgement - [ ] A reopened comparison shows its age ### Watch out - A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. ## Not in this build - Producing any regulated disclosure. - Live lender rate feeds. - Underwriting and eligibility assessment. - Advice or recommendation. ## After v1, if you want it - Scenario sensitivity, showing what a rate move does to the comparison - A borrower-facing view that lets them change assumptions themselves - Refinance break-even against an existing loan, which is the other common question ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Borrower scenarios and figures. Financial data about identified people. DATABASE_URL=postgres://localhost:5432/comparisons # Required. Stated on every output, because a borrower comparing against a bank needs to know. ROUNDING_POLICY=half_up_cent # Required. How long a quoted rate is presented as current. Short, because rates move. RATE_EXPIRY_DAYS=7 # Required. Whether the regulated term may be used. False unless the regulation's own calculation is implemented exactly. APR_TERM_ALLOWED=false # Required. Recorded per comparison, so you know what a past document said. DISCLAIMER_VERSION=2026-09 # Required. The default comparison horizon. Same across scenarios or the comparison is meaningless. COMMON_HORIZON_MONTHS=120
You are building a lean indie version of Mortgage Coach. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Mortgage Coach · indie build A loan comparison builder whose entire value is that the numbers are correct and their assumptions are visible: amortisation computed exactly, every scenario stating its inputs, and an output that cannot be mistaken for the regulated disclosure it is not. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Arithmetic | Exact decimal maths with a documented rounding policy | money compounding over thirty years amplifies every floating point error | | Amortisation | A generated schedule per scenario, retained | the schedule is the evidence behind every summary figure | | Assumptions | Stored per scenario and rendered on the output | a comparison whose assumptions are hidden is not a comparison | | Rates | Entered by hand with the date and source recorded | a stale rate presented as current is the most likely way this misleads | | Output | A document clearly distinct from any regulated disclosure | borrowers cannot be expected to tell them apart, so you must | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the calculation engine and the document renderer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An exact decimal arithmetic library** · free - Why: Three hundred and sixty periods of floating point produces a closing balance that is not zero, and a borrower checking your numbers will find it. - Get it: Any well-tested arbitrary precision decimal library for your runtime. - Verify: A thirty year schedule closes at exactly zero - [ ] **Published amortisation figures to verify against** · free - Why: Your own calculation cannot verify itself, and the whole value of this tool is that the numbers hold up. - Get it: Take a published schedule from a regulator or a bank for a stated principal, rate and term, and use it as a test fixture. - Verify: Your schedule matches the published one to the cent - [ ] **What you will and will not call things** · free to decide; compliance advice if this is commercial - Why: Using a regulated term for a figure computed differently is the most serious thing this build can get wrong, and the decision belongs before the first calculation. - Get it: Decide whether you will implement the regulation's own calculation exactly. If not, choose different names for your figures and write down what each includes. - Verify: A written note names every displayed figure and states exactly what it includes - [ ] **A copy of the prescribed disclosure forms** · free - Why: Phase 5 requires the output to be visually distinct from them, which you cannot check without having them side by side. - Get it: Download the official sample forms from the regulator. - Verify: Your output and the official form are distinguishable at a glance ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Producing any regulated disclosure. - Live lender rate feeds. - Underwriting and eligibility assessment. - Advice or recommendation. - Integrations with loan origination systems, pricing engines and CRMs, so every scenario is typed in by hand - Compliance-vetted presentation output and audit trail, which is the entire reason a regulated lender licenses this instead of using a spreadsheet - Borrower engagement tracking: who opened the presentation, how long they looked, which option they clicked - Polished branded video and mobile presentation formats that agents and borrowers are used to receiving - Someone else maintaining the edge cases: ARMs, buydowns, MI structures, state-specific cost lines If one of those is essential to you, that is the reason to keep paying for Mortgage Coach, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Mortgage Coach The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me side-by-side loan comparisons that a borrower can actually check to replace Mortgage Coach. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool produces a document a loan officer shows a borrower who is about to make the largest financial decision of their life. Two things follow. First, the arithmetic has to be right and checkable. If you display a figure called an annual percentage rate it must be computed the way the regulation defines it, which in the United States is Regulation Z Appendix J, and it is not the interest rate. If you are not going to implement that calculation exactly, do not use the term. Call it something else and say what it includes. Second, this is not a regulated disclosure and must never look like one. The Loan Estimate is a specific document with a prescribed form and a three business day timing rule from application. A comparison presentation that borrows its layout or its language invites a borrower to treat it as the official one. State on every output what it is, what it is not, and that the terms are illustrative until a lender issues the real disclosure. Every assumption behind every number is shown on the document itself, because a comparison whose assumptions are hidden is a sales tool rather than an analysis. ### Stack (fixed, do not substitute) - Arithmetic: Exact decimal maths with a documented rounding policy · money compounding over thirty years amplifies every floating point error - Amortisation: A generated schedule per scenario, retained · the schedule is the evidence behind every summary figure - Assumptions: Stored per scenario and rendered on the output · a comparison whose assumptions are hidden is not a comparison - Rates: Entered by hand with the date and source recorded · a stale rate presented as current is the most likely way this misleads - Output: A document clearly distinct from any regulated disclosure · borrowers cannot be expected to tell them apart, so you must ### Data model (create this before Phase 1) - scenarios(id, comparison_id, label, principal_minor, rate_bps, term_months, product_kind, points_bps, fees_json, escrow_json) - assumptions(scenario_id, key, value, source) · tax rate, insurance, appreciation, inflation, anything that moves a number - schedules(scenario_id, period, payment_minor, interest_minor, principal_minor, balance_minor) · generated, retained, checkable - rate_quotes(id, scenario_id, rate_bps, quoted_on, source, expires_on) · a rate has a date or it is not a rate - comparisons(id, borrower_ref, created_by, created_at, published_at, disclaimer_version) - outputs(id, comparison_id, path, rendered_at, inputs_hash) · so a document can be reproduced exactly from its inputs - Every displayed figure is derived from a retained schedule, so there is no summary number that cannot be traced to a row. ### Phase 1 · Amortisation that survives being checked Build: Generate an amortisation schedule from principal, rate and term using exact decimal arithmetic with a stated rounding policy. Verify against an independent source. Handle the final payment absorbing rounding rather than leaving a stray balance. Trap: Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. Done when: A thirty year schedule matches an independent calculator to the cent, asserted in a test against published figures. The final balance is exactly zero. The rounding policy is stated on the output. No floating point type appears in any money path, verified by inspecting the types. Do not build yet: No comparisons, assumptions or output documents. ### Phase 2 · Products that are not simple loans Build: Support adjustable rates with their caps and adjustment schedule, interest-only periods, and temporary buydowns. Model the worst case for an adjustable rate alongside the expected case, and show both. Trap: Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. Done when: An adjustable rate scenario shows initial, expected and cap-limited worst case payments together, with the worst case not suppressible. Interest-only periods compute correctly and the payment step at their end is shown. A temporary buydown shows the payment in each year including after it ends. Caps are applied per adjustment and over the life of the loan. Do not build yet: No comparison or output yet. ### Phase 3 · Comparing scenarios on the same assumptions Build: Compare scenarios over a chosen horizon, showing total interest, total cost and remaining balance. Require assumptions to match across scenarios or flag the difference prominently. Compute break-even where one has higher costs and a lower rate. Trap: Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. Done when: Two scenarios with differing assumptions cannot be compared without the differences being displayed prominently, tested by crafting such a pair. Break-even is computed and stated where costs and rate trade off. The horizon is the same across scenarios and is stated. Total cost figures reconcile to the retained schedules. Do not build yet: No output document or rate handling yet. ### Phase 4 · Rates have dates Build: Record every rate with the date it was quoted, its source and an expiry. Mark a scenario built on an expired rate. Refuse to publish a comparison whose rates have expired without an explicit acknowledgement. Trap: A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. Done when: Every rate carries a quote date, source and expiry. A comparison built on an expired rate is visibly marked wherever it appears. Publishing an expired comparison requires an explicit acknowledgement. Reopening an old comparison shows its age prominently rather than presenting it as current. Do not build yet: No output document yet. ### Phase 5 · A document that is honest about what it is Build: Render a comparison document that states what it is and is not, shows every assumption, and is visually distinct from the prescribed disclosure forms. Make it reproducible from stored inputs. Include the schedules as an appendix a borrower can check. Trap: Borrowing the layout or the language of the official disclosure makes the document feel authoritative in exactly the way it should not, and a borrower will reasonably treat it as the real thing. Make it look like what it is, state the distinction on the first page, and include the schedules so the figures can actually be verified. Done when: The output states on its first page what it is, what it is not, and that terms are illustrative until a lender issues the disclosure. It is visually distinct from the prescribed forms, checked side by side. Every assumption appears on the document. Re-rendering from stored inputs produces an identical document, verified by checksum. Do not build yet: Nothing after this. ### Out of scope (and why) - Producing any regulated disclosure. Those have prescribed forms, timing rules and liability, and generating one from an unlicensed tool is not something to attempt. - Live rate feeds from lenders, which is a licensing and integration business rather than a feature. - Underwriting or eligibility assessment, which decides whether someone can have a loan at all. - Advice. This computes and presents; it does not recommend, and the output should not read as though it does. ### README must contain - That any figure called an annual percentage rate must be computed as the regulation defines it, and if you have not implemented that, do not use the term - That the output is not a regulated disclosure and says so on its first page - That adjustable rate scenarios always show the cap-limited worst case - That every rate carries a date and expired comparisons are visibly stale - That every assumption is printed on the document itself ===== AGENTS.md ===== # Agent instructions · Mortgage Coach indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Exact decimal maths with a documented rounding policy, A generated schedule per scenario, retained, Stored per scenario and rendered on the output, Entered by hand with the date and source recorded, A document clearly distinct from any regulated disclosure. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. - Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. - Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. - A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. ===== BUILD_PLAN.md ===== # Build plan · Mortgage Coach A loan comparison builder whose entire value is that the numbers are correct and their assumptions are visible: amortisation computed exactly, every scenario stating its inputs, and an output that cannot be mistaken for the regulated disclosure it is not. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Amortisation that survives being checked Your schedule matches an independent calculation to the cent. ### Steps 1. Use exact decimal arithmetic Three hundred and sixty periods of floating point is a visible error. Files: `src/money.ts` 2. Verify against published figures An independent source, in the test suite. Your own maths is not a check. Files: `tests/amortisation.test.ts` 3. Absorb rounding in the final payment And assert the closing balance is exactly zero. Files: `src/amortise.ts` ### Done when - [ ] A thirty year schedule matches an independent calculator - [ ] The final balance is exactly zero - [ ] The rounding policy is stated on the output - [ ] No floating point appears in a money path ### Watch out - Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. ## Phase 2 · Products that are not simple loans Adjustable rates, interest-only periods and buydowns compute correctly. ### Steps 1. Model caps per adjustment and lifetime Both. Applying only one produces a worst case that is wrong. Files: `src/arm.ts`, `src/arm.test.ts` 2. Always show the worst case Not as an option. Suppressing it is the misleading default. Files: `src/scenarios.ts` 3. Show the payment step at every transition The end of an interest-only or buydown period is what surprises people. Files: `src/steps.ts` ### Done when - [ ] Adjustable scenarios show initial, expected and worst case - [ ] The worst case cannot be suppressed - [ ] Interest-only and buydown transitions are shown - [ ] Caps apply per adjustment and over the life ### Watch out - Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. ## Phase 3 · Comparing scenarios on the same assumptions Two loans are compared fairly or the comparison is refused. ### Steps 1. Enforce a common horizon Different horizons produce any answer you like. Files: `src/compare.ts` 2. Surface assumption differences prominently At the top, not in a footnote. This is where comparisons mislead. Files: `src/compare.ts` 3. Compute break-even explicitly Points against rate is the actual question. Answer it with a number. Files: `src/breakeven.ts`, `src/breakeven.test.ts` ### Done when - [ ] Differing assumptions are displayed prominently - [ ] Break-even is computed and stated - [ ] The horizon is common and stated - [ ] Totals reconcile to the retained schedules ### Watch out - Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. ## Phase 4 · Rates have dates No figure is presented as current when it is not. ### Steps 1. Attach a date and source to every rate A rate without a date is not information. Files: `src/rates.ts` 2. Expire and mark visibly In the document, not only in the editor. Files: `src/rates.ts` 3. Require acknowledgement to publish stale Possible, deliberate and recorded. Files: `src/publish.ts` ### Done when - [ ] Every rate has a date, source and expiry - [ ] Expired scenarios are marked wherever shown - [ ] Publishing expired requires acknowledgement - [ ] A reopened comparison shows its age ### Watch out - A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. ## Not in this build - Producing any regulated disclosure. - Live lender rate feeds. - Underwriting and eligibility assessment. - Advice or recommendation. ## After v1, if you want it - Scenario sensitivity, showing what a rate move does to the comparison - A borrower-facing view that lets them change assumptions themselves - Refinance break-even against an existing loan, which is the other common question ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Borrower scenarios and figures. Financial data about identified people. DATABASE_URL=postgres://localhost:5432/comparisons # Required. Stated on every output, because a borrower comparing against a bank needs to know. ROUNDING_POLICY=half_up_cent # Required. How long a quoted rate is presented as current. Short, because rates move. RATE_EXPIRY_DAYS=7 # Required. Whether the regulated term may be used. False unless the regulation's own calculation is implemented exactly. APR_TERM_ALLOWED=false # Required. Recorded per comparison, so you know what a past document said. DISCLAIMER_VERSION=2026-09 # Required. The default comparison horizon. Same across scenarios or the comparison is meaningless. COMMON_HORIZON_MONTHS=120
You are building a production product version of Mortgage Coach. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Mortgage Coach · product brief ## Problem The math here is not exotic: amortization schedules, PMI drop-off, points breakeven, net cost after N years, rent versus buy. An agent can build a clean side-by-side scenario comparator with charts and a printable one-pager in a single session, and for a person modeling their own refinance that is genuinely enough. What you cannot one-shot is the part that makes it a business tool: pulling live loan scenarios out of the LOS or pricing engine, compliance-reviewable output your employer will actually let you send to a borrower, and the delivery layer that tracks whether the client opened the presentation. If you are a licensed originator, your DIY version is a scratchpad, not something you put in front of a client. If you are the borrower trying to sanity check what your lender sent you, the DIY version is arguably better because you control the assumptions. ## Product outcome A comparison document whose arithmetic holds up against a bank's, whose assumptions are all on the page, and which no borrower could mistake for the official disclosure. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node 20+ - a browser - nothing else: no accounts, no API keys, all math runs locally ## Explicit non-goals for v1 - Producing any regulated disclosure. - Live lender rate feeds. - Underwriting and eligibility assessment. - Advice or recommendation. - Integrations with loan origination systems, pricing engines and CRMs, so every scenario is typed in by hand - Compliance-vetted presentation output and audit trail, which is the entire reason a regulated lender licenses this instead of using a spreadsheet - Borrower engagement tracking: who opened the presentation, how long they looked, which option they clicked - Polished branded video and mobile presentation formats that agents and borrowers are used to receiving - Someone else maintaining the edge cases: ARMs, buydowns, MI structures, state-specific cost lines ## Success criteria - A thirty year schedule is confirmed to match published figures to the cent - The adjustable rate worst case is confirmed not suppressible - Differing assumptions are confirmed displayed prominently - Expired rates are confirmed marked wherever shown - The output is confirmed visually distinct from the prescribed forms ===== BRIEF.md ===== # Build brief · Mortgage Coach The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me side-by-side loan comparisons that a borrower can actually check to replace Mortgage Coach. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool produces a document a loan officer shows a borrower who is about to make the largest financial decision of their life. Two things follow. First, the arithmetic has to be right and checkable. If you display a figure called an annual percentage rate it must be computed the way the regulation defines it, which in the United States is Regulation Z Appendix J, and it is not the interest rate. If you are not going to implement that calculation exactly, do not use the term. Call it something else and say what it includes. Second, this is not a regulated disclosure and must never look like one. The Loan Estimate is a specific document with a prescribed form and a three business day timing rule from application. A comparison presentation that borrows its layout or its language invites a borrower to treat it as the official one. State on every output what it is, what it is not, and that the terms are illustrative until a lender issues the real disclosure. Every assumption behind every number is shown on the document itself, because a comparison whose assumptions are hidden is a sales tool rather than an analysis. ### Stack (fixed, do not substitute) - Arithmetic: Exact decimal maths with a documented rounding policy · money compounding over thirty years amplifies every floating point error - Amortisation: A generated schedule per scenario, retained · the schedule is the evidence behind every summary figure - Assumptions: Stored per scenario and rendered on the output · a comparison whose assumptions are hidden is not a comparison - Rates: Entered by hand with the date and source recorded · a stale rate presented as current is the most likely way this misleads - Output: A document clearly distinct from any regulated disclosure · borrowers cannot be expected to tell them apart, so you must ### Data model (create this before Phase 1) - scenarios(id, comparison_id, label, principal_minor, rate_bps, term_months, product_kind, points_bps, fees_json, escrow_json) - assumptions(scenario_id, key, value, source) · tax rate, insurance, appreciation, inflation, anything that moves a number - schedules(scenario_id, period, payment_minor, interest_minor, principal_minor, balance_minor) · generated, retained, checkable - rate_quotes(id, scenario_id, rate_bps, quoted_on, source, expires_on) · a rate has a date or it is not a rate - comparisons(id, borrower_ref, created_by, created_at, published_at, disclaimer_version) - outputs(id, comparison_id, path, rendered_at, inputs_hash) · so a document can be reproduced exactly from its inputs - Every displayed figure is derived from a retained schedule, so there is no summary number that cannot be traced to a row. ### Phase 1 · Amortisation that survives being checked Build: Generate an amortisation schedule from principal, rate and term using exact decimal arithmetic with a stated rounding policy. Verify against an independent source. Handle the final payment absorbing rounding rather than leaving a stray balance. Trap: Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. Done when: A thirty year schedule matches an independent calculator to the cent, asserted in a test against published figures. The final balance is exactly zero. The rounding policy is stated on the output. No floating point type appears in any money path, verified by inspecting the types. Do not build yet: No comparisons, assumptions or output documents. ### Phase 2 · Products that are not simple loans Build: Support adjustable rates with their caps and adjustment schedule, interest-only periods, and temporary buydowns. Model the worst case for an adjustable rate alongside the expected case, and show both. Trap: Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. Done when: An adjustable rate scenario shows initial, expected and cap-limited worst case payments together, with the worst case not suppressible. Interest-only periods compute correctly and the payment step at their end is shown. A temporary buydown shows the payment in each year including after it ends. Caps are applied per adjustment and over the life of the loan. Do not build yet: No comparison or output yet. ### Phase 3 · Comparing scenarios on the same assumptions Build: Compare scenarios over a chosen horizon, showing total interest, total cost and remaining balance. Require assumptions to match across scenarios or flag the difference prominently. Compute break-even where one has higher costs and a lower rate. Trap: Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. Done when: Two scenarios with differing assumptions cannot be compared without the differences being displayed prominently, tested by crafting such a pair. Break-even is computed and stated where costs and rate trade off. The horizon is the same across scenarios and is stated. Total cost figures reconcile to the retained schedules. Do not build yet: No output document or rate handling yet. ### Phase 4 · Rates have dates Build: Record every rate with the date it was quoted, its source and an expiry. Mark a scenario built on an expired rate. Refuse to publish a comparison whose rates have expired without an explicit acknowledgement. Trap: A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. Done when: Every rate carries a quote date, source and expiry. A comparison built on an expired rate is visibly marked wherever it appears. Publishing an expired comparison requires an explicit acknowledgement. Reopening an old comparison shows its age prominently rather than presenting it as current. Do not build yet: No output document yet. ### Phase 5 · A document that is honest about what it is Build: Render a comparison document that states what it is and is not, shows every assumption, and is visually distinct from the prescribed disclosure forms. Make it reproducible from stored inputs. Include the schedules as an appendix a borrower can check. Trap: Borrowing the layout or the language of the official disclosure makes the document feel authoritative in exactly the way it should not, and a borrower will reasonably treat it as the real thing. Make it look like what it is, state the distinction on the first page, and include the schedules so the figures can actually be verified. Done when: The output states on its first page what it is, what it is not, and that terms are illustrative until a lender issues the disclosure. It is visually distinct from the prescribed forms, checked side by side. Every assumption appears on the document. Re-rendering from stored inputs produces an identical document, verified by checksum. Do not build yet: Nothing after this. ### Out of scope (and why) - Producing any regulated disclosure. Those have prescribed forms, timing rules and liability, and generating one from an unlicensed tool is not something to attempt. - Live rate feeds from lenders, which is a licensing and integration business rather than a feature. - Underwriting or eligibility assessment, which decides whether someone can have a loan at all. - Advice. This computes and presents; it does not recommend, and the output should not read as though it does. ### README must contain - That any figure called an annual percentage rate must be computed as the regulation defines it, and if you have not implemented that, do not use the term - That the output is not a regulated disclosure and says so on its first page - That adjustable rate scenarios always show the cap-limited worst case - That every rate carries a date and expired comparisons are visibly stale - That every assumption is printed on the document itself ===== ARCHITECTURE.md ===== # Architecture · Mortgage Coach ## Stack | Part | Choice | Why | | --- | --- | --- | | Arithmetic | Exact decimal maths with a documented rounding policy | money compounding over thirty years amplifies every floating point error | | Amortisation | A generated schedule per scenario, retained | the schedule is the evidence behind every summary figure | | Assumptions | Stored per scenario and rendered on the output | a comparison whose assumptions are hidden is not a comparison | | Rates | Entered by hand with the date and source recorded | a stale rate presented as current is the most likely way this misleads | | Output | A document clearly distinct from any regulated disclosure | borrowers cannot be expected to tell them apart, so you must | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Money | Exact decimal arithmetic and rounding. | Not swappable for floating point at any point, including intermediates. | | Products | Fixed, adjustable, interest-only and buydown modelling. | Any implementation, provided the worst case is always computed and shown. | | Comparison | Common horizons and assumption differences. | Any implementation that refuses to compare on mismatched assumptions silently. | | Rates | Dates, sources and expiry. | Any store. A rate without a date is the failure to avoid. | | Output | A document distinct from regulated forms. | Any renderer, provided the distinction is stated on the first page. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Borrower scenarios and figures. Financial data about identified people. - `ROUNDING_POLICY` · required · Stated on every output, because a borrower comparing against a bank needs to know. - `RATE_EXPIRY_DAYS` · required · How long a quoted rate is presented as current. Short, because rates move. - `APR_TERM_ALLOWED` · required · Whether the regulated term may be used. False unless the regulation's own calculation is implemented exactly. - `DISCLAIMER_VERSION` · required · Recorded per comparison, so you know what a past document said. - `COMMON_HORIZON_MONTHS` · required · The default comparison horizon. Same across scenarios or the comparison is meaningless. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Mortgage Coach product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Exact decimal maths with a documented rounding policy, A generated schedule per scenario, retained, Stored per scenario and rendered on the output, Entered by hand with the date and source recorded, A document clearly distinct from any regulated disclosure. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. - Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. - Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. - A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. - Borrowing the layout or the language of the official disclosure makes the document feel authoritative in exactly the way it should not, and a borrower will reasonably treat it as the real thing. Make it look like what it is, state the distinction on the first page, and include the schedules so the figures can actually be verified. ===== MILESTONES.md ===== # Delivery milestones · Mortgage Coach Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Amortisation that survives being checked Your schedule matches an independent calculation to the cent. ### Steps 1. Use exact decimal arithmetic Three hundred and sixty periods of floating point is a visible error. Files: `src/money.ts` 2. Verify against published figures An independent source, in the test suite. Your own maths is not a check. Files: `tests/amortisation.test.ts` 3. Absorb rounding in the final payment And assert the closing balance is exactly zero. Files: `src/amortise.ts` ### Done when - [ ] A thirty year schedule matches an independent calculator - [ ] The final balance is exactly zero - [ ] The rounding policy is stated on the output - [ ] No floating point appears in a money path ### Watch out - Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. ## M2 · Products that are not simple loans Adjustable rates, interest-only periods and buydowns compute correctly. ### Steps 1. Model caps per adjustment and lifetime Both. Applying only one produces a worst case that is wrong. Files: `src/arm.ts`, `src/arm.test.ts` 2. Always show the worst case Not as an option. Suppressing it is the misleading default. Files: `src/scenarios.ts` 3. Show the payment step at every transition The end of an interest-only or buydown period is what surprises people. Files: `src/steps.ts` ### Done when - [ ] Adjustable scenarios show initial, expected and worst case - [ ] The worst case cannot be suppressed - [ ] Interest-only and buydown transitions are shown - [ ] Caps apply per adjustment and over the life ### Watch out - Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. ## M3 · Comparing scenarios on the same assumptions Two loans are compared fairly or the comparison is refused. ### Steps 1. Enforce a common horizon Different horizons produce any answer you like. Files: `src/compare.ts` 2. Surface assumption differences prominently At the top, not in a footnote. This is where comparisons mislead. Files: `src/compare.ts` 3. Compute break-even explicitly Points against rate is the actual question. Answer it with a number. Files: `src/breakeven.ts`, `src/breakeven.test.ts` ### Done when - [ ] Differing assumptions are displayed prominently - [ ] Break-even is computed and stated - [ ] The horizon is common and stated - [ ] Totals reconcile to the retained schedules ### Watch out - Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. ## M4 · Rates have dates No figure is presented as current when it is not. ### Steps 1. Attach a date and source to every rate A rate without a date is not information. Files: `src/rates.ts` 2. Expire and mark visibly In the document, not only in the editor. Files: `src/rates.ts` 3. Require acknowledgement to publish stale Possible, deliberate and recorded. Files: `src/publish.ts` ### Done when - [ ] Every rate has a date, source and expiry - [ ] Expired scenarios are marked wherever shown - [ ] Publishing expired requires acknowledgement - [ ] A reopened comparison shows its age ### Watch out - A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. ## M5 · A document that is honest about what it is (production only) Nobody could mistake the output for a regulated disclosure. ### Steps 1. State the distinction on the first page Not in a footer. A borrower has to be able to tell. Files: `src/render.ts` 2. Print every assumption On the document. Hidden assumptions make it a sales tool. Files: `src/render.ts` 3. Make rendering reproducible Same inputs, identical document, verified by checksum. Files: `src/render.ts`, `tests/reproducible.test.ts` ### Done when - [ ] The first page states what the document is and is not - [ ] It is visually distinct from the prescribed forms - [ ] Every assumption appears on the document - [ ] Re-rendering produces an identical file ### Watch out - Borrowing the layout or the language of the official disclosure makes the document feel authoritative in exactly the way it should not, and a borrower will reasonably treat it as the real thing. Make it look like what it is, state the distinction on the first page, and include the schedules so the figures can actually be verified. ===== OPERATIONS.md ===== # Operations · Mortgage Coach ## Backup Comparisons are documents shown to borrowers and may be asked about later. Retain the inputs and the rendered outputs, since being able to reproduce exactly what someone was shown is the point of storing them at all. ## Restore Restore and confirm outputs re-render identically from stored inputs, since a restore that changes a figure means you can no longer say what a borrower was shown. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The proportion of published comparisons built on expired rates is the number worth watching, because it grows quietly and every one of them is a document presenting stale terms as current. ## Incident checklist A figure that does not reconcile to its schedule is the failure that matters, since every displayed number is supposed to be traceable. The schedules are retained precisely so this is answerable rather than arguable. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A thirty year schedule is confirmed to match published figures to the cent - [ ] The adjustable rate worst case is confirmed not suppressible - [ ] Differing assumptions are confirmed displayed prominently - [ ] Expired rates are confirmed marked wherever shown - [ ] The output is confirmed visually distinct from the prescribed forms ## Launch constraint Do not market omitted Mortgage Coach capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Borrower scenarios and figures. Financial data about identified people. DATABASE_URL=postgres://localhost:5432/comparisons # Required. Stated on every output, because a borrower comparing against a bank needs to know. ROUNDING_POLICY=half_up_cent # Required. How long a quoted rate is presented as current. Short, because rates move. RATE_EXPIRY_DAYS=7 # Required. Whether the regulated term may be used. False unless the regulation's own calculation is implemented exactly. APR_TERM_ALLOWED=false # Required. Recorded per comparison, so you know what a past document said. DISCLAIMER_VERSION=2026-09 # Required. The default comparison horizon. Same across scenarios or the comparison is meaningless. COMMON_HORIZON_MONTHS=120
# Mortgage Coach · indie build A loan comparison builder whose entire value is that the numbers are correct and their assumptions are visible: amortisation computed exactly, every scenario stating its inputs, and an output that cannot be mistaken for the regulated disclosure it is not. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Arithmetic | Exact decimal maths with a documented rounding policy | money compounding over thirty years amplifies every floating point error | | Amortisation | A generated schedule per scenario, retained | the schedule is the evidence behind every summary figure | | Assumptions | Stored per scenario and rendered on the output | a comparison whose assumptions are hidden is not a comparison | | Rates | Entered by hand with the date and source recorded | a stale rate presented as current is the most likely way this misleads | | Output | A document clearly distinct from any regulated disclosure | borrowers cannot be expected to tell them apart, so you must | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the calculation engine and the document renderer. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An exact decimal arithmetic library** · free - Why: Three hundred and sixty periods of floating point produces a closing balance that is not zero, and a borrower checking your numbers will find it. - Get it: Any well-tested arbitrary precision decimal library for your runtime. - Verify: A thirty year schedule closes at exactly zero - [ ] **Published amortisation figures to verify against** · free - Why: Your own calculation cannot verify itself, and the whole value of this tool is that the numbers hold up. - Get it: Take a published schedule from a regulator or a bank for a stated principal, rate and term, and use it as a test fixture. - Verify: Your schedule matches the published one to the cent - [ ] **What you will and will not call things** · free to decide; compliance advice if this is commercial - Why: Using a regulated term for a figure computed differently is the most serious thing this build can get wrong, and the decision belongs before the first calculation. - Get it: Decide whether you will implement the regulation's own calculation exactly. If not, choose different names for your figures and write down what each includes. - Verify: A written note names every displayed figure and states exactly what it includes - [ ] **A copy of the prescribed disclosure forms** · free - Why: Phase 5 requires the output to be visually distinct from them, which you cannot check without having them side by side. - Get it: Download the official sample forms from the regulator. - Verify: Your output and the official form are distinguishable at a glance ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Producing any regulated disclosure. - Live lender rate feeds. - Underwriting and eligibility assessment. - Advice or recommendation. - Integrations with loan origination systems, pricing engines and CRMs, so every scenario is typed in by hand - Compliance-vetted presentation output and audit trail, which is the entire reason a regulated lender licenses this instead of using a spreadsheet - Borrower engagement tracking: who opened the presentation, how long they looked, which option they clicked - Polished branded video and mobile presentation formats that agents and borrowers are used to receiving - Someone else maintaining the edge cases: ARMs, buydowns, MI structures, state-specific cost lines If one of those is essential to you, that is the reason to keep paying for Mortgage Coach, and the README should say so rather than pretend.
# Build brief · Mortgage Coach The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me side-by-side loan comparisons that a borrower can actually check to replace Mortgage Coach. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool produces a document a loan officer shows a borrower who is about to make the largest financial decision of their life. Two things follow. First, the arithmetic has to be right and checkable. If you display a figure called an annual percentage rate it must be computed the way the regulation defines it, which in the United States is Regulation Z Appendix J, and it is not the interest rate. If you are not going to implement that calculation exactly, do not use the term. Call it something else and say what it includes. Second, this is not a regulated disclosure and must never look like one. The Loan Estimate is a specific document with a prescribed form and a three business day timing rule from application. A comparison presentation that borrows its layout or its language invites a borrower to treat it as the official one. State on every output what it is, what it is not, and that the terms are illustrative until a lender issues the real disclosure. Every assumption behind every number is shown on the document itself, because a comparison whose assumptions are hidden is a sales tool rather than an analysis. ### Stack (fixed, do not substitute) - Arithmetic: Exact decimal maths with a documented rounding policy · money compounding over thirty years amplifies every floating point error - Amortisation: A generated schedule per scenario, retained · the schedule is the evidence behind every summary figure - Assumptions: Stored per scenario and rendered on the output · a comparison whose assumptions are hidden is not a comparison - Rates: Entered by hand with the date and source recorded · a stale rate presented as current is the most likely way this misleads - Output: A document clearly distinct from any regulated disclosure · borrowers cannot be expected to tell them apart, so you must ### Data model (create this before Phase 1) - scenarios(id, comparison_id, label, principal_minor, rate_bps, term_months, product_kind, points_bps, fees_json, escrow_json) - assumptions(scenario_id, key, value, source) · tax rate, insurance, appreciation, inflation, anything that moves a number - schedules(scenario_id, period, payment_minor, interest_minor, principal_minor, balance_minor) · generated, retained, checkable - rate_quotes(id, scenario_id, rate_bps, quoted_on, source, expires_on) · a rate has a date or it is not a rate - comparisons(id, borrower_ref, created_by, created_at, published_at, disclaimer_version) - outputs(id, comparison_id, path, rendered_at, inputs_hash) · so a document can be reproduced exactly from its inputs - Every displayed figure is derived from a retained schedule, so there is no summary number that cannot be traced to a row. ### Phase 1 · Amortisation that survives being checked Build: Generate an amortisation schedule from principal, rate and term using exact decimal arithmetic with a stated rounding policy. Verify against an independent source. Handle the final payment absorbing rounding rather than leaving a stray balance. Trap: Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. Done when: A thirty year schedule matches an independent calculator to the cent, asserted in a test against published figures. The final balance is exactly zero. The rounding policy is stated on the output. No floating point type appears in any money path, verified by inspecting the types. Do not build yet: No comparisons, assumptions or output documents. ### Phase 2 · Products that are not simple loans Build: Support adjustable rates with their caps and adjustment schedule, interest-only periods, and temporary buydowns. Model the worst case for an adjustable rate alongside the expected case, and show both. Trap: Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. Done when: An adjustable rate scenario shows initial, expected and cap-limited worst case payments together, with the worst case not suppressible. Interest-only periods compute correctly and the payment step at their end is shown. A temporary buydown shows the payment in each year including after it ends. Caps are applied per adjustment and over the life of the loan. Do not build yet: No comparison or output yet. ### Phase 3 · Comparing scenarios on the same assumptions Build: Compare scenarios over a chosen horizon, showing total interest, total cost and remaining balance. Require assumptions to match across scenarios or flag the difference prominently. Compute break-even where one has higher costs and a lower rate. Trap: Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. Done when: Two scenarios with differing assumptions cannot be compared without the differences being displayed prominently, tested by crafting such a pair. Break-even is computed and stated where costs and rate trade off. The horizon is the same across scenarios and is stated. Total cost figures reconcile to the retained schedules. Do not build yet: No output document or rate handling yet. ### Phase 4 · Rates have dates Build: Record every rate with the date it was quoted, its source and an expiry. Mark a scenario built on an expired rate. Refuse to publish a comparison whose rates have expired without an explicit acknowledgement. Trap: A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. Done when: Every rate carries a quote date, source and expiry. A comparison built on an expired rate is visibly marked wherever it appears. Publishing an expired comparison requires an explicit acknowledgement. Reopening an old comparison shows its age prominently rather than presenting it as current. Do not build yet: No output document yet. ### Phase 5 · A document that is honest about what it is Build: Render a comparison document that states what it is and is not, shows every assumption, and is visually distinct from the prescribed disclosure forms. Make it reproducible from stored inputs. Include the schedules as an appendix a borrower can check. Trap: Borrowing the layout or the language of the official disclosure makes the document feel authoritative in exactly the way it should not, and a borrower will reasonably treat it as the real thing. Make it look like what it is, state the distinction on the first page, and include the schedules so the figures can actually be verified. Done when: The output states on its first page what it is, what it is not, and that terms are illustrative until a lender issues the disclosure. It is visually distinct from the prescribed forms, checked side by side. Every assumption appears on the document. Re-rendering from stored inputs produces an identical document, verified by checksum. Do not build yet: Nothing after this. ### Out of scope (and why) - Producing any regulated disclosure. Those have prescribed forms, timing rules and liability, and generating one from an unlicensed tool is not something to attempt. - Live rate feeds from lenders, which is a licensing and integration business rather than a feature. - Underwriting or eligibility assessment, which decides whether someone can have a loan at all. - Advice. This computes and presents; it does not recommend, and the output should not read as though it does. ### README must contain - That any figure called an annual percentage rate must be computed as the regulation defines it, and if you have not implemented that, do not use the term - That the output is not a regulated disclosure and says so on its first page - That adjustable rate scenarios always show the cap-limited worst case - That every rate carries a date and expired comparisons are visibly stale - That every assumption is printed on the document itself
# Agent instructions · Mortgage Coach indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Exact decimal maths with a documented rounding policy, A generated schedule per scenario, retained, Stored per scenario and rendered on the output, Entered by hand with the date and source recorded, A document clearly distinct from any regulated disclosure. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. - Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. - Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. - A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong.
# Build plan · Mortgage Coach A loan comparison builder whose entire value is that the numbers are correct and their assumptions are visible: amortisation computed exactly, every scenario stating its inputs, and an output that cannot be mistaken for the regulated disclosure it is not. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Amortisation that survives being checked Your schedule matches an independent calculation to the cent. ### Steps 1. Use exact decimal arithmetic Three hundred and sixty periods of floating point is a visible error. Files: `src/money.ts` 2. Verify against published figures An independent source, in the test suite. Your own maths is not a check. Files: `tests/amortisation.test.ts` 3. Absorb rounding in the final payment And assert the closing balance is exactly zero. Files: `src/amortise.ts` ### Done when - [ ] A thirty year schedule matches an independent calculator - [ ] The final balance is exactly zero - [ ] The rounding policy is stated on the output - [ ] No floating point appears in a money path ### Watch out - Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. ## Phase 2 · Products that are not simple loans Adjustable rates, interest-only periods and buydowns compute correctly. ### Steps 1. Model caps per adjustment and lifetime Both. Applying only one produces a worst case that is wrong. Files: `src/arm.ts`, `src/arm.test.ts` 2. Always show the worst case Not as an option. Suppressing it is the misleading default. Files: `src/scenarios.ts` 3. Show the payment step at every transition The end of an interest-only or buydown period is what surprises people. Files: `src/steps.ts` ### Done when - [ ] Adjustable scenarios show initial, expected and worst case - [ ] The worst case cannot be suppressed - [ ] Interest-only and buydown transitions are shown - [ ] Caps apply per adjustment and over the life ### Watch out - Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. ## Phase 3 · Comparing scenarios on the same assumptions Two loans are compared fairly or the comparison is refused. ### Steps 1. Enforce a common horizon Different horizons produce any answer you like. Files: `src/compare.ts` 2. Surface assumption differences prominently At the top, not in a footnote. This is where comparisons mislead. Files: `src/compare.ts` 3. Compute break-even explicitly Points against rate is the actual question. Answer it with a number. Files: `src/breakeven.ts`, `src/breakeven.test.ts` ### Done when - [ ] Differing assumptions are displayed prominently - [ ] Break-even is computed and stated - [ ] The horizon is common and stated - [ ] Totals reconcile to the retained schedules ### Watch out - Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. ## Phase 4 · Rates have dates No figure is presented as current when it is not. ### Steps 1. Attach a date and source to every rate A rate without a date is not information. Files: `src/rates.ts` 2. Expire and mark visibly In the document, not only in the editor. Files: `src/rates.ts` 3. Require acknowledgement to publish stale Possible, deliberate and recorded. Files: `src/publish.ts` ### Done when - [ ] Every rate has a date, source and expiry - [ ] Expired scenarios are marked wherever shown - [ ] Publishing expired requires acknowledgement - [ ] A reopened comparison shows its age ### Watch out - A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. ## Not in this build - Producing any regulated disclosure. - Live lender rate feeds. - Underwriting and eligibility assessment. - Advice or recommendation. ## After v1, if you want it - Scenario sensitivity, showing what a rate move does to the comparison - A borrower-facing view that lets them change assumptions themselves - Refinance break-even against an existing loan, which is the other common question
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Borrower scenarios and figures. Financial data about identified people. DATABASE_URL=postgres://localhost:5432/comparisons # Required. Stated on every output, because a borrower comparing against a bank needs to know. ROUNDING_POLICY=half_up_cent # Required. How long a quoted rate is presented as current. Short, because rates move. RATE_EXPIRY_DAYS=7 # Required. Whether the regulated term may be used. False unless the regulation's own calculation is implemented exactly. APR_TERM_ALLOWED=false # Required. Recorded per comparison, so you know what a past document said. DISCLAIMER_VERSION=2026-09 # Required. The default comparison horizon. Same across scenarios or the comparison is meaningless. COMMON_HORIZON_MONTHS=120
# Mortgage Coach · product brief ## Problem The math here is not exotic: amortization schedules, PMI drop-off, points breakeven, net cost after N years, rent versus buy. An agent can build a clean side-by-side scenario comparator with charts and a printable one-pager in a single session, and for a person modeling their own refinance that is genuinely enough. What you cannot one-shot is the part that makes it a business tool: pulling live loan scenarios out of the LOS or pricing engine, compliance-reviewable output your employer will actually let you send to a borrower, and the delivery layer that tracks whether the client opened the presentation. If you are a licensed originator, your DIY version is a scratchpad, not something you put in front of a client. If you are the borrower trying to sanity check what your lender sent you, the DIY version is arguably better because you control the assumptions. ## Product outcome A comparison document whose arithmetic holds up against a bank's, whose assumptions are all on the page, and which no borrower could mistake for the official disclosure. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node 20+ - a browser - nothing else: no accounts, no API keys, all math runs locally ## Explicit non-goals for v1 - Producing any regulated disclosure. - Live lender rate feeds. - Underwriting and eligibility assessment. - Advice or recommendation. - Integrations with loan origination systems, pricing engines and CRMs, so every scenario is typed in by hand - Compliance-vetted presentation output and audit trail, which is the entire reason a regulated lender licenses this instead of using a spreadsheet - Borrower engagement tracking: who opened the presentation, how long they looked, which option they clicked - Polished branded video and mobile presentation formats that agents and borrowers are used to receiving - Someone else maintaining the edge cases: ARMs, buydowns, MI structures, state-specific cost lines ## Success criteria - A thirty year schedule is confirmed to match published figures to the cent - The adjustable rate worst case is confirmed not suppressible - Differing assumptions are confirmed displayed prominently - Expired rates are confirmed marked wherever shown - The output is confirmed visually distinct from the prescribed forms
# Build brief · Mortgage Coach The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me side-by-side loan comparisons that a borrower can actually check to replace Mortgage Coach. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This tool produces a document a loan officer shows a borrower who is about to make the largest financial decision of their life. Two things follow. First, the arithmetic has to be right and checkable. If you display a figure called an annual percentage rate it must be computed the way the regulation defines it, which in the United States is Regulation Z Appendix J, and it is not the interest rate. If you are not going to implement that calculation exactly, do not use the term. Call it something else and say what it includes. Second, this is not a regulated disclosure and must never look like one. The Loan Estimate is a specific document with a prescribed form and a three business day timing rule from application. A comparison presentation that borrows its layout or its language invites a borrower to treat it as the official one. State on every output what it is, what it is not, and that the terms are illustrative until a lender issues the real disclosure. Every assumption behind every number is shown on the document itself, because a comparison whose assumptions are hidden is a sales tool rather than an analysis. ### Stack (fixed, do not substitute) - Arithmetic: Exact decimal maths with a documented rounding policy · money compounding over thirty years amplifies every floating point error - Amortisation: A generated schedule per scenario, retained · the schedule is the evidence behind every summary figure - Assumptions: Stored per scenario and rendered on the output · a comparison whose assumptions are hidden is not a comparison - Rates: Entered by hand with the date and source recorded · a stale rate presented as current is the most likely way this misleads - Output: A document clearly distinct from any regulated disclosure · borrowers cannot be expected to tell them apart, so you must ### Data model (create this before Phase 1) - scenarios(id, comparison_id, label, principal_minor, rate_bps, term_months, product_kind, points_bps, fees_json, escrow_json) - assumptions(scenario_id, key, value, source) · tax rate, insurance, appreciation, inflation, anything that moves a number - schedules(scenario_id, period, payment_minor, interest_minor, principal_minor, balance_minor) · generated, retained, checkable - rate_quotes(id, scenario_id, rate_bps, quoted_on, source, expires_on) · a rate has a date or it is not a rate - comparisons(id, borrower_ref, created_by, created_at, published_at, disclaimer_version) - outputs(id, comparison_id, path, rendered_at, inputs_hash) · so a document can be reproduced exactly from its inputs - Every displayed figure is derived from a retained schedule, so there is no summary number that cannot be traced to a row. ### Phase 1 · Amortisation that survives being checked Build: Generate an amortisation schedule from principal, rate and term using exact decimal arithmetic with a stated rounding policy. Verify against an independent source. Handle the final payment absorbing rounding rather than leaving a stray balance. Trap: Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. Done when: A thirty year schedule matches an independent calculator to the cent, asserted in a test against published figures. The final balance is exactly zero. The rounding policy is stated on the output. No floating point type appears in any money path, verified by inspecting the types. Do not build yet: No comparisons, assumptions or output documents. ### Phase 2 · Products that are not simple loans Build: Support adjustable rates with their caps and adjustment schedule, interest-only periods, and temporary buydowns. Model the worst case for an adjustable rate alongside the expected case, and show both. Trap: Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. Done when: An adjustable rate scenario shows initial, expected and cap-limited worst case payments together, with the worst case not suppressible. Interest-only periods compute correctly and the payment step at their end is shown. A temporary buydown shows the payment in each year including after it ends. Caps are applied per adjustment and over the life of the loan. Do not build yet: No comparison or output yet. ### Phase 3 · Comparing scenarios on the same assumptions Build: Compare scenarios over a chosen horizon, showing total interest, total cost and remaining balance. Require assumptions to match across scenarios or flag the difference prominently. Compute break-even where one has higher costs and a lower rate. Trap: Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. Done when: Two scenarios with differing assumptions cannot be compared without the differences being displayed prominently, tested by crafting such a pair. Break-even is computed and stated where costs and rate trade off. The horizon is the same across scenarios and is stated. Total cost figures reconcile to the retained schedules. Do not build yet: No output document or rate handling yet. ### Phase 4 · Rates have dates Build: Record every rate with the date it was quoted, its source and an expiry. Mark a scenario built on an expired rate. Refuse to publish a comparison whose rates have expired without an explicit acknowledgement. Trap: A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. Done when: Every rate carries a quote date, source and expiry. A comparison built on an expired rate is visibly marked wherever it appears. Publishing an expired comparison requires an explicit acknowledgement. Reopening an old comparison shows its age prominently rather than presenting it as current. Do not build yet: No output document yet. ### Phase 5 · A document that is honest about what it is Build: Render a comparison document that states what it is and is not, shows every assumption, and is visually distinct from the prescribed disclosure forms. Make it reproducible from stored inputs. Include the schedules as an appendix a borrower can check. Trap: Borrowing the layout or the language of the official disclosure makes the document feel authoritative in exactly the way it should not, and a borrower will reasonably treat it as the real thing. Make it look like what it is, state the distinction on the first page, and include the schedules so the figures can actually be verified. Done when: The output states on its first page what it is, what it is not, and that terms are illustrative until a lender issues the disclosure. It is visually distinct from the prescribed forms, checked side by side. Every assumption appears on the document. Re-rendering from stored inputs produces an identical document, verified by checksum. Do not build yet: Nothing after this. ### Out of scope (and why) - Producing any regulated disclosure. Those have prescribed forms, timing rules and liability, and generating one from an unlicensed tool is not something to attempt. - Live rate feeds from lenders, which is a licensing and integration business rather than a feature. - Underwriting or eligibility assessment, which decides whether someone can have a loan at all. - Advice. This computes and presents; it does not recommend, and the output should not read as though it does. ### README must contain - That any figure called an annual percentage rate must be computed as the regulation defines it, and if you have not implemented that, do not use the term - That the output is not a regulated disclosure and says so on its first page - That adjustable rate scenarios always show the cap-limited worst case - That every rate carries a date and expired comparisons are visibly stale - That every assumption is printed on the document itself
# Architecture · Mortgage Coach ## Stack | Part | Choice | Why | | --- | --- | --- | | Arithmetic | Exact decimal maths with a documented rounding policy | money compounding over thirty years amplifies every floating point error | | Amortisation | A generated schedule per scenario, retained | the schedule is the evidence behind every summary figure | | Assumptions | Stored per scenario and rendered on the output | a comparison whose assumptions are hidden is not a comparison | | Rates | Entered by hand with the date and source recorded | a stale rate presented as current is the most likely way this misleads | | Output | A document clearly distinct from any regulated disclosure | borrowers cannot be expected to tell them apart, so you must | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Money | Exact decimal arithmetic and rounding. | Not swappable for floating point at any point, including intermediates. | | Products | Fixed, adjustable, interest-only and buydown modelling. | Any implementation, provided the worst case is always computed and shown. | | Comparison | Common horizons and assumption differences. | Any implementation that refuses to compare on mismatched assumptions silently. | | Rates | Dates, sources and expiry. | Any store. A rate without a date is the failure to avoid. | | Output | A document distinct from regulated forms. | Any renderer, provided the distinction is stated on the first page. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Borrower scenarios and figures. Financial data about identified people. - `ROUNDING_POLICY` · required · Stated on every output, because a borrower comparing against a bank needs to know. - `RATE_EXPIRY_DAYS` · required · How long a quoted rate is presented as current. Short, because rates move. - `APR_TERM_ALLOWED` · required · Whether the regulated term may be used. False unless the regulation's own calculation is implemented exactly. - `DISCLAIMER_VERSION` · required · Recorded per comparison, so you know what a past document said. - `COMMON_HORIZON_MONTHS` · required · The default comparison horizon. Same across scenarios or the comparison is meaningless. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Mortgage Coach product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Exact decimal maths with a documented rounding policy, A generated schedule per scenario, retained, Stored per scenario and rendered on the output, Entered by hand with the date and source recorded, A document clearly distinct from any regulated disclosure. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. - Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. - Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. - A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. - Borrowing the layout or the language of the official disclosure makes the document feel authoritative in exactly the way it should not, and a borrower will reasonably treat it as the real thing. Make it look like what it is, state the distinction on the first page, and include the schedules so the figures can actually be verified.
# Delivery milestones · Mortgage Coach Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Amortisation that survives being checked Your schedule matches an independent calculation to the cent. ### Steps 1. Use exact decimal arithmetic Three hundred and sixty periods of floating point is a visible error. Files: `src/money.ts` 2. Verify against published figures An independent source, in the test suite. Your own maths is not a check. Files: `tests/amortisation.test.ts` 3. Absorb rounding in the final payment And assert the closing balance is exactly zero. Files: `src/amortise.ts` ### Done when - [ ] A thirty year schedule matches an independent calculator - [ ] The final balance is exactly zero - [ ] The rounding policy is stated on the output - [ ] No floating point appears in a money path ### Watch out - Floating point arithmetic over three hundred and sixty periods accumulates error that shows up as a final balance that is not zero, and a borrower who checks your figures against a bank's will find the discrepancy and stop trusting the whole document. Use exact decimal types, state the rounding policy, and assert the final balance is exactly zero. ## M2 · Products that are not simple loans Adjustable rates, interest-only periods and buydowns compute correctly. ### Steps 1. Model caps per adjustment and lifetime Both. Applying only one produces a worst case that is wrong. Files: `src/arm.ts`, `src/arm.test.ts` 2. Always show the worst case Not as an option. Suppressing it is the misleading default. Files: `src/scenarios.ts` 3. Show the payment step at every transition The end of an interest-only or buydown period is what surprises people. Files: `src/steps.ts` ### Done when - [ ] Adjustable scenarios show initial, expected and worst case - [ ] The worst case cannot be suppressed - [ ] Interest-only and buydown transitions are shown - [ ] Caps apply per adjustment and over the life ### Watch out - Showing an adjustable rate loan at its initial rate for the whole term makes it look permanently cheaper than a fixed one, which is the single most misleading thing this category of tool can do. Compute and display the fully indexed and cap-limited worst case beside the initial period, always, not as an option. ## M3 · Comparing scenarios on the same assumptions Two loans are compared fairly or the comparison is refused. ### Steps 1. Enforce a common horizon Different horizons produce any answer you like. Files: `src/compare.ts` 2. Surface assumption differences prominently At the top, not in a footnote. This is where comparisons mislead. Files: `src/compare.ts` 3. Compute break-even explicitly Points against rate is the actual question. Answer it with a number. Files: `src/breakeven.ts`, `src/breakeven.test.ts` ### Done when - [ ] Differing assumptions are displayed prominently - [ ] Break-even is computed and stated - [ ] The horizon is common and stated - [ ] Totals reconcile to the retained schedules ### Watch out - Comparing two loans over different horizons, or with different assumed tax and insurance figures, produces whichever answer the person building it wanted, and it looks entirely legitimate. Require assumptions to match, and where they genuinely differ, show the difference at the top rather than in a footnote. ## M4 · Rates have dates No figure is presented as current when it is not. ### Steps 1. Attach a date and source to every rate A rate without a date is not information. Files: `src/rates.ts` 2. Expire and mark visibly In the document, not only in the editor. Files: `src/rates.ts` 3. Require acknowledgement to publish stale Possible, deliberate and recorded. Files: `src/publish.ts` ### Done when - [ ] Every rate has a date, source and expiry - [ ] Expired scenarios are marked wherever shown - [ ] Publishing expired requires acknowledgement - [ ] A reopened comparison shows its age ### Watch out - A saved comparison reopened three weeks later shows rates that are no longer available as though they were, and it is the most likely way this tool misleads someone without anyone intending to. Attach a date to every rate, expire it, and make a stale comparison visibly stale rather than quietly wrong. ## M5 · A document that is honest about what it is (production only) Nobody could mistake the output for a regulated disclosure. ### Steps 1. State the distinction on the first page Not in a footer. A borrower has to be able to tell. Files: `src/render.ts` 2. Print every assumption On the document. Hidden assumptions make it a sales tool. Files: `src/render.ts` 3. Make rendering reproducible Same inputs, identical document, verified by checksum. Files: `src/render.ts`, `tests/reproducible.test.ts` ### Done when - [ ] The first page states what the document is and is not - [ ] It is visually distinct from the prescribed forms - [ ] Every assumption appears on the document - [ ] Re-rendering produces an identical file ### Watch out - Borrowing the layout or the language of the official disclosure makes the document feel authoritative in exactly the way it should not, and a borrower will reasonably treat it as the real thing. Make it look like what it is, state the distinction on the first page, and include the schedules so the figures can actually be verified.
# Operations · Mortgage Coach ## Backup Comparisons are documents shown to borrowers and may be asked about later. Retain the inputs and the rendered outputs, since being able to reproduce exactly what someone was shown is the point of storing them at all. ## Restore Restore and confirm outputs re-render identically from stored inputs, since a restore that changes a figure means you can no longer say what a borrower was shown. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring The proportion of published comparisons built on expired rates is the number worth watching, because it grows quietly and every one of them is a document presenting stale terms as current. ## Incident checklist A figure that does not reconcile to its schedule is the failure that matters, since every displayed number is supposed to be traceable. The schedules are retained precisely so this is answerable rather than arguable. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A thirty year schedule is confirmed to match published figures to the cent - [ ] The adjustable rate worst case is confirmed not suppressible - [ ] Differing assumptions are confirmed displayed prominently - [ ] Expired rates are confirmed marked wherever shown - [ ] The output is confirmed visually distinct from the prescribed forms ## Launch constraint Do not market omitted Mortgage Coach capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Borrower scenarios and figures. Financial data about identified people. DATABASE_URL=postgres://localhost:5432/comparisons # Required. Stated on every output, because a borrower comparing against a bank needs to know. ROUNDING_POLICY=half_up_cent # Required. How long a quoted rate is presented as current. Short, because rates move. RATE_EXPIRY_DAYS=7 # Required. Whether the regulated term may be used. False unless the regulation's own calculation is implemented exactly. APR_TERM_ALLOWED=false # Required. Recorded per comparison, so you know what a past document said. DISCLAIMER_VERSION=2026-09 # Required. The default comparison horizon. Same across scenarios or the comparison is meaningless. COMMON_HORIZON_MONTHS=120
$ choose a build depth, inspect the files, then open the complete pack in your agent
Loan officers do not buy this to do arithmetic, they buy it because it turns a rate quote into a story a borrower can act on, and because the output is something their compliance department has already approved for client delivery. The tool also sits inside the workflow: scenarios flow in from the origination and pricing systems, presentations go out through the CRM, and engagement comes back as a lead signal. A self-built comparator gives you the numbers but none of that plumbing, and in a regulated sales process the plumbing is the product.
xIntegrations with loan origination systems, pricing engines and CRMs, so every scenario is typed in by hand
xCompliance-vetted presentation output and audit trail, which is the entire reason a regulated lender licenses this instead of using a spreadsheet
xBorrower engagement tracking: who opened the presentation, how long they looked, which option they clicked
xPolished branded video and mobile presentation formats that agents and borrowers are used to receiving
xSomeone else maintaining the edge cases: ARMs, buydowns, MI structures, state-specific cost lines
Nothing worth pointing at. That's why the prompt exists.
Vibecode Mortgage Coach
Kinda. The core of Mortgage Coach is buildable in a weekend with the prompt on this page, but there are real gaps: Integrations with loan origination systems, pricing engines and CRMs, so every scenario is typed in by hand, Compliance-vetted presentation output and audit trail, which is the entire reason a regulated lender licenses this instead of using a spreadsheet. Read the honest list above before committing.
How much does Mortgage Coach cost?
Mortgage Coach's pricing is usage-based or varies by plan · No pricing page exists (/pricing returns 404); site funnels to 'Reach out to see MortgageCoach in action'..
What do I lose by replacing Mortgage Coach?
Honestly: Integrations with loan origination systems, pricing engines and CRMs, so every scenario is typed in by hand; Compliance-vetted presentation output and audit trail, which is the entire reason a regulated lender licenses this instead of using a spreadsheet; Borrower engagement tracking: who opened the presentation, how long they looked, which option they clicked; Polished branded video and mobile presentation formats that agents and borrowers are used to receiving; Someone else maintaining the edge cases: ARMs, buydowns, MI structures, state-specific cost lines. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Mortgage Coach?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.