All projects
FintechLLM AgentsTool-callingNext.js

Exponencial Life — Personal Finance App with a Multi-Provider AI Agent

A personal finance app for Colombian users with a conversational AI agent that reads and modifies the user's financial data (transactions, debts, investments, goals) via tool-calling, built on an abstraction layer that supports 4 interchangeable LLM providers.

In progress Demo

Problem

An educational personal-finance app aimed at novice Colombian users (25-40 years old), built around the value proposition "see how today's financial decisions shape your net worth in 5, 10, and 20 years" (documented in exponencial-life-plan.md). It covers income/expenses, debts, investments (CDTs, FICs), savings goals, and "cadenas" (a rotating collaborative savings pool, an instrument specific to the Colombian context).

The most relevant piece of engineering is the AI Financial Advisor: a chat that doesn't just answer questions about the user's finances, but can act on their data (create a transaction, update a debt, etc.) via function calling.

Try it live: exponential-life.polluxai.net

Approach

  • Full financial suite on Firestore: wallet/transactions, debts, investments, goals, cadenas, and an aggregated net-worth view — each collection with its own CRUD service (src/lib/services/*Service.ts) scoped by userId.
  • Conversational agent with real tool-calling, not a read-only chatbot: the model has access to 6 groups of tools (src/lib/ai/tools/) that let it list/create/update/delete transactions, debts, goals, investments, recurring templates, and query cadenas.
  • Multi-provider LLM abstraction (src/lib/services/llm/): a single AI_PROVIDER env var switches between LM Studio (local), OpenAI, Claude, or DeepSeek without touching the frontend or the orchestrator — explicitly documented in docs/AI_ARCHITECTURE.md as the answer to the 30-90s cold start of running a local model.
  • Cost/usage governance built server-side ahead of scale: a hard monthly budget, daily per-user rate limiting, and token/cost logging per conversation, all persisted via the Firebase Admin SDK.
  • Guided onboarding that captures initial income/expenses/debts/investments/goals and persists them directly into each feature's real collections.

Architecture

Browser
  │  FinancialAdvisorChat.tsx
  │  POST /api/ai/chat
  ▼
route.ts (src/app/api/ai/chat/route.ts)
  ├─ isOverBudgetServer()               → Firestore: ai_usage_summary/{YYYY-MM}
  ├─ checkAndIncrementRateLimitServer() → Firestore: ai_rate_limits/{userId}
  └─ getProvider()  ← env AI_PROVIDER (lmstudio | openai | claude | deepseek)
        │
        ▼
  runAgentLoop()  (src/lib/ai/agentOrchestrator.ts)
    loop, max 8 iterations:
      ├─ provider.chatComplete()  → Claude /v1/messages · OpenAI/DeepSeek/LM Studio /v1/chat/completions
      │     (claude.ts translates bidirectionally: separate system prompt,
      │      SSE content_block_delta → delta.content, tool_use ↔ tool_calls)
      │
      ├─ tool_calls? → dispatchTool() (toolRegistry.ts)
      │        → tools/{transactions,debts,investments,goals,recurring,cadenas}.ts
      │        → *Service.ts → Firestore (scoped by userId), executed in parallel (Promise.all)
      │        → results are re-injected as "tool" messages and the loop continues
      │
      └─ stop? → text streamed as SSE to the client
               → usage log (tokens, cost) → Firestore, fire-and-forget

Financial context (before the conversation starts):
  buildFinancialContext(userId) → 5 parallel Firestore queries
  (transactions, debts, investments, goals, cadenas)
  → promptBuilder.ts assembles the snapshot as text (~1.5-2.5k tokens)

Key engineering decisions

  1. Agentive tool-calling over RAG. Instead of embeddings/vector search, the model receives a structured, live snapshot of the financial state (financialContext.ts) plus a registry of tools it can invoke to read and write. The loop is bounded (MAX_ITERATIONS = 8) to avoid infinite loops, and multiple tool calls run in parallel. Trade-off: more orchestration complexity than a simple read-only RAG, in exchange for an assistant that can genuinely "do things" (create a debt, log an expense) mid-conversation.

  2. Provider abstraction layer normalized to the OpenAI format. OpenAI/DeepSeek/LM Studio are drop-in because they share the same wire format; Claude requires a non-trivial bidirectional translation (claude.ts, ~260 lines): lifting system to a top-level field, remapping SSE events (content_block_deltadelta.content), and converting tool_use/tool_result to/from tool_calls/role:"tool" messages. The rest of the app (frontend, orchestrator) never knows which provider is active. This decision is explicitly documented in docs/AI_ARCHITECTURE.md as the fix for local-model cold starts — switching providers is a single env var edit.

  3. Fail-open cost governance. A hard monthly budget (AI_MONTHLY_BUDGET, default $5) returns a 503 when exceeded, and a daily rate limit of 20 messages per user, both checked against Firestore before the LLM is ever called. If the budget or rate-limit check itself fails (e.g. Firestore doesn't respond), the code explicitly lets the request through (catch { /* allow */ }) — prioritizing availability over strict cost control, consistent with a tightly budgeted project that would rather not break over a secondary infrastructure failure.

  4. Simple ownership-based Firestore rules, except for cadenas (the only genuinely multi-user feature): there, the model switches to membership-based reads (request.auth.uid in resource.data.participants) and creator-only writes, reflecting that it's the one piece of data shared across users in an otherwise single-tenant-per-document app.

Results

| Metric | Value | | --- | --- | | Test coverage | — no *.test.* / *.spec.* files found in the repo | | CI/CD | — no .github/workflows folder or visible CI config | | Latency / TTFB in production | — docs/AI_ARCHITECTURE.md gives estimates (LM Studio 30-90s cold, cloud under 2s) but no measured metrics in logs | | Users / traffic | — pending | | Real AI cost (vs. the $5/month budget) | — pending |

What I'd do next

  • No test suite. For an AI/Backend portfolio project, this is the most visible gap: tool dispatch, the agent loop, and provider normalization (especially claude.ts) are natural candidates for unit/integration tests.
  • Silent fail-open on budget and rate limiting — if Firestore fails, the system has no idea it's no longer protected. Worth at least logging/alerting when the check itself fails, instead of swallowing the error.
  • FUNCIONALIDADES.md documents a scope-reduction plan: the repo itself notes that the "new project" will focus only on expense management, dropping the rest of the features. This suggests the current product (15 features) is seen as over-built for validation and is being deliberately trimmed down to a core (wallet).
  • dashboard/budget/page.tsx is an explicit placeholder ("Budget & Savings") with no functionality — a feature announced in the navigation but not implemented.
  • No visible deploy configuration (no vercel.json or pipeline) — production/deployment status can't be confirmed from the code alone.