Payments & escrow

Secure transactions with escrow protection and seller payouts

Overview

Current state

No in-app payments, transactions happen outside the platform.

Target state

  • In-app checkout with Stripe
  • Escrow protection for buyers
  • Automated seller payouts
  • Platform fees and revenue

Tech stack

  • Payments: Stripe Connect
  • Escrow: Stripe Payment Intents with manual capture
  • Payouts: Stripe Connect Express/Standard
  • Webhooks: Stripe webhook handlers

Flow

Purchase flow

Purchase Flow
Buyer clicks "Buy"
    → Payment Intent created (escrow)
    → Funds held by Stripe
    → Seller ships item
    → Buyer confirms receipt
    → Funds released to seller
    → Platform fee deducted

Dispute flow

Dispute Flow
Buyer opens dispute
    → Funds remain in escrow
    → Admin reviews case
    → Decision: refund buyer OR release to seller

Architecture

Stripe Connect setup

packages/features/payments/stripe-connect.ts
const account = await stripe.accounts.create({
  type: 'express',
  country: 'BR',
  email: seller.email,
  capabilities: {
    transfers: { requested: true },
  },
});

// Store account ID
await db.update(users)
  .set({ stripeAccountId: account.id })
  .where(eq(users.id, seller.id));

Escrow payment

packages/features/payments/escrow.ts
const paymentIntent = await stripe.paymentIntents.create({
  amount: item.price * 100,
  currency: 'brl',
  capture_method: 'manual', // Hold funds
  transfer_data: {
    destination: seller.stripeAccountId,
  },
  application_fee_amount: Math.round(item.price * 0.10 * 100), // 10% fee
  metadata: {
    itemId: item.id,
    buyerId: buyer.id,
    sellerId: seller.id,
  },
});

Release funds

packages/features/payments/actions/release.ts
async function releaseEscrow(transactionId: string) {
  const transaction = await getTransaction(transactionId);

  await stripe.paymentIntents.capture(
    transaction.stripePaymentIntentId
  );

  await db.update(transactions)
    .set({ status: 'completed' })
    .where(eq(transactions.id, transactionId));
}

Refund

packages/features/payments/actions/refund.ts
async function refundBuyer(transactionId: string) {
  const transaction = await getTransaction(transactionId);

  await stripe.paymentIntents.cancel(
    transaction.stripePaymentIntentId
  );

  await db.update(transactions)
    .set({ status: 'refunded' })
    .where(eq(transactions.id, transactionId));
}

Implementation

Stripe Connect

Set up Stripe Connect platform, seller onboarding flow, and KYC/verification handling.

Checkout

Build payment UI components, Payment Intent creation, and order confirmation.

Escrow

Implement manual capture flow, delivery confirmation, and fund release automation.

Disputes

Create dispute opening flow, admin resolution panel, and refund processing.

Pricing model

Fee TypeAmount
Platform fee10% of sale
Stripe fee3.99% + R$0.39
Seller receives~86% of sale

Checklist

Infrastructure

  • Stripe Connect account setup
  • Webhook endpoint for events
  • Transaction table in database

Seller onboarding

  • Connect account creation
  • Onboarding UI flow
  • KYC status tracking
  • Payout settings

Checkout

  • Buy button and checkout modal
  • Payment method selection
  • Order confirmation page
  • Email notifications

Escrow & disputes

  • Delivery confirmation flow
  • Auto-release after X days
  • Dispute opening
  • Admin resolution tools