Personal project · 2025–2026

The BRF
Analyser.

Services like AllaBRF charge for basic financial data that should be free. I wanted a tool that gives you more information, not less – and costs nothing. The result reads a 40-page annual report PDF and returns a colour-coded financial health score in 20 seconds, no backend, no sign-up, no data leaving your browser.

Built by
Matthew MacLaine
Senior IT Engineer · solo build
Codebase
3,064 lines
one HTML file · zero deps
AI providers
3
Gemini · Claude · GPT-4o
Languages
SV / EN
120+ translated strings
Executive summary

A client-side web tool that takes a BRF annual report PDF, extracts text in the browser via PDF.js, sends it to one of three AI providers (Google Gemini free by default), and renders a full interactive financial dashboard – score ring, colour-coded metrics, loan portfolio, trend charts, fee calculator, and a grouped assessment. Everything runs in the browser. The PDF never leaves the user's device.

Built as a personal project by someone who is not a developer by trade – a Senior IT Engineer who wanted a tool that actually existed for apartment buyers in Sweden. Fully bilingual (Swedish/English), with analysis history, side-by-side BRF comparison, print-optimised export, and four structured data schemas for SEO. Live at maclaine.se/brf.

Why BRF Annual Reports Are Unreadable

In Sweden, buying an apartment means buying into a housing association. The association's annual report is the single most important document for assessing financial health. It is also, reliably, the document nobody reads.

A BRF annual report contains everything you need to know about your potential home's financial stability: total debt, savings rate, loan maturities, maintenance fund balance, energy costs, and the board's commentary on what's coming next. The problem is that it presents this information in 30–40 pages of Swedish accounting language, formatted for auditors rather than buyers.

The key number – skuld per kvadratmeter (debt per square metre) – tells you more about a BRF's future than the asking price of the apartment. Under 5,000 kr/sqm is healthy. Over 10,000 kr/sqm means likely fee increases ahead. But you have to calculate it yourself from the balance sheet and the total area, which are on different pages, in different formats.

Services like AllaBRF exist, but they charge for data that should be free – and still only give you a fraction of what the report contains. I wanted something that goes deeper, covers more metrics, and costs nothing. That tool did not exist. So I built it.

Debt / sqm – Total loans divided by living area. The #1 health metric.Skuld per kvm
Savings / sqm – Annual contribution to the maintenance fund, per sqm.Sparande per kvm
Equity ratio – How much of total assets are self-financed.Soliditet
Monthly fee – What you pay each month to the association.Årsavgift
Rate sensitivity – How fees change when interest rates rise.Räntekänslighet
Maintenance fund – Savings pool for future building repairs.Underhållsfond
Genuine BRF – 60%+ income from members. Affects tax treatment.Äkta förening
Pipe replacement – The most expensive renovation a BRF faces.Stambyte
0lines
in a single HTML file – UI, AI, rendering, i18n
0data fields
extracted by AI from each annual report
0backends
the PDF never leaves the browser

One HTML File, Zero Build Steps

The entire analyser – UI, PDF extraction, AI integration, report rendering, localisation, history management, and print styles – lives in a single 3,064-line HTML file. No npm, no bundler, no framework. Edit a file, push, deployed.

The architecture is deliberately boring. brf.html loads three CDN scripts (PDF.js for text extraction, Chart.js for trend lines, anime.js for scroll animations) and talks directly to AI APIs from the browser. Google Gemini and OpenAI support CORS, so those calls go straight from the client. Anthropic does not, so Claude routes through a 269-line Cloudflare Pages Function (functions/api/analyze.js) that proxies the request.

The single-file approach was a pragmatic choice. I'm an IT engineer, not a frontend developer. I did not want to debug webpack configs or manage a package.json. I wanted to write HTML, push to Cloudflare Pages, and have it work. The tradeoff is that all 3,064 lines live in one file – but for a personal project with one maintainer, that's a feature.

BROWSER PDF Upload PDF.js text extraction Gemini Claude GPT-4o JSON structured report

Teaching AI to Be a Financial Analyst

The core of the tool is not the UI – it is a structured system prompt that turns a raw annual report into JSON covering 16 data categories, benchmarked against industry standards from HSB, SBAB, and SEB.

The prompt defines a strict JSON schema with 16 top-level fields. The AI fills in every field from the report text: key metrics with RAG badges, multi-year trend data, income/cost breakdowns, individual loans with maturity risk flags, rate sensitivity projections, energy costs, maintenance plan status, a building profile, spotlight cards for unique traits, and a scored assessment.

Getting this right took the most iteration. Early versions produced inconsistent field names, hallucinated numbers, and mixed Swedish and English labels. The fix was explicit constraints: "All monetary values should be in SEK (kr)", "Use 'million kr' for large amounts", "Translate Swedish labels to English in the output". The scoring thresholds are baked into the prompt – debt/sqm under 5,000 is green, 5,000–10,000 is amber, over 10,000 is red – so the AI applies them consistently.

Gemini's responseMimeType: 'application/json' forces structured output and catches most schema violations. Claude and GPT-4o rely on prompt discipline alone, which works but occasionally returns markdown-wrapped JSON that needs stripping.

analyze.js · system prompt schema (excerpt)
{
  "name": "string – association name",
  "year": number,
  "executiveSummary": "2-3 sentence verdict",
  "keyMetrics": [{
    "label": "string",
    "value": "string with unit",
    "numericValue": number,
    "rangeMax": number,
    "nationalAvg": number|null,
    "badge": "green|amber|red"
  }],
  "trend": { "years": [], "debtPerSqm": [], "savingsPerSqm": [] },
  "loans": [{ "rate": "string", "expires": "date", "badge": "green|amber|red" }],
  "feeCalculator": { "currentFeePerSqm": number, "debtPerSqmTotal": number },
  "buildingProfile": { "constructionYear": number, "taxStatus": "äkta|oäkta" },
  "score": { "value": 0100, "grade": "A–F", "verdict": "string" }
  // + income, costs, rateSensitivity, costComparison,
  //   energy, maintenancePlan, spotlights, assessment
}
analyze.js · badge thresholds
// Debt per sqm
< 5,000 kr/sqm  →  GREEN   // healthy
5,00010,000AMBER   // average, watch for trends
> 10,000 kr/sqm →  RED     // high risk of fee increases

// Savings per sqm (annual contribution to maintenance fund)
> 300 kr/sqm    →  GREEN   // room for major renovations
200300AMBER   // covers routine maintenance
< 200 kr/sqm    →  RED     // underfunded, risk of special levies

// Equity ratio (soliditet)
> 50%           →  GREEN   // strong balance sheet
3050%         →  AMBER   // acceptable
< 30%           →  RED     // leveraged, vulnerable to shocks

// Loan maturity flags
Expiring > 24 months  →  GREEN
Expiring 1224 months →  AMBER
Expiring < 12 months  →  RED

Three AI Providers, One Interface

Google Gemini works out of the box with a built-in API key. Claude routes through a Cloudflare Pages Function. OpenAI calls directly from the browser. Users can bring their own key for any provider.

Google Gemini

Gemini 2.5 Flash

CostFree (built-in key)
CORSYes – direct from browser
JSON modeNative (responseMimeType)
Max output32,000 tokens
AccuracyGood – occasional hallucinations
Anthropic Claude

Claude Sonnet 4

CostBYOK (user's API key)
CORSNo – via CF Pages Function
JSON modePrompt-enforced
Max output8,000 tokens
AccuracyBest – most reliable numbers
OpenAI

GPT-4o

CostBYOK (user's API key)
CORSYes – direct from browser
JSON modePrompt-enforced
Max output8,000 tokens
AccuracySolid – reliable structured output

The Cloudflare Pages Function (functions/api/analyze.js, 269 lines) handles all three providers. For Google and OpenAI, it passes through the user's key. For Claude, it is the only path – Anthropic's API does not set CORS headers, so browser-direct calls fail. The function also truncates input to 80,000 characters and serves as the shared system prompt source.

The BYOK model was an intentional design choice. Gemini's free tier handles the default case. Users who want Claude's accuracy or GPT-4o's reliability can supply their own key – stored in localStorage only, never sent to any server I control.

From JSON to Financial Dashboard

The AI returns a JSON blob. A single renderReport() function turns it into an interactive dashboard with an animated score ring, colour-coded metric cards, context bars with national average markers, trend charts, and a grouped assessment.

Every JSON field maps to a visual component. The score ring animates from 0 to the final value using SVG stroke-dashoffset. Metric cards show the raw value, a RAG badge, a context bar (how the value sits within the benchmark range), and a national average marker. Spotlight cards highlight unique traits – parking income, solar panels, commercial tenants – that a standard metric grid would miss.

Below is a live rendering of the example report data (BRF Solbacken). The score ring, metrics, and context bars all use the same code and styling as the real tool.

B+
0
Debt per sqm
4,800 kr
Below national average – low debt
Healthy
Savings per sqm
265 kr/yr
Covers routine maintenance
Moderate
Equity ratio
62%
Strong balance sheet
Strong

What Does a Rate Rise Actually Cost You?

The most-used feature in the tool. Pick an apartment size, see your monthly fee, your share of the association's debt, and what happens if interest rates rise by 1–3%. Try it below with real data from BRF Solbacken.

Fee impact calculator – BRF Solbacken
Monthly fee
5,320 kr
76 kr/sqm
Your debt share
336,000 kr
4,800 kr/sqm
At +1% rate
+280 kr/mo
5,600 kr total
At +2% rate
+560 kr/mo
5,880 kr total

i18n, Print Styles, and Structured Data

The features nobody asks for but everyone notices: full Swedish/English localisation, an A4-optimised print stylesheet that converts dark mode to clean white, four JSON-LD schemas, a bilingual FAQ, and analysis history with side-by-side comparison.

Every visible string in the tool lives in a LANG object with en and sv keys – 120+ translated strings including all 10 FAQ items, 11 feature descriptions, and every report label. A one-line helper t('key') returns the active translation. The browser's language preference sets the default; a toggle switch persists the choice to localStorage (shared with the Finance page).

The print stylesheet inverts the entire colour scheme – dark background to white, light text to dark, accent gold to a printable brown – and hides all interactive elements (upload zone, settings, history, fee calculator buttons). The result is a clean A4 report you can hand to a mortgage adviser.

Analysis history saves every report to localStorage with the BRF name, year, timestamp, provider, and the full JSON data blob. The comparison view aligns two reports side-by-side, highlighting the better value in each metric row. It is the quickest way to compare two BRFs you are considering.

Four JSON-LD schemas (WebApplication, FAQPage with 10 questions, BreadcrumbList, HowTo with 4 steps) give Google structured data for rich results. The FAQ schema alone surfaces answers directly in search for queries like "vad är skuld per kvm" (what is debt per sqm).

A tool I wished existed.

I built the BRF Analyser because I was looking at apartments in Stockholm and could not find a tool that parsed annual reports automatically. The ones that existed required manual data entry or a paid subscription. I wanted to drop a PDF and get a verdict.

What surprised me was how well structured prompting works when the output schema is strict enough. Gemini, Claude, and GPT-4o all produce usable JSON from a 40-page Swedish accounting document – as long as you tell them exactly what shape the answer should take. The prompt is the product.

The single-file architecture was the right call for a solo project. No build step means I can fix a typo from my phone. No framework means no framework upgrades. The 3,064 lines are long, but they are all mine to read. For a personal tool maintained by one person, simplicity is a feature.

The tool is live at maclaine.se/brf. It will keep improving – OCR for scanned PDFs, more benchmark data, maybe a community comparison database. But the core promise is already working: drop a PDF, get a verdict. Twenty seconds.

Project · BRF Analyser
Live · maclaine.se/brf
Written · June 2026
Back to blog