Photograph a confusing letter. Get back what it actually says, in plain words.
ContractPal reads insurance policies, bills, contracts and renewal notices, and answers three questions: what is this, what do I need to do, and what's the fine print that could cost me. It is built for people who find paperwork stressful rather than interesting — often older, often on a phone, and not interested in learning an app.
Nothing you send is stored. That is enforced in code, not promised in a policy — see Privacy below.
Two screens. That is the whole product.
| Upload Hub | One large button. Take a photo or pick a file. |
| Breakdown | Charlie the owl, and three sections: The Big Picture, What You Actually Need to Do, The Hidden Gotchas. Dates and amounts are emphasised, because those are what people scan for. |
There are no menus, no dashboard, and no onboarding you have to sit through. A guided tour exists, but you have to ask for it.
Next.js 16 (App Router, TypeScript, Turbopack) as a single deployable. No CSS framework, no component library, no database, and no test framework — tests run on Node's built-in runner.
Document analysis goes through a provider-agnostic seam, so the AI vendor is one environment variable:
| Provider | Default model | Reads | Jurisdiction |
|---|---|---|---|
anthropic |
claude-opus-5 |
images + PDF | US |
openai |
gpt-5.6-terra |
images + PDF | US |
mistral |
mistral-medium-3-5-26-04 |
images only | EU |
openrouter |
none — you must set one | images only | varies by route |
A provider may only be added if it does not train on API data, publishes a retention window, states its jurisdiction, and offers a route to true zero retention. Each adapter carries those facts in code, and the privacy page is written from them.
OpenRouter is the one gateway, and it is treated as the exception it is. It sends provider.zdr so a request only reaches endpoints with a zero-retention policy, and require_parameters so it only reaches endpoints that actually honour the response schema. It has no default model — with a gateway, the model slug decides who reads the document, so guessing one would be a silent privacy decision. Auto-detection will never select it; you have to ask for it with AI_PROVIDER=openrouter.
The claim is narrow and therefore keepable:
- We store nothing. Your document exists as a buffer inside one request. It is never written to disk, never placed in a database, never put in cloud storage, and never logged.
npm run audit:retentionfails the build if any code path could do so, and it is negative-tested against a deliberately violating file. - There is no database at all — not for documents, not for customers. Stripe holds subscription state; the client holds a signed token.
- No trackers. No analytics, no advertising scripts, no third-party code. The Content-Security-Policy sets
connect-src 'self', so the page physically cannot send your document to another origin. - The honest part: the AI provider that does the reading sees your document transiently. None of the supported providers train on API data, but on a standard commercial tier they may retain it briefly for abuse monitoring. The privacy page says exactly that rather than implying an absolute. Signing a zero-retention agreement with a provider is what would change it.
npm install
cp .env.example .env.local # then fill in what you need
npm run dev # http://localhost:3000You need one AI provider key to get a real breakdown. Without one the app runs and reports a clear misconfiguration rather than failing obscurely.
| Command | What it guards |
|---|---|
npm run build / npm run lint |
compiles and lints clean |
npm test |
54 assertions on Node's runner — upload validation, rate limiting, token auth, jargon, provider capability |
npm run smoke |
every screen and every /api/analyze path, against a running server |
npm run check:contrast |
WCAG AA ratios and a hard 17px minimum type size |
npm run audit:retention |
no persistence path, no document content in logs |
npm run check:legal |
fails while the legal copy still holds placeholders |
The audience has ageing eyes, so this is treated as correctness rather than compliance:
- Body text is 19px and scales with the browser's font setting; nothing anywhere is below 17px, enforced mechanically.
- Every colour pair clears WCAG AA for normal text (4.5:1) even where the type is large enough to qualify for the easier 3:1 threshold. The lowest pair in the palette is 6.79:1.
- axe-core reports zero violations and zero incomplete results across all four pages.
- Emphasis is weight, never colour alone.
The repository documents itself. Start with CLAUDE.md — the operating contract — then:
docs/okf/— a knowledge graph of every service, module, endpoint and flow, linted for staleness against the code it describes.docs/okf/decisions/— nine ADRs covering every load-bearing choice, including the ones deliberately not taken.docs/phases/— the roadmap, with evidence required against each acceptance criterion.
Working and verified: both screens, the analysis pipeline with three providers, the free-scan gate and signed entitlement tokens, Stripe checkout, recovery links, legal pages, security headers.
Not yet exercised against live third-party APIs — the provider and Stripe calls are written and typed but have never run against a real key. See docs/context/state.md for exactly what is verified and what is not.
The owl is a placeholder, deliberately simple, and nothing references its shape. Replace public/charlie.svg and app/icon.svg to change the mascot everywhere.
In plain words, since that is this project's whole idea: use it, change it, run it. But if you run a modified version as a service that other people use, you have to offer them your changes under the same licence. That is AGPL section 13, and it is the reason this licence was chosen over MIT — the hosted product is the business, so improvements to a hosted fork should come back.
If you need different terms, ask.