Specification overview
Bank2ai connects digital banking data and operations with AI agents. The language of banking (accounts, transactions, transfers, bill payments, recipients, loans, savings) is universally identical, and bank2ai is the open standard that lets banks, fintechs, and AI builders collaborate on a single shared vocabulary instead of each reinventing one.
This document defines that vocabulary as a Model Context Protocol tool surface that any bank can expose so AI agents (and through them, end customers) can read accounts and transactions, look up recipients, run spending summaries, and prepare/execute transfers, using the same tool surface across every bank.
Compliant servers and the agent skills built on top of them are distributed through the bank2ai marketplace, which is packaged as a Claude Code plugin marketplace and consumable from any client that speaks the same plugin format.
The contract has two parts:
- The tool surface, a set of named MCP tools whose input and output JSON Schemas are fixed by the spec.
- The shared data models (
Account,Transaction,Category,Recipient) used inside tool inputs and outputs.
Authentication is intentionally outside the spec: servers obtain credentials however suits their backend (a bearer token from the inbound MCP access_token, server-configured API credentials, OAuth, etc.) and gate calls accordingly. See §4 for the rationale.
About RFC 2119 keywords. MUST, SHOULD, MAY are used per RFC 2119.
About field omission and tolerance. Schemas describe many optional fields, but default tool responses are lean. Servers SHOULD omit optional fields whose values are
null, empty, or equal to a documented default. Clients MUST tolerate missing optional fields, and MUST also tolerate unknown fields (see §7). Example payloads in this document and inbank2ai.jsonfollow the same rule, so an example reflects a typical response rather than enumerating every possible field. Required fields are unaffected: servers MUST NOT omit fields markedrequiredin the schemas.
1. Tool surface
A bank2ai server MAY register any subset of the following tools. Tools that are registered MUST use these exact names and the input/output shapes defined in bank2ai.json under tools[].inputSchema and tools[].outputSchema.
| Name | Purpose |
|---|---|
get-accounts | List bank accounts and cards with balances and identifiers (account number plus IBAN/BBAN/BIC or masked PAN where available), optionally filtered by type, lifecycle status, usage, or withdrawal-eligibility. |
get-transactions | List transactions, with filters for account, date range, signed amount range, categories, free-text search, and result count. Supports cursor-based paging via cursor / nextCursor, and a verbosity cap (minimal (default) / full) that selects between a compact LLM-friendly payload and the full transactions view. |
get-transaction | Look up a single transaction by id. Returns every optional field the server can populate, including ISO 20022 metadata. The audit / reconciliation entry point. |
get-categories | List the bank's transaction categories. |
get-transactions-summary | Aggregated transactions, scoped to either income or expenses (required direction). Group by none, category, month, or both; each row reports the corresponding category_id and/or month. Filters mirror get-transactions: account, date, amount, category ids. |
get-recipients | Look up saved payment recipients by partial name match. |
create-recipient | Save a new recipient for future transfers. |
prepare-transfer | Prepare a transfer on any supported rail (Rail enum: domestic-IS, sepa, sepa-instant, swift, plus vendor extensions). Returns a transferIntentId, a validated summary for user confirmation, and rail-specific metadata (fees, FX, Confirmation of Payee, warnings). Does not execute. |
execute-transfer | Execute a previously prepared transfer by transferIntentId. Servers reject expired or unknown intents with a structured error; the intent's amount, creditor, debtor, and rail are immutable. |
Servers MAY also register additional, vendor-specific tools, but they MUST NOT alter the names, inputs, or outputs of the tools above.
Why "prepare → execute"? Splitting transfers into two tools keeps the AI agent on a safe rail: the agent gathers details, the user confirms in their UI, and only then is
execute-transfercalled. Servers SHOULD rejectexecute-transfercalls that don't correspond to a recently prepared transfer.
1a. Output-schema disclosure
The bank2ai tools defined above have stable output JSON Schemas, captured under tools[].outputSchema in bank2ai.json. Inlining every one of these schemas in tools/list adds tens of kilobytes to every cold connection, even though a typical agent only needs an output schema after it has chosen which tool to call. Servers therefore MAY follow one of two disclosure modes, which differ only in how the schemas reach the client:
- Inline (default): the server returns each tool's full
outputSchemaintools/list, as MCP normally does. Best for clients that cache tool definitions once and prefer a single round-trip. - Discovery (progressive disclosure): the server omits
outputSchemafromtools/listand instead registers adescribe-toolsmeta tool, defined below. Clients fetch the schemas they need on demand. Best for bandwidth-sensitive deployments and for agents that only ever invoke a small subset of the surface.
A server MUST pick exactly one mode per connection, and a server in discovery mode MUST register describe-tools. The shape of every tool's output is the same under both modes — only the transport differs.
The describe-tools tool, when registered, MUST accept an optional tool_names: string[] input and MUST return:
{
"schemas": {
"<tool-name>": { "outputSchema": { ... } | null }
}
}
When tool_names is omitted, the server MUST return an entry for every bank2ai tool it has registered. When tool_names is provided, the server MUST return one entry per requested name, using null for names it does not recognise (rather than raising an error). describe-tools is itself excluded from progressive disclosure: its own outputSchema is intentionally absent so it stays self-bootstrapping.
The canonical JSON Schemas in bank2ai.json remain fully inlined regardless of which runtime mode a server picks — the spec file is the contract for humans and code generators, and discovery is purely a runtime optimisation.
2. Lifecycle
A typical bank2ai session looks like this:
- The MCP client connects and calls
tools/list. The server returns the bank2ai tools it has registered (any subset of those listed in §1). If the server is in discovery mode (§1a), tool entries omitoutputSchemaand the client MAY calldescribe-toolsto fetch them. - The client calls bank2ai tools as the user requests them. The server resolves credentials internally (see §4) and rejects calls it cannot authenticate.
- On a transfer, the client calls
prepare-transferfirst to validate, surfaces the prepared details to the user, and only invokesexecute-transferafter explicit confirmation.
3. Shared data models
The schemas in bank2ai.json under models{} define the canonical shapes listed below.
Each model is a profile of one or more upstream standards: it adopts a strict, named subset of fields with semantics preserved. Profiling buys interoperability without forcing servers to implement the full upstream surface. Where a model maps to a specific upstream element, this is called out with a "Profile of:" line at the top of the model's description; models that are bank2ai-defined and don't profile a single upstream are noted as such.
-
Account. Profile of: Berlin Group PSD2accountDetails. id, accountNumber, currency, balance; optional typed identifiers iban / bban / bic / maskedPan; optional availableBalance, overdraftLimit, ownerName, product, openedDate, balanceUpdatedAt; optional typedbalancesarray (ClosingBooked|Expected|InterimAvailable|ForwardAvailable|NonInvoiced); optional accountType (Current|Savings|Credit|Loan|Other), status (Enabled|Blocked|Deleted), usage (Private|Business), isWithdrawalAccount, isDefaultAccount; credit-only optional statementBalance, minimumPaymentDue, paymentDueDate, statementClosingDate. Statement-cycle fields go beyond PSD2 so an agent can answer "what do I owe and when?" without a second call. Debit and prepaid cards live underCurrent; their attached card is signalled bymaskedPan. Top-levelbalanceandavailableBalanceare derived shortcuts for the most-recentClosingBookedandInterimAvailableentries whenbalancesis populated; servers MUST keep them consistent. Servers MUST omit the statement-cycle fields on non-credit accounts. -
Transaction. Profile of: ISO 20022EntryDetails2, Berlin Group PSD2transactions, and Open FinancecardTransactions. One shape covers both account and card transactions. Always present: id, accountId, description, amount (in the user's default currency, negative = expense), date (ISO 8601 — the booking date when the entry is booked, the authorisation / point-of-sale date when it is still pending). Profiles ISO 20022 / Berlin GroupbookingDatebut is renamed todatebecause pending authorisations have no booking date yet — keeping a single, always-populated date field avoids forcing servers to fabricate one and lets clients sort and chart bydatewithout branching on booking state. OptionalisPending(boolean profile of Berlin GroupbookingStatuscollapsed to Booked → false / Pending → true): servers SHOULD set this to true only for pending authorisations and omit it on booked entries (the common case), so the field stays off the wire for the vast majority of rows; clients MUST treat an absentisPendingas equivalent to false. Theget-transactionsverbosityparameter selects between two payload shapes. Atminimal(default), each Transaction carries every field exceptcounterpartyand thepropertiesaudit bag — the merchant / counterparty name is read offdescriptionand rarely-used audit metadata is dropped to keep rows compact. Atfull(and fromget-transaction), Transactions additionally carrycounterparty(the merchant or other party; for card transactions,counterparty.brandNameholds the parent chain whencounterparty.nameis a specific outlet — e.g.name= "Starbucks Reserve Roastery",brandName= "Starbucks" — so clients can group by chain while still showing the outlet on each row) andproperties— adict<string,string>of ISO 20022 / Open Finance audit metadata keyed by name (remittanceInformation,creditorReference,endToEndId,maskedPan,merchantCategoryCode,categoryRaw,transactionCode,proprietaryBankTransactionCode,mandateId,creditorId,purposeCode,entryReference,additionalInformation, …). Servers SHOULD use those known keys when populating the corresponding value but MAY add server-specific keys; clients MUST tolerate any subset and any extra keys. Servers drop unset optional fields on the wire so every row stays compact. -
Category. bank2ai-defined categorization model; not profiled from a single upstream standard. id, name (localized). Localized names live here so clients can render category labels per the user's locale; programmatic identity goes throughid(see §6). Servers SHOULD use the canonical ids listed below when a category maps cleanly; non-canonical ids remain valid and clients MUST treat anyidas opaque.Canonical
Category.idvalues:Income,Transfer,Groceries,DiningAndEntertainment,Transport,Housing,Utilities,Shopping,Health,Travel,Subscriptions,Fees,Cash,Other. Sharing these ids across servers gives clients a portable taxonomy, with the localized display name still controlled per server viaCategory.name. -
Recipient. Profile of: ISO 20022CreditorandCreditorAccount(subset). id, name, accountIdentifier (typed discriminated union:iban/bban/accountNumber/alias); optional nickname, nationalId ({ value, country, type? }), bic, defaultDescription, lastUsedAt, isFavorite. Account routing flows through the typed identifier; the previous looseaccountNumber+accountNumberTypepair has been replaced. National identification is opaque labelling — bank2ai does not validate kennitala / SSN / etc. format.
Per the field omission and tolerance rule in the preamble, servers MAY omit any optional field they don't have, and clients MUST tolerate both missing optional fields and unknown additional fields. Servers MUST NOT omit fields marked required in the schemas.
4. Authentication
Bank2ai does not define an authentication protocol. How a server obtains the credentials it needs to talk to its backend is an implementation detail; servers MUST gate every bank2ai tool call on having valid credentials and MUST surface authentication failures as MCP errors.
Common approaches used by reference implementations:
- Inbound bearer token. A token attached to the MCP request (
access_token) is forwarded to the bank backend. Best when the MCP client is already authenticated against the bank's identity provider. - Server-configured credentials. The server reads credentials from its environment (e.g.
BANK2AI_*_EMAIL/BANK2AI_*_PASSWORD) and exchanges them for a backend session token, refreshing as needed. - Demo / no-auth. Servers backed by hardcoded data MAY skip authentication entirely.
Servers MUST NOT register a bank2ai-defined authenticate tool, earlier drafts of this spec described one and it has been removed.
5. Error model
- Servers SHOULD return MCP
ToolError(or equivalent protocol-level error) for unrecoverable failures, including authentication failures. - For recoverable user-facing conditions (e.g. "Insufficient funds", "Invalid recipient"), servers SHOULD return a successful tool call whose response model includes a human-readable
contentfield describing the problem. The mutating tools' response envelopes (CreateRecipientResponse,PrepareTransferResponse,ExecuteTransferResponse) additionally carry an optionalcodefield with a server-defined machine-readable identifier. Canonical codes:intent_not_found,intent_expired(execute-transfer).missing_creditor_identifier,insufficient_funds,invalid_account(prepare-transfer).- Servers MAY emit additional, server-specific codes; clients MUST treat unknown codes as opaque.
5a. Safety contract for mutating tools
These rules apply to every mutating tool in §1: create-recipient, prepare-transfer, execute-transfer.
- Idempotency. Every mutating tool accepts an optional
idempotency_key(≤128 chars). Servers SHOULD return the original response for repeat calls with the same key within at least 24 hours. The key is scoped to(tool name, caller), so two unrelated callers, or the same caller across two different tools, do not collide on the same key. Clients SHOULD generate keys that are unique to the logical operation being performed (e.g., a UUID per user action), not per request. - Intent expiry.
prepare-transferMUST returnexpiresAt. Servers SHOULD set this 5 minutes ahead of the prepare time as a recommended default; per-rail values MAY shorten or extend.execute-transferagainst an expired intent MUST return a recoverable error withcode: "intent_expired". The client is expected to callprepare-transferagain to obtain a fresh intent. - Immutable binding. The prepared transfer's
amount, creditor, debtor account, and rail MUST NOT be modified betweenprepare-transferandexecute-transfer. Theexecute-transferinput takes onlytransfer_intent_id(and an optionalidempotency_key) precisely so that the only intent that can be executed is the one the user confirmed. Any change to amount, creditor, debtor, or rail requires a freshprepare-transfercall. - Structured Confirmation of Payee. On rails that support payee verification (UK Confirmation of Payee, EU Verification of Payee, etc.), servers MUST populate
PreparedTransfer.confirmationOfPayeewhen the verification has run; on rails that do not, servers MUST omit the field. The fourstatusvalues (match,close-match,no-match,unavailable) are the structured signal the client renders;suggestedNamecarries the actual name on the destination account when the rail returns it onclose-matchorno-match.
6. Localization
- Currencies are ISO 4217 (
USD,ISK,EUR, …).Account.balanceandAccount.availableBalanceare in the account'scurrency.Transaction.amountis normalized to the user's default currency so clients can render transaction lists without per-row currency conversion; when the transaction was originally made in a different currency,Transaction.originalCurrencyandTransaction.originalAmountpreserve the original. Clients SHOULD omit the currency symbol on transaction amounts unless the user explicitly asks which currency a transaction is in. - Dates are ISO 8601 (
YYYY-MM-DD). - Category names are localized server-side. Clients MUST treat category names as opaque user-facing strings and use
Category.idfor programmatic identity; filtering MUST go through thecategory_idsparameter onget-transactions/get-transactions-summary, andTransaction.category_idresolves to the matchingCategoryfromget-categories.
7. Backwards compatibility
The spec versioning policy lives in README.md. Notable additive-change rules:
- Adding a new optional tool input is a minor bump.
- Adding a new optional output field is a minor bump; clients MUST tolerate unknown fields.
- Adding a new tool is a minor bump.
- Removing or renaming anything, or making a previously optional field required, is a major bump.
8. Reference implementations
examples/demo, full surface backed by hardcoded data; useful for client conformance testing without a real bank.