About Splitq
For demo please refer to the https://www.youtube.com/watch?v=pkJy52VPeR0 with subtitles on
Inspiration
It's always the same scene. A big group gets together — dinner, drinks, a weekend trip — and one person ends up paying the whole bill at the counter. Then comes the annoying part: opening the group chat, doing the math in your head (or in the calculator app, twice, because you don't trust yourself), figuring out who had what, and sending a separate Tikkie to every single person. Half of them don't open it that night, two people pay the wrong amount, one person quietly disappears, and a week later you're still trying to remember whether Mo ever paid you back for the Uber.
The math itself is small but easy to get wrong: someone didn't have a drink, someone left early, someone covered the tip. By the time you've split a €184 bill across seven people with three sub-totals and a 10% service charge, you've already missed something. And nobody wants to be the one chasing friends for €6.50.
So we asked the obvious question: why is this still a form? We don't talk to our friends in forms — we talk in messages. The inspiration for Splitq was simple: build the thing we wished existed when the bill arrives. One person pays, you tell Splitq what happened in plain language (or just show it the receipt), and everyone else gets pinged with the right amount automatically. No more group-chat math, no more chasing, no more forgotten Tikkies.
What it does
Splitq is an agentic money chat sitting on top of a real bank (Bunq). You talk to it the way you'd text a group chat:
"split last night's pizza four ways with Mo, Senna and Alex" "send Bob €12 for the Uber" "who still owes me?"
The agent figures out who, how much, what category, where it happened, and whether to send or request. If both sides are connected to Bunq, settlements are real IBAN-to-IBAN payments — they show up in the Bunq mobile app as native push notifications, no extra wiring needed. If not, the debt is tracked in-app until it's settled.
On top of the chat, Splitq has:
- a live dashboard showing what's owed to you, what you owe, your real Bunq balance, and a 30-day social-spending budget
- computer vision for receipts — point the camera at a receipt, and the agent itemises it and assigns shares
- a full voice mode for hands-free use
- location-aware suggestions ("you're near the place you split dinner with Mo last week — want to do it again?")
- a goal watcher that gently warns you when you're about to blow past your social budget
How we built it
Architecture at a glance
┌──────────────┐ HTTPS ┌──────────────┐
│ Next.js │ ◀────────▶ │ FastAPI │
│ (App │ │ + SQLModel │
│ Router) │ │ on SQLite │
└──────┬───────┘ └──────┬───────┘
│ WebSocket (audio) │
│ │
▼ ▼
AWS Transcribe Claude Sonnet 4.6
AWS Polly Bunq SDK (sandbox + prod)
- Frontend: Next.js 15 (App Router) + React 19, TypeScript, custom CSS. No component library — every tile is hand-built so the dashboard feels like a product, not a template.
- Backend: FastAPI + SQLModel on SQLite, argon2 password hashing, an async lifespan worker that watches goals and emits in-app notifications.
- AI brain: Anthropic Claude
claude-sonnet-4-6with a tool-using prompt. The model gets the user's friends, recent splits, and balance as context, and emits structured tool calls forcreate_split,settle_debt,set_goal, etc. - Voice: AWS Transcribe Streaming over a WebSocket for real-time STT, AWS Polly for TTS replies. The same chat pipeline handles both typed and spoken input — voice is just another transport.
- Vision: receipt photos are sent to Claude's vision input; the model returns a structured JSON of line items + suggested shares, which the user can tweak before confirming.
- Bank: official
bunq_sdk, oneApiContextper user persisted as JSON on the user row. The wrapper restores thread-local SDK state before each call so the connection survives across requests.
Modeling the social budget
The goal watcher computes, for each user every minute:
spent₃₀ₐ(u) = Σ share(s, u) · 𝟙[ cat(s) ∈ Cᵤ ]
s ∈ S₃₀ₐ
┌ "over" if spent₃₀ₐ(u) / Bᵤ ≥ 1.0
level(u) = ┤ "warn" if spent₃₀ₐ(u) / Bᵤ ≥ 0.8
└ "ok" otherwise
where Bᵤ is the user's monthly cap, Cᵤ their chosen categories, and S₃₀ₐ the splits in the last 30 days. The indicator 𝟙[·] is 1 when the split's category is tracked, 0 otherwise. Notifications are throttled to at most one per 24h per level so the assistant feels helpful, not naggy.
Proximity recommendations
When the user opts in, the dashboard takes their current coordinates and ranks past hangout spots by haversine distance:
d(p₁, p₂) = 2R · arcsin( √( sin²(Δφ/2) + cos(φ₁)·cos(φ₂)·sin²(Δλ/2) ) )
with R = 6371 km, φ the latitude and λ the longitude of each point. Anything under ~300m surfaces as a "want to do this again?" suggestion.
What we learned
- Conversational UI is a UX problem first, an LLM problem second. The hard part wasn't getting Claude to call tools — it was deciding what should never need a confirmation, what must always need one, and how to recover when the user says "wait, no, the other Mo".
- Real bank integration is humbling. Bunq's SDK keeps state thread-local; one stray import and you're talking to the wrong user's account. We rewrote our service layer twice before context isolation felt safe.
- Streaming voice is mostly a websocket lifecycle problem. Transcribe Streaming wants a steady audio cadence; Safari and Chrome encode mic audio differently; users tap-to-stop mid-sentence. Half our voice work was just making the socket forgiving.
- Prompt caching matters. Sending the user's friends + recent activity as context on every turn was wasteful — moving the static portion into a cached prefix dropped per-turn token cost meaningfully and made replies feel snappier.
- Throttling is a feature. Our first version of the goal watcher fired a notification every minute the user was over budget. It was awful. The 24h-per-level throttle turned the same logic into something that feels like a thoughtful friend.
Challenges we ran into
- Schema drift on SQLite. We migrated the
splitstable four times during the hackathon (addingcategory, thenlocation, thenlat/lon, then akinddiscriminator). We ended up writing inlinePRAGMA+ALTER TABLEmigrations inapp/db.pyso a fresh clone always boots cleanly without us shipping Alembic. - Hung backend on port 8000. During one demo prep the uvicorn process accepted TCP connections but never responded — we had to learn to detect that case (
curl -m 4) instead of trusting "port is bound = service is up". - Settlement race conditions. Two users tapping "settle" on the same debt at the same time briefly produced double Bunq payments in sandbox. Fixed with an optimistic check on
Debt.statusinside the transaction. - Voice latency. First end-to-end voice round-trip was ~3.2s. We trimmed it to ~1.4s by overlapping Polly synthesis with the last token streaming from Claude rather than waiting for the full reply.
What's next
- A real mobile app — the chat interaction was designed thumb-first, but we're shipping the demo as a web app.
- Direct integration inside Bunq — instead of running Splitq as a separate app, ship it as a feature inside the Bunq mobile app itself, so the chat agent, receipt scanner and voice mode live one tap away from the user's normal banking flow with no extra signup, no separate balance, and no token plumbing.
- A "split predictor" — Claude already knows the patterns; one tap to pre-fill "the usual Friday with Mo, Senna and Alex".
Built With
- anthropic-claude-api-(claude-sonnet-4-6)
- argon2-cffi
- aws-polly
- aws-transcribe-(streaming)
- bunq-production-api
- bunq-sandbox-api
- bunq-sdk
- custom-css
- eslint
- fastapi
- geolocation-api
- git
- github
- httpx
- localstorage
- mediarecorder-api
- next.js-15-(app-router)
- node.js
- npm
- pydantic-v2
- python
- python-3.11
- react-19
- sql
- sqlite
- sqlmodel
- typescript
- uvicorn
- venv
- websocket

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