Inspiration
Ontario covers medically necessary MRIs under OHIP. If you're covered and you have a referral, you wait. That's most people, most of the time.
The cash-pay market is everyone else, and it's bigger than it sounds. Scans an employer or an insurer asks for. Immigration, legal, and workplace injury imaging. Anything a plan doesn't count as medically necessary. And everyone in the province who isn't on OHIP at all.
For all of them there is no price list. You call clinics one at a time and every one gives you a different number. The first number usually isn't the real one either. A $380 quote turns into $575 once the facility fee and the contrast administration show up.
The obvious fix is an agent that calls all of them for you. But building that runs into a problem nobody really talks about: the most effective negotiating agent is a lying one. "Danforth quoted me $200" is a great line right up until it isn't true. An agent that makes up leverage is exactly the thing a patient would be right not to trust.
So I built the honest version, and I made the honesty structural instead of something I asked the model nicely to do.
What it does
Haggle takes a doctor's order. You can paste the text or drop the referral PDF, which it actually parses. It turns that into a spec and calls five imaging clinics at the same time.
Each clinic is its own LLM agent with its own personality and its own hidden floor. One is a straight shooter. One lowballs and hides $195 in fees until you ask for a breakdown. One is an upseller. One refuses to talk to you the moment you disclose you're automated, which is the honest thing for a clinic to do.
The negotiator plays their real quotes against each other over up to three rounds, stopping early once nobody moves. It presses the bundled quotes for itemization and returns a ranked list of itemized cash prices. A full five clinic run finishes in under 30 seconds. On a typical run the best price lands $150 to $300 below the highest opening quote, and that whole spread is fees nobody mentioned until we asked.
The part I care about most is that the agent cannot cite a price no clinic actually gave. There's a button in the UI that makes it try. It gets blocked, and you watch the sentence it was about to say get stopped before it's spoken.
Scope, up front: the five clinics are simulated. The negotiation, the itemization pressure, the audit trail, and the honesty gate are real, and the gate has unit tests.
How I built it
Java 25 and Spring Boot 4 on the backend, React 19 on the front, Postgres for state. The clinic and negotiator agents run on OpenAI, and every model call has a deterministic fallback underneath it. If the API is unreachable the dialogue gets blunter, but the run still completes and the gate still refuses. A dead key degrades the demo instead of killing it.
The orchestrator runs in rounds. Every clinic in a round gets dialled on its own virtual thread, and the round doesn't advance until all five have come back. One barrier per round, so the agents genuinely run at the same time instead of just looking like they do. Rounds repeat until nobody moves, then it stops.
Two things are enforced in code instead of in a prompt.
Hidden floors. Each clinic has a lowest price it will ever accept. The negotiator never sees it, and it gets checked when a quote is submitted. An LLM told "never go below $395" will eventually go below $395.
The leverage gate. Before the agent can cite a competing quote, the figure gets checked against a store of quotes actually captured on this run. No match, no citation. It's a tool boundary and not an instruction, so there's no prompt to talk it out of.
The UI streams over SSE, and every price movement lands in an audit log next to the line that caused it.
Challenges I ran into
The LLM raising its own price. I handed a clinic agent leverage and it came back higher than it opened. I fixed it with a wall in code that rejects any response above the current total. Another case of not trusting the model to follow an instruction.
Making the parallelism visible. It ran in parallel for days before it looked like it did. The UI was rendering one conversation at a time while five were actually running.
Phone calls. Twilio's voice webhook has a hard 15 second timeout and a real negotiation doesn't finish in 15 seconds, so I built a hold loop that parks the caller and polls for the result. The code is in the repo. I never got a number provisioned, because every provider wanted regulatory verification I couldn't finish in time. So it's built and not live, and I'd rather say that than imply otherwise.
What I learned
I used AI heavily to build this and I want to be straight about that, because it changes what the project actually demonstrates.
I designed the system myself. The round structure, the gate, where the floors live. And I wrote the files the whole claim rests on by hand: the leverage gate, the orchestrator, the clinic agent, and the negotiator agent. Basu designed and built the frontend across three iterations, and I wired the live negotiation data into it. Scaffolding and glue were AI assisted.
What I learned is that the interesting decisions were never the code. They were calls like "an instruction is not a constraint," and no model was going to make that one for me.
What's next
A real phone number.
The bigger thing is that nothing in the orchestrator knows it's negotiating for imaging. Swapping clinics.yaml points the same engine at dental, vision, veterinary, or auto repair. Those are all categories where almost nobody is covered and everybody calls around one shop at a time. Medical imaging is the wedge because the hidden fee problem is sharpest there. It isn't the ceiling.
Built With
- anti-gravity
- claude
- docker
- java
- neon
- openai
- postgresql
- react
- render
- server-sent-events
- spring-ai
- spring-boot
- spring-modulith
- twilio
- vercel
- virtual-threads
- vite



Log in or sign up for Devpost to join the conversation.