---
title: "Commission Rules and Rewards"
description: "The Commission Rules and Rewards engine manages partner compensation structures by defining flexible earning models, evaluation criteria, and automated payout pipelines across referral programs. It..."
last_updated: "2026-10-05T05:07:35.16912+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/commission-rules-and-rewards"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/lib/partner-referrals/create-referral-commission.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partner-referrals/create-referral-commission.ts)
- [apps/web/app/ee/api/workflows/create-partner-commission/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts)
- [apps/web/lib/zod/schemas/rewards.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts)
- [apps/web/app/ee/api/commissions/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/commissions/route.ts)
- [apps/web/lib/jobs/handlers/create-custom-commission-job.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/create-custom-commission-job.ts)
- [apps/web/app/ee/admin.dub.co/dashboard/commissions/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/page.tsx)
- [apps/web/app/ee/api/cron/rewards/queue-custom-commissions/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/rewards/queue-custom-commissions/route.ts)
- [apps/web/app/ee/app.dub.co/embed/referrals/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/app.dub.co/embed/referrals/page-client.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/groups/groupSlug/rewards/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/groups/%5BgroupSlug%5D/rewards/page.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/program/commissions/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/program/commissions/page.tsx)
- [apps/web/lib/api/sales/construct-reward-amount.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/sales/construct-reward-amount.ts)
- [apps/web/lib/partner-referrals/create-network-referral-commission.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partner-referrals/create-network-referral-commission.ts)
- [apps/web/lib/api/rewards/custom-reward-utils.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts)
- [apps/web/lib/rewardful/import-commissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/rewardful/import-commissions.ts)
- [apps/web/lib/lemonsqueezy/import-commissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/lemonsqueezy/import-commissions.ts)
- [apps/web/lib/api/sales/calculate-sale-earnings.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/sales/calculate-sale-earnings.ts)
- [apps/web/ui/partners/rewards/rewards-logic.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/rewards/rewards-logic.tsx)
- [apps/web/lib/partners/determine-partner-reward.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts)
- [apps/web/lib/partnerstack/import-commissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partnerstack/import-commissions.ts)
- [apps/web/app/app.dub.co/onboarding/onboarding/steps/program/reward/form.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/program/reward/form.tsx)
- [apps/web/lib/api/rewards/create-custom-reward-commissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/create-custom-reward-commissions.ts)
- [apps/web/app/ee/api/cron/payouts/aggregate-due-commissions/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts)
- [apps/web/lib/api/commissions/update-partner-commission.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/update-partner-commission.ts)
- [apps/web/ui/partners/rewards/reward-event-descriptions.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/rewards/reward-event-descriptions.tsx)
- [apps/web/lib/firstpromoter/import-commissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/firstpromoter/import-commissions.ts)
- [apps/web/scripts/customers/beehiiv/fix-case-a-complex.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/customers/beehiiv/fix-case-a-complex.ts)
- [apps/web/lib/api/commissions/create-manual-commissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/create-manual-commissions.ts)
- [apps/web/app/ee/app.dub.co/embed/referrals/earnings.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/app.dub.co/embed/referrals/earnings.tsx)
- [apps/web/scripts/customers/framer/tally-commissions.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/customers/framer/tally-commissions.ts)
- [apps/web/ui/partners/custom-reward-description.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/custom-reward-description.tsx)
</details>

## Overview

The Commission Rules and Rewards engine manages partner compensation structures by defining flexible earning models, evaluation criteria, and automated payout pipelines across referral programs. It solves the complexity of multi-tiered affiliate incentives by supporting both percentage-based revenue shares and flat-fee payouts, combined with conditional logic modifiers based on partner performance metrics, geographic attributes, or specific product IDs. The system ensures precise financial tracking through automated asynchronous workflows that enforce spend limits, duration caps, and strict eligibility checks prior to recording commissions. Furthermore, it integrates cron-driven periodic execution for custom retainers alongside native reconciliation mechanisms for importing foreign platform commissions, providing comprehensive administrative control over partner earnings and lifecycle adjustments. Sources: [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:366-408](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L366-L408), [apps/web/lib/zod/schemas/rewards.ts:113-173](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts#L113-L173), [apps/web/lib/partners/determine-partner-reward.ts:118-150](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L118-L150), and [apps/web/lib/api/rewards/create-custom-reward-commissions.ts:150-178](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/create-custom-reward-commissions.ts#L150-L178)

## Reward Configuration and Schema Rules

### Overview

The reward configuration and schema validation engine structures partner compensation parameters using Zod schemas and Prisma models. Payout structures divide into two primary models: flat incentives and percentage revenue shares, defined alongside commission types that determine whether payouts run as recurring ongoing rewards or single one-off transactions. Sources: [apps/web/lib/zod/schemas/rewards.ts:19-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts#L19-L32), [apps/web/ui/partners/rewards/rewards-logic.tsx:74-83](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/rewards/rewards-logic.tsx#L74-L83)

### Event Types and Commission Structures

The system supports five distinct event types for triggering compensation: sale, lead, click, referral, and custom. Each event type maintains specialized metadata and target use cases, ranging from high-DR traffic publishers to multi-month B2B sales cycles and recurring retainers. Sources: [apps/web/ui/partners/rewards/reward-event-descriptions.tsx:11-60](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/rewards/reward-event-descriptions.tsx#L11-L60)

| Event Type | Title | Description | Best For |
| :--- | :--- | :--- | :--- |
| `sale` | Sale reward | Reward when revenue is generated | Partners, creators, and long term partnerships |
| `lead` | Lead reward | Reward for sign ups or demos | B2B, demos, waitlists, or products with longer sales cycles |
| `click` | Click reward | Reward for traffic and reach | Publishers with high DR sites and trusted partners only |
| `referral` | Partner referral reward | Reward when partners refer more partners | Driving partner growth to your program |
| `custom` | Custom reward | Pay a fixed amount on a regular cadence | Retainers and scheduled partner payments |

Sources: [apps/web/ui/partners/rewards/reward-event-descriptions.tsx:21-59](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/partners/rewards/reward-event-descriptions.tsx#L21-L59)

### Commission Types and Duration Parameters

When configuring sale rewards, administrators choose between two commission structures: recurring ongoing payouts or one-off single payouts. Selecting recurring rewards enables duration configuration options, defaulting to lifetime or bounded month intervals. Sources: [apps/web/lib/zod/schemas/rewards.ts:19-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts#L19-L32), [apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/program/reward/form.tsx:254-354](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/program/reward/form.tsx#L254-L354)

> [!NOTE]
> Values of `0` and `1` month intervals are automatically filtered out from selectable recurring max durations in the form UI, as 1-month periods are restricted exclusively to discount rules.
> Sources: [apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/program/reward/form.tsx:346-347](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/program/reward/form.tsx#L346-L347)

### Reward Condition Entities and Attributes

Reward conditions evaluate nested entity attributes across four distinct domain entities: `partner`, `customer`, `sale`, and `lead`. Each entity exposes typed attributes for constructing rule modifiers and criteria groups. Sources: [apps/web/lib/zod/schemas/rewards.ts:34-111](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts#L34-L111)

- **Partner Entity**: Evaluates `country`, `totalClicks`, `totalLeads`, `totalConversions`, `totalSaleAmount` (currency), and `totalCommissions` (currency). Sources: [apps/web/lib/zod/schemas/rewards.ts:64-98](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts#L64-L98)
- **Customer Entity**: Evaluates `country`, alongside event-specific source channels such as tracked APIs, submitted partner leads, Stripe free trials, and HubSpot integrations. Sources: [apps/web/lib/zod/schemas/rewards.ts:101-111](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts#L101-L111), [apps/web/lib/zod/schemas/rewards.ts:131-167](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts#L131-L167)
- **Sale Entity**: Evaluates `productId`, `amount` (currency), `type` (new versus recurring), subscription duration months, subscription start dates, and signup dates. Sources: [apps/web/lib/zod/schemas/rewards.ts:229-253](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts#L229-L253)
- **Lead Entity**: Evaluates metadata attributes. Sources: [apps/web/lib/zod/schemas/rewards.ts:52-62](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/zod/schemas/rewards.ts#L52-L62)

## Commission Calculation and Reward Determination

### Overview

Evaluating commission rules and determining final reward amounts involves resolving context-specific modifiers, checking event column mappings, and applying rate structures against sales volumes. When an event occurs, the system maps the event type (`click`, `lead`, or `sale`) to its corresponding reward database column using `REWARD_EVENT_COLUMN_MAPPING`. Sources: [apps/web/lib/partners/determine-partner-reward.ts:14-18](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L14-L18), [apps/web/lib/partners/determine-partner-reward.ts:89](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L89)

### Partner Reward Determination Call Chain

The execution path for resolving an applicable partner reward coordinates link-level overrides, program enrollment defaults, metric aggregation, condition evaluation, and final parsing. 

```mermaid
graph TD
    A[determinePartnerReward] --> B[getLinkRewards]
    B --> C{linkRewards found?}
    C -->|Yes| D[Select linkReward column]
    C -->|No| E[Select programEnrollment column]
    D --> F[aggregatePartnerLinksStats]
    E --> F
    F --> G{reward.modifiers exist?}
    G -->|Yes| H[evaluateRewardConditions]
    G -->|No| I[getRewardAmount]
    H -->|Matched| J[Override reward parameters]
    H -->|No Match| I
    J --> I
    I --> K{amount === 0?}
    K -->|Yes| L[Return null]
    K -->|No| M[RewardSchema.parse]
```

Sources: [apps/web/lib/partners/determine-partner-reward.ts:78-162](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L78-L162)

1. `determinePartnerReward()` — Entry point that accepts an `event`, `programEnrollment`, `linkId`, and optional `context`. Sources: [apps/web/lib/partners/determine-partner-reward.ts:78-88](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L78-L88)
2. `getLinkRewards()` — Queries `prisma.linkReward.findUnique()` using the `linkId` to check if a specific link overrides the program's default reward. Sources: [apps/web/lib/partners/determine-partner-reward.ts:91-94](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L91-L94), [apps/web/lib/partners/determine-partner-reward.ts:261-282](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L261-L282)
3. `aggregatePartnerLinksStats()` — Aggregates link metrics to construct the partner statistics context, including total commissions and country codes. Sources: [apps/web/lib/partners/determine-partner-reward.ts:104-114](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L104-L114)
4. `evaluateRewardConditions()` — Safely parses reward modifiers via Zod and evaluates them against the enriched context to discover matching rule tiers. Sources: [apps/web/lib/partners/determine-partner-reward.ts:118-128](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L118-L128)
5. `getRewardAmount()` — Computes the numeric reward value from the serialized reward definition. Sources: [apps/web/lib/partners/determine-partner-reward.ts:152](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L152)
6. `RewardSchema.parse()` — Validates and serializes the final partner reward object before returning it alongside any matched condition. Sources: [apps/web/lib/partners/determine-partner-reward.ts:158-161](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L158-L161)

> [!WARNING]
> When Stripe line items include a `productId` modifier, `determinePartnerRewards` splits the evaluation per product by iterating over `context.sale.products`, assigning `product.amount` while forcing quantity to `1` for each product iteration.
> Sources: [apps/web/lib/partners/determine-partner-reward.ts:166-237](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L166-L237)

### Earnings Calculation and Formatting

Once a reward is determined, actual earnings are calculated via `calculateSaleEarnings`, which distinguishes between flat-fee and percentage models. Flat-fee rewards multiply sale quantity by the reward amount, whereas percentage rewards calculate rounded integer cents from the sale amount. Sources: [apps/web/lib/api/sales/calculate-sale-earnings.ts:8-28](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/sales/calculate-sale-earnings.ts#L8-L28)

```typescript
export const calculateSaleEarnings = ({
  reward,
  sale,
}: {
  reward: Pick<RewardProps, "type" | "amountInCents" | "amountInPercentage">;
  sale: Pick<Commission, "quantity" | "amount">;
}) => {
  if (!reward) {
    return 0;
  }

  const amount = getRewardAmount(reward);

  if (reward.type === "flat") {
    return sale.quantity * amount;
  } else if (reward.type === "percentage") {
    return Math.round((sale.amount * amount) / 100);
  }

  return 0;
};
```

Sources: [apps/web/lib/api/sales/calculate-sale-earnings.ts:8-28](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/sales/calculate-sale-earnings.ts#L8-L28)

For user-facing displays, `constructRewardAmount` evaluates modifier ranges. If every modifier matches the primary reward's type and maximum duration, it constructs a range string formatted as `"Up to X%"`, or formats flat values using `currencyFormatter`. If modifiers do not match the primary reward parameters, it falls back to displaying the primary reward value directly. Sources: [apps/web/lib/api/sales/construct-reward-amount.ts:6-79](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/sales/construct-reward-amount.ts#L6-L79)

### Reward Event Mapping and Multipliers Reference

| Event Type | Mapping Constant Key | Target Database Column | Description |
| :--- | :--- | :--- | :--- |
| `click` | `EventType.click` | `clickReward` | Reward triggered upon link click events |
| `lead` | `EventType.lead` | `leadReward` | Reward triggered upon lead conversion |
| `sale` | `EventType.sale` | `saleReward` | Reward triggered upon revenue generation |

Sources: [apps/web/lib/partners/determine-partner-reward.ts:14-18](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partners/determine-partner-reward.ts#L14-L18)

> [!TIP]
> When constructing display ranges in `constructRewardAmount`, modifiers with undefined amounts fall back to `Infinity` for minimum bounds and `0` for maximum bounds during calculation.
> Sources: [apps/web/lib/api/sales/construct-reward-amount.ts:40-56](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/sales/construct-reward-amount.ts#L40-L56)

## Partner Commission Workflow Execution

### Overview

The partner commission workflow execution pipeline processes newly generated commissions through a series of asynchronous validation steps, duration checks, earnings clamping rules, and post-processing triggers. When a commission workflow is invoked, raw earnings are computed based on event types (such as multiplying lead quantities or reducing sale items), evaluated against max duration constraints for recurring or first-sale subscriptions, checked against spend limits, validated for fraud or custom reward enrollment status, and finally dispatched alongside webhook and cron sync tasks.

Sources: [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:366-470](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L366-L470), [apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts:16-33](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts#L16-L33)

### Spend Limit Clamping and Fraud Gatechecks

Before commission creation is finalized, `clampEarningsToSpendLimit()` calculates whether a partner has exceeded configured spend limits for a specific reward interval. The reward cap scope differs by event type: sales are evaluated at both the partner and customer level, whereas clicks and leads are restricted at the partner level only. 

Sources: [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:773-775](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L773-L775), [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:802-838](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L802-L838)

The clamping helper queries existing commission records matching the criteria, sums their earnings, and clamps the current earnings between zero and the remaining spend limit budget.

```typescript
async function clampEarningsToSpendLimit({
  reward,
  earnings,
  programId,
  partnerId,
  customerId,
  referenceDate,
}: {
  reward: Pick<
    RewardProps,
    "event" | "spendLimitAmount" | "spendLimitInterval"
  >;
  earnings: number;
  programId: string;
  partnerId: string;
  customerId: string;
  referenceDate: Date;
}) {
  if (
    earnings === 0 ||
    !reward.spendLimitAmount ||
    !reward.spendLimitInterval
  ) {
    return earnings;
  }

  const { startDate, endDate } = getRewardSpendLimitWindow({
    spendLimitInterval: reward.spendLimitInterval,
    referenceDate,
  });

  const {
    _sum: { earnings: totalEarnings },
  } = await prisma.commission.aggregate({
    where: {
      programId,
      partnerId,
      ...(reward.event === "sale" ? { customerId } : {}),
      type: reward.event,
      status: {
        in: ["pending", "processed", "paid", "hold"],
      },
      ...(startDate && endDate
        ? {
            createdAt: {
              gte: startDate,
              lte: endDate,
            },
          }
        : {}),
    },
    _sum: {
      earnings: true,
    },
  });

  return Math.max(
    0,
    Math.min(earnings, reward.spendLimitAmount - (totalEarnings ?? 0)),
  );
}
```

Sources: [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:776-838](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L776-L838)

> [!CAUTION]
> Custom reward jobs are queued from a snapshot of eligible enrollments. The system re-checks at write time (`programEnrollment.customRewardId === rewardId` and `status === "approved"`) to prevent paying out partners who were banned, deactivated, or moved off a reward before the workflow executed.
> Sources: [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:446-463](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L446-L463)

### Workflow Post-Processing Steps and Cron Aggregation

Once earnings are verified, the execution pipeline wraps up by mapping asynchronous notifications and synchronization actions into an array of execution results. Concurrently, due commissions are aggregated into payouts via Upstash QStash job batches.

Sources: [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:760-771](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L760-L771), [apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts:38-58](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts#L38-L58)

The table below outlines the post-processing steps mapped during the final workflow return phase.

| Step Name | Description | Sources |
| :--- | :--- | :--- |
| `sendWorkspaceWebhook` | Dispatches workspace-level webhook events regarding the new commission | [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:761](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L761) |
| `sendPartnerPostback` | Triggers external partner postback URLs configured for conversion tracking | [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:762](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L762) |
| `syncTotalCommissions` | Synchronizes partner and workspace aggregate commission metrics | [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:763](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L763) |
| `notifyPartnerCommission` | Sends direct notifications alerting the partner of earned commissions | [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:764](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L764) |
| `executeWorkflows` | Triggers internal workflow listeners for metrics and event hooks | [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:765](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L765) |
| `triggerAggregateDueCommissions` | Enqueues background cron jobs to aggregate pending due commissions | [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:766](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L766) |

Sources: [apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts:760-771](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/workflows/create-partner-commission/route.ts#L760-L771)

## Recurring and Custom Scheduled Rewards

### Cron Cadence Evaluation and UTC Period Calculations

The custom reward subsystem relies on daily UTC cron executions to evaluate scheduled payouts. The cron entry point at `/api/cron/rewards/queue-custom-commissions` initializes by retrieving the current period date in UTC using `getUtcPeriodDate()`, which parses date boundaries via `toUtcDateOnly()` and formats them into a standardized `YYYY-MM-DD` string.

Sources: [apps/web/app/(ee)/api/cron/rewards/queue-custom-commissions/route.ts:12-13](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/rewards/queue-custom-commissions/route.ts#L12-L13), [apps/web/lib/api/rewards/custom-reward-utils.ts:22-42](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L22-L42)

Cadence evaluation is governed by `isCadenceDue()`, which compares the target date against a reward's anchor date across four distinct frequency tiers. If the target date precedes the anchor date, evaluation immediately returns `false`.

Sources: [apps/web/lib/api/rewards/custom-reward-utils.ts:61-70](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L61-L70)

| Frequency | Evaluation Logic | Sources |
| :--- | :--- | :--- |
| `day` | Computes calendar day difference via `differenceInCalendarDays`; returns `true` if remainder with interval is zero. | [apps/web/lib/api/rewards/custom-reward-utils.ts:74-77](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L74-L77) |
| `week` | Computes calendar day difference, multiplying interval by 7; checks if remainder is zero. | [apps/web/lib/api/rewards/custom-reward-utils.ts:79-82](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L79-L82) |
| `month` | Validates that current UTC day matches expected day of month (accounting for shorter months via `expectedUtcDayOfMonth`), then verifies calendar month difference modulo interval equals zero. | [apps/web/lib/api/rewards/custom-reward-utils.ts:84-93](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L84-L93) |
| `year` | Validates matching month index and expected day of month, then checks calendar year difference modulo interval equals zero. | [apps/web/lib/api/rewards/custom-reward-utils.ts:95-104](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L95-L104) |

Sources: [apps/web/lib/api/rewards/custom-reward-utils.ts:71-105](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L71-L105)

> [!NOTE]
> When evaluating monthly and yearly cadences, `expectedUtcDayOfMonth` safely clamps anchor dates (such as the 31st) to the final day of shorter months, preventing skipped or mismatched intervals during edge-case periods.
> Sources: [apps/web/lib/api/rewards/custom-reward-utils.ts:48-60](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L48-L60)

### Custom Reward Dispatch Pipelines

Once due rewards are identified via `findDueCustomRewards()`, the cron job fans out execution using `createCustomCommissionJob.dispatchBatch()`. Each batch entry assigns a deterministic deduplication identifier (`custom-commission-${rewardId}-${periodDate}`) and enforces flow control with a parallelism limit of 5.

Sources: [apps/web/app/(ee)/api/cron/rewards/queue-custom-commissions/route.ts:14-35](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/rewards/queue-custom-commissions/route.ts#L14-L35)

The execution call chain processes custom commissions through a pagination and recursion pipeline:
`GET /api/cron/rewards/queue-custom-commissions` → `createCustomCommissionJob.handle()` → `createCustomRewardCommissions()` → `dispatchWorkflows()`

Sources: [apps/web/app/(ee)/api/cron/rewards/queue-custom-commissions/route.ts:12-35](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/cron/rewards/queue-custom-commissions/route.ts#L12-L35), [apps/web/lib/jobs/handlers/create-custom-commission-job.ts:17-33](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/create-custom-commission-job.ts#L17-L33), [apps/web/lib/api/rewards/create-custom-reward-commissions.ts:19-190](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/create-custom-reward-commissions.ts#L19-L190)

During `createCustomRewardCommissions()`, partner enrollments are fetched in pages of 100. For rewards configured with a `maxDuration`, the handler batch-fetches the earliest custom commission per partner using Prisma's `groupBy` and `_min` aggregation, evaluating `hasRewardMaxDurationElapsed()` to filter out partners who have exceeded their lifetime earning window.

Sources: [apps/web/lib/api/rewards/create-custom-reward-commissions.ts:17-148](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/create-custom-reward-commissions.ts#L17-L148), [apps/web/lib/api/rewards/custom-reward-utils.ts:171-189](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L171-L189)

Eligible enrollments generate workflow jobs configured with an idempotency key built via `buildCommissionIdempotencyKey()`. If a result set reaches `PAGE_SIZE`, `nextCursor` returns the final partner identifier, prompting `createCustomCommissionJob` to re-dispatch itself with a 1-second delay to process the subsequent page.

Sources: [apps/web/lib/jobs/handlers/create-custom-commission-job.ts:18-32](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/create-custom-commission-job.ts#L18-32), [apps/web/lib/api/rewards/create-custom-reward-commissions.ts:150-189](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/create-custom-reward-commissions.ts#L150-L189)

| Design Choice | Benefit | Cost | Sources |
| :--- | :--- | :--- | :--- |
| Batch enrollment pagination (`PAGE_SIZE = 100`) | Prevents memory exhaustion and timeout exceptions when processing large partner programs. | Introduces multi-page job chaining and recursive queue roundtrips. | [apps/web/lib/api/rewards/create-custom-reward-commissions.ts:17](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/create-custom-reward-commissions.ts#L17), [apps/web/lib/jobs/handlers/create-custom-commission-job.ts:20-31](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/create-custom-commission-job.ts#L20-L31) |
| Grouped `_min` commission queries | Eliminates N+1 database roundtrips when checking partner duration elapsed status. | Requires additional memory lookup mapping across partner identifier subsets. | [apps/web/lib/api/rewards/create-custom-reward-commissions.ts:102-124](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/create-custom-reward-commissions.ts#L102-L124) |
| SHA-256 idempotency hashing | Guarantees unique invoice identifiers per reward, partner, and period date combination. | Adds minor cryptographic hashing overhead during job payload construction. | [apps/web/lib/api/rewards/custom-reward-utils.ts:157-169](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L157-L169) |

Sources: [apps/web/lib/api/rewards/create-custom-reward-commissions.ts:17-124](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/create-custom-reward-commissions.ts#L17-L124), [apps/web/lib/api/rewards/custom-reward-utils.ts:157-169](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/rewards/custom-reward-utils.ts#L157-L169), [apps/web/lib/jobs/handlers/create-custom-commission-job.ts:20-31](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/jobs/handlers/create-custom-commission-job.ts#L20-L31)

## Manual Adjustments and Direct Creation

### Overview

The manual commission subsystem provides programmatic endpoints to retrieve, generate, and update partner commissions outside of standard automated tracking events. Workspace administrators can list commissions via the `GET /api/commissions` endpoint or submit manual commission creations and adjustments via `POST /api/commissions`. When a request hits the `POST` route, it validates the request body using `createManualCommissionBodySchema` and invokes `createManualCommissions()`. Depending on whether the manual entry type is configured as a custom adjustment or a specific conversion event, the creation logic branches into direct queueing or event resolution.

Sources: [apps/web/app/(ee)/api/commissions/route.ts:18-80](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/commissions/route.ts#L18-L80), [apps/web/lib/api/commissions/create-manual-commissions.ts:83-113](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/create-manual-commissions.ts#L83-L113)

### Manual Creation Execution Walkthrough

When creating manual commissions via `createManualCommissions()`, the execution path follows a sequence of validations, event insertions, and background tasks:

1. `getProgramEnrollmentOrThrow()` verifies that the partner is enrolled in the target program and retrieves their associated links.
2. If `type === "custom"`, `queuePartnerCommissionCreation()` directly queues a custom commission with `CommissionSource.user` and returns immediately.
3. For lead or sale types, `resolveLinkAndCustomer()` resolves the appropriate target link and customer record.
4. If `type === "sale"` and `importStripeInvoices` is requested, the system verifies workspace Stripe connection settings and customer Stripe IDs, throwing a `DubApiError` if validation fails. It also checks for invoice collisions using `prisma.commission.findUnique()`.
5. `recordEvents()` logs the underlying click, lead, or sale events.
6. Commissions are pushed to an array (`commissionsToCreate`), then sequentially queued via `queuePartnerCommissionCreation()`, where only the final iteration triggers aggregate due commissions (`triggerAggregateDueCommissions: index === commissionsToCreate.length - 1`).
7. Finally, `waitUntil(executeSideEffects(...))` dispatches any secondary side effects in the background.

Sources: [apps/web/lib/api/commissions/create-manual-commissions.ts:86-258](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/create-manual-commissions.ts#L86-L258)

### Commission Update and Reconcile Workflow

Partner commissions can be modified via `updatePartnerCommission()`, which handles altering sale amounts, converting currencies using `convertCurrency()`, recalculating earnings via `calculateSaleEarnings()`, and transitioning commission statuses. 

```typescript
// Call-chain for updating a partner commission:
// updatePartnerCommission() → prisma.commission.findUnique() → convertCurrency() (if non-USD) 
// → determinePartnerReward() → calculateSaleEarnings() → prisma.commission.update() 
// → reconcilePayoutAmounts() → waitUntil(syncTotalCommissions() + trackCommissionActivityLog())
```

Sources: [apps/web/lib/api/commissions/update-partner-commission.ts:31-327](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/update-partner-commission.ts#L31-L327)

> [!CAUTION]
> Commissions that have already been paid (`commission.status === "paid"`) or belong to locked payouts (`!MUTABLE_PAYOUT_STATUSES.includes(commission.payout.status)`) cannot be updated. Attempting to modify them throws a `DubApiError` with a `bad_request` or `not_found` code.

Sources: [apps/web/lib/api/commissions/update-partner-commission.ts:66-88](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/update-partner-commission.ts#L66-L88)

> [!IMPORTANT]
> When updating a commission's status to `fraud` or `canceled` with `updateHistoricalCommissions` enabled, the system automatically sweeps un-paid historical commissions for the same customer and partner combination, updates their status, nullifies their `payoutId`, and triggers payout reconciliation across all affected payout identifiers.

Sources: [apps/web/lib/api/commissions/update-partner-commission.ts:232-300](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/update-partner-commission.ts#L232-300)

### API Endpoints and Parameters Reference

The manual commission and adjustments routes support specific query parameters and body schemas for interacting with partner financial records.

| Endpoint / Function | Parameter / Field | Type | Description | Sources |
| :--- | :--- | :--- | :--- | :--- |
| `GET /api/commissions` | `partnerId` | string (optional) | Filters returned commissions by specific partner identifier. | [apps/web/app/(ee)/api/commissions/route.ts:22-27](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/commissions/route.ts#L22-L27) |
| `GET /api/commissions` | `tenantId` | string (optional) | Resolves partner ID via program enrollment lookup when partner ID is absent. | [apps/web/app/(ee)/api/commissions/route.ts:22-50](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/commissions/route.ts#L22-L50) |
| `POST /api/commissions` | `type` | string | Commission type (e.g., custom, lead, sale). Custom negative amounts trigger clawbacks. | [apps/web/app/(ee)/api/commissions/route.ts:78-100](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/commissions/route.ts#L78-L100) |
| `updatePartnerCommission()` | `modifySaleAmount` | number (optional) | Increments or decrements the existing sale amount before FX conversion and recalculation. | [apps/web/lib/api/commissions/update-partner-commission.ts:41-138](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/update-partner-commission.ts#L41-L138) |
| `updatePartnerCommission()` | `updateHistoricalCommissions` | boolean | Flag to propagate fraud or cancellation status updates across historical customer commissions. | [apps/web/lib/api/commissions/update-partner-commission.ts:44-285](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/update-partner-commission.ts#L44-L285) |

Sources: [apps/web/app/(ee)/api/commissions/route.ts:22-100](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/commissions/route.ts#L22-L100), [apps/web/lib/api/commissions/update-partner-commission.ts:41-285](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/api/commissions/update-partner-commission.ts#L41-L285)

## External Importer Reconciliations and Tracking

### Overview

External platform commission ingestion pipelines allow historical or live commissions from foreign affiliate networks (such as Rewardful, FirstPromoter, PartnerStack, and Lemon Squeezy) to be synchronized and reconciled into Dub's core commission database. These ingestion modules parse foreign webhook payloads or API export responses, validate customer attribution via Tinybird click and lead events, handle foreign currency conversions using cached FX rates, and prevent double-crediting through dedoorprinting strategies like invoice ID lookups and ±1-hour sliding-window checks.

Sources: [apps/web/lib/rewardful/import-commissions.ts:1-404](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/rewardful/import-commissions.ts#L1-L404), [apps/web/lib/lemonsqueezy/import-commissions.ts:263-610](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/lemonsqueezy/import-commissions.ts#L263-L610), [apps/web/lib/partnerstack/import-commissions.ts:136-397](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partnerstack/import-commissions.ts#L136-L397), [apps/web/lib/firstpromoter/import-commissions.ts:35-422](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/firstpromoter/import-commissions.ts#L35-L422)

### Foreign Importer Ingestion Call-Chain

When ingesting commissions from external platforms, records pass through validation, conversion, attribution verification, and persistence steps. 

```typescript
// Call-chain for importing external commissions:
// importCommissions() → FirstPromoterApi.listCommissions() / equivalent → prisma.customer.findMany() 
// → getLeadEvents() → createCommission() → prisma.commission.findUnique() (deduplication check) 
// → convertCurrencyWithFxRates() → prisma.commission.create() → recordSaleWithTimestamp() 
// → prisma.link.update() → syncPartnerLinksStats() → syncTotalCommissions()
```

Sources: [apps/web/lib/rewardful/import-commissions.ts:141-403](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/rewardful/import-commissions.ts#L141-L403), [apps/web/lib/lemonsqueezy/import-commissions.ts:377-609](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/lemonsqueezy/import-commissions.ts#L377-L609), [apps/web/lib/firstpromoter/import-commissions.ts:35-421](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/firstpromoter/import-commissions.ts#L35-L421)

> [!WARNING]
> If an external platform commission does not provide a direct Stripe invoice ID, importers fall back to deduplicating against existing Dub records using the customer's Stripe customer ID or external key combined with a $\pm 1$-hour creation timestamp window (`createdAt` between $t - 3600000$ and $t + 3600000$). This protects against duplicate commission records during transition periods where both systems record charges simultaneously.

Sources: [apps/web/lib/rewardful/import-commissions.ts:237-251](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/rewardful/import-commissions.ts#L237-L251), [apps/web/lib/partnerstack/import-commissions.ts:294-306](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/partnerstack/import-commissions.ts#L294-L306), [apps/web/lib/firstpromoter/import-commissions.ts:251-263](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/firstpromoter/import-commissions.ts#L251-L263)

### Currency Conversion and Resolution

External transactions originating in non-USD currencies are normalized using exchange rates retrieved from Redis caches (`fxRates:usd`). Lemon Squeezy integration logic explicitly checks alternative pricing structures if order subtotals are zero during initial subscription checkouts.

```typescript
function resolveAmountUsd({
  amount,
  amountUsd,
  currency,
  fxRates,
}: {
  amount: number;
  amountUsd: number | null | undefined;
  currency: string;
  fxRates: Record<string, string> | null;
}): number | null {
  if (amountUsd != null) {
    return amountUsd;
  }

  if (currency.toUpperCase() === "USD") {
    return amount;
  }

  if (!fxRates) {
    return null;
  }

  return convertCurrencyWithFxRates({ currency, amount, fxRates }).amount;
}
```

Sources: [apps/web/lib/lemonsqueezy/import-commissions.ts:611-640](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/lemonsqueezy/import-commissions.ts#L611-L640)

> [!TIP]
> Lemon Squeezy imports prioritize platform-provided USD totals (`amountUsd`). If the order subtotal evaluates to zero on a subscription's first charge, the importer inspects `firstOrderItemPrice` before attempting FX conversion.

Sources: [apps/web/lib/lemonsqueezy/import-commissions.ts:467-488](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/lemonsqueezy/import-commissions.ts#L467-L488)

### Referral Earnings Views and Client Presentation

Partner dashboard interfaces and embed views render referral earnings by fetching paginated commission records via SWR hooks and presenting status badges, customer attribution identifiers, and formatted currency values.

| Component / Function | Data Source | Pagination Limit | Render Behavior | Sources |
| :--- | :--- | :--- | :--- | :--- |
| `ReferralsEmbedEarnings` | `/api/embed/referrals/earnings` | `REFERRALS_EMBED_EARNINGS_LIMIT` | Renders a table of customer emails, creation timestamps, raw amounts, earnings, and status badges. | [apps/web/app/(ee)/app.dub.co/embed/referrals/earnings.tsx:23-120](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/app.dub.co/embed/referrals/earnings.tsx#L23-L120) |
| `CommissionsPageClient` | `/api/admin/commissions` | N/A (Admin SWR) | Aggregates program-level commission timeseries data, filters by program ID, and renders analytics areas. | [apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/page.tsx:42-166](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/page.tsx#L42-L166) |

Sources: [apps/web/app/(ee)/app.dub.co/embed/referrals/earnings.tsx:23-120](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/app.dub.co/embed/referrals/earnings.tsx#L23-L120), [apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/page.tsx:42-166](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/page.tsx#L42-L166)

## Related

- [Conversion and Event Tracking](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/analytics-and-tracking/conversion-and-event-tracking)
- [Payout Processing](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/payout-processing)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
