Avowly

Docs

How Avowly works under the hood.

For developers and judges: the PayPal flows, where the AI stops and plain rules take over, how photos stay private, and how agents connect.

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

Phone browsercamera · PayPal JS v6Avowly (Next.js)Privacy redactionVision readingPolicy (plain code)Deal state + eventsMCP serverOpenAIimages in, JSON outPayPalOrders · PaymentsPayouts · DisputesPostgresNeon · PGlite locallyAI agentsvia MCPonly policycalls PayPalwebhooks
The vision model never calls PayPal and never decides money. Only the policy layer does.

Sale flow

StepWhat happensPayPal
ListSeller photographs the item with a one-time code. Photos are redacted, read, and policy verifies code + identifier.none
PayBuyer approves in the PayPal popup. Price + buyer protection is held.POST /v2/checkout/orders (intent AUTHORIZE), then /authorize
HandoffBuyer scans the item. The model transcribes identifiers without being told the expected value; policy compares.none
ReleaseOnly after a policy match. Capture, then pay the seller their full price./v2/payments/authorizations/:id/capture, POST /v1/payments/payouts
CancelEither 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

  1. One authorization for fee + protection + deposit.
  2. Pickup: the renter records condition and accessories; the fee is captured with final_capture: false, so the deposit stays held.
  3. Return: the owner scans it back; the model compares pickup and return photos against a fixed list of damage categories.
  4. 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

CapabilityUsed for
JS SDK v6PayPal button, one-time payment session
Orders v2 · AUTHORIZEHolds for sales and rentals; redirect approval for agent-started purchases
Payments v2Capture (partial and final), void, reauthorize
PayoutsPaying the seller their full price at release
WebhooksSignature-verified PayPal confirmations in every deal timeline
DisputesAutomatic 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/mcp

Tools: 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

RoutePurpose
POST /api/listingsCreate a listing; returns the seller key and one-time code
POST /api/listings/:id/verifyUpload possession photos (multipart)
POST /api/dealsStart a deal and PayPal order
POST /api/deals/:id/authorizePlace the hold after approval
POST /api/deals/:id/handoff · /releaseSale handoff scan and release
POST /api/deals/:id/pickup · /return · /settleRental steps
POST /api/deals/:id/cancelRelease the hold before capture
GET /api/deals/:id/recordHandoff record PDF (party key)
POST /api/supportOpen a support ticket
POST /api/checkRun the free scam check
GET /api/badge/:idLive 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 tests

Without 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.