Eligibility Engine
The eligibility engine evaluates household data against program rules to determine potential eligibility.
Overview
The engine takes two inputs and produces match results:Household Data Model
The screener collects data incrementally. Each field has a defined type and affects matching.Copy
interface HouseholdData {
// Location (Step 1)
zipCode?: string;
state?: string;
countyFips?: string;
city?: string;
// Household composition (Step 2)
householdSize?: number;
adults?: number;
children?: number;
childrenAges?: number[];
// Income (Step 3)
grossMonthlyIncome?: number;
netMonthlyIncome?: number;
incomeType?: IncomeType[]; // wages, self_employment, benefits, etc.
hasIncome?: boolean;
// Demographics (Step 4)
citizenshipStatus?: CitizenshipStatus;
hasElderly?: boolean; // Anyone 60+
hasDisabled?: boolean;
isPregnant?: boolean;
isVeteran?: boolean;
isStudent?: boolean;
// Assets (Step 5, optional)
countableResources?: number; // Bank accounts, etc.
hasVehicle?: boolean;
ownsHome?: boolean;
// Current benefits (Step 6, optional)
currentBenefits?: string[]; // Programs already receiving
}
type CitizenshipStatus =
| "citizen"
| "permanent_resident"
| "qualified_alien"
| "refugee"
| "asylee"
| "undocumented"
| "unknown";
type IncomeType =
| "wages"
| "self_employment"
| "social_security"
| "ssi"
| "ssdi"
| "unemployment"
| "child_support"
| "alimony"
| "pension"
| "investment"
| "rental"
| "other";
Federal Poverty Level (FPL)
Many programs use FPL-based income limits. The engine calculates FPL dynamically.Copy
// 2024 Federal Poverty Guidelines (48 contiguous states)
const FPL_2024 = {
baseAmount: 15060, // For 1 person
perPersonIncrement: 5380,
};
function calculateFPL(householdSize: number): number {
return FPL_2024.baseAmount + (householdSize - 1) * FPL_2024.perPersonIncrement;
}
function calculateFPLPercent(
monthlyIncome: number,
householdSize: number
): number {
const annualIncome = monthlyIncome * 12;
const fpl = calculateFPL(householdSize);
return Math.round((annualIncome / fpl) * 100);
}
// Example: Family of 4 earning $3,000/month
// Annual: $36,000
// FPL for 4: $15,060 + (3 * $5,380) = $31,200
// Percent: 36000 / 31200 * 100 = 115% FPL
FPL Thresholds by Program
| Program | Income Limit | Notes |
|---|---|---|
| SNAP | 130% gross / 100% net | Categorical eligibility may override |
| Medicaid (adults) | 138% | ACA expansion states |
| Medicaid (children) | 200-300%+ | Varies by state |
| CHIP | 200-400% | Above Medicaid, varies by state |
| LIHEAP | 150% | Federal minimum, states may differ |
| Head Start | 100% | Some slots for 100-130% |
| WIC | 185% | Or on Medicaid/SNAP |
| Free School Meals | 130% | |
| Reduced School Meals | 185% | |
| Lifeline | 135% | Or on qualifying program |
Rule Evaluation
The engine evaluates each rule and produces a tri-state result.Copy
type EligibilityResult = "eligible" | "maybe" | "ineligible";
interface RuleEvaluationResult {
ruleId: string;
result: EligibilityResult;
reason?: string;
}
function evaluateRule(
rule: EligibilityRule,
household: HouseholdData
): RuleEvaluationResult {
const fieldValue = getFieldValue(household, rule.field);
// If data is missing, result is "maybe"
if (fieldValue === undefined || fieldValue === null) {
return {
ruleId: rule.id,
result: "maybe",
reason: `Missing data for ${rule.field}`
};
}
const passes = evaluateCondition(fieldValue, rule.operator, rule.value, household);
return {
ruleId: rule.id,
result: passes ? "eligible" : "ineligible",
reason: passes ? undefined : `${rule.field} does not meet requirement`
};
}
Operator Implementations
Copy
function evaluateCondition(
fieldValue: any,
operator: EligibilityOperator,
ruleValue: RuleValue,
household: HouseholdData
): boolean {
switch (operator) {
case "equals":
return fieldValue === ruleValue;
case "not_equals":
return fieldValue !== ruleValue;
case "greater_than":
return fieldValue > ruleValue;
case "less_than":
return fieldValue < ruleValue;
case "greater_than_or_equal":
return fieldValue >= ruleValue;
case "less_than_or_equal":
return fieldValue <= ruleValue;
case "in":
return (ruleValue as string[]).includes(fieldValue);
case "not_in":
return !(ruleValue as string[]).includes(fieldValue);
case "contains":
return Array.isArray(fieldValue) && fieldValue.includes(ruleValue);
case "less_than_fpl_percent": {
const fplPercent = calculateFPLPercent(
household.grossMonthlyIncome || 0,
household.householdSize || 1
);
return fplPercent <= (ruleValue as number);
}
case "age_at_least":
return Math.max(...(household.childrenAges || [0])) >= (ruleValue as number);
case "age_under":
return (household.childrenAges || []).some(age => age < (ruleValue as number));
case "has_dependent_under":
return (household.childrenAges || []).some(age => age < (ruleValue as number));
case "is_citizen_or_qualified":
return ["citizen", "permanent_resident", "qualified_alien", "refugee", "asylee"]
.includes(household.citizenshipStatus || "");
default:
return false;
}
}
Matching Algorithm
The full matching process combines geography, rules, and scoring.Copy
interface MatchResult {
programId: string;
program: Program;
eligibility: "likely" | "possible" | "unlikely";
score: number; // 0-100
matchedRules: string[]; // Rules that passed
failedRules: string[]; // Rules that failed
unknownRules: string[]; // Rules with missing data
reasons: string[]; // Human-readable explanations
}
async function matchPrograms(
household: HouseholdData
): Promise<MatchResult[]> {
// Step 1: Get geographically available programs
const programs = await getProgramsByLocation(
household.state,
household.countyFips
);
// Step 2: Evaluate each program
const results: MatchResult[] = [];
for (const program of programs) {
const result = evaluateProgram(program, household);
results.push(result);
}
// Step 3: Sort by eligibility and score
return results.sort((a, b) => {
// Likely > Possible > Unlikely
const eligibilityOrder = { likely: 0, possible: 1, unlikely: 2 };
if (eligibilityOrder[a.eligibility] !== eligibilityOrder[b.eligibility]) {
return eligibilityOrder[a.eligibility] - eligibilityOrder[b.eligibility];
}
// Then by score descending
return b.score - a.score;
});
}
Program Evaluation
Copy
function evaluateProgram(
program: Program,
household: HouseholdData
): MatchResult {
const rules = program.eligibilityRules || [];
const ruleResults = rules.map(rule => evaluateRule(rule, household));
const passed = ruleResults.filter(r => r.result === "eligible");
const failed = ruleResults.filter(r => r.result === "ineligible");
const unknown = ruleResults.filter(r => r.result === "maybe");
// Calculate eligibility
let eligibility: "likely" | "possible" | "unlikely";
if (failed.length > 0) {
eligibility = "unlikely";
} else if (unknown.length > 0) {
eligibility = "possible";
} else {
eligibility = "likely";
}
// Calculate score
const totalRules = rules.length || 1;
const score = Math.round((passed.length / totalRules) * 100);
return {
programId: program.id,
program,
eligibility,
score,
matchedRules: passed.map(r => r.ruleId),
failedRules: failed.map(r => r.ruleId),
unknownRules: unknown.map(r => r.ruleId),
reasons: failed.map(r => r.reason).filter(Boolean) as string[]
};
}
Categorical Eligibility
Some programs grant automatic eligibility based on participation in other programs.Copy
const CATEGORICAL_ELIGIBILITY: Record<string, string[]> = {
// If you receive SNAP, you qualify for:
"snap": ["free_school_meals", "lifeline", "wic"],
// If you receive TANF, you qualify for:
"tanf": ["snap_categorical", "free_school_meals", "medicaid"],
// If you receive SSI, you qualify for:
"ssi": ["snap_categorical", "medicaid", "lifeline"],
// If you receive Medicaid, you qualify for:
"medicaid": ["wic", "lifeline"],
};
function applyCategoricalEligibility(
household: HouseholdData,
results: MatchResult[]
): MatchResult[] {
const currentBenefits = household.currentBenefits || [];
for (const benefit of currentBenefits) {
const qualifiesFor = CATEGORICAL_ELIGIBILITY[benefit.toLowerCase()] || [];
for (const result of results) {
if (qualifiesFor.includes(result.program.slug)) {
result.eligibility = "likely";
result.score = 100;
result.reasons.push(`Categorical eligibility from ${benefit}`);
}
}
}
return results;
}
Progressive Refinement
As users provide more data, matches become more accurate.Confidence Scoring
Copy
interface MatchConfidence {
overall: number; // 0-100
geographic: number; // Location data completeness
household: number; // Household data completeness
income: number; // Income data completeness
demographic: number; // Demographics completeness
}
function calculateConfidence(household: HouseholdData): MatchConfidence {
const geographic =
(household.state ? 25 : 0) +
(household.countyFips ? 50 : 0) +
(household.city ? 25 : 0);
const householdData =
(household.householdSize ? 50 : 0) +
(household.children !== undefined ? 25 : 0) +
(household.childrenAges?.length ? 25 : 0);
const income =
(household.hasIncome !== undefined ? 25 : 0) +
(household.grossMonthlyIncome ? 50 : 0) +
(household.incomeType?.length ? 25 : 0);
const demographic =
(household.citizenshipStatus ? 40 : 0) +
(household.hasElderly !== undefined ? 20 : 0) +
(household.hasDisabled !== undefined ? 20 : 0) +
(household.isVeteran !== undefined ? 20 : 0);
return {
overall: Math.round((geographic + householdData + income + demographic) / 4),
geographic,
household: householdData,
income,
demographic
};
}
Performance Considerations
Caching Strategy
Copy
// Cache FPL calculations (rarely change)
const fplCache = new Map<number, number>();
// Cache geographic lookups (expensive)
const geoCache = new Map<string, GeocodedLocation>();
// Don't cache program matches (user-specific)
Query Optimization
Copy
-- Use covering index for common query pattern
CREATE INDEX idx_programs_matching ON programs(
is_active,
coverage_type,
category
) INCLUDE (
name,
short_description,
benefit_value_cents
);
-- Partial index for active programs only
CREATE INDEX idx_programs_active_search ON programs
USING GIN(search_vector)
WHERE is_active = true;
Edge Cases
Handling Missing Data
Copy
// If household size is missing, use 1 (conservative)
const effectiveHouseholdSize = household.householdSize || 1;
// If income is missing, don't filter by income (permissive)
if (!household.grossMonthlyIncome) {
result.eligibility = "possible";
result.reasons.push("Provide income for more accurate matching");
}
State-Specific Rules
Some rules vary by state (e.g., Medicaid expansion, SNAP policies).Copy
interface StateOverride {
state: string;
programSlug: string;
overrideRules: EligibilityRule[];
}
// Example: California has higher SNAP income limits
const CA_SNAP_OVERRIDE: StateOverride = {
state: "CA",
programSlug: "snap",
overrideRules: [
{
id: "income",
field: "household_income_monthly",
operator: "less_than_fpl_percent",
value: 200 // California uses 200% FPL
}
]
};