Inspiration

Every group trip ends the same way: one person fronts the cabin, another covers groceries, someone leaves Saturday morning, and weeks later the group chat is still arguing about who owes what. Existing split apps assume a single payer and an equal split. Real life has partial attendance, items only some people joined, and rounding that never quite reconciles.

Meanwhile PayPal already moves money for millions of people, and AI can now reason over a messy plan. SplitMate is our answer: let AI handle the messy language and the fair-split reasoning, let PayPal handle the money, and keep the human in the loop for one approval click.

What it does

Paste a natural-language event plan — for example:

"Cabin weekend for 4: Alex, Sam, Priya and Jordan. Cabin $620 paid by Alex. Groceries $180 paid by Priya. Gas $95 paid by Priya. Firewood $40 paid by Jordan. Wine $90 paid by Alex, split between Alex and Priya only. Sam left Saturday morning, so he only shares the cabin and gas. Jordan skipped the wine."

SplitMate:

  • Parses it with AI into a strict JSON plan: participants, items, amounts, who paid, who shares each item, and the participation caveats. If the model is unavailable or returns invalid JSON, a deterministic rule-based parser takes over with a visible warning.
  • Proposes a fair itemised split with integer-cent, largest-remainder rounding, so every item sums exactly. Partial attendance narrows who shares an expense. Every person's net position is "paid minus share", and the reasoning is shown line by line and is fully editable.
  • Creates one real PayPal order per person who owes using the PayPal Orders v2 API in the sandbox (OAuth2 client credentials → create order → hosted checkout link → capture). Netting means a web of pairwise IOUs collapses into the smallest set of payments: in the demo, four people become three orders ($178.75 + $22.08 + $212.08 = $412.91) with one payer receiving a $412.91 credit.
  • Tracks every share on a dashboard: order created → approved → captured, with per-share Sync/Capture/Retry actions.
  • Drafts the awkward reminder — an AI-written nudge per participant (or a deterministic offline template). Nothing is ever sent automatically.

Mock mode is the default: it runs fully offline with a deterministic parser and a simulated PayPal checkout, so judges can try the entire flow with no accounts, no credentials and no network. Add PayPal sandbox credentials to .env and the same UI creates genuine Orders v2 orders and redirects to PayPal's sandbox checkout.

How we built it

Pure Python 3.9+ standard library: an http.server JSON API + static single-page app, no pip/npm dependencies and no build step. The UI is vanilla HTML/CSS/JS served by the app.

Architecture: app.py → HTTP server → service layer → plan parser, split engine, PayPal client. The PayPal client has two implementations of one interface — a real Orders v2 client (urllib, OAuth2 token cache, create/get/capture, normalised errors) and a deterministic offline mock that mirrors the same states. That is also how the tests stay honest: the same service code runs against both.

AI is used twice: (1) natural-language → validated JSON plan via an OpenAI-compatible Chat Completions endpoint, with strict schema validation, prompt-injection-resistant treatment of the pasted text, and a deterministic fallback; (2) AI-drafted payment reminders. The model never generates amounts or performs the split arithmetic — that is deterministic Python, so the numbers are always auditable.

The split engine allocates each expense in integer cents with the largest-remainder method (divmod, remainder cents in deterministic participant order), so sums always reconcile; partial attendance is modelled as the expense's shared_by set; and net positions (paid − share) decide who gets a PayPal order and who gets a credit.

Testing: 152 stdlib unittest tests plus a 56-assertion one-command smoke script (bash scripts/smoke.sh) that starts a fake PayPal + LLM server and proves both the offline E2E flow and the real-mode HTTP path — OAuth header, order payload shape, capture-before-approval 422 → 409 UX, sync, capture, token caching, keep-alive safety and secret redaction.

Challenges

  • "Fair" is much harder than total divided by N. Partial attendance, a payer covering something they do not share, and cent rounding all broke naive approaches; integer-cent allocation plus netting made it provable and explainable.
  • Trusting an LLM with money-adjacent output. The model's JSON is never executed as truth — it is schema-validated, normalised, capped, and every failure path degrades to a deterministic parser with a visible warning.
  • Building a real PayPal integration without live credentials in the build environment. We encoded the request/response shapes from PayPal's published Orders v2 OpenAPI schema and auth docs, then wrote a local fake PayPal server mirroring them so the real client is exercised over HTTP — including capture-before-approval.
  • Making the demo bulletproof: zero dependencies, offline mode, deterministic sample plan, one-command smoke proof.

Accomplishments

A complete product loop, not a mockup: natural language → validated plan → editable fair split → real PayPal sandbox orders → hosted checkout links → status sync → capture → AI reminder. 152 tests and a 56-assertion smoke script prove both modes. The split engine always reconciles to the cent and explains every line.

What we learned

  • PayPal Orders v2 is a clean REST API, and its HATEOAS links are the real contract — the approve URL comes from the create-order response, and the captured payment lives under purchase_units[].payments.captures[].
  • API schemas evolve: application_context is deprecated in favour of payment_source.paypal.experience_context. We kept the compatible flow as default and added an opt-in flag for the newer placement, documenting the trade-off instead of ignoring it.
  • AI is best used as a constrained structured-extraction layer, not as an oracle. JSON mode + validation + deterministic fallback gives LLM flexibility with script-like reliability.
  • Determinism is a feature in demos: fixed sample plan, fixed mock ids, exact integer cents — the video, docs and tests all agree to the cent.

What's next

PayPal Payouts to automatically reimburse people who fronted money; a hosted demo URL; webhook-driven status updates instead of manual sync; persistent storage with explicit PII boundaries; percentage/weighted splits, refunds and partial captures; richer reminder automation. Sandbox-only today — production would need refunds, disputes, webhooks and hardening.

Tools

We used an OpenAI-compatible Chat Completions API with the gpt-4o-mini model as the AI tool (configurable via LLM_MODEL). It is used in two ways:

  1. Plan parsing: the pasted natural-language plan is sent as data (not instructions) to the model with a strict JSON schema; the model extracts participants, expenses, amounts, payers, per-item sharers and participation notes. The response is schema-validated in Python before use; malformed/incomplete responses fall back to a deterministic rule-based parser.
  2. Reminder drafting: the model writes a short, polite payment reminder for a selected participant based on the event and their share.

The model never computes the split arithmetic and never moves money — amounts are integer cents in Python, and all PayPal actions are explicit user clicks. In offline mock mode (shown in the demo video), a deterministic rule-based parser and a templated reminder replace the LLM, so the demo runs with zero network and zero accounts.

PayPal technology used: PayPal Developer platform — Orders v2 REST API and OAuth 2.0 client credentials, sandbox environment (create order, hosted checkout approve link, get order, capture order).

Try it out

git clone the repo and run python3 app.py — offline mock mode needs no accounts. Real PayPal sandbox mode: copy .env.example to .env, set PAYPAL_CLIENT_ID and PAYPAL_CLIENT_SECRET from a sandbox app at developer.paypal.com, optionally set LLM_API_KEY, then python3 app.py. Details and troubleshooting are in README.md / RUNBOOK.md in the repo.

Built With

Share this project:

Updates

Submission history