Developer Docs

Revenue Sharing & Payouts

Mallnline operates as a marketplace where Malet Owners sell through the platform. The Revenue Sharing infrastructure handles automatic commission splitting on every Murchase, routing funds to sellers via Stripe Connect while retaining a tier-based platform fee.

Architecture Overview

Revenue sharing spans two subgraphs:

Subgraph Entity Responsibility
payments ConnectedAccount Stripe Connect Express onboarding, payout scheduling, balance tracking
murchases CommissionLedger Per-Murchase commission calculation, earnings aggregation, refund clawback
Buyer โ†’ Stripe (Destination Charge) โ†’ Seller's Connected Account
                                    โ†ณ Application Fee โ†’ Mallnline Platform

ConnectedAccount Entity

Each Malet has at most one active ConnectedAccount, linking it to a Stripe Connect Express account.

type ConnectedAccount {
  id: ID!
  maletId: ID!
  userId: ID!
  provider: PayoutProvider!          # STRIPE_CONNECT (future: PAYSTACK, MPESA, MANUAL)
  providerAccountId: String          # e.g. acct_xxx
  status: ConnectedAccountStatus!    # PENDING โ†’ ONBOARDING โ†’ ACTIVE โ†’ SUSPENDED โ†’ DEACTIVATED
  payoutSchedule: PayoutSchedule
  rollingReservePercent: Float!      # Default 10% for new sellers
  reserveExpiresAt: DateTime
  totalEarnings: Int!                # Lifetime (cents)
  totalPayouts: Int!                 # Lifetime (cents)
  pendingBalance: Int!               # Awaiting next payout (cents)
  reserveBalance: Int!               # Held in reserve (cents)
  currency: String!
  onboardingUrl: String              # Stripe-hosted, expires after use
  kycVerified: Boolean!
}

Account Lifecycle

PENDING โ†’ ONBOARDING โ†’ ACTIVE โ†’ SUSPENDED โ†’ DEACTIVATED
   โ†‘          โ†‘           โ†‘          โ†‘
   |          |           |          โ””โ”€โ”€ Chargeback threshold exceeded
   |          |           โ””โ”€โ”€ charges_enabled + payouts_enabled (webhook)
   |          โ””โ”€โ”€ Seller started Stripe Express flow
   โ””โ”€โ”€ Account record created

Stripe Connect Express Onboarding

// 1. Malet Owner clicks "Set Up Payments"
mutation {
  createConnectedAccount(input: { maletId: "..." }) {
    id
    onboardingUrl    # Redirect user here
    status           # PENDING โ†’ ONBOARDING
  }
}

// 2. Stripe sends account.updated webhook
// WebhookService delegates to ConnectedAccountService.handleAccountUpdated()
// If charges_enabled + payouts_enabled โ†’ status transitions to ACTIVE

// 3. If link expires, refresh it
mutation {
  refreshOnboardingLink(connectedAccountId: "...") {
    onboardingUrl
  }
}

CommissionLedger Entity

Every Murchase generates a CommissionLedger entry recording the exact commission breakdown.

type CommissionLedger {
  id: ID!
  orderId: ID!
  maletId: ID!
  grossAmount: Int!          # Total sale (cents)
  commissionRate: Float!     # e.g. 0.08 (8%)
  commissionAmount: Int!     # Platform take (cents)
  processingFee: Int!        # Stripe processing fee estimate
  netPayout: Int!            # Seller receives (cents)
  reserveAmount: Int!        # Held back for rolling reserve
  currency: String!
  status: LedgerStatus!      # PENDING โ†’ SETTLED โ†’ REFUNDED โ†’ PARTIALLY_REFUNDED
  refundedAmount: Int
  refundedCommission: Int
  settlementDate: DateTime
}

Commission Calculation

Commission rates are resolved dynamically from the subscription tier โ€” never hardcoded:

import { getTierLimits } from '@app/common/config/tier-limits';

const tier = 'pro';
const { commissionRate } = getTierLimits(tier);
// commissionRate = 0.05 (5%)
Tier Commission Rate Platform Take on $100 Sale
Starter 8% $8.00
Pro 5% $5.00
Enterprise 3% $3.00

Detailed Breakdown

Gross Sale:        $100.00
- Commission (8%): -$8.00
- Processing Fee:  -$3.20  (2.9% + $0.30)
- Reserve (10%):   -$8.88  (10% of net, held 30 days)
= Net Payout:      $79.92

Processing fee constants are stored as module-level variables in CommissionService:

const PROCESSING_FEE_RATE = 0.029;        // 2.9%
const PROCESSING_FEE_FIXED = 30;          // $0.30 in cents
const PLATFORM_PROCESSING_MARGIN_RATE = 0; // Platform absorbs 0% of processing

Stripe Destination Charges

The StripeHandler uses Destination Charges to route funds:

const paymentIntent = await stripe.paymentIntents.create({
  amount: 10000,           // $100.00
  currency: 'usd',
  transfer_data: {
    destination: 'acct_xxx',  // Seller's connected account
  },
  application_fee_amount: 800,  // $8.00 platform fee
  metadata: { orderId: '...' },
});

The Transaction model stores Connect metadata:

connectedAccountId?: string;   // acct_xxx
applicationFeeAmount?: number; // 800 (cents)
transferId?: string;           // tr_xxx

Rolling Reserve

New sellers have a 10% rolling reserve that holds back a portion of each payout for 30 days to protect against chargebacks.

Condition Reserve % Duration
New seller (default) 10% 90 days from first sale
Enterprise tier 0% N/A
After maturity 0% Reserve lifts automatically

The reserveExpiresAt field on ConnectedAccount tracks when the reserve period ends.

Refund Commission Clawback

When a Murchase is cancelled, the platform proportionally returns the commission to the seller:

// OrderService.updateStatus() โ€” on CANCELLED:
await this.commissionService.handleRefund(orderId, refundAmount);

The clawback is proportional โ€” a 40% partial refund returns 40% of the commission:

Original: Gross=$100, Commission=$8, Net=$92
40% Refund: RefundedAmount=$40, RefundedCommission=$3.20
Updated:  Commission=$4.80, Net=$55.20

RBAC Permissions

Permission Scope Usage
VIEW_PAYOUTS Read connected account, earnings summary Malet Owners
MANAGE_PAYOUTS Create/update/deactivate connected accounts Malet Owners
VIEW_REVENUE Read commission ledger Malet Owners
MANAGE_COMMISSIONS Admin commission adjustments Platform Admins

Webhook Events

The payments webhook controller handles these Stripe Connect events:

Event Handler Action
account.updated handleAccountUpdated() Transition ONBOARDING โ†’ ACTIVE when verified
transfer.created handleTransferCreated() Log payout transfer for audit

GraphQL API

Queries

# Get payout account for a Malet
query {
  connectedAccount(maletId: "...") {
    status
    pendingBalance
    totalEarnings
    payoutSchedule { interval dayOfWeek }
  }
}

# Get earnings summary (aggregated)
query {
  earningsSummary(maletId: "...") {
    totalGross
    totalCommission
    totalNetPayouts
    pendingBalance
    reserveBalance
  }
}

# Get commission history (paginated)
query {
  commissionLedger(maletId: "...", limit: 20, offset: 0) {
    orderId
    grossAmount
    commissionRate
    netPayout
    status
  }
}

Mutations

# Start Stripe Connect onboarding
mutation {
  createConnectedAccount(input: { maletId: "..." }) {
    onboardingUrl
  }
}

# Update payout schedule
mutation {
  updatePayoutSchedule(input: {
    connectedAccountId: "..."
    interval: WEEKLY
    dayOfWeek: 1
  }) { id payoutSchedule { interval dayOfWeek } }
}

Frontend Dashboard

The Malet Owner earnings dashboard is located at /{maletHandle}/manage/payouts and includes:

  • PayoutOverview โ€” KPI cards (total earnings, pending balance, next payout, commission rate)
  • StripeConnectOnboarding โ€” Gradient CTA card for first-time Stripe Express setup
  • CommissionTable โ€” Paginated per-Murchase commission history with status pills
  • PayoutSettings โ€” Schedule configuration, KYC status, rolling reserve info

The dashboard conditionally renders the onboarding flow for new accounts or the full dashboard for active accounts.

Provider Abstraction

The ConnectedAccount model includes a PayoutProvider enum supporting future payment providers:

enum PayoutProvider {
  STRIPE_CONNECT      // Phase 1 (current)
  PAYSTACK_SUBACCOUNT // Phase 2 (West Africa)
  MPESA_DIRECT        // Phase 3 (East Africa)
  MANUAL              // Fallback
}

The PaymentHandler interface accepts optional ConnectPaymentOptions to keep the payment flow provider-agnostic.