Zum Inhalt springen
IBANforge

Changelog

Alle nennenswerten Änderungen an API, SDKs und MCP-Server.

Versionierungs- & Deprecation-Politik

Die /v1-API ist stabil: Felder werden nur hinzugefügt, innerhalb von v1 nie umbenannt oder entfernt. Breaking Changes erschienen als neuer Major-Pfad (/v2) mit mindestens 6 Monaten Parallelbetrieb, angekündigt hier und per E-Mail an aktive Schlüsselinhaber. Das Projekt folgt 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

  • Announced: the website and the dashboard move from Vercel to a server in Switzerland on or after 19 October 2026 (8 October 2026). They will run on a virtual server in Infomaniak's public cloud in Switzerland, which we operate. Infomaniak Network SA, already a sub-processor for e-mail and backups, takes over the hosting from Vercel Inc., which leaves the list; no sub-processor is added, and the API does not move. Until 9 November 2026, the old Vercel deployment stays idle, as a way back if something goes wrong. Every active key with an address is told by e-mail between 8 and 12 October. Under DPA clause 5, a customer may object on reasonable grounds until 12 November 2026, in which case either party may terminate. The exact day of the move will be added here.
  • Privacy Policy 1.8 (8 October 2026). Section 1 names, among the uses of the e-mail address, at most one note from the founder asking whether the key is useful, and says that a reply STOP ends everything but legal and security notices; the payment data row says that we keep the payer's e-mail address, the amount and the currency of each purchase, and that an invoice you ask for carries the name and address you give us and is kept ten years, as Swiss accounting law requires. Nothing new is collected.

Fixed

  • A hosted MCP session opened with a valid API key is no longer refused by the per-address session ceiling (8 October 2026). The daily ceiling on new sessions now counts only the sessions opened without a valid key, per source address; Claude connectors, which all leave from the same addresses, no longer turn away a client that sends its key. A key keeps at most 30 live sessions: opening one more closes the least recently used session of that same key. An unknown, revoked or absent key keeps the per-address ceiling.
  • The end-of-subscription e-mail carries no purchase links for an address that replied STOP (8 October 2026). The notice itself is still sent: the key stays active and the e-mail says what it has left.
  • The Odoo module states both free allowances (8 October 2026). 200 requests a month with an e-mail address, 25 without, in its settings, field help, README and store description.
  • The MCP pages link to OpenAI's current guide for connecting a server to ChatGPT (7 October 2026). The previous developer-mode link answered 404.

[1.9.0] — 2026-10-07

Added

  • The hosted MCP server reads your API key (7 October 2026). https://api.ibanforge.com/mcp now accepts an IBANforge key in Authorization: Bearer ifk_… or X-API-Key, the two headers the REST API reads, and counts each tool call against that key exactly as the REST API does: the key's own allowance, one unit per call and one per IBAN in a batch, nothing on the free tools, the same refusals. Without a key nothing changes. A key in the URL is not read. An unknown, revoked or spent key gets the REST answer for that key and never falls back to the keyless allowance. The MCP pages say where the key goes in Claude Code, in Claude and Claude Desktop (Request headers of a custom connector) and in ChatGPT, which sends none.
  • Greek bank codes: the Hellenic Bank Association's HEBIC index, as a partial register served from a private file (7 October 2026). When GR_REGISTER_PATH is set, a listed code names its holder in bank_code_check, with the exact credit and the association's Important Note in full in register. An absent code keeps its previous answer, never not_allocated. Without the variable nothing changes.
  • The Monday bulletin keeps the week's alert history (7 October 2026). Each operational alert writes one row when its message leaves and closes it when a success closes the alert; the private bulletin shows the alerts opened and closed during the week, and says when a week predates the history instead of showing zero.
  • Proposals with Yes, Later and No in the Monday bulletin (7 October 2026). At most three a week: those posted by the operator and two rules without a model (a country whose BIC lookups keep missing, a watch with no Yes for four weeks). Yes and No never come back; Later comes back 28 days after the answer.
  • The two Monday watches deposit their summary in the bulletin (7 October 2026). POST /internal/bulletin/:source, behind its own token, takes at most three plain-text lines and, for the AI recommendation baseline, a score; their Telegram messages are unchanged.
  • Money received minus costs in the Monday bulletin (7 October 2026). For the previous month and the month to date: Stripe payments for packs, subscriptions and audits minus refunds, Stripe fees, and costs entered by the operator with their period and nature. Each currency stays its own; a cost that does not cover the whole period is unknown, never prorated nor counted as zero, and the result is then given as an upper bound.
  • A Security and trust page, in English, German and French (6 October 2026). /trust, /de/trust and /fr/trust state who runs IBANforge, where the API and the website run (with the command that checks each one), what is kept and for how long, the sub-processors with their transfer basis and Data Privacy Framework register entries (checked on 5 October 2026), the Swiss adequacy decision, security, availability stated plainly, where the data comes from, what happens if the service stops, and what we do not have. Linked from the footer and the Legal Notice.
  • German courtesy translations of the Legal Notice, Privacy Policy, DPA (AVV), SLA and Terms (6 October 2026). Served on /de/legal/..., each with a notice that the English version prevails.
  • German IBANs: the account number's own check digit is checked by the method the Deutsche Bundesbank lists for the bank code (6 October 2026). The method comes from field 9 of the Bankleitzahlendatei, and the result is served in national_check_digits (scheme: "de_pruefziffer", with method, verified_by, source and table_fetched_on). verified_by says what the verdict rests on: bundesbank_test_numbers (the method passes every test number and worked example the Bundesbank publishes; a fail adds the blocking step national_check_digits_failed) or independent_implementation (the Bundesbank publishes no test number for the method, which was verified against an independent implementation; a fail adds the warning step national_check_digits_suspect and never stops a payment). Method 09 answers not_applicable; method 44 and bank codes outside the Bundesbank file answer not_checked. valid never moves.
  • next_steps gains national_check_digits_suspect, a warning step, Germany only (6 October 2026). The file audit reports the finding of the same name as a warning, not an error.
  • GET /health reports the German check-digit table (6 October 2026): de_pruefziffer (available, fetched_on, bank_codes).
  • A file audit line whose bank could not be consulted says so (29 September 2026). A line whose bank_code_check is unavailable (no bank-code data we may use for the country, or a register not loaded) now carries the finding bank_not_consulted ("Bank not consulted", a warning, not an error) in the workbook, the free preview and the summary; it used to come out OK when nothing else fired.

Changed

  • A STOP also silences the usage warnings (7 October 2026). An address that answered STOP no longer receives the 80% monthly quota warning nor the 10% credit-pack warning. Payment receipts, the keys and codes a person asks for, and legal notices keep being sent.
  • request_api_key and poll_api_key say where the key goes (7 October 2026). The same text on the three MCP surfaces names the config_line for Claude Code, the Authorization: Bearer header for the hosted server, and says that ChatGPT sends no key to a connector. The npm package carries it from 1.9.0.
  • The n8n node passes n8n's community-package scan (7 October 2026). n8n-nodes-ibanforge 0.1.3 fixes the six findings of n8n's review tool on 0.1.2, ships an MIT LICENSE and an example workflow, and aligns its labels on n8n's UX guidelines; it is published by the existing trusted-publishing path.
  • The Odoo module's store page says where an IBAN goes (7 October 2026). ibanforge_bank_autofill now states that the IBAN is sent to the IBANforge API, with the retention described in the Privacy Policy; banner first, support and maintainer in the manifest, README table fixed.
  • 31 of the 44 Italian bank codes left without a BIC on 29 September now have one, from open sources only (7 October 2026). For the Italian branch of a foreign bank, the European Central Bank's list of monetary financial institutions gives the LEI of its head office, and the BIC is the single Italian one GLEIF pairs with that LEI (23 codes). Last, the BIC a bank publishes on its own website (8 codes, Poste Italiane 07601 among them; address, date and quoted words in scripts/data/it-bank-site-bics.json). The 13 other codes still answer without a BIC. No other answer changes.
  • GET /v1/admin/stripe-revenue adds ibanforge_days (7 October 2026). The packs, subscriptions and audits of each Swiss day, so the bulletin reads a period without a second Stripe read.
  • The key dialog says what the address is used for (7 October 2026). Instead of "nothing else is mailed to you", it now says the address also brings at most one note from the founder asking whether the key is useful, with a link to receive nothing more; same sentence on the agent approval page, in English, French and German.
  • The founder's note carries a stop link (7 October 2026). It ends with "Reply STOP, or use this link" and a mailto link to support with STOP as its subject.
  • The website moves to Next.js 16.3.8 and next-intl 4.14.9 (7 October 2026). Next.js 16.3.8 is a security release (server-side request forgery in image optimisation, cache poisoning of statically generated and incrementally regenerated pages, cache leaks across use cache values). Also lucide-react 1.50.0, shiki 4.5.0, shadcn 4.21.1 and @types/three 0.186.0.
  • Hono 4.13.12, @hono/node-server 2.1.3 and the x402 packages 2.28.0 (7 October 2026). The two Hono releases carry a path-decoding fix in serveStatic, which the API does not use. x402 2.28.0 adds default stablecoins for Arc and Monad testnet; nothing changes on Base.
  • Vitest 5.0.3 in every test suite (7 October 2026). The root, frontend/, mcp/ and sdks/typescript run the same version, with ESLint 10.12.0 at the root. Vitest 5 declares Node 22.12 or later; mcp/ and sdks/typescript are still tested on Node 20, where npm prints a warning and every test passes.
  • indexnow.yml uses actions/checkout@v7 and actions/setup-node@v7 (7 October 2026). It was the last workflow still on v4.
  • The /audit page reads its two tiers from one constant (7 October 2026). Prices and row ceilings on the page and in its structured data come from lib/audit-tiers.ts; French and German now write "149 $" like the home page, and a test keeps the audit's sentences to those two prices and ceilings.
  • German figures follow the house grouping (7 October 2026). The Pro quota on the home page is read from the pricing constant and grouped with a no-break space ("10 000", no longer "10.000"); our own thousands on /pricing, /audit, /vendors and /sheets follow the same rule.
  • Dashboard doors named after the home page of 28 September (7 October 2026). Every door of the current home page, the file-audit pricing link included, has a label in three languages and appears in page order.
  • DPA 1.6 (6 October 2026). The Processor informs the Controller immediately if an instruction appears to infringe data protection law; sub-processors are bound in writing to obligations no less protective (art. 28(4) GDPR); a new Annex II lists each sub-processor's role, region and transfer mechanism. A new sub-processor is announced at least 30 days before it receives personal data, by e-mail to the address of each active key (keys without an address: the changelog only) as well as in the changelog. Clause 7 provides for audits, including an inspection, once per calendar year at most and on 30 days' notice, where the documents are not sufficient. New clause 4.8 describes the backups of the account state, which already existed: every night to a server of Infomaniak in Switzerland, 30 days of copies, and monthly copies, the last three kept, on a computer of the operator in Switzerland; a deleted e-mail address ages out of them within 90 days. Every change adds an obligation on our side or describes processing that already took place, so 1.6 applies immediately.
  • Privacy Policy 1.7 (6 October 2026). Section 2 states where the API and the website's functions run instead of "processing stays within the EU", and describes the backups of the account state; Infomaniak's role includes them; a deleted e-mail address ages out of the backups within 90 days; the edge log of our API host is described as keeping the request path, so never put an IBAN in a URL; the API never sends an IBAN to Anthropic.
  • national_check_digits.status can now be not_checked (6 October 2026). Germany only: a bank code outside the Bundesbank file, or a method not checked here.
  • The monthly refresh writes data/de-pruefziffer.json beside the Bankleitzahlendatei (6 October 2026). It holds the check-digit method of each German bank code; data/bic.sqlite is unchanged.
  • The API redeploys only when its image can change (6 October 2026). railway.toml lists the paths that go into the API image (watchPatterns); a push that only touches the website or the documentation no longer restarts the API.
  • Terms of Service 1.11: six months' notice before the Service stops, for every paying customer (5 October 2026). §3 now promises Pro subscribers, holders of credit packs and Editor/OEM customers at least six months' notice, by e-mail and on the Terms page, before IBANforge is discontinued (previously 30 days, Pro subscribers only); §9 gives the same six months before we end a paying customer's keys for any reason other than abuse; §10 names the operator's seat without calling it registered. Only obligations on our side are added, so 1.11 applies immediately.
  • The Legal Notice gives the operator's postal address, and says the business is not in the commercial register and not registered for VAT (5 October 2026). Swiss law (art. 3 para. 1 let. s of the Unfair Competition Act) asks an online seller for an address shown on the site, not one given on request.
  • Privacy Policy 1.6 and DPA 1.5 describe the processing more precisely (5 October 2026). For an invalid IBAN at most 4 characters are kept (country code and check digits), as the code does, not 12; the edge log of our API host keeps the raw IP address and the request path for 7 days, outside our own logs; the website's functions run in the Frankfurt region; the processor list says which processors can receive submitted IBANs; DPA clause 6 names the European Commission's adequacy decision for Switzerland. Nothing new is collected.
  • The website's functions run in Vercel's Frankfurt region (5 October 2026). They ran in Washington, Vercel's default, while the DPA placed processing in the EU. frontend/vercel.json now pins fra1.
  • SLA 1.1: the status page reference is the last 30 days (5 October 2026). The page has shown 30 days since 1 September; the SLA still said 90. The commitment and the credits are unchanged.
  • One security contact everywhere (5 October 2026). Both security.txt files, SECURITY.md and the Legal Notice now give support@ibanforge.com with "SECURITY" in the subject. The site's file pointed to public GitHub issues, which SECURITY.md asks reporters not to use, and the API's file had no Expires field, which RFC 9116 requires.
  • The QR-bill check no longer gives a wrong deadline for a combined (type K) address (2 October 2026). Every surface of POST /v1/ch/qr-bill/check and of the MCP tool check_swiss_qr_bill said to convert "before 14.11.2026", the day banks would "stop processing" type K QR-bills. No published source says so, and the source string credited a SIX factsheet of 19.08.2025 with a sentence it does not contain. The combined_address finding now says what the documents say: type K has not been permitted since 21.11.2025 (SIX, QR-bill IG 2.3), the banks guarantee payment of a type K QR-bill only until the end of September 2026 (UBS: 30.09.2026; ZKB: end of September 2026 at the latest), and from 14.11.2026 any payment order with an unstructured address is refused (SIX). The finding carries its own source, naming those documents; the other findings cite the QR-bill guidelines alone. The field ready_for_2026_11_14 keeps its name and its meaning (valid, and every present address of type S); its description now says the name is not a deadline. Same sentences in the OpenAPI contract, the MCP and A2A descriptions, the docs in English, French and German, the QR-bill tool page, /llms.txt, the README and the Postman collection. The npm package ibanforge-mcp carries the new wording from its next release.
  • Every Belgian answer says the list is free on the National Bank of Belgium's website (1 October 2026). Asked in writing, the National Bank of Belgium answered that our source wording is appropriate and that its Copyright and Reuse Policy applies, which asks that recipients be told beforehand that the information can also be consulted free of charge on its website. The name of the Belgian register in bank_code_check.register now carries that sentence. The same day, the Magyar Nemzeti Bank confirmed in writing that its routing table may be used to return information about VIBER participants without any attribution requirement, procedure or fee; NOTICE records both answers.
  • The Swedish keys of the composite map name their publisher and carry its disclaimer (1 October 2026). The 36 Swedish keys come from the list "IBAN-ID och BIC-adress för banker" of Bankinfrastruktur i Sverige AB. Asked in writing, Finance Sweden answered for BSAB that the list is public, under a disclaimer: BSAB does not guarantee that the published information is accurate, and inaccuracies are reported to BSAB. Every answer served from these keys now says so in bic.source, and NOTICE moves the list to group A.
  • Terms of Service 1.10: what a Pro subscriber is owed if the Service stops (29 September 2026). §3 now says that if IBANforge is ever discontinued, every Pro subscriber is told at least 30 days before it stops, by e-mail and on the Terms page, and that no renewal is charged for the month in which it stops. The version only adds an obligation on our side and applies immediately to every customer.
  • The Italian and Romanian keys taken from commercial sites are rebuilt from open data where it names one BIC (29 September 2026). The Italian keys of the composite map whose code and BIC had come from a commercial IBAN site are rebuilt from the LEI the Banca d'Italia publishes for the ABI code and the BIC GLEIF pairs with that LEI (the SWIFT BIC-to-LEI Mapping Table): most keep their BIC; some now carry the bank's own BIC instead of its central institution's, or another branch code of the same BIC8. Among those keys, a code without a LEI, whose LEI GLEIF pairs with no single BIC, or that the registers do not list (Poste Italiane's 07601 among them) no longer has a bic block: the register's verdict beside it is unchanged, and a code outside the registers answers not_in_register with reason: "absent_from_reference_data" and authoritative: false, never a refusal. The map's other Italian keys, taken from national files or added by hand without a named source, are unchanged. The Romanian keys are rebuilt from GLEIF alone (a Romanian bank code is the first four letters of the BIC) where exactly one Romanian BIC8 of GLEIF matches, each with the same BIC as before; the other Romanian codes answer as any Romanian code the map does not carry: through the directory prefix search, which names its source (basis: "directory_prefix", with candidates where several BIC8 match), or with no BIC when the directory has none.
  • The central bank's credit on every answer served from the Slovenian, Lithuanian, Hungarian and Croatian keys of the composite map (29 September 2026). These keys come from the files of Banka Slovenije, Lietuvos bankas, the Magyar Nemzeti Bank and Hrvatska narodna banka, which ask to be named. bic.source now reads IBANforge curated bank-code map; followed by Vir: Banka Slovenije. This information is available free of charge on the Banka Slovenije website (www.bsi.si)., Source: Lietuvos bankas, Forrás: Magyar Nemzeti Bank or Izvor: HNB. No other field changes, and other countries keep the bare source.

Fixed

  • The agent key door names the approval button by its place (7 October 2026). The block an agent shows its human word for word said Click "Get the key", while the approval page follows the browser's language ("Obtenir la clé", "Schlüssel holen"). It now says to press the first button under the code.
  • request_api_key no longer promises a key for the current session (7 October 2026). No MCP surface uses the key in the session that asked for it: the hosted transport reads no key, the npm package reads IBANFORGE_API_KEY at start-up only, and the embedded stdio server reads none. The description now says the key works once placed in the MCP client configuration (config_line) or sent to the REST API. The npm package carries the new text from its next release.
  • A STOP is honoured for good (7 October 2026). An inbound STOP (or "unsubscribe"), as the subject or the first line, is recorded by the daily pass; neither the founder note nor the activation reminder is ever prepared for that address again, even after its thread is deleted from the CRM. A sentence that merely contains the word is not a STOP.
  • The Legal Notice says what a card payment receipt shows (7 October 2026). It claimed that receipts state the operator's full details; Stripe's payment receipts name IBANforge and its support address only. An invoice with the full details is issued on request at support@ibanforge.com.
  • The home page's live check explains itself without JavaScript (7 October 2026). Its buttons wait for a script; with JavaScript off, a note now says so and links to a real API answer at api.ibanforge.com/v1/demo.
  • Dead code of the former lens removed (7 October 2026). The .forge and .reveal styles, the Reveal component, four unused images and 67 message keys no page read.

Removed

  • Bank-code to BIC pairings no publisher granted us, from this repository and from the service (29 September 2026). The composite map src/db/bic_data.json loses the keys an import had taken from commercial IBAN sites or from publishers that reserve commercial use. The United Arab Emirates, Bosnia and Herzegovina, Estonia, Georgia, Kazakhstan, Moldova, Serbia and Türkiye lose all of theirs: a valid IBAN of these countries now answers bic: null and bank_code_check.status: "unavailable" with reason: "no_reference_data_for_country" and bank_code_holder: "unknown", where it used to name a bank; the directory prefix search no longer answers in the map's place for Georgian and Moldovan two-letter codes. On POST /v1/iban/compliance these IBANs resolve no bank, so the bank is not screened: sanctions.bank_screened is false and sanctions.institution_listed is null (bank_sanctioned keeps its documented false when no bank was screened), the flags carry no_bank_resolved and a new bank_code_data_unavailable, and risk_score is held at 50 (elevated) at least, a floor and not a weight, like sanctions_lists_unavailable. A few Georgian, Kazakh and Turkish codes whose bank is on a public sanctions list no longer get that bank-level match; they score elevated rather than critical, and say that the bank was not screened. Spain loses the keys taken from a commercial site and keeps the ones taken from national files: a Spanish code the map no longer carries answers not_in_register / absent_from_reference_data with no BIC, like any code it never carried. Every other country answers exactly as before, apart from the credit added to bic.source in Slovenia, Lithuania, Hungary and Croatia (see Changed), measured code by code (scripts/audit/curated-map-replay.ts). Earlier commits keep copies, still subject to their publishers' terms (see NOTICE, group C).

Security

  • MCP TypeScript SDK 1.32.1 across the API, the ibanforge-mcp package tree and the site (6 October 2026). From 1.12.0 to 1.30.x, the SDK's OAuth client could send credentials to an authorization server chosen by the MCP server (GHSA-6qxp-vccf-f47h). IBANforge only uses the server side in production, the client appears in tests, and the dependency floor is now ^1.31.0 in both manifests. The update also gives the hosted /mcp transport a 4 MiB request-body limit and a 100-message cap on JSON-RPC batches.
  • sharp 0.35.5 and two transitive site dependencies at patched versions (6 October 2026). sharp (image optimisation in Next.js, librsvg vulnerability), postcss-selector-parser (quadratic parsing time, in the stylesheet build) and @ai-sdk/provider-utils (uncontrolled resource consumption, pulled in by the API reference page) now resolve to fixed releases. The last two go through scoped overrides because their parents pin the old versions; the built stylesheets are byte-identical before and after.
  • Two advisories stay open for lack of a patched release (6 October 2026). braces and sprintf-js, both transitive in the site tree, have no fixed version published; they are re-checked on every dependency run.
  • Every open code-scanning alert is fixed or documented (6 October 2026). CodeQL, enabled the same day, raised 31 alerts. Ten were real weaknesses, all minor, and are fixed with a test each; the others are false positives (mostly test code, and SHA-256 digests of 256-bit random API keys, which are not passwords), recorded with their reason.
  • Counts of our resources in third-party catalogs match the exact API host (6 October 2026). The marketplace radar and the weekly discoverability canary counted any listing that began with https://api.ibanforge.com, including https://api.ibanforge.com.attacker.example; the host is now parsed and compared.
  • The admin key import only accepts the key shape the generator mints (6 October 2026). POST /v1/admin/keys/import required only the ifk_ prefix; it now requires ifk_ followed by 64 lowercase hexadecimal characters, so an imported key is a 256-bit random secret like any generated one.
  • No quadratic regular expression on input we do not control (6 October 2026). The TypeScript SDK trims the trailing slashes of its base URL, and the CRM reads recipient domains, with linear loops instead of regular expressions that slowed down on long runs of / or -.
  • Markup is stripped more robustly in internal tools (6 October 2026). HTML entities are decoded in one pass (&amp;lt; stays &lt;), end tags such as </script > close the script they end, and the Czech National Bank page loses every HTML comment, an unclosed one included.
  • Smaller hardening (6 October 2026). The IndexNow workflow's token is limited to contents: read; the BIC enrichment script clones without a shell; test IBANs draw their account digits with crypto.randomInt; the blog lists only posts whose slug the post page accepts; the dashboard no longer hides a referrer whose name merely ends in ibanforge.com.

[1.8.1] — 2026-09-29

Added

  • Receipts on the account page (28 September 2026). Below the keys, https://ibanforge.com/account now lists the credit packs and subscriptions paid with the signed-in address, the most recent first: date, pack or subscription, amount, status (paid, refunded, disputed) and the key it landed on. For a pack paid by card, Receipt opens its Stripe receipt; for a card subscription, Invoices opens the Stripe customer portal; a USDC payment is marked as such. A card payment belongs to the address its payer gave at checkout, and to that address alone (a key recharged by someone else does not show their receipt to the key's owner); a USDC payment, and a card payment recorded before the purchase register existed, belong to the address of the key they landed on, rotated or deactivated keys included. Nothing pending, failed or handed over by us is listed. Two new routes behind the account session, documented in the OpenAPI Account tag, both llms.txt, GET /v1 (endpoints.account) and the API keys page of the documentation in three languages: GET /v1/account/receipts (the list, which calls nothing at Stripe and carries no receipt link; each purchase is named by an opaque ref, never by a sequential number) and GET /v1/account/receipt?ref= (asks Stripe for a fresh receipt link at the click, since Stripe expires a receipt link 30 days after giving it; the same 404 receipt_not_found for an unknown ref, another address's purchase or a purchase without a card receipt; 503 receipt_unavailable when Stripe does not answer). The link is kept one hour in memory, never stored or logged; two simultaneous clicks share one call to Stripe, and a failure is kept one minute. The mail of a pack bought by card and the mail of a recharge now say where the receipt is, and so does the Stripe success page. Every field is added; nothing is renamed or removed.
  • A dispute that closes without loss gives the credits back (26 September 2026). When Stripe reports that a dispute on a pack paid by card has closed and the money stays with us (charge.dispute.closed with the status won, or warning_closed for an inquiry that never became a dispute), the credits the dispute had taken back return to the key they were taken from, or to the active key of its lineage after a rotation: exactly what was taken, once, and only when the dispute that closes is the one that took them. A lost dispute gives nothing back (if Stripe later reports it won, the credits come back then), and neither does a pack refunded in the meantime: a full refund of a disputed pack now marks it refunded. A second dispute on the same payment, or a partial refund while the dispute is open, blocks the automatic return and says so in an alert. Such a purchase counts as a sale again (reinstated).
  • Pro on the key you already hold, and a subscription that ends no longer deactivates the key (25 September 2026). The Pro link served to a key (topup.pro in GET /v1/keys/usage and GET /v1/credits/balance, credit_packs.topup_this_key.pro in a 402 served to the key, actions.subscribe_pro on the account page) puts the subscription on THAT key: its allowance replaces the key's own for as long as the subscription lives, and any prepaid credits left on the key come after it. A key that already carries a subscription is not offered a second one; a second payment made anyway delivers a new key. When the subscription ends, the key is no longer deactivated: it returns to what it had before the subscription (its free allowance if it had one, and any credits left on it), and a key created by the subscription itself answers 402 with the links that recharge it or subscribe it again, without being replaced. The success page of a Pro checkout made on a key names the key that now carries Pro, and never shows a key. An anonymous key that takes Pro leaves the anonymous tier (it can no longer be claimed by e-mail) and gets its anonymous monthly allowance back when the subscription ends; one that bought credits first keeps no free allowance. The account page shows a key with neither allowance nor credits as none, never as free. Keys deactivated by a cancellation before this change stay deactivated.
  • A refunded or disputed card pack takes its credits back by itself (25 September 2026). When Stripe reports a full refund (charge.refunded) or a dispute (charge.dispute.created) on a pack paid by card, the credits of that pack are removed from the key it landed on, or from the active key of its lineage after a rotation: never more than the pack, never below zero, and the key stays active. A partial refund takes nothing back and is left to a human; a dispute won later gives nothing back on its own. A refund or dispute that matches no pack is acknowledged and ignored.
  • The same key when the offer changes: a credit pack recharges the key you already hold (25 September 2026). Presenting a valid key on POST /v1/credits/buy/1k|5k|25k now credits THAT key: the answer carries same_key: true, recharged: true, credits_added and echoes the key you presented as api_key, so a client that switches to the returned key keeps using the same one; presenting the key costs no request. By card, every key has a recharge reference (ifr_ and 32 hexadecimal characters, drawn at random, never derived from the key, kept across rotations) carried by the Stripe payment links under topup.by_card, in GET /v1/keys/usage, GET /v1/keys/report, GET /v1/credits/balance, in credit_packs.topup_this_key of a 402 answered to a valid key, and as Recharge this key buttons on the account page; a pack paid through one of them lands on that key, and the success page and a confirmation mail say which key was recharged. A pack bought without a key, or through the links of the pricing page, is a new key as before; a reference that no longer leads to an active key also mints a new key, never reactivating a revoked one. On a key that holds an allowance AND credits, each call draws on the monthly allowance first, then on the credits; a batch that crosses from one to the other is split between them, all or nothing, and every billed response says which paid it in X-Charged-From (allowance, credits or allowance+credits). When the credits run out the key stays valid: a free key goes back to its monthly allowance, and a key born of a purchase answers 402 credits_exhausted with the links that recharge it (X-Credits-Topup-Url, cause.credits.topup). GET /v1/keys/usage keeps the basis of the allowance on such a key and adds credits_remaining, credits_total and billing_order: "allowance_then_credits"; GET /v1/credits/balance gains allowance, billing_order and topup, and now accepts the key as X-API-Key or ?api_key= as well. The account page shows both balances and a Recharge this key button per key; for a key whose address was never proven, it asks you to recognise the key before paying (address_proven in GET /v1/account/overview, where actions.topup now carries the three links). The 80% and 10% alert mails link the packs of the key they are about, and the 80% one says that the credits take over on a key that has some.
  • Italian bank codes are read in the Banca d'Italia's own registers, including the codes it has struck off (25 September 2026). The registers of banks, payment institutions and e-money institutions the Banca d'Italia publishes as open data under CC BY 4.0, with their history and its list of mergers and incorporations, become Italy's partial register, beside San Marino's: they list the institutions the Banca d'Italia registers, not the allocation of the ABI code space (Poste Italiane, the Banca d'Italia itself and the Italian branches of EU payment institutions hold codes outside them), so authoritative stays false and no Italian answer can become not_allocated. A code in force comes back verified with the institution named as the register writes it (bank_code_check.institution: registered office in Italy, LEI where published) and bank_code_holder: "confirmed". A code the Banca d'Italia has struck off comes back verified with retired: true, the new field retired_on (the last day the register lists the code for its last holder) and, where one exists, superseded_by: the legal successor by merger or incorporation, followed to a code in force, which is not necessarily the bank that now holds the account. It carries bank_code_holder: "inferred", since nobody holds the code today, no BIC, and the next_steps step bank_code_retired asks for the beneficiary's current details; it is never a refusal. What changes for callers: the Italian codes in force that our composite map answered now name the Banca d'Italia; the codes in force it did not carry, which came back not_in_register, are now verified; the codes the Banca d'Italia has struck off, which our map answered verified under the name of a bank that no longer holds them (03111, UBI Banca's code, answered "Banca Carige"), are now retired with no BIC, and so is the official Italian example of the ISO 13616 registry (ABI 05428, Banca Popolare di Bergamo, absorbed in 2017, whose legal successor today is Intesa Sanpaolo, 03069); a code the registers never listed keeps exactly the answer it had. The register is re-read every week; as_of is the edition, the date of the Banca d'Italia's file, and every answer that uses it carries the CC BY 4.0 credit in bank_code_check.register. New public pages /it and /it/{code} (codes in force and struck off), a documentation page /docs/it-bank-codes in three languages, and the country page /iban/it names the register. The OpenAPI and x402 descriptions of bank_code_check and the MCP output schema of validate_iban gain retired_on; retired and superseded_by say what they mean when authoritative is false.
  • The national check digits inside the BBAN are checked for France, Monaco, Belgium, Italy, San Marino and Spain (25 September 2026). A valid IBAN of these countries now carries a national_check_digits block (country; scheme: fr_rib_key, be_mod97, it_cin or es_dc; status: pass or fail; a detail sentence on a fail), and checks.national_check_digits repeats its status instead of not_checked. It is a second check, independent of mod-97: the French and Monegasque RIB key, the Belgian check digits (the first ten digits modulo 97), the Italian and San Marino CIN, the Spanish DC. pass means the account number is well formed, not that the account exists; fail means it cannot have been issued as written. Strictly additive: valid, bank_code_holder, the risk score, next_steps and every other field are unchanged, on POST /v1/iban/validate, /v1/iban/batch, /v1/iban/compliance and the MCP tools, whose output schemas declare the block. The United Kingdom keeps modulus_check; the German account-number methods are not checked yet. Our own examples now pass the check: the Belgian e-money example of the playground and the four San Marino register pages carried a national key that did not match (a fixed CIN letter for San Marino), and the playground shows a failed national key the way it already showed a failed UK modulus check.
  • A national check key that fails now stops the payment in next_steps (26 September 2026). When a valid IBAN of France, Monaco, Belgium, Italy, San Marino or Spain carries a national check key that does not match (national_check_digits.status: "fail"), next_steps now carries the blocking step national_check_digits_failed, on the same footing as modulus_check_failed: do not send, the account number cannot have been issued as written, ask the beneficiary to confirm it. Its because names the field and the algorithm (national_check_digits.status is fail (fr_rib_key), be_mod97, it_cin or es_dc). The step comes after bank_code_not_allocated, verify_payee_name and modulus_check_failed, before every other step, and no other step is added or removed. For these answers only, this replaces the "next_steps unchanged" of the entry of 25 September. Served on POST /v1/iban/validate, each item of POST /v1/iban/batch, POST /v1/iban/compliance and the MCP tools. Strictly additive: an answer whose national key passes, or that carries no national_check_digits block, does not change. The file audit counts it as a blocking finding, like the UK modulus check: such a row now reads "Do not pay" (finding "National check key fails", in English, French and German), with the sentence of national_check_digits.detail. The published list of step codes (OpenAPI and x402 descriptions of next_steps) now names it, together with modulus_check_failed, which it omitted; the MCP documentation and the validate_iban description of the MCP server say that both mean stop.
  • An account page that needs no key to paste (25 September 2026). https://ibanforge.com/account shows every active key attached to an e-mail address: its plan, what is left this month or its credit balance, the calls this month, the last call, the alerts mailed, the link that manages a Pro or Editor / OEM subscription and, on demand, the 30-day report of one key. It shows the first characters of a key, never the key itself. Signing in takes a 6-digit code mailed to that address, valid 15 minutes: no password, no link in the mail, and the same answer whether or not the address holds a key. The session is a sign-in cookie of 7 days that only reads: it opens no paid route, and rotating or revoking a key still takes the key itself, which can still be pasted on the same page. Five public routes serve the page: POST /v1/account/code, POST /v1/account/session, GET /v1/account/overview, GET /v1/account/keys/report?prefix= and POST /v1/account/logout, documented in the OpenAPI under a new Account tag with a cookie security scheme, in both llms.txt, in GET /v1 (endpoints.account) and on the API keys page of the documentation, in three languages. Every mail sent about a key (free key, pack purchase, first-call reminder, the 80% and 10% alerts, Pro and Editor / OEM welcome) says to sign in there with the address of the mail, and links https://ibanforge.com/account rather than /en/account; the Stripe success page says to sign in with the address typed at checkout, as text and never inside the link. A 402 answered to a valid key gains account_page, a sentence with the address of that page; it is absent without a key and with an invalid one, and never in the PAYMENT-REQUIRED header. Every field is added; nothing is renamed or removed.
  • Czech bank codes are checked against the Czech National Bank's own register. The ČNB's Číselník kódů platebního styku v České republice is the allocation of the code in IBAN positions 5-8 by law (decree 169/2011 Coll., § 4 c and § 6 (2)), so the Czech Republic joins the countries whose register settles a negative, eight in all: a code it holds comes back verified with authoritative: true and the provider named as the ČNB writes it (bank_code_check.institution), a code it does not hold comes back not_in_register / not_allocated with authoritative: true. The BIC the ČNB publishes for a code is served with basis: "national_register". Every answer the register decides carries the credit its terms require, "Zdroj: ČNB", in bank_code_check.register, and bic.source names the edition ("Zdroj: ČNB, Číselník kódů platebního styku v ČR, verze 254"); as_of is the edition's effective date. The ČNB publishes each edition ahead of its effective date, and not always for the 1st: the register is re-read every day, an announced edition waits beside the one in force and takes over at midnight in Prague on its date, never on the day it was downloaded, and one answer (or one batch) is always read from a single edition. What changes for callers: twelve allocated codes that used to come back not_in_register (among them Banking Circle 6600, Partners Banka 6363, Modrá pyramida 7990 and the other building societies) are now verified; two codes the ČNB removed (4000, in April 2025 on a bank merger, and 8280, in December 2024), which used to come back verified with the BIC of an institution that no longer holds them (and, for 4000, its name, EXPOBANK CZ), are now not_allocated, with no BIC and no bank named; every Czech answer that resolved through our composite map now names the ČNB instead. The country page /iban/cz, the API's /llms.txt, the README, both llms files of the site, the documentation (validation, IBAN to BIC, data sources, Finnish bank codes, Swiss QR-IBAN), the comparison page, the OpenAPI and x402 descriptions of bank_code_check and bic.basis, the validate_iban descriptions and output schema of both MCP servers and /roadmap.md list the eight countries; the contract texts now build that list from the code that decides the verdict, and a test fails when a register joins without them. /roadmap.md no longer counts Finland among the registers that settle a negative. The API's /llms.txt follows a Czech edition switch without a restart. The monthly proposal of registers to plug no longer proposes a country whose register is already read in full.
  • A private overlay for the data we may serve but not redistribute. The rows whose licence does not allow publishing them in this repository (EBA STEP2, NBP and OeNB directory rows, the Austrian, Belgian and San Marino registers, the Bank of England PRA list, the UN list and both EPC registers) are listed once, in src/lib/restricted-family.ts. An API instance can now receive them from a separate private file per database, named by RESTRICTED_BIC_OVERLAY_PATH and RESTRICTED_COMPLIANCE_OVERLAY_PATH, the way LU_REGISTER_PATH already brings the Luxembourg register. At start-up the fresh public database is copied next to the private file and the overlay is merged into the copy; the public database is never modified. Member by member, the fresher data is served: the overlay's rows when the public database has none, when the overlay is strictly newer (dated the same way on both sides) or when both are identical; otherwise the public rows are kept (kept_public), and the served last_refresh is never fresher than a member served from the overlay. Before merging, the file is checked (SQLite integrity, format version, expected tables and columns, no view or trigger, nothing outside the family, a row floor and a content fingerprint per member) and its SHA-256 is logged; tables are created from the definitions in the constant, never from SQL read in the overlay. A member that fails is left out and the others are served; a file that fails is left out entirely, the reason is logged and an operations alert is raised. The last accepted overlay is kept next to the private file and served at start-up if the file named by the variable is refused; a replaced file is reloaded without a restart (checked every ten minutes, only the database whose file changed), and a new file that fails its checks, or that would stop serving a member served today, leaves the current one in service. GET /health gains restricted_overlays, one state per database (off, applied, kept_public, partial, refused, or pending before the database is opened) with the first twelve characters of the served file's SHA-256, and fallback: true when the last accepted overlay is served. npm run overlay -- extract writes both files from the current databases without downloading anything, and refuses to write inside any git repository or through a link; the seeders of the family can write to a chosen database (BIC_DB_PATH, COMPLIANCE_DB_PATH, SEED_FAMILY=restricted), chained by npm run overlay:seed. Without the variables the public databases are served as before; /health only gains the restricted_overlays field (off).
  • The private overlay now refreshes itself: the API pulls it from a private repository. A private repository of ours rebuilds the overlay every week (compliance) and every month (BIC), commits no data, and publishes a release holding both files and a manifest.json (SHA-256, size, generation date, public commit and rows per member of each file, format in src/lib/restricted-overlay-manifest.ts), only after a quality gate: the loader's checks (npm run overlay -- check), then no file or member lost and no member down more than 10% against the previous release (npm run overlay -- manifest --previous; npm run overlay -- verify checks a downloaded release against its manifest). With RESTRICTED_OVERLAY_PULL_REPO and RESTRICTED_OVERLAY_PULL_TOKEN (a fine-grained token that can only read that repository), the API's ten-minute watcher pulls the latest release every four hours plus a random delay of up to thirty minutes, one hour after a failure, never at start-up and never blocking. A file whose hash is already served, in place or accepted is not downloaded again, across restarts too; otherwise the download is capped, its size and hash are checked, the loader's checks must accept every member, and the file named by RESTRICTED_BIC_OVERLAY_PATH or RESTRICTED_COMPLIANCE_OVERLAY_PATH is replaced atomically, then reloaded at once. A database whose variable is unset is not pulled, and any failure keeps what is served. The token never follows GitHub's redirect to its storage, and neither the token nor the repository name appears in logs, alerts or /health. Two operations alerts close on their own: overlay:pull after 24 hours without a successful pull, overlay:pull:stale when the latest release is older than 9 days or a file is older than 9 days (compliance) or 35 days (BIC). Without the two variables nothing changes and no network call is made; /health gains restricted_overlays.pull, off, and otherwise the state, the last attempt and success, the latest release, and for each database the release and generation date of the served file with its age in days.
  • "Valid" is no longer the only word about the bank: bank_code_holder and checks. Every valid IBAN now says who holds its bank code: confirmed (a register names the holder), inferred (our composite map, the prefix fallback or a published structural rule names one, which does not settle it), not_allocated (the national register says nobody holds it) or unknown. checks gives one status per check, pass, fail, inferred, unknown, not_checked or not_applicable, and lists what IBANforge never checks: the payee's name, whether the account exists, sanctions on the payee. valid is unchanged and still means "well formed". Both fields come right after valid in the answer. Served by POST /v1/iban/validate, each item of POST /v1/iban/batch, POST /v1/iban/compliance (where checks also fills institution_sanctions and country_sanctions) and the MCP tools validate_iban, batch_validate_iban and check_compliance.
  • A BIC named from the frozen directory copy now says so, and whether a current list still carries it. bic.source_as_of now also appears on curated-map answers when the directory row behind the name, the city, the LEI or the address comes from the public SWIFT directory copy frozen in January 2018; never on a national-register answer. bic.listed_in_current_source says whether the BIC8 still appears in a list refreshed this cycle (GLEIF, a national register, the EPC scheme registers): true when one of them carries it, null when it was not found in what could be read in full. It answers true or null for now, never false: the EBA STEP2 and NBP lists are only read through our deduplicated directory, which can drop a BIC they carry, so an absence is not proven. It does not prove the bank still exists under that name. GET /v1/bic/:code gains source_name, source_as_of and listed_in_current_source (on every answer of valid format, found or not), and the MCP lookup_bic tool now returns its source at all (source, source_name, source_as_of, listed_in_current_source).
  • /health counts the frozen rows nobody else lists. frozen_bic_sources gives, per frozen source, its rows and BIC8 and how many of them no source refreshed this cycle still carries; complete: false with null counts when a trace source could not be read in full, which is the case for now (the EBA STEP2 and NBP lists, as above). It is computed once per deployment, which is the rhythm of the monthly and weekly data. bic_sources, served since 1 September, is now declared in the OpenAPI beside it.
  • SEPA reachability at the bank, never borrowed from the country. sepa.bank_reachability (listed, not_listed, no_bank, bank_code_not_allocated, or null when the EPC registers are not loaded), sepa.bank_schemes (the bank's own schemes from the EPC registers, [] for an unallocated code, null otherwise) and sepa.vop_register_status (active, pending, inactive, not_listed, null). Absent outside SEPA. sepa.schemes, basis and member are unchanged.
  • Names that say what the compliance block checks. sanctions.institution_listed (null when no bank was screened, or when nothing matched while a list this service names is not loaded), sanctions.payee_screened (always false), vop.register_status, reachability.listed_in_epc_registers, on the IBAN and the BIC forms of POST /v1/iban/compliance, and the flag bank_code_inferred, which carries no weight: no risk score changes.
  • The private overlay can carry the Polish, Finnish and Luxembourg keys of the composite map and the Finnish list (25 September 2026). Four new overlay members: map_pl, map_fi and map_lu (table curated_bank_codes, rebuilt every month by the private chain from the latest mdomke/schwifty release on PyPI, its SHA-256 checked) and register_fi (table fi_monetary_codes, the Finance Finland list, a static transcription copied unchanged from one overlay to the next). While this repository still carries the same data, the freshness rule of the overlay decides and no answer changes: the keys of src/db/bic_data.json are undated and keep answering for every country they carry, and the overlay's Finnish list replaces the one in src/lib/fi-register.ts only when it is strictly more recent. An overlay written before these members is still accepted: they read as absent, and /health lists them under restricted_overlays.bic.absent until a release carries them. A refusal remembered by an older deployment is forgotten by a code that knows a different family, which retries once.

Changed

  • Privacy Policy 1.5 and Data Processing Agreement 1.4: what the file audit keeps (27 September 2026). Both texts said that submitted IBANs are never stored; that holds for validation, not for the file audit. They now say that the annotated report the file audit produces, which reproduces the columns of the uploaded file (for example payee names, IBANs and addresses), is kept 2 hours if unpaid and 24 hours after payment, then deleted: Privacy §1 and §4, DPA Clauses 2, 4.2 and 7 and Annex I. They describe what the service already does; nothing new is collected, so it is a clarification, not a material change under Privacy §8, and applies at once.
  • The Google Sheets page no longer says the add-on is being submitted to the Marketplace (27 September 2026). It is not on the Google Workspace Marketplace yet; the page says so, and the script still installs by hand.
  • Privacy Policy 1.4: what we keep from a USDC payment (26 September 2026). Section 1 now says that, for a USDC purchase, we store the paying wallet address and the on-chain transaction hash, besides the transaction reference, to reconcile a payment whose settlement could not be confirmed. It describes what the service already records to check such a payment on-chain; nothing new is collected, so it is a clarification, not a material change under section 8, and applies at once.
  • Terms of Service 1.9 and Data Processing Agreement 1.3 (25 September 2026). §3 of the Terms: cancelling the Pro subscription no longer deactivates the key, which returns at the end of the month already paid to what it had before the subscription, and a failed renewal payment keeps the Pro allowance while the payment is retried. §9 of the Terms and Clause 4.7 of the DPA: the relationship continues as long as a key is active; revoking the last key, or requesting it, ends it and starts the deletion period. Both apply immediately to new customers and, from 1 November 2026 (at least 30 days' notice, §9 of the Terms), to customers who accepted an earlier version.
  • The Austrian, Belgian and San Marino bank-code pages ask the API instead of a committed file (25 September 2026). /at/{code}, /be/{code} and /sm/{code} are rendered on request from the API's answer for that code, with the server key of the playground (never sent to the browser), and cached for a day; a failed read keeps the page already served, a code nobody holds answers 404. The index pages search a code instead of listing the register, the sitemap no longer lists the code pages, and the neighbouring codes and Belgian group codes are no longer shown: listing them would republish the register.
  • Terms of Service 1.8: refunding a pack on a key that holds several (25 September 2026). §3 now says how "unused" is read once packs can land on the same key: packs are used after any monthly allowance, oldest first, and a pack counts as unused while the key's balance still holds all of its credits and those of every pack bought after it; a refund removes that pack's credits from the key. The 14-day window and the address to write to are unchanged. Version 1.8 applies immediately to new customers and, from 1 November 2026 (at least 30 days' notice, §9 of the Terms), to customers who accepted an earlier version.
  • credits_total is the total ever bought on the key (25 September 2026). Now that a key can be recharged, credits_total (in GET /v1/credits/balance, the usage block, X-Credits-Total and POST /v1/keys/rotate) counts every credit bought on the key, recharges included, and credits_used = credits_total - credits_remaining stays true. For a key that was never recharged, the value does not change. A key born of a purchase keeps its basis: "credits", and its limit and remaining are served as before: nothing is enforced against them.
  • Buying a pack with a key presented no longer grants "200 requests once". A purchase never creates a free allowance: an anonymous key that buys credits moves to the paid tier with its credits and no monthly allowance, and can no longer be claimed by e-mail (409 already_claimed). Claim an anonymous key by e-mail before buying to keep 200 requests a month. The x402 pay-per-call payment made on a key keeps its "200 once" as before.
  • Nothing is credited or activated before a USDC payment settles. The pack route runs before the payment is settled, as the x402 protocol does: a key bought without a key presented is now created inactive and activated only once the settlement is confirmed, and a recharge is credited only then. Only a terminal refusal by the facilitator (an explicit reason, nothing broadcast) leaves nothing active and nothing recoverable. Any other answer (a timeout, a network error, a 5xx, a transfer broadcast but not yet confirmed) is an unknown outcome: the purchase stays pending with its key kept, the answer is 502 settlement_unconfirmed, whose recovery_note says not to pay again, and it is reconciled by hand once the transfer is confirmed on-chain. A payment already seen whose purchase was not credited answers 409 (payment_pending, payment_refused, payment_reversed) and is never settled again. If the key you presented is no longer active when the payment settles, the credits land on a new key and the answer says so (same_key: false, the new key and its recovery_url).
  • An unknown settlement outcome answers 502 on every paid route, never 402. Until now only our own facilitator timeout did: a network error, a gateway page or a 5xx during the settlement answered 402, which asks the client to pay again although the payment may already be on-chain. They now answer 502 settlement_unconfirmed too, on the pay-per-call routes as on the pack route. The body gains settlement.cause (timeout, settlement_pending, facilitator_error) and, when the facilitator returned one, settlement.transaction. A refusal (an explicit reason, nothing broadcast) still answers 402.
  • Privacy Policy 1.3, with the account page (25 September 2026). Section 6 now says that the account page uses a sign-in cookie for 7 days, strictly for authentication, and that sign-in codes are valid 15 minutes and stored only as a hash. It describes the new account page only: the cookie and the code exist when someone signs in there, and nothing changes for any other processing, so it applies from its publication.
  • Terms of Service 1.7: the MIT License covers the code, not the data files (25 September 2026). LICENSE is unchanged: it holds the MIT License text alone. NOTICE, at the head of its "Data Files and Their Sources" section, and the "License" section of the README state the scope of that licence: it covers the source code of the repository and its documentation, not the data files that NOTICE lists (data/bic.sqlite, data/compliance.sqlite, the register and country files exported for the website, frontend/app/[locale]/playground/captured-iban.json, src/db/*.json, scripts/data/, the list of bank codes in src/lib/fi-register.ts), nor any other file that reproduces records from those sources, such as exported API responses and test fixtures; those records remain subject to their publishers' terms, described in NOTICE. §7 of the Terms says the same, no longer calls the compiled datasets the operator's property (the operator keeps its compilation, the publishers keep their records), and, like §6(a), places the limits on bulk extraction on data obtained through the Service. Version 1.7 applies immediately to new customers and, from 1 November 2026 (at least 30 days' notice, §9 of the Terms), to customers who accepted an earlier version.
  • The Java and .NET SDKs follow the API version (29 September 2026). IBANforge.Sdk (NuGet) and com.ibanforge:ibanforge-sdk (Maven Central) jump from 1.5.0 to 1.8.1, the version every other package carries; their User-Agent says so. The Java SDK ships the patched jackson-databind above.

Deprecated

  • Reading these fields for what their names suggest. They keep their values; read the new field instead. bank_code_check.status: "verified" as a confirmation: read bank_code_holder. sepa.schemes as the bank's reachability: read sepa.bank_schemes and sepa.bank_reachability. sepa.vop_participant: read sepa.vop_register_status. risk_indicators.vop_coverage (a country obligation, like sepa.vop_required). compliance.sanctions.bank_sanctioned when bank_screened is false: read institution_listed. No field is removed and no removal date is set.

Removed

  • The data we may serve but not redistribute has left this repository (25 September 2026). data/bic.sqlite and data/compliance.sqlite no longer hold the EBA STEP2, NBP and OeNB directory rows, the Austrian, Belgian and San Marino registers, the Bank of England PRA list, the UN list or the EPC scheme and Verification of Payee registers: the hosted API serves them from its private overlay, and answers "not consulted" where it has not loaded them. The public refresh workflows no longer download them (without SEED_FAMILY, the seeders run in public mode), and src/lib/public-base-family-free.test.ts fails if a row comes back. Also removed: the Austrian, Belgian, Luxembourg, Polish and Finnish keys of the composite map src/db/bic_data.json (5,454 keys), the transcribed Finnish list src/lib/fi-register.ts, the Austrian, Belgian and San Marino website exports, the EPC fields of the other exports and of the SDK, MCP and documentation examples (now the answer without those registers), and the FCA entries of scripts/data/eu-emi-register-2026-05-22.json. Earlier commits keep copies, still subject to their publishers' terms (see NOTICE).
  • The Polish, Finnish and Luxembourg keys of the composite map and the Finnish list now come from the private overlay alone (25 September 2026). This repository no longer carries them, so the four overlay members added for them (map_pl, map_fi, map_lu and register_fi, see Added) now answer for these countries. The keys follow the latest mdomke/schwifty release the private chain reads every month, which differs from the removed keys on a few codes: some BICs change form, a few codes take the BIC the national source publishes today, and a few Polish and Luxembourg codes it no longer lists lose their BIC. Without these members, a Polish or Finnish IBAN answers bic: null with bank_code_check.status: "unavailable" and reason: "no_reference_data_for_country", never a refusal.

Fixed

  • A private overlay file that no longer carries a member the API serves is refused, at the pull and at restart (25 September 2026). A pulled file that simply lacked one of the members added after the first overlay (map_pl, map_fi, map_lu, register_fi) passed the pull, was refused at reload, and was then served at the next restart, overwriting the last accepted copy. The pull now refuses it (members_lost); at restart, as at reload, the accepted copy wins as soon as the file of the variable lacks a member it serves, even if that file gains another, and such a file never replaces the accepted copy; the release gate (manifest --previous) refuses a lost member even under --allow-shrink, unless the code has taken it out of the family. Also fixed: an explicit load of the Finnish list (FI_LIST_PATH) that fails now stops the private rebuild instead of republishing the previous list under the date of a run, the overlay's Finnish list is used only when its date is a well-formed day, and the size cap of the schwifty download applies before its body is read.
  • The playground no longer shows "No" for a register it did not consult. When the compliance answer says screened: false, the reachability and Verification of Payee rows read "Not checked" instead of turning the unconsulted false into a "No".
  • One source down no longer holds back the monthly rebuild of the private BIC overlay (25 September 2026). npm run overlay -- seed --kind bic rebuilds the family from a public copy without it, so a single source that did not answer (EBA STEP2, the Bank of England PRA list, the Austrian, Belgian or San Marino register, NBP, OeNB) left its member empty, the floor refused the whole overlay, and nothing was published that month, not even the members that had refreshed. Each family seeder now reports what became of each member (SEED_REPORT_PATH, scripts/seed-report.ts), and a member whose source failed, came back under its floor or came back empty is carried over as is from the previous overlay placed at the output path (scripts/restricted-carry-over.ts): every column is kept, updated_at, as_of and list_month included, so the API still sees its true age, and the bic_entries rows are reinserted in the rebuild's order (OeNB, NBP, EBA STEP2). The file says so (overlay_meta: seed_started_at, and carried_over with each member's original date and a short cause; the format stays 2), the release manifest repeats it (files.bic.carried_over, absent when nothing is carried over, ignored by older readers), and the run prints one ::warning:: annotation per member carried over, with codes and dates only. Still refused, with nothing written: no member refreshed, no previous overlay (a first publication, as before), a member refused in the previous overlay, a carried-over row of unknown date or older than 45 days (one missed monthly run, never two), a PRA list older than the window its seeder accepts (the month of the run and the two before; the list month, which the Bank of England permission requires in the attribution, is kept as is). A drop of more than 10% stays a manual check. The family-only chain no longer downloads the Czech register, which is public; the public refresh reads it as before.
  • A pay-per-call settlement the facilitator refused could be recorded as paid. The x402 middleware answers undefined for every payment it verified, a refused settlement included; the settlement ledger of a key (and the "200 once" promotion it leads to) now requires the settlement itself to have succeeded.
  • Card pack sales are read from what Stripe charged. The sale notification of the entry pack quoted its former price; it now reads the amount of the payment. A card payment naming a pack we do not sell raises an alert instead of being acknowledged in silence, a pack checkout settled at zero (no_payment_required) no longer credits anything and raises an alert, and a rotated credit key no longer counts as a second pack sale in the sales readers.
  • POST /v1/iban/compliance refused a bic-only body one middleware before the route that accepts it. The pre-x402 guard in src/app.ts demanded an iban field unconditionally and ran before the route (src/routes/iban-compliance.ts), which takes either iban or bic, ever got the request. Every caller this guard applies to (a Bearer API key, or any x402 payer) got a flat 400 invalid_request on a bic-only body, closing exactly the path GET /v1/bic/:code tells a caller to take when a BIC is on a sanctions list: POST /v1/iban/compliance {"bic": "..."}. Callers authenticating via X-API-Key or ?api_key= were unaffected, because the guard only runs when it sees a Bearer or a payment header. The guard now accepts either field, with the same message the route already gives when both are absent: "Request body must include an 'iban' or a 'bic' field (case-insensitive)." Refusing the two fields together stays the route's job alone.
  • A bank screened while the UN list is not loaded is now flagged: sanctions.listed: null on GET /v1/bic/{code}, an unweighted sanctions_list_unavailable_un on compliance, whose score and risk level do not change. When the EU and OFAC lists are loaded but the UN list is not (a deployment without its private overlay), a compliance answer about a screened bank now raises one unweighted flag per missing list, sanctions_list_unavailable_un, and GET /v1/bic/{code} answers sanctions.listed: null (not false) when nothing matched on the lists read, with sanctions.unscreened_lists: ["UN"]. A match on a loaded list is still listed: true. Nothing changes while every list is loaded. /openapi.json changes three texts: the description of sanctions.listed on GET /v1/bic/{code}, the new optional property sanctions.unscreened_lists, and the description of the compliance flags, which now names the per-list flag among the unweighted ones.
  • A register or a list that is not loaded now answers "not consulted", never "no". Some of the data behind the compliance and validation answers (the EPC scheme and Verification of Payee registers, the UN list, several national registers) is moving out of the public repository into a private file, so an API instance can run without it. Until now such an instance answered wrongly: compliance.reachability and compliance.vop came back screened: true over empty EPC registers, so every bank read "not reachable by SEPA Instant" and "not in the VoP register", and the risk score of an ordinary bank went from 0 to 10. Now, for a bank in the SEPA area whose register is not loaded: reachability.screened and vop.screened are false, the two weighted flags no_sepa_instant and no_vop are replaced by the unweighted sepa_register_unavailable and vop_register_unavailable, sepa.vop_participant is null (not false) on /v1/iban/validate, and sepa.basis stays country_default. Outside the SEPA area the country answers, register or not, and nothing changes. meta.sources is derived from what is actually loaded rather than read back from a stored string, so it never names a list or a register that was not consulted. With no sanctions list loaded, only the bank lookup is skipped: the country and FATF axes still answer, the BIC lookup says sanctions.screened: false, listed: null, and a compliance answer about a resolved bank raises sanctions_lists_unavailable and scores 50 (elevated) at least, never less than what the country already establishes. A VoP table present but unreadable no longer turns /v1/iban/validate into a 500: it answers vop_participant: null. When a compliance table cannot be read, compliance_data_unavailable now keeps every axis that could still be read (the country and FATF ones above all), with a score of 50 at least and never less than those axes establish; it used to replace them with country_sanctioned: false and non_member. Nothing changes on a complete database: every answer served today stays the same, meta.sources included.
  • No more empty strings. bic.city and bic.bank_name on the validation routes, and city on GET /v1/bic/:code and the MCP lookup_bic tool, answer null instead of "" when the source leaves the value blank (the EBA STEP2 list leaves the town blank).
  • A BIC record is complete or not found. GET /v1/bic/:code and the MCP lookup_bic tool answer found: true only when the row names an institution, and name the country (not its code) on a BIC they do not hold; the BIC form of POST /v1/iban/compliance does the same. No row of today's directory lacks a name: the rule guards the next import.
  • The contract declares what the API already served. The OpenAPI now describes the bic form of POST /v1/iban/compliance (request: exactly one of iban or bic; answer: the new BicComplianceResponse component), and address of GET /v1/bic/:code as nullable: the route answers null on a BIC found without a registered address. No answer changes.
  • Descriptions that overstated a field. sepa.vop_required and risk_indicators.vop_coverage are described as the country's obligation they are; bank_code_check.status, match and as_of say what they mean on the composite map. The next_steps step screen_compliance keeps its code and says, in because, when the institution named is our inference.
  • The machine-readable rate limits no longer call the keyless MCP allowance daily (29 September 2026). /.well-known/rate-limits.yml described the MCP taster, in the note of the REST trial, as "counted by the day" while its own mcp_anonymous block says window: 1 week; the note now says "counted by the week". Nothing else changes: the session ceiling (mcp_sessions) is still counted by the day.
  • Dependencies with published advisories (29 September 2026). ip-address 10.7.2 in the API and in the MCP package, jackson-databind 2.18.10 in the Java SDK.

[1.8.0] — 2026-09-25

Added

  • Real, dated answers for an assistant that can read a page but cannot call the API. GET /v1/demo now also validates the official example IBANs of Switzerland (CH9300762011623852957), Belgium (BE68539007547034) and Austria (AT611904300234573201): each passes mod-97, and its national register allocates the bank code to nobody (bank_code_check.reason = "not_allocated", authoritative: true), computed on the request like every other example of the page. The Swiss example once labelled "Credit Suisse" is labelled by what the answer shows: the former Credit Suisse IID 04835, redirected by the SIX register to UBS Switzerland AG (IID 00230). The demo gains served_at and a how_to_read sentence. GET /health gains served_at too (ISO 8601 in UTC, to the second), so that a copy of either answer quoted from an index or a cache dates itself. A block "If you cannot call the API", written once, lists full addresses of real answers (the demo, the Swiss IBAN page, three articles that reproduce served answers) and asks not to simulate: in the API's /llms.txt, GET /v1 (if_you_cannot_call), the MCP server card, the README and both llms files of the site. A 405 on a POST route now names what a plain GET can read instead (without_post), and GET /v1/keys/generate answers with the key that needs no e-mail (key_without_email); every 404 lists the demo. The upgrade_to_full_validation hint of /v1/iban/format gives the address of the demo. On the site, the playground's saved IBAN answer shows the day it was captured and says it is not a live answer; the page of each country whose official example is not allocated says the check digits pass and the register holds no such code, which is what the bank-code check catches; the MCP documentation says where to add https://api.ibanforge.com/mcp as a connector in Claude, ChatGPT (developer mode) and Gemini, from each vendor's page. Every field is added; nothing is renamed or removed.

Changed

  • ibanforge-mcp quotes no quota, and its tool descriptions say what the service checks. A published package stays as it is until its next release while the allowances of the service move (the keyless trial and the keyless MCP access both went from the day to the week on 24 September 2026), so the package writes no allowance figure or period any more: its README, the instructions it serves at connection, its tool descriptions, the note of its format-only answer and the hint of a 402 point to /.well-known/rate-limits.yml and GET /v1. The batch description no longer says "60% cheaper": the lower price per IBAN applies to x402 payments only, and on a key or prepaid credits each IBAN of a batch uses one request or one credit. The data tools follow the hosted transport word for word where it matters: lookup_ch_clearing names every IID of the SIX BankMaster instead of a superlative, lookup_bic describes its directory (GLEIF and national registers refreshed monthly, plus a public copy of the SWIFT directory frozen in January 2018 that still makes up most of the rows), check_compliance states that the sanctions lists are matched on the payee's bank (BIC8) and the country against a fixed list, never the payee's name, and validate_iban no longer speaks of a "European" IBAN. The MCP Registry description of the server now opens with the positioning sentence ("Check the bank behind an IBAN before you pay") instead of "Pre-payout IBAN screening for AI agents".
  • The hosted MCP transport's keyless allowance is counted by the week: 25 tool units per source address, no longer 10 a day. Claude-Alain's decision of 24 September 2026. The week is the ISO week in UTC and resets on Monday at 00:00 UTC, like the keyless REST trial; IPv6 is counted per /64; a tool call is one unit and batch_validate_iban one per IBAN; send_feedback, request_api_key and poll_api_key still cost nothing. The MCP allowance and the REST trial have the same figure and are two separate allowances: a source spending one keeps the other. Opening MCP sessions stays capped by the day (30 per source). Contract changes: the JSON-RPC refusal now reads "Weekly MCP free tier limit reached" and its data gains period: "week" and resets_at; the trial block of GET /v1 replaces mcp_daily_limit with mcp_weekly_limit and mcp_period; the free_tier block of GET /mcp replaces mcp_daily_limit and mcp_daily_limit_unit with mcp_weekly_limit, mcp_weekly_limit_unit, mcp_period, mcp_resets and mcp_resets_at (the old names are removed, not kept: they would carry a weekly figure under a daily name); /.well-known/rate-limits.yml says window: 1 week for mcp_anonymous and lists the session cap as mcp_sessions; the site's onboarding export renames remoteDaily to remoteWeekly. The private trial report (GET /v1/admin/trial) gains mcp_this_week, and its this_week counts the REST trial alone. Every text that gave the MCP allowance by the day says the week (the MCP instructions and tool descriptions, both llms.txt, the README, the MCP server card, the OpenAPI, apis.json, /.well-known/auth.md, the MCP, recipes and agent-payment documentation in three languages, the agents page). The npm package ibanforge-mcp no longer quotes this allowance: from 1.8.0 it gives neither its figure nor its period and points to rate-limits.yml and GET /v1.
  • 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.
  • The keyless trial is 25 validations a week, no longer 25 a day. POST /v1/iban/validate with a real iban and no key or payment is served 25 times per source address (IPv6 counted per /64) and per ISO week in UTC; the count resets on Monday at 00:00 UTC, for everyone at once. The key that needs no e-mail keeps its monthly allowance (200 requests a month once claimed), and the hosted MCP taster moved to the week the same evening (entry above). Contract change: the trial block of a keyless answer replaces calls_used_today, calls_left_today and daily_limit with calls_used_this_week, calls_left_this_week and weekly_limit, keeps resets (now "Monday 00:00 UTC") and adds resets_at (ISO 8601, the next Monday at 00:00:00Z). The old names are removed rather than kept: they would have carried weekly counts under daily names, and no published package reads them. X-Trial-Used, X-Trial-Limit and X-Trial-Remaining count the week, X-Trial-Reset is now the ISO instant (it was midnight UTC), and X-Trial-Period: week is new. The trial block of GET /v1 carries weekly_limit, period, resets and resets_at instead of daily_limit (mcp_daily_limit gave way to mcp_weekly_limit the same evening, entry above). In the trial_exhausted 402, cause.quota.month reads week and cause.quota.resets gives the instant. /.well-known/rate-limits.yml says window: 1 week. The API and the site say a week wherever they quoted the trial (documentation in three languages, the two articles that cite it, the pricing FAQ, both llms.txt, the README, the MCP server card, the OpenAPI). From 1.8.0 the npm package ibanforge-mcp no longer describes the trial as daily, in its README or in its instructions, and quotes no figure: it points to rate-limits.yml and GET /v1. The api:trial-exhausted event behind the dashboard's doors card is written once per source and per week, on the call that crosses the ceiling (it was once per address and per day, which counted a source again on every day it came back to the refusal). A source counted in memory rather than in the database (a caller whose address cannot be placed, or a new source while the week's table is full) starts again from zero after a restart.

Fixed

  • The npm MCP package no longer loses answers that carry a null. ibanforge-mcp declared as non-nullable several fields the API serves as null (the BIC of an unallocated bank code, the Swiss clearing block, the LEI and address of a BIC without an LEI, the risk score of an invalid IBAN, an unrecognised payment reference, the dates of an empty compliance metadata block) and did not know the register issuer classification nor the suspended FATF status. The official MCP client validates structured output and rejected those answers with error -32602. The schemas now follow the API's types.
  • The BIC/LEI Mapping Table notice is the one its licence asks for. /llms.txt said "This service uses the BIC to LEI relationship file…" with no version; it now carries the notice of the BIC/LEI Mapping Table License Agreement word for word ("SWIFT © and database rights [month year]. All rights reserved. This Mapping Table has been developed by SWIFT…"), the version read from the load date of the GLEIF rows. The site's llms.txt, a static file that cannot follow the monthly refresh, points to the served notice rather than typing a version.
  • The OpenAPI entry of POST /mcp says what the transport serves. It announced "7 MCP tools" while the transport served eleven, called tools "free" without separating the ones with no USDC price from the ones outside the keyless allowance, and declared an API key the route never reads: the list is now read from the tool inventory, the two lists are said apart, and the operation is declared anonymous only. The OAuth protected-resource document of /mcp (both spellings) and smithery.yaml no longer announce an API key or x402 for /mcp, and the refusal served when the allowance cannot be counted sends the caller to the REST API or the npm package instead of "use an API key". The /v1/demo schema declares label, compliance_example and the real fields of the BIC examples.
  • /mcp: a tool call sent with no live session no longer spends the keyless allowance. The allowance was charged before the session was looked up, so a tools/call sent on a session the last redeploy or the idle sweep had removed paid its units (one per IBAN in a batch) and then got its 404; a tools/call with no mcp-session-id paid a unit, spent a session opening and got a 400. The session is now looked up first: an unknown session answers 404 and a call with no session answers 400 before anything is charged, and neither is counted as a served tool call. On a live session, a call to a tool the server does not register, or a call the transport refuses with 406 (an Accept header without both application/json and text/event-stream), is no longer charged or counted either.
  • 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).
  • /v1/iban/format measures the IBAN, not its printed form, and the secondary texts say what the code does. The free format check measured the length before removing the spaces, so a valid IBAN written in groups of four was refused with 400 invalid_iban_length in the 13 countries whose printed IBAN passes 34 characters (Malta among them); spaces and hyphens are now removed first, as the paid route always did. Its upgrade_to_full_validation hint says that valid: true means well formed and nothing more, and what the paid validation adds (it no longer promises sanctions, which are /v1/iban/compliance). The OpenAPI lists the six validation error codes (invalid_check_digits and invalid_bban_structure were missing), says that an invalid IBAN is an HTTP 200 with valid: false, carries a valid and an invalid example, and names a support address. The free_key hint of the keyless trial no longer puts the trial's figure and the key's monthly figure in one sentence, and says that claiming takes a mailed code. Two articles and seven documentation pages, in three languages, no longer say "ten calls a day" or "200 a month with the free key"; the error reference is rewritten (a batch holds 100 IBANs, an unknown BIC is a 200, 405, 413 and 429 documented, the causes of a 402); the README, GET /v1 and both llms.txt (the API's and the site's) carry errors, limits and support, with the status page and the SLA, and the site footer links the SLA. The batch tool of the hosted MCP transport and the batch documentation no longer say "60% cheaper" (the npm package ibanforge-mcp follows in this release, entry above): the $0.002 per IBAN is the per-call price in USDC via x402, and on a key or a credit pack an IBAN in a batch uses one request or credit, like a single validation. /.well-known/auth.md and /.well-known/error-semantics.yml no longer announce a 401 unauthorized or a 402 quota_exceeded on paid routes, which never send either: a paid route answers 402 payment_required with the reason in cause.reason. They also say that an unknown BIC or clearing number is a 200 and not a 404, and that an unexpected 500 is plain text; the OpenAPI overview says the same. The error reference documents the 401 and 409 of the key routes, the 502 and 503 of the payment rail and a refused payment (payment_error), says that a prepaid credit key carries X-Credits-* rather than X-Quota-*, and no longer claims to list every code. /v1/iban/format names the 64-character cap on the raw input in its refusal.

[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 answers — GET /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 tooling — scripts/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_digits — CH99… 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 lifecycle — POST /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_of → 2026-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; zero nameless entries remain across all sources.
  • QR-IID lookups were semantically inverted — GET /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 floor — seed-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 release — mcp/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

Das Changelog wird auf Englisch geführt.