Changelog
Tous les changements notables de l'API, des SDK et du serveur MCP.
Politique de versionnage & dépréciation
L'API /v1 est stable : des champs peuvent être ajoutés, jamais renommés ni supprimés au sein de v1. Un changement cassant passerait par un nouveau chemin majeur (/v2) avec au moins 6 mois de fonctionnement en parallèle, annoncé ici et par email aux détenteurs de clés actives. Le projet suit le versionnage sémantique.
All notable changes to IBANforge are documented here. The format follows Keep a Changelog and the project adheres to Semantic Versioning.
[Unreleased]
Changed
- 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/mcpnow accepts an IBANforge key inAuthorization: Bearer ifk_…orX-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_PATHis set, a listed code names its holder inbank_code_check, with the exact credit and the association's Important Note in full inregister. An absent code keeps its previous answer, nevernot_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/trustand/fr/truststate 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", withmethod,verified_by,sourceandtable_fetched_on).verified_bysays what the verdict rests on:bundesbank_test_numbers(the method passes every test number and worked example the Bundesbank publishes; afailadds the blocking stepnational_check_digits_failed) orindependent_implementation(the Bundesbank publishes no test number for the method, which was verified against an independent implementation; afailadds the warning stepnational_check_digits_suspectand never stops a payment). Method 09 answersnot_applicable; method 44 and bank codes outside the Bundesbank file answernot_checked.validnever moves. next_stepsgainsnational_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 /healthreports 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_checkisunavailable(no bank-code data we may use for the country, or a register not loaded) now carries the findingbank_not_consulted("Bank not consulted", a warning, not an error) in the workbook, the free preview and the summary; it used to come outOKwhen 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_keyandpoll_api_keysay where the key goes (7 October 2026). The same text on the three MCP surfaces names theconfig_linefor Claude Code, theAuthorization: Bearerheader 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-ibanforge0.1.3 fixes the six findings of n8n's review tool on 0.1.2, ships an MITLICENSEand 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_autofillnow states that the IBAN is sent to the IBANforge API, with the retention described in the Privacy Policy; banner first,supportandmaintainerin 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-revenueaddsibanforge_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 cachevalues). 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-server2.1.3 and the x402 packages 2.28.0 (7 October 2026). The two Hono releases carry a path-decoding fix inserveStatic, 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/andsdks/typescriptrun the same version, with ESLint 10.12.0 at the root. Vitest 5 declares Node 22.12 or later;mcp/andsdks/typescriptare still tested on Node 20, where npm prints a warning and every test passes. indexnow.ymlusesactions/checkout@v7andactions/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.statuscan now benot_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.jsonbeside the Bankleitzahlendatei (6 October 2026). It holds the check-digit method of each German bank code;data/bic.sqliteis unchanged. - The API redeploys only when its image can change (6 October 2026).
railway.tomllists 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.jsonnow pinsfra1. - 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.txtfiles,SECURITY.mdand the Legal Notice now givesupport@ibanforge.comwith "SECURITY" in the subject. The site's file pointed to public GitHub issues, whichSECURITY.mdasks reporters not to use, and the API's file had noExpiresfield, 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/checkand of the MCP toolcheck_swiss_qr_billsaid 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. Thecombined_addressfinding 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 ownsource, naming those documents; the other findings cite the QR-bill guidelines alone. The fieldready_for_2026_11_14keeps 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 packageibanforge-mcpcarries 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.registernow 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;NOTICErecords 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, andNOTICEmoves 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
bicblock: the register's verdict beside it is unchanged, and a code outside the registers answersnot_in_registerwithreason: "absent_from_reference_data"andauthoritative: 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", withcandidateswhere 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.sourcenow readsIBANforge curated bank-code map;followed byVir: Banka Slovenije. This information is available free of charge on the Banka Slovenije website (www.bsi.si).,Source: Lietuvos bankas,Forrás: Magyar Nemzeti BankorIzvor: 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_keyno 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 readsIBANFORGE_API_KEYat 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
.forgeand.revealstyles, 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.jsonloses 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 answersbic: nullandbank_code_check.status: "unavailable"withreason: "no_reference_data_for_country"andbank_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. OnPOST /v1/iban/compliancethese IBANs resolve no bank, so the bank is not screened:sanctions.bank_screenedisfalseandsanctions.institution_listedisnull(bank_sanctionedkeeps its documentedfalsewhen no bank was screened), the flags carryno_bank_resolvedand a newbank_code_data_unavailable, andrisk_scoreis held at 50 (elevated) at least, a floor and not a weight, likesanctions_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 scoreelevatedrather thancritical, 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 answersnot_in_register/absent_from_reference_datawith no BIC, like any code it never carried. Every other country answers exactly as before, apart from the credit added tobic.sourcein 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 (seeNOTICE, group C).
Security
- MCP TypeScript SDK 1.32.1 across the API, the
ibanforge-mcppackage 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.0in both manifests. The update also gives the hosted/mcptransport a 4 MiB request-body limit and a 100-message cap on JSON-RPC batches. sharp0.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 scopedoverridesbecause 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).
bracesandsprintf-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, includinghttps://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/importrequired only theifk_prefix; it now requiresifk_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 (
&lt;stays<), 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 withcrypto.randomInt; the blog lists only posts whose slug the post page accepts; the dashboard no longer hides a referrer whose name merely ends inibanforge.com.
[1.8.1] — 2026-09-29
Added
- Receipts on the account page (28 September 2026). Below the keys,
https://ibanforge.com/accountnow 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 OpenAPIAccounttag, bothllms.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 opaqueref, never by a sequential number) andGET /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 same404 receipt_not_foundfor an unknown ref, another address's purchase or a purchase without a card receipt;503 receipt_unavailablewhen 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.closedwith the statuswon, orwarning_closedfor 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 itrefunded. 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.proinGET /v1/keys/usageandGET /v1/credits/balance,credit_packs.topup_this_key.proin a402served to the key,actions.subscribe_proon 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 answers402with 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 asnone, 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|25know credits THAT key: the answer carriessame_key: true,recharged: true,credits_addedand echoes the key you presented asapi_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 undertopup.by_card, inGET /v1/keys/usage,GET /v1/keys/report,GET /v1/credits/balance, incredit_packs.topup_this_keyof a402answered 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 inX-Charged-From(allowance,creditsorallowance+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 answers402credits_exhaustedwith the links that recharge it (X-Credits-Topup-Url,cause.credits.topup).GET /v1/keys/usagekeeps thebasisof the allowance on such a key and addscredits_remaining,credits_totalandbilling_order: "allowance_then_credits";GET /v1/credits/balancegainsallowance,billing_orderandtopup, and now accepts the key asX-API-Keyor?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_proveninGET /v1/account/overview, whereactions.topupnow 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
authoritativestaysfalseand no Italian answer can becomenot_allocated. A code in force comes backverifiedwith the institution named as the register writes it (bank_code_check.institution: registered office in Italy, LEI where published) andbank_code_holder: "confirmed". A code the Banca d'Italia has struck off comes backverifiedwithretired: true, the new fieldretired_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 carriesbank_code_holder: "inferred", since nobody holds the code today, no BIC, and thenext_stepsstepbank_code_retiredasks 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 backnot_in_register, are nowverified; the codes the Banca d'Italia has struck off, which our map answeredverifiedunder the name of a bank that no longer holds them (03111, UBI Banca's code, answered "Banca Carige"), are nowretiredwith 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_ofis the edition, the date of the Banca d'Italia's file, and every answer that uses it carries the CC BY 4.0 credit inbank_code_check.register. New public pages/itand/it/{code}(codes in force and struck off), a documentation page/docs/it-bank-codesin three languages, and the country page/iban/itnames the register. The OpenAPI and x402 descriptions ofbank_code_checkand the MCP output schema ofvalidate_ibangainretired_on;retiredandsuperseded_bysay what they mean whenauthoritativeis 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_digitsblock (country;scheme:fr_rib_key,be_mod97,it_cinores_dc;status:passorfail; adetailsentence on afail), andchecks.national_check_digitsrepeats its status instead ofnot_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.passmeans the account number is well formed, not that the account exists;failmeans it cannot have been issued as written. Strictly additive:valid,bank_code_holder, the risk score,next_stepsand every other field are unchanged, onPOST /v1/iban/validate,/v1/iban/batch,/v1/iban/complianceand the MCP tools, whose output schemas declare the block. The United Kingdom keepsmodulus_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_stepsnow carries the blocking stepnational_check_digits_failed, on the same footing asmodulus_check_failed: do not send, the account number cannot have been issued as written, ask the beneficiary to confirm it. Itsbecausenames the field and the algorithm (national_check_digits.status is fail (fr_rib_key),be_mod97,it_cinores_dc). The step comes afterbank_code_not_allocated,verify_payee_nameandmodulus_check_failed, before every other step, and no other step is added or removed. For these answers only, this replaces the "next_stepsunchanged" of the entry of 25 September. Served onPOST /v1/iban/validate, each item ofPOST /v1/iban/batch,POST /v1/iban/complianceand the MCP tools. Strictly additive: an answer whose national key passes, or that carries nonational_check_digitsblock, 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 ofnational_check_digits.detail. The published list of step codes (OpenAPI and x402 descriptions ofnext_steps) now names it, together withmodulus_check_failed, which it omitted; the MCP documentation and thevalidate_ibandescription of the MCP server say that both mean stop. - An account page that needs no key to paste (25 September 2026).
https://ibanforge.com/accountshows 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=andPOST /v1/account/logout, documented in the OpenAPI under a newAccounttag with a cookie security scheme, in bothllms.txt, inGET /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 linkshttps://ibanforge.com/accountrather than/en/account; the Stripe success page says to sign in with the address typed at checkout, as text and never inside the link. A402answered to a valid key gainsaccount_page, a sentence with the address of that page; it is absent without a key and with an invalid one, and never in thePAYMENT-REQUIREDheader. 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
verifiedwithauthoritative: trueand the provider named as the ČNB writes it (bank_code_check.institution), a code it does not hold comes backnot_in_register/not_allocatedwithauthoritative: true. The BIC the ČNB publishes for a code is served withbasis: "national_register". Every answer the register decides carries the credit its terms require, "Zdroj: ČNB", inbank_code_check.register, andbic.sourcenames the edition ("Zdroj: ČNB, Číselník kódů platebního styku v ČR, verze 254");as_ofis 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 backnot_in_register(among them Banking Circle 6600, Partners Banka 6363, Modrá pyramida 7990 and the other building societies) are nowverified; two codes the ČNB removed (4000, in April 2025 on a bank merger, and 8280, in December 2024), which used to come backverifiedwith the BIC of an institution that no longer holds them (and, for 4000, its name, EXPOBANK CZ), are nownot_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, bothllmsfiles 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 ofbank_code_checkandbic.basis, thevalidate_ibandescriptions and output schema of both MCP servers and/roadmap.mdlist 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.mdno longer counts Finland among the registers that settle a negative. The API's/llms.txtfollows 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 byRESTRICTED_BIC_OVERLAY_PATHandRESTRICTED_COMPLIANCE_OVERLAY_PATH, the wayLU_REGISTER_PATHalready 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 servedlast_refreshis 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 /healthgainsrestricted_overlays, one state per database (off,applied,kept_public,partial,refused, orpendingbefore the database is opened) with the first twelve characters of the served file's SHA-256, andfallback: truewhen the last accepted overlay is served.npm run overlay -- extractwrites 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 bynpm run overlay:seed. Without the variables the public databases are served as before;/healthonly gains therestricted_overlaysfield (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 insrc/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 -- verifychecks a downloaded release against its manifest). WithRESTRICTED_OVERLAY_PULL_REPOandRESTRICTED_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 byRESTRICTED_BIC_OVERLAY_PATHorRESTRICTED_COMPLIANCE_OVERLAY_PATHis 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:pullafter 24 hours without a successful pull,overlay:pull:stalewhen 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;/healthgainsrestricted_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_holderandchecks. 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) orunknown.checksgives one status per check,pass,fail,inferred,unknown,not_checkedornot_applicable, and lists what IBANforge never checks: the payee's name, whether the account exists, sanctions on the payee.validis unchanged and still means "well formed". Both fields come right aftervalidin the answer. Served byPOST /v1/iban/validate, each item ofPOST /v1/iban/batch,POST /v1/iban/compliance(wherechecksalso fillsinstitution_sanctionsandcountry_sanctions) and the MCP toolsvalidate_iban,batch_validate_ibanandcheck_compliance. - A BIC named from the frozen directory copy now says so, and whether a current list still carries it.
bic.source_as_ofnow 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_sourcesays whether the BIC8 still appears in a list refreshed this cycle (GLEIF, a national register, the EPC scheme registers):truewhen one of them carries it,nullwhen it was not found in what could be read in full. It answerstrueornullfor now, neverfalse: 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/:codegainssource_name,source_as_ofandlisted_in_current_source(on every answer of valid format, found or not), and the MCPlookup_bictool now returns its source at all (source,source_name,source_as_of,listed_in_current_source). /healthcounts the frozen rows nobody else lists.frozen_bic_sourcesgives, per frozen source, its rows and BIC8 and how many of them no source refreshed this cycle still carries;complete: falsewith 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, ornullwhen the EPC registers are not loaded),sepa.bank_schemes(the bank's own schemes from the EPC registers,[]for an unallocated code,nullotherwise) andsepa.vop_register_status(active,pending,inactive,not_listed,null). Absent outside SEPA.sepa.schemes,basisandmemberare 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 ofPOST /v1/iban/compliance, and the flagbank_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_fiandmap_lu(tablecurated_bank_codes, rebuilt every month by the private chain from the latest mdomke/schwifty release on PyPI, its SHA-256 checked) andregister_fi(tablefi_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 ofsrc/db/bic_data.jsonare undated and keep answering for every country they carry, and the overlay's Finnish list replaces the one insrc/lib/fi-register.tsonly when it is strictly more recent. An overlay written before these members is still accepted: they read as absent, and/healthlists them underrestricted_overlays.bic.absentuntil 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_totalis the total ever bought on the key (25 September 2026). Now that a key can be recharged,credits_total(inGET /v1/credits/balance, the usage block,X-Credits-TotalandPOST /v1/keys/rotate) counts every credit bought on the key, recharges included, andcredits_used = credits_total - credits_remainingstays true. For a key that was never recharged, the value does not change. A key born of a purchase keeps itsbasis: "credits", and itslimitandremainingare 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, whoserecovery_notesays 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 answers409(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 itsrecovery_url). - An unknown settlement outcome answers
502on every paid route, never402. Until now only our own facilitator timeout did: a network error, a gateway page or a 5xx during the settlement answered402, which asks the client to pay again although the payment may already be on-chain. They now answer502 settlement_unconfirmedtoo, on the pay-per-call routes as on the pack route. The body gainssettlement.cause(timeout,settlement_pending,facilitator_error) and, when the facilitator returned one,settlement.transaction. A refusal (an explicit reason, nothing broadcast) still answers402. - 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).
LICENSEis 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 thatNOTICElists (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 insrc/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 inNOTICE. §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) andcom.ibanforge:ibanforge-sdk(Maven Central) jump from 1.5.0 to 1.8.1, the version every other package carries; theirUser-Agentsays so. The Java SDK ships the patchedjackson-databindabove.
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: readbank_code_holder.sepa.schemesas the bank's reachability: readsepa.bank_schemesandsepa.bank_reachability.sepa.vop_participant: readsepa.vop_register_status.risk_indicators.vop_coverage(a country obligation, likesepa.vop_required).compliance.sanctions.bank_sanctionedwhenbank_screenedis false: readinstitution_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.sqliteanddata/compliance.sqliteno 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 (withoutSEED_FAMILY, the seeders run in public mode), andsrc/lib/public-base-family-free.test.tsfails if a row comes back. Also removed: the Austrian, Belgian, Luxembourg, Polish and Finnish keys of the composite mapsrc/db/bic_data.json(5,454 keys), the transcribed Finnish listsrc/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 ofscripts/data/eu-emi-register-2026-05-22.json. Earlier commits keep copies, still subject to their publishers' terms (seeNOTICE). - 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_luandregister_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 answersbic: nullwithbank_code_check.status: "unavailable"andreason: "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 unconsultedfalseinto 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 bicrebuilds 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_ofandlist_monthincluded, so the API still sees its true age, and thebic_entriesrows are reinserted in the rebuild's order (OeNB, NBP, EBA STEP2). The file says so (overlay_meta:seed_started_at, andcarried_overwith 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
undefinedfor 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/compliancerefused abic-only body one middleware before the route that accepts it. The pre-x402 guard insrc/app.tsdemanded anibanfield unconditionally and ran before the route (src/routes/iban-compliance.ts), which takes eitheribanorbic, ever got the request. Every caller this guard applies to (a Bearer API key, or any x402 payer) got a flat 400invalid_requeston abic-only body, closing exactly the pathGET /v1/bic/:codetells a caller to take when a BIC is on a sanctions list:POST /v1/iban/compliance {"bic": "..."}. Callers authenticating viaX-API-Keyor?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: nullonGET /v1/bic/{code}, an unweightedsanctions_list_unavailable_unon 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, andGET /v1/bic/{code}answerssanctions.listed: null(notfalse) when nothing matched on the lists read, withsanctions.unscreened_lists: ["UN"]. A match on a loaded list is stilllisted: true. Nothing changes while every list is loaded./openapi.jsonchanges three texts: the description ofsanctions.listedonGET /v1/bic/{code}, the new optional propertysanctions.unscreened_lists, and the description of the complianceflags, 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.reachabilityandcompliance.vopcame backscreened: trueover 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.screenedandvop.screenedarefalse, the two weighted flagsno_sepa_instantandno_vopare replaced by the unweightedsepa_register_unavailableandvop_register_unavailable,sepa.vop_participantisnull(notfalse) on/v1/iban/validate, andsepa.basisstayscountry_default. Outside the SEPA area the country answers, register or not, and nothing changes.meta.sourcesis 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 sayssanctions.screened: false, listed: null, and a compliance answer about a resolved bank raisessanctions_lists_unavailableand scores 50 (elevated) at least, never less than what the country already establishes. A VoP table present but unreadable no longer turns/v1/iban/validateinto a 500: it answersvop_participant: null. When a compliance table cannot be read,compliance_data_unavailablenow 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 withcountry_sanctioned: falseandnon_member. Nothing changes on a complete database: every answer served today stays the same,meta.sourcesincluded. - No more empty strings.
bic.cityandbic.bank_nameon the validation routes, andcityonGET /v1/bic/:codeand the MCPlookup_bictool, answernullinstead 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/:codeand the MCPlookup_bictool answerfound: trueonly when the row names an institution, and name the country (not its code) on a BIC they do not hold; the BIC form ofPOST /v1/iban/compliancedoes 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
bicform ofPOST /v1/iban/compliance(request: exactly one ofibanorbic; answer: the newBicComplianceResponsecomponent), andaddressofGET /v1/bic/:codeas nullable: the route answersnullon a BIC found without a registered address. No answer changes. - Descriptions that overstated a field.
sepa.vop_requiredandrisk_indicators.vop_coverageare described as the country's obligation they are;bank_code_check.status,matchandas_ofsay what they mean on the composite map. Thenext_stepsstepscreen_compliancekeeps itscodeand says, inbecause, 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.ymldescribed the MCP taster, in the note of the REST trial, as "counted by the day" while its ownmcp_anonymousblock sayswindow: 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-address10.7.2 in the API and in the MCP package,jackson-databind2.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/demonow 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 gainsserved_atand ahow_to_readsentence.GET /healthgainsserved_attoo (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 bothllmsfiles of the site. A405on a POST route now names what a plain GET can read instead (without_post), andGET /v1/keys/generateanswers with the key that needs no e-mail (key_without_email); every404lists the demo. Theupgrade_to_full_validationhint of/v1/iban/formatgives 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 addhttps://api.ibanforge.com/mcpas 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-mcpquotes 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.ymlandGET /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_clearingnames every IID of the SIX BankMaster instead of a superlative,lookup_bicdescribes 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_compliancestates that the sanctions lists are matched on the payee's bank (BIC8) and the country against a fixed list, never the payee's name, andvalidate_ibanno 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_ibanone per IBAN;send_feedback,request_api_keyandpoll_api_keystill 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 itsdatagainsperiod: "week"andresets_at; thetrialblock ofGET /v1replacesmcp_daily_limitwithmcp_weekly_limitandmcp_period; thefree_tierblock ofGET /mcpreplacesmcp_daily_limitandmcp_daily_limit_unitwithmcp_weekly_limit,mcp_weekly_limit_unit,mcp_period,mcp_resetsandmcp_resets_at(the old names are removed, not kept: they would carry a weekly figure under a daily name);/.well-known/rate-limits.ymlsayswindow: 1 weekformcp_anonymousand lists the session cap asmcp_sessions; the site's onboarding export renamesremoteDailytoremoteWeekly. The private trial report (GET /v1/admin/trial) gainsmcp_this_week, and itsthis_weekcounts the REST trial alone. Every text that gave the MCP allowance by the day says the week (the MCP instructions and tool descriptions, bothllms.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 packageibanforge-mcpno longer quotes this allowance: from 1.8.0 it gives neither its figure nor its period and points torate-limits.ymlandGET /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/:joband/v1/audit/checkout/:job: the fieldprice_chfis renamedpriceandcurrencycarries the ISO code of that price (USDfor every new job; a job created before the switch keepsCHFand is charged in CHF).tiers[].price_chfistiers[].price. The admin audit statistics gainrevenue_usdandpayment_amounts.usd;revenue_chfkeeps the francs of before. - Finland is a prudent register until its list is re-read. The Finance Finland list behind
bank_code_checkis 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 answersnot_in_register/not_allocatedwithauthoritative: true, it falls through to the composite answer withauthoritative: false, andnext_stepsnever saysbank_code_not_allocatedfor 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 capabilitiesswift_lookup,sanctions_screeningandvop_checkare replaced bybank_code_register_check,bank_level_sanctions_screeningandvop_readiness(bic_lookupis kept). The MCP server card gains afree_accessfield, 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, bothllms.txtfiles, 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 explanatorynoteof 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/validatewith a realibanand 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: thetrialblock of a keyless answer replacescalls_used_today,calls_left_todayanddaily_limitwithcalls_used_this_week,calls_left_this_weekandweekly_limit, keepsresets(now"Monday 00:00 UTC") and addsresets_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-LimitandX-Trial-Remainingcount the week,X-Trial-Resetis now the ISO instant (it wasmidnight UTC), andX-Trial-Period: weekis new. Thetrialblock ofGET /v1carriesweekly_limit,period,resetsandresets_atinstead ofdaily_limit(mcp_daily_limitgave way tomcp_weekly_limitthe same evening, entry above). In thetrial_exhausted402,cause.quota.monthreadsweekandcause.quota.resetsgives the instant./.well-known/rate-limits.ymlsayswindow: 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, bothllms.txt, the README, the MCP server card, the OpenAPI). From 1.8.0 the npm packageibanforge-mcpno longer describes the trial as daily, in its README or in its instructions, and quotes no figure: it points torate-limits.ymlandGET /v1. Theapi:trial-exhaustedevent 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-mcpdeclared as non-nullable several fields the API serves asnull(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 theregisterissuer classification nor thesuspendedFATF 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.txtsaid "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'sllms.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 /mcpsays 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) andsmithery.yamlno 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/demoschema declareslabel,compliance_exampleand 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 atools/callsent on a session the last redeploy or the idle sweep had removed paid its units (one per IBAN in a batch) and then got its404; atools/callwith nomcp-session-idpaid a unit, spent a session opening and got a400. The session is now looked up first: an unknown session answers404and a call with no session answers400before 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 with406(anAcceptheader without bothapplication/jsonandtext/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 (redactIbanShapedValuesinsrc/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 asGET /v1/bic/:code. Registered routes are unchanged (every pattern is checked by a test). /mcp:check_complianceacceptsmeta.sources: null. The API sends null when the compliance database carries no metadata; the output schema refused it, so everycheck_complianceanswer would have failed its own validation on that day. Same fix as in the npm package (PR 232)./v1/iban/formatmeasures 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 with400 invalid_iban_lengthin 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. Itsupgrade_to_full_validationhint says thatvalid: truemeans 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_digitsandinvalid_bban_structurewere missing), says that an invalid IBAN is an HTTP 200 withvalid: false, carries a valid and an invalid example, and names a support address. Thefree_keyhint 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 /v1and bothllms.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 packageibanforge-mcpfollows 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.mdand/.well-known/error-semantics.ymlno longer announce a401 unauthorizedor a402 quota_exceededon paid routes, which never send either: a paid route answers402 payment_requiredwith the reason incause.reason. They also say that an unknown BIC or clearing number is a200and not a404, and that an unexpected500is plain text; the OpenAPI overview says the same. The error reference documents the401and409of the key routes, the502and503of the payment rail and a refused payment (payment_error), says that a prepaid credit key carriesX-Credits-*rather thanX-Quota-*, and no longer claims to list every code./v1/iban/formatnames 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/deviceopens 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 opensibanforge.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 atPOST /v1/keys/device/token(long-polling,authorization_pendingandslow_downas the RFC spells them) together with the configuration line to paste. The two MCP toolsrequest_api_keyandpoll_api_keywrap that journey on the three MCP surfaces (the npm package, the stdio server of this repository and the remote/mcpendpoint) 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 withPOST /v1/keys/generatein 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; thepoll_api_keyrelay 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 adisplay_to_humanblock, 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.txtfiles (the full one listed five tools where the transport served nine), the/v1and/mcpdiscovery documents, the OpenAPI contract and the two READMEs now describerequest_api_keyandpoll_api_keyas 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/tokenlists 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'scauseand theRetry-Aftervalue, and turns a non-JSON 200 into an explicitinvalid_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/mcpallowance; 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.resetsand in a newX-Quota-Basisresponse 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) andIBANforge.generate_api_key()(Python, sync and async) accept no argument and return the anonymous key with itstierandclaim_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'sUser-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/generatewith no body at all now returns anifk_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 newPOST /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 survivesPOST /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, bothllms.txtfiles, the MCP instructions and the tool cost line of the three transports, the/v1and.well-knowndiscovery 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_digitnow 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: falsereads as a typo; it never becomesnot_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 → GRENGLGXXXXand losesGL:1601, a number that had no source anywhere and was served asverified. -
MCP:
audit_creditor_fileandaudit_status, the creditor-file audit as a tool call. The npm package (ibanforge-mcp) now wrapsPOST /v1/audit/upload,POST /v1/audit/checkout/:jobandGET /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 ajobid, with a plain-language note that the full annotated.xlsxis 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: truealso returns the Checkout URL for a human to open;audit_statuspolls payment and returns the download link once paid. A file over 5 MB (mirroringAUDIT_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.tsrecords 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 a200withfound: 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 on429(src/lib/fca-register.ts); no marketing use, enforced byfca-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 instats.sqlite(fca_firm_cache), an expired copy served markedstaleonly while the register is down and never more than six hours past its day. UntilFCA_REGISTER_API_KEYandFCA_REGISTER_API_EMAILare set the route answers503 not_configuredbefore 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-consolemakes 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 withnode:cryptoand exchanged for a read-only token (src/lib/search-console.ts), cached six hours instats.sqliteso opening the dashboard does not call Google,?refresh=1to spend a reading on purpose. Three answers rather than two: 503 whenGSC_SA_JSONis absent (every laptop, and not an incident), 502 carrying the last reading markedstalewhen 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 isnulland never0: 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/validatewith a realibanand 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 atrialblock (calls_used_today,calls_left_today,daily_limit,resets, the one request that mints a free key, and the docs link) andcost_usdc: 0: nobody was charged and nothing is booked as revenue. Past the ceiling the route answers402again withcause.reason: "trial_exhausted", the count served today, the reset and the free-key route — and, unlike an exhausted key, it keeps thefree_tierrail 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 itsinvalid_api_key402 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 withsource=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
verifiedwith aninstitutionblock 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. Sobank_code_check.authoritativestays false and a miss keeps the answer San Marino always gave —absent_from_reference_data, nevernot_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/smand/sm/{code}, doc/docs/sm-bank-codesin EN, FR and DE, each carrying that caveat rather than burying it. Measured before the change: every San Marino IBAN answeredbic: null, including all four real banks — the eleven curatedSM: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, andbic.basis: national_registerbecause 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 answersnot_in_register/not_allocated: bank code 1200 is not in the register, exactly as Austria'sAT61 1904…already reported. Pages/skand/sk/{code}for every code (the whole register, not a first batch), doc/docs/sk-bank-codesin 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 (PaymentRequiredExceptionwith the x402 challenge,QuotaExhaustedException,RateLimitException,PayloadTooLargeException,InvalidInputException,ApiException), the free routes without a key. Published through the Sonatype Central Portal (namespacecom.ibanforgeverified by DNS, signed, with sources and javadoc) by.github/workflows/java-publish.ymlon ajava-v*tag. Source undersdks/java/, eighty tests on a local HTTP server. -
.NET SDK.
IBANforge.Sdk1.5.0 (net8.0, no dependency beyond the framework): the same surface as async methods with aCancellationToken, records withSnakeCaseLowernaming, anHttpClientof the caller's choosing (IHttpClientFactory), the same exception hierarchy. Source undersdks/dotnet/, fifty-two xUnit tests,dotnet packverified;.github/workflows/dotnet-publish.ymlpushes to nuget.org on adotnet-v*tag once the API key exists. Both SDKs have their recipe on/docs/recipesin 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.tsturns 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 inkv_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 inGET /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-statsreturns 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 anattributionobject:text"Powered by IBANforge",url, and anotesaying 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, branch18.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/blzand/iidwith 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 throughPOST /v1/iban/batchin 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/sheetsin EN, FR and DE with the by-hand install path until the Marketplace listing exists; icons under/sheets/; README with the publishing procedure andLISTING.mdwith 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 inGET /v1/credits/bundlesundersubscription, 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 singleapiKeyvariable, 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
outputSchemapublished bytools/listand rejects an undeclared property in a closed object, so a German, Swiss, French or British validation was served with HTTP 200 andisError: falseand still thrown away by the official client: the schema omitted the BIC'ssource,as_of,lei,lei_status,addressandpostal_address, the register'sinstitutionand the Polishcheck_digitonbank_code_check, the Swissqr_iid_source, the UKmodulus_check,pra_authorisationandpsd_registration, and thescreenedflags of the compliance block.validate_iban,batch_validate_ibanandcheck_compliancenow 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_dateis 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.tsconverts with the European DST rule (noIntlin client components), the journal groups days in Swiss time, and a draft with originclaudecarries 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, andPOST /v1/audit/uploadallows five uploads every ten minutes per address, keyed on the platform-appended hop. -
/v1/ops/recentno 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 opaquecursor, 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-forsegment, the one the client writes (M1); it now readsx-real-ip, thenx-vercel-forwarded-for, then the lastx-forwarded-forhop. -
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
/auditsample 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 andfinalizeCandidate),POSTABLE_SOURCE_NAMESlimited 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.ymlpublishesibanforge-mcpand@ibanforge/sdkwith--provenancethrough 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
setupFileshook now gives every file its ownstats.sqlite, parallelism kept. The boot-corruption test creates its own healthy database instead of copying the checkout's.
Removed
- The
/livevillage 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}/liveredirects (temporarily) to the playground for the links already shared. The code is kept whole at git tagvillage-pause-2026-09-02. The backend feedGET /v1/ops/recentstays, now unused.
[1.5.0] — 2026-09-02
Added
- Creditor file audit, sold at a displayed price.
POST /v1/audit/uploadtakes 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/auditand/audit/donein EN, FR and DE. - Swiss QR-bill payload check, free.
POST /v1/ch/qr-bill/checkreads 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 withproposed_structured. Ninth MCP toolcheck_swiss_qr_billon all three servers, page/tools/qr-bill, documented in the OpenAPI contract and the docs. - Runtime-dependency guard.
npm run deps:checkfails when code undersrc/imports a package that is not a production dependency; it runs in CI and insidenpm 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
xlsxfromdevDependencies; the production image runsnpm ci --omit=dev, the container died at boot, and because the service mounts a volume the previous container had already been stopped.xlsxmoved todependencies; 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/x402document, and five in the staticmcp.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 fromsrc/mcp/inventory.ts, a ninth tool would publish itself everywhere at once, and a test fails when any document drops one. .well-known/x402says what can be tried before paying. Itsfree_endpointslist named/v1/demoand 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 noErrorcomponent existed — so a generated client (including the Custom GPT thatintegrations/openai/custom-gpt-setup.mdbuilds by pasting this document) could type every success and no failure, for a server whose failures are perfectly regular. A newApiErrorcomponent describes the{error, message}shape they have always had, withadditionalPropertiesleft open for the contextual recovery hints several routes add.429is now declared on every operation and413on 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/rotateandGET /v1/credits/balanceauthenticate 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/feedbackandGET /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.
sanctionson a BIC lookup (the one compliance signal on the cheap endpoint),qr_iid_sourceandqr_iidson Swiss clearing,processing_mson a batch (omitted from arequiredlist that named its four siblings) andmetaon 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.txtfiles 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/:codeanswered 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.listedisnull, neverfalse, 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, since402means "pay" and invites an agent to send the payment again. Such a case now answers502withpaid: null,confirmation_received: falseandauthoritative: 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 answervalid: nullwithstatus: unverifiable_without_creditor_config, because their rules are configured per creditor account by the beneficiary bank — answeringfalsewould reject valid references. Every scheme answer names the publishing document and its date.reference_checkonPOST /v1/iban/validate— pass areferencealongside 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 thevalidate_payment_referenceMCP tool on all three MCP surfaces.postal_addresson BIC answers —GET /v1/bic/{code}andPOST /v1/iban/validatenow carry the institution's address in ISO 20022 vocabulary (TwnNm,Ctry,PstCd,StrtNm…), with provenance andas_ofper block, for the November 2026 structured-address deadlines. Swiss institutions come fully structured from the SIX register; GLEIF-enriched entries arehybrid, keeping a concatenated street asAdrLinerather than guessing a split intoStrtNm+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) whosetowncolumn 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. Thecbpr+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_authorisationon 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 answersauthorised: 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 bothGET /v1/ch/clearing/:iidand 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_setupin 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/complianceaccepts abicinstead of aniban. 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 bothibanandbicis 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_bic8onGET /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.foundkeeps its exact meaning, so nothing that reads it changes behaviour.uk_moduluson/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 reportsstale: nulland does not turn the endpoint red, because that dataset is allowed to be missing.check_postal_addressMCP tool, on all three surfaces — the freePOST /v1/address/checkis now callable by agents over stdio (npmibanforge-mcp), the embedded stdio server and the HTTP transport. The npm and HTTP surfaces declare input and output schemas, sofindings[].sourcesurvives client-side schema validation instead of being silently stripped — the provenance is the point of the feature. Seven data tools (plussend_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/v1index and the npm README now all nameGET|POST /v1/reference/validateandPOST /v1/address/checkas 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/generatedocuments the optionalcodefield, the403and503it 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.codeis 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-mcpnow declaretitleandreadOnlyHint: 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_participanton every validate response — bank-level VoP readiness:truewhen the resolved institution is listed as ready in the EPC Verification of Payee scheme register (refreshed weekly),falsewhen it is not,nullwhen no institution was resolved. Country-level duty stays insepa.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 arenull, 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/generatereturnsterms_url, every 402 body carries atermsfield, 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.cjsdeletes 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_ibanstates its LIMITS explicitly. A repo-wide guard test (now scanning.mdxtoo) 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) andfeedback; the DPA 4.7 termination purge reachesoperations; 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/Aas 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_registerit deserves. ibanforge-mcp1.4.2 published — every outputSchema now declares nullable issuer types (an enum withoutnullmade MCP SDKs silently dropstructuredContenton exactly the answers that matter most),bank_code_check/next_steps/vop_participantare declared, and thekyc/amlnpm keywords are gone — they recruited the regulated name-screening use the Terms explicitly exclude.
Fixed
- ⚠️
/v1/iban/complianceno 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-pointmediumthreshold, so the less the API established, the safer its answer looked. Measured in production, a one-character typo took a Russian IBAN fromcritical/ 90 /sanctioned_countrydown tolow/ 10. Such a response now carriesrisk_score: null,risk_level: "unassessable"andflags: ["iban_invalid"].unassessableis 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 omittingmeta, so it never carried thebank_bic_onlydisclaimer; 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 / <300with 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, namingGET /pingso the caller can measure the network half himself. - MCP
lookup_bicnow also returnscountry: { code, name }, the shape REST has always used and thatvalidate_ibanalready shared. The flatcountry_code/country_namepair 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.namefalls back to the country code,country_namestill answersnull. - MCP
validate_iban's documented shape matches what it returns. It promisedbic: { code, institution, country_code, city }and returnsbic: { 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. metanow dates both country signals.risk_indicators.country_riskis a separate editorial AML axis layered on top ofcompliance.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 IBANCH93 0076 2011 6238 5295 7, never an allocated IID — a fixture that leaked into production data),31100and83036→ "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 answerbic: null, which is the truth for an unallocated bank code. compliance.risk_scoreis now nullable andcompliance.risk_levelgained the valueunassessable. Declared everywhere the contract is published: OpenAPI, the x402 discovery schema, both MCP output schemas, and the TypeScript and Python SDKs. Consumers that switch onrisk_levelor do arithmetic onrisk_scoreshould handle both before upgrading. The playground now renders an unassessable verdict as a neutral "non évalué · —" chip instead of a greenlow · 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/validatenow 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-friendlyinvalid_bban_structuredetail naming the field, the position and the expected charset. Check digits00/01/99(outside the ISO 13616 range 02–98) and non-numeric check digits are rejected asinvalid_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 andrisk_indicators./v1/iban/structure/:countrynow exposes per-field SWIFT charsets (4!a,8!n…), the fullbban_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
causeobject (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. NewX-API-Key-Invalidheader 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
causein its_hint, labels quota/credits/key-caused fallbacks asDEGRADED 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) andPOST /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,validateBatchinput guards, full HTTP-status → typed-error mapping (401/403/402/429-quota/429-rate/4xx/5xx), timeout/network wrapping, and theusage()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_clearingtable (SIX BankMaster) is now reseeded and committed by the existing monthly database cron, in the same workflow as the BIC database (both live indata/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/batchcall 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_insufficientcauses,X-Credits-Required+X-Credits-Remaining/X-Quota-Required+X-Quota-Remainingheaders) and nothing is consumed; handler-level 4xx rejections refund the full pre-charge. Successful multi-unit charges are surfaced viaX-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
suspendedstatus (surfaced as-is insanctions.fatf_status), scored at least as severely as non-membership (+10,fatf_suspendedflag). 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 tostandard: 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 withhigh_risk_country+sanctioned_country+fatf_suspended. - Revolut LT IBANs answered
sct:false, no_vop— Revolut Bank UAB is EPC-registered underRVUALT2Vwhile its customer IBANs resolve to BICREVOLT21; 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/30000answerediid:"30000", qr_iid:"9000". BankMaster QR rows (range 30000–31999) carry the institution's standard IID in their QR-IID column; the lookup now presentsiid= standard IID (09000),qr_iid= the queried QR-IID (30000), plusis_qr_iid: trueand 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 ofX-Forwarded-For) instead of the attacker-controlled first segment, so a forgedX-Forwarded-Forcan no longer rotate around the rate limiter. Single shared extractor (extractClientIp) used by both call sites. - Swiss-clearing seed sanity floor —
seed-bc-nummer.tsaborts before droppingch_clearingif 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_iidis a genuinely distinct allocation column (not a copy of the clearing IID), andgetCountryRiskis 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.typedmarker (PEP 561). The package already advertisedTyping :: Typed, but without the marker file downstreammypy/pyrightsilently 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/famillereworked 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-publisherinstalled from its release binary. - TypeScript SDK published as
@ibanforge/sdkwith 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.txtplain-text index,agent.jsonandmcp.jsonpath aliases, regeneratedllms.txt/mcp.jsonto 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
ibanforge1.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/formatendpoint — pure mod-97 + structure check, no DB hits, no API key, no quota. Returns valid/invalid + bban breakdown +upgrade_to_full_validationhint 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. /agentspage (EN/FR/DE) — agent-first integration guide with 3 paths (MCP, free key, x402)/openapipage — 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-liveandblinkanimations,.eyebrow,.kv-grid,.endpoint-row,.tnum,.tracking-capsutility classes - Reusable components:
StatusDot,RiskChip,EndpointRow,ApiKeyDialogwith provider - Per-locale
<title>,<meta description>,<html lang>,hreflangalternates (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.tomlforstats.sqlite(api keys, quotas, revenue) - WAL mode + busy_timeout + 5 missing indices on
stats.sqlitefor concurrent throughput - Permissions-Policy header denies camera/microphone/geolocation/payment/usb
- Content-Security-Policy on HTML responses (landing, MCP card)
IBANFORGE_FREE_MODEenv 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
outputSchemawith bare-output examples on every accept entry (CyberSapper recipe from CDP Discord) — unblocks CDP catalog + agentic.market indexing
Changed
ensureWalletConfigurednow fail-closes in production: missingX402_ENABLEDorWALLET_ADDRESStriggers 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_SECRETis now a distinct env var (not the password), session token includes a signediat, comparison is timing-safe - CORS_ORIGIN must be explicit in production (no wildcard); boot crashes if missing or
* - Frontend
nextupgraded 16.2.2 → 16.2.4 (fixes high-severity DoS advisory in Server Components) - Backend
npm audit fixresolves 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
/frand/denow serve localized titles + descriptions <html lang>was alwaysenregardless 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_SECRETrequirement 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:
- Set
SESSION_SECRETin production (openssl rand -hex 32) - Set
CORS_ORIGINto an explicit comma-separated list of your origins - Verify the Railway volume is mounted at
/app/data(boot logs warn if not) - To run in explicit free mode in production, set
IBANFORGE_FREE_MODE=true
Le changelog est tenu en anglais.