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.

Data Model ​

Core Prisma models involved in the submission system.


Submission ​

Table: Submission
Source: prisma/schema.prisma:316-382

Purpose ​

Core entity representing a clipper's video entry into a campaign.

Key Fields ​

FieldTypeNullableDefaultDescription
idIntNoautoincrementPrimary key
userIdStringNo—Discord ID (legacy) or webUserId fallback
webUserIdString?Yes—WebUser FK (preferred)
usernameStringNo—Clipper handle at submission time
videoLinkStringNo—Original video URL
videoTitleString?Yes—From platform API
previewVideoUrlString?Yes—Direct media URL for moderation
previewImageUrlString?Yes—Thumbnail for previews
initialViewsIntNo0Views at submission time
currentViewsIntNo0Latest scraped views
manualViewCountInt?Yes—Admin override (takes priority)
initialLikesIntNo0Likes at submission
currentLikesIntNo0Latest scraped likes
currentCommentsIntNo0Latest scraped comments
platformStringNo"youtube"youtube/tiktok/instagram
statusStringNo"PENDING"PENDING/ACCEPTED/DENIED/FLAGGED
campaignIdIntNo—Campaign FK
messageIdString?Yes—Legacy Discord log message ID
videoTypeStringNo"short"short/long
durationIntNo0Video duration (seconds)
customRateFloat?Yes—Custom RPM per 1k views (overrides campaign)
customViewCapInt?Yes—Custom view cap (overrides campaign)
addedByAdminBooleanNofalseManually added by staff
frozenViewCountInt?Yes—Budget clamp ceiling (payable views locked)
acceptedAtDateTime?Yes—When approved (drives polling)
lastPolledAtDateTime?Yes—Last successful scrape
nextPollAtDateTime?Yes—Scheduler target
trackingStoppedAtDateTime?Yes—Tracking ended (expiry/flag/freeze)
consecutiveScrapeFailuresIntNo0Auto-flag at 3
creatorHandleString?Yes—Author handle (captured at accept)
postedAtDateTime?Yes—Video publish date
paidOutBooleanNofalseLegacy single-shot payout flag
paidAmountFloat?Yes—Legacy payout amount
paidAtDateTime?Yes—Legacy payout timestamp
payoutIdString?Yes—Legacy Payout FK
paidViewsTotalIntNo0Cumulative views cashed out (delta flow)
paidAmountTotalFloatNo0Cumulative dollars cashed out (delta flow)
lastPaidAtDateTime?Yes—Last delta payout
createdAtDateTimeNonow()Creation timestamp

Relationships ​

RelationTargetTypeCascade
webUserWebUserMany-to-1—
campaignCampaignMany-to-1—
viewSnapshotsViewSnapshot1-to-ManyCascade delete
payoutItemsPayoutItem1-to-ManyCascade delete
payoutPayoutMany-to-1 (legacy)—

Indexes ​

prisma
@@index([nextPollAt, status])      // Scheduler query
@@index([userId, status])          // User dashboard
@@index([campaignId, status])      // Campaign admin, budget calc
@@index([status, createdAt])       // Admin queue
@@index([videoLink])               // Duplicate detection

Field Behavior Notes ​

FieldChanging It Affects
statusTracking eligibility, payout eligibility, UI visibility
currentViewsEarnings calc, budget clamp, payout delta
manualViewCountOverrides currentViews/frozenViewCount for earnings & payout
frozenViewCountHard ceiling on payable views; stops tracking writes
customRateOverrides campaign payout for this submission
customViewCapOverrides campaign viewCap for this submission
acceptedAtPolling cadence (12h/24h), tracking expiry (30 days)
paidViewsTotalBaseline for delta payout math
consecutiveScrapeFailuresAt 3 → auto FLAGGED, tracking stopped

Campaign ​

Table: Campaign
Source: prisma/schema.prisma:270-314

Key Fields (Submission-Relevant) ​

FieldTypeDescription
idIntPK
nameStringUnique
budgetStringe.g. "$5000" — parsed by parseBudget
payoutStringShort-form RPM, e.g. "$0.30"
payoutLongString?Long-form RPM
activeBooleanGlobal on/off
acceptingSubmissionsBooleanAuto-set false at 95% budget
deadlineDateTime?Auto-close timestamp
allowedPlatformsStringComma-separated: "youtube,tiktok,instagram"
viewCapInt?Max views per short submission
longViewCapInt?Max views per long submission
minViewsShortsInt?Minimum views to qualify (short)
minViewsLongInt?Minimum views to qualify (long)
requireNewUploadsBooleanOnly videos uploaded after campaign start
trackingDurationDaysIntDefault 30 — polling window
platformFeeRateFloatDefault 0.10 — accelerates budget burn
maxTotalViewsBigInt?Calculated budget ceiling
viewsFrozenBooleanBudget exhausted — stops tracking
viewsFrozenAtDateTime?When freeze occurred
pausedBooleanManual pause submissions

Relationships ​

  • submissions — Submission[]

ViewSnapshot ​

Table: ViewSnapshot
Source: prisma/schema.prisma:648-660

Purpose ​

Historical view/like/comment data points for charting and audit.

Fields ​

FieldTypeDescription
idStringUUID
submissionIdIntFK → Submission
viewCountIntTotal views at snapshot
likesInt?Like count
commentsInt?Comment count
snapshotDateDateTimeWhen captured
createdAtDateTimeRow insert time

Indexes ​

prisma
@@index([submissionId, snapshotDate])
@@index([snapshotDate])

Written By ​

  1. Submission create (initial scrape) — submissions.ts:515-527
  2. Tracking tick success — runTrackingTick.ts:102-110
  3. Payout rescrape success — rescrape.ts:107-115

Payout ​

Table: Payout
Source: prisma/schema.prisma:432-506

Purpose ​

Tracks money movement to clippers. Two coexisting flows:

  1. Legacy (admin-initiated, single-shot): Submission.paidOut=true, Payout.status ∈
  2. Delta (user-initiated): Payout.status ∈

Key Fields ​

FieldTypeDescription
idStringUUID
userIdStringDiscord ID (legacy)
webUserIdString?WebUser FK
amountFloatNet sent (after clipper fee + surcharge)
grossAmountFloat?Pre-fee total
feeAmountFloat?7% clipper fee
stripeSurchargeAmountFloat?Stripe 2.5% on top
methodStringstripe/paypal/usdt
statusStringSee flows above
scrapeStartedAtDateTime?Delta flow
scrapeCompletedAtDateTime?Delta flow
reviewedAtDateTime?Legacy /approve timestamp
reviewedByString?Admin who reviewed
approvedAtDateTime?Phase 8: bookkeeping done
approvedByString?Admin who approved
sentAtDateTime?Phase 8: money dispatched
sentByString?Admin who sent
rejectionReasonString?If REJECTED/FAILED
referralBonusAmountFloat?Affiliate swept into this payout
taxFormSnapshotIdString?TaxFormSubmission at approval

Relationships ​

  • submissions — Submission[] (legacy)
  • items — PayoutItem[] (delta flow)

PayoutItem ​

Table: PayoutItem
Source: prisma/schema.prisma:508-541

Purpose ​

One row per submission in a user-initiated payout. Holds the rescrape snapshot, delta math, and admin decision.

Fields ​

FieldTypeDescription
idStringUUID
payoutIdStringFK → Payout
submissionIdIntFK → Submission
viewsAtPayoutIntPlatform-reported views at rescrape
payableViewsIntmin(viewsAtPayout, viewCap, manualViewCount?, frozenViewCount?)
priorPaidViewsIntSubmission.paidViewsTotal at item creation
viewsCountedIntpayableViews - priorPaidViews (≥0)
ratePerKFloatRPM applied
grossAmountFloatviewsCounted/1000 * ratePerK
netAmountFloatgrossAmount * (1 - 7%)
decisionStringPENDING/APPROVED/REJECTED/FLAGGED
decisionReasonString?Admin note
decidedByString?Admin Discord ID
decidedAtDateTime?Decision timestamp
videoUnavailableReasonString?VIDEO_DELETED/VIDEO_PRIVATE/SCRAPE_FAILED
badgesStringComma-separated: LOW_LIKE_RATIO,VIEW_SPIKE

Indexes ​

prisma
@@index([payoutId])
@@index([submissionId])

LinkedSocialAccount ​

Table: LinkedSocialAccount
Source: prisma/schema.prisma:667-680

Purpose ​

Proves a WebUser owns a social account (bio-code verification). Required before first submission per platform/account.

Fields ​

FieldTypeDescription
idStringCUID
webUserIdStringFK → WebUser
platformStringyoutube/tiktok/instagram
platformAccountIdStringYouTube channelId, TikTok authorMeta.id, IG ownerId
handleStringDisplay handle
normalizedHandleStringLowercase for fallback matching
verifiedAtDateTimeWhen verified

Unique Constraint ​

prisma
@@unique([platform, platformAccountId])

Enforcement: submissions.ts:438-459 — checks before create; returns 412 if missing, 403 if owned by another user.


WebUser (Submission-Relevant Fields) ​

Source: prisma/schema.prisma:31-129

FieldRelevance
idPrimary key (CUID)
discordIdLegacy userId on Submission
email / emailVerifiedEmail gate (currently disabled)
referralCodeAffiliate attribution
preferredPaymentMethodPayout rail selection
isUSPersonTax routing hint
submissionsSubmission[] via webUserId
linkedSocialAccountsLinkedSocialAccount[]

UserChannel ​

Table: UserChannel
Source: prisma/schema.prisma:384-395

Purpose ​

Discord DM channel per user per campaign for expiry reports.

FieldTypeDescription
idIntPK
userIdStringDiscord ID
webUserIdString?WebUser FK
campaignIdIntFK → Campaign
channelIdStringDiscord channel ID
reportSentBooleanExpiry report delivered

Unique ​

prisma
@@unique([userId, campaignId])

Relationship Diagram ​

mermaid
erDiagram
    WebUser ||--o{ Submission : "webUserId"
    WebUser ||--o{ LinkedSocialAccount : "webUserId"
    WebUser ||--o{ Payout : "webUserId"
    WebUser ||--o{ UserChannel : "webUserId"
    Campaign ||--o{ Submission : "campaignId"
    Campaign ||--o{ UserChannel : "campaignId"
    Submission ||--o{ ViewSnapshot : "submissionId"
    Submission ||--o{ PayoutItem : "submissionId"
    Payout ||--o{ PayoutItem : "payoutId"
    Payout }o--|| TaxFormSubmission : "taxFormSnapshotId"

Enum-Like String Fields (Not Prisma Enums) ​

ModelFieldValues Observed
SubmissionstatusPENDING, ACCEPTED, DENIED, FLAGGED
Submissionplatformyoutube, tiktok, instagram
SubmissionvideoTypeshort, long
PayoutstatusREQUESTED, SCRAPING, READY_FOR_REVIEW, BELOW_THRESHOLD, AWAITING_SEND, PROCESSING, COMPLETED, REJECTED, FAILED, PENDING (legacy)
Payoutmethodstripe, paypal, usdt
PayoutItemdecisionPENDING, APPROVED, REJECTED, FLAGGED
CampaignallowedPlatformsCSV of youtube/tiktok/instagram
UserNotificationtypeSUBMISSION_ACCEPTED, CAMPAIGN_LAUNCH, PAYOUT_SENT, PAYOUT_REJECTED, ADMIN_WARNING, etc.

Cascade Behavior ​

Parent DeleteChild Effect
CampaignSubmissions: RESTRICT (FK required)
WebUserSubmissions: SET NULL on webUserId (but userId remains)
SubmissionViewSnapshots: CASCADE
SubmissionPayoutItems: CASCADE
PayoutPayoutItems: CASCADE
LinkedSocialAccount— (WebUser cascade)

Budget-Aware Earnings Calculation ​

Source: src/utils/calculateSubmissionEarnings.ts

Not stored — computed on read for dashboard, admin, payout.

Inputs per submission: currentViews, manualViewCount, frozenViewCount, customRate, customViewCap, videoType, createdAt, campaign config

Algorithm: First-come-first-earned by createdAt; budget burn accelerated by 1/(1-platformFeeRate).

Output: cappedEarnings (what clipper actually earns), isCapped (budget limited).