Skip to main content
doc0blade47/dubdocumentation home
Sign in

Bounties & Submissions

Last updated Oct 12, 2026100% grounded
Relevant source files

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

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 / TypeValuesDescription
BountyTypeperformance, submissionDistinguishes automated performance tracking from manual proof submissions apps/web/prisma/schema/bounty.prisma:1-4.
BountyPerformanceScopenew, lifetimeDetermines whether performance metrics evaluate new actions or cumulative history apps/web/prisma/schema/bounty.prisma:6-9.
BountyStartModeabsolute, relativeControls 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 EnumValuesPurpose
BountySubmissionStatusdraft, submitted, approved, rejectedRepresents the execution and review state of a partner's submission apps/web/prisma/schema/bounty.prisma:11-16.
BountySubmissionRejectionReasoninvalidProof, duplicateSubmission, outOfTimeWindow, didNotMeetCriteria, otherCategorizes why a submission failed operator review apps/web/prisma/schema/bounty.prisma:18-24.
BountySubmissionFrequencyday, week, monthDefines 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 TypeSchema FieldValidation Rule
ImagesubmissionRequirements.imageOptional max limit; validates attached file array size when required apps/web/ui/partners/bounties/bounty-submission-requirements.tsx:11-17.
URLsubmissionRequirements.urlOptional max limit and required allowed domain whitelist apps/web/ui/partners/bounties/bounty-submission-requirements.tsx:19-28.
Social ContentbountyInfo.hasSocialMetricsRequires 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.

  1. POST request or cron handler invokes hasReachedSocialMetricsEarningCap with the bounty and submission.
  2. hasReachedSocialMetricsEarningCap calls getSocialMetricsEarningCap.
  3. getSocialMetricsEarningCap invokes getSocialMetricsMilestones to retrieve all payable milestones.
  4. getSocialMetricsMilestones resolves 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 ElementTarget State / ComponentPurpose
submittedPending reviewIsolates submissions that require program manager moderation
approvedApprovedIndicates submissions that have successfully passed inspection
rejectedRejectedDenotes submissions declined by operators
Awaiting review buttonBountySubmissionFiltersQuick-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

Was this page helpful?