## Inspiration
Every freelancer we know has the same awkward ritual. The work is delivered, the invoice goes out, and then… nothing. Days pass. You check your inbox, then your PayPal balance. You draft a "just following up!" email three times, worried that it sounds needy or that it sounds rude. When the money finally arrives, there's a second chore: working out what you owe the designer or copywriter who helped, then sending each of them their share by hand.
Plenty of tools show you overdue invoices. We wanted something that does the follow-up for you: an agent that understands which clients are likely to pay late, writes the right message for each one, sends it through PayPal, and pays your collaborators the moment the client pays. It should do all of that while staying under your control.
The hackathon's "AI agents that actually transact" theme fit perfectly. We wanted AI that doesn't just recommend an action, but takes it, with a human in the loop where it matters.
## What it does
PayPilot is an AI collections and payout copilot for freelancers and small studios that bill through PayPal invoices.
- Triage: one click and the agent syncs every open invoice from PayPal and scores each client's late-payment risk.
- Tone-matched reminders: the AI drafts a gentle, friendly-firm or firm reminder and explains why that tone fits.
- Autopilot rules + approval queue: low-risk reminders go out automatically under rules you set. Everything else waits on a card for you to edit and approve.
- Real PayPal actions: reminders go out through PayPal's own reminder system, invoices are created and sent through PayPal Invoicing, and collaborator splits are paid through PayPal Payouts.
- Automatic split payouts: when PayPal reports a payment, PayPilot confirms it, splits the amount received and pays everyone their share.
- Copilot chat: "Which clients are most likely to pay late?" or "Invoice Brightline \$1,200, due in 15 days, split 30% to Maya."
- Full audit trail: every action by the agent, your rules, you or PayPal is logged with its reason.
## How we built it
Stack:
- App: Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS and shadcn/ui
- AI: Google Gemini (free tier) through the Vercel AI SDK, with Claude as a drop-in option
- PayPal: Invoicing v2, Payouts v1, Webhooks with signature verification, and the PayPal Agent Toolkit for the copilot's read-only invoice tools
- Data: Postgres on Neon with Drizzle ORM
- UI extras: AG Grid Community for the invoice risk grid, Recharts for cash flow
- Email: Resend
- Hosting: Render
Numbers from code, words from the model
Our main design rule was that the AI never decides money or risk on its own. Anything that should be predictable and auditable is deterministic code. The language model does what it's good at: explaining, and writing in the right voice.
The risk score R (from 0 to 100) is a weighted sum of capped, normalised features:
R = round( 35 * (min(max(d, 0), 30) / 30) + 25 * (min(l_bar, 20) / 20) + 20 * p + 10 * (min(i, 3) / 3) + 5 * min(max(a - 1, 0), 1) + 5 * (min(r, 2) / 2) )
Where:
- d = days overdue
- l_bar = the client's average days late
- p = the share of their invoices paid late
- i = reminders they've ignored
- a = this invoice's amount relative to their usual
- r = reminders already sent on this invoice The tone comes straight from the score band:
$$ \text{tone}(R) = \begin{cases} \text{gentle} & R < 40 \ \text{friendly-firm} & 40 \le R < 70 \ \text{firm} & R \ge 70 \end{cases} $$
Each term becomes a human-readable factor ("Overdue 20 days, +23.3"). The model receives these factors and turns them into a one-sentence explanation and the reminder itself. Because the score and tone are deterministic, the seeded demo always plays out the same way: one gentle reminder auto-sends and three wait for approval. We locked that in with a unit test.
The policy gate
Every agent action passes through one function that decides send now or ask the human. Checks run in a fixed order (action type → tone → amount → risk), and the first rule that fails becomes the reason printed on the approval card, e.g. "$650.00 is over your $500.00 auto-send limit". Chat goes through the same gate, so the copilot can't sneak a send past your rules.
Splitting money safely
For an invoice that received A cents, split across collaborators with percentages p_1, ..., p_n where the sum of p_k <= 100, collaborator k receives their calculated share (s_k), and the remainder (A - sum(s_k)) stays with the freelancer.
$$ s_k = \left\lfloor \frac{A \cdot p_k}{100} \right\rfloor, \qquad \sum_{k=1}^{n} s_k ;\le; A, $$
Money is stored as integer cents everywhere and converted to PayPal's decimal strings in exactly one function. A is the amount PayPal confirms was paid, not the amount on the invoice.
Challenges we ran into
- PayPal ignores custom reminder text. We planned to send the AI's message through PayPal's /remind endpoint, then discovered that PayPal always uses its own wording there. The fix has three parts: write the AI message onto the invoice's note to the client, trigger PayPal's official reminder (which links to that invoice), and email the same message with the pay link.
- Never paying anyone twice. Webhooks retry, and our polling backup can spot a payment at the same moment the webhook does. We made payouts idempotent: payout rows are inserted before calling PayPal, a unique (invoice, collaborator) constraint means only the first caller gets rows back, and each PayPal batch has a deterministic sender ID. Approvals use an atomic queued → executing claim, so a double click sends once, and a partial unique index allows at most one open action per invoice.
- Webhook signature verification. PayPal checks the event exactly as it sent it, so re-serialising the JSON can fail verification. We build the verification request by string concatenation around the raw request body.
- Not trusting the webhook. Our code review pointed out that we were paying splits on the invoiced amount as soon as the webhook arrived. We now re-fetch the invoice from PayPal, require a paid status, and split only what was actually received.
- Free-tier realities. Render's free tier sleeps, so webhooks can arrive late. We added a throttled polling sync that runs while someone is looking at the app. We also moved the default AI provider to Gemini's free tier so the whole stack costs nothing to run.
- Deadline pressure. We set a strict cut order (client page, then charts, then extras) and protected the core loop above everything else.
What we learned
- Agents need guardrails more than prompts. The most valuable parts of PayPilot aren't the prompts. They're the policy gate, the idempotency keys and the audit log. Those are what make it reasonable to let an AI move money.
- Let code own the numbers. Splitting the work between deterministic scoring and generative language made the agent explainable, testable and stable on camera.
- Read the API's fine print early. The reminder-text and webhook-verification surprises would have hurt if we'd found them on demo day, so we front-loaded a sandbox smoke test.
- Design for failure. Timeouts, retries, template fallbacks when the AI is unavailable, and an action that recovers if it gets stuck mid-send. In payments, the unhappy paths are the product.
What's next
- Real multi-user accounts with PayPal sign-in, instead of a shared demo workspace
- Learning each client's payment behaviour from real PayPal transaction history instead of demo data
- Multi-currency invoices and fee-aware splits
- Smarter timing, so reminders go out when each client is most likely to act
- Optional Pay Later and payment-plan offers for clients who are struggling to pay
Built With
- ag-grid
- drizzle-orm
- google-gemini
- neon
- next.js
- node.js
- paypal
- paypal-agent-toolkit
- paypal-payouts
- paypal-sandbox
- paypal-webhooks
- postgresql
- react
- recharts
- render
- resend
- shadcn-ui
- tailwindcss
- typescript
- vercel-ai-sdk
- vitest
- zod