Relevant source files
The following files were used as context for generating this wiki page:
- apps/web/app/ee/api/bounties/bountyId/sync-social-metrics/route.ts
- apps/web/app/ee/api/cron/bounties/sync-social-metrics/route.ts
- apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/bountyId/bounty-submission-details-sheet.tsx
- apps/web/lib/bounty/api/create-bounty-submission.ts
- apps/web/app/ee/api/bounties/route.ts
- apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts
- apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/add-edit-bounty/bounty-criteria-social-metrics.tsx
- apps/web/app/ee/api/partner-profile/programs/programId/bounties/bountyId/social-content-stats/route.ts
- apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/bountyId/bounty-submissions-table.tsx
- apps/web/lib/bounty/api/get-social-metrics-updates.ts
- apps/web/app/ee/api/embed/referrals/bounties/bountyId/social-content-stats/route.ts
- apps/web/app/ee/api/cron/bounties/queue-sync-social-metrics/route.ts
- apps/web/app/ee/app.dub.co/embed/referrals/bounties/submission-form.tsx
- apps/web/lib/zod/schemas/bounties.ts
- apps/web/lib/bounty/api/approve-bounty-submission.ts
- apps/web/ui/partners/bounties/bounty-submission-details-sheet.tsx
- apps/web/lib/bounty/social-metrics-milestones.ts
- apps/web/ui/partners/bounties/bounty-social-content.tsx
- apps/web/app/ee/api/embed/referrals/bounties/bountyId/submissions/route.ts
- apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/add-edit-bounty/bounty-criteria.tsx
- apps/web/app/ee/app.dub.co/embed/referrals/bounties/detail.tsx
- apps/web/ui/partners/bounties/claim-bounty-sheet.tsx
- apps/web/app/ee/app.dub.co/embed/referrals/bounties/submission-detail.tsx
- apps/web/ui/partners/bounties/use-social-metrics-milestones.ts
- apps/web/ui/partners/bounties/bounty-submission-requirements.tsx
- apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts
- apps/web/app/ee/app.dub.co/embed/referrals/bounties/index.tsx
- apps/web/prisma/schema/bounty.prisma
- apps/web/ui/partners/bounties/evaluate-social-content-requirements.ts
- apps/web/ui/partners/bounties/bounty-social-content-preview.tsx
Overview
The bounties and submissions subsystem in Dub powers partner incentive programs by supporting both performance-based tracking and social content verification apps/web/prisma/schema/bounty.prisma:1-4, apps/web/lib/bounty/api/create-bounty-submission.ts. It enables program operators to define flexible reward structures, configure automated submissions apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts, and synchronize external metrics via scheduled cron jobs apps/web/app/ee/api/cron/bounties/sync-social-metrics/route.ts. Partners can submit proof through file uploads, custom URLs, or social media links tied to verified accounts, while program managers inspect submissions apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/[bountyId]/bounty-submission-details-sheet.tsx, manage milestone approvals apps/web/lib/bounty/api/approve-bounty-submission.ts, and trigger commission payouts.
Bounty Data Models and Periodicity
Bounty Types and Performance Scopes
The Bounty data model supports two primary variants defined by BountyType: performance tracking and manual submission verification apps/web/prisma/schema/bounty.prisma:1-4. Performance bounties evaluate partner metrics using a BountyPerformanceScope which can be set to either new or lifetime activity apps/web/prisma/schema/bounty.prisma:6-9. Lifecycle and availability are further controlled by BountyStartMode, supporting absolute calendar dates (startsAt and endsAt) or relative durations calculated via endsAfterDays apps/web/prisma/schema/bounty.prisma:32-35, apps/web/prisma/schema/bounty.prisma:44-47.
| Enum / Type | Values | Description |
|---|---|---|
BountyType | performance, submission | Distinguishes automated performance tracking from manual proof submissions apps/web/prisma/schema/bounty.prisma:1-4. |
BountyPerformanceScope | new, lifetime | Determines whether performance metrics evaluate new actions or cumulative history apps/web/prisma/schema/bounty.prisma:6-9. |
BountyStartMode | absolute, relative | Controls schedule boundaries using fixed timestamps or relative day offsets apps/web/prisma/schema/bounty.prisma:32-35. |
Sources: apps/web/prisma/schema/bounty.prisma:1-35
Note
During updates via updateBountySchema, the bounty type and performanceScope are strictly omitted and cannot be modified after initial creation apps/web/lib/zod/schemas/bounties.ts:221-226.
Sources: apps/web/lib/zod/schemas/bounties.ts:221-226
Submission Lifecycles and Frequencies
Bounty submissions progress through specific lifecycle states defined by BountySubmissionStatus, accompanied by categorized failure logging via BountySubmissionRejectionReason apps/web/prisma/schema/bounty.prisma:11-24. Periodicity and recurrence are structured using BountySubmissionFrequency alongside a 1-indexed periodNumber apps/web/prisma/schema/bounty.prisma:26-30, apps/web/prisma/schema/bounty.prisma:110. Uniqueness constraints on submissions enforce a strict one-submission-per-period rule per partner and bounty apps/web/prisma/schema/bounty.prisma:124.
| Status / Reason Enum | Values | Purpose |
|---|---|---|
BountySubmissionStatus | draft, submitted, approved, rejected | Represents the execution and review state of a partner's submission apps/web/prisma/schema/bounty.prisma:11-16. |
BountySubmissionRejectionReason | invalidProof, duplicateSubmission, outOfTimeWindow, didNotMeetCriteria, other | Categorizes why a submission failed operator review apps/web/prisma/schema/bounty.prisma:18-24. |
BountySubmissionFrequency | day, week, month | Defines recurring temporal intervals for periodic bounty submissions apps/web/prisma/schema/bounty.prisma:26-30. |
Sources: apps/web/prisma/schema/bounty.prisma:11-30
Caution
The database schema enforces a unique composite index @@unique([bountyId, partnerId, periodNumber]) on BountySubmission, preventing duplicate active or draft submissions within the exact same periodic window apps/web/prisma/schema/bounty.prisma:124.
Sources: apps/web/prisma/schema/bounty.prisma:124
Partner Submission Ingestion and Validation
Submission Pipeline Execution Flow
The BountySubmissionHandler coordinates submission ingestion and validation apps/web/lib/bounty/api/create-bounty-submission.ts:48-111. The submit method executes a sequential series of validation checks and state operations before persisting the record:
Sources: apps/web/lib/bounty/api/create-bounty-submission.ts:91-111
Warning
If a submission exists for the finalPeriodNumber, persist() performs an update on the existing record's ID instead of creating a duplicate row apps/web/lib/bounty/api/create-bounty-submission.ts:587-607.
Sources: apps/web/lib/bounty/api/create-bounty-submission.ts:587-607
Requirement Rules and UI Integration
Partners interact with the submission pipeline via EmbedBountySubmissionForm or the claim bounty sheet UI apps/web/app/ee/app.dub.co/embed/referrals/bounties/submission-form.tsx:68-88, apps/web/ui/partners/bounties/claim-bounty-sheet.tsx:364-369. Requirements are parsed dynamically to enforce file uploads and URL constraints apps/web/ui/partners/bounties/bounty-submission-requirements.tsx:5-38.
| Requirement Type | Schema Field | Validation Rule |
|---|---|---|
| Image | submissionRequirements.image | Optional max limit; validates attached file array size when required apps/web/ui/partners/bounties/bounty-submission-requirements.tsx:11-17. |
| URL | submissionRequirements.url | Optional max limit and required allowed domain whitelist apps/web/ui/partners/bounties/bounty-submission-requirements.tsx:19-28. |
| Social Content | bountyInfo.hasSocialMetrics | Requires a connected partner account platform link posted after the bounty start date apps/web/ui/partners/bounties/bounty-submission-requirements.tsx:30-35. |
Sources: apps/web/ui/partners/bounties/bounty-submission-requirements.tsx:5-38
Note
Social metrics bounties automatically bypass manual file requirements and restrict URL inputs to a single content link, syncing external metrics directly during validation apps/web/lib/bounty/api/create-bounty-submission.ts:543-561, apps/web/lib/bounty/api/create-bounty-submission.ts:578-581.
Sources: apps/web/lib/bounty/api/create-bounty-submission.ts:543-561, apps/web/lib/bounty/api/create-bounty-submission.ts:578-581
Social Content Verification
Social content requirements evaluate whether a post belongs to the partner's verified platform handle and meets temporal boundaries apps/web/ui/partners/bounties/evaluate-social-content-requirements.ts:8-32.
export function evaluateSocialContentRequirements({
content,
bounty,
partnerPlatform,
}: {
content?: SocialContent | null;
bounty: Pick<PartnerBountyProps, "startsAt">;
partnerPlatform?: Pick<PartnerPlatformProps, "identifier" | "verifiedAt">;
}) {
const isPostedFromYourAccount =
!!content &&
!!partnerPlatform &&
!!partnerPlatform.verifiedAt &&
partnerPlatform.identifier.toLowerCase() === content.handle?.toLowerCase();
const isAfterStartDate =
!!content?.publishedAt &&
!!bounty.startsAt &&
!isBefore(content.publishedAt, bounty.startsAt);
return {
isPostedFromYourAccount,
isAfterStartDate,
};
}Sources: apps/web/ui/partners/bounties/evaluate-social-content-requirements.ts:8-32
Social Content Metrics and Synchronization
Stats Scraping and API Endpoints
Social content metrics require periodic scraping of external platform stats. Endpoints such as GET /api/partner-profile/programs/[programId]/bounties/[bountyId]/social-content-stats and GET /api/embed/referrals/bounties/[bountyId]/social-content-stats validate partner credentials, enforce rate limits via RATELIMIT_POLICIES.socialContentStats, verify bounty eligibility, and query external platform metrics using getSocialContent.
Sources: apps/web/app/ee/api/partner-profile/programs/programId/bounties/bountyId/social-content-stats/route.ts:21-91, apps/web/app/ee/api/embed/referrals/bounties/bountyId/social-content-stats/route.ts:21-80
Warning
Live metric scraping requests are rate-limited per partner identifier to prevent abuse of external scrapers when partners test or preview submission links.
Sources: apps/web/app/ee/api/partner-profile/programs/programId/bounties/bountyId/social-content-stats/route.ts:27-30, apps/web/app/ee/api/embed/referrals/bounties/bountyId/social-content-stats/route.ts:26-29
Cron Scheduling and Queueing
The synchronization of social metrics runs periodically through background jobs managed via QStash. The GET /api/cron/bounties/queue-sync-social-metrics route queries active submission bounties containing social metrics requirements and chunks them into batches of 100 to enqueue jobs to /api/cron/bounties/sync-social-metrics.
Sources: apps/web/app/ee/api/cron/bounties/queue-sync-social-metrics/route.ts:12-45
export const GET = withCron(async () => {
const bounties = await prisma.bounty.findMany({
where: {
type: "submission",
submissionRequirements: {
path: "$.socialMetrics",
not: Prisma.JsonNull,
},
...buildBountyActivePeriodWhere(),
},
select: {
id: true,
},
});
if (bounties.length === 0) {
return logAndRespond("No bounties to sync social metrics for.");
}
const chunks = chunk(bounties, 100);
for (const chunk of chunks) {
await enqueueBatchJobs(
chunk.map((bounty) => ({
queueName: "sync-bounty-social-metrics",
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/bounties/sync-social-metrics`,
deduplicationId: bounty.id,
body: {
bountyId: bounty.id,
},
})),
);
}
return logAndRespond(
`Queued ${bounties.length} bounties to sync social metrics.`,
);
});Sources: apps/web/app/ee/api/cron/bounties/queue-sync-social-metrics/route.ts:12-48
Synchronization Call-Chain Execution
When synchronizing metrics for a bounty or individual submission, the system evaluates earning caps and milestones.
POSTrequest or cron handler invokeshasReachedSocialMetricsEarningCapwith the bounty and submission.hasReachedSocialMetricsEarningCapcallsgetSocialMetricsEarningCap.getSocialMetricsEarningCapinvokesgetSocialMetricsMilestonesto retrieve all payable milestones.getSocialMetricsMilestonesresolves bounty details and extracts configured thresholds and bonuses.
Sources: apps/web/app/ee/api/bounties/bountyId/sync-social-metrics/route.ts:123-129, apps/web/lib/bounty/social-metrics-milestones.ts:84-109
Sources: apps/web/app/ee/api/bounties/bountyId/sync-social-metrics/route.ts:123-129, apps/web/lib/bounty/social-metrics-milestones.ts:33-92
Milestone Calculation and Submission Approval
Milestone Generation and Structure
The system calculates payable milestones for social metrics bounties by establishing an initial base tier followed by incremental bonus tiers. The function getSocialMetricsMilestones extracts the configured minCount and rewardAmount from bounty details to form the primary base milestone where fromThreshold is 0. If an incrementalBonus object is defined with valid parameters including incrementCount, bonusPerIncrement, and maxCount, additional milestone tiers are generated sequentially starting from minCount + incrementCount up to maxCount. Each intermediate milestone records its preceding boundary in fromThreshold, the target metric count in threshold, and the payout in rewardAmount.
Sources: apps/web/lib/bounty/social-metrics-milestones.ts:33-81
export function getSocialMetricsMilestones(
bounty: BountyInfoInput | undefined | null,
): SocialMetricsMilestone[] {
const bountyInfo = resolveBountyDetails(bounty);
const socialMetrics = bountyInfo?.socialMetrics;
const rewardAmount = bounty?.rewardAmount ?? 0;
if (!socialMetrics?.metric || !socialMetrics.minCount || rewardAmount <= 0) {
return [];
}
const { minCount, incrementalBonus } = socialMetrics;
const milestones: SocialMetricsMilestone[] = [
{
fromThreshold: 0,
threshold: minCount,
rewardAmount,
},
];
if (incrementalBonus) {
const { incrementCount, bonusPerIncrement, maxCount } = incrementalBonus;
const hasValidIncrementalBonus =
incrementCount != null &&
bonusPerIncrement != null &&
maxCount != null &&
incrementCount > 0;
if (hasValidIncrementalBonus) {
for (
let t = minCount + incrementCount;
t <= maxCount;
t += incrementCount
) {
milestones.push({
fromThreshold: t - incrementCount,
threshold: t,
rewardAmount: bonusPerIncrement,
});
}
}
}
return milestones;
}Sources: apps/web/lib/bounty/social-metrics-milestones.ts:33-81
Note
The earning cap for a social metrics bounty is determined by getSocialMetricsEarningCap, which evaluates the threshold value of the final element in the generated milestones array. When a partner submission reaches or exceeds this metric count, hasReachedSocialMetricsEarningCap returns true and disables further synchronization needs.
Sources: apps/web/lib/bounty/social-metrics-milestones.ts:84-109
Submission Approval and Commission Queueing
When a program operator approves milestones for a submission through approveSocialMetricsMilestones, the system identifies pending tiers via getPendingSocialMetricsMilestones where the milestone threshold falls between the submission's current socialMetricCount and its previously approvedSocialMetricThreshold. The approval transaction updates the submission record, advancing the approvedSocialMetricThreshold to the highest pending milestone's threshold and setting the submission status to approved if completesEarningCap is met, or keeping it submitted for intermediate progress.
Sources: apps/web/lib/bounty/api/approve-bounty-submission.ts:204-274
Concurrently, a descriptive commission entry is generated using buildMilestonesCommissionDescription and enqueued via queuePartnerCommissionCreation. The total reward is calculated by summing the rewardAmount across all pending milestones in the batch.
Sources: apps/web/lib/bounty/api/approve-bounty-submission.ts:271-304
const rewardAmount = pendingMilestones.reduce(
(sum, { rewardAmount }) => sum + rewardAmount,
0,
);
const description = buildMilestonesCommissionDescription({
bountyName: bounty.name,
metric,
milestone: {
fromThreshold: firstPendingMilestone.fromThreshold,
threshold: approvedThreshold,
},
});
await queuePartnerCommissionCreation({
event: "custom",
partnerId: submission.partnerId,
programId: submission.programId,
amount: rewardAmount,
quantity: 1,
userId: user.id,
source: CommissionSource.user,
description,
bountySubmissionId: submissionId,
metadata: {
socialMetrics: {
metric,
fromThreshold: firstPendingMilestone.fromThreshold,
threshold: approvedThreshold,
milestones: pendingMilestones,
},
},
});Sources: apps/web/lib/bounty/api/approve-bounty-submission.ts:271-304
Performance Submissions and Automated Upserts
QStash Trigger Pipeline for Draft Submissions
The system automates performance-based referral tracking by triggering draft bounty submissions through QStash. When triggerDraftBountySubmissionCreation is invoked, it evaluates program enrollments and finds matching performance bounties that use lifetime statistics (BountyPerformanceScope.lifetime). Eligible partner identifiers are collected, and individual JSON publish requests are dispatched to the cron route /api/cron/bounties/upsert-draft-submissions.
const results = await Promise.allSettled(
bountySubmissionCreationData.map(({ bountyId, partnerIds }) =>
qstash.publishJSON({
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/bounties/upsert-draft-submissions`,
body: {
bountyId,
partnerIds,
},
...(idempotencyKey && {
deduplicationId: `${idempotencyKey}-${bountyId}`,
}),
}),
),
);Sources: apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts:131-144
Note
If an idempotencyKey is provided when triggering draft submissions, QStash uses a composite deduplication identifier (${idempotencyKey}-${bountyId}) to prevent duplicate execution of the same bounty batch.
Sources: apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts:15-20, apps/web/lib/bounty/api/trigger-draft-bounty-submissions.ts:139-141
Cron Upsert Route Processing
The POST endpoint at /api/cron/bounties/upsert-draft-submissions validates incoming QStash signatures, parses request bodies with Zod, and queries the target bounty along with its workflow trigger conditions and program settings.
const schema = z.object({
bountyId: z.string(),
partnerIds: z.array(z.string()).optional(),
page: z.number().optional().default(0),
});
const MAX_PAGE_SIZE = 100;Sources: apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:20-26
The route enforces several guard clauses before planning submission upserts:
- Bounties starting 10 or more minutes in the future are deferred.
- Non-performance bounties or bounties restricted to new stats (
performanceScope === "new") skip submission creation. - Bounties missing associated workflow triggers are aborted.
Sources: apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:68-90
Program enrollments are fetched matching commission-eligible statuses (COMMISSION_ELIGIBLE_ENROLLMENT_STATUSES), grouped by group IDs and partner tag filters, and paginated up to MAX_PAGE_SIZE (100). Link metrics—including clicks, sales, leads, conversions, and sale amounts—are aggregated via aggregatePartnerLinksStats and planned into create or update actions through planDraftBountySubmissionUpserts.
const createdBountySubmissions =
toCreate.length > 0
? await prisma.bountySubmission.createMany({
data: toCreate,
skipDuplicates: true,
})
: { count: 0 };
if (toUpdate.length > 0) {
await Promise.allSettled(
toUpdate.map((update) =>
prisma.bountySubmission.update({
where: {
id: update.id,
status: "draft", // in case of race condition, we don't want to update an already submitted entry
},
data: {
performanceCount: update.performanceCount,
...(update.promoteToSubmitted && {
status: "submitted",
completedAt: new Date(),
}),
},
}),
),
);
}Sources: apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:203-228
Caution
When updating existing draft submissions, the update query explicitly checks status: "draft" to prevent race conditions from overwriting submissions that partners have already promoted to submitted status.
Sources: apps/web/app/ee/api/cron/bounties/upsert-draft-submissions/route.ts:214-218
Program Operator Review and UI
Submission Inspection and Rich Previews
Program operators inspect partner bounty submissions through dedicated management views, such as BountySubmissionDetailsSheet and BountySubmissionsTable. For bounties with social metrics requirements, the inspection interface automatically renders rich platform embeds via BountySocialContentPreview. This component parses submission URLs to generate embedded iframes for supported social platforms, applying platform-specific aspect ratios.
function getSocialContentEmbedAspectRatio({
platform,
url,
}: GetSocialContentEmbedAspectRatioParams) {
try {
const parsed = new URL(url);
const pathname = parsed.pathname;
if (platform === "youtube") {
return pathname.includes("/shorts/") ? "aspect-[9/16]" : "aspect-video";
}
if (platform === "tiktok") {
return "aspect-[9/16]";
}
if (platform === "instagram") {
return pathname.includes("/reel/") ? "aspect-[9/16]" : "aspect-square";
}
if (platform === "twitter") {
return "aspect-square";
}
if (platform === "linkedin") {
return "aspect-video";
}
return "aspect-video";
} catch {
return "aspect-video";
}
}Sources: apps/web/ui/partners/bounties/bounty-social-content-preview.tsx:121-153
Note
The embed generator supports exact hostnames and URL path patterns for youtube, instagram, tiktok, twitter, and linkedin, extracting unique post or video identifiers to construct clean iframe source URLs.
Sources: apps/web/ui/partners/bounties/bounty-social-content-preview.tsx:31-118
Status Badges and Filtering
Operator dashboards categorize submissions using BountySubmissionStatusBadges, providing quick visibility into submission lifecycles. Table filters built via BountySubmissionFilters allow operators to isolate submissions awaiting review and refresh social metrics synchronization status on demand.
| Status / Filter Element | Target State / Component | Purpose |
|---|---|---|
submitted | Pending review | Isolates submissions that require program manager moderation |
approved | Approved | Indicates submissions that have successfully passed inspection |
rejected | Rejected | Denotes submissions declined by operators |
| Awaiting review button | BountySubmissionFilters | Quick-action button displaying the live count of pending submissions |
Sources: apps/web/app/app.dub.co/dashboard/slug/ee/program/bounties/bountyId/bounty-submissions-table.tsx:508-544, apps/web/app/ee/app.dub.co/embed/referrals/bounties/submission-detail.tsx:214-217
Related
On This Page
OverviewBounty Data Models and PeriodicityBounty Types and Performance ScopesSubmission Lifecycles and FrequenciesPartner Submission Ingestion and ValidationSubmission Pipeline Execution FlowRequirement Rules and UI IntegrationSocial Content VerificationSocial Content Metrics and SynchronizationStats Scraping and API EndpointsCron Scheduling and QueueingSynchronization Call-Chain ExecutionMilestone Calculation and Submission ApprovalMilestone Generation and StructureSubmission Approval and Commission QueueingPerformance Submissions and Automated UpsertsQStash Trigger Pipeline for Draft SubmissionsCron Upsert Route ProcessingProgram Operator Review and UISubmission Inspection and Rich PreviewsStatus Badges and FilteringRelated