How we classify HSA/FSA eligibility at scale
Author: Adam Hood
Published: June 18, 2026

Summary
How our classification pipeline scores products for HSA/FSA eligibility, from rule-based pre-filters to a confidence-weighted model.
Truemed is a telehealth and payments company, and one of the hardest problems we run into is deceptively simple to state: given a product, could it be eligible for HSA/FSA reimbursement? The honest answer is almost never a clean yes or no. A resistance band could be exercise equipment or a general fitness accessory. A supplement could support a documented condition or could just be a vitamin someone takes out of habit. Our classification pipeline exists to turn that ambiguity into a defensible, auditable decision at the volume our merchant integrations require.
The classification problem
At a small scale you could staff a team of reviewers and have them read product descriptions all day. That doesn't work once you're classifying millions of SKUs across hundreds of merchant catalogs, many of which change their titles, descriptions, and categories weekly. We needed a pipeline that produces consistent output, degrades gracefully when the input is messy, and never silently guesses when it isn't confident.
The output of the pipeline is not a blanket claim that a product is eligible. It's a probability that a given product, in the context of a customer's stated condition, may qualify for reimbursement under a Letter of Medical Necessity. That distinction matters enormously for both the engineering design and the copy we show customers.
Pipeline architecture
The pipeline runs in three stages: normalization, rule-based pre-filtering, and confidence scoring. Each stage narrows the space of possible outcomes and hands a smaller, better-labeled problem to the next stage.
Rule-based pre-filter
Before anything touches a model, we run deterministic rules built from IRS guidance and our own compliance review. These rules catch the unambiguous cases fast and cheap:
def pre_filter(product: Product) -> PreFilterResult:
if product.category in HARD_EXCLUDE_CATEGORIES:
return PreFilterResult(decision="ineligible", reason="excluded_category")
if product.category in HARD_INCLUDE_CATEGORIES:
return PreFilterResult(decision="likely_eligible", reason="included_category")
return PreFilterResult(decision="needs_scoring", reason=None)
Roughly a third of catalog items resolve here. The rest move to scoring, which is where most of the engineering effort lives.
Confidence scoring model
For everything the pre-filter can't resolve, we score the product against the customer's condition using a weighted combination of signals: product title and description embeddings, category metadata, historical LMN outcomes for similar products, and merchant-provided attributes. The model returns a confidence score rather than a binary label, and we map that score to an action rather than a claim:
| Confidence range | Label shown to customer | Downstream action |
|---|---|---|
| 0.85 - 1.0 | "Likely eligible" | Auto-attach to LMN request |
| 0.55 - 0.84 | "May qualify" | Route to practitioner review |
| 0.0 - 0.54 | Not shown as eligible | Excluded from LMN flow |
That middle tier is deliberate. We'd rather route an ambiguous item to a practitioner for a real medical necessity determination than have our model assert something it can't back up. Every customer-facing label in the pipeline reflects the actual uncertainty in the score, using conditional language like "may qualify" instead of a definitive claim.
Handling edge cases
Two categories of edge case dominate our incident list: catalog drift and condition mismatch.
Catalog drift happens when a merchant renames or re-categorizes a product after we've scored it. We handle this with a nightly reconciliation job that re-scores any product whose title, description, or category hash has changed since the last run, rather than trusting a cached score indefinitely.
Condition mismatch is subtler. A product might be broadly eligible for some conditions and irrelevant for others — a continuous glucose monitor may qualify for a customer managing diabetes, and it wouldn't make sense to surface it for an unrelated condition. We score condition-product pairs, not products in isolation, which keeps the model honest about context instead of returning a single global eligibility flag.
Results and what's next
Since rolling out the confidence-scored pipeline, manual review volume for the ambiguous middle tier dropped by about 30% on average, and the practitioner queue now sees a cleaner set of genuinely hard cases instead of items the pre-filter should have caught. Our next investment is expanding the historical-outcome signal set so the model learns faster from practitioner decisions, closing the loop between what gets reviewed and what the model predicts next time.
Related articles

Idempotent split-cart payments with Stripe
How we built idempotent split-cart checkouts on Stripe so retries and network failures never result in a duplicate or partial charge.
By Adam Hood

An event-driven provider queue built on Celery
Why we rebuilt our practitioner matching queue as an event-driven system on Celery, and how it handles retries and backpressure.
By Adam Hood
Get engineering updates
New engineering writing from the Truemed team, delivered when we publish.