Inspiration
Deposits are where rental shops and their customers stop trusting each other. In October 2026 we interviewed the owner of a bicycle rental shop in Vietnam that does 400 to 600 rentals a month. By the owner's own estimates, staff photograph only 30 to 40 percent of pickups, check each returned bike by eye in 3 to 5 minutes, argue about deposits 5 to 10 times a month, and the shop absorbs 2 to 5 million dong (about $75 to $190) a month in repairs it cannot prove. At busy times renters wait 15 to 30 minutes to get their deposit back. Today the shop takes deposits in cash or by bank transfer, which is exactly the gap a PayPal authorization fills: it holds money without taking it.
We set out to settle that hold fairly, with AI doing the looking, code doing the pricing, and people doing the deciding. When we asked the owner what would make them trust an AI check, the answer described what a renter needs too: show where the damage is, mark it on before and after photos, keep the time and the bike's ID, and let the customer see the evidence. Handback is built around that answer.
What it does
- Book. The renter sees the shop's repair price list and cancellation terms, then pays the rental fee with the PayPal button (JS SDK v6). In the same approval PayPal saves their account so the shop can hold a deposit later. Every booking comes with a deposit mandate: a hashed statement of the maximum hold, when it starts and what can ever be charged.
- Pick up. Staff photograph the item and tap once. PayPal holds the deposit on the saved account as a merchant-initiated authorization, so its clock starts when the item leaves the shop. The renter confirms the pickup photo on their own phone.
- Return. Staff photograph it again. Two independent Gemini calls compare the photos and return schema-validated findings with boxes on both photos. The model points at the shop's price list and code sets the amount; a charge is proposed only when both looks agree, while marks that were already there and normal wear are never charged.
- Review. The renter accepts or questions each proposed charge on their phone, with the photos side by side. A person at the counter decides anything questioned.
- Settle. One final capture takes exactly the agreed amount and PayPal releases the rest of the hold, right at the counter. Repairs above the deposit go to the saved account.
- After the fact. Partial refunds that can never exceed what was kept or go out twice, cancellation before pickup with a fee refund by the shop's policy, and a dispute desk that answers a PayPal dispute with a one-page evidence pack built from the rental's own hash-chained record. Holds renew automatically before they lapse.
- Around the counter. A schedule (Bryntum Scheduler) shows which physical unit is where; when a returned item needs repair, an agent takes that unit off the schedule and proposes moving the bookings it affects, and staff approve each move with a click. An owner's dashboard (AG Studio) shows where every deposit dollar went, with a deposit-desk agent that explains a rental or drafts a refund for a person to confirm. A renter's AI assistant can book over MCP, and money moves only after the person approves in PayPal and the counter settles.
How we built it
Next.js 16 (App Router, server actions, server-sent events), TypeScript, Tailwind 4 and Postgres, deployed on Render from one Blueprint: web service, Render Postgres, Render Workflows for the photo inspection and hold renewals (idempotency keys derived from the rental), and an hourly cron job that also refreshes the public demo every night.
PayPal:
- Server SDK 2.5: Orders v2 (a booking order that saves PayPal with
store_in_vault: ON_SUCCESS; a merchant-initiatedAUTHORIZEon the savedvault_idwith stored-credential fields) and Payments v2 (partialfinal_capture, void, reauthorize, refund). - JS SDK v6 button through
@paypal/react-paypal-js, plus approval by redirect for bookings an assistant starts. - Disputes v1: read the case, provide evidence as multipart, and in the sandbox require-evidence and adjudicate, always following the links PayPal returns.
- Webhooks: offline RSA-SHA256 verification with the API as fallback, applied once per event id;
CHECKOUT.ORDER.APPROVEDbooks a renter who closed the window early. - A
PayPal-Request-Idderived from the rental on every POST, and PayPal'sdebug_idon every error the counter shows.
AI: Gemini 3.8 Flash with JSON-schema output and a repair turn, two independent looks and a consensus rule; Gemini 3 Pro Image to build labeled eval pairs. AG Studio's Agent Framework runs the dashboard's agent on Gemini through a staff-only server route. An MCP server built with @modelcontextprotocol/sdk. We used PayPal's APIMatic Context Plugin while writing the Server SDK integration (docs/ai-build-log.md).
Every PayPal behaviour we rely on was measured in the sandbox first and written down with its ids (docs/paypal-sandbox-notes.md). The project ships with 502 unit tests (Vitest), 18 Playwright end-to-end tests, GitHub Actions CI, and scripted runs against the real sandbox and the deployed copy.
Challenges we ran into
- Finding the exact renewal window. PayPal allows one reauthorization "from Day 4 to Day 29", so we measured it: holds were refused at 63, 70 and 71.92 hours and accepted at 72.05, and a renewed hold keeps the original 29-day expiry. That gave us the design: hold at pickup rather than at booking, and renew the day before the item comes back.
- Making AI findings trustworthy. A single model look can occasionally see damage that isn't there. Requiring two independent looks to agree removed every false charge in our eval.
- Keeping fine detail in photos. Our tests showed that storing uploads at JPEG quality 85 with reduced colour hid a small red rear light from the model. Storing at quality 95 with full colour brought detection back to 12 of 12 looks, and both eval sets still showed zero false charges on the new photos.
- Real-world photos. We built a second eval set on 55 real-photo pairs. Where the model misread printed text on a rotated photo, a prompt fix removed it; where it read glare as wear, the deterministic price gate kept it as a note, so neither ever became a charge.
- Reliable dispute actions. The sandbox takes minutes between dispute steps, so the desk confirms every action by reading the dispute back from PayPal, which keeps it from filing anything twice.
Accomplishments that we're proud of
- A full rental settled in the PayPal sandbox in about 33 seconds, from the v6 button to the final capture, and the same flow running live on Render with verified PayPal webhooks and Render Workflows.
- Zero false charges with two looks: 0 of 72 unchanged items on 36 AI-generated pairs and 0 of 99 on 55 pairs built on real photographs, while 41 of 42 and 63 of 66 real changes were charged at exactly the right price.
- Two real sandbox disputes answered from the counter and driven to a decision, one for the shop and one for the buyer.
- A design where the renter co-signs every charge and the AI can only propose, which is what the shop owner asked for.
What we learned
Talking to a real shop shaped the product. We started with "the AI finds damage"; the owner asked for "the AI shows its evidence to both sides", and that became the core of Handback: trust comes from evidence both people can see. We also learned to measure PayPal behaviour rather than assume it. The renewal window, the expiry of a renewed hold and how dispute actions behave all came from the sandbox, and the design is better for it.
What's next for Handback
- Bring Handback to a real rental counter, starting with bicycles.
- Per-person staff accounts and roles.
- Deposits for walk-in renters who didn't save PayPal at booking (the gateway already supports it).
- An eval set of real before-and-after photos taken at a counter, and a glare check before the model.
Built With
- ag-studio
- apimatic
- bryntum
- gemini
- github-actions
- google-genai
- model-context-protocol
- next.js
- node.js
- paypal
- paypal-js-sdk
- paypal-server-sdk
- playwright
- postgresql
- react
- render
- tailwindcss
- typescript
- vitest
Log in or sign up for Devpost to join the conversation.