Appearance
Current payout system audit
Observed: 2026-09-01. All findings below are from the checked-out workspace. No payout data or code was mutated.
Repository and runtime inventory
The workspace root is not itself a Git repository. The relevant repositories are:
| Repository | Observed branch/status | Payout relevance |
|---|---|---|
Bloxclips-backend/ | feat/local-submission-scraper, clean, tracking its origin | Prisma, auth, earnings, payout routes, rails, jobs, Whop/chat integration |
BloxClips-frontend/ | dev, clean, two commits behind its origin | creator balance/cashout/history and admin payout/review UI |
metric-scraper/ | main, clean | technical metric worker only |
The backend uses Express 5, Prisma 5.22, TypeScript 5.9, and @whop/sdk declared as ^1.0.14 and installed as 1.0.14. Existing backend tests cover chat, groups, referrals, and tracking, but no test suite establishes core earning, reservation, dispatch, webhook, or reconciliation invariants.
Configuration evidence:
Bloxclips-backend/.env.template:33-48declares one Whop key, company route/ID, and webhook secret. It does not establish a separate sandbox base URL or environment.Bloxclips-backend/.env.template:60advertisesMINIMUM_PAYOUT_AMOUNT=20, whilesrc/api/routes/payouts.tsandsrc/utils/payouts/processRequest.tsdefault to100when the variable is absent. Deployment behavior therefore depends on an unverified runtime value.- The checked-in Whop client does not pin
Api-Version-Date. - No secret values were printed or copied during this audit.
What is canonical today?
There is no single canonical earning record.
Three overlapping representations are live:
- Mutable estimates recomputed from the current
SubmissionandCampaignrows bycalculateCampaignEarnings. - Payout-time
PayoutItemsnapshots created by the creator-request flow, whose decisions and amounts can later become provisional paid counters. - Legacy
Submission.paidOut,paidAmount, andpayoutId, alongside newer cumulativepaidViewsTotalandpaidAmountTotalcounters.
PayoutItem is not an immutable earning ledger. It is generated only after a cashout request, can be generated again after restart, lacks a unique payout/submission constraint, and is deleted if its submission or payout is deleted through schema cascades. The cumulative submission counters are mutable denormalizations, not journal entries.
End-to-end current flow
text
ScrapeJob result or direct payout-time Apify call
-> mutable Submission current/manual/frozen view fields + ViewSnapshot
-> calculateCampaignEarnings / processRequest delta calculation
-> mutable campaign-spend recomputation (creator rate × platform-fee burn)
-> creator POST /api/payouts/request
-> SCRAPING -> PayoutItem creation -> READY_FOR_REVIEW
-> admin item decisions -> approve/commit -> AWAITING_SEND
-> admin send -> Stripe / PayPal / USDT -> COMPLETED or FAILED
Parallel legacy route:
admin eligible list -> admin process one/bulk
-> recompute mutable lifetime earnings
-> Stripe / PayPal / USDT
-> set Submission.paidOut and create/update PayoutThere is no automatic creator-payment scheduler. startPaymentSystemScheduler runs tax-related payment jobs, while creator release remains initiated by a creator request and completed by staff actions.
File and symbol inventory
Earnings, rates, and campaign budgets
| Path and symbol | Purpose and callers | Reads/writes and boundaries | Assessment |
|---|---|---|---|
Bloxclips-backend/src/utils/calculateSubmissionEarnings.ts:43 calculateCampaignEarnings | Shared mutable campaign calculation used by creator balance and other presentation/business paths | Reads supplied campaign budget/rate/caps/fee and accepted submissions' current/manual/frozen views, customRate, caps, and creation time; no transaction | Unsafe as accounting. Sorts by createdAt despite an “approval order” intent, rounds per submission, and caps creator RPM earnings against the campaign budget. Current row edits rewrite history. Useful only as a temporary estimate/shadow comparator. |
Same file calculateRawEarnings at line 101 | Chooses view, rate, threshold and cap for one submission | View precedence is manual, then frozen, then current. A truthy customRate wins; otherwise short/long campaign payout; amounts round to cents | Ambiguous/incomplete. 0 cannot be an intentional override; no group rate; no immutable rate snapshot; current rate affects prior views. |
Same file calculateTotalCampaignSpend at line 164 | Computes campaign spend from calculated creator earnings | Multiplies earnings by a platform-fee burn factor; no durable debit | Wrong boundary for target model. Campaign CPM and creator RPM cannot remain independent. |
Bloxclips-backend/src/utils/campaignBudget.ts:18 getCampaignSpend | Recomputes spend from accepted submissions | Reads mutable submission views/rates and campaign fee; no journal/reservation | Unsafe. Rejecting/deleting/editing a submission can reduce historic spend. Pending/reserved/provider funds are not represented. |
Same file computeScrapeBudgetClamp at line 130 | Caps a scrape result against recomputed remaining budget | Reads sibling submissions; caller-dependent transaction behavior | Helpful metric guard, but not a money reservation. Concurrent or alternate paths need a common locked accrual transaction. |
Same file checkAndCloseCampaign at line 248 | Ends or reopens campaign from recomputed spend | Writes campaign lifecycle | Unsafe for financial finality. A later decrease in mutable computed spend can reopen a depleted campaign. |
Bloxclips-backend/src/api/routes/admin.ts:1304,1344,1403,1459 submission mutations | Admin changes views, custom rate, cap, or review state | Authenticated admin writes mutable submission fields, generally followed by generic audit middleware | Operationally useful review controls, but they can retroactively change estimates/budget without financial corrections. Replace financial effects with append-only correction/reversal events. |
Bloxclips-backend/src/api/routes/admin.ts:2817,2891,2944 campaign create/launch/update | Admin campaign management | Stores budget, payout, payoutLong as strings and mutable caps/lifecycle | No distinct CPM/default RPM snapshot. Retain campaign management, replace money/rate storage and launch validation. |
Creator-request payout generation
| Path and symbol | Purpose and callers | Reads/writes and boundaries | Assessment |
|---|---|---|---|
Bloxclips-backend/src/api/routes/payouts.ts:78 computeUserNetBalance | Creator balance endpoint and payout preflight | Recomputes accepted paidOut=false submissions; subtracts gross PayoutItems only in selected approved states; adds affiliate balance later; no transaction | Unsafe/duplicated. Mutable, cacheable estimate is presented as balance; status omissions and two paid models can misstate availability. |
| Same file routes at lines 173, 232, 255 | GET /balance, GET /me, creator POST /request | Creator-auth boundary. Request checks external Stripe/PayPal/USDT method, tax/cooldown/threshold, creates Payout, and starts async processing | This is a creator withdrawal request plus staff-review workflow, not automatic credit. Email verification is commented out. Remove from primary release path after cutover; history can be adapted to read the new ledger. |
Bloxclips-backend/src/utils/payouts/processRequest.ts:15 PAYOUT_STATUS | Defines the newer request states | String constants, not schema enum | Incomplete state model: no reserved, provider-confirmed, ambiguous/unknown, reversed, or reconciliation state. |
Same file fetchEligibleSubmissions line 33 and rateForSubmission line 48 | Selects accepted legacy-unpaid submissions and current rate | Reads both Discord and WebUser identity paths, mutable campaign and submission; does not exclude ended/archived campaigns or apply groups | Unsafe/incomplete. The new model still depends on legacy paidOut and ignores group RPM. |
Same file processPayoutRequest line 61 | Direct rescrape, delta calculation, PayoutItem creation, threshold/status update | Multiple separate writes; no encompassing transaction; external calls occur first; createMany has no uniqueness guard | Retry duplication risk. A restart from SCRAPING can append the same items again. Partial failure leaves ambiguous work. Suitable only as dev scaffolding. |
Same file resumeStuckPayouts line 231 | Restarts REQUESTED/SCRAPING payouts at server boot | Fire-and-forget concurrent execution | Unsafe durability. It can race another process and reproduce items; there is no lease, attempt table, or owner. |
Bloxclips-backend/src/utils/payouts/rescrape.ts:140 rescrapeForPayout | Calls Apify for TikTok/Instagram and another YouTube metric path during cashout | External network calls; updates submission views/snapshots and campaign freezing in separate steps | Wrong boundary. Payout logic owns scraper selection and metric refresh. Replace with the existing durable ScrapeJob technical path; payout must consume finalized backend-applied metrics only. |
Review, reservation, and dispatch
| Path and symbol | Purpose and callers | Reads/writes and boundaries | Assessment |
|---|---|---|---|
Bloxclips-backend/src/api/routes/adminPayoutReview.ts:42-352 | Admin list/detail, per-item review, bulk approve, reject | requireAuth + requireAdmin; item decisions and payout state writes | Retain UX concepts, not the accounting implementation. The queue is manual and its states do not prove fund reservation. |
Same file prepareApprove line 456 and approveHandler line 577 | Validates and approves chosen items | Transaction increments submission paid totals, allocates referral commissions, changes payout to AWAITING_SEND; audit write is not consistently part of the same transaction | Concurrency risk. Non-locking reads and updates can let simultaneous approvals double-increment counters. It records payment before provider dispatch without a durable provider operation. |
Same file POST /:id/send line 714 | Staff-triggered external dispatch | requireAdmin + TOTP; changes to PROCESSING, calls dispatcher, then marks result | Double-send/uncertain-outcome risk. Concurrent calls can both pass; no compare-and-swap claim, provider-operation row, unknown state, or reconciliation-before-retry rule. |
Bloxclips-backend/src/utils/rails/dispatcher.ts:61 dispatchPayout | Chooses Stripe, PayPal, or USDT | External provider call outside a database transaction | No Whop transfer rail. Stripe uses a payout-derived idempotency value, but PayPal uses time-derived batch identity, so repeat calls can move twice. This abstraction assumes external withdrawals and should be replaced, not expanded casually. |
Bloxclips-backend/src/utils/payments/preflight.ts assertCanPayout | Validates legacy method/tax prerequisites | Reads Stripe/PayPal/USDT user configuration and tax state | Not reusable for Whop balance credit except the general idea of a server-side eligibility gate. |
Bloxclips-backend/src/utils/payments/scheduler.ts:23 startPaymentSystemScheduler | Starts payment-adjacent scheduled jobs | Tax/payment maintenance only | Not a creator payout scheduler. |
Parallel legacy payout path
| Path and symbol | Purpose and callers | Reads/writes and boundaries | Assessment |
|---|---|---|---|
Bloxclips-backend/src/api/routes/admin.ts:145 calculateUserEarnings | Legacy admin eligible/process calculation | Reads accepted paidOut=false submissions and mutable rates/views; selected campaign projection omits platformFeeRate, invoking a different default | Duplicated and contradictory. It can disagree with creator balance/review. |
Same file GET /payouts/eligible line 1669, POST /payouts/process/:userId line 1745, bulk at 1929 | Lists and immediately pays creators through old rails | Admin + TOTP + generic audit; provider calls and then fragmented local paidOut/Payout writes | Critical duplicate-money risk. New completed payouts do not consistently set legacy paidOut; the same submissions can remain eligible here. Provider success can precede a failed local update. Disable only through the gated later cutover. |
Same file DELETE /submissions/:id line 3115 | Hard-deletes a submission | Cascades related records under current schema | Destroys financial evidence and can lower recomputed spend/reopen capacity. Financially referenced submissions must become non-deletable/soft-deleted. |
Scraper, Whop, auth, and audit
| Path and symbol | Purpose and callers | Reads/writes and boundaries | Assessment |
|---|---|---|---|
Bloxclips-backend/src/utils/tracking/scrapeJobs.ts:60-260 | Enqueues and reconciles durable technical metric jobs | ScrapeJob, submission metrics, ViewSnapshot; appliedAt prevents reapplication; reconciliation locks affected campaign work | Correct direction. Extend the backend application transaction to create immutable accrual/budget entries. The scraper remains technical-only. |
Bloxclips-backend/src/utils/tracking/runTrackingTick.ts:174 | Runs due metric tracking | Technical scheduling and application | Retain as metric producer, not payout scheduler or policy owner. |
metric-scraper/ worker | Fetches platform metrics and writes job result | Has no payout policy ownership | Retain this boundary. Do not add rates, eligibility, budget, or provider decisions. |
Bloxclips-backend/src/lib/whopSupportChat.ts:175 ensureWhopIdentity | Provisions/fetches a child Whop company for embedded support chat and maps its owner user | Server Whop calls; writes WhopIdentity; uses verified BloxClips email during provision and may fall back to connected-account name/title discovery | Not payment proof. An existing row is returned without payment-recipient revalidation; the mapped child owner is not proven to be the creator's intended personal balance. Reuse only as a candidate signal after explicit payment verification. |
Bloxclips-backend/src/api/routes/webhooks/whop.ts:32 | Verifies Whop webhook and handles chat.message | Raw request body, Whop SDK unwrap, chat effects; no persisted webhook-event deduplication | Signature helper pattern is useful. Add an isolated transfer event inbox with DB dedupe and reconciliation; do not mix money events into chat side effects. |
Bloxclips-backend/src/utils/whopClient.ts | Singleton Whop SDK client | Server secret; no explicit API date or sandbox/production guard | Retain a client factory concept, but create money-specific configuration that pins API date, origin ID, environment, timeout, and retry ownership. |
Bloxclips-backend/src/api/middleware/auth.ts and admin middleware | Maps Google/Discord-backed app identity and roles | WebUser, auth accounts/session, admin role | Application authentication is separate from Whop recipient identity. Keep that separation explicit. |
AdminAuditLog/AuditLog helpers and tables | General request/action audit | Middleware/best-effort writes, generic JSON metadata | Insufficient financial audit. Cannot reliably prove every state transition, actor, reason, provider observation, and correlation atomically. Add a mandatory append-only payout audit event. |
Frontend payout and financial displays
| Path and symbol | Purpose and backend dependency | Assessment |
|---|---|---|
BloxClips-frontend/features/payouts/api/payouts.ts:5,29 fetchPayoutOverview/createPayoutRequest and hooks/usePayoutOverview.ts:40 | Reads /api/payouts/me and mutable /balance; posts creator /request; interprets tax and Stripe/PayPal/USDT method errors | Implements the withdrawal-request model that the target explicitly replaces. Network success text says the request is under review, not that money is scheduled automatically. |
surfaces/content-rewards/screens/payouts/PayoutOverviewScreen.tsx:25 and components/PayoutBalanceCard.tsx:18-55 | Presents “Cash out,” combines clipper net and affiliate pending, and labels it available | The displayed “available” amount is a mutable estimate and includes a different earning class. It does not distinguish held/payable/reserved/provider-unknown. |
surfaces/content-rewards/screens/overview/components/PayoutPanel.tsx:14-37 | Dashboard overview uses balance.balance/eligible only | It omits the affiliate-inclusive combinedNet/combinedEligible used on the payout screen and backend request gate, so two screens can disagree. |
features/payouts/api/payoutMethods.ts and surfaces/.../payouts/method/PayoutMethodScreen.tsx:80-119 | Connect/manage Stripe, PayPal, and USDT withdrawal destinations | Wrong UX for Whop-balance credit. Retire from creator-content payout flow; do not add Stripe to the replacement. |
features/payouts/lib/status.ts:3 getPayoutStatusLabel/tone | Maps only selected request states and otherwise humanizes raw strings | AWAITING_SEND, PROCESSING, BELOW_THRESHOLD, and future ambiguous/reversal states receive generic treatment; “COMPLETED” trusts local state without reconciliation evidence. |
features/admin/payout-detail/api/adminPayoutDetail.ts and related hook/screen | Staff item decisions and approve action against adminPayoutReview | Mirrors manual request review. Useful evidence UI can be redesigned, but its sums are float netAmount values and its approval has unsafe backend concurrency. |
features/admin/payout-list/api/adminPayoutList.ts:30 and AdminPayoutListSendButton.tsx | TOTP-protected staff /send action | Directly exposes the race-prone staff money trigger. Remove after the gated cutover; replacement admin actions reconcile, not mint a fresh send. |
features/admin/user-detail/api/adminUserDetail.ts:9 processAdminUserDetailPayout | Calls the parallel legacy /api/admin/payouts/process/:discordId | Critical evidence that the legacy provider path remains reachable from current UI. |
surfaces/.../admin/user-detail/components/AdminUserDetailSubmissionRow.tsx:147-180 | Shows customRate/campaign rate multiplied by 1,000 as dollars per 1M and mutable earnings | Contradicts backend schema/calculation per-1,000 semantics and can mislead an admin editing a real rate. |
features/admin/campaign-management/ and features/admin/clipper-groups/ | Campaign/group configuration and lifecycle UI | Campaign forms expose current budget/payout conventions; group UI has lifecycle/membership but no group RPM. Extend later with explicitly labeled CPM/RPM versions and effective dates. |
Registration, auth, manifests, migrations, and tests
Bloxclips-backend/src/api/index.ts:185registers the raw-body Whop webhook before JSON parsing, then mounts admin review, legacy admin, and creator payout routes together around lines 314–332.startApiServerstarts tax/payment maintenance and tracking schedulers, not creator payout automation.Bloxclips-backend/src/api/server.tsinvokesresumeStuckPayoutson startup, creating the restart/repeat path described above.- Backend authentication is BloxClips session identity (
WebUserplus Google/DiscordAuthAccountlinkage).requireAuth,requireAdmin, andrequireTOTPcreate useful application boundaries, but none prove a Whop payment recipient. Bloxclips-backend/package.json/lockfile resolve Prisma 5.22, Express 5.2.1, TypeScript 5.9, and official@whop/sdk1.0.14. Frontend uses Next 16.2.4. The scraper package remains an independent Crawlee/Postgres technical worker.- Payout migrations add the request/review item model and a partial one-open-request index, but no immutable earning/budget/provider-operation ledger. Group migrations add campaign-scoped lifecycle and membership without RPM.
- Backend payout-critical paths have no dedicated earning arithmetic, campaign-allocation concurrency, payout state-machine, duplicate dispatch, timeout, webhook dedupe, or reconciliation tests. Existing relevant tests validate tracking-job idempotency and group/chat domains only; those do not establish money safety.
Schema and status inventory
Relevant persisted models
prisma/schema.prisma:178WhopIdentity: uniquewebUserId, connected company ID, and Whop user ID for chat provisioning; no payment verification status, verification evidence, or stale/duplicate controls across provider identities.:283Campaign:budget,payout,payoutLongare strings; there is no separate CPM/default RPM or immutable rate version.platformFeeRateis a float.:331Submission: string review status; mutable metrics and overrides;customRate Float; legacy single-shot paid fields and newer cumulative paid counters coexist.:408ScrapeJob: durable technical metric work with application timestamps. This is the correct ingestion boundary.:484Payout: float amount fields and both legacy DiscorduserIdand WebUser ID; external method fields and a broad string status set.:563PayoutItem: payout-time mutable snapshots with float amounts and decisions; no unique(payoutId, submissionId)or source-accrual reservation constraint.:650AdminAuditLogand:1046AuditLog: general audits, not an enforced financial journal.:667ViewSnapshot: technical historical metric evidence, but not a finalized earning.:830ReferralCommission: append-oriented commission record with a useful unique source-payout pattern; it is still coupled to the unsafe payout flow.:1112ClipperGroupand:1130membership: campaign-scoped lifecycle exists, but no group RPM field or rate version exists.docs/clipper-groups-plan.mdexplicitly deferred rate precedence.
Migrations 20260502000000_add_user_initiated_payout_review, 20260504000000_add_payout_request_security, and 20260505200000_add_awaiting_send_status explain the newer workflow. A partial unique index prevents one open request under its enumerated statuses, but it does not protect earnings, approval counters, provider calls, or legacy routes.
Exact current status vocabulary
Legacy code uses PENDING, PROCESSING, COMPLETED, and FAILED. The request/review flow additionally uses REQUESTED, SCRAPING, READY_FOR_REVIEW, BELOW_THRESHOLD, AWAITING_SEND, and REJECTED.
Observed meanings are inconsistent:
| Status | Current observed meaning | Terminal? | Ambiguity |
|---|---|---|---|
REQUESTED | creator request created | No | async work may not have started |
SCRAPING | direct payout rescrape/item build started | No | restart can repeat item creation |
READY_FOR_REVIEW | items generated above threshold | No | no money reserved |
BELOW_THRESHOLD | current request calculation below threshold/no items | Effectively yes | later mutable views can make balance return; no clean retry lifecycle |
AWAITING_SEND | admin approved and counters/commissions provisionally updated | No | provider operation does not yet exist |
PROCESSING | send route began or legacy provider operation in progress | No | no distinction between not sent, accepted, timeout, or provider pending |
COMPLETED | local path believes provider succeeded | Yes in UI | may not synchronize legacy paid flags; no ongoing reconciliation |
FAILED | calculation or provider path failed | Ambiguous | no retryability/uncertain-money distinction |
REJECTED | admin rejected request | Yes | mutable source earnings remain and can reappear |
PENDING | legacy payout created/pending | Ambiguous | overlaps newer request concepts |
There is no modeled cancellation, provider confirmation distinct from final completion, unknown timeout, reversal, recovery, or reconciled state.
Answers to required audit questions
- Canonical earnings: none. Current balances are mutable recomputations;
PayoutItemis a payout-time snapshot, not a complete immutable ledger. - Snapshot/finality:
PayoutItemsnapshots views/rate/amount only when requested, while rate and budget before that remain mutable. There is no immutable earning accrual or rate-policy version for every eligible view delta. - Rate meaning: campaign
payout/payoutLongbehaves as creator RPM and also as the basis for budget burn.Submission.customRatewins if truthy. CPM is not represented independently. Group RPM is absent. Frontend admin code treats custom rate in at least one path as per-million by scaling 1,000, while backend/schema comments calculate per-1,000—an explicit unit contradiction. - Precedence: only individual nonzero
customRateover short/long campaign payout is implemented. Campaign/group/individual precedence is otherwise missing. - Budget decrease: no durable decrease occurs. Spend and remaining capacity are recalculated from current accepted submissions and rate/fee assumptions; scraper clamps/freeze logic derives from that value.
- Double count/overspend: yes. Two payout generations, nonunique item creation, concurrent approval/send gaps, mutable budget calculations, and legacy paid flags permit both local double counting and provider duplicate risk.
- Lifecycle changes: rejection or hard deletion removes/reduces computed spend; unapproval makes earnings disappear; later views increase mutable balance; payout-time and scheduled scraping use different paths; ended/frozen campaigns are not a consistent eligibility boundary. Hard deletion can cascade payout evidence. There is no formal correction/reversal policy.
- Current payout concept: a mixture of creator withdrawal request, internal mutable earning claim, staff review queue, and staff-triggered external provider payout. The parallel legacy route is a direct staff payout.
- Provider abstractions: Stripe/PayPal/USDT external-withdrawal dispatchers exist. Their identity, preflight, idempotency, and response models do not fit crediting Whop balances. Retain only generic lessons, not the interface.
- Retain/replace: retain campaign/submission review concepts, authenticated roles, technical
ScrapeJobboundary, view snapshots, and the useful UI idea of history/reconciliation. Replace financial calculations, money storage, reservation/status model, direct payout rescrape, both send paths, and payment identity proof after a clean gated cutover. - Retry duplicate risk: yes. Startup item regeneration and concurrent/repeated send endpoints can duplicate; PayPal's time-derived request identity is especially unsafe. A provider timeout has no
UNKNOWNstate or mandatory reconciliation. - Status model: listed above; terminal and ambiguous meanings are not enforced by schema transitions.
- Audit: no complete atomic financial audit. Generic audits cannot always answer who/what/why for every transition or prove provider request/response/webhook history.
- Frontend truth: creator pages show mutable “balance”/estimated earnings and cashout concepts; affiliate inclusion differs by screen; payment methods remain Stripe/PayPal/USDT; some raw/unmapped payout statuses can surface; admin user detail can invoke the legacy send path; custom-rate units conflict.
- Scraper decisions in payout: yes,
rescrapeForPayoutdirectly selects/calls scraping providers. Cut over toScrapeJobas the only technical metric acquisition mechanism; the backend reconciliation transaction validates monotonic/capped deltas and posts local earnings/budget journals. Payout selection then reads only finalized journal entries.
Risk ranking
| Severity | Risk | Concrete consequence |
|---|---|---|
| Critical | Legacy and new payout paths can both consider the same submission unpaid | Same creator earnings can be sent twice |
| Critical | No atomic provider-operation claim or uncertain-outcome reconciliation | Concurrent sends or timeout retries can move money twice |
| Critical | Campaign budget is mutable recomputation, not a locked journal | Overspend, spend rollback after edits/deletes, or accidental campaign reopening |
| High | Rates/views are mutable and group RPM does not exist | Prior earnings change retroactively; margin and creator liability cannot be proven |
| High | Float/string money and per-submission cent rounding | Drift, inconsistent totals, and no exact invariant across accrual/budget/transfer |
| High | Payout flow directly rescrapes | Money state depends on an ad hoc external call path and can repeat after crash |
| High | Chat WhopIdentity is treated as tempting but unproven payment identity | Funds could be sent to a provisioned/support identity rather than the creator's intended balance |
| High | Hard deletion/cascade of financial sources | Audit evidence can be destroyed |
| Medium | Generic/best-effort auditing and incomplete frontend mappings | Staff and creators cannot reliably explain balances or transitions |
Clean-cutover recommendation
Do not patch Whop into either current send route. Build the new journals and payout operation records alongside the existing dev-only data, run deterministic and concurrency tests, then shadow-accrue without dispatch. Once invariants pass, freeze both POST /api/payouts/request and the legacy admin process routes behind feature flags, settle/cancel any dev-only in-flight records, switch reads to the new projections, and only then enable sandbox dispatch. Existing records need not be migrated, but this audit did not delete or mutate them.