Cross payer drug coverage intelligence for market access analysts.
π Winner Β· Innovation Hacks 2.0 Β· Arizona State University Β· April 2026
Search, compare, and query medical benefit drug coverage policies across multiple health insurance payers through a single interface. Ingest payer PDFs once; query the normalized data through a comparison UI or a streaming natural language chat that runs structured retrieval against a SQLite schema.
Full Video: link
Health plans publish drug coverage policies as inconsistent, frequently changing documents scattered across hundreds of payer portals. Answering "what does Cigna require for Humira?" means opening multiple PDFs and manually normalizing each one. That takes hours.
Policy Lens centralizes payer policies into a structured schema and exposes them through a comparison UI and a chat interface. Each new payer document goes through an LLM powered extraction pipeline and lands in the same normalized tables, so cross payer comparison becomes a SQL query instead of a manual reading exercise.
Schema grounded retrieval, not vector RAG. Keyword extraction on the query fires up to four parallel parameterized SQL queries across the policy tables and assembles up to 15K chars of structured context before calling Gemini, grounded in the schema rather than chunk similarity. On named structured fields at this scale, exact and LIKE matching beats embeddings and ships no vector store to operate.
Streaming responses over SSE. FastAPI StreamingResponse emits data: {...} chunks; the frontend stitches them with a ReadableStream line buffer that holds partial frames across network reads.
LLM powered ingestion pipeline. Drop a PDF, pdfplumber pulls text page by page, gemini-2.0-flash runs a zero shot JSON schema extraction at temperature=0.1, and structured drug rows insert straight into the normalized schema. No regex parsers, no per payer adapters.
One schema for heterogeneous documents. Two document types feed a single drugs table; two SQL views use COALESCE chains to resolve drug_name and hcpcs_code so every downstream query reads clean fields and never knows which document type a row came from.
UI engineering as the product. The comparison surface and PA Friction Heatmap collapse a high dimensional payer by drug matrix into a scannable view: color graded cells, switchable lenses, row level aggregates, and auto surfaced highest and lowest burden callouts.
Cross payer Market Access Score. A 0 to 100 friction score per payer, round((1 - restriction_score / max_possible) * 100), summing PA, step therapy, and site of care drug counts. Lower means more friction.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β React 19 + Vite 8 + React Router 7 + TanStack Query 5 β
β Pages: DrugLookup, Comparison, Heatmap, AskAI, β
β PolicyChanges, Ingest, Library β
β api.ts ββ fetch() / SSE ReadableStream β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ
β /api/* (Vite proxy β :8000)
ββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ
β FastAPI + uvicorn β
β β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ βββββββββββ β
β β drugs.py β βcompare.pyβ βpolicies β βingest.py β β ai.py β β
β ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬βββββ β
β β β β β β β
β ββββββΌβββββββββββββΌβββββββββββββΌβββββββββββββΌβββββββββββββΌββββββ β
β β aiosqlite ββ db/policies.db β β
β β Tables: policies, drugs, covered_indications, β β
β β step_therapy, dosing_limits, excluded_indications β β
β β Views: drugs_unified, drug_access_summary β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β AsyncOpenAI βββΊ generativelanguage.googleapis.com/v1beta/openai/ β
β model: gemini-2.0-flash β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β pdfplumber (sync, in process)
βΌ
Uploaded PDFs (temp files, deleted after extraction)
User submits message
β POST /api/ai/chat { messages, stream: true }
β _build_context(last_user_message):
1. Tokenize, filter stopwords, take first 5 terms
2. LIKE query: drugs_unified (drug, generic, brand, category, hcpcs)
3. LIKE query: covered_indications
4. LIKE query: step_therapy
5. SELECT * FROM policies, plus COUNT aggregates
6. Concatenate sections, truncate to 15K chars
β messages = [system: SYSTEM_PROMPT, system: DB_CONTEXT, ...user history]
β AsyncOpenAI β gemini-2.0-flash, stream=True
β StreamingResponse emits "data: {...}\n\n" chunks
β Frontend SSE reader buffers and appends to assistant message
Drug Lookup. Search by drug, generic, or brand name. Returns per payer coverage cards with access status, prior auth flag, step therapy flag, effective date, and HCPCS code. A trending bar shows the top drugs by number of payers listing them.
Multi Drug Coverage Matrix. Chip input with autocomplete (active after 2 characters). Type multiple drugs; receive a { payers, drugs } grid showing per payer coverage across the full set. Payer cards turn green only when the payer covers every selected drug.
Payer Comparison View. One column per payer, six rows per drug: Coverage Status, Prior Auth, Step Therapy, Site of Care, Indications (first 3), Dosing/Quantity. Four summary metrics: Payer Coverage count, total entries, Clinical Variance (PA ratio), Market Access Score.
Ask AI. Streaming chat over the normalized policy database. Suggested prompts are generated dynamically from live data (3 random drugs and 2 random payers picked from the DB on page load). Responses render incrementally as the stream arrives.
PA Friction Heatmap. A data visualization surface that renders payer friction across three switchable lenses: PA Friction Score (1 to 10), Step Therapy Burden (number of required prior drugs), and Approval Time (days). Color graded cells, per row averages, and highest and lowest burden insight cards per view turn a dense payer by drug matrix into a view an analyst reads without a spreadsheet.
Policy Changes Feed. Timeline view of all policy revisions stored in the policy_changes JSON column. Severity is classified by keyword matching: Clinical, Notable, Moderate, Minor. Filterable by severity.
Policy Ingestion. Drag and drop PDF upload with optional payer hint. pdfplumber extracts text page by page, truncated to 15K chars and sent to gemini-2.0-flash with a JSON schema extraction prompt. Result rows insert into policies, drugs, and covered_indications in one transaction. Returns extracted drug count and policy title.
Policy Library. Indexed policies table with aggregate counts (drugs, PA drugs, step therapy drugs) via LEFT JOIN ... GROUP BY p.id.
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite 8, Tailwind CSS 4 |
| Routing | react-router-dom 7 |
| Server state | TanStack React Query 5 |
| Backend | FastAPI, uvicorn, aiosqlite |
| Database | SQLite |
| LLM | Google Gemini 2.0 Flash (via OpenAI compatible endpoint) |
| PDF parsing | pdfplumber |
| Streaming | Server Sent Events over StreamingResponse |
Hackathon prototype, 36 hour build. The ingestion pipeline, schema grounded retrieval, streaming chat, and comparison UI all work end to end against the live database. Production hardening was out of scope for the build.
Current demo dataset: 5 pre ingested policies (Blue Cross NC, Cigna, Florida Blue, Priority Health, UnitedHealthcare Commercial) covering 1,512 drug records. The ingestion pipeline accepts new PDFs and writes to the same schema, so the dataset grows with each upload.
Known limitations:
- No authentication or rate limiting. Single tenant, single process.
- No vector store or semantic search. Retrieval is keyword LIKE queries with leading and trailing wildcards (full table scans). No indexes defined beyond implicit rowids.
- 15K character truncation drops content from long multi page documents.
policy_changesdate sort is lexicographic on mixed format strings; ordering is unreliable.
Production roadmap:
- Auth and per tenant API keys.
- btree indexes on
drugs.drug_name_normalized,drugs.payer, andpolicies.payerfor sublinear search. - Replace LIKE wildcard retrieval with FTS5 or a hybrid keyword plus vector retriever once the corpus exceeds ~10K records.
- Background task queue for ingestion (Celery or arq); the upload endpoint currently blocks on Gemini.
- Bounded request size, structured logging, per step latency metrics.
git clone https://github.com/SuhasR3/Policy-Lens.git
cd Policy-Lens
# Backend
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.template .env
# add GOOGLE_API_KEY to .env
uvicorn main:app --reload
# Frontend (new terminal)
cd frontend
npm install
npm run devOpen http://localhost:5173. The Vite dev server proxies /api/* to localhost:8000.
Get a Gemini API key at aistudio.google.com/apikey.
backend/
βββ main.py # FastAPI entry point, CORS, router mounting
βββ database.py # aiosqlite connection manager
βββ routers/
βββ drugs.py # Drug search, coverage matrix, autocomplete
βββ compare.py # Payer comparison, summary metrics
βββ policies.py # Policy list, changes feed
βββ ingest.py # PDF upload, LLM extraction, DB insert
βββ ai.py # Chat endpoint, retrieval context builder, SSE streaming
frontend/src/
βββ App.tsx # Client routing (7 routes)
βββ lib/
β βββ api.ts # All HTTP calls, SSE reader
β βββ types.ts # TypeScript interfaces
βββ pages/
β βββ DrugLookupPage.tsx # Search + trending + multi drug grid
β βββ ComparisonPage.tsx # Per payer comparison table
β βββ AskAIPage.tsx # Streaming chat interface
β βββ PAFrictionHeatmapPage.tsx # Payer by drug friction grid
β βββ PolicyChangesPage.tsx # Changes timeline
β βββ IngestPage.tsx # PDF upload UI
β βββ LibraryPage.tsx # Indexed policies table
βββ components/layout/ # SideNavBar, TopAppBar, FloatingAIButton
db/
βββ policies.db # Pre seeded SQLite database