Modelling who can get what.

Eligibility is derived, never stored: keep rules and conditions with provenance on every one, evaluate them deterministically, and return a four-value verdict where unknown is a real state rather than a soft no. This is the model behind RateAPI’s eligibility API, and the reasoning applies whether you use ours or build your own.

Last updated 2026-08-21Schema + evaluation codeApplies beyond credit unions

Three questions people conflate

Almost every eligibility bug traces back to one of these being answered by a model built for another.

Access
Can this person obtain this product at all? Membership, geography, product-level constraints. This is what an eligibility model should answer.
Underwriting
Will the lender approve them, on what terms? Needs credit data and belongs to the lender. Different system, different authority.
Suitability
Should they take it? A recommendation question, downstream of both, and a regulated one in many contexts.
A system that answers access but presents the result as approval is making a claim it has no authority to make. Keep the vocabulary honest all the way to the UI.

The schema

//Rules, conditions, provenance
-- Rules and conditions, with provenance as a first-class citizen.
CREATE TABLE eligibility_rules (
rule_id BIGSERIAL PRIMARY KEY,
institution_id TEXT NOT NULL,
kind TEXT NOT NULL, -- geography | employer | association | military | family | open
description TEXT NOT NULL, -- human-readable, rendered to users
-- Provenance. Not metadata: without it a verdict cannot be explained,
-- corrected, or aged out, which are the three things you will need most.
source_id BIGINT NOT NULL REFERENCES eligibility_sources(source_id),
evidence_quote TEXT NOT NULL, -- the sentence that justified the rule
observed_at TIMESTAMPTZ NOT NULL,
confidence REAL NOT NULL,
-- Supersede, never overwrite: last year's verdict must stay explainable.
superseded_at TIMESTAMPTZ,
superseded_by_rule_id BIGINT
);
CREATE TABLE eligibility_conditions (
condition_id BIGSERIAL PRIMARY KEY,
rule_id BIGINT NOT NULL REFERENCES eligibility_rules(rule_id),
kind TEXT NOT NULL,
-- Canonical keys, never free text: 'county:NC:mecklenburg'. This is what
-- turns a fuzzy geographic question into an exact index hit.
geo_level TEXT,
geo_value TEXT,
org_id TEXT,
params_json JSONB
);
-- The inverse index is the whole performance story: it turns "which of 4,000
-- institutions might apply to this person" into a handful of key lookups.
CREATE INDEX ON eligibility_conditions (geo_value) WHERE geo_value IS NOT NULL;
CREATE INDEX ON eligibility_conditions (org_id) WHERE org_id IS NOT NULL;
Two ways to qualify are two rules, not one rule with an OR. Conditions within a rule are ANDed. That keeps every explanation self-contained: you can point at the one rule that qualified someone and read its evidence quote aloud.

Evaluation: narrow, then evaluate properly

TSThe two-stage evaluation
// Narrow, then evaluate. The order matters and the second half is the subtle part.
function findEligible(person: Facts): Verdict[] {
// 1. NARROW — cheap index hits from the person's facts.
const candidates = new Set([
...byGeo(person.geoKeys),
...byOrg(person.orgIds),
...openToAnyone(),
]);
// 2. EVALUATE — against each institution's FULL rule set.
//
// The trap: an institution surfaced by ONE matching condition is not
// thereby eligible. Its rule may require that condition AND another the
// person fails. Scoring on the condition that surfaced the candidate is
// the single most common way an eligibility engine becomes confidently
// wrong, because it only ever over-reports.
return [...candidates].map((id) => evaluate(allRulesFor(id), person));
}

The verdict vocabulary

Four values. The fourth is the one that keeps the system honest.

eligible
A rule matched the facts you have.
conditionally_eligible
One step away — and the model must be able to NAME the step and its cost, or the value is unusable in a product.
possibly_eligible
A rule plausibly applies but the facts do not confirm it. Drives your next question to the user.
unknown
Insufficient rule data. NEVER treat as ineligible: coverage gaps should shrink an answer’s confidence, never its recall.
Let LLMs extract rules. Never let them decide. Extraction from prose is a fuzzy problem with a human-checkable output. Deciding must be deterministic, reproducible and explainable — properties a model cannot promise. Populate the graph with models; evaluate it with code.

Frequently asked