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.

Payouts & Dependencies ​

How submissions feed into the payout system.


Two Payout Flows (Coexisting) ​

1. Legacy Admin-Initiated (Single-Shot) ​

Status: Still in code, used for historical payouts

Submission Fields:

  • paidOut: Boolean — true = excluded from delta flow
  • paidAmount: Float? — amount paid
  • paidAt: DateTime? — when paid
  • payoutId: String? — FK to Payout (legacy)

Payout Model (Legacy Statuses):

  • PENDING → PROCESSING → COMPLETED / FAILED

Flow: Admin creates payout → adds submissions → sends money → marks submissions paidOut=true

Exclusion: paidOut=true submissions are filtered out of user-initiated flow (processRequest.ts:39-40)


2. User-Initiated Delta Flow (Current) ​

Status: Active, primary flow

Submission Fields (Delta Tracking):

  • paidViewsTotal: Int (default 0) — cumulative views cashed out
  • paidAmountTotal: Float (default 0) — cumulative dollars cashed out
  • lastPaidAt: DateTime? — last delta payout timestamp

Payout Model (New Statuses):

REQUESTED → SCRAPING → READY_FOR_REVIEW → AWAITING_SEND → PROCESSING → COMPLETED
                    ↓
               BELOW_THRESHOLD (if net < $100)
                    ↓
               REJECTED (admin rejects)
                    ↓
               FAILED (rail error)

PayoutItem: One row per submission per payout request


When a Submission Becomes Payout-Eligible ​

Conditions (from processRequest.ts:33-45):

  1. status = 'ACCEPTED'
  2. paidOut = false (not in legacy flow)
  3. Owned by requesting user (userId OR webUserId)
  4. Has views > 0 (after caps)

Campaign State: Even frozen/tracking-stopped submissions included — uses last-known views


Delta Payout Math ​

Per-Submission Calculation (processRequest.ts:132-171) ​

typescript
// At payout rescrape time:
ratePerK = customRate || campaign rate
cap = customViewCap || campaign viewCap

// Payable views priority: manual > frozen > rescraped
rawViews = manualViewCount ?? frozenViewCount ?? viewsAtPayout
payableViews = cap ? min(rawViews, cap) : rawViews

// Delta: new views since last payout
priorPaid = submission.paidViewsTotal
viewsCounted = max(0, payableViews - priorPaid)

grossAmount = (viewsCounted / 1000) * ratePerK
netAmount = applyClipperFee(grossAmount)  // 7% clipper fee

Campaign Budget Clamp (Applies at Rescrape) ​

Same as tracking tick: computeScrapeBudgetClamp called per submission during rescrape (rescrape.ts:88)

Result: frozenViewCount may be set on submission, limiting payableViews

Threshold Gate (Per Campaign) ​

In calculateRawEarnings (calculateSubmissionEarnings.ts:126-133):

typescript
const threshold = isLong ? campaign.minViewsLong : campaign.minViewsShorts
if (threshold && views < threshold) {
  return { rawEarnings: 0, views: cappedViews, ratePerK }
}

Note: This gates earnings calculation, not PayoutItem creation. Items with 0 net still appear in admin review.


PayoutItem Fields (Snapshot at Rescrape) ​

FieldSource
viewsAtPayoutFresh scrape (or last-known)
payableViewsmin(rawViews, cap)
priorPaidViewssubmission.paidViewsTotal at item creation
viewsCountedpayableViews - priorPaidViews (≥0)
ratePerKCustom or campaign rate
grossAmountviewsCounted/1000 * ratePerK
netAmountgrossAmount * 0.93 (7% fee)
videoUnavailableReasonVIDEO_DELETED/PRIVATE/SCRAPE_FAILED
badgesLOW_LIKE_RATIO, VIEW_SPIKE, etc.

Payout Status Transitions ​

User Requests Payout ​

POST /api/payouts/request
  → cooldown check (24h since last COMPLETED)
  → daily rescrape cap (200)
  → open payout check
  → payment method connected?
  → balance ≥ $100 (clipper net + affiliate)?
  → tax form gate (assertCanPayout)
  → create Payout(REQUESTED)
  → async processPayoutRequest()

Processing (Async) ​

processPayoutRequest(payoutId):
  → status=SCRAPING
  → rescrape all eligible submissions
  → build PayoutItems
  → sum net
  → if net < $100: BELOW_THRESHOLD
    else: READY_FOR_REVIEW

Admin Review ​

GET /api/admin/payouts/review → READY_FOR_REVIEW queue
Admin reviews items → per-item decision (APPROVE/REJECT/FLAG)

Admin Approve (Bookkeeping Only — No Money) ​

POST /api/admin/payouts/review/:id/approve
  → gate check (tax, method, amount)
  → sticky status:
     * REJECTED items → submission DENIED, trackingStoppedAt
     * FLAGGED items → submission FLAGGED, trackingStoppedAt
  → bump paidViewsTotal/paidAmountTotal on APPROVED items
  → tax form snapshot
  → affiliate sweep + accrual
  → status = AWAITING_SEND

Admin Send (Money Moves) ​

POST /api/admin/payouts/review/:id/send (TOTP)
  → re-verify gate
  → dispatchPayout (Stripe/PayPal/USDT)
  → COMPLETED or FAILED
  → On FAILED (terminal): revert paidViewsTotal, reverse affiliate

On Approve (adminPayoutReview.ts:499-509):

typescript
for each APPROVED item:
  submission.paidViewsTotal += item.viewsCounted
  submission.paidAmountTotal += item.netAmount
  submission.lastPaidAt = now

On Terminal Send Failure (adminPayoutReview.ts:843-865):

typescript
for each APPROVED item:
  submission.paidViewsTotal -= item.viewsCounted
  submission.paidAmountTotal -= item.netAmount

On Transient Send Failure: Stays AWAITING_SEND — no revert (admin retries)


Campaign Budget Interaction ​

At Submission Accept ​

  • Budget pre-check (100% hard block)
  • Budget clamp on accept (may freeze campaign)

At Tracking Tick ​

  • computeScrapeBudgetClamp per scrape
  • May freeze campaign mid-tick

At Payout Rescrape ​

  • Same clamp applied
  • Frozen campaigns skipped (use frozenViewCount)

At Payout Approve ​

  • No additional budget check (already clamped at rescrape)
  • paidViewsTotal bump is informational (budget already consumed)

Affiliate / Referral Interaction ​

Source: src/utils/referrals/sweep.ts, accrual.ts

On Payout Approve ​

  1. sweepCommissionsIntoPayout — moves pending referral commissions into this payout
  2. accrueCommissionForPayout — creates ReferralCommission rows for referrer

On Terminal Send Failure: reverseSweepForPayout + reverseAccrualForPayout

Referral Bonus: Added to Payout.referralBonusAmount — sent in same transfer


Tax Form Dependency ​

Gate: assertCanPayout (src/utils/payments/preflight.ts)

Checked at:

  1. Payout request (payouts.ts:382)
  2. Payout approve (adminPayoutReview.ts:582)
  3. Payout send (adminPayoutReview.ts:736)

Logic: If projected taxable gross ≥ $600 (configurable) → valid tax form required

Snapshot: On approve, taxFormSnapshotId captured → used for year-end 1099


Payment Rails ​

RailMethod ValueSurchargeConfig
Stripestripe2.5% (on top of 7%)STRIPE_PAYOUT_SURCHARGE_BPS (default 250)
PayPalpaypalNonePAYPAL_CLIENT_ID/SECRET
USDTusdtNone (network fee via NowPayments)NOWPAYMENTS_API_KEY

Rail Selection: WebUser.preferredPaymentMethod → fallback order: stripe → paypal → usdt


Minimum Payout ​

Config: MINIMUM_PAYOUT_AMOUNT env (default $100)

Checked at:

  • Balance endpoint (payouts.ts:205) — eligible flag
  • Request endpoint (payouts.ts:361) — 400 if below
  • Payout processing (processRequest.ts:190) — BELOW_THRESHOLD status

Cooldowns & Caps ​

LimitValueEnforced At
Payout cooldown24h after COMPLETEDpayouts.ts:279
Daily rescrape cap200 items/userpayouts.ts:304
Open payout limit1 per userpayouts.ts:318 (DB unique index)

Legacy vs. Delta Coexistence ​

AspectLegacyDelta
Submission.paidOuttrue = donefalse (eligible)
Submission.paidAmountSingle amountIgnored
Submission.paidViewsTotalUnusedCumulative
Payout.statusPENDING/PROCESSING/COMPLETED/FAILEDREQUESTED→...→COMPLETED
Payout.itemsEmptyPayoutItem[]
Admin UISeparate bulk flowPer-item review

Migration: No automatic migration. Legacy payouts remain as-is. New flow only for paidOut=false.


Key Files ​

FilePurpose
src/api/routes/payouts.tsUser-facing: balance, request, history
src/api/routes/adminPayoutReview.tsAdmin: review queue, approve, send
src/utils/payouts/processRequest.tsDelta payout orchestrator
src/utils/payouts/rescrape.tsPayout-time rescrape
src/utils/payouts/badges.tsFraud badges
src/utils/calculateSubmissionEarnings.tsBudget-aware earnings
src/utils/campaignBudget.tsBudget clamp
src/utils/referrals/sweep.tsAffiliate sweep
src/utils/referrals/accrual.tsCommission accrual
src/utils/payments/preflight.tsTax/method gate
src/utils/rails/dispatcher.tsRail dispatch (Stripe/PayPal/USDT)