Skip to content

Historical snapshot archived 2026-09-25. This records an earlier review or plan, not current implementation or live ticket state. For current work, follow root AGENTS.md, the relevant BloxClips skill, and owning repository source/tests. Preserve approved decisions as evidence; verify their present authority before acting.

Backend Flow ​

All submission-related API endpoints and internal functions.


Public Endpoints ​

GET /api/submissions ​

File: src/api/routes/submissions.ts:127-235
Auth: requireAuth (cookie/JWT)
AuthZ: User owns submission (userOwnedWhere: webUserId OR discordId)

Query Params:

  • status — filter (PENDING/ACCEPTED/DENIED/FLAGGED)
  • campaignId — filter
  • limit (1-100, default 10)
  • offset (default 0)

Flow:

  1. Build where clause (user + optional filters)
  2. Fetch submissions with campaign select (budget, payout, caps, thresholds)
  3. Fetch ALL accepted submissions for those campaigns (for budget calc)
  4. calculateCampaignEarnings(campaign, allCampaignSubs) → per-submission capped earnings
  5. Apply clipper fee (7%) → estimatedEarnings
  6. Return {submissions: [...], total}

Response: {submissions: SubmissionWithEarnings[], total: number}

Errors: 500 on DB error


POST /api/submissions ​

File: src/api/routes/submissions.ts:237-539
Auth: requireAuth
AuthZ: Authenticated user

Request Body:

json
{ "videoUrl": "https://...", "campaignId": 123 }

Validation: SubmitVideoSchema (Zod) — HTTPS, allowed domains, positive campaignId

Flow:

  1. Parse + validate
  2. Email gate (disabled — commented)
  3. Fetch campaign → must exist, active, acceptingSubmissions, not deleted
  4. detectPlatform(videoUrl) → youtube/tiktok/instagram/unknown
  5. Check platform in campaign.allowedPlatforms
  6. Duplicate checks: a. Same videoLink + campaignId → 400 b. Same videoLink + same user within 10 min → 429
  7. Scrape metadata:
    • YouTube: extractVideoId → getVideoDetails (batch 1)
    • TikTok: scrapeTikTok (Apify) — cached 10 min
    • Instagram: scrapeInstagram (Apify) — cached 10 min
  8. Verify LinkedSocialAccount for (platform, creatorAccountId)
    • Missing → 412 ACCOUNT_VERIFICATION_REQUIRED
    • Owned by other WebUser → 403
  9. Validate video type vs campaign acceptsShorts/acceptsLong
  10. Create Submission (PENDING) with scraped data
  11. Create initial ViewSnapshot
  12. Return 201 + submission

Response: {message, submission}

Errors:

  • 400 — validation, invalid URL, platform not allowed, duplicate, campaign inactive
  • 403 — account owned by another user
  • 412 — account verification required
  • 429 — cross-campaign cooldown
  • 500 — scrape error, DB error

Admin Endpoints ​

GET /api/admin/submissions/review ​

File: src/api/routes/admin.ts (search for "submissions/review")
Auth: requireAuth + requireAdmin
Query Params: campaignFilter, status, sortBy, sortDirection, page, pageSize

Flow:

  1. Build where clause (status filter, campaign filter)
  2. Paginated fetch with campaign + user info
  3. Return submissions + campaigns summary + stats + pagination

Response: {submissions: Submission[], campaigns: CampaignSummary[], stats: ReviewStats, pagination}


PUT /api/admin/submissions/:submissionId/status ​

File: src/api/routes/admin.ts:1459-1627
Auth: requireAuth + requireAdmin + auditLog('UPDATE_SUBMISSION_STATUS', 'SUBMISSION')
Body: {status: "ACCEPTED"|"DENIED"|"FLAGGED"|"PENDING", reason?: string}
Validation: UpdateSubmissionStatusSchema (Zod enum)

Flow:

  1. Fetch submission + campaign
  2. If ACCEPTED: a. Budget pre-check (getCampaignSpend) → 409 if ≥100% b. Update: status=ACCEPTED, acceptedAt (or now), nextPollAt=now c. Budget clamp on accept:
    • getCampaignSpend → remaining budget
    • If currentViews would exceed → frozenViewCount = maxViews
    • campaign.acceptingSubmissions=false, viewsFrozen=true, viewsFrozenAt=now d. checkAndCloseCampaign (95% threshold) e. Create UserNotification (SUBMISSION_ACCEPTED)
  3. Else: update status only
  4. Audit log: SUBMISSION_{NEWSTATUS} with previousStatus, reason, campaignId, videoLink
  5. Return {success, submissionId, status}

Side Effects on ACCEPT:

  • Tracking starts immediately (nextPollAt=now)
  • Budget clamp may freeze campaign
  • User notified in-app
  • Audit trail

Errors: 404 (not found), 409 (budget full), 500


PUT /api/admin/submissions/:submissionId/views ​

File: src/api/routes/admin.ts:1308-1345
Auth: requireAuth + requireAdmin + auditLog('UPDATE_SUBMISSION_VIEWS', 'SUBMISSION')
Body: {manualViewCount: number | null} (null = revert)
Validation: UpdateViewCountSchema

Flow: Direct prisma.submission.update({manualViewCount})

Use Case: Admin corrects view count (e.g., known bot traffic)


PUT /api/admin/submissions/:submissionId/rate ​

File: src/api/routes/admin.ts:1347-1403
Auth: requireAuth + requireAdmin + auditLog('UPDATE_SUBMISSION_RATE', 'SUBMISSION')
Body: {customRate: number | null} (null = use campaign default)
Validation: UpdateCustomRateSchema (0-1000)

Flow: Update customRate; logs to console

Use Case: Override RPM for specific submission


PUT /api/admin/submissions/:submissionId/cap ​

File: src/api/routes/admin.ts:1406-1457
Auth: requireAuth + requireAdmin + auditLog('UPDATE_SUBMISSION_CAP', 'SUBMISSION')
Body: {customViewCap: number | null}
Validation: UpdateCustomViewCapSchema

Flow: Update customViewCap; returns campaign default for reference


GET /api/admin/submissions/:submissionId/snapshots ​

File: src/api/routes/admin.ts:1631-1647
Auth: requireAuth + requireAdmin

Flow: prisma.viewSnapshot.findMany({where: {submissionId}, orderBy: {snapshotDate: asc}})

Response: {snapshots: {snapshotDate, viewCount, likes, comments}[]}


Internal Functions (Non-HTTP) ​

runTrackingTick() ​

File: src/utils/tracking/runTrackingTick.ts
Trigger: Scheduler every 30 min (src/scheduler.ts:17-36)

Flow:

  1. Query due ACCEPTED submissions (nextPollAt <= now, not stopped, not frozen)
  2. Filter expired (acceptedAt + trackingDurationDays)
  3. Group by platform
  4. YouTube: batch getVideoDetails (50 IDs) → applySuccess
  5. TikTok: batch scrapeTikTok → applySuccess
  6. Instagram: batch scrapeInstagram → applySuccess
  7. applySuccess:
    • computeScrapeBudgetClamp(submissionId, newViews)
    • Transaction: update submission + create ViewSnapshot
    • If clamp froze campaign → markCampaignFrozen
  8. applyFailure:
    • consecutiveScrapeFailures++
    • If ≥3 → FLAGGED, trackingStoppedAt, nextPollAt=null
    • Else → nextPollAt = now + 24h

Stats Returned: {polled, succeeded, failed, expired, flagged, frozen}


computeScrapeBudgetClamp() ​

File: src/utils/campaignBudget.ts:130-225

Inputs: submissionId, newViews

Logic: First-come-first-earned

  1. If campaign frozen → alreadyFrozen=true
  2. Sum sibling ACCEPTED submissions' spend (excluding this)
  3. remainingBudget = budgetLimit - spendExcludingThis
  4. maxViewsByBudget = floor(remainingBudget * 1000 / (ratePerK * burn))
  5. If cappedNewViews <= maxViewsByBudget → no clamp
  6. Else → frozenViewCount = maxViewsByBudget, didFreezeCampaign=true

Returns: {finalCurrentViews, frozenViewCount, didFreezeCampaign, alreadyFrozen}


checkAndCloseCampaign() ​

File: src/utils/campaignBudget.ts:247-279

Trigger: After accept, after budget clamp

Logic:

  1. getCampaignSpend → shouldClose (≥95%)
  2. If shouldClose and acceptingSubmissions=true → set false
  3. If NOT shouldClose and acceptingSubmissions=false and % < 95 → set true (re-open)

calculateCampaignEarnings() ​

File: src/utils/calculateSubmissionEarnings.ts:43-96

Inputs: Campaign config, all accepted submissions for campaign

Algorithm:

  1. Sort submissions by createdAt (first approved = first priority)
  2. For each: rawEarnings = (cappedViews / 1000) * ratePerK
  3. remainingBudget = budgetLimit - runningSpend
  4. maxEarningsFromRemaining = remainingBudget / burn
  5. cappedEarnings = min(rawEarnings, maxEarningsFromRemaining)
  6. runningSpend += cappedEarnings * burn

Threshold Gate: If actual views < minViewsShorts/Long → earnings = 0


processPayoutRequest() ​

File: src/utils/payouts/processRequest.ts:61-225

Trigger: Async after POST /api/payouts/request creates Payout (REQUESTED)

Flow:

  1. Update Payout → SCRAPING, scrapeStartedAt=now
  2. Fetch eligible submissions (ACCEPTED, paidOut=false, user-owned)
  3. rescrapeForPayout() → fresh views per submission
  4. Re-fetch submissions (capture frozenViewCount from rescrape)
  5. Build PayoutItem per submission:
    • payableViews = min(manualViewCount ?? frozenViewCount ?? viewsAtPayout, viewCap)
    • viewsCounted = max(0, payableViews - priorPaidViews)
    • grossAmount = (viewsCounted/1000) * ratePerK
    • netAmount = applyClipperFee(grossAmount)
    • Badges: ratio (likes/views, comments/views) + spike (snapshot history)
  6. Bulk create PayoutItems
  7. Sum net for available items (exclude unavailable)
  8. If totalNet < $100 → BELOW_THRESHOLD Else → READY_FOR_REVIEW

rescrapeForPayout() ​

File: src/utils/payouts/rescrape.ts:140-291

Similar to tracking tick but:

  • Skips frozen campaigns / tracking-stopped / frozen submissions (uses last-known)
  • Applies budget clamp per submission
  • Returns RescrapeOutcome with unavailableReason (VIDEO_DELETED/PRIVATE/SCRAPE_FAILED)

Call Graph: Submission Creation ​

POST /api/submissions
  └─ validate (Zod)
  └─ fetch campaign
  └─ detectPlatform
  └─ check platform allowed
  └─ duplicate check (campaign + cross-campaign 10min)
  └─ scrape metadata
       ├─ YouTube: extractVideoId → getVideoDetails
       ├─ TikTok: scrapeTikTok (Apify)
       └─ Instagram: scrapeInstagram (Apify)
  └─ verify LinkedSocialAccount
       ├─ missing → 412
       └─ owned by other → 403
  └─ validate video type vs campaign acceptsShorts/Long
  └─ prisma.submission.create (PENDING)
  └─ prisma.viewSnapshot.create (initial)
  └─ return 201

Call Graph: Admin Accept ​

PUT /api/admin/submissions/:id/status {ACCEPTED}
  └─ fetch submission + campaign
  └─ budget pre-check (getCampaignSpend) → 409 if ≥100%
  └─ update submission (ACCEPTED, acceptedAt, nextPollAt=now)
  └─ budget clamp on accept (computeScrapeBudgetClamp)
       └─ if clamp → frozenViewCount, campaign.viewsFrozen=true
  └─ checkAndCloseCampaign (95% threshold)
  └─ create UserNotification (SUBMISSION_ACCEPTED)
  └─ audit log (SUBMISSION_ACCEPTED)
  └─ return success

Call Graph: Tracking Tick ​

runTrackingTick() [every 30 min]
  └─ find due submissions
  └─ filter expired
  └─ group by platform
  └─ YouTube batch → applySuccess
  └─ TikTok batch → applySuccess
  └─ Instagram batch → applySuccess
  └─ applySuccess:
       └─ computeScrapeBudgetClamp
       └─ transaction: update submission + ViewSnapshot
       └─ if didFreezeCampaign → markCampaignFrozen
  └─ applyFailure:
       └─ consecutiveScrapeFailures++
       └─ if ≥3 → FLAGGED, trackingStoppedAt

Missing / Incomplete Endpoints ​

EndpointCalled FromExists?
DELETE /api/admin/submissions/:idadminUserDetail.ts:36NO — 404
GET /api/admin/submissions (list all)—NO (only /review with filters)
POST /api/admin/submissions (manual add)—NO (but addedByAdmin field exists)

Validation Schemas (Submission-Relevant) ​

File: src/api/validation/schemas.ts

SchemaUsed By
SubmitVideoSchemaPOST /api/submissions
UpdateViewCountSchemaPUT /admin/submissions/:id/views
UpdateCustomRateSchemaPUT /admin/submissions/:id/rate
UpdateCustomViewCapSchemaPUT /admin/submissions/:id/cap
UpdateSubmissionStatusSchemaPUT /admin/submissions/:id/status