Appearance
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— filterlimit(1-100, default 10)offset(default 0)
Flow:
- Build where clause (user + optional filters)
- Fetch submissions with campaign select (budget, payout, caps, thresholds)
- Fetch ALL accepted submissions for those campaigns (for budget calc)
calculateCampaignEarnings(campaign, allCampaignSubs)→ per-submission capped earnings- Apply clipper fee (7%) →
estimatedEarnings - 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:
- Parse + validate
- Email gate (disabled — commented)
- Fetch campaign → must exist, active, acceptingSubmissions, not deleted
detectPlatform(videoUrl)→ youtube/tiktok/instagram/unknown- Check platform in
campaign.allowedPlatforms - Duplicate checks: a. Same
videoLink+campaignId→ 400 b. SamevideoLink+ same user within 10 min → 429 - Scrape metadata:
- YouTube:
extractVideoId→getVideoDetails(batch 1) - TikTok:
scrapeTikTok(Apify) — cached 10 min - Instagram:
scrapeInstagram(Apify) — cached 10 min
- YouTube:
- Verify
LinkedSocialAccountfor(platform, creatorAccountId)- Missing → 412
ACCOUNT_VERIFICATION_REQUIRED - Owned by other WebUser → 403
- Missing → 412
- Validate video type vs campaign
acceptsShorts/acceptsLong - Create Submission (PENDING) with scraped data
- Create initial
ViewSnapshot - 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:
- Build where clause (status filter, campaign filter)
- Paginated fetch with campaign + user info
- 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:
- Fetch submission + campaign
- If
ACCEPTED: a. Budget pre-check (getCampaignSpend) → 409 if ≥100% b. Update:status=ACCEPTED,acceptedAt(or now),nextPollAt=nowc. Budget clamp on accept:getCampaignSpend→ remaining budget- If
currentViewswould exceed →frozenViewCount = maxViews campaign.acceptingSubmissions=false,viewsFrozen=true,viewsFrozenAt=nowd.checkAndCloseCampaign(95% threshold) e. CreateUserNotification(SUBMISSION_ACCEPTED)
- Else: update status only
- Audit log:
SUBMISSION_{NEWSTATUS}with previousStatus, reason, campaignId, videoLink - 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:
- Query due ACCEPTED submissions (
nextPollAt <= now, not stopped, not frozen) - Filter expired (
acceptedAt + trackingDurationDays) - Group by platform
- YouTube: batch
getVideoDetails(50 IDs) →applySuccess - TikTok: batch
scrapeTikTok→applySuccess - Instagram: batch
scrapeInstagram→applySuccess applySuccess:computeScrapeBudgetClamp(submissionId, newViews)- Transaction: update submission + create ViewSnapshot
- If clamp froze campaign →
markCampaignFrozen
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
- If campaign frozen →
alreadyFrozen=true - Sum sibling ACCEPTED submissions' spend (excluding this)
remainingBudget = budgetLimit - spendExcludingThismaxViewsByBudget = floor(remainingBudget * 1000 / (ratePerK * burn))- If
cappedNewViews <= maxViewsByBudget→ no clamp - 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:
getCampaignSpend→shouldClose(≥95%)- If shouldClose and
acceptingSubmissions=true→ set false - If NOT shouldClose and
acceptingSubmissions=falseand % < 95 → set true (re-open)
calculateCampaignEarnings()
File: src/utils/calculateSubmissionEarnings.ts:43-96
Inputs: Campaign config, all accepted submissions for campaign
Algorithm:
- Sort submissions by
createdAt(first approved = first priority) - For each:
rawEarnings = (cappedViews / 1000) * ratePerK remainingBudget = budgetLimit - runningSpendmaxEarningsFromRemaining = remainingBudget / burncappedEarnings = min(rawEarnings, maxEarningsFromRemaining)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:
- Update Payout →
SCRAPING,scrapeStartedAt=now - Fetch eligible submissions (ACCEPTED, paidOut=false, user-owned)
rescrapeForPayout()→ fresh views per submission- Re-fetch submissions (capture frozenViewCount from rescrape)
- Build
PayoutItemper submission:payableViews = min(manualViewCount ?? frozenViewCount ?? viewsAtPayout, viewCap)viewsCounted = max(0, payableViews - priorPaidViews)grossAmount = (viewsCounted/1000) * ratePerKnetAmount = applyClipperFee(grossAmount)- Badges: ratio (likes/views, comments/views) + spike (snapshot history)
- Bulk create PayoutItems
- Sum net for available items (exclude unavailable)
- If
totalNet < $100→BELOW_THRESHOLDElse →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
RescrapeOutcomewithunavailableReason(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 201Call 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 successCall 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, trackingStoppedAtMissing / Incomplete Endpoints
| Endpoint | Called From | Exists? |
|---|---|---|
DELETE /api/admin/submissions/:id | adminUserDetail.ts:36 | NO — 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
| Schema | Used By |
|---|---|
SubmitVideoSchema | POST /api/submissions |
UpdateViewCountSchema | PUT /admin/submissions/:id/views |
UpdateCustomRateSchema | PUT /admin/submissions/:id/rate |
UpdateCustomViewCapSchema | PUT /admin/submissions/:id/cap |
UpdateSubmissionStatusSchema | PUT /admin/submissions/:id/status |