Payments & Escrow
Stripe Connect escrow — data model, flow and platform fee strategy
Priority: heavier bet — after traction
This is what enables taking a commission (standard C2C monetization), but carries significant compliance work. Prerequisite: company registration.
Why Stripe Connect
The only reasonable choice for a C2C marketplace with escrow:
- Built-in escrow: funds are held until delivery is confirmed
- Handles seller onboarding (KYC/identity verification required by law)
- Supports refunds, disputes, and chargebacks natively
- Handles VAT/tax reporting; platform fee collection is built-in
Data model
model StripeAccount {
id Int @id @default(autoincrement())
userId String @unique
user User @relation(fields: [userId], references: [id])
stripeAccountId String @unique // acct_xxx
onboardingComplete Boolean @default(false)
createdAt DateTime @default(now())
}
model Transaction {
id Int @id @default(autoincrement())
dealId Int @unique
deal Deal @relation(fields: [dealId], references: [id])
buyerId String
buyer User @relation("buyer", fields: [buyerId], references: [id])
sellerId String
seller User @relation("seller", fields: [sellerId], references: [id])
stripePaymentIntent String @unique // pi_xxx
amount Decimal
currency String
platformFee Decimal // our commission
status TransactionStatus
paidAt DateTime?
releasedAt DateTime? // funds released to seller
refundedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum TransactionStatus {
PENDING // payment intent created
PAID // buyer paid, funds held in escrow
DELIVERY_PENDING // awaiting shipping
DELIVERED // tracking confirms delivery
COMPLETED // funds released to seller
DISPUTED // buyer opened dispute
REFUNDED // full or partial refund
CANCELLED // cancelled before payment
}Flow
Seller onboarding. Seller clicks "Start selling" → redirected to Stripe Connect onboarding (Stripe handles KYC: ID, bank account, address). Webhook confirms onboarding → onboardingComplete = true. Seller can't publish deals until onboarding is complete.
Buyer purchases. Backend creates a PaymentIntent with transfer_data.destination = seller's account, application_fee_amount = platform commission, capture_method = manual (hold funds). Buyer pays via Stripe Elements. Webhook payment_intent.succeeded → status PAID.
Escrow period. Funds held by Stripe. Seller ships → enters tracking number; the system monitors tracking via carrier API (delivery spec). On delivery: status DELIVERED.
Release or dispute. After delivery + 48h grace period: auto-capture → funds to seller minus fee. Buyer disputes → status DISPUTED → admin review. Refund full/partial via the Stripe Refund API.
Tasks
Backend: StripeModule; Connect onboarding endpoint (account link) + account.updated webhook; PaymentIntent endpoint (escrow hold) + payment webhooks; capture endpoint (release funds); refund endpoint; transaction REST endpoints (status, history); cron auto-release after 48h post-delivery if no dispute; guard preventing deal publishing without completed onboarding.
Frontend: Stripe Elements payment form; seller onboarding flow (redirect + return page); "Buy Now" on deal page; transaction status page (buyer + seller views); payment history in profile; seller dashboard (earnings, pending payouts).
Platform fee strategy
Decision: flat 5% charged to the buyer on top of the deal price.
Deal price €100 → buyer pays €105 → seller receives €100 → platform keeps €5. Transparent and seller-friendly (the seller always gets what they listed).