Partner Revenue Sharing for ChatGPT Apps: Affiliate Management

Building a ChatGPT app is only half the battle—getting it into users' hands requires strategic distribution partnerships. Partner revenue sharing programs transform your affiliates, influencers, and strategic partners into a powerful sales force. With the ChatGPT App Store reaching 800 million weekly users, implementing a robust affiliate management system can accelerate your growth exponentially while rewarding those who champion your app.

This comprehensive guide covers everything from commission models and tracking infrastructure to partner portals and payout automation. Whether you're launching your first affiliate program or scaling an existing partnership network, you'll learn production-ready strategies for building a revenue sharing ecosystem that drives sustainable growth. We'll explore tiered commission structures, cross-device attribution, automated payouts, and partner recruitment tactics that top SaaS companies use to build multi-million dollar channel programs.

The key to successful partner revenue sharing isn't just tracking referrals—it's creating a frictionless system that motivates partners, provides transparency, and automates administrative overhead. Let's build an affiliate management system that turns your partners into your most valuable growth engine.

Commission Models

Choosing the right commission structure directly impacts partner motivation and program economics. Different models suit different business objectives and partner types.

Percentage-Based Commissions

Percentage-based commissions scale with transaction value, aligning partner incentives with revenue growth:

// commission-calculator.ts - Production-grade commission engine
import Stripe from 'stripe';
import { Firestore } from '@google-cloud/firestore';

interface CommissionTier {
  name: string;
  minReferrals: number;
  percentage: number;
  recurringMonths: number;
}

interface CommissionCalculation {
  partnerId: string;
  orderId: string;
  orderValue: number;
  commissionAmount: number;
  tierApplied: string;
  isRecurring: boolean;
  recurringPayments: number;
}

export class CommissionCalculator {
  private db: Firestore;
  private stripe: Stripe;

  // Commission tiers based on performance
  private tiers: CommissionTier[] = [
    { name: 'Bronze', minReferrals: 0, percentage: 20, recurringMonths: 3 },
    { name: 'Silver', minReferrals: 10, percentage: 25, recurringMonths: 6 },
    { name: 'Gold', minReferrals: 50, percentage: 30, recurringMonths: 12 },
    { name: 'Platinum', minReferrals: 100, percentage: 35, recurringMonths: 24 }
  ];

  constructor(db: Firestore, stripeKey: string) {
    this.db = db;
    this.stripe = new Stripe(stripeKey, { apiVersion: '2023-10-16' });
  }

  async calculateCommission(
    partnerId: string,
    orderId: string,
    orderValue: number
  ): Promise<CommissionCalculation> {
    // Get partner's performance tier
    const tier = await this.getPartnerTier(partnerId);

    // Calculate base commission
    const commissionAmount = orderValue * (tier.percentage / 100);

    // Determine if this is a subscription (recurring)
    const order = await this.getOrderDetails(orderId);
    const isRecurring = order.type === 'subscription';

    return {
      partnerId,
      orderId,
      orderValue,
      commissionAmount,
      tierApplied: tier.name,
      isRecurring,
      recurringPayments: isRecurring ? tier.recurringMonths : 1
    };
  }

  private async getPartnerTier(partnerId: string): Promise<CommissionTier> {
    const partnerRef = this.db.collection('partners').doc(partnerId);
    const partnerDoc = await partnerRef.get();

    if (!partnerDoc.exists) {
      throw new Error(`Partner ${partnerId} not found`);
    }

    const totalReferrals = partnerDoc.data()?.totalReferrals || 0;

    // Find applicable tier (descending order)
    for (let i = this.tiers.length - 1; i >= 0; i--) {
      if (totalReferrals >= this.tiers[i].minReferrals) {
        return this.tiers[i];
      }
    }

    return this.tiers[0]; // Default to Bronze
  }

  private async getOrderDetails(orderId: string): Promise<any> {
    const orderRef = this.db.collection('orders').doc(orderId);
    const orderDoc = await orderRef.get();

    if (!orderDoc.exists) {
      throw new Error(`Order ${orderId} not found`);
    }

    return orderDoc.data();
  }

  async recordCommission(calculation: CommissionCalculation): Promise<void> {
    const commissionRef = this.db.collection('commissions').doc();

    await commissionRef.set({
      ...calculation,
      status: 'pending',
      createdAt: new Date(),
      paidAt: null,
      paymentId: null
    });

    // Update partner stats
    await this.updatePartnerStats(calculation.partnerId, calculation.commissionAmount);
  }

  private async updatePartnerStats(partnerId: string, amount: number): Promise<void> {
    const partnerRef = this.db.collection('partners').doc(partnerId);

    await this.db.runTransaction(async (transaction) => {
      const partnerDoc = await transaction.get(partnerRef);
      const currentEarnings = partnerDoc.data()?.totalEarnings || 0;
      const currentReferrals = partnerDoc.data()?.totalReferrals || 0;

      transaction.update(partnerRef, {
        totalEarnings: currentEarnings + amount,
        totalReferrals: currentReferrals + 1,
        updatedAt: new Date()
      });
    });
  }

  // Process recurring commission payments
  async processRecurringCommission(
    originalCommissionId: string,
    paymentNumber: number
  ): Promise<void> {
    const originalRef = this.db.collection('commissions').doc(originalCommissionId);
    const originalDoc = await originalRef.get();

    if (!originalDoc.exists) {
      throw new Error('Original commission not found');
    }

    const original = originalDoc.data() as CommissionCalculation;

    // Only process if within recurring period
    if (paymentNumber <= original.recurringPayments) {
      const recurringRef = this.db.collection('commissions').doc();

      await recurringRef.set({
        partnerId: original.partnerId,
        orderId: original.orderId,
        orderValue: original.orderValue,
        commissionAmount: original.commissionAmount,
        tierApplied: original.tierApplied,
        isRecurring: true,
        recurringPaymentNumber: paymentNumber,
        originalCommissionId,
        status: 'pending',
        createdAt: new Date()
      });

      await this.updatePartnerStats(original.partnerId, original.commissionAmount);
    }
  }
}

This calculator handles tiered commissions, recurring payments, and automatic tier upgrades based on performance.

Flat-Rate and Hybrid Models

Flat-rate commissions provide predictability, while hybrid models combine both approaches:

// hybrid-commission-model.ts
interface HybridCommissionConfig {
  baseFlat: number;
  percentageBonus: number;
  minimumPayout: number;
  maximumPayout: number;
}

export class HybridCommissionModel {
  private config: HybridCommissionConfig = {
    baseFlat: 50, // $50 base per conversion
    percentageBonus: 10, // Plus 10% of order value
    minimumPayout: 50,
    maximumPayout: 500
  };

  calculate(orderValue: number): number {
    const flatAmount = this.config.baseFlat;
    const percentageAmount = orderValue * (this.config.percentageBonus / 100);
    const totalCommission = flatAmount + percentageAmount;

    // Apply caps
    return Math.max(
      this.config.minimumPayout,
      Math.min(totalCommission, this.config.maximumPayout)
    );
  }
}

Learn more about ChatGPT app monetization strategies and subscription management for ChatGPT apps.

Affiliate Tracking

Accurate tracking is the foundation of any revenue sharing program. Modern affiliate systems must handle cross-device journeys, multiple touchpoints, and attribution windows.

Cookie-Based Attribution

// affiliate-tracker.ts - Production tracking system
import { Request, Response } from 'express';
import { Firestore, Timestamp } from '@google-cloud/firestore';
import crypto from 'crypto';

interface AffiliateClick {
  clickId: string;
  partnerId: string;
  userId?: string;
  timestamp: Timestamp;
  ip: string;
  userAgent: string;
  referrer: string;
  landingPage: string;
  device: 'mobile' | 'desktop' | 'tablet';
  campaign?: string;
}

interface ConversionEvent {
  conversionId: string;
  clickId: string;
  partnerId: string;
  userId: string;
  orderId: string;
  orderValue: number;
  timestamp: Timestamp;
  attributionModel: 'first-touch' | 'last-touch' | 'linear';
}

export class AffiliateTracker {
  private db: Firestore;
  private cookieName = 'aff_ref';
  private cookieDuration = 30 * 24 * 60 * 60 * 1000; // 30 days

  constructor(db: Firestore) {
    this.db = db;
  }

  // Track affiliate click
  async trackClick(req: Request, res: Response, partnerId: string): Promise<string> {
    const clickId = this.generateClickId();

    const click: AffiliateClick = {
      clickId,
      partnerId,
      timestamp: Timestamp.now(),
      ip: this.getClientIP(req),
      userAgent: req.get('user-agent') || 'unknown',
      referrer: req.get('referer') || 'direct',
      landingPage: req.query.landing as string || '/',
      device: this.detectDevice(req.get('user-agent') || ''),
      campaign: req.query.campaign as string
    };

    // Store click in database
    await this.db.collection('affiliate_clicks').doc(clickId).set(click);

    // Set tracking cookie
    res.cookie(this.cookieName, JSON.stringify({
      clickId,
      partnerId,
      timestamp: Date.now()
    }), {
      maxAge: this.cookieDuration,
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax'
    });

    return clickId;
  }

  // Track conversion event
  async trackConversion(
    userId: string,
    orderId: string,
    orderValue: number,
    req: Request
  ): Promise<ConversionEvent | null> {
    // Get affiliate cookie
    const affiliateCookie = req.cookies[this.cookieName];

    if (!affiliateCookie) {
      return null; // No affiliate attribution
    }

    const { clickId, partnerId, timestamp } = JSON.parse(affiliateCookie);

    // Check if within attribution window
    const clickAge = Date.now() - timestamp;
    if (clickAge > this.cookieDuration) {
      return null; // Attribution expired
    }

    const conversionId = this.generateConversionId();

    const conversion: ConversionEvent = {
      conversionId,
      clickId,
      partnerId,
      userId,
      orderId,
      orderValue,
      timestamp: Timestamp.now(),
      attributionModel: 'last-touch'
    };

    // Store conversion
    await this.db.collection('conversions').doc(conversionId).set(conversion);

    // Update click record
    await this.db.collection('affiliate_clicks').doc(clickId).update({
      converted: true,
      conversionId,
      userId
    });

    return conversion;
  }

  // Multi-touch attribution for complex journeys
  async trackMultiTouchConversion(
    userId: string,
    orderId: string,
    orderValue: number
  ): Promise<ConversionEvent[]> {
    // Get all clicks for this user
    const clicksSnapshot = await this.db
      .collection('affiliate_clicks')
      .where('userId', '==', userId)
      .where('timestamp', '>', Timestamp.fromMillis(Date.now() - this.cookieDuration))
      .orderBy('timestamp', 'asc')
      .get();

    const clicks = clicksSnapshot.docs.map(doc => doc.data() as AffiliateClick);

    if (clicks.length === 0) {
      return [];
    }

    // Linear attribution: split commission equally
    const commissionPerClick = orderValue / clicks.length;

    const conversions: ConversionEvent[] = [];

    for (const click of clicks) {
      const conversionId = this.generateConversionId();

      const conversion: ConversionEvent = {
        conversionId,
        clickId: click.clickId,
        partnerId: click.partnerId,
        userId,
        orderId,
        orderValue: commissionPerClick,
        timestamp: Timestamp.now(),
        attributionModel: 'linear'
      };

      await this.db.collection('conversions').doc(conversionId).set(conversion);
      conversions.push(conversion);
    }

    return conversions;
  }

  private generateClickId(): string {
    return `clk_${crypto.randomBytes(16).toString('hex')}`;
  }

  private generateConversionId(): string {
    return `conv_${crypto.randomBytes(16).toString('hex')}`;
  }

  private getClientIP(req: Request): string {
    return (
      (req.headers['x-forwarded-for'] as string)?.split(',')[0] ||
      req.socket.remoteAddress ||
      'unknown'
    );
  }

  private detectDevice(userAgent: string): 'mobile' | 'desktop' | 'tablet' {
    const ua = userAgent.toLowerCase();

    if (/tablet|ipad/.test(ua)) return 'tablet';
    if (/mobile|android|iphone/.test(ua)) return 'mobile';
    return 'desktop';
  }

  // Generate partner-specific tracking link
  generateTrackingLink(partnerId: string, campaign?: string): string {
    const baseUrl = process.env.APP_URL || 'https://makeaihq.com';
    const params = new URLSearchParams({
      ref: partnerId,
      ...(campaign && { campaign })
    });

    return `${baseUrl}/?${params.toString()}`;
  }
}

This tracker handles cookie-based attribution, multi-touch attribution, and device detection across the user journey.

Partner Portal

A self-service partner portal empowers affiliates with real-time analytics, marketing resources, and payout transparency:

// partner-portal.tsx - React component for partner dashboard
import React, { useState, useEffect } from 'react';
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
import { Firestore } from '@google-cloud/firestore';

interface PartnerStats {
  totalClicks: number;
  totalConversions: number;
  conversionRate: number;
  totalEarnings: number;
  pendingPayouts: number;
  paidOut: number;
  currentTier: string;
  nextTierReferrals: number;
}

interface PerformanceData {
  date: string;
  clicks: number;
  conversions: number;
  earnings: number;
}

export const PartnerPortal: React.FC<{ partnerId: string }> = ({ partnerId }) => {
  const [stats, setStats] = useState<PartnerStats | null>(null);
  const [performanceData, setPerformanceData] = useState<PerformanceData[]>([]);
  const [trackingLinks, setTrackingLinks] = useState<string[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadPartnerData();
  }, [partnerId]);

  const loadPartnerData = async () => {
    setLoading(true);

    // Fetch partner stats
    const statsData = await fetchPartnerStats(partnerId);
    setStats(statsData);

    // Fetch performance history (last 30 days)
    const performanceHistory = await fetchPerformanceHistory(partnerId, 30);
    setPerformanceData(performanceHistory);

    // Generate tracking links
    const links = generateTrackingLinks(partnerId);
    setTrackingLinks(links);

    setLoading(false);
  };

  const fetchPartnerStats = async (partnerId: string): Promise<PartnerStats> => {
    // In production, this would call your API
    const response = await fetch(`/api/partners/${partnerId}/stats`);
    return response.json();
  };

  const fetchPerformanceHistory = async (
    partnerId: string,
    days: number
  ): Promise<PerformanceData[]> => {
    const response = await fetch(`/api/partners/${partnerId}/performance?days=${days}`);
    return response.json();
  };

  const generateTrackingLinks = (partnerId: string): string[] => {
    return [
      `https://makeaihq.com/?ref=${partnerId}`,
      `https://makeaihq.com/pricing?ref=${partnerId}&campaign=pricing`,
      `https://makeaihq.com/templates?ref=${partnerId}&campaign=templates`
    ];
  };

  const copyToClipboard = (text: string) => {
    navigator.clipboard.writeText(text);
    alert('Tracking link copied to clipboard!');
  };

  if (loading) {
    return <div className="loading">Loading partner dashboard...</div>;
  }

  return (
    <div className="partner-portal">
      <header className="portal-header">
        <h1>Partner Dashboard</h1>
        <div className="tier-badge">{stats?.currentTier} Partner</div>
      </header>

      <div className="stats-grid">
        <div className="stat-card">
          <h3>Total Earnings</h3>
          <div className="stat-value">${stats?.totalEarnings.toLocaleString()}</div>
          <div className="stat-meta">
            Pending: ${stats?.pendingPayouts.toLocaleString()}
          </div>
        </div>

        <div className="stat-card">
          <h3>Conversions</h3>
          <div className="stat-value">{stats?.totalConversions}</div>
          <div className="stat-meta">
            {stats?.conversionRate.toFixed(2)}% conversion rate
          </div>
        </div>

        <div className="stat-card">
          <h3>Clicks</h3>
          <div className="stat-value">{stats?.totalClicks.toLocaleString()}</div>
          <div className="stat-meta">
            {stats?.nextTierReferrals} more for next tier
          </div>
        </div>

        <div className="stat-card">
          <h3>Paid Out</h3>
          <div className="stat-value">${stats?.paidOut.toLocaleString()}</div>
        </div>
      </div>

      <div className="performance-charts">
        <h2>Performance Trends</h2>

        <div className="chart-container">
          <h3>Earnings Over Time</h3>
          <LineChart width={600} height={300} data={performanceData}>
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis dataKey="date" />
            <YAxis />
            <Tooltip />
            <Legend />
            <Line type="monotone" dataKey="earnings" stroke="#D4AF37" name="Earnings ($)" />
          </LineChart>
        </div>

        <div className="chart-container">
          <h3>Clicks vs Conversions</h3>
          <BarChart width={600} height={300} data={performanceData}>
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis dataKey="date" />
            <YAxis />
            <Tooltip />
            <Legend />
            <Bar dataKey="clicks" fill="#0A0E27" name="Clicks" />
            <Bar dataKey="conversions" fill="#D4AF37" name="Conversions" />
          </BarChart>
        </div>
      </div>

      <div className="tracking-links">
        <h2>Your Tracking Links</h2>
        {trackingLinks.map((link, index) => (
          <div key={index} className="link-item">
            <input type="text" value={link} readOnly />
            <button onClick={() => copyToClipboard(link)}>Copy</button>
          </div>
        ))}
      </div>

      <div className="marketing-resources">
        <h2>Marketing Resources</h2>
        <div className="resource-grid">
          <a href="/partners/banners" className="resource-card">Banner Ads</a>
          <a href="/partners/email-templates" className="resource-card">Email Templates</a>
          <a href="/partners/social-media" className="resource-card">Social Media Kit</a>
          <a href="/partners/case-studies" className="resource-card">Case Studies</a>
        </div>
      </div>
    </div>
  );
};

Explore analytics for ChatGPT apps and user onboarding for ChatGPT apps.

Payout Automation

Automating payouts reduces administrative overhead while ensuring partners get paid on time:

// payout-manager.ts - Automated payout processing
import Stripe from 'stripe';
import { Firestore, Timestamp } from '@google-cloud/firestore';

interface PayoutSchedule {
  frequency: 'weekly' | 'biweekly' | 'monthly';
  minimumThreshold: number;
  nextPayoutDate: Date;
}

interface PayoutBatch {
  batchId: string;
  partnerId: string;
  amount: number;
  commissionIds: string[];
  status: 'pending' | 'processing' | 'completed' | 'failed';
  createdAt: Timestamp;
  processedAt?: Timestamp;
  stripeTransferId?: string;
}

export class PayoutManager {
  private db: Firestore;
  private stripe: Stripe;
  private defaultSchedule: PayoutSchedule = {
    frequency: 'monthly',
    minimumThreshold: 100, // $100 minimum payout
    nextPayoutDate: this.getNextPayoutDate('monthly')
  };

  constructor(db: Firestore, stripeKey: string) {
    this.db = db;
    this.stripe = new Stripe(stripeKey, { apiVersion: '2023-10-16' });
  }

  // Generate payout batches for all partners
  async generatePayoutBatches(): Promise<PayoutBatch[]> {
    const partnersSnapshot = await this.db.collection('partners').get();
    const batches: PayoutBatch[] = [];

    for (const partnerDoc of partnersSnapshot.docs) {
      const partnerId = partnerDoc.id;
      const schedule = partnerDoc.data().payoutSchedule || this.defaultSchedule;

      // Check if it's time for payout
      if (new Date() < schedule.nextPayoutDate) {
        continue;
      }

      // Get pending commissions
      const commissionsSnapshot = await this.db
        .collection('commissions')
        .where('partnerId', '==', partnerId)
        .where('status', '==', 'pending')
        .get();

      const commissions = commissionsSnapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data()
      }));

      // Calculate total amount
      const totalAmount = commissions.reduce(
        (sum, commission) => sum + commission.commissionAmount,
        0
      );

      // Check minimum threshold
      if (totalAmount < schedule.minimumThreshold) {
        continue;
      }

      // Create payout batch
      const batch: PayoutBatch = {
        batchId: this.generateBatchId(),
        partnerId,
        amount: totalAmount,
        commissionIds: commissions.map(c => c.id),
        status: 'pending',
        createdAt: Timestamp.now()
      };

      await this.db.collection('payout_batches').doc(batch.batchId).set(batch);
      batches.push(batch);
    }

    return batches;
  }

  // Process payout batch via Stripe
  async processPayout(batchId: string): Promise<void> {
    const batchRef = this.db.collection('payout_batches').doc(batchId);
    const batchDoc = await batchRef.get();

    if (!batchDoc.exists) {
      throw new Error(`Payout batch ${batchId} not found`);
    }

    const batch = batchDoc.data() as PayoutBatch;

    try {
      // Update status to processing
      await batchRef.update({ status: 'processing' });

      // Get partner's Stripe Connect account
      const partnerDoc = await this.db
        .collection('partners')
        .doc(batch.partnerId)
        .get();

      const stripeAccountId = partnerDoc.data()?.stripeConnectAccountId;

      if (!stripeAccountId) {
        throw new Error('Partner has no Stripe Connect account');
      }

      // Create Stripe transfer
      const transfer = await this.stripe.transfers.create({
        amount: Math.round(batch.amount * 100), // Convert to cents
        currency: 'usd',
        destination: stripeAccountId,
        description: `Affiliate payout - Batch ${batchId}`
      });

      // Update batch and commissions
      await this.db.runTransaction(async (transaction) => {
        // Mark batch as completed
        transaction.update(batchRef, {
          status: 'completed',
          processedAt: Timestamp.now(),
          stripeTransferId: transfer.id
        });

        // Mark commissions as paid
        for (const commissionId of batch.commissionIds) {
          const commissionRef = this.db.collection('commissions').doc(commissionId);
          transaction.update(commissionRef, {
            status: 'paid',
            paidAt: Timestamp.now(),
            paymentBatchId: batchId
          });
        }
      });

      // Update partner's next payout date
      const schedule = partnerDoc.data()?.payoutSchedule || this.defaultSchedule;
      await this.db.collection('partners').doc(batch.partnerId).update({
        'payoutSchedule.nextPayoutDate': this.getNextPayoutDate(schedule.frequency),
        totalPaidOut: (partnerDoc.data()?.totalPaidOut || 0) + batch.amount
      });

    } catch (error) {
      // Mark batch as failed
      await batchRef.update({
        status: 'failed',
        errorMessage: (error as Error).message,
        processedAt: Timestamp.now()
      });

      throw error;
    }
  }

  private getNextPayoutDate(frequency: 'weekly' | 'biweekly' | 'monthly'): Date {
    const now = new Date();
    const next = new Date(now);

    switch (frequency) {
      case 'weekly':
        next.setDate(now.getDate() + 7);
        break;
      case 'biweekly':
        next.setDate(now.getDate() + 14);
        break;
      case 'monthly':
        next.setMonth(now.getMonth() + 1);
        next.setDate(1); // First of next month
        break;
    }

    return next;
  }

  private generateBatchId(): string {
    return `batch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  // Generate tax report for partners
  async generateTaxReport(partnerId: string, year: number): Promise<any> {
    const startDate = new Date(year, 0, 1);
    const endDate = new Date(year, 11, 31, 23, 59, 59);

    const commissionsSnapshot = await this.db
      .collection('commissions')
      .where('partnerId', '==', partnerId)
      .where('status', '==', 'paid')
      .where('paidAt', '>=', Timestamp.fromDate(startDate))
      .where('paidAt', '<=', Timestamp.fromDate(endDate))
      .get();

    const totalEarnings = commissionsSnapshot.docs.reduce(
      (sum, doc) => sum + doc.data().commissionAmount,
      0
    );

    return {
      partnerId,
      year,
      totalEarnings,
      paymentCount: commissionsSnapshot.size,
      generatedAt: new Date()
    };
  }
}

Review payment processing for ChatGPT apps and Stripe integration for ChatGPT apps.

Partner Recruitment

Building a high-performing partner network requires strategic recruitment and relationship management:

// partner-recruitment.ts - Automated recruitment system
import { Firestore, Timestamp } from '@google-cloud/firestore';
import nodemailer from 'nodemailer';

interface PartnerApplication {
  applicationId: string;
  email: string;
  name: string;
  website?: string;
  socialMedia: {
    twitter?: string;
    linkedin?: string;
    youtube?: string;
  };
  audience: string;
  estimatedReach: number;
  experience: string;
  status: 'pending' | 'approved' | 'rejected';
  submittedAt: Timestamp;
}

export class PartnerRecruitment {
  private db: Firestore;
  private emailer: nodemailer.Transporter;

  constructor(db: Firestore, emailConfig: any) {
    this.db = db;
    this.emailer = nodemailer.createTransport(emailConfig);
  }

  async submitApplication(data: Omit<PartnerApplication, 'applicationId' | 'status' | 'submittedAt'>): Promise<string> {
    const applicationId = this.generateApplicationId();

    const application: PartnerApplication = {
      applicationId,
      ...data,
      status: 'pending',
      submittedAt: Timestamp.now()
    };

    await this.db.collection('partner_applications').doc(applicationId).set(application);

    // Send confirmation email
    await this.sendApplicationConfirmation(application);

    return applicationId;
  }

  async approveApplication(applicationId: string): Promise<void> {
    const appRef = this.db.collection('partner_applications').doc(applicationId);
    const appDoc = await appRef.get();

    if (!appDoc.exists) {
      throw new Error('Application not found');
    }

    const application = appDoc.data() as PartnerApplication;

    // Create partner account
    const partnerId = await this.createPartnerAccount(application);

    // Update application status
    await appRef.update({
      status: 'approved',
      partnerId,
      approvedAt: Timestamp.now()
    });

    // Send welcome email with credentials
    await this.sendWelcomeEmail(application, partnerId);
  }

  private async createPartnerAccount(application: PartnerApplication): Promise<string> {
    const partnerId = this.generatePartnerId();

    await this.db.collection('partners').doc(partnerId).set({
      partnerId,
      email: application.email,
      name: application.name,
      website: application.website,
      socialMedia: application.socialMedia,
      tier: 'Bronze',
      totalReferrals: 0,
      totalEarnings: 0,
      totalPaidOut: 0,
      createdAt: Timestamp.now()
    });

    return partnerId;
  }

  private async sendApplicationConfirmation(application: PartnerApplication): Promise<void> {
    await this.emailer.sendMail({
      from: 'partners@makeaihq.com',
      to: application.email,
      subject: 'Partner Application Received',
      html: `
        <h1>Thank you for applying!</h1>
        <p>Hi ${application.name},</p>
        <p>We've received your application to join the MakeAIHQ Partner Program.</p>
        <p>Our team will review your application and get back to you within 2-3 business days.</p>
      `
    });
  }

  private async sendWelcomeEmail(application: PartnerApplication, partnerId: string): Promise<void> {
    const loginUrl = `https://partners.makeaihq.com/login`;

    await this.emailer.sendMail({
      from: 'partners@makeaihq.com',
      to: application.email,
      subject: 'Welcome to MakeAIHQ Partner Program!',
      html: `
        <h1>Welcome ${application.name}!</h1>
        <p>Congratulations! Your application has been approved.</p>
        <p><strong>Your Partner ID:</strong> ${partnerId}</p>
        <p><strong>Login:</strong> <a href="${loginUrl}">${loginUrl}</a></p>
        <h2>Next Steps:</h2>
        <ul>
          <li>Login to your partner dashboard</li>
          <li>Set up your payment details</li>
          <li>Generate your tracking links</li>
          <li>Download marketing materials</li>
        </ul>
        <p>Questions? Reply to this email or visit our Partner FAQ.</p>
      `
    });
  }

  private generateApplicationId(): string {
    return `app_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  private generatePartnerId(): string {
    return `partner_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

Database Schema

Complete schema for partner revenue sharing:

-- partners table
CREATE TABLE partners (
  partner_id VARCHAR(255) PRIMARY KEY,
  email VARCHAR(255) UNIQUE NOT NULL,
  name VARCHAR(255) NOT NULL,
  website VARCHAR(500),
  stripe_connect_account_id VARCHAR(255),
  tier VARCHAR(50) DEFAULT 'Bronze',
  total_referrals INTEGER DEFAULT 0,
  total_earnings DECIMAL(10, 2) DEFAULT 0,
  total_paid_out DECIMAL(10, 2) DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_email (email),
  INDEX idx_tier (tier)
);

-- affiliate_clicks table
CREATE TABLE affiliate_clicks (
  click_id VARCHAR(255) PRIMARY KEY,
  partner_id VARCHAR(255) NOT NULL,
  user_id VARCHAR(255),
  ip_address VARCHAR(45),
  user_agent TEXT,
  referrer VARCHAR(500),
  landing_page VARCHAR(500),
  device VARCHAR(50),
  campaign VARCHAR(255),
  converted BOOLEAN DEFAULT FALSE,
  conversion_id VARCHAR(255),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (partner_id) REFERENCES partners(partner_id),
  INDEX idx_partner (partner_id),
  INDEX idx_user (user_id),
  INDEX idx_created (created_at)
);

-- commissions table
CREATE TABLE commissions (
  commission_id VARCHAR(255) PRIMARY KEY,
  partner_id VARCHAR(255) NOT NULL,
  order_id VARCHAR(255) NOT NULL,
  order_value DECIMAL(10, 2) NOT NULL,
  commission_amount DECIMAL(10, 2) NOT NULL,
  tier_applied VARCHAR(50),
  is_recurring BOOLEAN DEFAULT FALSE,
  recurring_payment_number INTEGER,
  status VARCHAR(50) DEFAULT 'pending',
  paid_at TIMESTAMP,
  payment_batch_id VARCHAR(255),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (partner_id) REFERENCES partners(partner_id),
  INDEX idx_partner_status (partner_id, status),
  INDEX idx_status (status),
  INDEX idx_created (created_at)
);

-- payout_batches table
CREATE TABLE payout_batches (
  batch_id VARCHAR(255) PRIMARY KEY,
  partner_id VARCHAR(255) NOT NULL,
  amount DECIMAL(10, 2) NOT NULL,
  commission_count INTEGER NOT NULL,
  status VARCHAR(50) DEFAULT 'pending',
  stripe_transfer_id VARCHAR(255),
  error_message TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  processed_at TIMESTAMP,
  FOREIGN KEY (partner_id) REFERENCES partners(partner_id),
  INDEX idx_partner (partner_id),
  INDEX idx_status (status)
);

See also database schema design for ChatGPT apps and Firestore security rules for ChatGPT apps.

Conclusion

Partner revenue sharing transforms passive affiliates into active growth drivers. By implementing tiered commissions, accurate attribution, self-service portals, and automated payouts, you create a partnership ecosystem that scales alongside your ChatGPT app business.

The key differentiators are transparency and automation—partners who can track their performance in real-time and receive payouts automatically are more engaged and motivated. Whether you're starting with a handful of influencers or building a network of hundreds of partners, the infrastructure we've covered provides the foundation for sustainable, profitable growth.

Ready to build a partner revenue sharing program? Start your free trial with MakeAIHQ and deploy ChatGPT apps with built-in affiliate tracking, commission management, and partner portals—no coding required.

For deeper insights on monetization strategies, explore our guides on freemium models for ChatGPT apps, tiered pricing strategies, and ROI tracking for ChatGPT apps.


Related Resources: