Case file
Prior authorization pre-screening. It reads the note a physician has already written, checks it against the insurer's current published criteria, and names what is missing while the patient is still in the room.
The problem
Prior authorization is the most expensive administrative process in American medicine. A physician practice spends roughly two business days a week of staff time on it. Around a quarter of physicians report a PA delay that led to a serious adverse event.
Most of that cost is avoidable, and for a specific reason: the information the payer wants is almost always already in the note. The clinician documented the 14 months of symptoms, the failed physical therapy, the weight-bearing films. What they had no way of knowing is that this payer, for this CPT code, this month, requires 12 weeks of documented conservative care and an explicit laterality, and that the note says "arthroplasty" without saying "right".
So the request goes out, comes back three weeks later as a request for additional information, and the whole loop runs again. Nobody learned anything clinical. A field was missing.
Clinchec closes that loop before the request is ever sent, while the patient is still in the room.
What it does
Paste a SOAP note. In about 40 milliseconds you get:
The three products
A spaCy pipeline over a curated clinical lexicon covering the highest-volume PA categories: advanced imaging, musculoskeletal surgery, interventional pain, sleep medicine.
It does the unglamorous things that decide whether clinical NLP is usable:
pt means patient,
uppercase PT means physical therapy. Getting this wrong invents a course
of conservative care that never happened.Payer criteria change constantly and are published as prose across hundreds of documents. Live crawls them on a schedule and maintains a structured rules database per insurer, procedure and line of business.
The interesting problem is not crawling. It is establishing that what was crawled actually adjudicates anything.
DELEGATED, carried end to end: shown to the
clinician, never scored, and blocked by a database constraint from holding
evidence requirements. Detection runs per adjudication rather than per
document, because one policy can state real criteria for one procedure and
defer another in the same file.Three adapters ship, each solving a different structural problem:
| adapter | shape | state |
|---|---|---|
| Aetna | numbered Clinical Policy Bulletins, criteria under a Policy heading |
crawled live |
| UnitedHealthcare | one PDF per policy, discovered through the sitemap, with commercial and Medicare Advantage lines | crawled live |
| Blue Cross Blue Shield | 33 independent licensees, no national criteria endpoint | access registry, see below |
No BCBS licensee publishes criteria to an unauthenticated client. Ten state plans were audited in August 2026, in eight registry entries. Every URL in the previous version of the registry was dead, and the adapter reported that as a normal empty crawl. The registry now records what each licensee does when a crawler asks, dated, with the evidence: provider login, click-through terms gate, client-side rendering, 403, 404, 406. None of those is routed around. Clicking through a terms of use, spoofing a browser user-agent past a stated block, or authenticating as a provider would each get the data and each be the kind of thing that surfaces in a vendor security review of a PHI-adjacent product. They sit on the commercial data agreement track alongside the BAA and the clearinghouse contract. A licensee becoming crawlable is a registry edit, not a code change.
The same line was drawn twice more. The CMS coverage database (LCDs and NCDs) is reachable only through a click-through licence granting "personal use only … non-commercial uses", identical across the web UI, the bulk export and the API, so it was not accepted on a commercial product's behalf. That audit also found the product storing AMA-copyright CPT descriptors, which were removed; codes are stored, descriptions are not.
Maps extracted clinical data onto the correct PA form for that payer and procedure, resolving most-specific-first: a procedure-specific form, then the payer's catch-all, then the universal fax form.
Every field carries its provenance (from record, reformatted, from note, not found), because auto-population a clinician cannot audit is auto-population they have to re-read in full, which saves nothing.
It also refuses to help you fail. An invalid NPI (checked against the Luhn
check digit) is dropped rather than passed through. A required attestation the
note cannot support blocks submission with a 422 rather than going out
unchecked. When transmission is not enabled the response says so in as many
words and reports transmitted: false, never a silent no-op a clinician could
mistake for a filed request.
What it does not do yet is render or store anything. The submission result
carries an export_url of /exports/<id>.pdf, and no route serves that path;
no PDF is generated and no object is written. The mapping is real, the packet
is not.
Technical architecture
Epic / Cerner ──SMART on FHIR──► apps/web (Next.js 14, App Router)
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
Clinchec Scan Clinchec Live Clinchec Forms
spaCy + rules crawler + API field mapper
FastAPI FastAPI + Celery FastAPI
│ │ │
└────────┬────────┴─────────────────┘
▼
PostgreSQL 16 + pgvector · Redis
| Layer | Choice |
|---|---|
| Frontend | Next.js 14 (App Router), TypeScript, Tailwind, Radix UI |
| Backend | FastAPI, async throughout |
| Database | PostgreSQL 16 + pgvector (embedding search over payer criteria) |
| NLP | spaCy clinical pipeline; GPT-4o for justification drafting |
| Auth | SMART on FHIR OAuth 2.0 + PKCE (Epic / Cerner compatible) |
| Queue | Redis + Celery |
| Infra | AWS ECS Fargate, RDS, ElastiCache, S3, all in Terraform |
Four decisions worth calling out:
The score is a rule engine, not a model, on purpose. A supervised approval-prediction model needs thousands of submitted requests paired with payer outcomes, and Clinchec has to be useful before it can collect them. The rule engine is built to become that dataset: every score decomposes into named drivers with signed deltas, which is exactly the feature vector a model trains on. It also has a property a model would not: a clinician can read why the score is what it is and fix the note. An opaque 43% changes nobody's behaviour.
The metric claims only what it can support. It was called "approval likelihood" until the name was audited against the system behind it. Nothing in Clinchec has ever seen a payer decision, so there is no outcome data to calibrate a probability against. It is a documentation score, and the band labels describe the note rather than predicting a decision. Driver counterfactuals are stated in points, which is arithmetic over weights visible on the same screen, and never in percentage points of approval.
PHI surface is minimised by construction. Note text is never persisted, only a SHA-256 digest and the de-identified structured extraction. It is never logged. The GPT-4o drafter is prompted with the extracted fact sheet, never the raw note, which bounds what leaves the process to coded facts the payer will see anyway. Its output is checked against those facts before being returned; a draft containing a number we did not supply is discarded for the deterministic template.
Every response uses one envelope. { data, error, meta }, including
validation failures and unhandled exceptions. The Pydantic models are mirrored
as Zod schemas that the frontend validates against, so version skew between a
deployed service and a deployed frontend surfaces as a named error at the
boundary rather than as undefined rendering inside a clinical judgement. The
two directions of that mirror fail differently and both are now asserted in CI:
a missing enum value fails the whole parse, and a missing field is silently
stripped.
Deeper detail: docs/architecture.md,
docs/fhir-integration.md,
docs/handover.md,
docs/payer-adapter-guide.md in
clinchec-core-live (private).
Where it stands
Stated plainly, because the distinction matters more to an investor than a feature list.
aud binding, constant-time state comparison, an iss
allowlist, encrypted JWE session cookies and automatic token refresh. Not
mocked, not stubbed. It has never handshaken with a real EHR.OFFLINE_SEED_MODE=true serves
transcribed criteria; setting it false runs the live crawl. Aetna and UHC
have had hand-reviewed live runs. Anything added after them needs one before
it is trusted.export_url points at, and no route serves that
path. Real transmission additionally needs per-payer portal credentials, an
executed BAA, and an X12 278 clearinghouse contract. Those last three are
commercial gates, not engineering ones; the export renderer is not.SPACY_CLINICAL_MODEL with no code change.pa_requests is a table and nothing else. 001_init.sql creates it and
indexes it; no service reads or writes it. Forms mints a request id per
submission and returns it without persisting a row, so a PA request does not
outlive its response. The detail page validates the identifier and reports
that the record is not retrievable rather than rendering a fabricated one.apps/dashboard, the practice admin view, is a documented placeholder. It
gets built once there are submitted outcomes to analyse.SubmissionChannel.FHIR_CRD). Portal scraping is what works today.