Aller au contenu
IBANforge

Changelog

Tous les changements notables de l'API, des SDK et du serveur MCP.

Politique de versionnage & dépréciation

L'API /v1 est stable : des champs peuvent être ajoutés, jamais renommés ni supprimés au sein de v1. Un changement cassant passerait par un nouveau chemin majeur (/v2) avec au moins 6 mois de fonctionnement en parallèle, annoncé ici et par email aux détenteurs de clés actives. Le projet suit le versionnage sémantique.

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

[Unreleased]

Changed

  • The entry credit pack costs $4 (was $5). 1k = 1,000 credits for $4.00, by card or in USDC, so $0.004 a credit: less than paying per call for a validation ($0.005) or a compliance check ($0.02), more than a BIC lookup ($0.003) or an x402 batch IBAN ($0.002). GET /v1/credits/bundles, the 402 bodies, the x402 discovery and the pricing page all quote this price. The 5k and 25k packs and the Pro plan are unchanged.
  • The creditor file audit is priced in US dollars, like everything else. 149 USD up to 5,000 rows, 349 USD up to 20,000 rows (the same figures were charged in CHF until now). Contract change on /v1/audit/upload, /v1/audit/status/:job and /v1/audit/checkout/:job: the field price_chf is renamed price and currency carries the ISO code of that price (USD for every new job; a job created before the switch keeps CHF and is charged in CHF). tiers[].price_chf is tiers[].price. The admin audit statistics gain revenue_usd and payment_amounts.usd; revenue_chf keeps the francs of before.
  • Finland is a prudent register until its list is re-read. The Finance Finland list behind bank_code_check is a hand transcription dated 15.10.2025 that nothing refreshes. A hit still names the banking group and its BIC, dated from the list; a code the list does not carry no longer answers not_in_register / not_allocated with authoritative: true, it falls through to the composite answer with authoritative: false, and next_steps never says bank_code_not_allocated for Finland.
  • Pro subscription: a customer portal and a paragraph in the Terms. Pro subscribers manage their card, their invoices and their cancellation in the Stripe customer portal (linked from the pricing page, the key e-mail and §3 of the Terms of Service, version 1.5 of 16 September 2026).
  • Terms of Service 1.6: a service for professionals. §1 of the Terms now states that IBANforge is built for, and sold to, businesses and professionals, and that every paid offer is sold to professional customers only: paying confirms that the buyer acts for their trade, business, craft or profession. Checkout asks for nothing more: giving a VAT number stays optional. Version 1.6 applies immediately to new customers and, from 23 October 2026 (30 days' notice, §9 of the Terms), to customers who accepted an earlier version.
  • Discovery documents and first lines say what is checked, country by country. In /.well-known/agents.json (and its aliases) the capabilities swift_lookup, sanctions_screening and vop_check are replaced by bank_code_register_check, bank_level_sanctions_screening and vop_readiness (bic_lookup is kept). The MCP server card gains a free_access field, one sentence per free way in, read from the constants the API applies. The first lines read by agents (MCP descriptions, the A2A card, the x402 catalogue and 402 descriptions, the OpenAPI overview, both llms.txt files, the README) are rewritten: the national registers that settle a bank code are named, sanctions: the OFAC, EU and UN lists are said to be matched on the bank's BIC8, and the country checked against a fixed list of sanctioned jurisdictions, VoP is the bank's listing in the EPC register, the BIC directory says that about two thirds of its rows are a public copy frozen in January 2018, and the national check digits of the BBAN are listed as not checked yet. No data route changes a field, a value or a verdict; only the explanatory note of GET /v1/bic for a BIC absent from the directory is reworded.

Fixed

  • An IBAN in a request path is masked however it is written. Only an IBAN written in one block was masked in request_log (kept twelve months) and, in capitals only, in the console log. Written in lowercase in the console, or in groups in either journal — spaces, %20, +, dashes, dots, underscores, no-break spaces, one group per segment, inside braces — it was not masked. Both journals now share one rule (redactIbanShapedValues in src/lib/stats.ts): escapes decoded, separators dropped, case ignored, then the shape of an IBAN (a registry country code, two digits, 15 to 34 characters; mod 97 for a country the table does not list). A mistyped IBAN is masked too. An IBAN spread over several segments is stored as :redacted/:redacted, so it never joins a one-segment family such as GET /v1/bic/:code. Registered routes are unchanged (every pattern is checked by a test).
  • /mcp: check_compliance accepts meta.sources: null. The API sends null when the compliance database carries no metadata; the output schema refused it, so every check_compliance answer would have failed its own validation on that day. Same fix as in the npm package (PR 232).

[1.7.0] — 2026-09-15

Added

  • A durable key for an agent, approved by a human in a browser: the device grant (RFC 8628). POST /v1/keys/device opens a request and returns a short code and a link; the agent shows both to its human and never opens the link itself; the human opens ibanforge.com/device, sees who is asking and what for, and approves in one click — with no address at all (the smaller free allowance) or with a mailbox verified by a 6-digit code (the full one); the agent collects the key exactly once at POST /v1/keys/device/token (long-polling, authorization_pending and slow_down as the RFC spells them) together with the configuration line to paste. The two MCP tools request_api_key and poll_api_key wrap that journey on the three MCP surfaces (the npm package, the stdio server of this repository and the remote /mcp endpoint) and stay free past the daily MCP allowance, because they are the way out of it. The whole quota guard lives in one module: the request shares the daily per-network budget with POST /v1/keys/generate in both directions (a pending request is a promised key), an hourly cap bounds the churn of requests, the approval page holds a short-lived token so a cross-site request cannot approve or refuse, unknown, expired and already-decided codes answer one uniform 404 at one uniform pace, the key is handed over once even under concurrent collection, and a key approved that nobody ever collected is revoked by the daily purge. The page is served in three languages, is not indexed, and drops the code from its address before its first request. The key is never shown on the page.

Changed

  • ibanforge-mcp: thirteen tools. The two device-grant tools join the package; the instructions sent at connection name them; the poll_api_key relay waits longer than the server's long-polling so the client never loses the race by a hair; a refused request (device_rate_limited) and a pending poll come back as data with a display_to_human block, never as tool errors, because they are the way out of the daily limit rather than a failure of it.
  • Agent-facing texts say the approval door is open. The consent block, the MCP instructions of the three surfaces, both llms.txt files (the full one listed five tools where the transport served nine), the /v1 and /mcp discovery documents, the OpenAPI contract and the two READMEs now describe request_api_key and poll_api_key as available; the card checkout opened from the API remains announced as planned.

Fixed

  • Device grant, after its adversarial review. A key approved but never collected is now revoked with its revocation date, so the retention purge of terminated keys finds it; the loser of a race between two approval tabs no longer keeps an active key that no request carries and no longer sees "Done"; the delay of an unknown-code answer holds a place in the same counter as long-polling; a lookup can no longer return an approval token that nothing recorded; the extension of a request when a verification code is mailed follows every mailing, capped at two lifetimes from creation, instead of once; an operations probe watches the long-polling ceiling; the contract of POST /v1/keys/device/token lists the global request limiter.

[1.6.0] — 2026-09-15

Changed

  • ibanforge-mcp: a refused call stays a refused call. The npm package bounds every request (IBANFORGE_TIMEOUT_MS, 30 s by default, body included), never retries a POST on its own, keeps the API's cause and the Retry-After value, and turns a non-JSON 200 into an explicit invalid_response. The format-only fallback is reserved for keyless callers and is marked _degraded: true / _scope: "format_only" with the upstream status, so an agent cannot read a checksum as a bank verdict; a configured key that is exhausted is an explicit error rather than a silent downgrade; a refused batch is no longer expanded into one free request per IBAN. The instructions the installed server sends at connection say that it calls the REST API and shares its keyless trial, distinct from the remote /mcp allowance; the README and the hint on a 402 no longer promise an x402 wallet the package does not have, and the configuration examples no longer ship a placeholder key.

  • One definition of each monthly allowance. The free monthly figure used to exist in nine independent copies plus four bare literals; every surface that quotes an allowance now reads it from src/lib/tiers.ts, and a guard test refuses the wordings that state a single free tier, or an e-mail address as the only door. The same guard now sweeps every file that publishes the consent sentence and fails if one of them drops the boundary that goes with it.

  • A 402 no longer promises an allowance will come back on the 1st when it never will. A key whose allowance is measured over its whole life — one issued with a reduced allowance while automated signups are being screened, or one raised against a payment, which grants the full allowance once rather than monthly — was answered with the monthly wording, and its usage was attributed to the current month while the ceiling was being read across every month. The wall now says which of the two it is, in the sentence, in cause.quota.resets and in a new X-Quota-Basis response header; an exhausted anonymous key is offered the free claim before any purchase, and a key already past that step is not offered a door the route would refuse.

  • 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

  • SDKs: a key without an address, in one call. IBANforge.generateApiKey() (TypeScript) and IBANforge.generate_api_key() (Python, sync and async) accept no argument and return the anonymous key with its tier and claim_url; an explicit address and code keep the historical path. The creation call never carries an ambient key from the environment, the Python one sends the SDK's User-Agent, and the TypeScript timeout now covers reading the JSON body. The quickstart fixture was re-recorded from the real route against a throwaway database.

  • A free API key that asks for nothing. POST /v1/keys/generate with no body at all now returns an ifk_ key on the spot: no address, no card, nothing to confirm, nothing mailed and no record opened. It carries the smaller of the two free monthly allowances. The e-mail address became optional on that route, and the mailbox check that used to gate a second key from one network moved to the new POST /v1/keys/claim, which raises that same key — same secret, same prefix, same history — to the full free allowance against a 6-digit code mailed to an address the caller chooses to give. An x402 settlement made on the key also claims it, for a one-off allowance rather than a monthly one, and the texts say so wherever they name that rail. The tier survives POST /v1/keys/rotate: rotating is not a way to reset an allowance. Announced with a consent sentence written for agents rather than for people — the empty POST first, then the exact words to put to their human ("Use my address … to create a free IBANforge key") and, travelling with it everywhere, the line that says when not to use it: never send an address your human has not handed you for this purpose — on the 402 body, both llms.txt files, the MCP instructions and the tool cost line of the three transports, the /v1 and .well-known discovery documents and the npm package's README (the OpenAPI contract documents the routes, not the consent sentence). Why: an agent asked to fetch a key refused, in its own words, rather than register a person's address with a third party — and it was right to. The address was never what the free tier was protecting; the key was.

  • Poland: the settlement number's own check digit, served beside the register verdict. A Polish IBAN opens on the eight-digit numer rozliczeniowy (three digits of bank, four of unit, one check digit under NBP's numbering ordinance). bank_code_check.check_digit now says whether the eight digits form a number NBP could have issued — weights 3, 9, 7, 1, 3, 9, 7 over the first seven, complement modulo 10, pinned by the published numbers of NBP, PKO BP, Santander/Erste and mBank because the ordinance text is not machine-readable. valid: false reads as a typo; it never becomes not_allocated, which stays reserved for an authoritative register. This settles the August atlas' open question: the IBAN carries the eight digits, not the ECB's three-digit institution number.

  • Greenland: registration number 6471 resolves to Grønlandsbanken. The Danish FSA's register of banks and the account details Greenlandic public institutions publish agree on 6471; the IBAN registry's own example for GL carries it. The curated map gains GL:6471 → GRENGLGXXXX and loses GL:1601, a number that had no source anywhere and was served as verified.

  • MCP: audit_creditor_file and audit_status, the creditor-file audit as a tool call. The npm package (ibanforge-mcp) now wraps POST /v1/audit/upload, POST /v1/audit/checkout/:job and GET /v1/audit/status/:job: an agent hands over a CSV or XLSX of creditors as base64 and gets back the free preview (masked IBANs, per-row findings, file-wide summary) plus a job id, with a plain-language note that the full annotated .xlsx is a paid deliverable (149 CHF up to 5,000 rows, 349 CHF up to 20,000) settled through a one-off Stripe Checkout Session — never paid automatically. checkout: true also returns the Checkout URL for a human to open; audit_status polls payment and returns the download link once paid. A file over 5 MB (mirroring AUDIT_MAX_BYTES) is refused locally, before any network call. Eleven tools on the npm package now; deliberately npm-only for the moment (Stripe pricing, not x402/API-key) — scripts/mcp-parity.test.ts records the gap against the other two MCP surfaces as dated and named rather than silent.

  • UK firm lookup in the FCA Financial Services Register, built ahead of its key. GET /v1/gb/firm/{frn} serves one regulated firm per request — name, register status and its effective date, business type, Companies House number, client-money permission, PSD/EMD and MLR statuses, the register's notices — with the source, the retrieval date, the cache state and the FCA's own disclaimer on every answer. Same price as a BIC lookup; a miss is a 200 with found: false, charged and cached like a hit so nobody scans the FRN space at the register's expense. The FCA's written permission of 07/09/2026 came with four conditions and each lives in the code: one request in flight and a floor between calls with a single honoured wait on 429 (src/lib/fca-register.ts); no marketing use, enforced by fca-register.marketing-guard.test.ts, which fails if the CRM or the prospecting ever imports the client; only the firm resource is called, never the register's individuals; the disclaimer verbatim. Cached one day in stats.sqlite (fca_firm_cache), an expired copy served marked stale only while the register is down and never more than six hours past its day. Until FCA_REGISTER_API_KEY and FCA_REGISTER_API_EMAIL are set the route answers 503 not_configured before any key or payment is read, is absent from the x402 route table, and llms.txt says so; OpenAPI (lookupGbFirm, GbFirmResult) and the docs page (/docs/gb-firms, EN/FR/DE) describe it already. The request and response shapes were confirmed from three open-source clients that record real exchanges, the portal itself rendering nothing without a browser; the rate limit is a constant to align on the portal's figure the day the key lands.

  • Search Console, read every Monday instead of never. The property https://ibanforge.com/ had been verified since August and never read; the first reading, taken by hand on 06/09/2026 with two throwaway scripts, said a handful of clicks a week — which is how we learned the signups do not come from Google. GET /v1/admin/search-console makes that reading permanent: four complete Monday-to-Sunday weeks (clicks, impressions, CTR recomputed from the totals, position weighted by impressions), the ten first queries and the ten first pages over 28 days, the sitemap's submitted-versus-indexed counters and last read, and the index verdict of eight witness URLs — one per family, because URL inspection is quota'd at 2 000 calls a day. The window stops at J-3, where Search Console actually has data, and the card says so rather than letting the reader assume it runs to this morning; a week that is not over is left out whole, since a half-collected Sunday beside four full weeks reads as a collapse. No SDK and no new dependency: a service-account JWT signed with node:crypto and exchanged for a read-only token (src/lib/search-console.ts), cached six hours in stats.sqlite so opening the dashboard does not call Google, ?refresh=1 to spend a reading on purpose. Three answers rather than two: 503 when GSC_SA_JSON is absent (every laptop, and not an incident), 502 carrying the last reading marked stale when Google refuses — the dashboard's own reader keeps that body, so an outage shows last week's figures instead of an empty box — and 200 otherwise. An impossible figure is null and never 0: a quiet week has no CTR and no rank, and "position 0" would claim better than first place.

  • Twenty-five keyless validations a day, per address, on the REST API. POST /v1/iban/validate with a real iban and no credential at all is now served in full — enrichment included — up to 25 times a day per source address (IPv6 counted per /64), resetting at midnight UTC. The response carries a trial block (calls_used_today, calls_left_today, daily_limit, resets, the one request that mints a free key, and the docs link) and cost_usdc: 0: nobody was charged and nothing is booked as revenue. Past the ceiling the route answers 402 again with cause.reason: "trial_exhausted", the count served today, the reset and the free-key route — and, unlike an exhausted key, it keeps the free_tier rail in the body, because this caller has no key to exhaust. Parity with the HTTP MCP taster, which has had the same allowance since July: a developer's first contact is a terminal, and until now the curl in our own documentation hit a paywall before printing a single field. The x402 discovery probe is untouched: an empty {} body, a body-less POST or a bare GET still return the 402 envelope the indexers read — the trial is granted only when a real IBAN is present. A key that is presented and invalid keeps its invalid_api_key 402 rather than falling into the trial. Counted in the service database (src/lib/daily-ip-ledger.ts, extracted from the MCP route), so the count survives a redeploy; the block now points at the key that needs no address rather than at one that needs an e-mail, announced in /v1, llms.txt, llms-full.txt, .well-known/rate-limits.yml, .well-known/auth.md, the OpenAPI contract and the quickstart and API-keys docs in the three languages. Measured on the dashboard's doors card as an "Essai sans clé" group: address-days that tried, address-days that hit the ceiling, and the conversion that matters — keys born with source=api-trial.

  • San Marino: a register that names holders without covering the space. The Central Bank of the Republic of San Marino publishes the four banks it supervises — name, registered office, ABI code and BIC — and IBANforge now serves them: a listed code answers verified with an institution block and the BIC the Central Bank pairs with it (bic.basis: national_register). What it does not do is turn an absence into a denial. The page lists operating banks, not the allocation of the ABI code space; San Marino also licenses non-bank payment and e-money institutions, and its own official example IBAN carries an ABI absent from the page. So bank_code_check.authoritative stays false and a miss keeps the answer San Marino always gave — absent_from_reference_data, never not_allocated. It is also the one place where the two authority flags part company in the opposite direction from Switzerland: the code space is not the Central Bank’s to settle, but the BIC beside a code it lists is its own pairing. Pages /sm and /sm/{code}, doc /docs/sm-bank-codes in EN, FR and DE, each carrying that caveat rather than burying it. Measured before the change: every San Marino IBAN answered bic: null, including all four real banks — the eleven curated SM: keys are four-letter BIC stems while a San Marino IBAN carries five digits, so they could never match. The licence is recorded as unknown: bcsm.sm publishes no terms of use at all, so the credit is given by choice, the date is the day we read the page, and a letter to the Central Bank is queued.

  • Slovakia is the eighth authoritative bank-code country. The Národná banka Slovenska publishes the prevodník of identification codes for the domestic payment system and allocates the codes it lists, so a Slovak payment code absent from it is held by nobody: bank_code_check.authoritative: true, and bic.basis: national_register because the register publishes the BIC per code. 38 codes, four of them allocated with no BIC at all, eight held by Czech institutions publishing a Czech BIC — the register lists them, so the API serves them rather than requiring the BIC country to match the IBAN country. Slovakia's own official example IBAN, SK31 1200 0000 1987 4263 7541, now answers not_in_register / not_allocated: bank code 1200 is not in the register, exactly as Austria's AT61 1904… already reported. Pages /sk and /sk/{code} for every code (the whole register, not a first batch), doc /docs/sk-bank-codes in EN, FR and DE. The curated map disagreed with the register in both directions before this — 6 keys named a bank the register no longer lists, 7 of its codes were missing from the map — and is now pruned against it. Attribution lives in the database (national_bank_codes.source / .as_of, read by every surface): the NBS site terms permit reuse without prior consent provided the source is named and the file is unaltered, so names are served verbatim with their diacritics and the credit carries the edition and effective date the seeder actually read.

  • 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

  • MCP: the declared output schemas match the answers served. A conformant MCP client validates each tool result against the outputSchema published by tools/list and rejects an undeclared property in a closed object, so a German, Swiss, French or British validation was served with HTTP 200 and isError: false and still thrown away by the official client: the schema omitted the BIC's source, as_of, lei, lei_status, address and postal_address, the register's institution and the Polish check_digit on bank_code_check, the Swiss qr_iid_source, the UK modulus_check, pra_authorisation and psd_registration, and the screened flags of the compliance block. validate_iban, batch_validate_iban and check_compliance now share one definition of the enriched result, and a new test drives the real HTTP route through the official MCP client with every published discovery example and still rejects a mistyped verdict. No register and no verdict changed.

  • CRM: stamps are shown in Swiss time, and a scheduled draft says so. Every msg_date is stored in UTC (the API, the IMAP sync and the scheduled sends all write UTC), and the journal, the thread and the draft card printed the digits as stored — a draft the VPS would send at 10:22 read "08:22", and at 09:50 looked overdue. lib/crm/zurich.ts converts with the European DST rule (no Intl in client components), the journal groups days in Swiss time, and a draft with origin claude carries the badge « programmé » instead of « brouillon ».

  • Creditor-file audit: the row cap runs before the parse, and uploads have a budget per address. A 4.5 MB workbook of 150 000 rows used to cost about 1.3 s of synchronous CPU before its too_many_rows, on a keyless route the general limiter allowed a hundred times a minute (adversarial review of 07/09/2026, A1). SheetJS now stops reading at the cap (sheetRows), text files are refused on their byte-level line count before decoding, and POST /v1/audit/upload allows five uploads every ten minutes per address, keyed on the platform-appended hop.

  • /v1/ops/recent no longer publishes the operations counter. The row key was the table's auto-increment id, i.e. the API's throughput in clear for anyone reading the feed twice (F1). Rows now carry an opaque cursor, and ?after= takes it back verbatim; a cursor that is not ours yields the current window, never an empty feed.

  • Dashboard login limiter keys on the address the platform sets. It counted the first x-forwarded-for segment, the one the client writes (M1); it now reads x-real-ip, then x-vercel-forwarded-for, then the last x-forwarded-for hop.

  • CSP report endpoint: thirty reports a minute per address, and only about our pages. A forged stream could fill the function logs before the 05/10 reading (F7).

  • Weekly sanctions refresh survives one bad week upstream. On 06/09 the EU list answered HTTP 500 and the run shipped OFAC and UN only, which the claims gate rightly refused — at the price of the whole week's refresh. A list whose download fails is now carried over from the previous database when that database is younger than 21 days, recorded in metadata.carried_over; older than that, the run fails as before.

  • 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@company.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

Le changelog est tenu en anglais.