Skip to content
AskFlorence
Main Navigation ArchitectureFlorence AIAgentsMembersAgent PlatformValidationInfrastructure

Appearance

Sidebar Navigation

Overview

Home

Glossary

System Architecture

Consumer & Agent Flow

Florence AI

Overview

Principles

Runtime

Tool surface

Adding a tool

Tool registry

Knowledge: SBC scenarios & CSR

Voice

Evals & observability

Provider risk & portability

Outage playbook

Roadmap

Build plan

Agents

Overview

Workflows & pain points

Members

Overview

Medicaid coverage gap

Carriers

Overview

Marketplaces

Overview

Agency

Overview

Regulations

Overview

Agent Platform

Overview

Auth Architecture

MongoDB Permissioning

Compliance Model

Data Models

Data Sources

Overview

CMS Marketplace API

CMS dependency map

PUF Data

State Subsidies

SBE Ingestion Playbook

SBE State Watchouts + Decisions

CA Phase C/D Playbook

NY Phase C/D Playbook

Validation

Overview

Methodology

APTC Formula

California 2026

New York 2026

CAPS Formula

Scenario Results

Infrastructure

Account Inventory

AWS Setup Runbook

AWS Organizations

CloudTrail

GuardDuty

Security Hub

Config

CloudFront + WAFv2

Data sources & ingest

Phase 4 DNS

Change Log

Vulnerability Management

MongoDB Setup

Access Control

Data Classification

Documentation Hosting

Post-deploy Smoke

Development

Preflight (local CI mirror)

Testing strategy

Compliance

Overview (auditor entry point)

SOC 2 Control Mapping

HIPAA Control Mapping

CMS EDE Appendix A Mapping

Risk Assessment

Encryption Policy

Data Retention Policy

Privacy Impact Assessment

Consent Capture & Versioning

Incident Response Plan

Access Control Policy

Marketing vs. Portal Analytics

Vendor / Subprocessor Register

Dependency Vulnerability Policy

BAA / Compliance Evidence

Compliance-Automation Integration

Compliance-Automation Vendor Evaluation

Penetration Test Reports

Architecture

Portal entry handoff

Mobile app strategy

Deferred architecture decisions

Session cookie architecture

Share flows

Decisions (ADRs)

Index

0001 — Atlas project isolation

0002 — Append-only audit log

0003 — Narrow-scoped Mongo users

0004 — Cross-cluster Atlas PrivateLink

0005 — Delayed-job architecture

0006 — Mongo user simplification

0007 — Terraform owns ECS task def

0008 — E2E testing strategy

0009 — Self-hosted analytics + observability (superseded)

0010 — PostHog HIPAA Cloud (supersedes 0009)

Runbooks

Security Incident Response

Break-Glass Root Login

Onboard Team Member

Offboard Team Member

Atlas user provisioning

Deploy via Terraform (ENG-277)

Rollback via Terraform (ENG-277)

S3 data bucket migration (planned Phase 11)

Access Reviews

2026-Q2 Review

Session log

Index

2026-04-23 — Phase 10 DNS cutover

2026-04-22 — Phase 8 prod AWS mirror

2026-04-22 — Phase 7 Atlas VPC peering

2026-04-22 — Phase 6 CloudFront + WAF

2026-04-21 — Phase 5 staging go-live

2026-04-17 — Atlas staging

Briefs

Index

Member portal plan (ENG-187)

2026-04-16/17 handoff

2026-04-17 Atlas handoff

System briefing (2026-04-17)

Creative AdBundance proposal brief

Creative AdBundance analytics brief

ElevenLabs RN integration research

Policies

Overview

On this page

CMS Marketplace API dependency map ​

This page documents where AskFlorence depends on the live CMS Marketplace API today vs where we serve from our own MongoDB. It also captures the forward plan for retiring remaining CMS dependencies if we ever need to (CMS outage, rate-limit pressure, performance issue, or strategic independence).

Bottom line as of 2026-05-14: We are not technically dependent on CMS for any compute in the consumer flow. We have the data and math in our system for everything except a few narrow edge cases. Remaining CMS calls are historical inertia from the pre-owned-data migration. Until we hit an issue, it's not worth the time to migrate.

Use this page as the reference when planning route changes, rate-limit posture, or CMS-outage incident response.

Current state — what calls CMS today ​

Traced end-to-end through the standard member smoke flow: home → calculator → takeover → /plans → drug + provider search → /plans/[planId].

Routes that hit CMS ​

RouteWhenWhat CMS providesWhy we still call it
POST /api/eligibilityCalculator submit (federal states only)APTC, CSR tier, Medicaid flag, hardship exemption, coverage gap flagHistorical. We have all the underlying data + math in our system. See "Migration plan: federal eligibility" below.
POST /api/drugs/autocompleteEvery keystroke (3+ chars) on the /plans drug search inputDrug names → RxCUI codesHistorical + missing index. We have formularies_staging (RxCUI-keyed) but no name-indexed search built on top yet.
POST /api/drugs/coveredWhen a drug is selected, batched across all visible plans"Covered / NotCovered / PartialCovered / GenericCovered" + drug_tier per (rxcui, plan_id)CMS primary, our DB gap-fill. lookupStagingDrugTiers() reads formularies_staging only when CMS returns "Covered without drug_tier". Our data is actually richer than CMS for tier info.
POST /api/providers/autocompleteEvery keystroke (3+ chars) on the /plans provider search input. Fans out 3 calls (Individual + Facility + Group)Provider names → NPI codesHistorical + missing index. We have providers_staging (NPI-keyed) but no name-indexed search built on top yet.
POST /api/providers/coveredWhen a provider is selected, batched across all visible plans"Covered / NotCovered / PartialCovered" + network_tier per (npi, plan_id)CMS primary, our DB gap-fill. lookupStagingProviderNetworks() reads providers_staging only when CMS returns "Covered without network_tier".
GET /api/counties?zip=... (fallback only)Only when a zip isn't in our zip_county collectionCounty FIPS lookup → SBE-redirect detection for state-based marketplace ZIPsOperational backstop. Mostly catches SBE-state ZIPs missing from our DB (e.g., a CA ZIP we haven't ingested). 99%+ of legitimate federal-state traffic stays in our DB.
POST /api/plans (CMS branch — effectively dead)Only when place.state is not in OWNED_DATA_STATES (the ~20 SBE states)Plan searchDead code path. Users for SBE states are caught by /api/counties sbeRedirect first; this branch is never reached in practice.

Routes that DON'T hit CMS (served from our DB) ​

RouteServed byNotes
GET /api/counties (happy path)zip_county MongoDB collection100% of federal-30 + NY traffic. Multi-county ZIPs handled; whole-ZIP SBE redirect handled; cross-state border ZIPs handled.
POST /api/eligibility (NY only)calculateNyEligibility() in src/lib/owned-plans.tsNY uses community rating + Essential Plan, our own math. This is the template proving we can do the same for federal states.
POST /api/plans (NY + 30 federal states)searchOwnedPlans() over plans MongoDB collectionCovers OWNED_DATA_STATES = NY + AK, AL, AR, AZ, DE, FL, HI, IA, IN, KS, LA, MI, MO, MS, MT, NC, ND, NE, NH, OH, OK, OR, SC, SD, TN, TX, UT, WI, WV, WY. Pricing math audit-locked vs CMS at tier-1 through tier-5.
GET /plans/[planId] (server fetch of extras)fetchPlanDetailExtras() reads puf.sbcScenarios + puf.qualityRating from plans collectionPhase A PUF ingest. No CMS call.
All write paths (/api/waitlist, /api/agents/discovery*, /api/unsubscribe, /api/enroll/applications)MongoDB (agent_waitlist_submissions, agent_survey_responses, etc.)No CMS involvement.

Per-flow CMS call count (typical Utah Medicaid scenario, member smoke flow) ​

StepCMS callsMongo queries
Counties lookup01
Eligibility (UT → federal → CMS)10
Plans (UT → owned data)01
Takeover render00
/plans page loads (re-runs eligibility + plans)11
Doctor autocomplete ("Tyler" → 5 keystrokes after 3-char threshold)15 (5 × 3 fan-out)0
Doctor coverage check (16 plans, 1 doctor)20 (or fallback only)
Drug autocomplete ("Synthroid")60
Drug coverage check (16 plans, 1 drug)20 (or fallback only)
2nd drug "Lipitor" autocomplete + coverage6 + 2 = 80
3rd drug "Ozempic" autocomplete + coverage6 + 2 = 80
Plan detail page load (extras + re-run pipeline)11
Total per smoke flow~43 CMS calls~4 Mongo queries

For NY (owned-data state), the federal eligibility CMS call drops, taking it to ~42 CMS calls. The dominant CMS load is doctor + drug search, not eligibility or plans.

What is NOT in CMS (and therefore CMS-independent already) ​

  • Plan pricing (premium, deductible, MOOP, copays, coinsurance) — 100% from our MongoDB plans collection. Audited byte-for-byte against CMS at tier-1 through tier-5.
  • CSR variants (94/87/73 cost-sharing adjustments) — from puf.csrVariants on each plan doc.
  • SBC scenarios (Having a Baby / Diabetes / Simple Fracture cost-share breakdowns) — from puf.sbcScenarios.
  • Quality rating (QRS star ratings) — from puf.qualityRating.
  • Plan documents (SBC URL, formulary URL, provider directory URL, brochure, enrollment) — from puf.urls.
  • Full benefit table (40+ benefits per plan with cost-share) — from puf.benefitDetails.
  • Rating areas + age curves — from premiumsByRatingArea + ageRatesByArea.

Why we still call CMS (the honest answer) ​

Two reasons across the board, both historical:

  1. Commit 330871e order of operations moved federal plans from CMS to our MongoDB. Eligibility, drugs, and providers were left on CMS to limit migration scope — plan pricing was the audit-locked part that needed careful proof of byte-for-byte parity with CMS first. Eligibility math is conceptually downstream of pricing math (APTC formula needs SLCSP, which comes from premium data we now own). Drug/provider data was on CMS because the LARK pipeline (ENG-236) hadn't shipped delta-aware refresh yet when those routes were built.

  2. "CMS as ground truth" psychology — calling CMS feels safer because CMS is the official ACA reference. But our pricing tier audit harness already proves we match CMS byte-for-byte on the data layer; eligibility math is a simpler derivation from the same underlying data.

Migration plans (do NOT execute now — reference only) ​

These are proposed plans to execute if and only if we hit a real issue: CMS outage during OEP, CMS rate-limit pressure, performance regression, strategic competitor pressure, or compliance / portability requirement. Until then, leaving the CMS calls in place is the right call (not busy work for no needle-moving outcome).

When the trigger fires, file a Linear issue and use the relevant plan below as the starting point.

Migration plan: federal eligibility ​

Trigger to execute: CMS eligibility endpoint outage / rate-limit / >500ms P95 latency / strategic decision to eliminate CMS dependency.

Scope: Replace src/app/api/eligibility/route.ts federal-state branch (lines 161-207) with an owned calculateFederalEligibility() helper mirroring the existing calculateNyEligibility() in src/lib/owned-plans.ts.

What we already have:

  • calculateFpl() in src/lib/csr.ts — FPL % from income + household size.
  • deriveCsrTierFromFpl() in src/lib/csr.ts — 94/87/73/zero band lookup per 45 CFR 155.305(g).
  • calculateMedicaidThreshold() in src/lib/csr.ts — currently uniform 138% FPL.
  • findSlcspPremium() in src/lib/utils.ts — Second Lowest Cost Silver Plan from our plans collection.
  • calculateAptc() in src/lib/utils.ts — max(0, SLCSP - expectedContribution(FPL%) × income).

What we'd need to add:

  • State Medicaid expansion table: 10 non-expansion states with different thresholds for parents vs childless adults. KFF publishes this annually. ~100 lines of constants.
  • Hardship exemption: narrow edge case, currently not surfaced in our UI — can skip entirely.
  • Coverage gap flag: derived from state-expansion-status + FPL band — pure derivation, no new data needed.

Effort: 1-2 days.

  • Half day on code (collapse route from 216 → ~120 lines).
  • Half day on state Medicaid table + verification.
  • Half day on regression test (50-100 scenarios across 30 federal states, byte-for-byte vs CMS output).

Reversibility: Trivial. One file revert.

Acceptance criteria template:

  • [ ] calculateFederalEligibility() returns identical {aptc, csr, is_medicaid_chip, in_coverage_gap} to CMS for 50+ regression scenarios.
  • [ ] State Medicaid threshold table covers all 10 non-expansion states with parent/childless-adult variants where applicable.
  • [ ] Calculator submit + /plans + /plans/[planId] all return correct subsidized prices for a federal Medicaid scenario.
  • [ ] Calculator regression diff (scripts/audit/calculator-baseline-diff.ts) ZERO DIFFS on all 12 scenarios.

Migration plan: drug + provider autocomplete ​

Trigger to execute: CMS autocomplete rate-limit pressure / want richer name-search / strategic decision.

Scope: Build name-indexed search over formularies_staging + providers_staging, rewrite /api/drugs/autocomplete + /api/providers/autocomplete to query MongoDB first.

What we already have:

  • formularies_staging collection — RxCUI + plan_id + drug_tier + step_therapy + prior_auth + quantity_limit per row. Keyed by RxCUI.
  • providers_staging collection — NPI + plan_id + network_tier per row. Keyed by NPI.

What we'd need to add:

  • Drug name index: denormalize the RxNorm name (or brand name) into formularies_staging and create a MongoDB Atlas Search or $text index over name.
  • Provider name index: denormalize the NPPES-derived display name into providers_staging and create a similar index.
  • Result shape parity: ensure the response shape matches what useDrugAutocomplete() + useDoctorAutocomplete() expect today (no client-side changes).

Effort: 2-3 days (most of it on the index build + verification that names roll up cleanly across issuers).

Reversibility: Trivial. Routes can be A/B'd via env var.

Migration plan: drug + provider coverage (/covered) ​

Trigger to execute: After autocomplete migration. CMS /covered rate-limit pressure or strategic decision.

Scope: Flip /api/drugs/covered + /api/providers/covered to query our staging tables first, CMS only as fallback for the narrow gap case (or eliminate CMS entirely if our data confidence is high enough).

What we already have:

  • lookupStagingDrugTiers() + lookupStagingProviderNetworks() — already implemented, currently used as gap-fill.
  • Both staging tables are production-confidence after LARK pipeline (ENG-236) shipped delta-aware refresh.

What we'd need to verify:

  • Coverage of staging tables vs CMS — are there plan + drug combinations where CMS knows the answer but our staging table doesn't?
  • Issuer §1311 file completeness — some issuers may file partial data.

Effort: 2 days (most of it on coverage gap audit, not code).

Reversibility: Trivial. Same A/B-via-env-var pattern.

Migration plan: /api/counties CMS fallback removal ​

Trigger to execute: When zip_county collection is verified to cover 100% of valid US ZIPs (including all SBE-state ZIPs).

Scope: Remove lookupZipViaCms() fallback path from /api/counties. Pure code cleanup.

Effort: 1 hour + ZIP coverage audit (already tracked under tier-1 + tier-1.5 audit harness).

Operational guidance ​

CMS outage incident response ​

If CMS is down (outage / 5xx / >5s latency):

  • Federal eligibility breaks (all federal calculator submits fail).
  • Drug + provider autocomplete breaks (no results return).
  • Drug + provider coverage check breaks (coverage filters return empty).
  • Plan pricing display continues working (served from our DB).
  • NY traffic continues working (no CMS dependency).
  • Counties resolution continues working for happy-path ZIPs (CMS fallback used only for non-ingested SBE ZIPs).

Mitigation: until federal eligibility is migrated, there is no full-flow mitigation. NY users are unaffected. Federal users see calculator submit errors. Consider posting a banner: "Federal subsidy calculator temporarily unavailable while CMS systems recover."

When federal eligibility is migrated (per plan above), the only remaining CMS surface is the drug/provider routes. Outage impact narrows to "doctor + drug search temporarily unavailable" — calculator + plan pricing + plan list all keep working.

Route-to-avoid guidance (for future engineers) ​

When adding a new feature or surface:

  • DO NOT add new direct calls to cms-api.ts helpers (drugsAutocomplete, providersAutocomplete, drugsCovered, providersCovered, fetchEligibility).
  • DO consume the existing API routes (/api/drugs/*, /api/providers/*, /api/eligibility) — they're the centralized abstraction. If we ever flip them to DB-first, every caller upgrades for free.
  • DO prefer our DB collections directly when the data is in plans, formularies_staging, providers_staging, or zip_county.

What the audit harness covers (and doesn't) ​

  • scripts/audit/tier-1 through tier-5 — verify our plan pricing data matches CMS byte-for-byte. Does NOT verify eligibility math or coverage data.
  • If we migrate federal eligibility, we'll need a tier-6-eligibility.js audit harness as the regression gate.
  • If we migrate drug/provider coverage, we'll need similar tier-N harnesses against CMS.

Cross-references ​

  • docs/data-sources/cms-api.md — CMS Marketplace API reference.
  • docs/data-sources/puf-data.md — PUF ingest pipeline (the source of all puf.* fields).
  • src/lib/owned-plans.ts — OWNED_DATA_STATES, calculateNyEligibility, searchOwnedPlans.
  • src/lib/csr.ts — FPL + CSR tier math.
  • src/lib/utils.ts — SLCSP + APTC calculation.
  • src/lib/fetch-plans.ts — Unified eligibility + plans pipeline used by both useCalculator() and PlansMarketplace.
  • ADR 0005 — Delayed jobs architecture (CMS is not used for delayed jobs).
Pager
Previous pageCMS Marketplace API
Next pagePUF Data

AskFlorence Internal Documentation. Not for public distribution.

AskFlorence

Internal Documentation

Access restricted. Not for public distribution.