Inspiration

The starting point was a familiar freelancer complaint. The work is finished, the files are sent, and the payment is still a conversation. A polite follow up in the first week becomes a firmer one in the third, sent across time zones to a client the freelancer may never meet. Plenty of freelancers price that wait into their rates. Others turn down overseas work they would otherwise take.

The client has a matching fear. Pay in full up front and the money is gone before anything arrives. Both sides end up trusting a promise when the money could settle the question earlier. Rilis came from that gap. The client commits the money before the work starts, and the delivery decides what happens to it.

What it does

Rilis handles milestone payments between a client and a freelancer who work directly with each other.

  1. The client funds a milestone with a PayPal authorization hold. The money is reserved and visible to the freelancer before work begins. Nobody has been paid yet.
  2. The freelancer delivers against a written brief with a checklist.
  3. A verifier checks the delivery item by item and returns a verdict of pass, fail, or unclear for each one, with quoted evidence. Code computes the score from those verdicts.
  4. A policy engine in plain code applies the rules the client set: a minimum score, an auto pay limit, and a project budget cap. If every rule passes, PayPal captures the hold. If a rule fails, the milestone goes to the client, who can approve a capture or reject the work and void the hold.
  5. Each step is written to an audit log with the PayPal order, authorization, and capture IDs.

The seeded demo has Sarah, a founder in Austin, paying Bima, a designer in Yogyakarta, for a coffee startup logo. Her budget cap is $1,000, her auto pay limit is $250, and her pass threshold is 80. The $200 logo milestone captures on its own after delivery. The $600 brand kit goes to Sarah for a decision. The demo runs on the PayPal sandbox, so no real money moves. The release after a capture is a ledger entry labelled "Simulated payout" in the UI.

How we built it

Next.js with the App Router and TypeScript covers both sides of the app. The pages handle login, a dashboard, milestones, activity, and profiles, and the API route handlers run the payment workflow in the same codebase.

PayPal carries the money. We use the Orders API with intent: AUTHORIZE, the PayPal JS SDK for the approval popup, and a server side REST client for authorize, capture, and void calls. The client secret stays in server environment variables. Amounts are integer cents throughout, and mutating PayPal calls carry a PayPal-Request-Id derived from the milestone and the transition.

The verifier calls an OpenAI compatible chat endpoint and has to answer in strict JSON: one verdict and one quoted piece of evidence per checklist item. Code adds the verdicts into a score in lib/scoring.ts. The policy decision lives in lib/policy.ts, and the allowed state changes live in lib/transitions.ts. If no AI key is set, or the provider errors or returns output we cannot use, a deterministic fallback verifier runs instead and the UI shows which provider produced the result.

Data sits in a libSQL database, which runs as a local file in development, in memory during tests, and on Turso when hosted. The demo project seeds itself on first boot. Vitest covers the policy table, scoring, the state machine, the PayPal client, the verifier, the workflow, and persistence. The suite currently runs 127 tests, all passing.

Challenges we ran into

Money code has to survive retries. A double click, a page refresh, or a repeated request can ask PayPal to do the same thing twice. We derived the idempotency key for each mutating call from the milestone and the transition, serialized workflow changes so two requests cannot act on the same stale state, and made the terminal states, RELEASED and VOIDED, refuse further transitions on the server.

Model output was the next problem. A model can return malformed JSON, skip a checklist item, or hand back a score it made up. We validate every verdict and its evidence quote, discard any score the model supplies, and compute the score in code. When the provider output fails validation, the fallback verifier takes over rather than leaving the milestone stuck.

The budget cap nearly had a hole in it. A client approving an escalated milestone could have pushed the project past its total budget. Approval now overrides the auto pay limit and a failing score, and the cap still blocks the capture. The hold stays in place until the client raises the cap or voids it, and the policy edit is logged.

Persistence caused quieter trouble. The first data layer relied on a native SQLite driver that had to compile for each platform, which made Windows development and hosting setup fragile. Moving to the libSQL client kept the same SQL and gave us one code path for a local file, tests, and a hosted database.

Accomplishments that we're proud of

The full loop runs end to end in the sandbox, in both directions. One milestone captures itself under policy. A larger one stops, waits for the client, and gets voided on rejection. The demo resets to its seeded state, so a judge can run the whole thing again from the start.

The agent also works without an AI key. The fallback verifier produces the same verdict format, the score still comes from code, and the policy outcome is unchanged. A missing key never becomes a stuck demo.

We kept one line visible throughout: PayPal sandbox captures are real sandbox calls with real sandbox IDs, and the final release to the freelancer is a simulated ledger entry. The UI says "Simulated payout" on that entry. We would rather a judge see the boundary than discover it.

What we learned

Payment authority belongs in code. The model reads a delivery against a checklist well, quotes useful evidence, and that is where its job ends. The decision to capture money came out more trustworthy once a short policy table owned it, because anyone can read the table and predict what it will do with a score, an amount, and a cap.

Idempotency and state transitions turned out to be user facing concerns. People trust a payment tool based on what happens when they click twice or reload at the wrong moment. Those cases got tests before some visible features did, and they earned the time.

The audit log changed how we explain the product. Showing the PayPal ID beside each decision answers most questions faster than describing the agent's reasoning. The fallback verifier taught us a similar lesson: a demo people can run without credentials teaches more than a smarter one they cannot start.

What's next for Rilis

Replace the simulated ledger release with a PayPal Payouts transfer, so captured money reaches the freelancer's PayPal account. That work starts in the sandbox, and a live mode would follow only after the payment controls around it are in place.

Give clients and freelancers their own accounts and projects. The seeded Sarah and Bima roles made the demo easy to run, and the same workflow should accept a real pair with their own briefs, checklists, and policy settings.

Check more kinds of delivery. Text and links cover the current demo. Design files, PDFs, and source repositories each need their own way of producing evidence against a checklist.

Add revision rounds. When one checklist item fails, the freelancer should resubmit that item and keep the passing verdicts, instead of starting the milestone over.

Send the client a notice when a milestone escalates, with the authorization expiry in view. A hold that expires while it waits for a decision helps nobody, and the data for that reminder is already stored.

Built With

  • a-local-file
  • metawonder
  • next.js
  • or-in-memory-for-tests-vitest-5
  • paypalapi
  • paypaljssdk
  • sandbox-with-intent:-authorize-paypal-js-sdk-(@paypal/paypal-js)-an-openai-compatible-chat-endpoint-for-deliverable-verification
  • tailwind
  • turso
  • typescript
  • vercel
  • with-a-deterministic-fallback-verifier-when-no-ai-key-is-set-libsql-(@libsql/client)-on-turso
Share this project:

Updates

Submission history