# AI Finance Assistant Architecture: Engineering Design and Production Requirements

    This article breaks down how to build an AI-powered personal finance assistant that turns manual reconciliation into natural language queries. It covers the three-layer separation of interaction, semantic, and deterministic computation; read-only authorization boundaries; the data pipeline from raw statements to a queryable semantic layer; four validation gates that keep natural-language-to-SQL generation safe; rule-first anomaly detection with the model limited to explanation; three hard constraints against hallucinated numbers; and a declarative approach to dashboards and scheduled reports. It closes with a three-phase rollout plan and the core takeaway: the hard part is not the model, it is the rigor of the boundaries.

    1. The Core Requirement: Turning “Manual Reconciliation” into “Natural Language Queries”

    The pain point in traditional personal finance is not that people “can’t read numbers.” It is four things: fragmented accounts, inconsistent definitions, domain jargon, and anomalies that are hard to spot. Users jump between multiple banks and payment apps, copy transactions into a spreadsheet by hand, and categorize them according to their own rules. When they want to ask a simple question at month end — “where did my money go?” — it can easily cost an hour or two.

    The value a large language model brings to this scenario is collapsing three steps — data retrieval, categorization, and explanation — into a single natural language interaction. But one thing must be clear: the model is not a calculation engine, and it is not an accounting system. What it is good at is understanding intent, translating vague questions into structured queries, and turning structured results into conclusions a human can read. Any actual numeric computation must be done by a deterministic engine.

    The division of labor across the whole solution should therefore be fixed at three layers:

    Layer Responsibility Allowed to “improvise”?
    Interaction layer Intent understanding, terminology explanation, presenting conclusions Yes, but constrained by evidence
    Semantic layer Metric definitions, unified calculation rules, field metadata No — must be fixed
    Computation layer Aggregation, sorting, period-over-period comparisons, anomaly determination No — purely deterministic execution

    This layering is the premise for everything that follows: any question about “how much” must have an answer that traces back to specific transaction rows.

    2. Permission Boundaries: Read-Only Is an Architectural Constraint, Not Marketing Copy

    For financial data, the authorization model has to be established before features are discussed. A sound implementation includes five constraints:

    1. Minimum authorization scope. Credentials or authorization tokens should request only “read account information” and “read transaction records” scopes, and no write scopes at all. Maintain a whitelist at the code level; any capability not on the whitelist is refused by default.
    1. ALLOWED_SCOPES = frozenset({
    2. "account.balance:read",
    3. "account.transaction:read",
    4. "account.holder:read",
    5. })
    6. def guard_call(requested_scopes: set[str], granted_scopes: set[str]) -> None:
    7. illegal = requested_scopes - ALLOWED_SCOPES
    8. if illegal:
    9. raise PermissionError(f"Rejected capability beyond read-only boundary: {illegal}")
    10. missing = requested_scopes - granted_scopes
    11. if missing:
    12. raise PermissionError(f"User has not authorized: {missing}")
    1. Short-lived tokens that can be revoked. Tokens carry an expiry, and users can unlink at any time. Once revoked, caches and intermediate results must be invalidated in sync.

    2. Sensitive data never lands anywhere it shouldn’t. Raw credentials, full card numbers, and ID numbers are not written to logs, do not enter the vector store, and do not enter the model context. The logging layer performs field-level redaction.

    3. Full-chain auditing. Every query records who asked what at what time, which statement was generated, how many rows were returned, and how long it took. This serves both debugging and compliance evidence.

    4. Explicit statement of functional boundaries. The system does not execute transfers, does not change account settings, and does not place orders for any financial product on the user’s behalf. This boundary should be declared twice: in the product copy and in the system prompt.

    3. Data Pipeline: From Raw Transactions to a Queryable Semantic Layer

    Raw data from open banking APIs or statement files usually can’t be used as-is. Typical dirty data includes: mixed amount units (major units vs. minor units), opposite sign conventions (expenses negative in one source, positive in another), timezone offsets causing transactions to be bucketed into the wrong month, merchant names stored as acquirer codes rather than readable names, and multiple spellings for the same merchant.

    The recommended order for cleaning and normalization:

    • Normalize base fields: amounts unified as integers in the smallest currency unit plus a currency code, timestamps unified to UTC while retaining the local timezone, transaction types turned into enums.
    • Merchant name normalization: first match with a rules dictionary (case folding, suffix removal, store number removal); anything unmatched goes through model-assisted clustering, and the results are written into a mapping table that a human can correct — rather than letting the model guess at query time.
    • Tag taxonomy: define a stable set of spending categories (housing, dining, transportation, subscriptions, medical, education, financial fees, and so on), and store the mapping in versioned form so historical statistics remain reproducible.
    • Recurring bill detection: cluster multiple records that share a merchant, a similar amount, and a stable interval into a subscription item, and output the expected next charge date.

    Once normalization is complete, it is best to build a wide table that carries the facts, then add a layer of metric views on top of it. Example query:

    1. -- Monthly category spend with month-over-month change
    2. WITH monthly AS (
    3. SELECT date_trunc('month', occurred_at) AS month,
    4. category,
    5. SUM(amount_minor) / 100.0 AS amount,
    6. COUNT(*) AS tx_count
    7. FROM txn_fact
    8. WHERE direction = 'OUT'
    9. AND occurred_at >= :from_date
    10. GROUP BY 1, 2
    11. )
    12. SELECT month, category, amount, tx_count,
    13. amount - LAG(amount) OVER (PARTITION BY category ORDER BY month) AS mom_delta
    14. FROM monthly
    15. ORDER BY month DESC, amount DESC;

    The key point: each metric is defined in exactly one place. Whether the user asks “how much did I spend this month” or “what is the dining spend trend over the last three months,” both should hit the same set of metric views. Otherwise the same underlying question can produce different numbers depending on how it was phrased.

    4. Natural Language to Query: Four Validation Gates Stop Bad Generation

    Turning natural language into SQL is the most error-prone step in this scenario. Engineering-wise, the right approach is a two-stage “generate + validate” pipeline, not direct execution of model output.

    (1) Trim the schema and add metadata. Expose only the necessary tables and fields. Put field comments, enum values, and example queries into metadata, and build a separate dictionary for user-facing business terms to support retrieval augmentation.

    (2) Validate against an output whitelist. Allow only a single read-only query statement. Reject multiple statements, reject comment-based bypasses, and reject access to tables outside the whitelist.

    1. FORBIDDEN = ("insert", "update", "delete", "drop", "alter",
    2. "create", "grant", "truncate", "copy")
    3. def validate_sql(sql: str, allowed_tables: set[str], row_limit: int = 500) -> str:
    4. stmt = sql.strip().rstrip(";")
    5. if ";" in stmt:
    6. raise ValueError("Only a single statement is allowed")
    7. lowered = stmt.lower()
    8. for kw in FORBIDDEN:
    9. if re.search(rf"\b{kw}\b", lowered):
    10. raise ValueError(f"Non read-only keyword detected: {kw}")
    11. if not re.match(r"^\s*(select|with)\b", lowered):
    12. raise ValueError("Only query statements are allowed")
    13. if not re.search(rf"\bfrom\s+({'|'.join(allowed_tables)})\b", lowered):
    14. raise ValueError("Access to an unauthorized table")
    15. return f"{stmt} LIMIT {row_limit}"

    (3) Constrain resources and results. Enforce a row limit, an execution timeout, and a scan time-range limit, so a single question cannot pull the entire transaction history.

    (4) Evidence drill-down. Any amount-related conclusion must carry the list of transaction detail IDs that support it. The UI provides a “view the evidence” entry point, and clicking it shows the raw transactions. This one mechanism solves both the trust problem and the correction-efficiency problem: if a number is off, the user can immediately locate which few transactions were miscategorized.

    Terminology explanations take a separate path: if the term hits the dictionary, return the definition and calculation formula directly; if not, search the local knowledge base; if there is still no result, state clearly that the term is “not covered.” Making up a financial definition from general knowledge is not allowed.

    5. Anomaly Detection: Rules First, Model as Backstop

    The value of abnormal-transaction detection lies in surfacing losses the user “cannot see.” Common patterns can be covered by deterministic rules first:

    • Duplicate charges: multiple charges from the same merchant for the same amount within a time window.
    • Hidden fees: small high-frequency debits, currency conversion fees on cross-border transactions, account maintenance fees, and insurance-type debits collected periodically.
    • Silent subscription price increases: the amount for the same subscription rises above a threshold relative to its historical median and stays there for two or more consecutive periods.
    • Spending structure imbalance: one category’s share jumps, or total spending exceeds total income.
    • Interest cost: estimate the interest expense for the current period from the annualized rate and the compounding cycle, and explain the difference between compound and simple interest.

    Thresholds should not be global fixed values. They should adapt to the user’s own historical distribution to keep false positives low:

    1. import statistics
    2. def detect_amount_spike(amounts: list[float], current: float,
    3. z_threshold: float = 3.0) -> bool:
    4. if len(amounts) < 6:
    5. return False
    6. med = statistics.median(amounts)
    7. mad = statistics.median([abs(a - med) for a in amounts]) or 1e-9
    8. # Robust Z-score, resistant to outliers
    9. robust_z = 0.6745 * (current - med) / mad
    10. return robust_z >= z_threshold

    The model’s role here is explanation and expression, not judgment. Once a rule fires, the model generates a natural language explanation of why something looks suspicious, which transactions are involved, and where the user should go to verify it, along with a confidence hint. Every anomaly conclusion must be labeled “requires manual verification against bank statements.”

    6. Three Hard Constraints for Suppressing Hallucination

    The tolerance for error in a financial scenario is extremely low — one wrong amount can cause a user real loss. Three constraints must be enforced:

    1. No self-computation. The model output must never contain amounts, interest rates, or balances that it derived on its own; every number comes from a query result set. If data is missing, answer “no relevant records” rather than estimating.
    2. No field guessing. When a dimension is missing (for example, a transaction has no merchant name), state the data gap explicitly instead of filling in a default value and continuing with the statistics.
    3. Reproducibility. The same question asked at different times must return the same result if the underlying data has not changed. The way to achieve this is to cache metric results keyed by a data version number, with the model layer handling only the wording.

    Pair this with an offline evaluation set: a batch of questions with labeled answers, covering five categories — multi-account aggregation, cross-month comparison, terminology explanation, anomaly detection, and refusal boundaries. Re-run the regression every time the prompt or the semantic layer changes, and track query accuracy, evidence hit rate, and inappropriate refusal rate.

    7. Visualization, Proactive Reports, and Editable Dashboards

    What users often want is not a paragraph of text but a chart they can modify. The recommended approach is to model the dashboard definition as declarative configuration: the model only generates or modifies the configuration, while rendering is handled by deterministic frontend components.

    1. dashboard: monthly_overview
    2. widgets:
    3. - type: bar_chart
    4. title: Category spend comparison
    5. query: category_spend_by_month
    6. params: { months: 6 }
    7. - type: category_pie
    8. title: This month's spending breakdown
    9. query: category_share
    10. params: { month: current }
    11. - type: bill_calendar
    12. title: Upcoming bills
    13. query: upcoming_recurring_bills
    14. params: { days_ahead: 30 }

    When a user says “change the time range to the past year” or “add a subscription spend trend chart,” it is really a matter of changing one parameter in this configuration or adding one node. That is both reliable and easy to roll back.

    For proactive service, scheduled tasks periodically generate weekly and monthly reports; after being decoupled through a message queue, they are delivered via notification channels. Failed tasks must be retryable and idempotent to avoid duplicate pushes.

    8. Common Cloud Capabilities and an Implementation Roadmap

    The runtime dependencies of a system like this are mostly standard component categories: raw statement files and export results go to object storage with lifecycle management; audit logs and query logs feed into a logging service configured with search and alerting; credentials and keys are managed by a key management service and never written into code or images; the terminology dictionary and knowledge documents use vector search; dashboard configuration reads and writes go to a relational database; asynchronous report tasks are driven by a message queue, while a container platform hosts the stateless inference gateway and query validation service. Monitoring and alerting cover query failure rate, the proportion of generated statements that get blocked, end-to-end latency, and model invocation cost. On mainstream cloud platforms such as Baidu AI Cloud, all of these map to standard product categories you can select directly; the actual choice should be evaluated against data residency requirements, cost per query, and operational capacity.

    The rollout is best done in three phases:

    Phase 1 (read-only Q&A): complete account linking, transaction ingestion, merchant normalization, metric views, and restricted query validation. Support four question types first — balance, transactions, category summaries, and terminology explanations — and verify that the authorization chain and evidence drill-down close the loop.

    Phase 2 (insights and visualization): add rule-based anomaly detection, subscription detection, declarative dashboards, and weekly report delivery, with the main focus on reducing false positives.

    Phase 3 (prediction and planning): on a stable foundation of metric definitions, introduce cash flow forecasting, bill reminders, and scenario simulation, while completing the evaluation set and compliance review.

    9. Conclusion

    When large language models enter personal finance, the real barrier is not model capability but how rigorously the boundaries are designed. Five rules — read-only permissions, a single source of truth for metric definitions, a query whitelist, drillable evidence, and conclusions that require verification — form the safety and trust foundation for this class of product. Give computation to the deterministic engine and understanding and expression to the model, and at month end a user only has to ask “where did my money go this month” to get an answer that is grounded, verifiable, and open to follow-up. That is the position this technology should occupy.