This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

In 2020 and 2021 a lot of Canadians locked in five-year fixed mortgage rates at historic lows. Those terms are ending now. The Bank of Canada estimated that people with a five-year fixed mortgage renewing in 2026 would see their payment go up about 20% on average.

That rate was a quiet thing. It came out of your account every month and you stopped noticing it. Then the term ends and it's gone.

So for Halloween I let it come back as a ghost.

Ghost of Rates Past asks for four things: when you signed, how much you borrowed, the rate you got, and the renewal rate you've been offered. It works out what happens to your payment, shows you the Bank of Canada's posted rate the week you signed next to the one today, and then your old rate writes you a short letter:

I've lingered here, a steady hum within your walls for a long time now. I was the small, quiet payment that flowed monthly, a constant reassurance. I've faded, haven't I?

I was born in 2021 at 1.89%. While I lived, your payment was $2,091 a month. At renewal in 2026, the rate waiting for you is 4.39%. Your payment becomes $2,610 a month: $519 more, or $6,231 more a year.

Do not wait for the letter in the mail. Start comparing lenders early, and read the renewal offer closely.

— your old rate

(A real letter from a test run. Example numbers: $500,000 borrowed, 25-year amortization, 1.89% in March 2021, renewing at 4.39%.)

The spooky part is written by an open-weight model running in your own browser. The numbers are not.

Demo

Live: https://alenzacodes.github.io/ghost-of-rates-past/

Open it, press Summon. The first time, your browser downloads the model once (about 760 MB with WebGPU) and keeps it. If your browser can't run it, untick the model box and you'll get the plain letter with the same numbers.

Ghost of Rates Past with the model box unticked: the numbers table and the plain letter

Code

GitHub logo ALENZACODES / ghost-of-rates-past

Your old mortgage rate comes back as a ghost to warn you before renewal. Runs Gemma 3 1B in your browser; numbers from plain, tested JavaScript.

👻 Ghost of Rates Past

Your old mortgage rate comes back to warn you before renewal.

Many Canadians locked in a low 5-year fixed rate in 2020 or 2021 and are renewing now. Type in the rate you signed and the renewal rate you've been offered. The page works out what happens to your payment, and the ghost of your old rate writes you a short letter about it.

The letter's spooky opening is written by Gemma 3 1B, an open-weight model, running in your browser with Transformers.js. Your numbers never leave your device. There is no server and no API key.

Made in Toronto, Ontario for the DEV Hacktoberfest Open-Source AI Challenge, Week 4: Ghost in the Machine. Started Oct 30, 2026.

The one rule: the ghost never touches the numbers

Small models are bad at numbers. When I gave the model the figures and asked…

It's a static site: index.html, about 240 lines of JavaScript, a small JSON file of Bank of Canada rate data, and tests. No build step, no server, no API keys.

How I Built It

The model: Gemma 3 1B instruct, using the ONNX build from onnx-community.

The runtime: Transformers.js loads it straight from a CDN and runs it on WebGPU when the browser has it, and on the CPU (WASM) when it doesn't.

const adapter = navigator.gpu ? await navigator.gpu.requestAdapter().catch(() => null) : null; const device = adapter ? 'webgpu' : 'wasm'; generator = await pipeline('text-generation', 'onnx-community/gemma-3-1b-it-ONNX', { dtype: device === 'webgpu' ? 'q4f16' : 'q4', device, }); 

The math: plain JavaScript in calc.js. Fixed mortgage rates in Canada are compounded semi-annually, so the monthly rate is (1 + r/2)^(1/6) - 1, not r/12. A test checks it against the textbook case: $100,000 at 5% over 25 years is $581.60 a month.

The part I learned the most from: the ghost isn't allowed to touch the numbers.

My first version gave Gemma all the figures and asked it to write the whole letter. I ran it a bunch of times on my own machine. With a list of facts in the prompt, it just copied the list back to me (10 out of 10 times). When I asked it to rewrite a plain letter in a ghost's voice instead, the writing got better, but none of 6 tries kept every number right. It left out the $519 increase, which is the whole point. One letter said "$2,610 a year" when it's a month. In my very first test it turned 1.89% into 2.09%.

For a tool about someone's mortgage, a confident wrong number is the scariest thing on the page. So I split the job:

Part of the letter Written by
Payments, balance, difference calc.js, unit tested
The eerie opening Gemma 3 1B, in your browser
The practical nudge fixed text

The model is never shown a single number, and guard.js checks what comes back:

export function check(text) { const numbers = findNumbers(text); // any digit, or words like "twenty", "half", "percent" const tooLong = text.split(/[.!?]+\s/).length > 5; const empty = text.trim().length < 40; const placeholder = /[\[\]{}<>]|---/.test(text); // "[Recipient's Name]" happened once return { pass: numbers.length === 0 && !tooLong && !empty && !placeholder, numbers, tooLong, empty, placeholder }; } 

If an attempt fails, it's thrown out and the ghost tries again, up to three times, and then the page falls back to a plain opening. Small models also stop mid-sentence when they hit the token limit, so tidy() keeps whole sentences only, three at most, and drops "Dearest you," style greeting lines. With the final prompt, 10 out of 10 test runs on my own machine passed the guard on the first try. An earlier prompt passed 8 of 10, and the two misses ran long, so the guard and retry still earn their place.

The result: the model does what it's good at (mood), and plain code does what it's good at (arithmetic).

Why Does Open Innovation Matter?

Because of what people type into it. Your mortgage balance, your rate and the date you bought your home are private. With an open-weight model running in the browser, none of it goes to anyone's server. Mine included. There is no backend to leak and no log to keep.

It also costs nothing to run. A closed API would mean a key, a bill and rate limits for a free Halloween tool. Here the cost is one download on the visitor's side, and after that the model is cached and runs on their own device.

And it will still work later. Free tools tend to disappear when the service behind them shuts down or changes its pricing. This one is a folder of static files plus a model anyone can download. If a host disappears, you can run it from your own laptop with python3 -m http.server.

Open weights also made the guard possible to test properly. I could run the exact same model on my own machine, as many times as I wanted, to see how it fails before shipping.

The Spirit of It

Día de los Muertos is about remembering what's gone without pretending it isn't. That felt like the right frame for a renewal. The low rate isn't coming back. The ghost isn't there to scare you; it's there so you see the new number months early, while you still have time to shop around, instead of on the day the renewal letter shows up.

Prize Categories

  • Best Use of Gemma

Built in Toronto, Ontario. Thanks for reading. If you renewed recently, I'd like to hear whether the letter matched what you saw.