Inspiration
Freelancers and small studios run two systems that never talk to each other: a project plan and their invoices. When a client pays late, the plan doesn't know. The freelancer keeps working on the next phase, unpaid, and only notices weeks later. When money does come in, they have to remember to start the next phase, pay the subcontractor who helped, and update the client.
I wanted one place where the schedule reacts to money on its own, and where an assistant does the chasing but never acts without asking.
What it does
- Contract to plan. Paste a contract, or upload a PDF or a photo of one. The AI model reads it and pulls out the phases, the payment milestones and any subcontractor payouts. Dates and amounts are worked out in code, not by the model, and you check the plan before saving it.
- A Gantt chart that follows the money. Each payment milestone becomes a PayPal invoice. Phases that wait for a payment start on hold (dashed bars). When the client pays, PayPal sends a webhook, the invoice is marked paid, and the next phase unlocks live in the browser.
- A payments agent that asks first. Every few hours, after every save, and after every PayPal webhook, the agent syncs invoices and payouts from PayPal, works out what needs doing, and drafts suggestions:
- send an invoice that's coming due
- remind a client about a late invoice, using PayPal's own reminder email, with a drafted note
- split a late invoice, so the client can pay part now and unblock the next phase
- move a phase that's stuck waiting on money, along with everything after it
- pay a subcontractor through PayPal Payouts once the client has paid for their phase
- hold work when a client opens a PayPal dispute on a payment
Each suggestion shows why, the numbers, and the exact message. You can edit it, approve it or dismiss it. Nothing reaches your client or PayPal until you click.
- Replans you can see. When a phase moves, the Gantt keeps the original dates as a dotted ghost bar, so the slip is obvious.
- What actually reached you. For invoices paid through PayPal, Milestone reads the payment behind the invoice and shows the PayPal fee and the amount you received.
How we built it
- Frontend: React 19, Vite, Tailwind CSS 4 and TanStack Query, with the Bryntum Gantt for the timeline. The Gantt shows dependencies between phases, a "today" line, payment labels on each bar, on-hold phases as dashed bars, and baselines for phases that moved. Updates arrive over server-sent events, so a PayPal payment changes the chart without a reload.
- Backend: Hono on Node 22, Drizzle ORM and Postgres on Neon, deployed on Render.
- PayPal:
- Invoicing API (REST): create, send, remind, cancel, and record payments.
- Webhooks: invoice, payout and dispute events, each verified with PayPal's signature check before anything happens.
- Payouts API: paying subcontractors.
- Disputes: a dispute event is matched back to the invoice through its transaction IDs.
- PayPal Server SDK (
@paypal/paypal-server-sdk, Payments): reads each capture to get the PayPal fee and net amount. - PayPal Agent Toolkit: gives the AI agent read-only tools to look up invoices and disputes.
- AI: Vercel AI SDK with Ollama Cloud running gemma4:31b.
- One call turns a contract into a structured plan.
- The agent uses tool calling: it gets a summary of the project, can look things up in PayPal, and must answer through a
proposetool. Its drafts are checked in code (allowed actions, percent and day ranges) before they're shown. - If the model fails, the agent falls back to plain written templates, so suggestions still appear.
- APIMatic ContextMatic: I used APIMatic's Context Plugin in Claude Code while adding the PayPal Server SDK. It gave the right SDK calls, models and errors, for example that a one-step card order needs a
PayPal-Request-Id.
Challenges we ran into
- Bryntum and React re-renders. The React wrapper re-applies any prop whose object reference changes. A new
subGridConfigsobject on a screen-size change rebuilt the grid and crashed the page with "Id already in use". The fix was to keep every object prop stable and change the grid width on the instance directly. - Sandbox limits from India. Payouts and card payments are blocked for an India business account, and an India buyer can't pay an India seller. Partial payments on invoices aren't available either, so "split payment" cancels the invoice and sends two new ones. Real client payments in the demo use a US sandbox buyer.
- Keeping the AI in its lane. The model can draft and look things up, but every action goes through code that checks it and through a human click. Approvals are claimed in the database first, so a double click can't send two reminders.
- Dates. Exclusive end dates, local time zones vs UTC, and "days late" had to match between server and browser. One mismatch showed 11 days late in one place and 12 in another.
Accomplishments that we're proud of
- A real payment made by a sandbox buyer on PayPal's invoice page unlocks the next phase on the Gantt live, a few seconds after PayPal's webhook arrives.
- The agent's drafts read like something a freelancer would actually send, and you can edit every one before it goes out.
- The whole thing runs on free tiers (Render, Neon, Ollama Cloud, PayPal sandbox).
What we learned
- PayPal's invoicing, payouts and disputes fit together well around one record per payment. The webhook flow with signature checks is simple once set up.
- "Suggest, then approve" is a much better shape for a payments agent than full autonomy. It's also easier to trust and to test.
- Smaller open models handle tool calling well if the tools are narrow and every output is checked in code.
What's next for Milestone
- Client-facing project page, so the client sees the timeline and pays from it.
- Using real PayPal partial payments where supported, instead of splitting invoices.
- More currencies and tax handling.
- Live PayPal mode for real freelancers.
Built With
- apimatic
- bryntum-gantt
- drizzle-orm
- gemma
- hono
- neon
- node.js
- ollama
- paypal-agent-toolkit
- paypal-disputes-api
- paypal-invoicing-api
- paypal-payouts-api
- paypal-server-sdk
- paypal-webhooks
- postgresql
- react
- render
- server-sent-events
- tailwindcss
- tanstack-query
- typescript
- vercel-ai-sdk
- vite
Log in or sign up for Devpost to join the conversation.