Inspiration Since 2021, every US hospital has been legally required to publish its real prices: the list price, the discounted cash price, and the rate it negotiated with each insurer. Almost nobody uses this data because it’s buried in machine-readable files containing hundreds of megabytes to several gigabytes of JSON and CSV.

Opentab - Agentic Hospital price comparison map with natural audio search.

Meanwhile, people with high deductibles, denied claims, or out-of-network care often pay whatever the first hospital bills. I built OpenTab for patients trying to save money on radiology when insurance doesn’t work out. Imaging is among the most shoppable types of care: you get a referral, then choose where to go.

The price differences are enormous. In San Francisco, the cash price for a lower back MRI is $2,218 at UCSF and $6,944 at Zuckerberg San Francisco General, about 1.5 miles apart. For an abdominal CT, the Aetna-negotiated rate ranges from $422 to $4,741 across Bay Area hospitals.

That’s approximately an 11× difference for the same scan with the same insurer.

What it does Ask out loud: “How much is a lower back MRI if I pay cash?” The map of nearby hospitals turns into green-to-red price tags, and OpenTab speaks a two-sentence answer.

Every hospital card shows:

Your insurer’s negotiated rate, the cash price, and the list price.

“Save $X vs. the most expensive.”

A flag when cash beats your insurance rate.

Which insurers have published rates there, with the caveat: “Likely in network; call to confirm.”

The price file’s date and a link to the hospital’s own source file.

Works anywhere in the US: Open it in a new city, and it finds nearby hospitals in a 4,600-hospital CMS directory, then reads their price files live. Pins pulse while each file loads.

Add any hospital by name and watch it progress through Searching → Fetching → Parsing → Indexed in about 20–30 seconds.

How I built it Bright Data SERP API finds a hospital’s official site from just its name. I send hedged parallel requests because latency varies.

Web Unlocker fetches cms-hpt.txt and price files when hospital sites block access. Some hospitals’ files can only be loaded this way.

Apify I built a custom TypeScript Actor that:

Discovers price files through cms-hpt.txt.

Stream-parses CMS JSON (v2/v3), tall CSV, wide CSV, ZIP, and gzip files within a time and byte budget.

Normalizes insurer names and plan types.

Reports live stages back to the app.

Every Bay Area hospital was ingested through Apify runs.

Elastic Serverless + Agent Builder Elastic stores prices, hospitals, a US hospital directory, and a procedure-synonym index, all with explicit mappings.

An OpenTab Advisor agent uses four ES|QL tools: lookup_procedure, compare_procedure, cash_prices, and list_payers. The web app, the agent, and an MCP client all call the same tools, so they always see identical numbers.

Claude (Opus 5.5) Claude is the model behind the Agent Builder agent. In the app, it turns speech into a structured search using enums, so it can only select real procedure codes and insurers. It also writes the spoken summary, and every dollar amount it says is verified against numbers I computed.

Convex Convex is the backend. It:

Orchestrates each question.

Runs a live ingestion job queue that streams Apify stages to the UI.

Tracks nearby hospital discovery for each map area.

Mints short-lived voice tokens.

Cartesia I use streaming speech-to-text (ink-whisper) over WebSocket from an AudioWorklet, along with sonic-3 text-to-speech. Both are called from the browser using 60-second scoped tokens, so the API key never leaves the server.

Frontend and deployment The app runs on Next.js, deployed on Vercel, with Leaflet and OpenStreetMap, at opentab.health.

Challenges I ran into Huge, inconsistent files I had to handle:

Three file layouts.

UTF-8, UTF-16, and byte order marks (BOMs).

Zipped CSVs and Box redirects.

Insurer names like CIGNA HMO [1010103].

Medicare plans abbreviated as MCR ADV, which look like commercial plans unless you catch them.

Speed Generic streaming CSV and JSON parsers ran at 2–4 MB/s, far too slow for a live demo. I wrote prefiltering readers that skip rows without a target code and reach 43–66 MB/s, allowing a 343 MB file to parse in about five seconds.

Honesty over coverage Many hospitals publish commercial rates as formulas instead of dollar amounts, and some rows represent per-diem rates or component fees. I exclude those from scan prices, show cash prices as a median with a range, and say “not published” instead of estimating.

Voice on phones Mobile browsers block audio that isn’t initiated by a tap. I unlock playback during the microphone tap and reuse it for the spoken answer.

Live dependencies A public map-search service went down across every server mid-build. I switched to the official CMS hospital directory with Census ZIP coordinates, which turned out to be more reliable.

Accomplishments I’m proud of Real data only: no mocked prices anywhere, with a source link for every number. It tells you when it doesn’t know. Open it anywhere in the US, and it builds the map live.

What I learned Price transparency data is real and powerful, but it’s messy enough that “just parse the file” is the whole product.

LLMs are most trustworthy when they never touch the numbers. Claude interprets the question and phrases the answer; deterministic tools do everything in between.

Sharing one set of ES|QL tools across the UI, an agent, and MCP kept every surface consistent.

What’s next for OpenTab Add separately billed radiologist and physician fees.

Estimate the patient’s share based on their deductible and coinsurance.

Add a phone number using Twilio and Cartesia for people without smartphones.

Expand beyond imaging.

Built With

Share this project:

Updates

Submission history