Inspiration
Small takeaway places lose money in two boring ways. The phone rings during the rush and nobody can pick it up, so the order goes somewhere else. Or someone orders by phone, never shows up, and the food goes in the bin.
AI phone agents already exist, but most of them stop at taking the order. Payment happens later, at the counter, or not at all. I wanted to see if an AI could take the order and get it paid through PayPal while the caller is still on the line, so that only paid orders ever reach the kitchen.
The harder question was trust. If an AI is going to take money from strangers over the phone, what stops it from charging the wrong amount, giving away a discount, or refunding someone who talks it into it? That question shaped the whole project.
What it does
You call the shop (in the browser, or through Twilio). The agent takes your order in plain speech, reads back the total, and sends a PayPal link. You approve it in PayPal, and the agent tells you it's paid before you hang up. The kitchen screen only shows paid orders.
If you've ordered before and chose to save your PayPal, the next order doesn't need a link at all. The agent reads back the exact total, you get a one-time code by SMS, you say yes, and it charges your saved PayPal under a spending cap you set yourself.
The owner gets a live console built on AG Grid with every order, every call transcript and every decision the system made, plus a copilot that answers questions about the day's money using only real ledger and PayPal data.
How I built it
The one rule everything follows is this. The LLM talks, code decides what's allowed, PayPal moves the money.
The model can only pick a tool and fill in typed arguments. It can't create a price, because prices come from a pure pricing engine working in integer cents. There's no discount tool for it to call. Every tool call goes through a guard that checks the order's state, the shop's policy and the quote, and every decision is logged with a reason.
A few details that matter for payments
- Every quote gets a SHA-256 fingerprint. The payment link can only be sent for the exact quote the caller heard read back. If the cart changes, the guard blocks the link and the agent re-quotes.
- A capture only counts as paid if PayPal says
COMPLETEDand the amount, currency andcustom_idmatch the quote. Anything else goes to pending, denied or needs-review. - Refunds can never exceed what's left on the capture, so for a capture of \( c \) with earlier refunds \( r_1, \dots, r_k \), a new refund \( r \) is only allowed when
$$ r \le c - \sum_{i=1}^{k} r_i $$
- Every money-moving PayPal call carries a deterministic
PayPal-Request-Id, so a retry can never double charge. - Webhooks are signature verified, stored once by event id, and processed by a worker. Order state changes use optimistic locking and a transactional outbox, which feeds Redis and the live screens over SSE.
- Spoken consent for a saved-wallet charge is stored as a record (who, which order, which amount, when). The guard loads it itself, so no part of the code can just claim consent happened.
PayPal pieces used are Orders v2 (including vaulting and merchant-initiated charges with a vault id), Payments v2 refunds, webhook verification, and Transaction Search for the copilot.
The stack is FastAPI, Pydantic, SQLAlchemy, Postgres, Redis and a background worker on the backend, with Next.js, Tailwind and AG Grid on the front end. It deploys to Render, traces go to Elastic through OpenTelemetry, and order events go out to Zapier.
Challenges
Being honest about what works. My first version looked finished but cut corners. The "AI" was keyword matching, some latency numbers were typed into a script instead of measured, and the demo used a fake approval button. I tore that out and rebuilt it properly with a real tool-calling loop, real PayPal sandbox flows and reports that only contain numbers the code actually produced. The README now has a limitations section, and every claim links to the test or report behind it.
Making the vault flow safe. Charging a saved wallet by voice is powerful and dangerous, because caller ID can be spoofed. That's why it needs an SMS code, a recorded yes to the exact amount, a per-order cap the customer set, and it only works at the one restaurant they saved it for.
Free-tier LLM limits. On a free tier the agent sometimes hits rate limits mid-call. Instead of failing, it retries and asks the caller to repeat, so the call stays alive.
Speech is messy. People mumble item names. Menu matching uses lexical and phonetic matching, and when confidence is low the agent asks instead of guessing.
What I learned
The interesting part of an AI payments product isn't the AI. It's the boundary around it. Once the model physically can't touch an amount, the safety questions become ordinary software questions you can test, like state machines, idempotency, reconciliation and races.
I also learned that a smaller thing that's real beats a bigger thing that's staged.
How it's tested
128 unit and flow tests, plus a harness of 148 scripted scenarios with 0 wrong charges (run against a mock gateway, which the report says clearly). It also includes an opt-in suite against the real PayPal sandbox, an LLM evaluation with simulated callers, a Postgres concurrency test, and an axe accessibility scan of every page. [CHECK: only mention the sandbox suite and LLM eval as "run" if their reports are committed. Otherwise say they're included and can be run.]
What's next
- Put it in front of a real restaurant owner with their own menu and customers.
- Push approvals instead of SMS codes for saved wallets.
- Catering orders with PayPal invoice deposits (the client methods exist, the flow doesn't yet).
- POS and kitchen printer integrations.
Built With
- ag-grid
- anthropic
- docker
- elastic
- fastapi
- groq
- next.js
- opentelemetry
- paypal
- paypal-orders-api
- paypal-vault
- postgresql
- postman
- pydantic
- python
- react
- redis
- render
- sqlalchemy
- tailwindcss
- twilio
- typescript
- web-speech-api
- zapier