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.
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
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
Match the product and scenario
Compare the same term, program, occupancy, condition, and other material qualifiers.
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.
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.
Carry provenance to the response
Return the source, verbatim evidence, and
as_ofso a consumer or reviewer can verify the claim.
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
No. The note rate and APR answer different questions and must remain separate nullable fields. Combining them destroys meaning and makes a missing APR indistinguishable from a published rate.
No. Zero means the publisher explicitly states no fee; null means the source did not establish the amount. APIs should preserve that distinction.
Only if you possess every input required by the applicable calculation and clearly label the result as calculated. A rate aggregation API should preserve the publisher’s APR and avoid silently fabricating one from the note rate.
At minimum: the same product category, term, relevant program and occupancy or vehicle condition, plus compatible assumptions about points and fees. Sorting unlike offers by one percentage produces a precise-looking but invalid ranking.