AI Financial Planning Assistant in Mobile Applications
A financial AI assistant in mobile apps works with sensitive data and must provide practical advice without becoming a manipulation tool. Technical challenge: aggregate data from multiple sources (bank transactions, manual entry, OpenBanking APIs), build a spending picture, and provide specific recommendations.
Financial Data Sources
Three data layers:
Open Banking / PFM API: Plaid (US/Europe), Salt Edge (CIS and Europe), Tinkoff API for Russia. Return transactions with categories, account balances, 12+ month history. Require OAuth authorization.
// iOS - initiate Plaid Link
import LinkKit
func openPlaidLink() {
var config = LinkTokenConfiguration(token: plaidLinkToken) { result in
switch result {
case .success(let success):
self.exchangePublicToken(success.publicToken)
case .failure(let error):
print("Plaid error: \(error.localizedDescription)")
}
}
let result = Plaid.create(config)
switch result {
case .success(let handler):
handler.open(presentUsing: .viewController(self))
case .failure:
break
}
}
Apple Pay / Google Pay transactions via PassKit / Google Wallet API—limited access, only for apps with appropriate permissions.
Manual entry—always needed as fallback, with AI auto-fill of category and amount via NLTagger / receipt camera recognition.
Transaction Categorization
Banks provide their own categories—heterogeneous and incomplete. Build consistent classification for all sources.
func categorizeTransaction(_ transaction: RawTransaction) async throws -> Category {
// First try rules (fast, free)
if let ruleCategory = ruleBasedCategorizer.categorize(transaction) {
return ruleCategory
}
// Then AI for non-standard cases
let prompt = """
Categorize this transaction into ONE category.
Categories: food_groceries, food_restaurants, transport, housing, utilities, entertainment, health, education, shopping, travel, income, transfer, other
Transaction: "\(transaction.merchantName)", amount: \(transaction.amount) \(transaction.currency)
MCC code: \(transaction.mccCode ?? "unknown")
Return only the category name, nothing else.
"""
let category = try await openAI.complete(prompt: prompt, maxTokens: 10)
return Category(rawValue: category.trimmingCharacters(in: .whitespacesAndNewlines)) ?? .other
}
Rules first—70–80% of transactions by patterns (MCC codes, known networks). AI for the rest.
Budget Analysis and Recommendations
Expense analysis is deterministic code, not AI. AI is for interpretation and recommendations.
struct FinancialSnapshot {
let monthlyIncome: Decimal
let expensesByCategory: [Category: Decimal]
let savingsRate: Double // %
let recurringExpenses: [RecurringExpense]
let unusualExpenses: [Transaction] // above category average
}
func generateInsight(snapshot: FinancialSnapshot) async throws -> String {
let expenseSummary = snapshot.expensesByCategory
.sorted { $0.value > $1.value }
.prefix(5)
.map { "\($0.key.displayName): \($0.value.formatted(.currency(code: "RUB")))" }
.joined(separator: "\n")
let prompt = """
Financial data for this month:
Income: \(snapshot.monthlyIncome.formatted(.currency(code: "RUB")))
Savings rate: \(String(format: "%.1f", snapshot.savingsRate))%
Top expenses:
\(expenseSummary)
Unusual this month: \(snapshot.unusualExpenses.map { $0.description }.prefix(3).joined(separator: ", "))
Give 2-3 specific, actionable insights. Be direct. No generic advice.
Example of good insight: "Café spending increased 40% vs last month—18 transactions instead of 12."
"""
return try await openAI.complete(prompt: prompt, maxTokens: 200)
}
"No generic advice" in the prompt is critical. Without it, the model gives "reduce food spending" instead of specific observations.
Forecasting and Goals
// Android - calculate goal timeline
data class SavingsGoal(
val name: String,
val targetAmount: BigDecimal,
val savedAmount: BigDecimal,
val monthlyContribution: BigDecimal
)
fun calculateGoalTimeline(goal: SavingsGoal): GoalTimeline {
val remaining = goal.targetAmount - goal.savedAmount
if (goal.monthlyContribution <= BigDecimal.ZERO) {
return GoalTimeline.Unachievable
}
val months = (remaining / goal.monthlyContribution).toLong()
val achieveDate = LocalDate.now().plusMonths(months)
return GoalTimeline.Achievable(achieveDate, months)
}
AI connects when recommending savings increases: find categories with the most reduction potential from user history.
Security and Privacy
Never send financial data to LLMs in raw form. Before sending:
- Round amounts to orders of magnitude (not 47,839 rubles, but ~48,000)
- Hash or replace store names with category
- Never send account numbers, details, names
On iOS, DataProtection.complete for local transaction storage: file encrypted with a key inaccessible while device is locked. On Android, EncryptedSharedPreferences + EncryptedFile from security-crypto.
Server-side: all LLM requests logged without user identifiers (session hash only), vector store data—public regulations, no personal data.
Timeline Estimates
Basic analysis with manual transaction entry + AI insights—1 week. Full implementation with Open Banking (Plaid/Salt Edge), auto-categorization, goals, and forecasts—6–10 weeks (much is integrations and certification).







