Documentation Index Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
Use this file to discover all available pages before exploring further.
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.
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.
// 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.
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
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.
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
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.
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
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
};
}
Caching Strategy
// 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
-- 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
// 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).
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
}
]
};
Next Steps
Screener Implementation How the multi-step screener collects data
Programs Database Schema and query patterns