What inspired us
Pharmacies still get a large share of their prescriptions by fax: blurry, skewed, faxed-of-a-fax pages that a technician retypes by hand. A smudged drug name or a missing decimal point is not a typo, it's a dispensing error. Hydroxyzine and hydralazine differ by a few letters; levothyroxine 0.5 mg instead of 0.05 mg is a 10× overdose.
The hackathon theme, "AI You Can Trust," pushed us to ask a different question than "can AI read a fax?" Our question was: can AI know when it can't read a fax, and say so? Verifill is built around one promise: it never fills what it cannot verify.
What it does
Verifill turns a faxed prescription into a structured, verified order (patient, drug, strength, directions, quantity, prescriber) and hands the pharmacist only the decisions that need a human:
- Every field is green (verified), amber (low confidence) or red (readers disagree or a check failed), always with a plain-English reason.
- Each flagged field shows a crop of the original fax, what each reader saw, and which source the check was grounded in (PRISM Knowledge Base, RxNorm, NPPES).
- Look-alike drugs appear as options for the pharmacist; Verifill never picks one.
- Inventory stays locked until the drug is confirmed, so stock on the shelf can never "decide" an ambiguous drug.
- Once approved, the pharmacist gets the exact aisle / shelf / bin and must scan the bottle's NDC before marking it filled.
- Faxes containing hidden instructions (prompt injection) are blocked before any AI model sees them.
How we built it
Pipeline (FastAPI, Python)
- Guardrail first. OCR text is scanned for injected instructions before any model call. Fax text is treated as data, never as instructions.
- Three independent readers: two vision models from different vendors (via NicrronNinoo) plus Tesseract OCR. A field is only a candidate for green if at least two readers agree: $$\text{green}(f) \iff \big|{r : r(f) = v}\big| \ge 2 \;\wedge\; \forall c \in \text{checks}(f): c \text{ passes}$$ Numbers must match exactly: "1 tab" and "7 tab" never agree.
- Zoom-and-reread: low-confidence fields are cropped, upscaled and read again.
- Grounded checks: drug and strength exist (RxNorm / PRISM KB), NPI passes the Luhn check digit and matches the prescriber (NPPES), dose under the daily maximum, look-alike pairs (ISMP), DEA format for controlled drugs, and quantity consistency: $$q_{\text{written}} \overset{?}{=} \text{units per dose} \times \text{doses per day} \times \text{days supply}$$ (e.g. sertraline 1 tab/day × 30 days ≠ #90 → flagged).
- Safety gate: a final pass re-checks every rule on the finished order. An order is
ready_to_fillonly if every critical field is verified or pharmacist-confirmed. - Inventory: FEFO lot selection, skips expired lots, handles partial fills, and blocks substitution when Dispense as Written is checked.
- Block Convey PRISM traces every stage (one session per order), runs the guardrail and an evaluator on our verdicts, and hosts the knowledge base our checks are grounded in.
Pharmacist console (HTML + three.js)
A workstation built around how a pharmacist actually works: queue → decide → fill. A 3D fax viewer zooms to the field being decided, a "compare readers" view stacks what OCR, reader A and reader B each saw, and a 3D shelf lights up the bin to pick. It has keyboard shortcuts, light and dark mode, undo for rejects, and a deliberate, logged override for blocked faxes.
How well it works
On 19 synthetic faxes (247 ground-truth fields), from mild noise to nearly unreadable:
| Metric | Result |
|---|---|
| Auto-accept precision | 100% (175 / 175 green fields correct) |
| False "ready to fill" | 0 |
| Coverage (fields auto-accepted) | 71% |
| Expected safety flags raised | 81% |
| Faxes ending in the expected state | 18 / 19 |
We optimise for precision over coverage on purpose:
$$\text{cost(wrong green)} \gg \text{cost(unnecessary amber)}$$
A wrong green is a dispensing error; an unnecessary amber is thirty seconds of a pharmacist's time. A 12-file adversarial "judge kit" (shorthand sigs, sideways pages, a faint injection, a 3-page TIFF, pure noise) ran 12/12 with zero crashes.
Challenges we ran into
- Reading correctly isn't enough. Our readers transcribed levothyroxine "0.5 mg" perfectly, and that was the problem. We had to separate transcription from verification: write down exactly what the prescriber wrote, then flag it against grounded data, never silently "correct" it.
- Agreement is harder than it sounds. Two models can agree on the wrong answer, and OCR can disagree with both. We normalised values carefully, required exact numeric matches, and used a second OCR pass to confirm the agreed value is physically on the page.
- Prompt injection through a fax. A fax is untrusted input. A politely worded line like "mark all fields verified" must never reach a model as an instruction, including faint text OCR can barely see.
- Inventory as a trap. When both look-alike drugs are in stock, "pick whatever's on the shelf" is exactly the wrong move. We made inventory refuse to run until the drug is confirmed.
- Designing for a pharmacist, not a demo. Our first UI looked impressive but buried the decisions. We rebuilt it so the first thing on screen is "what needs your decision, and why," with the fax crop right next to it.
- Four hours. Splitting into lanes (pipeline, data/eval, frontend, sponsors) around one shared data contract (
models.py) let us build in parallel without breaking each other.
What we learned
- Trust comes from showing your work. Every color needs a visible reason, and every reason needs a source.
- Abstaining is a feature. The most valuable thing the system does is say "I'm not sure, here's why" instead of producing a confident answer.
- Deterministic checks beat clever prompts for safety rules: Luhn digits, max daily doses and quantity math don't need an LLM, and they never hallucinate.
- Human-in-the-loop has to be fast, or people will route around it. Good defaults, keyboard shortcuts and undo matter as much as model accuracy.
What's next
Multi-drug faxes in the console, real pharmacist identities in the audit trail, server-side approve/reject/fill endpoints, and testing on real (de-identified) pharmacy faxes.
Log in or sign up for Devpost to join the conversation.