Read-Only Bank Account Integration for AI Assistants: Technical Implementation and Security Boundaries
This article explains how an AI assistant can integrate with personal bank accounts in a read-only manner. It covers OAuth-based account aggregation, token governance, field-level data minimization, deterministic computation outside the model, privacy retention and training isolation, and a practical implementation checklist. The key idea is to keep the assistant inside a narrow permission boundary: it can retrieve and explain information, but it must not initiate payments, modify account settings, or trigger any write operation.
1. Why Read-Only Is the Core Security Boundary
When connecting an AI assistant to a financial account, the biggest risk is not that it “analyzes incorrectly,” but that it “acts incorrectly.” Personal financial data has three important characteristics:
- Leaks are irreversible. Balances, transaction histories, and spending habits cannot be reset like passwords.
- Highly correlatable. Financial data can be used to infer income, debt, health conditions, and location patterns.
- Directly connected to money flows. Write access implies the ability to transfer funds, set up recurring deductions, or place orders.
Because of these risks, products in this space commonly adopt a capability-reduction approach: they shrink the agent’s scope from the “fund operation domain” to the “information retrieval domain.” In permission-model terms, the AI is treated as a read-only principal and is not granted write, payment initiation, or credit-limit-change permissions.
This design choice brings two direct benefits:
- Controllable risk boundary. Even if the model hallucinates, falls victim to prompt injection, or suffers from session hijacking, the worst outcome is “seeing something it should not see,” rather than “transferring something it should not transfer.”
- Shorter compliance path. Read-type processing usually falls under personal information query and display. It does not require the product to take on payment institution licensing or clearing responsibilities, making audit boundaries and responsibility scopes clearer.
That said, read-only does not mean low risk. Even with read permissions, the system still needs to address unauthorized access, excessive data collection, over-retention, and context pollution. These are the real engineering challenges.
2. Account Access: Delegated Authorization and Token Governance
A common industry pattern is to avoid direct bank integration and instead use an account aggregation service as an intermediary. Technically, this is usually a variant of the OAuth 2.0 authorization code flow:
User → AI assistant: initiates “link account”AI assistant → aggregation service: generates authorization redirect URL (with client_id, scope, redirect_uri, state)User → aggregation service: completes identity verification and consent on the bank or aggregation sideAggregation service → AI assistant callback: returns authorization codeAI assistant → aggregation service: exchanges the authorization code for access_token / refresh_tokenAI assistant → aggregation service: uses the token to fetch account list and transaction history
Illustrative implementation, expressed as pseudocode to show the flow structure:
# 1. Build the authorization redirectdef build_auth_url(state: str) -> str:params = urlencode({"client_id": CLIENT_ID,"scope": "accounts.read transactions.read", # read-only scope; no payment-related scope"redirect_uri": REDIRECT_URI,"state": state, # one-time CSRF protection"response_type": "code",})return f"{AGGREGATOR_BASE}/oauth/authorize?{params}"# 2. Exchange the callback code for tokensdef exchange_token(code: str):resp = http_post(f"{AGGREGATOR_BASE}/oauth/token", data={"grant_type": "authorization_code","code": code,"client_id": CLIENT_ID,"client_secret": KMS.get("aggregator/client_secret"), # secrets must not be committed to the code repository})return resp.json() # short-lived access_token + long-lived refresh_token; refresh_token must be stored encrypted
Engineering requirements:
- Scope whitelisting. Only read-only permissions should be requested. Payment- and transfer-related scopes must be prohibited at the code level and checked by static analysis in CI.
- Encrypted custody of refresh_token. Long-lived tokens should be encrypted and stored in a key management service or dedicated credential store, isolated from the business database. Access should require secondary authorization and be recorded in audit logs.
- Short-lived access_token plus proactive revocation. Tokens should be revoked when the session ends. The product should provide an “unlink account” entry, and the revocation action should be synchronized with the aggregation service to prevent zombie authorizations from remaining valid indefinitely.
- state/nonce and one-time callbacks. These mechanisms help prevent authorization-link replay and session fixation attacks.
- Encryption in transit and at rest. Use TLS for the entire链路; use field-level encryption for stored fields such as account numbers and transaction records. Keys and data should be stored separately.
3. Data Minimization: Separate “What Can Be Retrieved” from “What the Model Can See”
This layer is often overlooked, but it is critical. The aggregation API can return far more fields than the model needs to consume. The correct approach is to enforce a server-side field-level whitelist and trim data before it enters the model context.
| Data item | Allowed to enter model context | Handling |
|---|---|---|
| Account balance, credit limit | Allowed | Convert to numeric values; remove account suffix |
| Transaction amount, date, merchant name | Allowed | Normalize merchant name into categories |
| Account number, card number, routing number | Prohibited | Trimmed server-side; cannot be bypassed |
| Login credentials, bank passwords | Prohibited | Held by the aggregation service; never touched by the business side |
| Name, address, ID number, contact information | Prohibited by default | Must be masked or replaced with placeholders |
# Context trimming: feed only necessary fields to the modelALLOWED_FIELDS = {"merchant_category", "amount", "date", "merchant_name"}def to_model_context(tx: dict) -> dict:ctx = {k: v for k, v in tx.items() if k in ALLOWED_FIELDS}ctx["merchant_name"] = normalize_merchant(tx["merchant_name"]) # normalize and remove random suffixesctx["amount"] = round(float(tx["amount"]), 2)return ctx
Supporting engineering constraints:
- Do not pull everything first and let the model choose later. Filter at retrieval time based on the target fields of the analysis task, reducing the window during which sensitive data is exposed in memory.
- Control context length and data volume. Inject only aggregated statistical results, such as “total dining expenses this month” or “average of similar bills over the last three months,” instead of stuffing thousands of raw transaction records into the prompt. This reduces both cost and exposure.
- Do not log intermediate results. Raw transaction details must not appear in application logs, telemetry, or crash reports. Logs should contain only non-sensitive metadata such as task ID, record count, and duration.
4. Model-Side Design: Move Numerical Computation Outside the Model
A common mistake is to let a large model directly sum transactions and infer trends. A model is fundamentally a probabilistic generator. It does not have reliable arithmetic ability for tasks such as multi-digit addition or month-over-month comparison. The practical approach is to give computation to deterministic tools and let the model only organize and explain results:
User question → intent recognition → select statistical tool (category summary / monthly comparison / duplicate subscription detection)→ deterministic code execution (SQL or aggregation function) → numerical result→ model only handles: result interpretation + natural language expression + terminology explanation
Example:
# Deterministic aggregation; results do not pass through model reasoningdef monthly_category_summary(user_id: str, months: int = 6):sql = """SELECT strftime('%Y-%m', ts) AS month,category,SUM(amount) AS total,COUNT(*) AS cntFROM txn_readonly_view -- read-only view; write operations are prohibitedWHERE user_id = :uidAND ts >= :sinceGROUP BY month, categoryORDER BY month DESC, total DESC"""return db.query(sql, uid=user_id, since=shift_months(months))
Likewise, the model should not be allowed to generate free-form text such as “execute SQL.” It should choose from predefined, parameter-limited query templates. Direct model-generated SQL statements should be disabled at the mechanism level, eliminating both data exfiltration and accidental write risks.
5. Privacy Retention and Training Isolation
Users often worry: “Will my transaction history be used to train models?” This requires both product commitments and engineering implementation; policy language alone is not enough. Practical measures include:
- Data partitioning. Store financial conversation data in an independent partition or a separate encryption-key domain, isolated physically or logically from general conversation data. This makes it easier to apply lifecycle policies by partition.
- Exclusion from training pipelines. Explicitly exclude this data source during data collection, cleaning, sampling, and labeling. Document the exclusion rule in data lineage documentation and review it regularly.
- Isolation from long-term memory. Do not write this type of conversation into cross-session long-term memory. If the user explicitly asks the assistant to “remember my budget,” store it as an explicit user preference entry that can be deleted individually.
- Retention period and automatic cleanup. Set a shorter retention window for raw transaction details. After expiration, automatically downsample them into aggregated statistics or delete them. Provide one-click export and one-click deletion.
- Auditability and explainability. Every read operation should record “who accessed it, when, which field types were read, and for which task.” Users should be able to view their access history.
6. Implementation Checklist for Self-Building
If you want to build similar capabilities in your own product, proceed in the following order:
- Define the permission model first. Make it explicitly read-only and prohibit payment-related scopes. Create an “allowed/prohibited field list” as the single source of truth.
- Define the data pipeline. Aggregation access → field trimming → masking → statistical aggregation → model consumption. Minimize exposure at every hop.
- Define the computation model. Delegate statistics and arithmetic to deterministic code; let the model only interpret and express results.
- Add supporting infrastructure. Use general cloud capabilities for critical components: a key management service to store tokens and encryption keys; a logging service for audit trails with sensitive fields removed; monitoring and alerting for abnormal retrieval volume, high-frequency token usage, and requests after unlinking; object storage for encrypted archival of user-exported files; and a message queue to decouple long-running analysis tasks.
- Validate from an adversarial perspective. Test four scenarios: prompt injection that tries to make the model request card numbers, unauthorized access through cross-user ID queries, bulk export, and token leakage. Convert the findings into regression test cases.
7. Conclusion
The viable path for bringing an AI assistant into personal finance is not “giving the model more capabilities,” but “making the model explain things clearly within an extremely narrow permission boundary.” Read-only boundaries, field minimization, computation externalization, and controllable retention form the security foundation. If any one of these is missing, richer functionality only increases the risk exposure.
For engineering teams, real competitiveness lies in the rigor of this boundary design, not in how many data sources have been connected.
