Inspiration

When you get hurt, you have three questions: what do I do right now, where should I go, and what will it cost me? Your insurer’s chatbot only helps its own members. ChatGPT doesn’t know your plan or what’s open near you. And if you’re uninsured or on Medi-Cal, the answer is even harder to find, though those are the people who can least afford a wrong guess. About 4 in 10 Californians have medical debt, and around 2 million Medi-Cal members could lose coverage under H.R. 1.

We wanted one number you can text with any medical question. It knows your coverage and where you are, so it can answer all three questions.

What it does

You text something like “think i sprained my ankle”. In order:

A safety check runs in code first. Chest pain, stroke signs, overdose and suicidal thoughts get a fixed 911 or 988 reply before any AI sees the message. You get general first-aid steps and the signs that mean you should go to the ER. It asks where you are. You get 2 or 3 places that are open now and that your coverage accepts. Each one shows distance, hours, whether there’s an X-ray, your cost, and a source link with when it was last checked. Afterward you can text DIRECTIONS for a map link, or CARD for a note in English to show the front desk. The card helps Spanish speakers at check-in. It texts you the next day to see how you’re doing.

The same question gets three different answers for Kaiser, Medi-Cal and uninsured users, and all three are correct.

How we built it Messaging: iMessage through Sendblue, with a FastAPI webhook. Gemini Flash: reads insurance cards, writes first aid, picks the level of care, ranks options, and pulls facts out of crawled pages. Neon Postgres + PostGIS: stores clinics, plans, users and conversation state. One SQL query filters by coverage, open now (computed in SQL), X-ray, and distance. Bright Data Web Unlocker: crawls clinic sites that block plain requests (AFC, Kaiser). Every crawl loads into its own Neon branch, gets validated (row counts, every point inside San Diego County, hours that parse, a source on every row), and then swaps into main in one transaction. Community Health Group’s FHIR provider directory: our source for the Medi-Cal network. Medi-Cal plans are required to publish it.

For uninsured users, cost depends on income relative to the federal poverty level (FPL) for their household size:

$$\text{\% FPL} = \frac{\text{household income}}{\text{FPL}(\text{household size})} \times 100$$

That percentage decides which sliding-fee tier applies. Family Health Centers of San Diego charges $25 a visit for people who qualify.

One rule shaped everything: the model never writes facts. Gemini returns IDs from our database plus a short reason. Code writes every name, address, price, hour and link. If nothing verified is open, the app says so and gives you 211 instead of guessing.

Challenges Data that looked real but wasn’t. Our AFC pricing URLs returned 404 pages, and the model read the address out of the footer. We only noticed when the extracted prices came back empty. Now we check page titles, and the crawler reads the hours from the page’s structured data. Nobody posts cash prices. None of the urgent cares we crawled list a self-pay price. So the app says “not confirmed, ask when you call” rather than show a made-up number. Geocoding. A Hillcrest clinic first landed in Escondido. We now accept a geocode only if it falls within 8 km of the clinic’s ZIP code, and we skip sites we can’t place. Branching without losing users. Promoting a Neon branch would have erased every user and conversation saved after the branch was made. So we copy only the locations table into main. Rigid conversation. A real phone test showed that “it might be broken”, sent after the answer, restarted the whole flow. Now one model call sorts each follow-up into directions, an update on the same problem, or a new question. Free tiers. Gemini’s free quota and Sendblue’s verified-contacts-only line both shaped how we tested. What we learned

Provider directories are often wrong, so every answer should show where its information came from and how old it is. Safety checks belong in code, not just in a prompt. And the most useful thing an LLM did here was choose between real options, not make them up.

Built With

Share this project:

Updates

Submission history