Shared payment flow for posters and contractors — wallet management, escrow, payouts, transaction history, and Stripe Connect onboarding.
WalletScreen (poster variant)
useQuery(api.payments.wallet.getPosterSummary)
WalletScreen (contractor variant)
useQuery(api.payments.wallet.getContractorEarnings)
AddPaymentMethodScreen
useMutation(api.stripe.stripe.createSetupIntent)
TransactionHistoryScreen
useQuery(api.payments.financialTransactions.list)
TransactionDetailScreen
useQuery(api.payments.financialTransactions.get)
EscrowDetailScreen
useQuery(api.payments.escrow.getForJob)
InstantPayoutScreen
useMutation(api.stripe.stripe.createInstantPayout)
PaymentFailedScreen
Error state from useMutation(api.payments.escrow.fund)
RefundRequestScreen
useMutation(api.payments.escrow.requestRefund)
StripeConnectOnboardingScreen
useMutation(api.stripe.stripe.createConnectOnboardingLink)
jobId: Id<jobs>
posterId: Id<posterUsers>
contractorId: Id<contractorUsers>
totalBidAmount: number
escrowBalance: number
status: pending | hold_period | funded | first_payout_pending | first_payout_sent | second_payout_pending | released | refunded | disputed
platformFee: number?
contractorNetAmount: number?
holdPeriodEndsAt: number?
firstPayoutAmount/At: number?
secondPayoutAmount/At: number?
version: number? (optimistic lock)
escrowId: Id<escrow>
jobId: Id<jobs>
posterId: Id<posterUsers>
contractorId: Id<contractorUsers>
transactionType: escrow_funding | contractor_payout | platform_fee | refund
transactionDirection: credit | debit
amount: number
description: string
stripePaymentIntentId: string?
stripeTransferId: string?
clerkId: string
userType: "poster" | "contractor"
stripeCustomerId: string
stripeConnectAccountId: string?
subscriptionStatus: string?
defaultPaymentMethodBrand: string?
defaultPaymentMethodLast4: string?
connectChargesEnabled: boolean?
connectPayoutsEnabled: boolean?
connectDetailsSubmitted: boolean?
pending → hold_period (24h) → funded
→ first_payout_pending → first_payout_sent
→ second_payout_pending → released
Any stage can → disputed or refunded
Platform Fee: 6.7% = $281.40
Contractor Net: = $3,918.60
1st Payout: 40% = $1,567.44 (after 24h hold)
2nd Payout: 60% = $2,351.16 (after completion)
Constants: PLATFORM_FEE_RATE = 0.067
FIRST_PAYOUT_RATE = 0.4
HOLD_PERIOD_MS = 86,400,000 (24h)
useQuery(api.payments.wallet.getPosterSummary)
useQuery(api.payments.wallet.getContractorEarnings)
useQuery(api.payments.escrow.getForJob)
useQuery(api.payments.financialTransactions.list)
useQuery(api.payments.financialTransactions.get)
useMutation(api.payments.escrow.fund)
useMutation(api.payments.escrow.requestRefund)
useMutation(api.stripe.stripe.createSetupIntent)
useMutation(api.stripe.stripe.createInstantPayout)
useMutation(api.stripe.stripe.createConnectOnboardingLink)
createCustomer — on user signup
createPaymentIntent — escrow funding
listPaymentMethods — wallet display
createConnectAccount — contractor onboarding
createConnectOnboardingLink — step-through
createTransfer — payout to contractor
constructEvent — webhook verification
Webhooks: payment_intent.succeeded,
account.updated, transfer.created,
charge.refunded
Optimistic locking (version field)
TOCTOU race condition prevention
Stripe webhook signature verification
Idempotency keys on all mutations
Refund deduplication (processedRefundIds)
Rate limiting on public endpoints
PCI compliance via Stripe Elements
escrow: by_job_id, by_poster_id,
by_contractor_id, by_status,
by_stripe_payment_intent
financialTransactions: by_escrow_id,
by_job_id, by_stripe_refund_id
stripeSync: by_clerk_id,
by_stripe_customer_id, by_user