Model rate, APR, points, and fees as different facts.

A lending API should use separate nullable fields for note rate, published APR, discount points, and itemized fees. Preserve the source product name, attach the observation time and evidence, and never coerce “not published” to zero.

Last updated 2026-09-05TypeScript schemaComparison rulesEvidence required

What each field means

rate
The note or interest rate applied to principal. It does not, by itself, express every borrowing cost.
apr
The publisher’s annual percentage rate for the offer. Keep it nullable and tied to the exact scenario.
points
Upfront discount points, normally expressed as a percentage of principal. Zero and not-published are distinct.
fees
Named charges with amount and applicability. A single total loses which fees are conditional or avoidable.
term and scenario
Term, program, occupancy, vehicle condition, and other qualifiers define what the percentages describe.
as_of and evidence
When and where the publisher stated the values. Without both, a rate cannot be proven current.

A schema that preserves meaning

TSPublished loan offer boundary
type PublishedLoanOffer = Readonly<{
product_name: string; // publisher's identity; never rewrite it
rate: number | null; // note/interest rate, percentage points
apr: number | null; // published APR, percentage points
points: number | null; // discount points; null means not published
term_months: number | null;
fees: ReadonlyArray<{
name: string;
amount_usd: number | null;
applies_when: string | null;
}>;
as_of: string; // required observation time
source_url: string;
evidence: string; // verbatim source snippet
}>;
null means unknown or unpublished. 0 means the publisher established zero. Treating them as interchangeable is a data-quality bug.

Comparison requires a cohort

  1. Match the product and scenario

    Compare the same term, program, occupancy, condition, and other material qualifiers.

  2. Choose a cost measure explicitly

    APR can rank offers when it is present and comparable. Scenario cost can be better when points, fees, amount, and holding period are known.

  3. Keep incomplete offers visible but unranked

    A missing APR is not an infinite cost and not zero. Explain why the row is outside the ranking.

  4. Carry provenance to the response

    Return the source, verbatim evidence, and as_of so a consumer or reviewer can verify the claim.

TSConservative APR ranking
function comparableCost(offer: PublishedLoanOffer): number | null {
// APR is useful only when the publisher supplied it for this exact offer.
// Never manufacture APR from rate, and never turn a missing value into zero.
if (offer.apr === null) return null;
return offer.apr;
}
const comparable = offers
.filter((offer) => offer.term_months === requestedTerm)
.filter((offer) => offer.apr !== null)
.toSorted((a, b) => comparableCost(a)! - comparableCost(b)!);

Frequently asked questions