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.
System Architecture
Overview
The submission system spans frontend (Next.js), backend API (Express), background scheduler, Discord bot (legacy), and external scrapers (Apify, YouTube Data API).
┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
│ Frontend │────▶│ Backend │────▶│ PostgreSQL │
│ (Next.js) │ │ (Express) │ │ (Prisma) │
└─────────────┘ └──────┬──────┘ └──────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Scheduler│ │ Apify │ │ YouTube │
│ (30 min) │ │ Scrapers │ │ Data API │
└──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────┐
│ Discord │ (notifications only; legacy approval removed)
└──────────┘Major Components
1. Frontend (Next.js 15, App Router)
Routes:
/dashboard/submit— Clipper submission modal (SubmitVideoScreen.tsx)/dashboard/history— Clipper submission history (SubmissionHistoryScreen.tsx)/dashboard/admin/submissions— Admin review queue (AdminSubmissionsScreen.tsx)/dashboard/admin/campaigns/[id]— Campaign-scoped review (AdminCampaignDetailScreen.tsx)
State/Hooks:
useSubmissionHistory— Fetches paginated user submissionsuseAdminSubmissions/useAdminCampaignDetail— Admin review with filters, snapshots, actions
2. Backend API (Express)
Public Routes (src/api/routes/submissions.ts):
GET /api/submissions— User's submissions with budget-aware earningsPOST /api/submissions— Create submission (scrape, verify account, create row)
Admin Routes (src/api/routes/admin.ts):
GET /api/admin/submissions/review— Paginated queue with filtersPUT /api/admin/submissions/:id/status— Accept/Deny/Flag (triggers tracking, budget clamp, notifications)PUT /api/admin/submissions/:id/views— Manual view overridePUT /api/api/admin/submissions/:id/rate— Custom RPM overridePUT /api/admin/submissions/:id/cap— Custom view cap overrideGET /api/admin/submissions/:id/snapshots— View history for chart
3. Background Scheduler (src/scheduler.ts)
Runs on Express server startup via setInterval:
| Job | Interval | Purpose |
|---|---|---|
| Tracking tick | 30 min | Poll due ACCEPTED submissions, update views, budget clamp |
| Campaign expiry | 60 sec | Close expired campaigns, send Discord reports |
Tracking tick detail (src/utils/tracking/runTrackingTick.ts):
- Queries
Submissionwherestatus=ACCEPTED,nextPollAt <= now,trackingStoppedAt=null,frozenViewCount=null - Groups by platform: YouTube (Data API, batch 50), TikTok/Instagram (Apify actors)
- On success: writes
currentViews,ViewSnapshot, resetsconsecutiveScrapeFailures, setsnextPollAtviapollScheduler - On failure: increments
consecutiveScrapeFailures; at 3 →FLAGGED,trackingStoppedAt - Budget clamp:
computeScrapeBudgetClamp(first-come-first-earned) → may setfrozenViewCountandcampaign.viewsFrozen=true
4. External Scrapers
| Platform | Provider | Auth | Cost |
|---|---|---|---|
| YouTube | YouTube Data API v3 | YOUTUBE_API_KEY | Free (quota) |
| TikTok | Apify clockworks/tiktok-scraper | APIFY_TOKEN | Per result |
Apify apify/instagram-scraper | APIFY_TOKEN | Per result |
Cost tracking: src/utils/scrapers/costTracker.ts records rows per user/platform.
5. Discord Integration
Current: In-app UserNotification only (created on accept, payout sent/rejected, campaign launch)
Legacy (removed): Discord message with Approve/Deny buttons in requestLogChannel — code commented in submissions.ts:37-124. Button handler (buttonHandler.ts) now returns "use dashboard" for submission actions.
Data Flow Summary
Submission Creation
POST /api/submissions
→ validate URL (Zod: HTTPS, allowed domains)
→ detect platform (YouTube/TikTok/Instagram)
→ check campaign active, platform allowed
→ duplicate check: same videoLink + campaignId
→ cross-campaign cooldown (10 min)
→ scrape metadata (YouTube API / Apify)
→ verify LinkedSocialAccount ownership
→ if missing: return 412 ACCOUNT_VERIFICATION_REQUIRED
→ if owned by another: 403
→ create Submission (PENDING, initialViews=scraped views)
→ create ViewSnapshot (initial data point)
→ return submissionReview → Tracking
PUT /api/admin/submissions/:id/status {status: ACCEPTED}
→ budget pre-check (getCampaignSpend ≥ 100% → 409)
→ update status=ACCEPTED, acceptedAt, nextPollAt=now
→ budget clamp on accept (may freeze campaign)
→ checkAndCloseCampaign (95% threshold)
→ create UserNotification (SUBMISSION_ACCEPTED)
→ audit log (SUBMISSION_ACCEPTED)Tracking Tick (every 30 min)
runTrackingTick()
→ find due ACCEPTED submissions
→ filter expired (acceptedAt + trackingDurationDays)
→ group by platform, scrape batch
→ per submission: computeScrapeBudgetClamp
→ if clamp.didFreezeCampaign → markCampaignFrozen
→ update currentViews, ViewSnapshot, nextPollAt
→ on 3 failures → FLAGGED, trackingStoppedAtPayout Request
POST /api/payouts/request
→ cooldown check (24h since last COMPLETED)
→ daily rescrape cap (200)
→ open payout check
→ payment method connected?
→ balance check (clipper net + affiliate ≥ $100)
→ tax form gate (assertCanPayout)
→ create Payout (REQUESTED)
→ async processPayoutRequest()
→ rescrape all eligible submissions
→ build PayoutItems (delta math: payableViews - priorPaidViews)
→ if net ≥ $100 → READY_FOR_REVIEW
else → BELOW_THRESHOLDPayout Review → Send
Admin: GET /api/admin/payouts/review → READY_FOR_REVIEW queue
Admin: POST /api/admin/payouts/review/:id/items/:itemId {decision: APPROVED}
Admin: POST /api/admin/payouts/review/:id/approve
→ gate check (tax, method, amount)
→ sticky status: REJECTED→DENIED, FLAGGED→FLAGGED (stops tracking)
→ bump paidViewsTotal / paidAmountTotal on submissions
→ tax form snapshot
→ affiliate sweep + accrual
→ status → AWAITING_SEND
Admin (Pending Payouts tab): POST /api/admin/payouts/review/:id/send (TOTP)
→ re-verify gate
→ dispatchPayout (Stripe/PayPal/USDT)
→ COMPLETED or FAILED (reverts paid-totals, reverses affiliate)Key Services / Utilities
| File | Responsibility |
|---|---|
src/utils/campaignBudget.ts | getCampaignSpend, computeScrapeBudgetClamp, checkAndCloseCampaign, markCampaignFrozen |
src/utils/calculateSubmissionEarnings.ts | calculateCampaignEarnings (budget-aware, first-come-first-earned) |
src/utils/tracking/pollScheduler.ts | computeNextPollAt (12h/24h cadence), isTrackingExpired |
src/utils/tracking/runTrackingTick.ts | Unified polling tick |
src/utils/payouts/processRequest.ts | User-initiated payout orchestrator |
src/utils/payouts/rescrape.ts | Payout-time rescrape with budget clamp |
src/utils/payouts/badges.ts | Auto-detect fraud signals (like ratio, view spike) |
src/utils/fees.ts | applyClipperFee (7%), burnMultiplier (platform fee) |
src/utils/payout.ts | parseRate, parseBudget |
src/utils/platforms.ts | URL detection, ID extraction |
src/utils/scrapers/apify.ts | TikTok/Instagram Apify wrappers |
src/utils/youtube.ts | YouTube Data API batch calls |
Database Relationships (Core)
WebUser 1──∞ Submission ∞──1 Campaign
Submission 1──∞ ViewSnapshot
Submission 1──∞ PayoutItem ∞──1 Payout
Payout ∞──1 WebUser
LinkedSocialAccount ∞──1 WebUserEnvironment Variables (Submission-Relevant)
| Variable | Used By |
|---|---|
DATABASE_URL | Prisma |
YOUTUBE_API_KEY | YouTube scraping |
APIFY_TOKEN | TikTok/Instagram scraping |
APIFY_TIKTOK_ACTOR_ID | TikTok actor (default: clockworks/tiktok-scraper) |
APIFY_INSTAGRAM_ACTOR_ID | IG actor (default: apify/instagram-scraper) |
STRIPE_SECRET_KEY | Payout dispatch |
PAYPAL_CLIENT_ID/SECRET | PayPal payouts |
NOWPAYMENTS_API_KEY | USDT payouts |
DISCORD_BOT_TOKEN | Notifications, campaign expiry reports |
MINIMUM_PAYOUT_AMOUNT | Default $100 |
FRONTEND_URL | Referral links, email templates |