Skip to main content

Wrap a real bank

This guide walks through the patterns for wiring a real bank backend behind the shared bank2ai tool surface, including how to handle the most common authentication pattern (credentials → bearer token).

Configure

Most banks expose a base URL and require some form of credentials. A typical env layout:

VariableRequiredDescription
BANK2AI_<BANK>_BASE_URLyesAPI base URL for the bank backend.
BANK2AI_<BANK>_EMAILnoDefault credential email (otherwise prompted via MCP elicitation).
BANK2AI_<BANK>_PASSWORDnoDefault credential password.
BANK2AI_<BANK>_CULTUREnoLocale, e.g. en-GB.

Adjust the variable names for your bank, and copy .env.example.env before running.

Run

uv run --package bank2ai-<your-bank> bank2ai-<your-bank>

How authentication works

A typical bank2ai server supports three credential paths, in order of preference:

  1. Inbound MCP access_token. If the MCP client forwards a bearer token issued by the bank's identity provider, the server uses it directly. Best for clients that have already authenticated.
  2. Server-configured email + password. If credential env vars are set, the server exchanges them for a bearer token at startup and refreshes as needed.
  3. MCP elicitation. If the client supports elicitation, the server prompts the end user interactively for email / password. Otherwise it exposes a dynamic authenticate tool that the LLM can call once with credentials.

This three-way fallback is a useful template, most real banks need at least options 1 and 2.

How handlers map onto a bank API

Each bank2ai tool is implemented as a thin async handler that calls the bank API and maps the response into the bank2ai shape. The handler does the work the spec defines; the mapper does the work your backend shape forces.

async def get_accounts(*, only_withdrawal_accounts, account_type, status, usage):
rows = await bank_client.list_accounts()
if only_withdrawal_accounts:
rows = [r for r in rows if r["IsActive"] and r["IsAvailable"]]
if account_type:
rows = [r for r in rows if r["AccountTypeName"] == account_type]
if status:
rows = [r for r in rows if r["Status"] == status]
if usage:
rows = [r for r in rows if r["Usage"] == usage]
return [_to_bank2ai_account(r) for r in rows]

For a working reference implementation, see the example servers in the examples/ directory.

What to copy when wrapping your own bank

  1. Project layout. pyproject.toml, src/<your_pkg>/server.py, src/<your_pkg>/__main__.py. Mirror an existing reference server.
  2. Credential handling. Whichever of the three patterns above fits your backend.
  3. Mappers. One per bank2ai shape, _to_bank2ai_account, _to_bank2ai_transaction, etc. Unit-test these.
  4. The two-step transfer flow. Cache the prepared transfer keyed by (withdrawal_account_id, recipient_account_number, amount) and reject execute_transfer calls without a matching preparation. See Writing handlers → prepare → execute.

Then run the drift test and you're compliant.