-
-
A photo of the box is blocked: the label ZIP is not the PayPal ship-to
-
Reprinted to the PayPal address, tracking pushed with Orders v2 /track and read back
-
The buyer's Resolution Center shows the tracking number Sealed pushed
-
Evidence filed with PayPal after the seller approved it
-
The buyer chat is read by a free model; rules quote PayPal's clause
Inspiration
"He had tracking. He had screenshots. He still lost $1,000." Stories like that fill r/paypal and r/hardwareswap. People who sell used GPUs, cameras and music gear get paid with PayPal Goods and Services, ship themselves, and lose claims they thought they were protected for. A common reason is a mistake made at the packing table, before any dispute exists: the buyer asks in a DM to ship to a work address or a friend, the seller agrees to be nice, and PayPal's US Seller Protection terms (updated January 26, 2026) say "You must ship the item to the shipping address on the Transaction Details page." Other tools check what PayPal's record says. We wanted something that checks the box.
What it does
Sealed follows one sale from checkout to a filed dispute response:
- Sale and DM check. The seller creates a PayPal Goods and Services checkout (Orders v2, serial as SKU, ship-to taken from the buyer's PayPal account). They paste the buyer chat. A free LLM extracts the facts (new address, Friends and Family request, buyer-arranged label, meet-up) and a versioned rule table flags them, quoting PayPal's clause with source and version date.
- Payment. Sealed captures the order and shows the Transaction Details ship-to plus PayPal's own seller_protection flag. Packing is blocked unless the capture is COMPLETED.
- Packing bench (the core). The seller photographs the boxed label. The browser decodes the barcode and checks the USPS check digit, a vision model reads the address, and plain code compares ZIP, street number and street name with PayPal's ship-to. If it does not match: "Stop. Don't tape this box." with the exact clause and what differs.
- Fix it the way PayPal suggests. One tap reprints a label to the PayPal address (EasyPost test mode), or refunds and creates a new checkout, which is PayPal's own advice when a buyer insists on another address.
- Tracking with proof. Tracking is pushed with Orders v2 /track and read back from PayPal's tracker list, so the seller sees it is on the transaction. When our sandbox buyer reported a problem, PayPal showed them that tracking number first.
- When a case comes anyway. A verified CUSTOMER.DISPUTE webhook attaches the case to the sale. Sealed reads what PayPal requests, builds a timeline from PayPal's own records, has a free model word the reviewer notes, and builds a one-page PDF. Notes that drop the tracking number or ZIP, or claim something the record does not show, are rejected by code. Nothing is filed until the seller ticks the box and taps File. Sealed uses provide-evidence in INQUIRY and provide-supporting-info once PayPal has moved the case to CHARGEBACK.
- Observed chain. Every state PayPal returned is stored with its time and debug_id, so the case page shows what actually happened, not an assumed lifecycle.
How we built it
- Next.js 15 (App Router, server actions), TypeScript, Tailwind, zod; Supabase for the sale record; Vercel hosting with a daily keep-alive.
- PayPal sandbox over REST: Orders v2 create and capture (with PayPal-Request-Id), Orders v2 /track, GET /v1/shipping/trackers for read-back, Webhooks with verify-webhook-signature (decided on verification_status, not the HTTP code), Disputes get / provide-supporting-info / provide-evidence (multipart), Payments refund.
- AI on free tiers through one OpenAI-compatible client: OpenRouter free models only (the code refuses any id without :free; NVIDIA Nemotron VL by default), Gemini Flash-Lite as fallback, Typhoon for text. The model reads chats and label photos and words the notes; it never decides coverage.
- zxing for barcodes, pdf-lib for the evidence PDF, EasyPost test mode for labels.
- 18 unit tests with no network, including the notes guard.
Challenges we ran into
- PayPal sandbox moved every Item Not Received case that already had tracking from INQUIRY to CHARGEBACK within minutes, where only supporting info is accepted. Sealed reads the case's links and picks the path PayPal leaves open.
- GET /v1/customer/disputes took up to 50 minutes to list new cases, while the webhook arrived in about 3. The case record is built from the webhook payload.
- A capture succeeded while the next GET of the order still said APPROVED. Sealed fills the view from the capture response.
- The sandbox accepted a tracking number that fails the USPS check digit, so Sealed checks digits itself.
- EasyPost test labels print VOID over the barcode, and zxing-js missed barcodes next to a dark label frame. We built phone-style sample photos with readable barcodes and fall back to the model's reading of the digits only when it passes the check digit.
- A free model once wrote "the label was checked" for an order with no packing record. That is now rejected by code, with a test.
Accomplishments that we're proud of
One unbroken real run on production: sale, payment, a wrong-address label blocked from a photo, reprint, tracking pushed and read back, the buyer's case attached by a verified webhook, and evidence filed with PayPal (HTTP 200, debug_id f10460246e606). Everything in the video comes from that run.
What we learned
Seller Protection is mostly lost or won before a dispute exists, and the useful AI here is narrow: read messy inputs, then let rules and PayPal's own records decide.
What's next for Sealed
Connect real sellers through PayPal Partner Referrals with tracking and dispute permissions, add a delivered-scan check from the carrier, a serial-number photo at packing, and the same checks for UPS and FedEx labels.
Built With
- easypost
- gemini
- nextjs
- nvidia-nemotron
- openrouter
- paypal-disputes-api
- paypal-orders-api
- paypal-payments-api
- paypal-shipment-tracking
- paypal-webhooks
- pdf-lib
- react
- supabase
- tailwindcss
- typescript
- typhoon
- vercel
- zod
- zxing