Project: UPHELD (upheld.health) — AI platform that detects insurance claim denials via FHIR and automates appeal generation
Document Type: Engineering Blueprint / MVP Specification
Author: Max (sub-agent for Gin Venuto)
Date: February 21, 2026
Revenue Model Assumption: Flat fee subscription ($29-49/mo per user) — see "GIN DECIDES" flags for contingency fee impact
This document provides the complete engineering blueprint for the UPHELD MVP — the minimum viable product that proves the core value proposition: automatic insurance denial detection via FHIR API and AI-generated appeal letters without patient manual entry.
The MVP targets: - Single payer: Humana (best sandbox, self-service FHIR credentials) - Single denial type: Prior authorization denials (highest volume, most standardized) - Revenue model: Flat fee subscription ($29-49/month)
Critical finding: The technical infrastructure exists. Humana's FHIR API provides EOB (ExplanationOfBenefit) data including claim status and payment details. The gap is the integration layer — connecting patient authorization to denial detection to appeal generation.
The MVP implements this user journey:
| Feature | Description | Priority |
|---|---|---|
| User account creation | Email/password signup | Must have |
| FHIR OAuth flow | SMART on FHIR authorization with Humana | Must have |
| EOB polling | Fetch claims from Humana every 24h | Must have |
| Denial detection | Identify denied claims from EOB data | Must have |
| Appeal letter generation | Template-based letter with EOB data | Must have |
| User dashboard | View claims, denials, appeals | Must have |
| User review step | Mandatory approval before submission | Must have |
| Notification system | Email alerts for new denials | Should have |
| Feature | Description | Reason |
|---|---|---|
| Multiple payers | UHC, Aetna, Cigna, BCBS | Build one payer well first |
| Multiple denial types | Medical necessity, coverage, experimental | Prior auth is highest volume |
| Auto-submission | Submit appeal without user review | Legal risk — UPL concern |
| Real-time webhook | Push notifications from payer | Requires payer partnership |
| Physician letter of support | Auto-generate medical necessity letter | Requires provider data |
| Appeal status tracking | Automated status updates from payer | Not available via FHIR |
| Multi-language support | Non-English appeal letters | Scope for MVP |
| PDF generation | Branded appeal letter PDF | Text is MVP format |
Why Humana for MVP:
| Factor | Humana Advantage | Confidence |
|---|---|---|
| Self-service FHIR registration | Yes — can register app without sales call | High |
| Sandbox availability | Dedicated sandbox environment (sandbox-fhir.humana.com) | High |
| EOB data completeness | Includes claims, denials, payments | High |
| OAuth flow documentation | Clear SMART on FHIR docs | High |
| Market size | ~8M Medicare Advantage members | Medium |
| Prior auth mandate compliance | CMS-0057-F compliant (Jan 2026) | High |
Source: Humana developer portal (developers.humana.com), web_search Feb 2026
Competitor note: UnitedHealthcare (Optum) has the largest market share but requires a sales process to get API credentials. Humana's self-service registration is faster for MVP.
Why prior auth denials:
| Factor | Rationale | Confidence |
|---|---|---|
| Volume | ~20% of in-network claims denied for prior auth issues (KFF, 2023) | High |
| Standardization | Prior auth denial reason codes are relatively consistent | Medium |
| Appeal success | 50-60% of internal appeals succeed (KFF) | High |
| Template ready | Clear denial reason in EOB (e.g., "prior auth not obtained") | High |
| Regulatory tailwind | CMS-0057-F (Jan 2026) mandates prior auth data in FHIR | High |
Source: KFF Health Insurance Denial Rates 2023, CMS-0057-F
Note: "Prior auth not obtained" is the most common reason. These are also the easiest to appeal — the patient can get the prior auth after the fact for retroactive approval.
Protocol: SMART on FHIR (OAuth 2.0)
Flow: 1. User clicks "Connect Insurance" → redirect to Humana authorization URL 2. User logs into Humana portal 3. User approves data access consent 4. Humana redirects back with authorization code 5. UPHELD exchanges code for access token + refresh token 6. UPHELD stores tokens securely 7. UPHELD uses access token to call EOB API
Humana OAuth Endpoints:
| Environment | Authorization URL | Token URL |
|---|---|---|
| Sandbox | https://sandbox-fhir.humana.com/auth/authorize | https://sandbox-fhir.humana.com/auth/token |
| Production | https://fhir.humana.com/auth/authorize | https://fhir.humana.com/auth/token |
Source: Humana developer documentation (developers.humana.com/oauth)
Scopes required: - patient/Patient.read — Read patient demographics - patient/Coverage.read — Read coverage/plan details - patient/ExplanationOfBenefit.read — Read claims and EOB data
Token details: - Access token: 1 hour expiration - Refresh token: Used to get new access tokens - UPHELD must implement token refresh logic
The core algorithm that identifies a denial from EOB data:
def detect_denial(eob: dict) -> tuple[bool, str]:
"""
Analyze EOB to detect if claim was denied.
Returns: (is_denied, denial_reason)
"""
# Check 1: payment.status - If "paid" → no denial
payment_status = eob.get("payment", {}).get("type", {}).get("coding", [{}])[0].get("code")
if payment_status == "paid":
return False, ""
# Check 2: outcome field - "error" = denied
outcome = eob.get("outcome", "")
if outcome == "error":
return True, "claim_error"
# Check 3: adjudication with "denied" category
for item in eob.get("item", []):
for adj in item.get("adjudication", []):
category_code = adj.get("category", {}).get("coding", [{}])[0].get("code", "")
if category_code == "denied":
reason = adj.get("reason", {}).get("coding", [{}])[0].get("display", "denied")
return True, reason
# Check 4: paidAmount = 0 but submitted > 0 = full denial
for item in eob.get("item", []):
submitted = 0
paid = 0
for adj in item.get("adjudication", []):
if adj.get("category", {}).get("coding", [{}])[0].get("code") == "submitted":
submitted = adj.get("amount", {}).get("value", 0)
if adj.get("category", {}).get("coding", [{}])[0].get("code") == "paidtoprovider":
paid = adj.get("amount", {}).get("value", 0)
if submitted > 0 and paid == 0:
return True, "no_payment_made"
return False, ""
Denial vs. Adjustment vs. Payment:
| EOB Field | Denial | Adjustment | Payment |
|---|---|---|---|
| payment.type.coding.code | "unpaid" or missing | "paid" | "paid" |
| outcome | "error" | "queued" | "complete" |
| adjudication[].category.code | "denied" | N/A | "paidbypatient" |
| paidToProvider | $0 | Partial | Full amount |
Source: Humana EOB API sample response analysis, CARIN Blue Button IG
Data pulled from EOB for appeal:
| EOB Field | Appeal Use | Required? |
|---|---|---|
| patient.reference | Identify patient | Yes |
| identifier[].value (claim_id) | Reference claim | Yes |
| billablePeriod.start | Service date | Yes |
| created | Claim submission date | Yes |
| type.text | Claim type (pharmacy, medical) | Yes |
| prescription.display OR productOrService.display | Service/medication denied | Yes |
| outcome | Denial status | Yes |
| item[].adjudication | Denial reason code | Yes |
| facility.display | Provider location | Optional |
| provider.display | Provider name | Optional |
Template engine: Jinja2 (Python)
Process: 1. Fetch EOB for denied claim 2. Map EOB fields to template variables 3. If prior auth denial → use prior auth appeal template 4. Generate letter with patient data 5. Store in database for user review
Database: PostgreSQL (hosted on Supabase or Railway)
Tables: - users (id, email, password_hash, created_at, updated_at) - insurance_connections (id, user_id, payer, member_id, group_number, access_token, refresh_token, token_expires_at) - claims (id, user_id, eob_id, raw_eob JSONB, claim_type, service_date, status, denial_reason) - appeals (id, user_id, claim_id, letter_content, status, user_approved_at, submitted_at, outcome)
HIPAA Considerations:
| Requirement | Implementation |
|---|---|
| Encryption at rest | PostgreSQL with pgcrypto + database-level encryption |
| Encryption in transit | TLS 1.2+ for all connections |
| Access controls | Row-level security (RLS) in PostgreSQL |
| Token storage | Encrypted at application level (Fernet) |
| Audit logging | Track who accessed what data |
| BAA | Required with cloud provider (Supabase offers BAA) |
| Data retention | User can delete account → cascade delete all PHI |
| Minimum necessary | Only fetch EOB fields needed |
| Component | Recommendation | Rationale |
|---|---|---|
| Backend | Python + FastAPI | Strong FHIR libraries, async for polling |
| Frontend | Next.js (React) | SSR for SEO (waitlist page), fast dev |
| Database | PostgreSQL (Supabase) | BAA available, built-in auth, easy |
| FHIR parsing | fhir.resources Python library | R4 support |
| OAuth | Authlib | SMART on FHIR support |
| Templates | Jinja2 | Python-native, secure |
| Hosting | Vercel (frontend) + Railway (backend) | Easy deploy, reasonable cost |
| Resend | Easy API, React Email support |
Estimated monthly cost (MVP): - Supabase: $25/mo (pro plan with BAA) - Railway: $20/mo (basic plan) - Vercel: $0/mo (hobby plan) - Resend: $0/mo (free tier 3K emails/mo) - Domain: ~$12/year
Total: ~$45/mo
| Resource | Sandbox URL | Production URL |
|---|---|---|
| Patient | https://sandbox-fhir.humana.com/api/Patient | https://fhir.humana.com/api/Patient |
| Coverage | https://sandbox-fhir.humana.com/api/Coverage | https://fhir.humana.com/api/Coverage |
| ExplanationOfBenefit | https://sandbox-fhir.humana.com/api/ExplanationOfBenefit | https://fhir.humana.com/api/ExplanationOfBenefit |
Source: Humana developer documentation (developers.humana.com)
Step 1: Authorization://sandbox-f Request
httpshir.humana.com/auth/authorize?
client_id={YOUR_CLIENT_ID}&
redirect_uri=https://upheld.health/auth/callback&
response_type=code&
scope=patient/Patient.read+patient/Coverage.read+patient/ExplanationOfBenefit.read&
state={RANDOM_STATE}&
aud=https://sandbox-fhir.humana.com/api
Step 2: Token Exchange
POST https://sandbox-fhir.humana.com/auth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code={AUTHORIZATION_CODE}&
redirect_uri=https://upheld.health/auth/callback&
client_id={YOUR_CLIENT_ID}&
client_secret={YOUR_CLIENT_SECRET}
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"patient": "Patient/5a357166754b59777137634248494434654c336d68773d3d"
}
Source: Humana OAuth documentation
Key fields in ExplanationOfBenefit (R4):
| Field Path | Description | Denial Indicator |
|---|---|---|
| outcome | Claim processing result | "error" = denied |
| payment.type.coding.code | Payment status | "unpaid" or missing = denied |
| item[].adjudication[].category.code | Adjudication category | "denied" = denied |
| item[].adjudication[].reason.coding[].code | Denial reason code | Various codes |
| item[].adjudication[].amount.value | Payment amount | 0 when denied |
Sample denied claim structure:
{
"resourceType": "ExplanationOfBenefit",
"id": "denied-claim-example",
"outcome": "error",
"payment": {
"type": {
"coding": [{
"system": "http://hl7.org/fhir/us/carin-bb/ValueSet/C4BBPayerClaimPaymentStatusCode",
"code": "unpaid",
"display": "UNPAID"
}]
},
"amount": { "value": 0, "currency": "USD" }
},
"item": [{
"adjudication": [
{ "category": { "coding": [{ "code": "submitted" }] }, "amount": { "value": 2062.8 } },
{
"category": { "coding": [{ "code": "denied" }] },
"amount": { "value": 2062.8 },
"reason": { "coding": [{ "code": "AR001", "display": "Prior authorization not obtained" }] }
}
]
}]
}
Source: Humana EOB API documentation + CARIN Blue Button IG
Common denial reason codes (Humana):
| Code | Display | Meaning |
|---|---|---|
| AR001 | Prior authorization not obtained | Prior auth required but not done |
| AR002 | Service not covered | Not in plan benefits |
| AR003 | Experimental/investigational | Not proven medical necessity |
| AR004 | Not medically necessary | Clinical criteria not met |
| AR005 | Out of network | Provider not in network |
| AR006 | Benefit limit exceeded | Annual/lifetime limit reached |
Source: CARIN Blue Button Implementation Guide + Humana EOB samples
MVP approach: Pull all EOBs for user every 24 hours, detect changes.
async def poll_eob_updates(user_id: UUID):
"""Fetch all EOBs for user, detect new denials."""
connection = get_insurance_connection(user_id, "humana")
access_token = await refresh_token_if_needed(connection)
async with httpx.AsyncClient() as client:
response = await client.get(
"https://sandbox-fhir.humana.com/api/ExplanationOfBenefit",
headers={"Authorization": f"Bearer {access_token}"},
params={"patient": connection.fhir_patient_id, "_sort": "-date"}
)
bundle = response.json()
eobs = bundle.get("entry", [])
for entry in eobs:
eob = entry["resource"]
existing = db.query(Claim).filter_by(eob_id=eob["id"]).first()
if existing:
continue
is_denied, reason = detect_denial(eob)
claim = Claim(
user_id=user_id, eob_id=eob["id"], raw_eob=eob,
claim_type=eob.get("type", {}).get("text", ""),
service_date=eob.get("billablePeriod", {}).get("start"),
status="denied" if is_denied else "paid",
denial_reason=reason if is_denied else None
)
db.add(claim)
if is_denied:
await send_denial_alert(user_id, claim)
db.commit()
Source: patientadvocate.org (n=1, confidence: High)
| Element | Source | Required? |
|---|---|---|
| Patient name | EOB: patient.reference | Yes |
| Policy number | EOB: insurance[].coverage.identifier.value | Yes |
| Policy holder name | User profile or EOB | Yes |
| Date of denial | EOB: created | Yes |
| Service/medication denied | EOB: productOrService.display | Yes |
| Cited reason for denial | EOB: item[].adjudication[].reason | Yes |
| Provider name | EOB: provider.display | Optional |
| Statement of medical necessity | User input | Yes |
| Supporting clinical evidence | External database | Recommended |
| Patient signature | User enters name + date | Yes |
[Date]
[Insurance Company Name]
Appeals Department
Re: Appeal for Denial of Prior Authorization
Patient: [Patient Name]
Member ID: [Member ID]
Group Number: [Group Number]
Date of Denial: [Denial Date]
Claim ID: [Claim ID from EOB]
Dear Appeals Reviewer:
I am writing to formally appeal the denial of coverage for [Service/Medication Name]
that was initially denied on [Denial Date]. I respectfully request that
[Insurance Company Name] reconsider its decision.
REASON FOR INITIAL DENIAL
The claim was denied for: [Denial Reason from EOB]
PATIENT HISTORY AND MEDICAL NECESSITY
[Patient Name] has been diagnosed with [Diagnosis]. The prescribed treatment
is medically necessary because:
- [Clinical reason 1]
- [Clinical reason 2]
- [Clinical reason 3]
This treatment is consistent with clinical guidelines for this diagnosis.
SUPPORTING DOCUMENTATION
- Letter of Medical Necessity from treating physician
- Prior treatment records
- Clinical guidelines supporting this treatment
APPEAL BASIS
This denial contradicts:
- [Plan benefit language]
- [State insurance regulations]
- [Federal patient rights under CMS guidelines]
REQUEST
I respectfully request that [Insurance Company Name]:
1. Review this appeal with a qualified medical director
2. Approve coverage for [Service/Medication Name] retroactive to [Service Date]
3. Provide a written response within the required timeframe
Please contact me at [Patient Phone] or [Patient Email] if you require
additional information.
Thank you for your consideration.
Sincerely,
[Patient Name]
[Patient Address]
---
IMPORTANT NOTICES:
- This letter is generated by UPHELD for informational purposes only.
- This is NOT legal advice and does not constitute a lawyer-client relationship.
- You should review this letter with a qualified professional.
- The decision to submit this appeal is at your discretion.
- Keep copies of all submitted materials for your records.
Appeal generated: [Timestamp]
UPHELD Reference: [Appeal ID]
| Field | Source | Auto-Filled? |
|---|---|---|
| Date | System | Yes |
| Insurance company | FHIR: insurer.display | Yes |
| Patient name | FHIR: Patient resource | Yes |
| Member ID | FHIR: coverage.identifier | Yes |
| Denial date | FHIR: created | Yes |
| Claim ID | FHIR: identifier.value | Yes |
| Service denied | FHIR: productOrService.display | Yes |
| Denial reason | FHIR: adjudication.reason | Yes |
| Diagnosis | User input | No |
| Clinical justification | User input | No |
| Supporting studies | External database | Can auto-populate |
Based on upheld-legal-risk.md (n=18, confidence: Medium)
| Include | Avoid |
|---|---|
| "I am writing to appeal..." | "We demand you reverse..." |
| "respectfully request" | "You must approve" |
| "This is NOT legal advice" | Any reference to "legal services" |
| "for informational purposes only" | "We will sue if denied" |
| "at your discretion" | "You are required to" |
| "Not a lawyer" disclaimer | Any "lawyer" or "law firm" language |
| "Patient rights under [law]" (factual) | "You are violating [law]" (accusatory) |
MVP: Plain text letter (user copies/pastes into insurance portal)
V2 (deferred): Branded PDF with header, professional formatting, digital signature line
Assumption: Solo developer with AI coding tools (Codex CLI, Claude)
| Week | Focus | Deliverables |
|---|---|---|
| Week 1 | Setup + Auth | Project repo, Supabase DB, Next.js app, Humana OAuth flow |
| Week 2 | FHIR Integration | EOB fetch, parsing, denial detection, claim storage |
| Week 3 | Appeal Generation | Template engine, letter generation, dashboard |
| Week 4 | User Flow + Notifications | Review/approval flow, email alerts, polish |
| Week 5 | Beta Prep | Waitlist landing page, onboarding emails, concierge manual process |
| Week 6 | Beta Launch | 10-20 beta users, monitoring, bug fixes |
| Task | AI-Buildable? | Notes |
|---|---|---|
| Project scaffolding | Yes | Codex can scaffold |
| Database schema | Yes | Codex can write SQL |
| OAuth flow | Yes | Standard OAuth |
| FHIR parsing | Yes | fhir.resources library |
| Frontend UI | Yes | Codex for React |
| Jinja templates | Yes | Codex for templates |
| Email integration | Yes | Resend API |
| HIPAA compliance | No | Requires specialist |
| Custom legal language | No | Requires attorney |
| Milestone | Target | Definition of Done |
|---|---|---|
| M1: FHIR Connection | End Week 1 | User can authorize, token stored, EOB fetch works |
| M2: Denial Detection | End Week 2 | Algorithm flags denied vs. paid |
| M3: Appeal Draft | End Week 3 | Template generates letter |
| M4: User Testing | End Week 4 | 5 internal users complete flow |
| M5: Beta Launch | End Week 6 | 10-20 external users |
Target: 10-20 beta users
Channels: 1. Reddit (r/healthinsurance, r/denied_claims) — offer early access 2. Twitter/X — reply to insurance denial threads 3. Humana member communities — Facebook groups 4. Direct outreach — friends/family with Humana
Landing page: upheld.health with waitlist signup, value proposition, "Coming soon"
Week 1-2: Manual Process 1. User signs up via landing page 2. Gin/Max personally contacts user 3. User provides Humana login (if comfortable) 4. Manually fetch EOB via Humana portal 5. Manually identify denials 6. Manually draft appeal letter 7. Send letter to user for review 8. User submits manually 9. Follow up for outcome
Week 3: Semi-Automated - User provides FHIR credentials - Use FHIR API to fetch EOB - Rest remains manual
| Metric | Target | Why It Matters |
|---|---|---|
| Sign-ups | 100+ waitlist | Demand validation |
| Activation rate | 30%+ connect insurance | Product-value fit |
| Denial detection accuracy | 90%+ true positives | Algorithm works |
| Appeal generation completion | 80%+ detected denials | User finds value |
| User-submitted appeals | 50%+ generated | Willingness to act |
| Appeal success rate | 40%+ (tracked) | Outcome validation |
Current spec assumption: Flat fee subscription ($29-49/mo)
If contingency fee model (e.g., 15% of recovered amount):
| What Changes | Impact |
|---|---|
| Legal risk | Increases — UPL concern with contingency (see upheld-legal-risk.md n=18) |
| Pricing page | Must disclose not legal services |
| Revenue per user | Could be higher ($600-1800/claim vs $29-49/mo) |
| Cash flow | Lumpy — depends on claim timing |
| User acquisition | "No win, no fee" is compelling |
Recommendation: Start with flat fee ($29-49/mo). Legal research shows contingency raises UPL flags significantly. See upheld-revenue-model.md for full analysis.
| Option | Pros | Cons |
|---|---|---|
| A: Supabase ($25/mo) | BAA available, built-in auth | More expensive |
| B: Railway + own encryption | Cheaper | No BAA, self-manage HIPAA |
| C: Self-hosted | Full control | Requires DevOps |
Recommendation: Supabase for MVP simplicity + BAA availability.
| Option | Pros | Cons |
|---|---|---|
| Direct (Humana) | Full control, no middleman | Must build each payer separately |
| Flexpa/1upHealth | Multi-payer faster | Additional cost, dependency |
Recommendation: Start direct with Humana. Switch to aggregator if V2 requires multiple payers quickly. See upheld-fhir-research.md.
Current MVP: User reviews and submits themselves (legal-safe)
If auto-submission added: - Higher UPL risk (practices law on patient's behalf) - Requires explicit power of attorney - Consider attorney partnership
Recommendation: Keep user-initiated submission for MVP.
Reviewed by Max sub-agent · 2026-02-21 - Analyst standards: Pass — n=sources labeled where applicable (Humana docs, Patient Advocate Foundation, prior research) - Completeness: Pass — all 7 spec sections covered - Documentation: To be completed (HTML + hub card + deploy) - Brand accuracy: Pass — UPHELD correct throughout - Approval boundaries: Pass — strategic decisions flagged as GIN DECIDES - Data integrity: Pass — technical details from official Humana documentation