Skip to main content
doc0blade47/dubdocumentation home
Sign in

Reward Rules

Last updated Oct 12, 2026100% grounded
Relevant source files

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

Overview

Reward rules govern how affiliate commissions are determined across workspace programs, resolving event structures, payout calculations, and conditional overrides for sales, leads, clicks, referrals, and custom schedules. They provide the foundational engine for calculating precise compensation by evaluating contextual attributes such as partner statistics, customer geolocations, and product metadata against configured reward modifiers. The system resolves hierarchy rules from workspace groups down to individual partner links while enforcing spend limits and duration constraints. Additionally, built-in validation pipelines, natural-language formatting utilities, and AI consistency auditing support program managers in creating and maintaining accurate payout configurations.

Sources: apps/web/lib/api/sales/construct-reward-amount.ts:6-79, apps/web/lib/zod/schemas/rewards.ts:1-253, apps/web/lib/api/rewards/validate-reward.ts:15-212, apps/web/lib/partners/determine-partner-reward.ts:78-245, apps/web/lib/partners/evaluate-reward-conditions.ts:10-69, apps/web/lib/rewards/format-reward-condition.ts:14-70, apps/web/lib/rewards/validate-tooltip-suggestion.ts:156-245, apps/web/lib/ai/generate-reward.ts:108-315, apps/web/ui/partners/rewards/reward-quality.tsx:10-125, apps/web/ui/partners/format-reward-description.ts:14-78, apps/web/app/ee/api/workflows/create-partner-commission/route.ts:364-444

Reward Schema Definition

Commission and Reward Entities

Reward structures dictate how payouts are calculated for distinct conversion events across programs. The validation and schema layer establishes structural constraints for flat, percentage, tiered, and custom event rewards. Conditional attributes are organized by entity categories, allowing rules to inspect contextual data for partners, customers, sales, and leads.

Entity IDLabelAttributesSources
leadLeadmetadataapps/web/lib/zod/schemas/rewards.ts:52-62
partnerPartnercountry, totalClicks, totalLeads, totalConversions, totalSaleAmount, totalCommissionsapps/web/lib/zod/schemas/rewards.ts:64-99
customerCustomercountryapps/web/lib/zod/schemas/rewards.ts:101-111
saleSaleproductId, amount, typeapps/web/lib/zod/schemas/rewards.ts:228-253

Sources: apps/web/lib/zod/schemas/rewards.ts:52-111, apps/web/lib/zod/schemas/rewards.ts:228-253

Validation Rules and Constraints

Caution

Percentage rewards are strictly forbidden for click, lead, and custom events. Attempting to assign amountInPercentage or setting type to percentage for these event types triggers an immediate bad_request API error during validation.

Sources: apps/web/lib/api/rewards/validate-reward.ts:18-76

Validation checks enforce strict boundaries depending on the target reward event type:

  • Click and Lead Rewards: Must use flat structures with amountInCents defined. amountInPercentage is disallowed, and maxDuration must be set to 0.
  • Custom Rewards: Require a valid config payload including frequency, interval, and an anchorDate that cannot be set in the past. Spend limits (spendLimitAmount and spendLimitInterval) and modifiers are disallowed, and maxDuration cannot be 0.
  • Sale Rewards: Require either amountInCents or amountInPercentage to be provided, but both cannot be used simultaneously. Flat sales mandate amountInCents, while percentage sales mandate amountInPercentage.
  • Referral Rewards: Require a configuration object containing a valid trigger type. Percentage referral rewards must reference percentage triggers, while flat referral rewards must reference flat triggers. Reward modifiers are prohibited.

Sources: apps/web/lib/api/rewards/validate-reward.ts:15-212

Condition Evaluation Pipeline

Field Value Resolution and Context Mapping

The evaluation pipeline resolves dynamic rule conditions by inspecting incoming reward contexts across entities such as partner stats, customer attributes, and metadata fields. When evaluating modifier conditions, field values are extracted based on the target entity and attribute type. For metadata attributes, the pipeline utilizes prepareMetadataFieldValue to sanitize and map the custom metadata value keyed by metadataField.

function resolveConditionFieldValue({
  condition,
  context,
}: {
  condition: RewardCondition;
  context: RewardContext;
}): string | number | string[] | number[] | undefined {
  if (condition.attribute === "metadata") {
    const metaKey = condition.metadataField?.trim();
 
    if (!metaKey) {
      return undefined;
    }
 
    const entityMap = {
      partner: undefined,
      customer: undefined,
      lead: context.lead,
      sale: context.sale,
    } as const;
 
    return prepareMetadataFieldValue(
      entityMap[condition.entity]?.metadata?.[metaKey],
      condition,
    );
  }
 
  const entityMap = {
    partner: context.partner,
    customer: context.customer,
    lead: undefined,
    sale: context.sale,
  } as const;
 
  return entityMap[condition.entity]?.[condition.attribute];
}

Sources: apps/web/lib/partners/evaluate-reward-conditions.ts:71-106

EntityMapped Context SourceHandled AttributesSources
partnercontext.partnerPartner metrics, country, metadataapps/web/lib/partners/evaluate-reward-conditions.ts:98-99
customercontext.customerCountry, metadataapps/web/lib/partners/evaluate-reward-conditions.ts:100-100
leadcontext.leadMetadata fieldsapps/web/lib/partners/evaluate-reward-conditions.ts:88-88
salecontext.saleProduct ID, amount, type, metadataapps/web/lib/partners/evaluate-reward-conditions.ts:89-89

Sources: apps/web/lib/partners/evaluate-reward-conditions.ts:78-106

Boolean Operators and Condition Evaluation

Once field values are resolved, individual condition rules are assessed using comparison operators. Condition groups combine multiple individual rules with either AND or OR logical operators. If all evaluations within an AND group or any evaluation within an OR group pass, the condition group is marked as met.

const evaluateCondition = ({
  condition,
  fieldValue,
}: {
  condition: RewardCondition;
  fieldValue: string | number | string[] | number[];
}) => {
  // Equals
  if (condition.operator === "equals_to") {
    return fieldValue === condition.value;
  }
 
  // Not equals
  if (condition.operator === "not_equals") {
    return fieldValue !== condition.value;
  }
 
  // Starts with
  if (condition.operator === "starts_with") {
    if (
      typeof fieldValue !== "string" ||
      typeof condition.value !== "string" ||
      condition.value === ""
    ) {
      return false;
    }
 
    return fieldValue.startsWith(condition.value);
  }
 
  // Ends with
  if (condition.operator === "ends_with") {
    if (
      typeof fieldValue !== "string" ||
      typeof condition.value !== "string" ||
      condition.value === ""
    ) {
      return false;
    }
 
    return fieldValue.endsWith(condition.value);
  }
 
  // Contains
  if (condition.operator === "contains") {
    if (typeof fieldValue !== "string" || typeof condition.value !== "string") {
      return false;
    }
 
    const trimmedValue = condition.value.trim();
 
    if (trimmedValue === "") {
      return false;
    }
 
    return String(fieldValue).includes(trimmedValue);
  }
 
  // Not contains
  if (condition.operator === "not_contains") {
    if (typeof fieldValue !== "string" || typeof condition.value !== "string") {
      return false;
    }
 
    const trimmedValue = condition.value.trim();
 
    if (trimmedValue === "") {
      return false;
    }
 
    return !String(fieldValue).includes(trimmedValue);
  }
 
  // In
  if (condition.operator === "in") {
    if (!Array.isArray(condition.value)) {
      return false;
    }
 
    return (condition.value as (string | number)[]).includes(

Sources: apps/web/lib/partners/evaluate-reward-conditions.ts:108-224

Operator IDEvaluation LogicSources
equals_tofieldValue === condition.valueapps/web/lib/partners/evaluate-reward-conditions.ts:116-118
not_equalsfieldValue !== condition.valueapps/web/lib/partners/evaluate-reward-conditions.ts:121-123
starts_withfieldValue.startsWith(condition.value)apps/web/lib/partners/evaluate-reward-conditions.ts:126-136
ends_withfieldValue.endsWith(condition.value)apps/web/lib/partners/evaluate-reward-conditions.ts:139-149
containsString(fieldValue).includes(trimmedValue)apps/web/lib/partners/evaluate-reward-conditions.ts:152-164
not_contains!String(fieldValue).includes(trimmedValue)apps/web/lib/partners/evaluate-reward-conditions.ts:167-179
incondition.value.includes(fieldValue)apps/web/lib/partners/evaluate-reward-conditions.ts:182-190
not_in!condition.value.includes(fieldValue)apps/web/lib/partners/evaluate-reward-conditions.ts:193-201
greater_thanNumber(fieldValue) > Number(condition.value)apps/web/lib/partners/evaluate-reward-conditions.ts:204-206
greater_than_or_equalNumber(fieldValue) >= Number(condition.value)apps/web/lib/partners/evaluate-reward-conditions.ts:209-211
less_thanNumber(fieldValue) < Number(condition.value)apps/web/lib/partners/evaluate-reward-conditions.ts:214-216
less_than_or_equalNumber(fieldValue) <= Number(condition.value)apps/web/lib/partners/evaluate-reward-conditions.ts:219-221

Sources: apps/web/lib/partners/evaluate-reward-conditions.ts:108-224

Override Selection and Precedence

When multiple condition groups match the evaluation context, the pipeline selects the best matching condition by sorting the matching groups in descending order by their calculated reward amount and picking the first entry.

  if (matchingConditions.length === 0) {
    return null;
  }
 
  // Find the best matching condition (highest amount)
  return matchingConditions.sort(
    (a, b) =>
      getRewardAmount({
        type: b.type!,
        amountInCents: b.amountInCents,
        amountInPercentage: b.amountInPercentage,
      }) -
      getRewardAmount({
        type: a.type!,
        amountInCents: a.amountInCents,
        amountInPercentage: a.amountInPercentage,
      }),
  )[0];
};

Sources: apps/web/lib/partners/evaluate-reward-conditions.ts:51-69

Note

If multiple condition groups evaluate to true against the provided context, evaluateRewardConditions automatically sorts candidates using getRewardAmount and prioritizes the group yielding the highest commission amount.

Sources: apps/web/lib/partners/evaluate-reward-conditions.ts:55-68

When a matched condition overrides the base reward, properties such as type, amountInCents, amountInPercentage, and maxDuration are updated on the reward object. For sales featuring Stripe line items with productId modifiers, determinePartnerRewards iterates over each product in the sale context to calculate distinct line-item overrides.

let matchedCondition: RewardConditions | null = null;
 
if (partnerReward.modifiers && context) {
  const modifiers = rewardConditionsArraySchema.safeParse(
    partnerReward.modifiers,
  );
 
  // Parse the conditions before evaluating them
  if (modifiers.success) {
    matchedCondition = evaluateRewardConditions({
      conditions: modifiers.data,
      context,
    });
 
    if (matchedCondition) {
      partnerReward = {
        ...partnerReward,
        // Override the reward amount, type and max duration with the matched condition
        type: matchedCondition.type || partnerReward.type,
        amountInCents:
          matchedCondition.amountInCents != null
            ? matchedCondition.amountInCents
            : null,
        amountInPercentage:
          matchedCondition.amountInPercentage != null
            ? new Prisma.Decimal(matchedCondition.amountInPercentage)
            : null,
        maxDuration:
          matchedCondition.maxDuration !== undefined
            ? matchedCondition.maxDuration
            : partnerReward.maxDuration,
      };
    }
  }
}

Sources: apps/web/lib/partners/determine-partner-reward.ts:116-150, apps/web/lib/partners/determine-partner-reward.ts:209-237

Payout Amount Calculations

Earnings Calculation and Base Reward Retrieval

The commission earnings for individual sales are computed via calculateSaleEarnings, which evaluates the underlying reward structure against sale parameters such as quantity and total monetary amount. It first resolves the raw numeric amount using getRewardAmount, handling nullable configurations by defaulting unassigned reward values to zero.

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, apps/web/lib/partners/get-reward-amount.ts:3-11

Note

calculateSaleEarnings branches explicitly on the reward type: flat rewards multiply the sale quantity by the resolved unit amount, whereas percentage rewards multiply the sale amount by the percentage rate and divide by 100, rounding the final integer result with Math.round.

Sources: apps/web/lib/api/sales/calculate-sale-earnings.ts:21-25

Modifier Validation and Range Formatting

When rewards contain modifier conditions, constructRewardAmount validates them using rewardConditionsArraySchema. Each modifier's type and maxDuration are evaluated against the primary reward parameters. If every modifier matches both the primary reward type and maxDuration, the engine calculates a boundary range across the primary reward and all valid modifiers.

// If there are modifiers, we need to check if they match the primary reward
if (reward.modifiers) {
  const parsedModifiers = rewardConditionsArraySchema.safeParse(
    reward.modifiers,
  );
 
  if (parsedModifiers.success) {
    const modifiers = parsedModifiers.data;
 
    // if no type or maxDuration, it falls back to the primary reward type and maxDuration
    const matchPrimary = modifiers.every((m) => {
      const typeMatches =
        m.type === undefined ? true : m.type === reward.type;
      const durationMatches =
        m.maxDuration === undefined
          ? true
          : m.maxDuration === reward.maxDuration;
      return typeMatches && durationMatches;
    });
 
    // If the type AND maxDuration matches the primary, show a range
    if (matchPrimary) {
      const amount = getRewardAmount(reward);
 
      const min = Math.min(
        amount,
        ...modifiers.map((modifier) =>
          reward.type === "flat"
            ? modifier.amountInCents ?? Infinity
            : modifier.amountInPercentage ?? Infinity,
        ),
      );
 
      const max = Math.max(
        amount,
        ...modifiers.map((modifier) =>
          reward.type === "flat"
            ? modifier.amountInCents ?? 0
            : modifier.amountInPercentage ?? 0,
        ),
      );
 
      if (min !== max) {
        return `Up to ${
          reward.type === "percentage"
            ? `${max}%`
            : currencyFormatter(max, {
                trailingZeroDisplay: "stripIfInteger",
              })
        }`;
      }
    }

Sources: apps/web/lib/api/sales/construct-reward-amount.ts:16-67

Reward TypeMinimum Value FallbackMaximum Value FallbackFormatting Logic
flatInfinity (if null)0 (if null)currencyFormatter(max, { trailingZeroDisplay: "stripIfInteger" })
percentageInfinity (if null)0 (if null)${max}%

Sources: apps/web/lib/api/sales/construct-reward-amount.ts:40-65

Warning

If modifiers omit either type or maxDuration, they default to matching the primary reward's respective properties. However, if any modifier fails to match both properties, range construction is bypassed entirely and the engine falls back to displaying the primary reward amount.

Sources: apps/web/lib/api/sales/construct-reward-amount.ts:25-34, apps/web/lib/api/sales/construct-reward-amount.ts:71-78

Hierarchy Resolution and Display Filters

Workspace groups and partner links resolve active rewards through dedicated filtering utilities. The getPartnerLinkDisplayRewards function processes event reward parameters including clickReward, leadReward, saleReward, discount, and enrollmentRewards. It invokes pickDisplayReward on primary event rewards to filter out null instances or negative monetary values, while getDisplayCustomRewards extracts custom event rewards where the computed reward amount is greater than or equal to zero.

function pickDisplayReward(reward: RewardProps | null | undefined) {
  if (!reward || getRewardAmount(reward) < 0) {
    return null;
  }
 
  return reward;
}
 
function getDisplayCustomRewards(enrollmentRewards: RewardProps[]) {
  return enrollmentRewards.filter(
    (reward) => reward.event === "custom" && getRewardAmount(reward) >= 0,
  );
}
 
export function getPartnerLinkDisplayRewards({
  clickReward,
  leadReward,
  saleReward,
  discount,
  enrollmentRewards = [],
}: {
  clickReward?: RewardProps | null;
  leadReward?: RewardProps | null;
  saleReward?: RewardProps | null;
  discount?: DiscountProps | null;
  enrollmentRewards?: RewardProps[];
}) {
  return {
    rewards: [
      pickDisplayReward(saleReward),
      pickDisplayReward(leadReward),
      pickDisplayReward(clickReward),
      ...getDisplayCustomRewards(enrollmentRewards),
    ].filter((reward): reward is RewardProps => reward != null),
    discount: discount ?? null,
  };
}

Sources: apps/web/lib/rewards/resolve-partner-link-rewards.ts:4-39

Note

When prioritizing partner incentives via getPartnerLinkPrimaryIncentive, the engine evaluates primary events in the strict order of sale, lead, and click. If none of these primary event rewards exist, the primary incentive falls back to the associated discount.

Sources: apps/web/lib/rewards/resolve-partner-link-rewards.ts:42-60

Spend Limits and Duration Enforcement

During commission creation workflows, the Commission Engine evaluates whether active rewards have breached duration limits or cumulative spend thresholds. For sale events carrying a maxDuration property, one-time sales with a maxDuration of 0 immediately skip commission generation for recurring events. For recurring subscriptions where maxDuration exceeds zero, the workflow computes the month difference via differenceInMonths against the first commission creation date. If this duration meets or exceeds the configured maxDuration, commission processing is halted.

Sources: apps/web/app/ee/api/workflows/create-partner-commission/route.ts:366-389

Furthermore, when a reward defines a spendLimitAmount and spendLimitInterval, earnings are evaluated through clampEarningsToSpendLimit. If the clamped earnings drop to zero, commission creation is skipped entirely. Custom reward commissions additionally verify that the target partner remains approved and actively bound to the specific customRewardId at write time.

if (
  customerId &&
  event !== "custom" &&
  reward &&
  reward.spendLimitAmount &&
  reward.spendLimitInterval
) {
  cappedEarnings = await clampEarningsToSpendLimit({
    reward,
    earnings,
    programId,
    partnerId,
    customerId,
    referenceDate: createdAt ?? new Date(),
  });
 
  // If spend limit clamped earnings to 0, skip commission creation
  if (cappedEarnings === 0) {
    return logAndReturn({
      commission: null,
      outputLog: `Partner ${partnerId} has reached spend limit (${currencyFormatter(reward.spendLimitAmount)} ${reward.spendLimitInterval === "allTime" ? "" : `per ${reward.spendLimitInterval}`}) for ${event} event, skipping commission creation...`,
    });
  }
}
 
// Custom reward jobs are queued from a snapshot of eligible enrollments.
// Re-check at write time so we don't pay a partner who was banned, deactivated,
// or moved off this reward before the workflow ran.
// Only jobs that pass rewardId (create-custom-commission) should be gated —
// manual commissions and bounty payouts also use event "custom" but have no rewardId.
const isCustomRewardCommission = event === "custom" && Boolean(rewardId);
const stillOnCustomReward = programEnrollment.customRewardId === rewardId;
 
if (
  isCustomRewardCommission &&
  (programEnrollment.status !== "approved" || !stillOnCustomReward)
) {
  return logAndReturn({
    commission: null,
    outputLog: `Partner ${partnerId} is no longer eligible for custom reward ${rewardId} (status: ${programEnrollment.status}), skipping commission creation...`,
  });
}

Sources: apps/web/app/ee/api/workflows/create-partner-commission/route.ts:421-462

Referral And Bounty Rewards

Partner Referral Compensation Rules

The partner referral rule engine processes two-tier affiliate compensation by evaluating whether a commission originates from a source commission or a direct partner approval and commission threshold event. When a partner refers another affiliate, the system prevents self-referrals by checking whether the active partner identifier matches the referredByPartnerId context, immediately skipping commission generation if both values are identical.

type CreateReferralCommissionProps =
  | { sourceCommissionId: string; partnerId?: never; programId?: never } // Based on a source commission
  | { sourceCommissionId?: never; partnerId: string; programId: string }; // Based on partner approval and commission threshold
 
export const createReferralCommission = async (
  props: CreateReferralCommissionProps,
) => {
  const context = await resolveReferralContext(props);
 
  if (!context) {
    return null;
  }
 
  const { sourceCommission, programId, partnerId, referredByPartnerId } =
    context;
 
  if (partnerId && referredByPartnerId && partnerId === referredByPartnerId) {
    console.log(
      `Skipping referral commission creation for self-referral (partner ${partnerId}).`,
    );
    return null;
  }

Sources: apps/web/lib/partner-referrals/create-referral-commission.ts:14-35

Reward structures for referrals are split into percentage and flat triggers. Percentage triggers evaluate commission basis rules using predefined percentage events, whereas flat triggers operate on specific partner milestones such as partner approval or commission thresholds.

Trigger CategoryTrigger IdentifierDescription & Behaviour
PercentagesaleRecordedEvaluates percentage reward based on recorded sales.
PercentagecommissionEarnedEvaluates percentage reward based on earned partner commissions.
FlatpartnerApprovedTriggers flat reward immediately upon partner approval (clears maxDuration).
FlatcommissionThresholdTriggers flat reward when accumulated commissions hit a defined threshold in cents.

Sources: apps/web/ui/partners/rewards/partner-referral-reward-builder.tsx:6-11, apps/web/ui/partners/rewards/partner-referral-reward-builder.tsx:56-101, apps/web/ui/partners/rewards/partner-referral-reward-builder.tsx:207-214

Caution

Flat referral triggers such as partnerApproved and commissionThreshold do not support duration limits. Whenever a flat trigger becomes active, the builder automatically forces maxDuration to null. Switching back to a percentage trigger automatically restores maxDuration to Infinity if it was previously cleared.

Sources: apps/web/ui/partners/rewards/partner-referral-reward-builder.tsx:77-101

Manual Bounty Submission Criteria

Bounty programs manage time-bound content challenges through submission requirements that dictate whether creators must attach proof URLs or image files. The manual submission criteria engine reads submission options from the bounty form context, tracking requirement flags and configuration limits for images and URLs.

export function BountyCriteriaManualSubmission() {
  const { watch, setValue } = useBountyFormContext();
 
  const [submissionRequirements, rewardType = "flat"] = watch([
    "submissionRequirements",
    "rewardType",
  ]);
 
  const requireImage = !!submissionRequirements?.image;
  const requireUrl = !!submissionRequirements?.url;
  const imageMax = submissionRequirements?.image?.max;
  const urlMax = submissionRequirements?.url?.max;
  const urlDomains = submissionRequirements?.url?.domains ?? [];

Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/add-edit-bounty/bounty-criteria-manual-submission.tsx:48-61

Bounty payouts support two reward structure types managed via interactive badges: flat rate payouts and custom descriptions. Flat rate bounties validate numeric currency amounts greater than or equal to zero, while custom bounties require a text description limited to 100 characters.

Reward TypeValue IdentifierValidation RuleDisplay Formatting
Flat RateflatrewardAmount != null && !isNaN && rewardAmount >= 0Formatted via currencyFormatter in cents.
Custom Amountcustom!rewardDescription?.trim() (max 100 characters)Displays trimmed description text or fallback placeholder.

Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/add-edit-bounty/bounty-criteria-manual-submission.tsx:22-31, apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/add-edit-bounty/bounty-criteria-manual-submission.tsx:384-436

Note

When configuring custom bounty rewards, input values matching an empty string are explicitly converted to null before updating form state, and the controller enforces a strict maximum length rule of 100 characters.

Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/add-edit-bounty/bounty-criteria-manual-submission.tsx:418-435

Form Construction And Tooltips

Visual program builders construct and edit reward structures through interactive form contexts, field arrays, and natural-language description engines. The reward form interface manages conditional rule groups, payout rates, and cadence configurations via react-hook-form state management. When configuring custom interval rewards, builders parse custom cadence configurations using customConfigSchema and format schedule descriptions and duration labels dynamically.

Sources: apps/web/ui/partners/rewards/rewards-logic.tsx:55-102, apps/web/ui/partners/rewards/custom-reward-builder.tsx:40-51, apps/web/ui/partners/custom-reward-description.tsx:12-38

Natural-language formatting functions convert raw reward objects and condition clauses into human-readable text strings for tooltips, embeds, and summary rows. The formatRewardDescription utility processes event types, flat or percentage amounts, duration constraints, and spend limit intervals into polished payout descriptions.

Event TypeCondition Clause FormatterDuration Suffix LogicSpend Limit Integration
saleformatRewardConditionClausefor the customer's lifetime or for N months / for N yearsAppends , up to [amount] [interval] per customer
referralformatReferralRewardDescriptionfor the referred partner's lifetime or one timeNone
customformatCustomRewardDescriptionfor [durationLabel]None
lead / clickDefault event clauseNoneNone

Sources: apps/web/lib/rewards/format-reward-condition.ts:47-70, apps/web/ui/partners/format-reward-description.ts:14-78

Reward quality grading evaluates configured amounts and durations against predefined competitiveness tiers, surfacing visual indicators with descriptive tooltips. The quality engine categorizes rewards into four discrete quality levels based on event type, payout structure, and duration thresholds.

Reward QualityTarget Event & CriteriaVisual Badge StylingTooltip Summary
lowClicks < $0.03, leads < $1, or percentage sales < 10%Orange background (bg-orange-100), orange textMay struggle to attract partners
limitedOne-time flat sales or short duration caps (maxDuration === 1)Orange background (bg-orange-100), orange textNo long term earning potential
goodClicks 0.03–0.05, leads 1–10, percentage sales 10–20%Cyan background (bg-cyan-200), cyan textSolid reward with room to grow
strongClicks > 0.05, leads > 10, percentage sales > 20% with lifetime/12m+ durationGreen background (bg-green-200), green textHigh earning potential for partners

Sources: apps/web/ui/partners/rewards/reward-quality.tsx:10-125, apps/web/ui/partners/rewards/reward-quality.tsx:127-165

Note

AI consistency auditing uses Anthropic Claude Sonnet via streaming server actions (generateReward and reviewRewardTooltip) to check whether partner-facing copy contradicts structured reward conditions, automatically enforcing confidence floors of 0.5 and filtering out uninstalled customer source integrations.

Sources: apps/web/lib/ai/generate-reward.ts:192-202, apps/web/lib/ai/review-reward-tooltip.ts:221-238, apps/web/lib/rewards/validate-tooltip-suggestion.ts:1-8, apps/web/lib/rewards/validate-tooltip-suggestion.ts:194-204

Was this page helpful?