Inspiration
The brief was HackMTY’s “Reto UI Generativa,” set by Banorte and Tec de Monterrey: prove that a banking interface can be assembled live by an agent, not just replied to inside a chat bubble. The easy paths both fail the brief in obvious ways. Let a model free-compose raw primitives and layouts drift toward ugly and occasionally hallucinated. Hide behind fixed templates keyed to intent and it stops being generative at all — it’s a chatbot wearing a UI costume, and the trade-off notes for this project say so directly.
The harder middle was the actual goal: an agent that composes real banking screens by choosing and wiring together components from a closed, validated catalog — and that never gets to invent a peso figure, because it’s standing in for a bank.
What it does
Before a client types a word, the bank has already read them. Twelve months of transactions, card statements, and balances become a financial profile — cash flow, spend by category, card habits, liquidity, investment posture — and a ranked list of recommendations, each carrying the evidence that triggered it, its impact in pesos, and the tool that resolves it. No model call, no tokens spent.
Investing Ask something underspecified — “I have 80k pesos sitting idle, could leave it five years” — and if there’s no risk profile on file, the agent builds a four-question profiler instead of asking in plain text. The answers get scored by the bank, turned into a fund allocation, simulated with Monte Carlo, and rendered as an allocation donut plus a p10/p50/p90 projection. Dragging the contribution slider recalculates in place with a single data update, not a remount. “What if I were more conservative?” produces a side-by-side comparison screen nobody hard-coded as a template. Confirming an order is a real two-step flow — a token-gated ticket, then execution — and the surface itself turns into an account statement carrying a real folio.
Personal banking & payments The same agent and the same catalog cover accounts and cards (inline nicknaming, one-tap card freeze, spend-limit sliders, category budgets against real spend) and a payments domain built on real Mexican rails — CFE, Telmex, and water-utility bill pay with per-biller reference validation, SPEI transfers with live CLABE checksum validation, no-card cash withdrawal with a one-time code, and cash-deposit reconciliation. Everything shares one A2UI catalog: 31 components and 26 actions, guarded by one contract test that fails the moment the prompt, the validator, and the TypeScript types disagree.
How we built it
A React renderer applies A2UI messages to a Zustand store and draws domain charts with Recharts. It talks over SSE to a FastAPI gateway, which owns sessions, streaming, and the LLM-free dashboard. The gateway hands real turns to a hand-written agent loop over the raw anthropic SDK — no LangChain, prompt-cached with a single cut point so a new turn never invalidates the system prompt. The agent speaks to a standalone MCP server over stdio, a subprocess the gateway launches and tears down itself; that server exposes a services layer, the only surface the agent is allowed to touch, which sits on SQLite and a deterministic finance engine underneath.
Four decisions hold the rest together:
The model never does arithmetic. Every figure on screen traces back to a tool_result computed in the finance engine. Guardrails are code, not prompt. Suitability and source-of-funds checks live where the model can’t argue with them, and a pending order is re-checked at execution, not just at registration. The catalog is the single source of truth. a2ui/catalog.json generates the prompt fragment, the Pydantic validator, and the TypeScript types — never the other way around. Nothing moves money in one step. A confirmation token plus a unique idempotency key close the loop so a retried request can’t double-execute.
Challenges we ran into
Matching the real A2UI spec. An early build had actions as a flat {name, context} object; the actual v0.9 schema nests them under event and adds deleteSurface. Fixing it meant touching all eight action-emitting components, the validator, the prompt, and every recorded fixture at once. Guardrails that can’t be talked out of it. place_order checks suitability twice — once at registration, once at execution — because the client’s risk profile can lapse in the minutes between the two calls. Making leverage honest. Money borrowed at card rates has to lose against an arbitrage condition, not just a risk cap. With the cheapest credit product at 10.75% and the best instrument at 14.9%, almost every credit-funded trade gets blocked — and that’s the correct result, not a bug to explain away in the demo. Synthetic data with something to discover. Random per-category charges made every client look the same. Swapping in per-client habits, simulated event by event, meant a trait like “always pays the card late” had to be deduced by the profile engine from statements — never handed to it as a label — and a test checks the deduction, not the label.
Accomplishments that we're proud of
Three domains — investing, personal banking, payments — closed and rehearsed on one catalog and one agent, not three demos stitched together. A Monte Carlo seed derived from the call’s own arguments: dragging a slider back mid-demo returns the exact same numbers, every time. Money never moves in one step — a confirmation token and a database-level idempotency key close the loop across the component, the agent, and the bank, with a test that proves an invented token can’t execute. Real look-through risk: fifteen actual BMV issuers held inside funds, with concentration and volatility computed from what’s really inside a fund, not from what the client nominally bought.
What we learned
Interception beats orchestration here. render_surface has to be caught and validated between the model and the client, and no framework that resolves its own agent loop leaves room to insert that step. The ~300-line loop written directly on the SDK ended up being less code than fighting one. “Probability of losing money” isn’t one number. A client investing with a credit card behind them needs three — nominal, real net of inflation, and against the actual cost of that money — because only the third one is honest once there’s debt in the picture. A seed only teaches what it’s honest about. Plausible-looking random data made every synthetic client identical. Hand-authored habits, simulated in chronological order with real consequences, were what finally gave the profile engine something true to find.
What's next for Solemn Banking AI
The project’s own cut rule — a domain opens once the last one is closed and rehearsed, never in parallel — carries forward into the roadmap:
Phase 1 — close out service payments. Scheduled and recurring bill pay, autopay enrollment for recurring billers, and DiMo/CoDi/QR transfers, all explicitly out of scope until now. Phase 2 — credit as a full domain. Amortization and recalibration, debt consolidation, and card autopay. The schema already carries real credit accounts, so this is a services-and-catalog build, not a new database. Phase 3 — insurance. A policy catalog, coverage recommendations wired to the same financial profile, and two-step enrollment on the same confirmation-token pattern used everywhere else. Phase 4 — voice. Screen-reader accessibility and spoken confirmation on anything that moves money, plus a voice input/output channel onto the same agent loop — deliberately last, once the component catalog is stable across all three business domains.
Built With
- a2ui
- anthropic-claude
- fastapi
- model-context-protocol
- numpy
- pytest
- python
- react
- recharts
- sqlite
- typescript
- vite
Log in or sign up for Devpost to join the conversation.