Inspiration A chargeback notice lands in a merchant's inbox at 11pm. Somewhere across five systems PayPal, the storefront, the 3PL, the support inbox, a folder of PDFs is the proof that the order shipped, that the customer signed for it, that they already got a refund. The merchant has days to find it, understand which of PayPal's evidence types applies, and respond. Miss the window and the money is gone.
For small teams this is triage under deadline with incomplete information. We kept coming back to one question: what if an agent could start the investigation the moment the dispute arrives and what if it could be trusted to stop at the edge of the money?
That question became PayPal Dispute Defender. Not a chatbot that explains disputes, and not another transaction dashboard an operational case workspace where PayPal is the source of dispute truth, merchant records are the source of fulfilment truth, AI explains what is known and what is missing, and the merchant makes every consequential decision.
What it does A dispute webhook arrives. Within seconds the case appears in the workspace with its real deadline, lifecycle stage and allowed actions. The agent classifies the reason, maps PayPal's requested evidence to the merchant's actual orders, shipment scans, refunds and support messages, flags what is missing or conflicting, and drafts a recommendation with citations back to specific evidence records.
The merchant sees the exact payload before anything leaves the system, edits the draft if needed, and approves. High-consequence actions accept claim, escalate, appeal require typing a confirmation word. Only then does the server call PayPal, and it re-checks with PayPal that the action is still permitted at that moment. The genuine response, the refreshed case status and the human approval all land in an append-only audit trail.
Five capabilities
Real-time intake, signature-verified webhooks, deduplicated by event ID, acknowledged fast, processed durably. Classification , the agent identifies the transaction, categorises the dispute and states what must be investigated. Evidence assembly , PayPal dispute data joined with merchant orders, shipments, refunds, messages and uploaded documents, each record carrying its source ID and retrieval time. Explainable analysis , completeness scoring, missing/conflicting evidence, risk indicators, and a recommendation the merchant can interrogate. Human-controlled resolution , approval-gated actions, typed confirmation, idempotency, and a full audit history. The shipped build also runs a fully working Demo Mode on clearly labelled synthetic data, so the entire workflow can be explored without touching a real merchant account. Nothing simulated is ever presented as a real PayPal result.
How we built it Stack: Next.js 14 (App Router) + TypeScript, React 18, Tailwind, Prisma with SQLite locally on a PostgreSQL-compatible schema, Zod for every boundary, and Vitest for testing.
Event pipeline. The listener preserves the raw body, asks PayPal's verify-webhook-signature endpoint whether the transmission is genuine, and fails closed , only SUCCESS proceeds. The event ID is a unique database key, so a redelivery is acknowledged and discarded rather than reprocessed. The event is stored durably, we return 2xx immediately, and a database-backed worker does the slow work: retries with exponential backoff and jitter, then dead-letters after five attempts.
PayPal is the authority, not our database. A webhook body is a trigger, not truth. The worker always re-fetches GET /v1/customer/disputes/{id} and treats PayPal's HATEOAS links and allowed_response_options as the only definition of which buttons exist. That rule is enforced in one domain module, so no screen or AI output can conjure an action PayPal doesn't currently allow. We implemented all eight Disputes v1 writes (provide evidence, make offer, accept claim, send message, escalate, provide supporting info, appeal, acknowledge return item). Ordinary payment refunds use Payments v2 capture refund, never Payouts , Payouts is for sending money out, not for reversing a purchase. A guard blocks a second concession on the same case so an accept-claim and a refund can't double-pay.
The AI agent investigates; it does not act. It has read-only tools and no write credentials. Output is schema-validated; the validator rejects fabricated evidence IDs, a dispute-ID mismatch, out-of-range confidence, and any recommendation outside the currently allowed actions. Every finding must cite a real record, and requiresHumanApproval must be true. Case text, buyer messages and order notes are framed as untrusted data rather than instructions. With no model key configured, a deterministic rule-based investigator runs and is labelled rule-based-fallback never disguised as an LLM.
Completeness is explainable, not a vibe. Optional items are excluded from the denominator, and unscored cases say "Not scored" instead of inventing 0%
S
round ( 100 ⋅ satisfied required items required items ) S=round(100⋅ required items satisfied required items )
Security was designed in, not bolted on: HMAC-signed HttpOnly session cookies with scrypt password hashes, server-side RBAC (ADMIN/OPERATOR write, VIEWER read-only), merchant scoping on every query, AES-256-GCM for secrets at rest, upload validation by extension and magic bytes with a 10 MB cap, private downloads with nosniff and an attachment disposition, login rate limiting per account and per IP, CSV formula-injection escaping, and CSP/frame-ancestors 'none'/DENY/Referrer-Policy/COOP headers on every response. The server refuses to boot in production without a real AUTH_SECRET.
We tested it like it handles money. 170 unit and integration tests across 12 files, including real-SQLite integration tests for prepare→approve, idempotent re-approval, double-refund blocking, merchant isolation, webhook dedupe, fail-closed rejection and job dead-lettering. Plus a 127-check HTTP end-to-end harness covering auth, RBAC, approvals and webhooks, and a full browser walkthrough of every screen at desktop and mobile widths.
Challenges we ran into
Webhooks lie about ordering. Events arrive late, out of order, and more than once. We learned that trusting the payload is how you act on a status PayPal has already moved past. The fix was architectural: verify, dedupe, store, acknowledge, then always re-read the authoritative case. We also hit a documented PayPal gotcha verify-webhook-signature does not support simulator/mock events, so the full verification path has to be exercised with real Sandbox events.
Teaching a model to say "I don't know." Early prompts produced confident narratives stitched from adjacent orders. We tightened it by requiring citations to concrete evidence IDs and rejecting any ID not already in the case. The agent now reports MISSING, CONFLICTING and NOT_CONNECTED as first-class outcomes, which is more useful to a merchant than a fluent guess.
Action availability is a moving target. The same button is legal at INQUIRY → WAITING_FOR_SELLER_RESPONSE and illegal at INQUIRY → OPEN. Instead of encoding PayPal's lifecycle matrix as our own truth, we made the UI and the approval service both derive from the live response, and re-check at approval time so a stale tab cannot execute a dead action.
Dispute semantics are easy to get dangerously wrong. Accepting a claim is not a merchant win — it closes the case in the buyer's favour and triggers a refund. A capture refund is a different operation from a dispute action. We treated these as separate primitives with separate confirmations, and added a double-concession guard.
We found a privilege escalation in our own code. Our audit caught VIEWER accounts able to trigger AI analysis and dispute sync because those routes used a session check instead of a write-role check. It's fixed, and eleven write attempts as a viewer now return 403. It was a good reminder that "authenticated" is not "authorised."
The unglamorous stuff counts. Browser testing at 390 px found a settings page overflowing to 551 px and a total absence of navigation on mobile. Both are fixed a bottom nav with proper aria-current, and wrapping long form hints.
Accomplishments that we're proud of A real approval boundary. The agent cannot refund, accept a claim or submit evidence. Every consequential call is a separate, human-approved server action with a re-fetched authority check, an idempotency key, and a genuine response on the record. An evidence model with provenance. Every fact traces to a source ID and timestamp, so a recommendation can always be audited back to the record that justified it. Honest state everywhere. Demo results are labelled DEMO/simulated. Missing connectors say "not connected" rather than guessing. Unscored cases say "not scored." A test suite that exercises failure, not just success duplicate webhooks, rejected signatures, dead-lettered jobs, blocked double refunds, cross-merchant access attempts. A codebase that passed its own audit: clean lint, clean typecheck, production build, 170/170 tests and 127/127 HTTP checks, with zero failing requirements. What we learned The biggest lesson is that automating a financial decision is not the same as automating a workflow. A model that is right 90% of the time is a liability if the remaining 10% moves money. So the architecture separates intelligence from authority: the AI is allowed to be uncertain, to ask for missing evidence, and to recommend; only an authorised human plus a live PayPal permission check can execute.
We also learned to read API semantics before writing integration code. Four things that look interchangeable are not investigating a dispute, submitting evidence, accepting a claim, and refunding a capture. Each has its own endpoint, its own consequences, and its own confirmation. Getting that taxonomy right mattered more than any prompt engineering.
And we learned that explainability is a product feature, not a compliance checkbox. A merchant will not approve an action they cannot follow. Citations, a transparent completeness formula and a visible audit trail are what make the recommendation usable.
What's next for PayPal Dispute Defender Live Sandbox activation end to end connect a real merchant Sandbox app, register the webhook against a public HTTPS listener, and run the full create → sync → analyse → approve → reflect cycle against genuine PayPal responses. The integration is implemented and unit-tested against the documented contracts; this step is the credential-dependent remainder. The correct multi-merchant model. A commercial version needs PayPal's partner/third-party seller authorisation, not one client-credentials token pretending to speak for every merchant. That's an explicit design boundary we won't paper over. Deeper evidence connectors , carrier and 3PL APIs so tracking and proof-of-delivery stop being manual entry, plus storefront connectors for order and returns data. Production hardening , PostgreSQL, Redis-backed rate limiting and queues, object storage for evidence, per-request CSRF tokens, antivirus scanning on uploads, and the Next 15 / Prisma 6 upgrade path. Outcome learning , track which evidence sets actually correlate with favourable resolutions, and feed that back as better triage, while keeping the human approval gate permanently in place. Our vision is professional-grade dispute operations for businesses that can't afford a chargeback team: an agent that organises the investigation and explains the decision, and a merchant who stays in control of every cent.
Built With
- agents
- ai
- api
- disputes
- fintech
- human-in-the-loop
- idempotency
- language
- large
- models
- next.js
- oauth
- openai
- paypal
- postgresql
- prisma
- react
- rest
- security
- sqlite
- tailwind
- typescript
- vitest
- webhooks
- zod
Log in or sign up for Devpost to join the conversation.