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.
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 flowpaidAmount: Float?— amount paidpaidAt: DateTime?— when paidpayoutId: 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 outpaidAmountTotal: Float(default 0) — cumulative dollars cashed outlastPaidAt: 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):
status = 'ACCEPTED'paidOut = false(not in legacy flow)- Owned by requesting user (
userIdORwebUserId) - 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 feeCampaign 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)
| Field | Source |
|---|---|
viewsAtPayout | Fresh scrape (or last-known) |
payableViews | min(rawViews, cap) |
priorPaidViews | submission.paidViewsTotal at item creation |
viewsCounted | payableViews - priorPaidViews (≥0) |
ratePerK | Custom or campaign rate |
grossAmount | viewsCounted/1000 * ratePerK |
netAmount | grossAmount * 0.93 (7% fee) |
videoUnavailableReason | VIDEO_DELETED/PRIVATE/SCRAPE_FAILED |
badges | LOW_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_REVIEWAdmin 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_SENDAdmin 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 affiliatePaid-Total Bumps (Sticky)
On Approve (adminPayoutReview.ts:499-509):
typescript
for each APPROVED item:
submission.paidViewsTotal += item.viewsCounted
submission.paidAmountTotal += item.netAmount
submission.lastPaidAt = nowOn Terminal Send Failure (adminPayoutReview.ts:843-865):
typescript
for each APPROVED item:
submission.paidViewsTotal -= item.viewsCounted
submission.paidAmountTotal -= item.netAmountOn 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
computeScrapeBudgetClampper 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)
paidViewsTotalbump is informational (budget already consumed)
Affiliate / Referral Interaction
Source: src/utils/referrals/sweep.ts, accrual.ts
On Payout Approve
sweepCommissionsIntoPayout— moves pending referral commissions into this payoutaccrueCommissionForPayout— createsReferralCommissionrows 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:
- Payout request (
payouts.ts:382) - Payout approve (
adminPayoutReview.ts:582) - 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
| Rail | Method Value | Surcharge | Config |
|---|---|---|---|
| Stripe | stripe | 2.5% (on top of 7%) | STRIPE_PAYOUT_SURCHARGE_BPS (default 250) |
| PayPal | paypal | None | PAYPAL_CLIENT_ID/SECRET |
| USDT | usdt | None (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) —eligibleflag - Request endpoint (
payouts.ts:361) — 400 if below - Payout processing (
processRequest.ts:190) —BELOW_THRESHOLDstatus
Cooldowns & Caps
| Limit | Value | Enforced At |
|---|---|---|
| Payout cooldown | 24h after COMPLETED | payouts.ts:279 |
| Daily rescrape cap | 200 items/user | payouts.ts:304 |
| Open payout limit | 1 per user | payouts.ts:318 (DB unique index) |
Legacy vs. Delta Coexistence
| Aspect | Legacy | Delta |
|---|---|---|
Submission.paidOut | true = done | false (eligible) |
Submission.paidAmount | Single amount | Ignored |
Submission.paidViewsTotal | Unused | Cumulative |
Payout.status | PENDING/PROCESSING/COMPLETED/FAILED | REQUESTED→...→COMPLETED |
Payout.items | Empty | PayoutItem[] |
| Admin UI | Separate bulk flow | Per-item review |
Migration: No automatic migration. Legacy payouts remain as-is. New flow only for paidOut=false.
Key Files
| File | Purpose |
|---|---|
src/api/routes/payouts.ts | User-facing: balance, request, history |
src/api/routes/adminPayoutReview.ts | Admin: review queue, approve, send |
src/utils/payouts/processRequest.ts | Delta payout orchestrator |
src/utils/payouts/rescrape.ts | Payout-time rescrape |
src/utils/payouts/badges.ts | Fraud badges |
src/utils/calculateSubmissionEarnings.ts | Budget-aware earnings |
src/utils/campaignBudget.ts | Budget clamp |
src/utils/referrals/sweep.ts | Affiliate sweep |
src/utils/referrals/accrual.ts | Commission accrual |
src/utils/payments/preflight.ts | Tax/method gate |
src/utils/rails/dispatcher.ts | Rail dispatch (Stripe/PayPal/USDT) |