Finey
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Fineywhat's my current account balance?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Finey
Finey is a personal finance dashboard and MCP server for bank accounts linked through openfeed, an Australian Consumer Data Right (open banking) data aggregator. It has three parts:
Web app (Next.js + shadcn/ui): link bank accounts, view balances and transactions, manage syncs.
MCP server: exposes the same cached data as tools, so Claude (or any MCP client) can query your accounts and transactions directly.
SQLite database (via Drizzle): the local cache and source of truth for both the web app and the MCP server. Refresh tokens are encrypted at rest (AES-256-GCM); access tokens are never persisted.
This document explains both how to run an existing checkout and how to set the whole thing up from scratch against your own openfeed developer account.
How it works
openfeed uses FAPI 2.0: Pushed Authorization Requests (PAR), PKCE, private_key_jwt client
authentication (no client secret, just a signed JWT), and DPoP-bound access tokens (RFC
9449, proof-of-possession tokens instead of plain bearer tokens). None of the PAR or token
exchange calls can happen from a browser: openfeed rejects them if they carry a browser
Origin header, since a private_key_jwt client's private key must never reach a page.
That is why the whole OAuth exchange runs server-side, in Next.js route handlers, with only
the login and consent screen happening in the user's browser.
The flow, in order:
PAR: the server POSTs to openfeed's
/requestendpoint with a signed client assertion and a DPoP proof, and gets back a short-livedrequest_uri.Authorize: the user's browser is redirected to openfeed's
/authendpoint with thatrequest_uri. They log in to their bank and consent to sharing data.Callback: openfeed redirects back to the app's registered redirect URI with an authorization code.
Token exchange: the server exchanges the code for a DPoP-bound access token and a refresh token, again with a signed client assertion and a DPoP proof.
API calls: every call to
GET /v1/banking/accounts,GET /v1/banking/accounts/{id}/balance, andGET /v1/banking/accounts/{id}/transactionscarries a fresh, per-request DPoP proof.
Related MCP server: BankBridge
Prerequisites
Node.js 22 or newer (uses
node:crypto'srandomUUID/randomBytes, nativefetch, andprocess.loadEnvFile).An openfeed developer account and a registered app. If you do not have one, sign up at app.openfeed.au and register a new app (see below).
A Mac, Linux, or Windows machine with normal outbound internet access. openfeed rejects calls proxied through a restrictive network sandbox, so this will not work from an environment with locked-down egress.
Setting up from scratch
1. Register an app on openfeed
In the openfeed dashboard, register a new app with:
Auth method:
private_key_jwt(no client secret).Scopes:
openid,offline_access, andopenfeed-au:data:banking:read. Leave the energy scope unchecked unless you need it.Public key source: inline JWKS (you will paste this in step 3, after generating keys).
Redirect URI:
http://localhost:3000/api/auth/callback. The dev server has to run on port 3000 to match this exactly.
Note the app's client ID (looks like app-<uuid>) once it is registered.
2. Clone and install
git clone <this-repo> finey
cd finey
npm install3. Generate keys
npm run generate:keysThis creates, in a gitignored secrets/ folder:
finey_client_key.pem: the private client-authentication key (RSA-2048, PS256). Signs theprivate_key_jwtclient assertions.finey_client_key_public.pem: its public half.finey_client_jwks.json: the public key as a JWKS document.finey_dpop_key.default.json: a private DPoP key (RSA-2048, PS256). Signs DPoP proofs.
The script prints a kid and a JWKS document. Paste the JWKS into the openfeed dashboard's
"Public key (JWKS)" field for your app, then open lib/openfeed/config.ts and set:
export const CLIENT_ID = "app-<your app id>";
export const KID = "finey-<the kid the script printed>";4. Set up the encryption key
cp .env.example .env.localThen generate a 32-byte key and put it in .env.local:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"TOKEN_ENCRYPTION_KEY=<paste the output here>This key encrypts refresh tokens at rest. Losing it means every linked bank has to be re-linked; do not commit it.
5. Create the database
npm run db:migrateThis creates data/finey.db (SQLite, WAL mode, gitignored).
6. Prove the OAuth flow works, in isolation
Before touching the web app, run the standalone spike script. It does the full PAR, browser
consent, token exchange, and a real GET /v1/banking/accounts call, with no database or UI
involved:
npm run oauth:spikeIt prints a URL. Open it in your own browser, log in to a bank, and consent. The script
catches the redirect on localhost:3000 automatically and prints your real account data if
everything is wired up correctly. This is the fastest way to debug key, scope, or redirect
URI problems before layering the rest of the app on top.
Running
npm run devOpen http://localhost:3000 and go to Connections > Add bank.
This submits a real HTML form POST to /api/auth/start (not a client-side fetch, for the
reason explained above), which redirects your browser to openfeed. Log in and consent, and
you land back on the connection's detail page with accounts and balances populated.
Repeat for each bank you want to link. Each one becomes its own row in bank_connections
with its own encrypted refresh token.
Syncing and caching
Reads are cache-first: both the web app's pages and the MCP tools read straight from SQLite, never live from openfeed on every request. A background sync keeps that cache fresh:
Balances and account details are considered stale after 30 minutes.
Transactions are considered stale after 4 hours.
A stale read triggers a non-blocking background resync (stale-while-revalidate): you get the cached data immediately, and the next read picks up whatever the background sync found.
"Sync now" (the UI button, or the
sync_nowMCP tool) always forces an immediate, blocking resync regardless of staleness.
Transactions are deduplicated by openfeed's transaction ID, so re-syncing the same period repeatedly is always safe and idempotent.
Using the MCP server
The MCP server (mcp/server.ts) runs over stdio and exposes five tools:
list_bank_connections, list_accounts, get_balances, get_transactions, and
sync_now. See mcp/tools/*.ts for exact input schemas.
Claude Code
A project-level .mcp.json is already checked in. Open Claude Code with this repository as
the working directory and the finey server connects automatically.
Claude Desktop
Claude Desktop's MCP configuration format does not support a working-directory field, so the server needs an absolute path to its entry script and an explicit environment variable telling it where the project lives. In Settings > Developer > Edit Config, add:
{
"mcpServers": {
"finey": {
"command": "npx",
"args": ["-y", "tsx", "/absolute/path/to/finey/mcp/server.ts"],
"env": {
"FINEY_PROJECT_ROOT": "/absolute/path/to/finey"
}
}
}
}Restart Claude Desktop after saving. If you see Cannot find module '/mcp/server.ts' in
the logs, the config is missing one of these two fixes; see lib/root.ts for how path
resolution works.
Project structure
finey/
app/ Next.js App Router pages and API routes
api/auth/start starts the OAuth flow (PAR, redirect to openfeed)
api/auth/callback handles the redirect back, exchanges the code
api/connections list, sync, and unlink bank connections
connections/, accounts/ the dashboard pages
components/ shadcn/ui primitives plus app-specific components
lib/
openfeed/ the FAPI 2.0 client: JWT signing, PAR, token exchange,
DPoP proofs, and typed API wrappers
db/ Drizzle schema, SQLite client, token encryption
sync/engine.ts the cache-first sync engine shared by the web app and MCP
root.ts resolves the project root reliably across run contexts
mcp/
server.ts MCP entrypoint (stdio transport)
tools/ one file per MCP tool
scripts/
generate-keys.ts one-time key generation for a fresh app registration
oauth-spike.ts standalone end-to-end OAuth test
migrate.ts applies Drizzle migrations
secrets/ gitignored: client-auth key, DPoP key, JWKS
data/ gitignored: the SQLite databaseScripts
Command | Purpose |
| Start the web app at |
| Start the MCP server directly (normally launched by an MCP client instead). |
| Generate a fresh client-auth key and DPoP key. |
| Standalone end-to-end OAuth test, independent of the database and UI. |
| Generate a Drizzle migration after editing |
| Apply migrations, creating |
| Run |
Notes on the openfeed API
Some of this was not obvious from documentation and was only confirmed by making real calls:
The account list (
GET /v1/banking/accounts) does not include balances at all. Balance is a separate call,GET /v1/banking/accounts/{id}/balance, returningcurrentBalance,availableBalance,creditLimit, andcurrency.An account's primary key field is
accountId, notid. A transaction's primary key field istransactionId, notid.Institution name and ID (
providerName/providerId) are included directly on each account. No separate institution lookup is needed. Different accounts under the same connection can report different provider names.Transactions have no
categoryfield. The closest equivalents aretransactionType(for examplePAYMENT,TRANSFER_INCOMING) andmerchantCategoryCode(a raw MCC number, not a friendly category name).Access tokens last one hour (
expires_in: 3600).offline_accessyields a refresh token, and in testing that refresh token was not rotated on use (the same value kept working across repeated refresh calls).Re-consenting to the same institution can invalidate the previous grant's refresh token (it starts failing with
invalid_grant), even though the two grants may not cover the same set of accounts. The sync engine detects this, marks the connectionerror, and records the failure insync_runs; you then need to unlink and re-add that bank.Only the scopes actually enabled on your app registration will be accepted. Requesting a scope that exists in the discovery document but is not enabled for your app (for example a
grant:self:*scope) fails withinvalid_scope, not a silently ignored request.
Security notes
Only refresh tokens are ever written to disk, and only encrypted (AES-256-GCM, key in
.env.local, never in the database). Access tokens are short-lived and never persisted.The client-auth and DPoP private keys live as plain files under
secrets/, gitignored, never logged..gitignorecovers.env.local,secrets/, anddata/(including the SQLite WAL and SHM sidecar files). Checkgit statusbefore your first commit rather than trusting the ignore file blindly."Unlink" calls openfeed's revocation endpoint so consent is actually withdrawn at the bank, not just deleted locally.
The dev server binds to localhost only. It has no authentication of its own by design (a personal, single-user tool), so it must never be exposed to a network or reverse proxied publicly.
Troubleshooting
"Bad Request" when opening the consent link. The PAR request_uri is short-lived
(around 60 seconds). If more than a minute passes between generating the link and opening
it, generate a fresh one and open it immediately.
PAR fails with invalid_scope. You are requesting a scope your app registration does
not have enabled. Check the scopes checked on the openfeed dashboard against
SCOPES in lib/openfeed/config.ts.
Token exchange fails with invalid_grant on a previously working connection. The
refresh token has been invalidated, most often by re-consenting to the same institution
from a different connection. Unlink the broken connection and add it again.
MCP server logs Cannot find module '/mcp/server.ts'. The client that launched it did
not set a working directory. Use an absolute path in the args and set
FINEY_PROJECT_ROOT in env, as shown in the Claude Desktop section above.
"Sync now" or a background sync fails after linking a new bank. Check
bank_connections.last_error (or the connection detail page in the UI, or the
list_bank_connections MCP tool) for the underlying openfeed error message before
assuming it is a bug in Finey.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read-only access to your bank, investment, and crypto accounts: balances, transactions, holdings.
Read-only access to Genie accounts, transactions, investments, and financial summaries.
An MCP server that provides read access to your cloud storage providers, bank accounts and more.
- TruoOAuthau.com.truo
Ask your AI about your own Australian bank accounts. It is read-only at the bank.
Related MCP Servers
- AlicenseBqualityDmaintenanceEnables read-only access to personal bank accounts via the TrueLayer API, allowing querying of accounts, balances, transactions, and generating financial analytics such as spending by category, subscriptions, and monthly summaries.9MIT

BankBridgeofficial
FlicenseNot gradedqualityDmaintenanceEnables financial data access from connected bank accounts via MCP tools, allowing natural language queries about balances, transactions, subscriptions, investments, and more, with a focus on privacy and read-only access.-- AlicenseNot gradedqualityBmaintenanceEnables secure, read-only access to personal European bank accounts through Enable Banking and Cloudflare Workers, allowing MCP clients to list accounts, retrieve balances, search transactions, and summarize cash flow.1MIT
- AlicenseAqualityAmaintenanceEnables read-only access to Banco General accounts, cards, and pension data through MCP, allowing balance checks, transaction searches, and spending summaries without moving funds.1429 npm1MIT