Inspiration
Freelancers do not lose money because they cannot write an invoice. They lose it because the invoice sits in drafts, a total was copied wrong, and the follow-up never goes out. Every AI invoicing demo we tried had the same hole: the model writes the number and nobody checks it.
What it does
Paste an invoice as text. A forwarded email, an OCR dump, a PDF flattened to text. A deterministic parser or a model reads it, and both return the same structure. The total is then recomputed from the line items and compared with the total the document prints. A mismatch is a block, not a warning.
Policy scores the document on four things: missing client email, no line items, amount over 2,500, first invoice to a client. The verdict is SEND, REVIEW or BLOCK. BLOCK returns HTTP 409 and no PayPal call is made.
When the verdict is SEND, it bills. A PayPal invoice is created and sent through Invoicing v2. A one-tap payment link is created through Orders v2 and the payment is captured. Money that arrives through the emailed invoice instead is caught by a signed webhook using verify-webhook-signature, so the trail is complete either way.
Every step is appended to a SHA-256 hash chain on disk. Restart the process and the chain continues. Edit one record and verification fails from that point.
How we built it
Node 20, no runtime dependencies. src/agent.mjs holds the parser, the policy and the chain. src/llm.mjs is the model reader. src/paypal.mjs is a two-mode adapter, mock and sandbox. src/store.mjs is the durable trail. src/server.mjs is the HTTP surface and the single page. 24 tests, no network required. npm run demo drives the whole flow and prints it.
PayPal surfaces used: /v1/oauth2/token, /v2/invoicing/invoices and /send, /v2/checkout/orders and /capture, /v1/notifications/verify-webhook-signature.
Line items land in an editable AG Grid Community 31 table. Every cell edit posts to /api/recompute, which recomputes the total and re-runs the policy. Correcting a quantity can move an invoice from BLOCK to SEND. Breaking the arithmetic moves it straight back, naming both numbers.
Where the AI is
A model is genuinely better at the messy cases, so it reads the document and copies the numbers. It is never trusted with arithmetic. normalise() recomputes the total from the model's own items and compares it with the total the model says the document printed.
We measured this on a real invoice shape the parser cannot read, because it writes the quantity as (x2).
Parser: 2 items, the (x2) missed, total 1,650.00, verdict BLOCK 45, does not add up.
Model: 2 items, quantity read, total 2,100.00, verdict SEND 0.
The model did not fix the total. It read the quantity and the same verifier agreed.
Challenges we ran into
PayPal's Invoicing API returns 201 with only a self link unless you send Prefer: return=representation. The invoice id lives in the Location header. We shipped the first version without that header, and every invoice after the first one silently failed to send.
Sending an invoice returns 202 with an empty body, so status has to be read back with a GET. Capturing an order returns 422 ORDER_NOT_APPROVED until the buyer approves the checkoutnow link, and the real capture id sits at purchase_units[0].payments.captures[0].id, not at capture_id.
The sandbox was the hardest part to test. The approval link opens a login page for a sandbox personal account, and a merchant account cannot pay its own invoice. The end-to-end run only became real once we had a second sandbox account.
Accomplishments that we're proud of
The verifier is the part we trust. A model can read a document wrong and the invoice still cannot go out with a total that does not add up. We watched the parser fail on (x2) and the model succeed on the same document, with the same code deciding both times.
The hash chain held up under the test we cared about. We edited one record in the trail and verification failed at that record, not at the end of the file.
Tools used
PayPal REST: Invoicing v2, Orders v2, webhook signature verification.
AG Grid Community 31: the editable line-item grid, loaded from CDN with a read-only fallback.
The model: any OpenAI-compatible chat completion endpoint. The recorded demo ran on agnes-3.0-flash. The tests and the demo run without one.
What's next for Invoice Cash Agent
Partial payments and refunds. A second currency. An operator view over the trail, since the data is already there.