Inspiration
Nishit: cabin's $320, 4 of us, $80 each 🏡
Anuj: I'm in
Ravi: in, but payday's Monday 😅
Manveer: who's booking?
(seen by 3)
Every group chat has this moment. Someone has to pay first, and that person either fronts the whole cabin or spends weeks chasing everyone with "hey, just a reminder…". If one friend drops out, the booking can't be refunded and the payer is stuck with it. So nobody pays first, and the plan quietly dies in the chat.
It's a coordination problem, not a money problem: everyone is happy to pay if everyone else does. PayPal already has the primitive for "if": an authorization, money held but not taken. We built the group around it.
What it does
Foldback turns "I'm in" into a promise backed by PayPal. The whole product is one rule:
At the deadline:
| Outcome | Condition |
|---|---|
| CAPTURE ALL | Every friend has approved ((H = n)) |
| VOID ALL | At least one friend has not approved ((H < n)) |
Note: 'H' is the number of successful PayPal authorizations and 'n' is the total number of friends. Nobody fronts the money, and nobody needs a refund.
- Paste the chat (or speak it). The AI reads the price, the people, the deadline and any extra time one person needs, and shows the exact line it used for each. Nothing is active until the organizer confirms.
- Each friend opens their own pay link and approves in PayPal. That places a real PayPal authorization: the money is held, not taken.
- The board shows everyone live: Waiting, Held, Kept or Released.
- If the whole group holds by the deadline, Foldback captures every hold. If not, it voids every hold, and nobody pays. One named person can get a grace period.
- An organizer assistant answers "What happens if Ravi doesn't pay?" by running the real rule on a copy of the plan, drafts reminders, and proposes clock moves. It never moves money: every action waits for the organizer's Approve.
How we built it
- Frontend: Next.js 16, React 19, Redux Toolkit, MUI. The live board is a Bryntum Scheduler fed by a server-side projection and updated over Socket.IO. Speak the plan uses the browser's speech recognition.
- Backend: Node 24, Express 5, Socket.IO, PostgreSQL 16, zod everywhere input crosses a boundary.
- PayPal (sandbox): Orders v2 to create and authorize, Payments v2 to capture, void and read authorizations. Every write carries a deterministic
PayPal-Request-Id. - AI: a model chain over plain
fetch(DeepSeek first, then Groq's gpt-oss-120b and Gemini Flash-Lite as fallbacks), picked by a bake-off on our own fixtures. Strict JSON output, validated by the server. - Hosting: the frontend on Vercel. The API runs in Docker on a Mac mini behind a Cloudflare Tunnel, with a pull-based deploy every 5 minutes, a smoke test and automatic rollback.
Challenges we ran into
The demo path was easy. The hard part was everything that happens when the network blinks at the wrong moment.
- Money that might have moved. A PayPal call can time out after PayPal has already acted. We record every call as
pendingbefore sending it, settle it in a second transaction, and treat any unclear reply as unknown: the plan pauses, and only a fresh read of PayPal clears it. We tested this against the real sandbox with a proxy that let PayPal act and then swallowed the reply. Authorize, capture and void each ended unknown, sent nothing more, and settled correctly. Resending the same key never created a second hold or capture. - Trusting an LLM with amounts. The model only proposes. The server checks that every amount and name appears in the pasted text, recomputes times from the stated time zone, and the organizer confirms the exact extraction before any share exists.
- Prompt injection from a group chat. Pasted text and tool results are treated as data, never instructions, and the assistant has no tool that writes to the database or calls PayPal.
Accomplishments that we're proud of
- "Capture only when the decision is keep" is enforced three times: in code, and by two Postgres triggers. Transition tables refuse any illegal status change.
- 531 backend tests on a real Postgres with a scripted PayPal, and 222 frontend tests.
- The assistant's "what if" answers come from the same pure
decide()function that moves the money, so a simulation can't drift from the real result. - A full sandbox pass of the failure cases (lost replies, timeouts, a refused boot on
PAYPAL_ENV=live), recorded indocs/spec/paypal.md.
What we learned
- Authorize-then-capture is the right primitive for group commitments: nobody fronts money, and nobody needs a refund.
- The scariest bug in payments isn't an error, it's a timeout. "I don't know" has to be a real state in the database, not an exception.
- Idempotency keys only help if they're derived from the object being acted on, not generated per attempt.
- An AI feature is much easier to trust when it shows its evidence and code makes the decisions.
What's next for Foldback
- Paying the organizer directly through PayPal's multiparty products, instead of a single sandbox merchant.
- A scheduled resolver and PayPal webhooks in place of the demo clock.
- Reauthorization for holds older than 3 days, and groups larger than four.
The goal stays the same: the next time someone types "who's booking?", the answer is "nobody has to".
Built With
- bryntum
- cloudflare
- deepseek
- docker
- express.js
- gemini
- groq
- jest
- material-ui
- next.js
- node.js
- paypal
- paypal-orders-api
- paypal-payments-api
- paypal-sandbox
- postgresql
- react
- redux-toolkit
- socket.io
- typescript
- vercel
- web-speech-api
- zod
