The problem that started it
Every quarter, finance teams spend days doing the same thing: pulling numbers out of PDFs and spreadsheets, recomputing ratios by hand, writing the same analysis in three different formats for three different audiences. The tools that exist either make you do the work yourself, or trust a chatbot that confidently makes up the numbers.
That second problem is what Finance Copilot is built to solve.
The core insight
Language models hallucinate arithmetic. Ask GPT to total a column of revenue figures and it gives you a confident, wrong answer. Every AI finance tool that stuffs a spreadsheet into a prompt has this problem. The model reads a sample, estimates the rest, and presents it as fact.
Finance Copilot never asks the model to add anything.
Instead, it uses a trust loop:
- GPT-5 plans the visible workflow
- Codex writes a self-contained Python script to compute exact figures
- A guarded sandbox runs that script over every row of the real file
- The printed output, verified feeds back as ground truth
- GPT-5 reasons from those figures and writes the answer
The model is never the calculator. It is the analyst who reads the calculator output and explains what it means.
How I built it
The agent loop
The backend is FastAPI streaming Server-Sent Events. Each message triggers a five-phase agent: plan → Codex writes code → sandbox executes → verified figures → GPT-5 answers. The frontend renders each phase live so you see the work, not a spinner.
# The three lines that define Finance Copilot
code = ai_service.generate_analysis_code(request, context) # Codex
calc_output = run_analysis(code, context, data_files) # Sandbox
answer = ai_service.stream_answer(request, context, calc_output) # GPT-5
The sandbox
The generated Python runs in a subprocess with no network access, CPU and memory limits, and a wall-clock timeout. The real uploaded file is copied into the temp directory and exposed as FILES[0]. Generated code reads it with csv.DictReader, row by row. This is why exact math scales past 500,000 rows the sandbox reads the whole file, not a prompt excerpt.
Three model roles
- GPT-5 (
OPENAI_MODEL) plans, interprets, writes narrative, summarizes - Codex (
OPENAI_CODE_MODEL) writes the analysis script, called via/v1/responses - Realtime (
OPENAI_REALTIME_MODEL) powers voice over WebRTC with an ephemeral key
All three model names are environment variables. The app resolves whichever your key exposes.
Deliverables
One analysis, four renders: PDF (shareholder letter style, statement tables, chart), Word memo, Excel model, CSV. The expensive model calls happen once. Four deterministic renders cost nothing extra.
Voice that acts
The backend injects extracted financial data into the Realtime session instructions and declares a generate_document tool. Say "prepare a report for Germany" and the analyst filters the data, computes the exact subset, produces the file, and confirms out loud. The API key never reaches the browser.
What I learned
Codex needs the Responses endpoint. It returns a 404 on /v1/chat/completions pointing to /v1/responses. The SDK version most projects pin does not have .responses yet. I built a direct httpx fallback that detects the 404, switches endpoints, parses the response shape, and returns a shim.
Reasoning models truncate code. With max_output_tokens set too low, Codex spends the budget on reasoning and the code arrives cut off mid-line, producing a SyntaxError in the sandbox. Fix: set reasoning.effort to low, raise the cap to 16,000 tokens, and compile-check every script before it runs. If it fails, a robust fallback reads the full file and sums every numeric column.
Pre-computed digests beat prompt stuffing. I compute a complete analytical rollup at upload time every period, region, and segment, and place it at the front of every prompt. The model never misses a month on a multi-period file, even when the raw data exceeds the context window.
SSE streams can silently die. A 30-60 second generation step would hit the frontend idle timeout and wipe the conversation. Fix: emit a progress answer_delta before blocking work to keep the connection alive, and always reach answer_done regardless of what fails.
Challenges
- Getting Codex to run through three different failure modes before the Responses API fallback worked
- Voice transcript was empty for several runs, input transcription requires both a session-level config and a
session.updateover the data channel after connection - Parsing markdown pipe tables into real table structures consistently across PDF (ReportLab), DOCX (python-docx), and XLSX (openpyxl)
- Keeping the SSE stream alive during blocking generation without the frontend timing out
What Finance Copilot can do that a prompted LLM cannot
| Capability | Prompted LLM | Finance Copilot |
|---|---|---|
| Exact arithmetic on 500K+ rows | Samples and guesses | Codex reads the full file |
| Auditable figures | No source | Code block attached to every reply |
| Voice grounded in your documents | Generic answers | Ephemeral key, injected context |
| Four board-ready formats from one analysis | Manual export | One analysis, four renders |
| Scoped reports (e.g. Germany only) | Estimated | Filtered rows, exact subset figures |
Built With
- csv
- docker
- docker-compose
- fastapi
- framer-motion
- httpx
- jwt
- nginx
- openai-codex
- openai-gpt-5
- openai-realtime-api
- openpyxl
- pandas
- postgresql
- python
- python-docx
- react
- reportlab
- server-sent-events
- sqlalchemy
- tailwind-css
- vite
- webrtc


Log in or sign up for Devpost to join the conversation.