rates.json schema
rates.json is a precomputed snapshot of every IGCP-published monthly base rate and every cohort-anchored annual rate, generated by scripts/build-rates-json.ts and published to GitHub Pages. Python, Java, Excel, and spreadsheet users can fetch it once and reproduce IGCP’s numbers without porting any code.
Where it lives
Section titled “Where it lives”- Latest:
https://primor.github.io/igcp-aforro/rates.json - Per-release snapshot:
https://primor.github.io/igcp-aforro/v/<calver>/rates.json(e.g.v/2026.420.0/rates.json)
The file is regenerated:
- after every release (the workflow runs
pnpm build:rates-jsonand deploys the snapshot to Pages); - after every Euribor / IGCP base-rate refresh PR is merged via
data-refresh.yml.
Top-level shape
Section titled “Top-level shape”{ "schemaVersion": 1, "generatedAt": "2026-04-20T08:00:00Z", "libraryVersion": "2026.420.0", "euriborSourceMeta": { "lastRefreshedAt": "2026-04-19T07:42:11Z", "source": "Deutsche Bundesbank time-series API", "sourceUrl": "https://api.statistiken.bundesbank.de/rest/download/BBIG1/...", "seriesId": "BBIG1.D.D0.EUR.MMKT.EURIBOR.M03.BID._Z" }, "series": { "E": { "metadata": { "...": "..." }, "monthlyBaseRates": [], "cohortRates": [] }, "F": { "metadata": { "...": "..." }, "monthlyBaseRates": [], "cohortRates": [] } }}series.E and series.F carry the same shape and are populated independently — Série E rows start in November 2017 (the first month with a clean ≥10-business-day Euribor 3M history under the IGCP averaging rule), Série F rows start in June 2023.
schemaVersion is bumped whenever a backwards-incompatible change ships, so consumers can pin or assert. Adding a new series under series.<code> is not considered a breaking change.
series.<code>.metadata
Section titled “series.<code>.metadata”The static SeriesMetadata for the series — maturityYears, subscriptionStartDate, subscriptionEndDate (only present for closed series like Série E), minUnits, maxUnits, baseRateClampMinPct, baseRateClampMaxPct, baseRateSpreadPct (Série F: '0', Série E: '1'), defaultIrsRate, the full premiumTiers list, etc. Identical to what the npm library returns from getSeries('E') / getSeries('F').
series.<code>.monthlyBaseRates
Section titled “series.<code>.monthlyBaseRates”One entry per calendar month for which a fixing can be resolved.
{ "month": "2024-03", "fixingDate": "2024-02-27", "basePct": "3.892"}| Field | Type | Notes |
|---|---|---|
month | YYYY-MM | Calendar month the rate applies to. |
fixingDate | YYYY-MM-DD | Antepenultimate TARGET2 business day of the previous month. |
basePct | decimal string | Final, post-clamp base rate rounded to 3 decimals. For Série F: rounded mean clamped to [0, 2.5]. For Série E: rounded mean + 1.000 (the +1pp spread), clamped to [0, 3.5]. |
series.<code>.cohortRates
Section titled “series.<code>.cohortRates”One entry per anchored quarter, from each subscription month through min(maturity, last published month).
{ "subscribed": "2024-03", "subscriptionDate": "2024-03-01", "quarterIndex": 8, "quarterStartDate": "2026-03-01", "quarterEndDate": "2026-06-01", "yearsSinceSubscription": 2, "basePct": "2.500", "premiumTierYearsRange": "2-5", "premiumPct": "0.25", "annualRatePct": "2.750"}| Field | Type | Notes |
|---|---|---|
subscribed | YYYY-MM | Cohort identifier; subscriptionDate always resolves to the 1st. |
subscriptionDate | YYYY-MM-DD | Always <subscribed>-01; see the day-of-month note below. |
quarterIndex | integer | 0-based, counted from subscription. |
quarterStartDate / quarterEndDate | YYYY-MM-DD | Anchored to the subscription day, with end-of-month roll-forward. |
yearsSinceSubscription | integer | Whole anniversary years elapsed at quarterStartDate. |
basePct | decimal string | Same format as in monthlyBaseRates. |
premiumTierYearsRange | string | E.g. "2-5". |
premiumPct | decimal string | Premium added to basePct. |
annualRatePct | decimal string | Composite annual rate for that cohort × quarter. |
Conventions
Section titled “Conventions”- All percentage fields are decimal strings (e.g.
"2.500", not2.5). This matches the npm library’s public API and avoids float drift across language boundaries. subscribedis a calendar month (always normalised toYYYY-MM-01). IGCP’s anchored-quarter rule keys off the subscription day, so a precomputed table cannot represent every day-of-month cohort without exploding in size. Consumers needing day-precision should use the npm library or replicate the math frommonthlyBaseRatesplus the premium tiers inmetadata.cohortRatesrows enumerate every anchored quarter from subscription throughmin(maturity, last published month).quarterEndDateis the next quarter’squarterStartDate; both followshiftMonths’ end-of-month roll-forward semantics.
Minimal Python example
Section titled “Minimal Python example”import json, urllib.requestfrom decimal import Decimal, ROUND_HALF_EVEN
data = json.load(urllib.request.urlopen('https://primor.github.io/igcp-aforro/rates.json'))rows = [r for r in data['series']['F']['cohortRates'] if r['subscribed'] == '2024-03']
units = Decimal('1000')balance = unitsirs = Decimal('0.28')
for r in rows: annual = Decimal(r['annualRatePct']) / Decimal('100') quarterly = annual / 4 gross = (balance * quarterly).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) withheld = (gross * irs).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN) balance += gross - withheld
print(balance)