Got an ER bill you can't pay? Recount reads a photo of a California hospital bill, checks every line against the hospital's own published prices, applies California's charity care law to your household, flags where the hospital's own policy conflicts with state law, and drafts the letters to ask for a discount. You approve. Nothing is sent for you.
Track: Apify · Best Actor
![]() |
![]() |
Hackathon: Cal Hacks 13.0 (San Francisco, Oct 23 to 25, 2026)
Tracks: Microsoft Built for the Real World · Anthropic Claude for Public Benefit · Best Use of Apify · Best Actor (Apify) · Best Use of Convex · Best People-First Hack
Team: Volodymyr Borysenko
Live demo: recount-nu.vercel.app (open it on a phone, tap Try a sample bill)
Demo video: youtu.be/24TXyeufQPs
|
Android: scan to download the app. iPhone: open the website and add it to the Home Screen. |
| Page | What you'll find |
|---|---|
| Start | Take a photo of your bill, or try the sample bill. English and Spanish |
| Your bill | One step per screen: what we found, three questions, your answer, your letters, how to send them |
| My bills | Every bill you checked, saved on this phone. No account needed |
| Account | Optional email and password, to open your bills on any device. You can delete it all |
| Privacy | What Recount keeps, and how to delete it |
| Hospital policy audit | All 470 California hospitals on a map: whose own policy conflicts with state law, with the quote |
| Apify Actor | Hospital Price & Charity Care Finder, public on the Apify Store |
| Android app | The same flow as a native app (Expo), on the same backend |
An uninsured ER visit in San Francisco can produce a five figure bill. California law already says many of these patients owe much less:
- Hospitals must offer discounted care to uninsured patients up to 400% of the federal poverty line. For one person in 2026 that is $63,840 a year.
- Since January 2025, hospitals can't count your assets, can't set a deadline to apply, can't report the debt to credit bureaus, and can't put a lien on the home of a patient who qualifies for help.
- Many hospitals give free care below their own cutoff. Zuckerberg San Francisco General's is 138% of the poverty line, about $22,000 a year for one person.
How a hospital bill is made. The patient gets care. The physician, nurse or care coordinator documents it, and it is coded (CPT, HCPCS and revenue codes). The hospital sends the claim to the insurer, or to Medicare or Medi-Cal. The payer reviews it and reimburses the hospital. The patient is billed the rest. Uninsured patients skip the payer and get the full list price.
Most patients never find out. The numbers needed to check a bill are public, but they sit in hospital price files of up to 90 MB and in 40-page policy PDFs. So people pay, or the bill goes to collections.
Hospital billing rules (✓ = Recount checks it; sources and limits in docs/REIMBURSEMENT.md):
- ✓ No Surprises Act: out-of-network emergency care can't be billed above in-network cost sharing (42 U.S.C. 300gg-131). Recount checks this from the insurance statement. The same law covers out-of-network doctors at in-network hospitals (300gg-132) and good faith estimates for uninsured patients (300gg-136). Recount doesn't check those two.
- ✓ Federal hospital price transparency rule (45 CFR 180): hospitals publish every standard charge in a machine-readable file. Recount checks each line against it.
- ✓ California Hospital Fair Pricing Act (Health and Safety Code 127400 and after): free or discounted care up to 400% of the poverty line, no asset test, no deadline to apply. Recount computes the likely answer and checks each hospital's written policy against it.
- ✓ SB 1061 (2024, Civil Code 1785.27, Health and Safety Code 127425): no medical debt on credit reports since January 2025. Recount flags hospital policies that still allow it.
- ✓ Medi-Cal patients can't be billed beyond share of cost (Welfare and Institutions Code 14019.4). Recount checks this when the patient says they have Medi-Cal.
- ✓ Medicare QMB billing ban (42 U.S.C. 1396a(n)(3)(B)): providers can't bill Qualified Medicare Beneficiaries for Medicare cost sharing. Recount checks this when the patient has Medicare and Medi-Cal.
Recount also flags a Medicare patient's bill that shows no Medicare payment (42 CFR 489.21). For uninsured patients whose income fits Medi-Cal, it points to retroactive Medi-Cal (42 CFR 435.915).
Recount does the checking from a phone in a few minutes, and every number it shows links to the document it came from.
| Area | Stack |
|---|---|
| Web app | Next.js 16 (App Router) · React 19 · TypeScript · English and Spanish strings for everything the patient reads |
| Backend | Convex: case documents, file storage for the photo, scheduled actions, a live step log the phone subscribes to, upload rate limit |
| AI | Claude (Opus 5.5) via the Anthropic API: reads the bill into a typed schema, drafts the letters, reviewed the statewide audit |
| Azure | AI Document Intelligence (second read of the charges, when AZURE_DOCINTEL_* is set) · Azure OpenAI (backup model for letters and chat) · AI Search (policy passages for Lupa) · AI Content Safety (chat) |
| Rules | Plain TypeScript: price checks, duplicate and total checks, eligibility under CA Health and Safety Code 127400 and after |
| Hospital data | Apify Actor (Python): finds and streams each hospital's CMS price file, extracts charity care limits with quotes |
| Policy audit | Python: HCAI crawl, PDF text extraction, regex candidates, two Claude reviews with a quote check |
| Eval | Python: synthetic SF ER bills priced from real price files, planted errors, rendered as scans, photos and PDFs |
| Price file audit | Rust: streams each hospital's price file and compares the cash price with the gross charge and insurer rates (findings) |
| Mobile | Expo (React Native), Android APK built with EAS |
| Hosting | Vercel (web) · Convex Cloud (backend) · Apify (Actor) |
flowchart LR
subgraph PHONE["Patient's phone"]
UP[Photo or PDF<br/>of the bill]
UI[Step-by-step screens<br/>EN / ES]
end
subgraph CVX["Convex"]
FS[File storage]
CASE[cases + steps<br/>live subscription]
ACT[Agent action<br/>readAndCheck]
end
subgraph SRC["Sources"]
CL[Claude<br/>read + letters]
AP[Apify Actor<br/>price file + policy]
SN[Saved snapshots<br/>4 SF hospitals]
AU[Statewide audit<br/>470 hospitals]
end
subgraph RULES["Rules (no model)"]
CHK[Line checks<br/>price, duplicate, total]
ELIG[Eligibility<br/>% of poverty line]
end
UP --> FS --> ACT
ACT --> CL
ACT --> AP
AP -.fallback.-> SN
ACT --> CHK
AU --> ACT
CHK --> CASE
ELIG --> CASE
CASE --> UI
UI -->|3 answers| ELIG
sequenceDiagram
participant P as Patient
participant C as Convex
participant AI as Claude
participant A as Apify Actor
P->>C: upload photo, cases.start (gets a private token)
C->>AI: read the bill into ParsedBill (schema enforced)
AI-->>C: lines, codes, amounts, total, unclear fields
C->>C: match the hospital among 470 (patient picks if unsure)
C->>A: price file rows + charity policy for these codes (~17 s)
A-->>C: gross charge, cash price, income limits, quotes
C->>C: rules: duplicates, above list price, above cash price, total mismatch
C-->>P: findings, one per screen, each with its source
P->>C: household size, income, insured?
C->>C: rules: % of poverty line vs hospital cutoff and 400% floor
C->>AI: draft letters from the computed facts only
AI-->>C: assistance request + billing dispute
P->>C: edit, fill in details, approve
C-->>P: how to send it (Recount sends nothing)
flowchart TD
H[HCAI Fair Pricing<br/>Policy Lookup] --> CR[crawl.py<br/>470 hospitals]
CR --> EX[extract.py<br/>2,333 documents]
EX --> RX[rules.py<br/>997 candidate sentences]
RX --> R1[review.py<br/>Claude, exact quote required]
R1 --> R2[verify.py<br/>second review argues<br/>the hospital's side]
R2 --> RP[report.py<br/>milder verdict wins]
RP --> MAP["/audit page"]
RP --> CASE[Each bill's<br/>policy warning]
- Snap your bill. Take a photo of your itemized ER bill. A clear photo is all we need.
- We read every line. We compare each charge to the prices your hospital publishes.
- We flag the problems. You see each mistake and what it costs you.
- You answer 3 questions. Who lives with you, your income, and your insurance. That's it.
- You see what you really owe. One plain answer, and the rule behind it.
- Your letters are ready. Read them, approve them, and send them yourself.
The live app at phone size, recorded with the sample bill: a made-up ER bill at Zuckerberg San Francisco General, priced from the hospital's real published charges. Cut to about 45 seconds.
Also in the app:
- Lupa, a helper chat that can read an attached photo or PDF and only answers bill questions
- Bills of up to 6 photo pages
- The insurance statement as a photo or typed amounts
- Spanish
- Optional accounts
- The Android app, and iPhone via Home Screen
- A follow-up check-in and a state complaint letter
- "Not a valid document" for photos that aren't a bill
- A Display mode with larger type
- /dashboard with live agent metrics
Who does each step
In the order a bill goes through Recount. Some steps use two sponsors together.
| Step | Who does it | Why |
|---|---|---|
| Take the photo and start the check | Convex file storage and a scheduled action | The photo goes straight to Convex. The check runs in the background, so closing the app doesn't stop it |
| Read the bill photo | Claude, plus Azure AI Document Intelligence as a second read of the charges | Bills are messy photos. Output is forced into a schema. Code compares the two reads line by line and flags any amount they disagree on |
| Find price files and policies | Apify Actor, plain code | No model. It crawls and streams files |
| Show each step as it happens | Convex live queries on the step log | The phone subscribes to the case, so every step and finding appears without a refresh |
| Flag bad charges | Rules only | Every flag points at a bill line and a published number |
| Decide eligibility | Rules only | A wrong "you qualify" costs a family real money |
| Draft letters | Claude, template fallback, scheduled by Convex | Claude gets only the computed facts as JSON |
| Check the topic of a chat question | Rules first, then a short model check | Lupa answers only questions about bills, insurance, help paying and medical debt. A crisis message always gets the crisis reply |
| Lupa helper chat | Claude, Azure OpenAI gpt-4.1-mini as backup, messages kept in Convex | Answers from this bill's facts, with quotes from the hospital's policies found by Azure AI Search. Azure AI Content Safety checks every message. It doesn't change answers or send anything |
| Follow-up and state complaint | Convex for the check-in date, Claude for the complaint letter, template fallback | If the hospital says no, the patient gets a complaint to HCAI with the same facts |
| Review audit sentences | Claude, twice, with a quote check | A verdict is dropped if its quote isn't in the policy text |
| Dashboard metrics | Convex cron, pulling Apify's run records | Live counts from an anonymous log, with no bill contents |
| Send anything | Nobody | The patient sends the letters |
Made by eval/plot_results.py.
We crawled HCAI's Hospital Fair Pricing Policy Lookup for all 470 listed California hospitals and extracted text from 2,333 policy documents (463 hospitals readable; 7 had only scanned PDFs). We checked six rules from the 2025 law changes: no application deadlines, no asset tests, limited income proof, no credit reporting, no liens on a home, no Medi-Cal-first requirement.
| Result | Hospitals |
|---|---|
| Plain conflict with state law | 14 |
| Likely conflict | 47 |
| Wording worth asking about | 192 |
Each hospital is counted once, under its strongest result. The three numbers are never added up.
- Example: PIH Health Whittier Hospital. Its charity care policy says it will "consider individual or family net worth including all liquid and non-liquid assets owned". Its debt collection policy says "Collection agencies may notify the credit reporting agencies of the outstanding balance … after 180 days" and "Under certain circumstances when income does not exceed 400% FPL, PIH’s collection agency may file a lien on a primary residence". California bans all three. Both documents are dated 2026-07-10.
- The second review clears flags too. An earlier pass flagged San Francisco General's income proof wording. The second review found a fair compliant reading and cleared it. We'd rather miss a problem than accuse a hospital wrongly.
These are our readings of public documents, not legal findings. Every flag on /audit shows the policy's own words, the page, and the document's effective date.
eval/ generates synthetic San Francisco ER bills priced from each hospital's real published charges, plants known
errors in most of them, and renders them as clean scans, phone photos and PDFs.
- Rules on a correct reading: 24 of 24 planted errors caught, 0 false alarms on clean bills. The generator only plants errors the rules are meant to catch, so this tests the rules, not reading accuracy.
- Live reading on all 30 eval bills (10/24): the app's own reads (
eval/read.py, same model, prompts, settings and two-read amount check asconvex/agent.ts) got 30 of 30 bills exactly right: 181 of 181 lines with the right code, amount, quantity and date, and every total and amount due to the cent. That held for each kind: 10 of 10 clean scans, 16 of 16 phone photos, 4 of 4 PDFs. - End to end, read then checked: 24 of 24 planted errors caught, 0 false alarms on the 8 clean bills.
- The second read never disagreed with the first, so no bill asked the patient to confirm an amount. Two photos added a "please double-check" note anyway, saying the charges column looked out of line with the rows. Both were read correctly.
- Azure gpt-4.1-mini as the reader, same 30 bills: 95.6% of lines right and 18 of 24 planted errors caught, so Claude stays the main reader.
- Claude on the sample photo: 8 of 8 lines and the $12,741 total read correctly.
- Azure AI Document Intelligence as the second read (10/24): its layout model (OCR and table detection, not a
language model) read 181 of 181 lines with the right code and charge on all 30 eval bills (10 clean scans,
16 phone photos, 4 PDFs) and 8 of 8 on the sample photo, in a median 7.4 s. Next to Claude's reads it agreed
on every charge of every bill. Same caveat as above: these are generated bills.
eval/read_docintel.pyreproduces it. - Live QA uploads: a rotated, blurred photo read all 7 lines and its total; a ZSFG PDF and a CPMC photo flag exactly their planted errors (a total mismatch, a $647 duplicate) and nothing else.
A harder set (10/25). eval/hard/ adds 50 more synthetic bills in six layouts (an itemized statement grouped by
revenue code with subtotals, a revenue-code summary, 2 to 4 page bills with the total on the last page, a UB-04-like
claim form, collection agency letters, and the classic one), photographed crumpled, folded, at an angle, in hard
shadow, in low light, blurred, with glare, cut off, and marked with pen notes, highlighter and coffee rings. Plus 24
bill and insurance statement pairs in four statement layouts. Half of each set was held out: looked at only for the
score, never for a fix. Everything ran through the app's own reading code and rules.
- Held-out bills, after the fixes below: 260 of 265 lines read right, every total and amount due right (25 of 25), 22 of 22 planted errors caught, 0 false alarms on clean bills, 4 false alarms in all, every one on a photo that is both dim and blurred. Before the fixes: 250 of 265 lines, 19 of 22 caught, 2 letters sent back as "retake".
- Per condition, all 50 bills: crumpled, folded, angled, shadowed, glare, cropped, coffee and highlighter bills read 100% of their lines; pen marks 98%; low light 78% and blur 77%. The reader marks those amounts as hard to read.
- Insurance statements, all 24 pairs: 22 of 22 planted problems (denied lines and claims, pending claims, out-of-network ER, a bill skipping the plan discount, a line the plan never got, a statement for another provider) caught with the right dollar amount, 0 false alarms, 129 of 129 statement lines read. One crumpled statement with two claims on a page had its "you may owe" total wrong; no false finding came of it.
- What it found and fixed: a UB-04's split cents ("11963 00") read as $1,196,300; collection letters with only a
balance sent back as "retake the photo"; and when the two reads disagreed, the second read's amount replaced the
first even though the first was right on 18 of 25 such lines. Now amounts are parsed one way everywhere
(
convex/lib/amount.ts), balance-only letters go on to ask for an itemized bill, and the first read's amount stays until the patient picks. Details and per-layout tables:eval/README.md.
Caveat. All of these are generated bills and statements, made to look like real paper. No real patient bill was used, by law and by choice. A dim, blurry photo is still misread now and then, and 50 bills read once can't show how often a rare misread happens.
Alex Rivera (fictional) owes $12,741 to Zuckerberg San Francisco General, priced from the hospital's real charges. Recount finds a blood count billed twice ($269) and a chest X-ray billed at $482 against a published $402. One person earning $20,000 is at 125% of the poverty line, under the hospital's 138% free care cutoff, so the answer is "You likely owe $0", with both rules shown.
- Any California hospital. Hospital matching, eligibility, letters and the policy warning work for all 470. Free-care cutoffs come from each hospital's own policy for 159 of 470, kept only where the quoted sentence ties the number to free care. Every other hospital gets the state's 400% discount rule, never a guessed "free". Line-by-line price checks work for 340 of 470 hospitals: four SF hospitals from hand-checked snapshots, plus 336 whose own price files our Apify Actor read. A file counts only when it states the hospital's California license number, or names the hospital and no other hospital's license (docs/PRICE-FILES.md). For every other hospital the app says why in plain words: no price file found, a file we couldn't read, files that aren't clearly this hospital's, or a file without the ER codes we check. The Actor reads any US hospital's price file, so adding a hospital means verifying its file, not writing code.
- Cost. One bill read is about $0.04 of Claude (2,449 input and 1,422 output tokens). With letters, a bill costs on the order of 10 cents.
- Privacy, with optional accounts. Without an account, each bill opens only with a random secret kept on the patient's phone, in the link fragment, which never reaches a server. With an account, you can also open your own bills by signing in on any device. Passwords are stored hashed, and the account can be deleted along with its bills. Patients can delete a bill and its photo. Uploads are rate-limited.
- Security. The Azure parts (Document Intelligence, Azure OpenAI, AI Search, Content Safety) are built on HIPAA-eligible Azure services. Bills and photos are stored in Convex, and Claude reads them. Recount has no BAA and isn't a HIPAA covered entity, so this is not a HIPAA compliance claim. Details in docs/SECURITY-COMPLIANCE.md.
- Honest fallbacks. If the live lookup or the model fails, the app uses saved data or standard letters and labels which one you got.
npm install
npx convex dev # starts a Convex dev backend, writes .env.local
npx convex env set ANTHROPIC_API_KEY sk-ant-... # needed to read real photos and write letters
npx convex env set APIFY_TOKEN ... # optional: live hospital lookups
npx convex env set APIFY_ACTOR_ID NXiKTRZKK6p6qCSXk
npx convex env set AUTH_RESEND_KEY re_... # optional, with AUTH_EMAIL_FROM: password reset by email
npx convex env set AUTH_EMAIL_FROM "Recount <help@your-domain>" # a sender on a domain verified in Resend
npm run dev # http://localhost:3000
npx vitest run # rule tests (tests/)Without an API key, the sample bill still works from its source data and letters come from fixed templates. Both are labeled in the app.
Data pipelines (Python, run with -I):
python3 -I audit/crawl.py && python3 -I audit/extract.py # HCAI policies -> audit/corpus/
python3 -I audit/verify.py batches 40 # second review of flagged sentences
python3 -I audit/report.py # -> public/audit/findings.json, convex/lib/auditFindings.json
python3 -I pipeline/build_snapshots.py # price files -> convex/lib/snapshots/
cd eval && python -I generate.py && python -I score.py --oracle # synthetic bills, score the rulesMobile: see mobile/README.md. Actor: see actor/README.md.
| Path | What |
|---|---|
src/app/ |
Pages: start (/), a bill (/case/[id]), My bills (/bills), the audit (/audit), /dashboard, /privacy, /account, the app download (/app), the MCP server (/api/mcp) |
src/components/flow/ |
The step-by-step bill screens, including the insurance statement screens |
src/components/helper/ |
Lupa, the chat helper (web) |
src/components/audit/ |
Audit map, search and the policy-vs-law view |
src/components/dashboard/ |
The public dashboard: agent timings, Azure services, Apify runs |
src/components/i18n/ |
English and Spanish strings, language toggle |
src/components/bills/ |
My bills, on this phone or in your account |
src/components/account/, privacy/ |
Optional accounts; the privacy page, built from what's switched on |
convex/schema.ts |
Tables: cases, steps, agentRuns, agentMetrics, chatMessages, limits, hospitals, pushTokens, profiles |
convex/cases.ts |
Upload, start, household answers, line fixes, hospital choice, insurance statement, approve, delete, retry |
convex/agent.ts |
The background pipeline: read the bill, second read, hospital lookup, checks, letters, insurance statement |
convex/chat.ts, chatAgent.ts |
Lupa: messages and attachments; the reply (crisis check, Content Safety, topic gate, policy search, model) |
convex/followUp.ts, followUpAgent.ts |
After sending: check-in date and the complaint to HCAI |
convex/notify.ts |
Phone notifications when a bill check finishes (Expo push) |
convex/metrics.ts, dashboard.ts, crons.ts |
The dashboard's anonymous log, its summary, and the Apify run sync |
convex/lib/model.ts |
The one place that calls a model: Anthropic, Azure OpenAI or OpenAI-compatible, per task, with a fallback |
convex/lib/checks.ts |
Line checks: duplicates, list price, cash price, totals |
convex/lib/eobChecks.ts, eob.ts |
Bill vs insurance statement rules; what an insurance statement read returns |
convex/lib/eligibility.ts |
Charity care rules: poverty line, hospital cutoff, 400% floor |
convex/lib/coverage.ts |
Medicare and Medi-Cal rules from the coverage answer (docs/REIMBURSEMENT.md) |
convex/lib/fhir.ts, convex/fhir.ts |
HL7 FHIR R4 export of a bill |
convex/lib/hospital.ts, statewide.ts |
One hospital's reference data; matching a bill to one of 470 hospitals |
convex/lib/letters.ts, letterCheck.ts, complaint.ts |
Letter templates and facts; the invented-amount check; the HCAI complaint |
convex/lib/docIntel.ts |
Azure AI Document Intelligence second read of the charges |
convex/lib/search.ts, safety.ts, topic.ts |
Azure AI Search policy passages; Azure AI Content Safety; Lupa's topic gate |
convex/lib/privacy.ts |
What Recount stores and who processes it, built from the deployment's settings |
convex/lib/pages.ts, attach.ts, notice.ts |
Multi-photo bills; chat attachment limits; notification text |
convex/lib/snapshots/ |
Saved price data for the four SF hospitals |
actor/ |
Apify Actor: price file finder and streamer, charity policy limit extractor |
audit/ |
Statewide HCAI crawl, extraction, rule candidates, two reviews, report; index_search.py builds the Azure AI Search index |
pipeline/ |
Hospital index, price snapshots, income cutoffs with quotes |
pipeline/price-audit/ |
Rust price file audit: cash price vs gross and insurer rates, min/max consistency, NPI check digits |
eval/ |
Synthetic bill generator, renderer and scorer; eval/eob/ for insurance statements; eval/agents/ Lupa on Claude vs Azure OpenAI |
mobile/ |
Expo app (Android, iPhone) on the same Convex backend |
scripts/preflight.mjs |
Live check of the deployed app: pages, sample bills end to end, Lupa |
scripts/set-azure-env.sh |
Copies the Azure settings into a Convex deployment |
docs/HOW-IT-WORKS.md |
A code tour with line links |
docs/LOGBOOK.md, docs/CHANGELOG.md |
What changed and why; every commit with its time |
- Price checks for 340 of 470 hospitals. The others get eligibility, the policy warning and letters, but no line-by-line price check, and the app says why. Batch prices are as of the run date (2026-10-25) and are not refreshed live yet.
- ER codes only. Saved price data covers 17 to 23 common ER codes per hospital.
- Hospital bills only. ER physician bills are separate and follow different rules (HSC 127450 and after).
- No payment cap amount. The law caps discounted bills at what Medicare or Medi-Cal would pay. Price files don't publish those rates, so Recount explains the cap but doesn't compute it.
- Audit gaps. 7 hospitals have only scanned PDFs. "Worth asking" flags got one review, not two. Some policies on file are from 2022, and the audit judges the latest one HCAI holds.
- Accounts are optional and basic. Without one, My bills lives on one phone and anyone with a bill's private link can
open it. Accounts use email and password, or Google: no email check for password accounts and no password reset yet. Reset by
emailed code is built and switches on when the deployment has a Resend key (
AUTH_RESEND_KEYandAUTH_EMAIL_FROM). - Not legal or medical advice. Recount checks a bill against public rules and prices.
Thanks to Daniel Lui, Cal Hacks mentor, for the advice on reimbursement and HL7, and for backing our use of HCAI's official data.



