-
-
Live: the milestone plan Nemotron proposed from the brand-refresh brief, in the review editor before anything is sent
-
Live: the deposit invoice waits for approval, showing the exact PayPal calls (create_invoice + send_invoice)
-
Live PayPal sandbox: the verified INVOICING.INVOICE.PAID webhook unblocks the next milestone and moves delivery 7 days sooner
-
Simulator: an overdue invoice pushes downstream work +4 days and the agent drafts a reminder for approval
-
Plain-language ledger: "group by client" becomes AG Grid row grouping with group totals
Inspiration
Freelancers and small studios that bill by milestone keep two documents in sync by hand: the project plan and the invoices. The plan says "phase 2 starts once the deposit is in", but nothing enforces it. The plan does not know when PayPal gets paid, and when a client pays late, every downstream date is quietly wrong. We wanted a plan in which "invoice paid" is an actual dependency, and an agent that does the bookkeeping without ever touching money on its own.
What it does
- Brief to plan. Paste a brief or statement of work. NVIDIA Nemotron (on Nebius Token Factory) proposes a milestone plan: deliverables, working days, amounts, payment terms and gates (which payment unlocks which work). The plan is validated against a JSON schema and against the brief (a stated total must appear in the brief and equal the sum of the milestones; a client email that is not in the brief is refused), then opens in an editor. Nothing has been sent anywhere yet.
- The plan as a Gantt. Approve it and it becomes a Bryntum Gantt where each milestone is a work bar plus an invoice bar, joined by dependency lines. The "paid" link is dashed while the invoice is open.
- Approval before money. When a milestone becomes billable, the agent proposes an invoice. One approval covers PayPal
create_invoice+send_invoice, with exactly the amount, dates and recipient shown on the card. - PayPal moves the plan. PayPal sends
INVOICING.INVOICE.PAID. Paceline has PayPal verify the signature over the raw body, drops duplicates, re-reads the invoice and recomputes the schedule: the gated milestone unlocks (the Gantt highlights and scrolls to it), the ledger row flips to Paid, and the agent explains how many days the delivery date moved. - Overdue. When an invoice passes its due date unpaid, everything gated on it slides, the project shows "+N d vs plan", and the agent drafts a reminder for you to approve (sent through PayPal's remind endpoint).
- Ledger in plain language. The ledger is AG Grid. Type "overdue over $500", "group by client" or "unpaid, largest first" and the words become a validated query that drives AG Grid's filter model, row grouping and sort.
How we built it
- Stack. TypeScript end to end: React + Vite client, Node server, and one shared workflow engine that runs on the server and also entirely in the browser (
?mock=1). - PayPal. Invoicing v2 over REST against
api-m.sandbox.paypal.com(create, send, get, remind, cancel), with webhooks verified throughverify-webhook-signature. Every call was run against the real sandbox. We evaluated the PayPal Agent Toolkit and kept its tool names, but call REST directly: we needed host pinning, token expiry, structured errors, webhook verification, and an approval gate instead of handing write tools to a model. - Approval gate.
GatedPayPalis the only object that can call a PayPal write method. Each call needs an approved proposal id and reads amounts, dates and recipient from that proposal. A test scans the source to prove nothing else calls a write method. - AI.
nvidia/nemotron-3-super-120b-a12bthrough Nebius's OpenAI-compatible API. It proposes plans as a tool call, writes explanations and reminder drafts, and turns ledger questions into aLedgerIntent. It never sees a PayPal write tool. Tool calls are validated with Ajv and errors are fed back to the model (up to three attempts). - No-invention guard. Every amount, date, id, number, email and status word in model text must match facts from the approved plan and PayPal responses; otherwise one retry with the violations listed, then template text. The UI labels each explanation "Model · figures verified" or "Template".
- Bryntum Gantt. Work bar + invoice bar per milestone, payment-gated dependency lines, baselines against the approved plan, an unlock highlight, and theming from the app's design tokens in light and dark. The engine computes the dates and Bryntum renders them, so the chart and the engine cannot disagree.
- AG Grid 36. Theming API, custom cell renderers (status pills, due-date colouring, PayPal payment ids, row actions), a pinned totals row, and cell flash when a row flips to paid. Enterprise row grouping, set filters and tool panels are used where they earn their place.
- Ops. A hardened Docker container (read-only, non-root, capped memory) behind a Cloudflare tunnel, and 328 offline tests.
Challenges we ran into
- The real sandbox disagreed with our assumptions. Invoice dates are read in the merchant's time zone: in the US evening it is already tomorrow for PayPal, so
sendreturned 202 SCHEDULED and then remind and cancel failed with 422.webhook_idmust be alphanumeric. A client-credentials token is reused for about 9 hours, so a feature enabled afterwards does not appear in its scope. The invoicereferenceis visible to the recipient, so it must not carry anything private. Each one became a fix and a test. - The model serving stack renders JSON
nulltool arguments as the string "None". Every plan failed until we normalised those values, and only where the schema allows null. After that, 4 of 4 sample briefs produced a valid plan on the first call. - A public demo on one sandbox merchant account. If every visitor got the live sandbox, anyone could make it send invoices. Public visitors get a clearly labelled simulator that goes through the same webhook handler; only the owner's browser unlocks the live sandbox, through a pairing code so the token never appears on screen.
- Spend. A public "Propose a plan" button needs a cap. A metered fetch prices every model call; past the daily cap, or if the provider returns 402 or 5xx, the plan comes from a rule-based planner and the UI says so.
Accomplishments that we're proud of
- The full loop has run end to end on the public URL against the real PayPal sandbox: brief → Nemotron plan → approved invoice created and sent → payment → real webhook, signature verified by PayPal → next milestone unlocked.
- Nothing touches money or a client without an approval, and that rule is enforced in one place and tested.
- The whole live run used 3 model calls, about $0.003.
What we learned
- Treat the sandbox as the spec. Four of our assumptions about PayPal's behaviour were wrong, and we only found out by calling it.
- Here a model is most useful as a proposer, not an actor: plans stay proposals until a person approves them, and every figure in the model's prose is checked against real data.
What's next for Paceline
- A buyer payment on sandbox.paypal.com (status PAID rather than MARKED_AS_PAID) in the automated run.
- A polling fallback for when a webhook never arrives.
- Partial payments, taxes and discounts in the plan editor.
- A shared database so more than one instance can run.
Notes for judges
- Try it without keys: https://paceline.gotclass.xyz serves the simulator to visitors. Click "Load sample workspace", or pick a sample brief and "Propose a plan". "Pay as buyer" plays the PayPal buyer, and "+1 week" moves the clock so you can watch an invoice go overdue.
- The video is a real take against the live PayPal sandbox with the real Nemotron planner. The payment was recorded through the Invoicing API (
record-payment) rather than by a buyer logging in, and PayPal delivered the same signedINVOICING.INVOICE.PAIDwebhook. The video says so. - Run it locally:
npm ci && npm run dev(Node 22.12 or newer) runs in simulator mode with no keys. The README section "PayPal sandbox setup" covers running against your own sandbox. - Bryntum Gantt and AG Grid Enterprise run under their trial terms, so trial watermarks are visible.
- Built with an AI coding assistant (Claude Code).
Built With
- ag-grid
- bryntum
- cloudflare
- docker
- nebius
- node.js
- nvidia-nemotron
- paypal
- paypal-webhooks
- react
- typescript
- vite
- vitest
Log in or sign up for Devpost to join the conversation.