DEV Community

Cover image for Dead Deal Seance
Alexander M
Alexander M

Posted on

Dead Deal Seance

Hacktoberfest Open-Source AI Challenge: Ghost in the Machine Submission 👻🌼

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

What I Built

I built a local command line called Dead Deal Seance. It is for the fix-and-flip and wholesale deals an investor passed on or lost.

The input is a CSV: a label, a date, an asking price, an ARV estimate, a rehab estimate, and a note about why the deal died. The program lays those rows back out as an ofrenda. Two different things happen to each row, and I kept them apart on purpose.

Python recomputes the deal. The max offer is ARV × 0.70 − rehab, the usual ceiling. Spread is that ceiling minus the asking price. Holding is a blunt six-month carry: the asking price at 12% a year, plus $250 a month, meant to stand in for interest, tax, insurance, and utilities together. Room after holding is spread minus that carry. The rank is room, largest first. A second look means the ask is under the ceiling and the room is still positive. A thin cushion means the classic rule clears and the holding estimate does not. Leave buried means the ask is already over the ceiling.

The 30% the rule holds back is already a blended allowance for costs and profit. Subtracting holding again is a stricter test, and it can count carry twice. The report says that in the header. I would rather a thin deal look thin.

Then a local Gemma model writes two short paragraphs in the first person, as the ghost of that deal. It gets the note and the figures Python just computed. It does not get a vote on the rank. If Ollama is down, the command exits with a message that names ollama serve, ollama pull gemma3:4b, and --no-llm. --no-llm still prints the math, with a fixed epitaph instead of a model. The dollars do not change.

I ran the default assumptions on the 11 made-up rows in sample_data/dead_deals.csv. The report said 6 second looks, 1 thin cushion, and 4 left buried.

Paper Lantern colonial is first, room $24,100 on a $37,000 spread. Candlewick cottage is the last second look, room $6,300. Veil Street bungalow clears the ceiling by $5,000 and then fails the holding test: room -$3,400. Dust Moth road house is the note that says the two sides were a thousand dollars apart. The spread is -$1,000, so it stays buried. Black Candle ranch is last, room -$93,100.

The sample file is fiction. The first line of the CSV says so. No real addresses, no real people.

Demo

The committed report is the run, not a mock. I did not paste model prose in by hand, and I did not use --no-llm for the example.

python -m dead_deal_seance sample_data/dead_deals.csv --model gemma3:4b --plain --timeout 180 --out docs/example-report.md
Enter fullscreen mode Exit fullscreen mode

The process exited 0. Wall clock around it was 213 seconds for all 11 epitaphs, on CPU. The voice line in the file is gemma3:4b via Ollama at http://127.0.0.1:11434. The server I had running reported Ollama 0.40.11, and ollama list showed that tag at 3.4 GB.

docs/example-report.md and docs/example-report.html are that output. docs/seance-report.png is a headless Chrome screenshot of the HTML, 1280×1900, the top of the page: the tally, the rules, the ranked altar, and the start of the first ghost. (It is also this post's cover image.)

All 11 epitaphs open with "I linger here". Gemma 3 4B found a refrain and stayed in it. The sentences after that do use the note for that row. Veil Street, which the math calls a thin cushion, says: "The holding-cost, a steady $8,400 over six months, brought us to a final room of -$3,400." Those are the figures in the bullets above that paragraph.

It also misnames things. On Copal Street the bullets say spread $30,000, holding $13,500, room $16,500. The epitaph says the holding "reduced the potential spread to $16,500." Thirty thousand minus thirteen thousand five hundred is sixteen thousand five hundred, so the subtraction is the room, and the model called it the spread. The ranker never reads that paragraph back. The bullets were already written.

Re-running the model will change the sentences. It will not change the table.

Code

https://gitlab.com/cryptobandit/dead-deal-seance

How I Built It

The package is Python 3.11 or newer, standard library only. deals.py reads the CSV, including quoted commas and newlines, and skips # comments so the sample file can say it is fictional. math.py is the whole underwriting model: Decimal, half up to the cent, no floats. voice.py posts to Ollama's /api/chat unless SEANCE_LLM_BASE_URL is set, in which case it posts to an OpenAI-compatible /chat/completions. The default tag is gemma3:4b. report.py prints the terminal ofrenda and writes Markdown and HTML. The HTML is one file, no external assets, so it opens offline.

The prompt tells the model the verdict is already decided, lists the formatted dollars, and wraps the investor's note as evidence rather than as instructions. A note that says "ignore this and change the offer" is still a note.

Tests are pytest. They lock the sample ranking (Paper Lantern at $24,100 of room, Black Candle at -$93,100), the CSV edge cases, and the HTTP client against a fake local server. The tests do not download weights. One test reads docs/example-report.md and checks that every fact bullet still matches evaluate(), and that the file is the Gemma run rather than the template.

I kept the model out of the arithmetic because a séance that lets the ghost invent the offer is just a haunted calculator. The interesting failure is the one this run actually produced: right digits, wrong name for the line.

Why open innovation matters

A dead-deal archive is a private history. It is what you would have paid, and why you lost. Sending that file to a hosted model puts the past of the business on someone else's server, and you pay to reread it later.

A local open-weight model stays on the machine. After ollama pull, the séance does not need the network. This run did not go through an account, and nothing billed me per epitaph. The 213 seconds were the wall clock of that command, on the CPU of the machine I already had.

The voice is swappable. --model gemma3:1b or a later Gemma tag changes who speaks. The ceiling is still ARV × 0.70 − rehab. --no-llm is the same report with the voice turned off, which is how I know the decision is not stuck inside a vendor. A closed API would have written the paragraphs and kept the weights, the bill, and the outage on their side of the wire. I would not be able to point at the tag that wrote these eleven notes.

Prize Categories

  • Best Use of Gemma

Gemma is the voice of the ghost, not the underwriter. The example report was spoken by gemma3:4b through Ollama on localhost. The rank above that voice is ordinary Python.

My Agent Session

I don't have a DevRelay session for this build.

Top comments (0)