Run against the PayPal sandbox (2026-10-10 and 2026-10-14)

Chaser was run against a real PayPal sandbox business account (MOCK_PAYPAL=0, @paypal/agent-toolkit 1.11.0, api-m.sandbox.paypal.com). 8 invoices were created and sent through the toolkit (create_invoice x8, send_invoice x8, ids INV2-…). 5 reminders were sent by the real agent loop under the policy gate in one "Run collections" (3 skipped as not yet due; every audit row names its rule). An external $800 payment was recorded on INV-0006 (payment_id EXTR-3W091620YC457544T; PARTIALLY_PAID, $1,000 due). The Disputes API provide-evidence and send-message calls reached PayPal (authenticated 404 RESOURCE_NOT_FOUND on a probe id; no success claimed). On 2026-10-14 a second probe read all 8 invoices again to verify the signal behind viewed-aware chasing: detail.viewed_by_recipient is present on every invoice and false on all 8 (the recipients are .example addresses), debug ids recorded in docs/sandbox-evidence/VIEWED-BY-RECIPIENT.md. Call-by-call JSON, audit dumps and screenshots: docs/sandbox-evidence/; the README has a live-vs-demo table saying exactly what the hosted site runs against.

Not exercised: list_transactions answered 403 (Transaction Search is not enabled on this sandbox app), so reconciliation against real sandbox payments, a buyer paying, webhooks, refunds, accepting a dispute, a successful provide-evidence and a viewed_by_recipient: true ran only in demo mode with sandbox-shaped responses; those paths are covered by the 119 vitest tests, not by PayPal.

Why not PayPal's built-in reminders, or a chat bot over the toolkit

Five things we checked no other entry in the invoicing / AR cluster does:

  • Predicted pay date and adaptive cadence from the client's own PayPal history. Each payer's paid invoices (payments.transactions[].payment_date vs due_date) give a median lateness, a late rate and a risk band. The grid shows Predicted pay and Risk; reminders.adaptToHistory brings firm/final forward for habitual late payers and back for reliable ones, and the ledger cites the numbers ("firm at day 8 (threshold 7, 10 − 3 for a late-risk payer): Acme Corp 2 paid invoices, median 14 days late, 100% paid late"). payer-history.ts.
  • Promise to pay. Paste a client's reply ("we'll pay the rest by the 20th"): a code classifier extracts the date and amount, record_promise stores it (policy promises.maxDays decides allow / ask), chasing pauses until then, and a broken promise escalates the next reminder one stage with the promise named in the subject and the ledger. promise.ts.
  • Published matcher accuracy. A labelled benchmark of 71 transactions over 81 invoices (exact, partial, split, net of PayPal fee, memo typos, paid from another email, overpayments, duplicates, refunds, ambiguous pairs, wrong currency, off-invoice): 70 / 71 correct, 100% precision on the 53 matches the agent records without asking; per-rule precision/recall in docs/RECONCILIATION-BENCHMARK.md, pinned by a test. The one miss is a documented known limit (a second equal instalment with no memo is held for review rather than recorded).
  • Viewed-aware chasing. PayPal's viewed_by_recipient decides between a firmer reminder and a re-delivery with an address check for an invoice the client never opened (reminders.unviewedStrategy), still through send_invoice_reminder, still counted against the cadence.
  • A decision after the final notice. Every chase loop in the field ends at "final notice" and goes quiet. collections.afterFinal proposes exactly one of: write-off under a threshold (cancel_sent_invoice), a settlement offer (a discounted replacement invoice created and sent), or a hand-off pack (statement of account + chase history + payment record + evidence, as Markdown). Each is an approval card where money is affected. Per-client statement of account (aging buckets, one total, one pay action) from the invoice detail panel or by asking.

And what Chaser already did: it reconciles what PayPal cannot link (one transaction, several allocations, edited in an AG Grid match review before record_payment_for_invoice writes it back); it answers disputes through provide-evidence / send-message, which the toolkit does not expose; and every action is decided by a policy you edit as data, with a ledger that names the field.

What it does

An accounts-receivable agent for PayPal sellers. Reconciliation with an editable match review and published accuracy; a chase that reads the payer's history, PayPal's viewed flag and the client's promise, not just the calendar; policy as JSON with an approval ledger; dispute evidence packs through the Disputes API; write-off / settlement / hand-off after the final notice; statements of account; invoices from a sentence; shipment tracking; retainers as subscriptions; webhooks that flip invoices to PAID and update the payer history. Anything outside the policy becomes an approval card.

How we built it

PayPal Agent Toolkit 1.11 (Invoicing v2, Transaction Search, Payments v2, Disputes v1, Shipment Tracking, Catalog, Subscriptions) plus plain REST for provide-evidence / send-message, OAuth and verify-webhook-signature. Vercel AI SDK 7 (streamText, toolApproval) with the policy engine before every execution; an adapter re-wraps the toolkit's SDK v4 tools. The planning that touches money is code with named rules (matcher, cadence, classifier, after-final), unit-tested and benchmarked; the model acts on structured results and writes the prose. A keyless rule planner implements LanguageModelV3, so the whole product runs with zero keys: the hosted demo runs on it (the drawer header says so); with a free Gemini / Groq / Nemotron key the same tools are driven by an LLM behind daily caps. Next.js 16, React 19, AG Grid Community (predicted pay / risk / next action / promise / viewed columns, editable match review, ledger, detail panel).

Challenges

Toolkit v4 vs SDK v7; string money values and a no-spaces regex on accept_dispute_claim.note; Invoicing v2 rejecting a placeholder invoicer_email_address (422); state drifting across Vercel instances until the snapshot moved to the client; designing a benchmark whose scenarios do not collide (two invoices with equal balances are a feature, an accidental one is a bug); deciding what to cite when a stage is moved by history and the channel is changed by the viewed flag (the ledger now carries both).

What's next

Enable Transaction Search and a buyer payment so reconciliation and provide-evidence are shown succeeding on the sandbox, and a real recipient opening an invoice so viewed_by_recipient: true is observed; PDF export of the statement and hand-off pack; instalment plans via Subscriptions from the chase; multi-merchant auth.

Built With

Share this project:

Updates

Submission history