Skip to content
IBANforge

Changelog

Every notable change to the API, SDKs and MCP server.

Versioning & deprecation policy

The /v1 API is stable: fields are only added, never renamed or removed within v1. Breaking changes would ship as a new major path (/v2) with at least 6 months of parallel operation, announced here and by email to active key holders. The project follows semantic versioning.

All notable changes to IBANforge are documented here. The format follows Keep a Changelog and the project adheres to Semantic Versioning.

[Unreleased]

Changed

  • CRM: the French translation no longer replaces the original, and the answer has room to be read. In a thread bubble and on a draft card, "afficher l'original" now reveals the source text under the French instead of swapping one for the other, so comparing two languages costs one click and no going back. In both composers the translation is a plain panel rather than a fold, the body field grows with its content (one scrollbar where there were two nested ones), the sheet stands up by itself as soon as a proposal lands, and the height the operator chooses is remembered.

Added

  • Java SDK, on Maven Central. com.ibanforge:ibanforge-sdk:1.5.0 (Java 17, Jackson as the only dependency): the TypeScript client method for method, forty records mirroring the response types, the same exception hierarchy (PaymentRequiredException with the x402 challenge, QuotaExhaustedException, RateLimitException, PayloadTooLargeException, InvalidInputException, ApiException), the free routes without a key. Published through the Sonatype Central Portal (namespace com.ibanforge verified by DNS, signed, with sources and javadoc) by .github/workflows/java-publish.yml on a java-v* tag. Source under sdks/java/, eighty tests on a local HTTP server.
  • .NET SDK. IBANforge.Sdk 1.5.0 (net8.0, no dependency beyond the framework): the same surface as async methods with a CancellationToken, records with SnakeCaseLower naming, an HttpClient of the caller's choosing (IHttpClientFactory), the same exception hierarchy. Source under sdks/dotnet/, fifty-two xUnit tests, dotnet pack verified; .github/workflows/dotnet-publish.yml pushes to nuget.org on a dotnet-v* tag once the API key exists. Both SDKs have their recipe on /docs/recipes in EN, FR and DE and their CI job.
  • Register pages, second batch: Austria and Belgium. /at/{code} (871 Bankleitzahlen of the OeNB directory: seat address and LEI where the register publishes them; first batch of 400 listed in German, every institution once before any institution twice) and /be/{code} (782 bank identifiers of the NBB list in 108 institutions: the NBB allocates blocks of identifiers to one bank, so one canonical page per bank, listed in French and English, every other code of a block keeps its address and points to it; the example IBAN carries the two national check digits). Same export as the German and Swiss pages, same monthly refresh, footer links.
  • The demand ledger proposes, once a month. src/lib/demand-proposal.ts turns the ranked ledger into one deterministic proposal per month: bank codes no source knows aggregate per country into "plug this national register" (with a hint to the publisher for the countries most likely to be named), a BIC of valid shape absent from every source proposes the composite map, unallocated codes propose nothing, under five hits the ledger says too early. Hourly tick, one record per calendar month in kv_state, sent on the ops channel only when it names a register or a BIC and the ledger is at least four weeks old. Rides in GET /v1/admin/demand-gaps (proposal, monthly) and in the weekly facts (demand), shown on the living-tool card.
  • Dashboard, the audit card lists its uploads. GET /v1/admin/audit-stats returns each upload of the window (time, row count, whether a key was sent and whether it is one of ours) and the sales of the window; the card prints them under its four figures.
  • Attribution on the free tier. Every paid-endpoint response served on a free key (/v1/iban/validate, /batch, /bic/{code}, /ch/clearing/{iid}, /iban/compliance) carries an attribution object: text "Powered by IBANforge", url, and a note saying when it is owed (results shown to people) and when it is not (backend-only use, paid plans). Terms §2 (version 1.3) states the obligation; the API-keys doc explains it in three languages; the OpenAPI contract and the TypeScript and Python SDK types carry the optional field.
  • Recipes: Odoo. The Odoo 18 module installs from its repository (cammac-creator/ibanforge-odoo, branch 18.0) until the Apps store listing exists; the recipes page says how, in three languages.
  • Pricing: the Pro plan where the free tier ends. The plans table of the API-keys doc (EN/FR/DE), the free-key welcome mail and the vendors page name the $29 tier.
  • Dashboard, "Courrier à rattacher": read in French, pre-selected match, calmer rows. Each orphan mail gets a French gist (who writes, what they want) from the VPS writer, generated once and kept on the row (gist_fr, POST /v1/admin/orphan-mail/gist); replies ask for theirs on sight, first contacts on a click, the original text folds underneath. The attach control now names the most likely client before anything is typed (shared company domain first, then the domain in a file's label, then a name fragment), with the reason, one click away from the two-click confirmation. Automated notices (DMARC reports, no-reply senders) are labelled as such with the dismissal first. Rows read sender, subject, gist, then actions. "Mail complet" unfolds the whole text: the original as the sync now sends it (body, up to 6,000 characters) and its French translation, made by the sync as the mail arrives (body_fr) or by the dashboard on first sight for older rows (POST /v1/admin/orphan-mail/translation, written once). Nothing to click: the operator asked for automatic French, and the gist is requested for every row on sight.
  • Register pages: every German BLZ and every Swiss IID has a page. /blz/{blz} (3,506 Bankleitzahlen, Deutsche Bundesbank register) and /iid/{iid} (1,164 SIX BankMaster clearing numbers) in EN, FR and DE: the register facts, a synthetic IBAN showing where the code sits, related codes of the same institution, and the exact answer of the API, produced by calling the routes in-process at export time (npm run pages:export, hooked into the monthly register refresh). First batch listed in the sitemap: the 800 German head-office BLZ (in German) and the 336 Swiss headquarters IIDs (in German and French); every code renders on demand, the site being server-rendered. Index pages /blz and /iid with a lookup box.
  • Google Sheets add-on, source published. integrations/sheets/: four custom functions (IBAN_VALID, IBAN_BANK, IBAN_BIC, IBAN_CHECK, with French and German aliases) that check a column of IBANs through POST /v1/iban/batch in batches of 100 on the user's own key, a six-hour per-user cache so a recalculation does not pay twice, a sidebar that tests and stores the key in the user's script properties. Page /sheets in EN, FR and DE with the by-hand install path until the Marketplace listing exists; icons under /sheets/; README with the publishing procedure and LISTING.md with the store texts. Tested under Node with the Apps Script services stubbed, plus the invalid-key path against the live API.
  • Pro subscription, the flat monthly tier. $29 a month for 10,000 requests: the allowance resets on the 1st, the key dies with its subscription, cancel anytime. Sold through a public Stripe Payment Link on the pricing page and minted by the webhook through the same path as Editor / OEM (metadata.plan = 'pro'), with its own welcome e-mail. Listed for machines in GET /v1/credits/bundles under subscription, in the card hint of every 402 body, on the API landing and in llms.txt.
  • Postman collection. integrations/postman/ibanforge.postman_collection.json, generated from the live OpenAPI contract: 28 requests in 8 folders, bearer auth from a single apiKey variable, the free endpoints marked as such.

Fixed

  • The demand ledger no longer counts the textbook IBANs. The ISO 13616 registry's example IBANs, our own docs' and the /audit sample file's are what everybody pastes first; CH93 0076… alone topped the ledger for two days. src/lib/textbook-ibans.ts (every entry checked against mod-97 by a test) is skipped at recording time, and the rows they left behind are purged at table ensure time, idempotently.
  • Forum radar: threads under a month old, on forums we can post to, that ask a question. MAX_THREAD_AGE_DAYS = 30 (provider windows and finalizeCandidate), POSTABLE_SOURCE_NAMES limited to Stack Exchange and GitHub, closed issues dropped, and titles that submit, announce or release something (NOT_A_QUESTION_RE) dropped: none of them is a conversation anyone waits on.
  • npm publication through trusted publishing. release-publish.yml publishes ibanforge-mcp and @ibanforge/sdk with --provenance through the GitHub Actions OIDC identity (npm 11.5+), no token; best-effort until each package's trusted publisher is registered on npmjs.com, then one tag publishes npm, PyPI, the MCP Registry and the GitHub release in the right order.
  • Tests: one private stats database per test file. Vitest 4 had silently dropped the serialisation the suite relied on, so a different delta assertion failed on every local run; a setupFiles hook now gives every file its own stats.sqlite, parallelism kept. The boot-corruption test creates its own healthy database instead of copying the checkout's.

Removed

  • The /live village page is paused and no longer served. Operator decision of 2026-09-02: the idea — watch the agents and every actor of the pipeline at work, to see what happens backstage — stays; the execution was judged too rough to be live, and the nine redesign mock-ups audited the same day had not found the form yet. Removed: the route and its canvas engine, the sprite atlas and its rebuild script, the two relays only that page used (/api/ops, /api/health-sources), the menu entry, the landing link and the sitemap entry. /{locale}/live redirects (temporarily) to the playground for the links already shared. The code is kept whole at git tag village-pause-2026-09-02. The backend feed GET /v1/ops/recent stays, now unused.

[1.5.0] — 2026-09-02

Added

  • Creditor file audit, sold at a displayed price. POST /v1/audit/upload takes a CSV or XLSX of creditors and audits every row in process with the registers the API already serves: IBAN structure and check digits, bank code against the national register, bank name and BIC, SEPA reach, issuer type, plus the checks only a whole file allows (duplicates, the file's BIC against the register, address country against IBAN country, Swiss structured-address rules ahead of 14 November 2026). Free preview with masked IBANs, then a one-off Stripe Checkout Session created from code (149 CHF up to 5,000 rows, 349 CHF up to 20,000); the webhook marks the job paid and the status route asks Stripe directly when the webhook is late. Reports live on the persistent volume for 2 h unpaid or 24 h after payment, then purge. Pages /audit and /audit/done in EN, FR and DE.
  • Swiss QR-bill payload check, free. POST /v1/ch/qr-bill/check reads the text inside a QR-bill code (SPC to EPD) and returns every rule verdict at once: header and version, creditor IBAN and QR-IBAN range, QRR/SCOR/NON reference checksum and its pairing with the IBAN, amount, currency, and whether the creditor and ultimate debtor addresses are structured (type S) or still combined (type K), which the standard removed on 21 November 2025 and banks stop processing on 14 November 2026. A combined address comes back with proposed_structured. Ninth MCP tool check_swiss_qr_bill on all three servers, page /tools/qr-bill, documented in the OpenAPI contract and the docs.
  • Runtime-dependency guard. npm run deps:check fails when code under src/ imports a package that is not a production dependency; it runs in CI and inside npm run check.

Changed

  • The production-image boot check in CI now blocks. It was advisory while it proved itself; the same day an image that could not boot reached Railway with a green CI.

Fixed

  • Thirteen minutes of API outage on 2026-09-02 (17:02 to 17:15 CEST). The audit route imported xlsx from devDependencies; the production image runs npm ci --omit=dev, the container died at boot, and because the service mounts a volume the previous container had already been stopped. xlsx moved to dependencies; the two guards above exist so this class of mistake stops in CI.

[1.4.4] — 2026-09-01

Fixed

Audit of 2026-09-01 — machine discovery, tool inventory and the public contract.

  • The eight MCP tools are published as eight everywhere, from one table. The tool list was written out by hand on six surfaces and had drifted into five different answers for one product: eight tools served, seven on the MCP server card, "7 tools" in the prose of /llms.txt, five in the A2A agent card, five in the .well-known/x402 document, and five in the static mcp.json — which had also been frozen at version 1.3.3 since July. The two documents that were furthest behind are the two that agent directories and x402 indexers read first, and the tools they omitted (validate_payment_reference, check_postal_address) are the only two that answer with no key and no payment: the surfaces meant to attract an agent were hiding the free doors. All of them now derive from src/mcp/inventory.ts, a ninth tool would publish itself everywhere at once, and a test fails when any document drops one.
  • .well-known/x402 says what can be tried before paying. Its free_endpoints list named /v1/demo and this document's own metadata routes, so an agent asking what it could try for free concluded there was a demo and nothing else. It now names the six free API endpoints (/v1/iban/format, /v1/iban/structure, /v1/reference/validate, /v1/address/check, /v1/demo, /v1/credits/bundles), and a test calls each one unauthenticated to prove the claim.
  • Every error response in the OpenAPI document now has a schema. All thirty-one declared 4xx/5xx responses carried a sentence and no content, and no Error component existed — so a generated client (including the Custom GPT that integrations/openai/custom-gpt-setup.md builds by pasting this document) could type every success and no failure, for a server whose failures are perfectly regular. A new ApiError component describes the {error, message} shape they have always had, with additionalProperties left open for the contextual recovery hints several routes add. 429 is now declared on every operation and 413 on every operation that takes a body, both of which are enforced by globally mounted middleware and were declared on three paths and none respectively.
  • The self-service key lifecycle is in the contract. POST /v1/keys/revoke, POST /v1/keys/rotate and GET /v1/credits/balance authenticate with the caller's own key, not with an admin secret, so they are public routes — and they were absent from the OpenAPI document, which meant a developer who leaked a key and read only the contract could not find out they were able to kill it themselves in one call. POST /v1/feedback and GET /v1/feedback/{id} are documented alongside them, with the error-type enum and the flood cap read from the route rather than retyped.
  • Five served fields were in no schema, and seven declared fields never said when they appear. sanctions on a BIC lookup (the one compliance signal on the cheap endpoint), qr_iid_source and qr_iids on Swiss clearing, processing_ms on a batch (omitted from a required list that named its four siblings) and meta on a compliance answer (the block that says what the verdict does not cover) are now declared. In the other direction, the conditional fields of a validation result — error, error_detail, reference_check, issuer, psd_registration, official_identity, modulus_check — each state the condition under which they are served, instead of leaving a generated client to guess.

Changed

  • The machine-readable surfaces sell the promise the product actually keeps. Both llms.txt files opened on "Pre-payout screening for AI agents", a framing the landing page had already dropped; they now open on "Know the bank behind any IBAN", with the capability list unchanged.

[1.4.4] — 2026-08-30

Fixed

  • ⚠️ The bank sanctions screen was discarding designated banks it could not name. When the compliance database was rebuilt, a sanctioned BIC was kept only if that BIC already existed in our own bank directory. This is backwards: a bank a sanctions authority has designated and that no commercial directory lists is precisely the dangerous case, not a data error to drop. The EU consolidated list carries only two bank BICs, and one of them — a Libyan bank the EU designates — was discarded this way, so half the EU bank coverage was missing. 33 designated banks in total were unreachable; the database now holds them and the screen answers on them. Corrected on 2026-08-21, database regenerated from the primary sources, and the refresh now fails loudly instead of dropping a row in a best-effort catch.
  • We cannot tell you whether your own queries were affected, and that is the retention policy working. Request paths are normalised before they are written down: a lookup of a specific BIC is stored as /v1/bic/:code, never the code you asked about. So no log anywhere records which institutions any customer queried, which also means no individual notice is possible. If you screened bank BICs against the EU or UN lists through this API before 2026-08-21, re-run the ones that matter to you.
  • A designated bank no longer hides behind "not found" on the cheap lookup. GET /v1/bic/:code answered a bare "not found, coverage may be partial" for a BIC our directory cannot name, which was the most reassuring sentence available about the least reassuring institution it knows. Every answer now carries the bank-level sanctions screen, found or not. listed is null, never false, when the database cannot be read: a check that did not happen must not look like a check that passed.
  • A settlement that hangs is no longer reported as a payment problem. A call to the payment facilitator could exceed the shutdown drain, and a timeout surfaced as a bare 402. That is the worst available answer, since 402 means "pay" and invites an agent to send the payment again. Such a case now answers 502 with paid: null, confirmation_received: false and authoritative: false. On the credit-pack route the response keeps its single-use recovery link, so the one case where you may have been charged keeps its way back.

Added

  • GET|POST /v1/reference/validate, free — checksum validation for structured payment references: the RF Creditor Reference (ISO 11649, mod 97-10), the Swiss QR reference (27 digits, modulo 10 recursive per SIX Annex B), the Belgian OGM/VCS (modulo 97 with remainder 0 written as 97) and the Finnish viitenumero (weights 7-3-1). Norwegian KID and Swedish OCR are recognised but answer valid: null with status: unverifiable_without_creditor_config, because their rules are configured per creditor account by the beneficiary bank — answering false would reject valid references. Every scheme answer names the publishing document and its date.
  • reference_check on POST /v1/iban/validate — pass a reference alongside the IBAN and the answer adds the pairing verdict no standalone reference checker can give: a Swiss QR reference is only permitted with a QR-IBAN (QR-IID range 30000–31999) and an RF reference is forbidden with one, judged against the SIX register we already embed. Also exposed as the validate_payment_reference MCP tool on all three MCP surfaces.
  • postal_address on BIC answersGET /v1/bic/{code} and POST /v1/iban/validate now carry the institution's address in ISO 20022 vocabulary (TwnNm, Ctry, PstCd, StrtNm…), with provenance and as_of per block, for the November 2026 structured-address deadlines. Swiss institutions come fully structured from the SIX register; GLEIF-enriched entries are hybrid, keeping a concatenated street as AdrLine rather than guessing a split into StrtNm + BldgNb. The block is omitted when town or country are missing, because an address no rail accepts is not worth serving. Guarded against a real trap found while building: the SIX register lists 75 foreign institutions (euroSIC participants) whose town column holds postal designations, not towns — the Swiss enrichment now applies to CH/LI seats only.
  • POST /v1/address/check, free — pure rule evaluation of an already-structured postal address against a named scheme (sps, hvps_plus, fedwire), one finding per rule with the source document and date on each. The cbpr+ scheme is refused with the reason rather than guessed: those guidelines are not publicly citable, and a compliance verdict from uncitable rules would be theatre.
  • pra_authorisation on UK lookups — the Bank of England's monthly List of PRA-regulated Banks, used with the Bank's written permission and served with the attribution it asked for: source and list month, with the month read from the loaded data and a guard test that fails the build if any surface's displayed month drifts from it. The join is by LEI only, never by name similarity, and bounded to GB/GI BICs — unbounded, a branch's head-office LEI would have claimed UK authorisation on over a thousand non-UK BICs (measured on the real data before shipping). The block never answers authorised: false: the list's own preamble says it does not supersede the Financial Services Register, so absence of the block is absence of a claim, not a finding. Refreshed monthly through the same workflow as the other registers.
  • /docs/swiss-qr-iban (EN/FR/DE) — the QR-IBAN / QR-IID reference page: the dedicated Swiss QR-IID range, real captured responses for both GET /v1/ch/clearing/:iid and validated CH/LI IBANs, and the reverse question no other API answers — which QR-IID the bank behind an ordinary Swiss IBAN holds. Includes the vIBAN end-user identification context of AMLR Art. 22(3), stated within its verified limits.
  • wallet_setup in every 402 body — the payment-required response carried every rail except the "I have no wallet yet" path. It now links the three-step agent onboarding guide (/docs/pay-as-an-agent), so the response that names the wall also names the way through it.
  • /v1/iban/compliance accepts a bic instead of an iban. Nineteen countries have a purely numeric bank code with no reliable public map to a BIC, so no IBAN from them could ever produce the BIC a screen needs — Libya among them. Fabricating that map would be inventing a register. A BIC also carries its own country in positions 5-6, so sanctions, FATF, reachability and VoP all answer with no resolution step that can fail. Sending both iban and bic is refused rather than arbitrated: they can designate different institutions, and silently picking one would report a screen of a bank you did not ask about.
  • shared_bic8 on GET /v1/bic/:code. A BIC8 shared across a whole banking network resolves to no single institution, and the endpoint reported that as an absence of data when it is the opposite: too much data to name one. Unresolved codes with rows behind them now return how many institutions and entries share the code. It never names one, even when the group holds a single institution, because a contract that sometimes names would be depended on for the name. found keeps its exact meaning, so nothing that reads it changes behaviour.
  • uk_modulus on /health — whether the UK modulus table is loaded, the day it was fetched, its age in days, and whether that age is past sixty. The table refreshes only at image build, so between deployments it aged with nothing watching: the existing probe proved it was present, which a six-month-old table satisfies exactly like a fresh one while answering wrongly for every sorting code reallocated since. An absent table reports stale: null and does not turn the endpoint red, because that dataset is allowed to be missing.
  • check_postal_address MCP tool, on all three surfaces — the free POST /v1/address/check is now callable by agents over stdio (npm ibanforge-mcp), the embedded stdio server and the HTTP transport. The npm and HTTP surfaces declare input and output schemas, so findings[].source survives client-side schema validation instead of being silently stripped — the provenance is the point of the feature. Seven data tools (plus send_feedback) on every surface, and the parity test keeps the three lists identical by construction.
  • /sources (EN/FR/DE) — the sources & permissions page: the written permission (Bank of England), the public licences honoured inside the responses themselves (EBA, FATF, Bundesbank, ECB/Banco de España), the national registers named and dated on every answer, and — spelled out — what this API deliberately does not claim. Only settled permissions are listed; the page grows as publishers answer, never ahead of them.

Changed

  • The two free endpoints are named wherever a buyer or an agent looks first — the landing endpoints list, the pricing page's free-endpoints tile, the documentation introduction, /llms.txt, the /v1 index and the npm README now all name GET|POST /v1/reference/validate and POST /v1/address/check as free. The npm README's tool table also caught up with reality: it listed five of the eight tools the package ships.
  • The landing page tells the pipeline as a forge — heat, strike, quench, stamp, ship — in a scroll-driven film, with the anvil mark and a new wordmark. Fully readable without JavaScript and with reduced motion respected, because a good share of the page's readers are agents, not humans.
  • The OpenAPI specification describes email verification. POST /v1/keys/generate documents the optional code field, the 403 and 503 it can answer, and the rate limit it actually applies (keys per network per day, not per email). A client generated from the specification alone could not previously get past a step every caller after the first has to take. code is deliberately not required: the first key on a network never needs it, and every client generated earlier posts the email alone.
  • Served surfaces name all three sanctions lists screened (OFAC, EU, UN), including the home page, the data-sources and compliance documentation in all three languages, the MCP tool description and the OpenAPI summary. A line-by-line guard test now fails if any of them narrows again.
  • Card payment amounts are recorded as the processor reports them rather than re-derived from the price of the pack bought. The derivation answers "what does this cost today", never "what did this customer pay", so a price change, discount or partial refund would have made the whole history wrong retroactively with nothing to show it. Historical rows keep no invented value and are reported as deduced.

[1.4.3] — 2026-08-06

Added

  • MCP tool annotations on the npm package — all five tools in ibanforge-mcp now declare title and readOnlyHint: true (plus idempotent/non-destructive hints), matching the remote server. MCP clients that gate tool calls on annotations (Claude Desktop, Claude Code, Cursor) stop asking for a per-call confirmation on what are pure reads.
  • sepa.vop_participant on every validate response — bank-level VoP readiness: true when the resolved institution is listed as ready in the EPC Verification of Payee scheme register (refreshed weekly), false when it is not, null when no institution was resolved. Country-level duty stays in sepa.vop_required. Under the Instant Payments Regulation, euro-area PSPs answer VoP since 2025-10-09, and the EPC VoP scheme rulebook v1.1 takes effect on 2026-09-20 — this field answers the half an integrator needs before initiating: is the recipient bank reachable for VoP at all? New /docs/vop page (EN/FR/DE) with the regulatory dates and the limits stated plainly.
  • bank_code_check.institution — what the national register publishes about the institution holding the bank code, served only on authoritative answers: full seat address for CH/LI (SIX) and AT (OeNB, plus the LEI), postal code + town for DE (the Bankleitzahlendatei has no street column), name only for BE (the BNB file has no address at all). Absent fields are null, never guessed; Finland stays without the block (its codes belong to banking groups). This is the institution allocated the code — not a branch, and not proof of any account.
  • Terms acceptance at the moment of commitment, on all three sales rails — the key dialog and the API landing form show an acceptance notice (EN/FR/DE), POST /v1/keys/generate returns terms_url, every 402 body carries a terms field, and the pricing page, Stripe success page and key-delivery emails link the Terms and the 14-day refund rule for unused card-paid packs.
  • Right-to-erasure toolingscripts/forget-customer.cjs deletes everything attributable to one customer email across every table that holds it (dry run by default), so the "deleted on request" promise in Privacy §4 / DPA §8 is honoured within its 30-day window.

Changed

  • Privacy Policy v1.1 and DPA v1.2 — the hosting region is now stated truthfully everywhere (Railway's Amsterdam (EU) region; only Railway's network edge sits in Zurich — for EU controllers processing stays within the EU, for Swiss customers the EU is adequate). Processor list completed (AI drafting assistance under redaction rules, CI infrastructure, dashboard traffic through Vercel functions, the x402.org fallback facilitator) and a new privacy section describes business contacts and prospecting, with the opt-out. Each document carries a dated revision note.
  • Served copy now promises only what the product performs — "vet a counterparty IBAN" became "check the bank behind a counterparty IBAN" across every descriptor (llms.txt, OpenAPI, MCP stdio+HTTP, discovery manifests, 402 bodies); the vendors page says "issuing bank identified" instead of "bank exists"; the "Live · Zurich" badge says "Live · Europe". MCP tool triggers no longer invite "is this a real bank" / "will the payment go through" questions, and validate_iban states its LIMITS explicitly. A repo-wide guard test (now scanning .mdx too) forbids account-level-verification phrasing from returning.
  • Retention now covers every per-request table — the 12-month purge reaches operations (where invalid-IBAN prefixes live) and feedback; the DPA 4.7 termination purge reaches operations; never-retrieved one-time key views are cleared after 7 days; the feedback endpoint stores the client IP as the same salted hash used everywhere else (existing rows migrated, raw column dropped). Owner Telegram notifications no longer carry customer email addresses.

Fixed

  • Two Belgian vacant slots served as banks named "VRIJ" — the register writes VRIJ in the BIC column for vacant slots, except two (154, 529) that carry N/A as BIC and VRIJ as their name, so the BIC-column filter served them as allocated banks. The name filter now matches the vacancy words whole-word too: Belgium drops 783 → 781 codes, matching the register's own arithmetic (781 allocated + 211 VRIJ + 8 reserved = 1000).
  • /docs/vop listed an "April 2026 real-time" milestone no primary source backs — replaced with the verifiable one: the EPC VoP scheme rulebook v1.1 and its API specifications take effect on 2026-09-20. (The regulatory deadline for euro-area PSPs was 2025-10-09, already in force.)
  • One canonical version of our dataset figures, everywhere — a third-party inventory found directories quoting five different versions of our own numbers, each copied from some surface of ours. Swept 80+ occurrences (README, marketing drafts, i18n messages, static llms.txt, glama.json, Postman collection, Odoo module description, served JSON-LD) to the canonical wording — 121k+ BIC, 39k+ LEI-enriched, 1,100+ Swiss entries, 89 countries — and added a repo-wide guard test that bans the stale variants, exempting dated snapshot lines.
  • JSON-LD slimmed to what is still consumed — FAQPage removed (Google dropped the FAQ rich result on 2026-05-07; ours also carried a stale "84 countries" and an unfounded Bazaar claim) along with the never-consumed WebAPI block; the HowTo example now uses the canonical demo IBAN instead of the phantom-clearing one this API exists to catch.
  • The EU sanctions feed silently degraded on 2026-08-02 — the weekly refresh ran in a CI environment where the EU consolidated-list download failed inside a best-effort catch, and the workflow committed a database whose served "(OFAC, EU)" coverage claim had become false. Database regenerated from the primary sources; the refresh workflow now runs the claims-vs-database guard test before it is allowed to commit, so degraded data fails loudly instead of shipping a lie.
  • Eight reserved Belgian bank-code slots were stored as allocated banks — the BNB register writes Onbeschikbaar (unavailable) for slots it reserves, and the seeder kept them, so code 539 — the bank code of the web's favourite example IBAN — resolved to a bank literally named "Onbeschikbaar". Reserved slots now drop like VRIJ ones and the example IBAN gets the authoritative not_in_register it deserves.
  • ibanforge-mcp 1.4.2 published — every outputSchema now declares nullable issuer types (an enum without null made MCP SDKs silently drop structuredContent on exactly the answers that matter most), bank_code_check / next_steps / vop_participant are declared, and the kyc / aml npm keywords are gone — they recruited the regulated name-screening use the Terms explicitly exclude.

Fixed

  • ⚠️ /v1/iban/compliance no longer returns a reassuring verdict for an IBAN it could not read. An IBAN that failed validation (bad checksum, unknown country, wrong length) was still scored, and scored 10 / low: the two "we could not check" penalties (no_sepa_instant, no_vop) added up to just under the 20-point medium threshold, so the less the API established, the safer its answer looked. Measured in production, a one-character typo took a Russian IBAN from critical / 90 / sanctioned_country down to low / 10. Such a response now carries risk_score: null, risk_level: "unassessable" and flags: ["iban_invalid"]. unassessable is the absence of a verdict, never a favourable one — do not fold it into a "safe to pay" branch. The scoring model itself is unchanged: a valid IBAN scores exactly as before.
  • The same fix reached the MCP transports, not only REST. The compliance response was assembled by hand in four places (REST route, HTTP MCP, stdio MCP, demo) and two had already drifted: the HTTP MCP transport — the one agents actually reach at /mcp — was the only surface omitting meta, so it never carried the bank_bic_only disclaimer; and three of the four derived country risk from a field that is absent when BBAN parsing fails. All four now call one shared assembly (buildComplianceResponse), which closes both divergences for free.

Changed

  • Dataset sizes are read from the data, not written by hand. Twenty-four files announced how much data ships, in four different values: the Swiss clearing table holds 1,165 rows and the product said "~1,200" sixty-one times, "1190+" once, "1,000+" four times. Served surfaces now interpolate datasetFacts(); static files (copy, manifests, the three locales) keep a literal that a test holds to be less than or equal to the live count. Every formatted figure rounds DOWN, so a claim survives a monthly refresh instead of becoming false the day it is written.
  • Latency claims say what they measure. The five x402 trust tags said p99 <50ms / <30 / <20 / <80 / <300 with no unit stated; server-side processing is 0.55 ms median but a client in Zurich observes p50 132 ms. And the MCP descriptions contradicted the x402 tags on batch by a factor of ten. One honest bound now, naming GET /ping so the caller can measure the network half himself.
  • MCP lookup_bic now also returns country: { code, name }, the shape REST has always used and that validate_iban already shared. The flat country_code / country_name pair is kept and deprecated since 1.4.0, removed no earlier than 2027-01-01. The two differ on the missing-name fallback on purpose: country.name falls back to the country code, country_name still answers null.
  • MCP validate_iban's documented shape matches what it returns. It promised bic: { code, institution, country_code, city } and returns bic: { code, bank_name, city }; the example showed an 11-character BIC where the service normalises to 8. For an agent the description IS the contract, so this was the expensive place to be wrong.
  • meta now dates both country signals. risk_indicators.country_risk is a separate editorial AML axis layered on top of compliance.sanctions.fatf_status, not a restatement of it, so the two can disagree on a country by design (Bulgaria: grey-listed and standard). Only one of them carried a date, which made a considered difference look like a stale list. Both are dated now and the response states the distinction. The sets themselves are unchanged: deriving one from the other would downgrade sanctioned countries.

Fixed

  • Swiss bank codes the SIX BankMaster no longer lists are no longer given an institution name. Four were: 00762 → "UBS Switzerland AG" (the bank code of the canonical example IBAN CH93 0076 2011 6238 5295 7, never an allocated IID — a fixture that leaked into production data), 31100 and 83036 → "radicant bank ag", 83015 → "++MBaer Merchant Bank AG in Liquidation", literal ++ artifact included. Pruned at load time against the register rather than edited out of the JSON, so the next monthly refresh cannot re-create the problem. Those codes now answer bic: null, which is the truth for an unallocated bank code.
  • compliance.risk_score is now nullable and compliance.risk_level gained the value unassessable. Declared everywhere the contract is published: OpenAPI, the x402 discovery schema, both MCP output schemas, and the TypeScript and Python SDKs. Consumers that switch on risk_level or do arithmetic on risk_score should handle both before upgrading. The playground now renders an unassessable verdict as a neutral "non évalué · —" chip instead of a green low · 0/100.

Added

  • Telemetry deletion after termination, by default (DPA 4.7) — request metadata attributable to a customer's API keys is now automatically deleted 30 days after the customer's last key is deactivated (revocation, subscription cancellation), instead of only on request. Key rotations don't trigger it — the customer relationship continues on the fresh key. Runs at boot and daily, next to the existing 12-month purge. DPA revised to v1.1 (numbered clause 4.7 + new Annex I with the full description of processing); Privacy Policy retention summary updated. +4 retention tests.
  • Full BBAN structural validation (all 89 countries)/v1/iban/validate now enforces the SWIFT IBAN Registry character structure of the BBAN on top of length + mod-97. DE17ABCDEFGH1234567890 (letters inside Germany's all-numeric bank code, mod-97-valid) is finally rejected with an agent-friendly invalid_bban_structure detail naming the field, the position and the expected charset. Check digits 00/01/99 (outside the ISO 13616 range 02–98) and non-numeric check digits are rejected as invalid_check_digitsCH99… used to validate. Patterns are cross-checked against three sources (registry Release 101 via python-stdnum, schwifty, ibantools — 8 ibantools divergences resolved against the registry) and precompiled at module load (hot path unchanged, ~0.0015 ms). A registry-conformance suite validates all 89 official sample IBANs to guard against over-rejection.
  • BBAN decomposition for the 47 missing countries (incl. SEPA members BG, RO, IS): they previously returned an empty bank_code, silently disabling BIC lookup, issuer classification and risk_indicators. /v1/iban/structure/:country now exposes per-field SWIFT charsets (4!a, 8!n…), the full bban_pattern, and an official example IBAN for every country (was 26).
  • Explicit paywall cause in 402 responses — when a request falls through to the paywall because of an exhausted monthly quota, used-up credit bundle or invalid/revoked API key, the 402 body now carries a cause object (reason, detail, plus quota/credits numbers) and a message stating the real situation, instead of the generic "authentication or payment required" that reads as "you are anonymous" to an authenticated client. New X-API-Key-Invalid header on the broken-key path. +3 integration tests. (Micro-audit conversion 2026-07-03: a trial user dying silently on the quota wall is invisible churn.)
  • MCP npm package 1.3.2 tells the truth on degraded results — the stdio package now relays the 402 cause in its _hint, labels quota/credits/key-caused fallbacks as DEGRADED RESULT (instead of the misleading "Anonymous mode" when a key is configured), no longer masks an invalid-IBAN 400 behind a "payment required" message, and stops promising that an API key raises the per-IP rate limit (it does not).
  • Self-service API-key lifecyclePOST /v1/keys/revoke (kill a leaked key, idempotent) and POST /v1/keys/rotate (mint a fresh key that atomically inherits the email, monthly limit and remaining credits, then deactivates the old one). +7 regression tests.
  • TypeScript SDK test suite — 25 Vitest specs (mocked fetch): base-URL normalization, Bearer-auth headers, request shapes, validateBatch input guards, full HTTP-status → typed-error mapping (401/403/402/429-quota/429-rate/4xx/5xx), timeout/network wrapping, and the usage() no-key precondition. The SDK previously had zero tests. Both SDK suites now run in CI on every push.
  • Monthly Swiss-clearing refresh — the ch_clearing table (SIX BankMaster) is now reseeded and committed by the existing monthly database cron, in the same workflow as the BIC database (both live in data/bic.sqlite, so one workflow / one commit avoids a same-file race).

Changed

  • Bank-level sanctions are now built from primary sources — OFAC SDN (US public domain) as the spine, with EU / UN / SECO consolidated lists best-effort — removing the runtime dependency on the CC-BY-NC OpenSanctions dataset. Country-level FATF/sanctions signals are unchanged. Net effect on BIC8 indicators: identical coverage minus one entity.

Fixed

  • Batch validation now bills 1 credit per IBAN on API keys — a /v1/iban/batch call of N IBANs debits N free-tier requests or N prepaid credits. Previously a whole batch billed a single unit, so a 100-IBAN batch cost the same as one validation — a ~100× underbilling that x402 callers never got (the x402 price was already $0.002 × N). Billing is all-or-nothing: a batch that exceeds the remaining allowance is refused with a machine-readable 402 naming the shortfall (credits_insufficient / monthly_quota_insufficient causes, X-Credits-Required + X-Credits-Remaining / X-Quota-Required + X-Quota-Remaining headers) and nothing is consumed; handler-level 4xx rejections refund the full pre-charge. Successful multi-unit charges are surfaced via X-Credits-Charged / X-Quota-Charged. Bundle descriptions now say "credits" instead of "calls" across the OpenAPI spec, x402 discovery metadata and llms.txt, and the batch's "10x cheaper" claim is corrected to the real 2.5× ($0.002 vs $0.005 per IBAN).
  • Russia FATF status was 'member' — factually wrong since its suspension on 24 Feb 2023. New suspended status (surfaced as-is in sanctions.fatf_status), scored at least as severely as non-membership (+10, fatf_suspended flag). FATF lists synced to the 17–19 June 2026 plenary: grey list +BA +IQ / −DZ −NA (22 jurisdictions), black list unchanged, fatf_as_of2026-06.
  • Compliance under-scored countries without a BBAN structure — country risk was read from risk_indicators (absent when BBAN parsing failed) and silently fell back to standard: production RU scored 60/high without the country flag. The country-risk axis is now derived straight from the country code; RU answers ≥80/critical with high_risk_country + sanctioned_country + fatf_suspended.
  • Revolut LT IBANs answered sct:false, no_vop — Revolut Bank UAB is EPC-registered under RVUALT2V while its customer IBANs resolve to BIC REVOLT21; the BIC8 join missed the membership. Documented EMI-alias step (refresh script + data): REVOLT21 now reports SCT / SCT_INST / SDD / VoP-ready. Wise (TRWIBEB1/TRWIGB22), N26 (NTSBDEB1) and Bunq (BUNQNL2A) verified already present under their IBAN-facing BICs.
  • 188/189 EBA STEP2 BIC entries had found:true, institution:null — the seed read the XLSX 'Comment' column instead of the institution-name column. Seed fixed and all 188 names backfilled from the official EBA file (e.g. ATPIITM7XXX → "A-Tono Payment Institute"); zero nameless entries remain across all sources.
  • QR-IID lookups were semantically invertedGET /v1/ch/clearing/30000 answered iid:"30000", qr_iid:"9000". BankMaster QR rows (range 30000–31999) carry the institution's standard IID in their QR-IID column; the lookup now presents iid = standard IID (09000), qr_iid = the queried QR-IID (30000), plus is_qr_iid: true and an explanatory note. Standard-IID lookups are byte-identical to before; QR-IBAN enrichment gains the same corrected semantics.
  • Spoof-resistant client-IP extraction — rate-limiting and stats now read x-real-ip (or the last, trusted-proxy hop of X-Forwarded-For) instead of the attacker-controlled first segment, so a forged X-Forwarded-For can no longer rotate around the rate limiter. Single shared extractor (extractClientIp) used by both call sites.
  • Swiss-clearing seed sanity floorseed-bc-nummer.ts aborts before dropping ch_clearing if the SIX BankMaster feed returns fewer than 800 rows, so a truncated download can never wipe the ~1190-row table under the unattended cron.

Notes

  • Two audit findings were investigated and confirmed false positives (no change, now documented in code): qr_iid is a genuinely distinct allocation column (not a copy of the clearing IID), and getCountryRisk is a deliberately separate AML axis that stacks on top of the DB FATF/sanctions signal — re-deriving it from the FATF table would downgrade sanctioned/grey-listed country scores.

[1.3.2] — 2026-06-03

Fixed

  • Python SDK now ships its py.typed marker (PEP 561). The package already advertised Typing :: Typed, but without the marker file downstream mypy/pyright silently ignored the inline type hints. pip install -U ibanforge (≥ 1.3.2) now gives type-checked autocompletion and signature checking. Python SDK only — the API, npm SDK and MCP server are unchanged at 1.3.1.

[1.3.1] — 2026-05-30

Fixed

  • Broken copy-paste SDK examples on /agents — corrected field names to the real response shape (bank_name, sepa.member, risk_indicators.country_risk, compliance.risk_score/risk_level, validateIban(iban)), plus stale country counts.
  • Coherence pass — every remaining "75+ countries" string aligned to the real 89 (layout meta in 3 languages, OG image, landing FAQ/meta/H3, blog articles, footer version, published MCP enum).

Changed

  • Frontend: wired the free-key modal end-to-end, lead with the Swiss / MCP USP, 3-rail pricing.
  • /fr/famille reworked into a clear, illustrated FAQ.
  • Release process — hybrid procedure documented (RELEASING.md): PyPI + MCP Registry publish automatically (MCP via GitHub Actions OIDC), npm publishes manually behind the 2026 npm 2FA approval gate; mcp-publisher installed from its release binary.
  • TypeScript SDK published as @ibanforge/sdk with corrected package metadata and an accurate README.

[1.3.0] — 2026-05-29

Added

  • BBAN structure for LT, EE, LV, MT, CY — revives EMI / virtual-IBAN detection in those jurisdictions.
  • EBA Clearing STEP2 SCT as an official SEPA reachability source (+201 BICs); EMI classification extended via the EBA / FCA registers.
  • Compliance transparency — every response now discloses scope, a disclaimer, and data-freshness metadata.
  • Discovery surfaces/agents.txt plain-text index, agent.json and mcp.json path aliases, regenerated llms.txt / mcp.json to 1.3.0 truth.
  • Stripe Checkout credit-pack rail + success page that retrieves the API key once.

Changed

  • Hardened the compliance enrichment pipeline (primary-source ingestion).
  • Aligned tool schemas and prices across all three MCP surfaces (stdio, HTTP, card).

Fixed

  • Flag XX-country BICs as test BICs; cap oversized IBAN input.
  • Corrected bank-code drift against the SWIFT IBAN Registry.
  • Correctness, data-accuracy and SDK-parity fixes surfaced by the 4.8 multi-agent audit.

[1.2.0] — 2026-04-29

Added

  • PyPI Python SDK ibanforge 1.1.0 — pip install ibanforge. Sync (IBANforge) + async (AsyncIBANforge) clients, 6 endpoints (format_iban, validate_iban, validate_batch, lookup_bic, lookup_ch_clearing, check_compliance), 1-line free key generator (IBANforge.generate_api_key("you@example.com")), TypedDict response shapes, 6 typed exception classes (AuthError, PaymentRequiredError, QuotaExhaustedError, RateLimitError, InvalidInputError, APIError, IBANforgeError), 16 respx-mocked tests, MIT license. https://pypi.org/project/ibanforge/
  • Free GET /v1/iban/format endpoint — pure mod-97 + structure check, no DB hits, no API key, no quota. Returns valid/invalid + bban breakdown + upgrade_to_full_validation hint pointing to the paid /v1/iban/validate ($0.005). Lets agents pre-filter malformed IBANs before paying for full enrichment.
  • Glama containerized releasemcp/Dockerfile (two-stage, Node 20-slim, non-root user) registered on https://glama.ai/mcp/servers/cammac-creator/ibanforge. Server Coherence ✅ unlocked, Tool Definition Quality scanning enabled, badge upgrade D → A pending.
  • /agents page (EN/FR/DE) — agent-first integration guide with 3 paths (MCP, free key, x402)
  • /openapi page — interactive Scalar API reference (try-it-out, codegen)
  • 6 JSON-LD schemas at the layout level (SoftwareApplication, Organization, FAQPage, HowTo, BreadcrumbList, WebAPI) for richer agent + SEO discovery
  • Design tokens ported from the previous Vite version: --ink-0..5, --fg-1..5, --amber-50..700, --swiss-500/600, --risk-{low,med,high}, --syn-*, pulse-live and blink animations, .eyebrow, .kv-grid, .endpoint-row, .tnum, .tracking-caps utility classes
  • Reusable components: StatusDot, RiskChip, EndpointRow, ApiKeyDialog with provider
  • Per-locale <title>, <meta description>, <html lang>, hreflang alternates (EN/FR/DE)
  • Compliance-bundle endpoint POST /v1/iban/compliance ($0.02) advertised in pricing, calculator, landing
  • 5th endpoint GET /v1/ch/clearing/:iid ($0.003) advertised in pricing, calculator, landing
  • Persistent volume declaration in railway.toml for stats.sqlite (api keys, quotas, revenue)
  • WAL mode + busy_timeout + 5 missing indices on stats.sqlite for concurrent throughput
  • Permissions-Policy header denies camera/microphone/geolocation/payment/usb
  • Content-Security-Policy on HTML responses (landing, MCP card)
  • IBANFORGE_FREE_MODE env flag for explicit production free mode (loud warning)
  • Trust signals appended to all 5 paid 402 descriptions (production status, p99 latency, dataset size, version) — agents that filter on description quality reward this
  • outputSchema with bare-output examples on every accept entry (CyberSapper recipe from CDP Discord) — unblocks CDP catalog + agentic.market indexing

Changed

  • ensureWalletConfigured now fail-closes in production: missing X402_ENABLED or WALLET_ADDRESS triggers a boot crash instead of silent fail-open
  • API-key middleware: when monthly quota is exhausted, the request now falls through to the x402 middleware (advertises payment requirements) instead of returning a hard 429 dead-end. Agents can keep using IBANforge by paying per call.
  • Dashboard auth refactored: SESSION_SECRET is now a distinct env var (not the password), session token includes a signed iat, comparison is timing-safe
  • CORS_ORIGIN must be explicit in production (no wildcard); boot crashes if missing or *
  • Frontend next upgraded 16.2.2 → 16.2.4 (fixes high-severity DoS advisory in Server Components)
  • Backend npm audit fix resolves transitive postcss/hono path-traversal advisories

Fixed

  • Per-locale metadata was being overridden on the home route by a static EN export — removed the override so /fr and /de now serve localized titles + descriptions
  • <html lang> was always en regardless of locale
  • 30+ hardcoded user-visible strings (Copy/Copied in code blocks, uptime tooltips, locale-aware date formatting in monitoring) now go through next-intl
  • 2 blocking ESLint errors (setState-in-effect, JSX-in-try/catch) resolved
  • Stale dashboard rate-limit comment clarifies per-Lambda scope

Security

  • New SESSION_SECRET requirement for dashboard cookies (independent of password)
  • Constant-time login response delays prevent timing-attacks
  • CSP + Permissions-Policy on HTML
  • Strict CORS in production

Operational

The website is now served by the Next.js project (Vercel project ibanforge, repo subfolder frontend/). The previous Vite design-system project (ibanforge-design-system) is orphaned (no domain attached) and can be archived after a 30-day rollback window.

Migration notes

For self-hosted deployments:

  1. Set SESSION_SECRET in production (openssl rand -hex 32)
  2. Set CORS_ORIGIN to an explicit comma-separated list of your origins
  3. Verify the Railway volume is mounted at /app/data (boot logs warn if not)
  4. To run in explicit free mode in production, set IBANFORGE_FREE_MODE=true