Skip to main content

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:

  1. The tool surface, a set of named MCP tools whose input and output JSON Schemas are fixed by the spec.
  2. 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 in bank2ai.json follow 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 marked required in 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.

NamePurpose
get-accountsList 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-transactionsList 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-transactionLook up a single transaction by id. Returns every optional field the server can populate, including ISO 20022 metadata. The audit / reconciliation entry point.
get-categoriesList the bank's transaction categories.
get-transactions-summaryAggregated 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-recipientsLook up saved payment recipients by partial name match.
create-recipientSave a new recipient for future transfers.
prepare-transferPrepare 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-transferExecute 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-transfer called. Servers SHOULD reject execute-transfer calls 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 outputSchema in tools/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 outputSchema from tools/list and instead registers a describe-tools meta 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:

  1. 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 omit outputSchema and the client MAY call describe-tools to fetch them.
  2. The client calls bank2ai tools as the user requests them. The server resolves credentials internally (see §4) and rejects calls it cannot authenticate.
  3. On a transfer, the client calls prepare-transfer first to validate, surfaces the prepared details to the user, and only invokes execute-transfer after 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 PSD2 accountDetails. id, accountNumber, currency, balance; optional typed identifiers iban / bban / bic / maskedPan; optional availableBalance, overdraftLimit, ownerName, product, openedDate, balanceUpdatedAt; optional typed balances array (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 under Current; their attached card is signalled by maskedPan. Top-level balance and availableBalance are derived shortcuts for the most-recent ClosingBooked and InterimAvailable entries when balances is populated; servers MUST keep them consistent. Servers MUST omit the statement-cycle fields on non-credit accounts.

  • Transaction. Profile of: ISO 20022 EntryDetails2, Berlin Group PSD2 transactions, and Open Finance cardTransactions. 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 Group bookingDate but is renamed to date because 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 by date without branching on booking state. Optional isPending (boolean profile of Berlin Group bookingStatus collapsed 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 absent isPending as equivalent to false. The get-transactions verbosity parameter selects between two payload shapes. At minimal (default), each Transaction carries every field except counterparty and the properties audit bag — the merchant / counterparty name is read off description and rarely-used audit metadata is dropped to keep rows compact. At full (and from get-transaction), Transactions additionally carry counterparty (the merchant or other party; for card transactions, counterparty.brandName holds the parent chain when counterparty.name is 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) and properties — a dict<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 through id (see §6). Servers SHOULD use the canonical ids listed below when a category maps cleanly; non-canonical ids remain valid and clients MUST treat any id as opaque.

    Canonical Category.id values: 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 via Category.name.

  • Recipient. Profile of: ISO 20022 Creditor and CreditorAccount (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 loose accountNumber + accountNumberType pair 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 content field describing the problem. The mutating tools' response envelopes (CreateRecipientResponse, PrepareTransferResponse, ExecuteTransferResponse) additionally carry an optional code field 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-transfer MUST return expiresAt. Servers SHOULD set this 5 minutes ahead of the prepare time as a recommended default; per-rail values MAY shorten or extend. execute-transfer against an expired intent MUST return a recoverable error with code: "intent_expired". The client is expected to call prepare-transfer again to obtain a fresh intent.
  • Immutable binding. The prepared transfer's amount, creditor, debtor account, and rail MUST NOT be modified between prepare-transfer and execute-transfer. The execute-transfer input takes only transfer_intent_id (and an optional idempotency_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 fresh prepare-transfer call.
  • Structured Confirmation of Payee. On rails that support payee verification (UK Confirmation of Payee, EU Verification of Payee, etc.), servers MUST populate PreparedTransfer.confirmationOfPayee when the verification has run; on rails that do not, servers MUST omit the field. The four status values (match, close-match, no-match, unavailable) are the structured signal the client renders; suggestedName carries the actual name on the destination account when the rail returns it on close-match or no-match.

6. Localization

  • Currencies are ISO 4217 (USD, ISK, EUR, …). Account.balance and Account.availableBalance are in the account's currency. Transaction.amount is 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.originalCurrency and Transaction.originalAmount preserve 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.id for programmatic identity; filtering MUST go through the category_ids parameter on get-transactions / get-transactions-summary, and Transaction.category_id resolves to the matching Category from get-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.