Track
PillSpark: Fax to Fill
Inspiration
The brief asked for a pipeline that flags uncertain fields instead of guessing. We started from the assumption that the hard case is a fax the model can't read. During the build we found out it is the opposite: the hard case is a fax the model reads confidently and gets wrong.
It happened to us. Demo fax B has a faded "75 mg". We sent it to two models from different vendors, expecting them to disagree. Both read it as "15 mg". They agreed with each other and both were wrong. The only thing that stopped it was a plain rule with no model in it: 15 mg is not a strength that drug is sold in.
That shaped the project. We do not ask a model how sure it is. We look for evidence around its answer, and we let rules and a pharmacist decide what can proceed.
What it does
RxBridge takes a faxed prescription and produces a structured draft where every field is either verified or turned into a specific question for a pharmacist.
- Two independent reads of the drug and strength, from models by different vendors. If they disagree, the field goes to review.
- Reference checks against a local drug catalog with marketed strengths and a table of look-alike drug pairs such as hydroxyzine and hydralazine.
- Rule checks with no model involved: identifier check digits, marketed strengths, and whether the quantity matches the directions.
- A review screen that shows the fax, the competing reads, the candidates, and one plain-English reason. There are no confidence percentages anywhere.
- A blocked order. It cannot move forward while any critical field is unresolved, and only a pharmacist can approve it.
Our favorite case is a perfectly legible prescription: 10 mL three times a day for 10 days, with a stated quantity of 50 mL. The model reads every word correctly, and the order still is not ready:
$$10\ \text{mL} \times 3\ \text{per day} \times 10\ \text{days} = 300\ \text{mL} \neq 50\ \text{mL}$$
RxBridge never rewrites 50 to 300. It reports the conflict and waits for a pharmacist.
How we got to the design
Each of us wrote a design before we compared notes, and the final spec is a merge of all three.
- Karthik started from the idea that confidence should be derived, not asked for. His draft had the evidence signals, the look-alike drug table, and three error classes: impossible, implausible, and plausible but wrong.
- Noah framed the product as a trust boundary. That draft had the state machine, exceptions as first-class objects, the rule that an unresolved field blocks the order, and the quantity conflict case.
- Disha focused on measurement and operations. Her draft had the 40-fax synthetic test set with ground truth, a scoring script, silent errors as the headline metric, and the build plan.
Merging them took real arguing. At one point a single document held three field contracts, two storage plans, and three different code-freeze times. We fixed it by writing one table of final decisions at the top and deleting everything it overruled.
How we built it
- Extraction: fax images go through Nicrron to two vision models from different vendors, with a second read on drug and strength.
- Verification: a FastAPI backend runs the reference and rule checks. A policy engine then decides each field by precedence, never by averaging scores.
- Review: a React and TypeScript front end, built against hand-written fixture data so it did not have to wait for extraction.
- Reliability: [VERIFY: confirm live before submitting] Block Convey PRISM traces every model call, with one session per prescription. Its Guardrail flags instruction-like text found inside a fax, and our own policy makes sure document content can never change approval state.
- Test data: 40 synthetic faxes across 4 templates and 3 quality tiers, with planted problems, plus separate demo faxes. Everything is invented, so no patient data was used.
One of the rule checks is the NPI check digit. Prefix the first nine digits with 80840, run the Luhn sum \(s\) over those 14 digits, and the tenth digit must be:
$$d_{10} = (10 - (s \bmod 10)) \bmod 10$$
A single misread digit always fails this check, and no model is involved in the decision.
How it handles accuracy and uncertainty
A field is verified only when independent evidence agrees. Disagreement between reads, a look-alike drug pair, a failed check digit, a strength that is not marketed, or a quantity that does not match the directions each send the field to a pharmacist with a reason.
Our headline metric is silent errors: a critical field that was wrong and was accepted without review. We report it next to the false-review rate, so it is clear the system is not succeeding by flagging everything.
$$\text{silent errors} = \left|{\text{critical fields that are wrong and not flagged}}\right|$$
$$\text{false-review rate} = \frac{\text{correct fields that were flagged}}{\text{all correct fields}}$$
Results on our 40-fax synthetic benchmark [UPDATE: re-run on the submitted code; state which fax set]:
| Measure | Result |
|---|---|
| Planted problems caught | 16 of 16 |
| Silent errors | 2 [UPDATE: name the fields; these were misreads, not planted problems] |
| False-review rate | 15% [VERIFY: of which fields] |
| Reduction in fields a pharmacist must review | 86% [VERIFY: of which fields] |
These numbers describe this benchmark only. They do not establish safety on real faxes.
Challenges we ran into
- Two models agreed on a wrong value. Both read the faded 75 mg as 15 mg. We had treated agreement as strong evidence, and this showed it is not. Rule checks now run even when the reads agree.
- Bounding boxes landed in the wrong place. The first boxes the model returned did not line up with the fields. [UPDATE: how we fixed it] We also made crops helpful but not required, so a bad box never blocks a field.
- 8 of 40 faxes failed on cut-off answers. The model's reply was truncated before the structured output finished. [UPDATE: how we fixed it]
- Two hand-off formats were built in parallel. Extraction and the backend each assumed a different shape for the same data, and we had to reconcile them mid-build.
- Partly readable fields were being marked verified. A field the source only partly supports should go to review. [UPDATE: status of the fix]
- [UPDATE: front-end and PRISM challenges from Noah and Karthik]
What we learned
- Agreement is not proof. The 75 mg case was caught only because 15 mg is not a marketed strength. If both models had read 25 mg, which is a real strength, nothing in our system would have caught it. That is the risk that remains, and the reason a pharmacist makes the final call.
- A fax can be read perfectly and still be wrong. The quantity conflict taught us that rule checks have to run even when extraction looks flawless.
- A guardrail cannot screen a fax before the model reads it. The injected text does not exist as text until after extraction, so the check belongs on the next step.
- A handful of test documents cannot support a safety claim. We built a scored set with ground truth so we could report a measured number, including the errors.
- Synthetic handwriting is neater than real handwriting. Accuracy on real faxes would be lower, and a real deployment would need privacy, compliance, and regulatory review.
Built with
Nicrron, Block Convey PRISM, FastAPI, Python, Pydantic, React, TypeScript, Vite, ElevenLabs
Team
- Karthik: backend, verification rules, policy engine
- Noah: review interface
- Disha: extraction, test data, benchmark, submission
Built With
- blockconvey
- elevenlabs
- fastapi
- nicrron
- prism
- pydantic
- python
- react
- typescript
- vite
Log in or sign up for Devpost to join the conversation.