Overview
Avowly ties a PayPal payment to a physical item. The buyer's money is authorized, not captured. It's captured only after the item in the buyer's hands is proven to be the one that was listed: the same serial or IMEI, or the same unique marks. Sales use one handoff; rentals use two (pickup and return) against a fee-plus-deposit hold.
Source: github.com/angelraph/avowly (MIT). Stack: Next.js 15, TypeScript, Drizzle on Postgres (Neon in production, embedded PGlite locally), the OpenAI Responses API with strict structured outputs, PayPal REST APIs and JS SDK v6, sharp for image processing, web push.
Architecture
Sale flow
| Step | What happens | PayPal |
|---|---|---|
| List | Seller photographs the item with a one-time code. Photos are redacted, read, and policy verifies code + identifier. | none |
| Pay | Buyer approves in the PayPal popup. Price + buyer protection is held. | POST /v2/checkout/orders (intent AUTHORIZE), then /authorize |
| Handoff | Buyer scans the item. The model transcribes identifiers without being told the expected value; policy compares. | none |
| Release | Only after a policy match. Capture, then pay the seller their full price. | /v2/payments/authorizations/:id/capture, POST /v1/payments/payouts |
| Cancel | Either side, before capture. | /authorizations/:id/void |
Holds are honored for 3 days. After that Avowly reauthorizes (allowed to day 29) before capturing. Every money call carries aPayPal-Request-Id, and each step is guarded by a compare-and-set status change, so a double tap can't charge twice.
Rental flow
- One authorization for fee + protection + deposit.
- Pickup: the renter records condition and accessories; the fee is captured with
final_capture: false, so the deposit stays held. - Return: the owner scans it back; the model compares pickup and return photos against a fixed list of damage categories.
- Settle: policy charges only confident findings from the owner's pre-agreed price list, capped at the deposit, with
final_capture: true. A clean return voids the remainder.
AI reads, rules decide
The model is a reader. Everything that moves money lives in src/lib/policy.ts and is unit-tested:
- Identifiers below 0.8 confidence are ignored; IMEIs must pass the Luhn check, which rejects most single-digit misreads.
- A mismatch never shows the listed identifier in full, so it can't teach a scammer the real one.
- Phones and tablets must show the anti-theft lock switched off.
- Items without serials need at least two recorded marks seen, and none clearly missing.
- Rental charges come only from the owner's price list, each applied once, capped at the deposit. Anything else is shown as “flagged, not charged”.
- Market checks (Channel3) compare only against products with the same brand and model number, and need at least 3 offers before judging a price.
Photo privacy
Before any photo is stored, a privacy pass finds other people's personal information (phone numbers, contact names, call logs, messages, emails, faces) and pixelates it with sharp. The item's serial and the one-time code stay readable. Images are re-encoded, which strips EXIF data like GPS. The original is never stored; if the check fails, the upload is refused.
Scam-check input is never stored, and IP addresses are kept only as salted hashes for rate limiting. Photos are private by default. Only shots the model marks as showing the item and nothing identifying appear on public pages. Every other photo needs a seller or buyer key.
PayPal integration
| Capability | Used for |
|---|---|
| JS SDK v6 | PayPal button, one-time payment session |
| Orders v2 · AUTHORIZE | Holds for sales and rentals; redirect approval for agent-started purchases |
| Payments v2 | Capture (partial and final), void, reauthorize |
| Payouts | Paying the seller their full price at release |
| Webhooks | Signature-verified PayPal confirmations in every deal timeline |
| Disputes | Automatic evidence: the handoff record PDF via provide-evidence |
Settlement uses platform_payout: Avowly is the payee and pays sellers by Payouts, so it can add buyer protection on top (5%, min $1). Naming the seller as payee is supported in code, but PayPal refuses AUTHORIZE holds for third-party payees without partner approval.
Webhooks & disputes
POST /api/webhooks/paypal verifies each event with PayPal's verify-webhook-signature and maps it to a deal via custom_id, payout batch ID, or disputed capture. Subscribed events include authorization created/voided, capture completed/declined/refunded, payout item outcomes, and dispute created/updated/resolved.
On a dispute, Avowly builds the handoff record (possession proof, match, photos, PayPal timeline) and submits it with the evidence type PayPal requested, falling back to OTHER. Submissions are recorded so nothing is sent twice.
Agents (MCP)
Streamable HTTP MCP server, no auth for search:
https://avowly.vercel.app/api/mcpTools: search_verified_listings, get_listing, start_protected_purchase,get_deal_status. Agents can start a purchase but never move money: the tool returns a PayPal approval link that the human must open. See For AI agents.
Scam check & badge
The free scam check sends pasted text or screenshots for analysis and stores only the verdict. The model can only choose flags from a fixed list; scoreListingRisk in policy.ts scores them with published weights and adds live price evidence. A clean result never claims a listing is safe. Instructions planted in a listing are ignored.
GET /api/badge/:id returns a live SVG badge that reflects the listing's real status, so a sold item can't keep showing as available.
HTTP API
| Route | Purpose |
|---|---|
POST /api/listings | Create a listing; returns the seller key and one-time code |
POST /api/listings/:id/verify | Upload possession photos (multipart) |
POST /api/deals | Start a deal and PayPal order |
POST /api/deals/:id/authorize | Place the hold after approval |
POST /api/deals/:id/handoff · /release | Sale handoff scan and release |
POST /api/deals/:id/pickup · /return · /settle | Rental steps |
POST /api/deals/:id/cancel | Release the hold before capture |
GET /api/deals/:id/record | Handoff record PDF (party key) |
POST /api/support | Open a support ticket |
POST /api/check | Run the free scam check |
GET /api/badge/:id | Live verified badge (SVG) |
Write endpoints and every AI call are rate limited per caller and per listing or deal.
Run it
git clone https://github.com/angelraph/avowly
cd avowly && npm install
cp .env.example .env.local # PayPal sandbox + OpenAI keys
npm run dev # http://localhost:3000
npm test # policy unit testsWithout DATABASE_URL, Avowly uses an embedded Postgres in ./.data, so there's nothing else to install.
Limits
- Runs on the PayPal sandbox; production would use PayPal's platform features (fees, delayed disbursement), which need partner approval.
- The vision model reads; it is not a certified inspector. Low-confidence findings are never charged.
- Lock status is read from the device's own screen; carrier blacklist and IMEI database checks aren't included.
- Redaction relies on the model finding personal areas; boxes are padded, but very small or unusual text could be missed.