Skip to content
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e7198ad
Add social engagement tracking for partner platforms
devkiran Mar 6, 2026
10bd7bd
Update backfill-twitter-platform-ids.ts
devkiran Mar 6, 2026
97a4731
Update route.ts
devkiran Mar 6, 2026
3b6d4f7
Merge branch 'main' into partner-historical-social-engagement
devkiran Mar 9, 2026
527a96b
Add per-post engagement tracking and median baselines for fraud detec…
devkiran Mar 9, 2026
973fd98
Consolidate X platform adapter, client, and schema into single file
devkiran Mar 9, 2026
a1467d8
Update route.ts
devkiran Mar 9, 2026
692e9ac
Improve X API error handling with structured error typing
devkiran Mar 9, 2026
de4182c
Move social content fetching into per-platform adapters
devkiran Mar 9, 2026
4554d3a
Add bounty submission fraud detection and risk fields
devkiran Mar 9, 2026
037e15f
Move social profile fetching into per-platform adapters
devkiran Mar 9, 2026
7da7da1
update the cron path
devkiran Mar 9, 2026
f59f266
Update route.ts
devkiran Mar 9, 2026
1d4fa8c
Update get-social-content.ts
devkiran Mar 9, 2026
ef6a48f
Move getSocialProfile and getSocialContent to lib/social-platforms
devkiran Mar 9, 2026
a28546a
Update get-social-profile.ts
devkiran Mar 9, 2026
da58a78
Fix code review issues: batch upserts, type safety, pruning, and cleanup
devkiran Mar 9, 2026
d139b4b
Update route.ts
devkiran Mar 9, 2026
b6af9be
Update route.ts
devkiran Mar 9, 2026
4da7b86
Update x-adapter.ts
devkiran Mar 9, 2026
f5badeb
cleanup
devkiran Mar 9, 2026
1119560
Update backfill-twitter-platform-ids.ts
devkiran Mar 9, 2026
4abf034
Update route.ts
devkiran Mar 9, 2026
c5ba400
rename the types
devkiran Mar 9, 2026
1f39948
move to /api
devkiran Mar 9, 2026
f69935a
Integrate fraud detection into all bounty submission flows
devkiran Mar 9, 2026
34d4834
remove noBaselineHistory
devkiran Mar 9, 2026
7736164
Update detect-bounty-submission-fraud.ts
devkiran Mar 9, 2026
63c2f2f
format
devkiran Mar 9, 2026
c9077b4
Merge branch 'main' into partner-historical-social-engagement
devkiran Mar 10, 2026
0ef3682
Bounty social sync: verified platforms only, engagement overlap windo…
devkiran Mar 10, 2026
def5ce5
Split YouTube and X adapters into schemas/client; add YouTube queue-s…
devkiran Mar 10, 2026
ad649af
Replace $transaction with Promise.allSettled to avoid Vitess timeout
devkiran Mar 10, 2026
224673f
Fix X adapter comments mapping and paginate queue-sync
devkiran Mar 10, 2026
1f7d0a0
Update route.ts
devkiran Mar 10, 2026
875bcbc
format
devkiran Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ YOUTUBE_API_KEY=
# Scrape Creators
SCRAPECREATORS_API_KEY=

# X API (Twitter) - for social engagement tracking
X_API_BEARER_TOKEN=
Comment thread
devkiran marked this conversation as resolved.

# Paypal
PAYPAL_CLIENT_ID=
PAYPAL_CLIENT_SECRET=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DubApiError } from "@/lib/api/errors";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { parseRequestBody } from "@/lib/api/utils";
import { withWorkspace } from "@/lib/auth";
import { detectBountySubmissionFraud } from "@/lib/bounty/api/detect-bounty-submission-fraud";
import { getBountyOrThrow } from "@/lib/bounty/api/get-bounty-or-throw";
import { getSocialMetricsUpdates } from "@/lib/bounty/api/get-social-metrics-updates";
import { resolveBountyDetails } from "@/lib/bounty/utils";
Expand Down Expand Up @@ -51,6 +52,7 @@ export const POST = withWorkspace(
id: true,
urls: true,
status: true,
partnerId: true,
partner: true,
},
},
Expand Down Expand Up @@ -142,6 +144,34 @@ export const POST = withWorkspace(
socialMetricsLastSyncedAt,
};

if (socialMetricCount != null) {
const partnerPlatformBaseline = await prisma.partnerPlatform.findUnique(
{
where: {
partnerId_type: {
partnerId: submission.partnerId,
type: bountyInfo.socialPlatform!.value,
},
},
select: {
medianViews: true,
medianLikes: true,
medianComments: true,
medianEngagementRate: true,
subscribers: true,
},
},
);
Comment thread
devkiran marked this conversation as resolved.

const fraudResult = detectBountySubmissionFraud({
socialMetricCount,
bountyMetric: bountyInfo.socialMetrics!.metric,
partnerPlatform: partnerPlatformBaseline,
});
updateData.fraudRiskLevel = fraudResult.fraudRiskLevel;
updateData.fraudFlags = fraudResult.fraudFlags;
}

const hasMetCriteria =
socialMetricCount != null &&
bountyInfo.socialMetrics?.minCount != null &&
Expand Down
37 changes: 37 additions & 0 deletions apps/web/app/(ee)/api/cron/bounties/sync-social-metrics/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { detectBountySubmissionFraud } from "@/lib/bounty/api/detect-bounty-submission-fraud";
import { getSocialMetricsUpdates } from "@/lib/bounty/api/get-social-metrics-updates";
import { resolveBountyDetails } from "@/lib/bounty/utils";
import { qstash } from "@/lib/cron";
Expand Down Expand Up @@ -70,6 +71,7 @@ export const POST = withCron(async ({ rawBody }) => {
urls: true,
socialMetricCount: true,
status: true,
partnerId: true,
partner: {
select: {
email: true,
Expand Down Expand Up @@ -99,6 +101,31 @@ export const POST = withCron(async ({ rawBody }) => {
submissions,
});

// Batch-fetch partner platform baselines for fraud detection
const partnerIds = [...new Set(submissions.map((s) => s.partnerId))];
const partnerPlatforms = await prisma.partnerPlatform.findMany({
where: {
partnerId: {
in: partnerIds,
},
type: bountyInfo.socialPlatform!.value,
verifiedAt: {
not: null,
},
},
select: {
partnerId: true,
medianViews: true,
medianLikes: true,
medianComments: true,
medianEngagementRate: true,
subscribers: true,
},
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const baselineByPartnerId = new Map(
partnerPlatforms.map((p) => [p.partnerId, p]),
);

const minCount = bountyInfo.socialMetrics?.minCount;

if (!minCount) {
Expand Down Expand Up @@ -134,6 +161,16 @@ export const POST = withCron(async ({ rawBody }) => {
socialMetricsLastSyncedAt,
};

if (socialMetricCount != null) {
const fraudResult = detectBountySubmissionFraud({
socialMetricCount,
bountyMetric: bountyInfo.socialMetrics!.metric,
partnerPlatform: baselineByPartnerId.get(submission.partnerId) ?? null,
});
updateData.fraudRiskLevel = fraudResult.fraudRiskLevel;
updateData.fraudFlags = fraudResult.fraudFlags;
}

if (shouldTransitionToSubmitted) {
updateData.status = "submitted";
updateData.completedAt = now;
Expand Down
9 changes: 6 additions & 3 deletions apps/web/app/(ee)/api/cron/partner-platforms/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { qstash } from "@/lib/cron";
import { withCron } from "@/lib/cron/with-cron";
import {
AccountNotFoundError,
getSocialProfile,
} from "@/lib/api/scrape-creators/get-social-profile";
import { qstash } from "@/lib/cron";
import { withCron } from "@/lib/cron/with-cron";
} from "@/lib/social-platforms/get-social-profile";
import { prisma } from "@dub/prisma";
import { APP_DOMAIN_WITH_NGROK } from "@dub/utils";
import { subDays } from "date-fns";
Expand Down Expand Up @@ -89,6 +89,9 @@ export const POST = withCron(async ({ rawBody }) => {
});

const newStats = {
...(socialProfile.platformId && {
platformId: socialProfile.platformId,
}),
subscribers: socialProfile.subscribers,
posts: socialProfile.posts,
avatarUrl: socialProfile.avatarUrl,
Expand Down
45 changes: 45 additions & 0 deletions apps/web/app/(ee)/api/cron/queue-sync-social-engagement/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { enqueueBatchJobs } from "@/lib/cron/enqueue-batch-jobs";
import { withCron } from "@/lib/cron/with-cron";
import { prisma } from "@dub/prisma";
import { APP_DOMAIN_WITH_NGROK } from "@dub/utils";
import { logAndRespond } from "../utils";

export const dynamic = "force-dynamic";

// GET /api/cron/queue-sync-social-engagement
// Runs daily at 07:00 UTC. Queues one sync job per eligible partner platform.
export const GET = withCron(async () => {
const partnerPlatforms = await prisma.partnerPlatform.findMany({
where: {
verifiedAt: {
not: null,
},
platformId: {
not: null,
},
type: {
in: ["twitter"],
},
},
select: {
id: true,
},
});

if (partnerPlatforms.length === 0) {
return logAndRespond("No eligible partner platforms for engagement sync.");
}

const jobs = await enqueueBatchJobs(
partnerPlatforms.map((pp) => ({
queueName: "sync-social-engagement",
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/sync-social-engagement`,
deduplicationId: pp.id,
body: {
partnerPlatformId: pp.id,
},
})),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

return logAndRespond(`Queued ${jobs.length} social engagement sync jobs.`);
});
Loading
Loading