FightThis

Case studyAI document-analysis prototypeProject 06

Tested prototype · not launched · AI, payment and email providers never exercised live · private repository

Screenshot · synthetic data FightThis — screenshot of an analysis, mock AI output on a synthetic bill

Summary

FightThis is a tested prototype that turns a confusing medical bill or insurance denial into a plain-language explanation, a dispute-strength estimate, a short plan and draft letters. What it demonstrates is guardrail design around a model: validate what it returns, check what it says, fail into a saved case marked for review, and never let a payment or a document depend on trusting the browser. Those safeguards are tested with a mock model and mocked providers; they have not been proven against a live model, real payments or real email, so this is not a safe-to-ship claim.

The problem and who it serves

Medical bills and insurance denials are hard to read, and people often pay or give up without checking them. FightThis explains a bill or denial in plain language, estimates how strong a dispute is and drafts the letters, while refusing to act like a lawyer or promise an outcome.

People facing a medical bill or a health-insurance denial they do not understand. The product scope is deliberately narrow (medical bills and insurance denials) although the data model allows other dispute types. It has no hosted instance and no users.

My role

Solo. I set the product scope, the safety rules (no legal or medical advice, no promised outcomes, no invented laws or deadlines, a human-review path), the architecture and the milestone plan. Implementation was AI-assisted under my direction over 2026-06-11 to 2026-06-13 and committed as one initial commit, so git does not show the individual steps. In September 2026 I added CI after finding that a clean clone no longer installed.

Architecture

The diagram is drawn from the code. Each external dependency sits behind an interface with a real implementation and a local one, and a health endpoint reports which mode each subsystem is in. In production and staging the app refuses to start with any mock subsystem or a weak secret.

Screenshot of the documents panel in FightThis: an analysis-ready notice with three purchase options, an empty documents list, and cards to generate an appeal letter, itemized bill request, financial assistance request, complaint letter, follow-up email, phone call script, evidence checklist and case summary.
Screenshot: the document panel after analysis, before purchase. Local development server, 2026-10-24, synthetic bill; the purchase buttons reach Stripe only when it is configured, and it was not.
Phone-width screenshot of a FightThis case page titled ER visit bill (synthetic sample), showing the upload area and the uploaded file marked completed.
Screenshot: a case at phone width (Pixel 7 viewport), same synthetic case.

Decisions and trade-offs

  • Check meaning, not just shape. Schema validation guarantees the model returned the right fields. A second pass checks what those fields say: it rejects outcome promises and lawyer language, requires a real statement of uncertainty, and refuses a high dispute-strength rating that cites no source. Anything that fails goes to human review instead of the user. Phrase lists are blunt and will miss paraphrases, so they are a floor with a review path behind them, not a guarantee.
  • Fail into a saved case, never a partial answer. A timeout, invalid output or provider error marks the analysis failed, keeps the case and its documents, sets it to “action needed” and writes a timeline entry. Five database-integration tests force each failure mode and check the stored state.
  • Let only the webhook grant a purchase. The browser’s return from checkout is never trusted. Entitlements are created by the Stripe webhook alone, a repeated event ID is skipped, and refunds revoke access. The cost is a short delay between paying and unlocking. Database tests cover this with a stubbed Stripe client; it has not run against Stripe itself.
  • Make every dependency optional in development and mandatory in production. Without keys the app uses a deterministic AI mock, local text extraction, inline jobs, disk storage and a file outbox, so it can be built and tested offline. Production configuration refuses to boot in any of those modes. The trade-off is two code paths per subsystem, and the real paths are the less exercised ones.

Verified results

MeasureResultEvidence
Tests in CI124 passed in 29 filesCI run 34171715157, main 5920540, 2026-09-07, with a PostgreSQL 16 service
What those tests are72 unit + 52 database-integration tests (12 files)Rerun on a fresh clone against PostgreSQL 17, 2026-10-24: the same 124 pass. The AI provider, Stripe client and email are mocked; the database is real
Clean installnpm ci failed on a clean clone → passes, and is now the first CI stepCommit a76be80 (2026-09-07): a lockfile drift in optional native packages
Local walk-throughsign-up → email verification → consent → case → upload → text extraction → analysis → documents panel2026-10-24, synthetic bill, every subsystem in local or mock mode as reported by the health endpoint; screenshots on this page
Live integrations0 of 7 credentialed checks executedThe project’s launch-readiness document lists AI, image OCR, Stripe, email, S3, Redis and Sentry as not yet run

Limitations and remaining work

  • Not launched and not deployed; no users. A staging package (container, Railway config, runbook) is prepared but has never been run.
  • No real AI model has analysed a document. The analysis on this page and in the tests comes from the deterministic mock provider.
  • Payments, email delivery, S3 storage, the Redis queue and image OCR have only been exercised through mocks or local modes. Client-side error monitoring is not wired.
  • The guardrails are rule- and phrase-based. They reduce risk; they do not make the output correct, and the product is not legal or medical advice. It is privacy-aware but not HIPAA-compliant.
  • Losing the data-encryption key makes stored document text unrecoverable; key rotation needs a re-encryption plan that does not exist yet.
  • Development history is a single initial commit, so the milestone-by-milestone work is recorded in project notes and documents, not in git.