Relevant source files
The following files were used as context for generating this wiki page:
- apps/web/lib/api/sales/construct-reward-amount.ts
- apps/web/lib/zod/schemas/rewards.ts
- apps/web/lib/api/rewards/validate-reward.ts
- apps/web/lib/partners/determine-partner-reward.ts
- apps/web/ui/partners/rewards/rewards-logic.tsx
- apps/web/lib/partners/get-reward-amount.ts
- apps/web/app/app.dub.co/dashboard/slug/ee/program/groups/groupSlug/rewards/page.tsx
- apps/web/lib/partners/evaluate-reward-conditions.ts
- apps/web/lib/rewards/resolve-partner-link-rewards.ts
- apps/web/lib/rewards/format-reward-condition.ts
- apps/web/lib/rewards/validate-tooltip-suggestion.ts
- apps/web/lib/ai/review-reward-tooltip.ts
- apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/add-edit-bounty/bounty-criteria-manual-submission.tsx
- apps/web/lib/partners/get-group-rewards-and-discount.ts
- apps/web/ui/partners/rewards/reward-event-descriptions.tsx
- apps/web/app/ee/app.dub.co/embed/referrals/page-client.tsx
- apps/web/lib/rewards/resolve-click-reward.ts
- apps/web/ui/partners/rewards/partner-referral-reward-builder.tsx
- apps/web/app/ee/api/workflows/create-partner-commission/route.ts
- apps/web/lib/ai/generate-reward.ts
- apps/web/ui/partners/rewards/custom-reward-builder.tsx
- apps/web/lib/partner-referrals/create-referral-commission.ts
- apps/web/ui/partners/custom-reward-description.tsx
- apps/web/ui/partners/rewards/ai-reward-builder.tsx
- apps/web/ui/partners/rewards/reward-quality.tsx
- apps/web/ui/partners/format-reward-description.ts
- apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
- apps/web/lib/api/sales/calculate-sale-earnings.ts
- apps/web/app/ee/app.dub.co/new-program/slug/program/new/rewards/form.tsx
- apps/web/lib/ai/ai-reward-schema.ts
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 ID | Label | Attributes | Sources |
|---|---|---|---|
lead | Lead | metadata | apps/web/lib/zod/schemas/rewards.ts:52-62 |
partner | Partner | country, totalClicks, totalLeads, totalConversions, totalSaleAmount, totalCommissions | apps/web/lib/zod/schemas/rewards.ts:64-99 |
customer | Customer | country | apps/web/lib/zod/schemas/rewards.ts:101-111 |
sale | Sale | productId, amount, type | apps/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
amountInCentsdefined.amountInPercentageis disallowed, andmaxDurationmust be set to0. - Custom Rewards: Require a valid
configpayload including frequency, interval, and ananchorDatethat cannot be set in the past. Spend limits (spendLimitAmountandspendLimitInterval) andmodifiersare disallowed, andmaxDurationcannot be0. - Sale Rewards: Require either
amountInCentsoramountInPercentageto be provided, but both cannot be used simultaneously. Flat sales mandateamountInCents, while percentage sales mandateamountInPercentage. - 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
| Entity | Mapped Context Source | Handled Attributes | Sources |
|---|---|---|---|
partner | context.partner | Partner metrics, country, metadata | apps/web/lib/partners/evaluate-reward-conditions.ts:98-99 |
customer | context.customer | Country, metadata | apps/web/lib/partners/evaluate-reward-conditions.ts:100-100 |
lead | context.lead | Metadata fields | apps/web/lib/partners/evaluate-reward-conditions.ts:88-88 |
sale | context.sale | Product ID, amount, type, metadata | apps/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
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 Type | Minimum Value Fallback | Maximum Value Fallback | Formatting Logic |
|---|---|---|---|
flat | Infinity (if null) | 0 (if null) | currencyFormatter(max, { trailingZeroDisplay: "stripIfInteger" }) |
percentage | Infinity (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
Group And Link Resolution
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 Category | Trigger Identifier | Description & Behaviour |
|---|---|---|
| Percentage | saleRecorded | Evaluates percentage reward based on recorded sales. |
| Percentage | commissionEarned | Evaluates percentage reward based on earned partner commissions. |
| Flat | partnerApproved | Triggers flat reward immediately upon partner approval (clears maxDuration). |
| Flat | commissionThreshold | Triggers 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 ?? [];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 Type | Value Identifier | Validation Rule | Display Formatting |
|---|---|---|---|
| Flat Rate | flat | rewardAmount != null && !isNaN && rewardAmount >= 0 | Formatted via currencyFormatter in cents. |
| Custom Amount | custom | !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.
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 Type | Condition Clause Formatter | Duration Suffix Logic | Spend Limit Integration |
|---|---|---|---|
sale | formatRewardConditionClause | for the customer's lifetime or for N months / for N years | Appends , up to [amount] [interval] per customer |
referral | formatReferralRewardDescription | for the referred partner's lifetime or one time | None |
custom | formatCustomRewardDescription | for [durationLabel] | None |
lead / click | Default event clause | None | None |
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 Quality | Target Event & Criteria | Visual Badge Styling | Tooltip Summary |
|---|---|---|---|
low | Clicks < $0.03, leads < $1, or percentage sales < 10% | Orange background (bg-orange-100), orange text | May struggle to attract partners |
limited | One-time flat sales or short duration caps (maxDuration === 1) | Orange background (bg-orange-100), orange text | No long term earning potential |
good | Clicks 0.03–0.05, leads 1–10, percentage sales 10–20% | Cyan background (bg-cyan-200), cyan text | Solid reward with room to grow |
strong | Clicks > 0.05, leads > 10, percentage sales > 20% with lifetime/12m+ duration | Green background (bg-green-200), green text | High 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
Related
On This Page
OverviewReward Schema DefinitionCommission and Reward EntitiesValidation Rules and ConstraintsCondition Evaluation PipelineField Value Resolution and Context MappingBoolean Operators and Condition EvaluationOverride Selection and PrecedencePayout Amount CalculationsEarnings Calculation and Base Reward RetrievalModifier Validation and Range FormattingGroup And Link ResolutionHierarchy Resolution and Display FiltersSpend Limits and Duration EnforcementReferral And Bounty RewardsPartner Referral Compensation RulesManual Bounty Submission CriteriaForm Construction And TooltipsRelated