Who it is for. Dana is a freelance designer. She bills from her work log and is paid through PayPal. A client pays her USD 296 invoice, then doubts one hour on one line: a call billed at her design rate. Today that ends in an e-mail thread, a refund typed by hand, or a chargeback that puts all USD 296 at stake. With InvoiceProof she gives back exactly that hour on PayPal (USD 50), with the question as the reason on PayPal's record, and keeps USD 246. (Dana is a made-up example; the payment and refund are real sandbox calls.)
What is new. Not "a partial refund": PayPal has that. Three things together that PayPal, Stripe credit notes and chargeback tools do not have: the refund belongs to one invoice line on PayPal, both sides read the evidence for that line before a dispute exists, and the AI review that ran before sending decides what the app suggests after payment. PayPal's own refund record then names the invoice, the line and the reason. The comparison by product, and what we measured, are under Details.
Why it matters. Getting paid is the freelancer's oldest problem:
- 71% of freelancers have had trouble collecting a payment at some point, and the average unpaid amount in one year was USD 5,968 (Freelancers Union, "The Costs of Nonpayment", 5,358 surveyed) [1].
- 62% of New York freelancers have lost wages to a client who refused to pay; of those, 51% lost more than USD 1,000 (Authors Guild, Freelancers Union and five other groups, 2022) [2].
- On PayPal, a disagreement that becomes a claim or a chargeback costs the seller a Dispute Fee. PayPal waives it when buyer and seller settle before a claim [3]. Settling on the one line is that earlier exit.
These surveys are about non-payment in general. We have not measured how often one questioned line turns into a dispute.
What it does, in three steps.
- An AI drafts the invoice from the work log. Every line must quote the log and the price list word for word; a second AI reads each line for meaning. A line without support is blocked or flagged before PayPal sees it.
- The client opens a page that shows each line beside the words it came from, and can question one line.
- The app drafts the answer from the same log and suggests what to do: refund that line (or part of its hours) or keep it. The freelancer clicks, and PayPal makes the refund.
In the demo the AI flagged the line that bills a client call at the design rate before the invoice was sent, and that is the line the client later questioned.
A judge's path, three minutes. Watch the video (2:49; the demo starts at
0:15). Then, with no account, run node local-app/start.ts ledger.sqlite 4300
in the repository and load an example: the same flow on recorded model
replies and a local PayPal mock, both labelled on screen. The test results the
README cites are in verification/*.tap.
[1] https://onlabor.org/wp-content/uploads/2017/05/FU_NonpaymentReport_r3.pdf [2] https://authorsguild.org/news/survey-finds-62-percent-of-ny-freelance-workers-have-lost-wages-due-to-nonpayment [3] https://www.paypal.com/us/cshelp/article/what-is-the-paypal-dispute-fee-and-why-was-i-charged-one-help349 [4] https://www.paypal.com/us/cshelp/article/how-do-i-issue-a-refund-help101 [5] https://developer.paypal.com/api/invoicing/v2/invoices-refunds [6] https://docs.stripe.com/invoicing/dashboard/credit-notes [7] https://chargeflow.io/blog/winning-paypal-chargebacks (all read 2026-11-12)
Details
Everything above is the pitch. What follows is for a reader who wants to check a claim: what PayPal does and what the app adds, how each step works, and how we tested it.
Measured, not asserted
| What we ran | Result | Where |
|---|---|---|
| The drafting AI on 10 work logs, 3 runs each, with and without our checks | It drafted 48 lines worth USD 5,703. 6 of them (USD 525, 9%) had no support: a rush fee with no price, and waiting billed as design work. Without the checks all 6 go on the invoice; with them all 6 were stopped, and no supported line was stopped (30 of 30 cases) | local-app/draft-eval-results.json |
| Orders to the AI hidden in the work log and in the price list | No line produced, in 3 of 3 runs | same file |
| One real sandbox payment (2026-11-12) | USD 296.00 paid by card on PayPal's page, USD 50.00 refunded for 1 of 2 hours on one line, USD 246.00 kept; PayPal reads the invoice back as PARTIALLY_REFUNDED | video 2:29, docs/screens/3-paypal-partially-refunded.png |
| PayPal's reply lost after the refund was made | The retry got the same refund id back (201, then 200): one refund, not two | verification/http-contract-recovery.tap |
| The reply AI on 10 client questions it was never tuned on | 9 of 10 | local-app/answer-holdout-results.json |
| The second AI on 12 labelled lines | 11 agreed; none of the 9 wrong lines passed; 1 correct line was disputed | local-app/audit-eval-results.json |
The cases are synthetic and small: these are test results, not rates for real invoices.
What already exists, by name
| Product | What it does with a questioned line | What is missing |
|---|---|---|
| PayPal refund [4] | Full or partial refund within 180 days: an amount and a note | Tied to no invoice line; the client sees no reason for the line |
| PayPal Invoicing API, record refund [5] | Marks the invoice refunded or partially refunded | An amount on the invoice, not a line |
| Stripe Invoicing credit notes [6] | Credits a line item's quantity or amount, with a reason. The closest thing to this idea | Stripe only; the customer is not shown where the line came from |
| Chargeback-response guides and services such as Chargeflow [7] | Help the seller send evidence once a claim or chargeback exists | Too late: the whole payment is already at stake |
| InvoiceProof | A line-level refund on PayPal, with the log entry behind the line shown to both sides before any dispute, on a line the AI had already flagged | Sandbox only, one payment per invoice |
| When … | A plain AI invoice drafter, or PayPal's own refund | InvoiceProof |
|---|---|---|
| the client questions one line after paying | e-mail thread, manual refund, or a dispute on the whole payment | a reply drafted from the same log, then that line, or only the questioned part of its quantity, refunded on the PayPal payment; the rest stays paid (video 1:55) |
| the freelancer refunds part of the payment | PayPal: an amount and a free-text note, linked to no line | the line's exact amount, shown beside that line and its log entry; PayPal's own refund record names the invoice, the line and the reason (video 2:30) |
| the client questions one line before paying | a new invoice, a new link | the line is removed or changed on the sent PayPal invoice, same number and payer link; the proof page shows version 1 beside version 2 with the reason |
| the log asks for a fee the price list does not have | bills it | blocks it before PayPal (0:28) |
What is PayPal's and what is ours
PayPal already lets a seller refund part of a payment: an amount and a note, tied to nothing. Dispute-evidence tools help defend the whole charge once a dispute is open. InvoiceProof settles it before that: the refund is tied to one invoice line, the log entry behind that line is the reason, and client and freelancer read the same proof page.
PayPal creates and sends the invoice, takes the card payment on its own page,
makes the refund and reports the invoice as "Partially refunded" (Invoicing
API v2, Payments API v2). InvoiceProof adds what PayPal does not hold: which
log entry each line came from, the check that blocks a line without one, and
the link from a refund to its line. PayPal's refund record then carries that
link too: the line and the reason are in its note, beside PayPal's invoice id
(docs/paypal-refund-record.md). Each invoice item on PayPal carries its two
quotes, and the proof page compares itself with PayPal's copy on every load.
So one line is followed from draft to payment: blocked before PayPal,
corrected on the sent invoice before payment, refunded on the payment after
it. That works because every line carries the words it was drafted from: the
log entry for the work and the price-list entry for the rate. A line without
both never reaches PayPal.
| Also, when … | A plain AI invoice drafter | InvoiceProof |
|---|---|---|
| the client wants to know where a line came from | asks the freelancer | opens the proof page: each line beside the log and price entry it came from |
| hours billed as design work that the log says were a client call (the demo) or waiting (the evaluations) | bills it | a second model, reading for meaning, flags it; kept only with a reason the client sees (0:41) |
| the same work-log entry is billed on a second invoice | bills it twice | asks for a written reason, which the client sees |
| PayPal's invoice changes later | nobody sees it | the proof page reads PayPal on every load and names what changed. We raised one quantity on PayPal outside the app, and the page said "L1 quantity: proof 2, PayPal now 5" (screenshot 4) |
How it works
Paste a work log and a price list. An AI model drafts the lines and must quote, word for word, the log line for the work and the price-list line for the rate. The app finds each quote in the pasted text and checks that the quantity and rate are numbers inside them; a line that fails is blocked. In our runs the log asked for a rush fee the price list does not have; the model drafted it and the app blocked it. That check is about text, so a second model (from a different model family) reads each kept line for meaning and says whether its quotes support it. In the demo it flagged a line whose two hours include one on a client call, billed at the design rate. A flagged line can be removed, or kept with a reason the client will see. The freelancer compares each line with its quotes, ticks it, and approves.
The app then creates the invoice through the PayPal Invoicing API v2 (sandbox): each item carries its two quotes, and the note links to a proof page. It reads the invoice back, sends it through PayPal, and shows the payer link and QR code. The client's proof page shows each line beside its quotes and the AI review, and on every load reads the invoice from PayPal and says whether it still matches the approved lines. The client can question a line. A reply agent then drafts the freelancer's answer from the work log and price list, with a quote the app finds word for word in them, or says the log does not answer it; the freelancer edits and publishes the reply on the proof page. Beside the draft the app suggests what to do with the line itself: give back that line alone when the AI review disputed it before sending or its own log entry does not answer the question, otherwise keep it. The amounts on that card are the app's arithmetic, and nothing reaches PayPal until the freelancer clicks. When the client is right, the freelancer corrects the sent invoice instead of sending a new one. They remove the line, or change its numbers within the same quotes and have it reviewed again, and write a reason. The app first reads the invoice from PayPal and refuses when it is paid or was edited elsewhere. It then updates the invoice in place, keeping the same number and payer link. In our sandbox run of 2026-11-10 a flagged USD 100 line came off a sent USD 296 invoice. The proof page now shows version 1 beside version 2, with the reason and the question it answers, and PayPal's read-back matches version 2. A line edited after the review is reviewed again before approval.
Once the client has paid, the invoice is not edited: the freelancer refunds exactly the questioned line on the client's PayPal payment, with a reason PayPal passes to the payer. In our sandbox run a guest paid USD 296.00 by test card on PayPal's own invoice page, asked about the hour on the call, and got a drafted reply quoting the log; the freelancer refunded 1 of that line's 2 hours, USD 50.00, and PayPal read the invoice back as PARTIALLY_REFUNDED. The proof page shows the refund beside the line with PayPal's refund id. Each refund is saved before PayPal is asked and carries a key PayPal remembers, so when PayPal's answer is lost the refund is resent with the same key and PayPal returns the one it already made (sandbox, 2026-11-10: 201, then 200 with the same refund id). That holds while PayPal keeps the key (its API guide: up to 45 days for a refund), so the app stops resending a staged refund after 30 days and asks for a manual check; a refund made outside the app is read back from PayPal and counted, not prevented. The app never sends a second create for one approval: in our sandbox runs a repeated invoice number was refused while the request key did not stop a second invoice, so each approval gets one number and is looked up by it after a timeout.
How we tested
We ran the review on 12 synthetic labelled lines: 11 agreed with the label, none of the 9 wrong lines passed, and one correct line was disputed. That is a small test, not a rate. The reply agent was checked on 10 synthetic client questions: 4 the log answers, 3 it does not, 2 that try to give the agent orders, and 1 about another line. When we tightened the checks (numbers only from the sources, never the question; the quote from this line's own entry; no follow-up the freelancer never promised), the earlier replies fell to 8 of 10, and one later reply claimed an agreement the log does not record. After the prompt fixes a run passed 10 of 10; these ten cases shaped the prompt, so it is a development set (after a change of model route it fell to 8 of 10, and one more prompt rule brought it back to 10 of 10). On 10 held-out questions written before any run and never tuned against, the reply agent passed 9 of 10 on the first run and 9 of 10 again after that rule; each miss was a different reply (one cited another log entry, one wrote "my mistake", a phrase the checks forbid). The app also flags any promise in a drafted reply to the freelancer before it is published. We also ran the whole pipeline (draft, quote check, review) on 10 hostile or awkward work logs, including orders to the AI hidden in the log and in the price list. The first run passed 6 and showed four gaps; the worst was a quote that left out "mostly waiting for the client's feedback", so the review now also reads the whole log line each quote came from. After the fixes we ran the set three times: 30 of 30, and neither hidden order produced a line. Approving the same work log twice is caught too: a log line already billed on a live invoice needs a written reason, which the client sees. Limits: sandbox only, synthetic data, a sandbox test card; the checks show where a line came from, not that the work was done; the review is advice and can be wrong. Without any account the repository runs on recorded model replies and a local mock, and the page says so. Built with Node 26 and TypeScript, no framework; drafting model anthropic/claude-sonnet-5.5 and review model openai/gpt-5.6-terra through OpenRouter.
Built With
- node.js
- openrouter
- paypal
- paypal-invoicing-api
- paypal-payments-api
- sqlite
- typescript