Skip to content

Target BloxClips payout architecture ​

Decision and ownership boundary ​

Build a durable, database-backed BloxClips accounting and automatic payout system. The owner-confirmed provider source is BloxClips' Whop business balance. Use a Whop Current API ledger transfer from that source only after the gates in 05-sandbox-validation.md pass.

text
technical metric result
  -> backend validates and applies metric delta
  -> campaign-locked CPM budget allocation
  -> immutable earning and budget entries with rate snapshots
  -> finality/risk policy makes earnings payable
  -> scheduled run atomically reserves creator earnings
  -> durable provider operation calls Whop once semantically
  -> webhook/API reconciliation proves provider outcome
  -> paid allocation and immutable audit event

BloxClips owns: campaign funding/configuration/lifecycle, submission review, accepted metrics, CPM, rate precedence, creator RPM, caps, budget debit, earnings, holds, eligibility, transfer scheduling, local payout state, audit, corrections, and reconciliation decisions.

Whop owns: custody of BloxClips' business source balance and the creator destination balance, transfer execution/state and provider fees, provider-side compliance/risk controls, and the creator's later external withdrawal/KYC/method flow.

Whop webhooks are provider observations, not a replacement for the BloxClips entitlement ledger. Conversely, BloxClips must not claim money moved when Whop has not confirmed it.

Money representation and accounting level ​

MVP precision ​

Use one currency per campaign and initially allow only USD. Persist all BloxClips rates and economic amounts as signed BigInt USD micros (1 USD = 1,000,000 micros), with a three-letter currency column. Do not use Prisma Float or parse currency from display strings.

  • RPM/CPM fields are micros per 1,000 views.
  • Accrual target for a fixed rate segment is roundHalfUp(eligibleViews * rateMicrosPerThousand / 1000) in micros. Calculate the cumulative target for that rate-policy segment and journal only the difference from previously posted target; do not round every scrape delta independently.
  • Provider transfer amount is integer cents. At reservation, transfer floor(totalPayableMicros / 10_000) cents and leave the remainder payable as a carry. This never overpays and does not discard fractions.
  • UI values are derived from integers and formatted; display rounding never writes accounting values.
  • If Whop returns decimal strings, parse with a decimal library into the provider's declared minor precision and compare exactly. Do not pass through JavaScript float arithmetic.

Double entry decision ​

A general double-entry ledger is not required for MVP. An append-only creator earning journal plus an append-only campaign budget journal, tied by a unique source and checked by reconciliation invariants, is sufficient while BloxClips supports one currency, one provider origin, and simple accrual/correction/transfer flows.

This is not permission to keep mutable balances. Cached balances are projections only. Move to a full double-entry subledger before supporting multiple currencies/conversions, creator deposits, refunds as a general wallet, multiple funding accounts, or spendable BloxClips balances.

Proposed Prisma model responsibilities ​

Names follow existing singular PascalCase conventions. Exact indexes may use SQL migrations where Prisma cannot express partial constraints.

Policy and identity models ​

ModelEssential fields/constraintsResponsibility
CampaignRateVersionUUID/id, campaignId, currency, cpmMicrosPerThousand BigInt, defaultRpmMicrosPerThousand BigInt, optional long-form variants only if product retains them, effectiveAt, endedAt, createdByWebUserId, reason; unique effective orderingVersion campaign CPM and default creator RPM. Initial version is required before launch. Past versions never mutate.
ClipperGroupRateVersionid, clipperGroupId, rpmMicrosPerThousand, effective interval, actor/reasonVersions group RPM. Archiving a group ends future applicability, not historical earnings.
CampaignCreatorRpmOverrideid, campaignId, webUserId, RPM, effective interval, actor/reason; no overlapping active intervalIndividual creator override for future deltas within one campaign. Replaces ambiguous submission float override.
WhopPayoutIdentityid, unique active webUserId, unique active (environment, whopUserId), environment, whopUserId, optional discovered ledger ID, status, verificationMethod, verifiedAt, lastCheckedAt, provider evidence hash/version, revoked reasonPayment-specific recipient proof. It may reference existing WhopIdentity as evidence but never trusts it automatically.

Submission.customRate should be removed in the clean dev cutover, not renamed in place. Its scope/unit/history are contradictory. If a later requirement truly needs per-submission exceptions, introduce a separately audited SubmissionRpmOverrideVersion; do not overload the creator/campaign override.

Earning and campaign budget journals ​

ModelEssential fields/constraintsResponsibility
EarningLedgerEntryUUID, entryType (ACCRUAL, FLAT_FEE, CORRECTION, REVERSAL, ROUNDING_CARRY), campaignId, submissionId?, webUserId, currency, signed amountMicros, viewDelta?, from/to accepted views, metricObservedAt?, finalizesAt, rate source/type/version IDs, snapshotted CPM/RPM, cap snapshot, creator-fee policy snapshot, sourceType, sourceId, reversesEntryId?, createdAt; unique (sourceType, sourceId, entryType) or a canonical sourceKeyImmutable economic creator entitlement/correction. No amount/source/rate update or cascade delete.
EarningStateEventid, earningEntryId, from/to state, hold code, actor type/id, reason, correlation ID, timestamp; unique transition command keyAppend-only lifecycle/audit. EarningLedgerEntry.currentState may exist only as a transactionally maintained projection.
CampaignBudgetEntryUUID, campaignId, earningEntryId?, entryType (CPM_DEBIT, FLAT_FEE_DEBIT, TOP_UP, CORRECTION_CREDIT, MANUAL_ADJUSTMENT), signed amountMicros, currency, source key, CPM snapshot, view delta, actor/reason, createdAt; unique source key and one budget effect per earning sourceImmutable campaign budget consumption. Positive/negative sign convention must be fixed in code; recommended positive debit, negative credit.
CreatorEarningBalanceunique (webUserId, currency), heldMicros, payableMicros, reservedMicros, paidMicros, recoveryDueMicros, version/update timestamp; nonnegative/check constraints where applicableRow-lockable, rebuildable projection used to serialize reservation. It is never the source of an earning and must reconcile to entries, state events, and allocations.

Database rules must prohibit deleting a campaign/submission referenced by either journal. Use onDelete: Restrict and soft-delete/archive source records. Corrections and reversals are new signed entries linked to the original.

Payout execution models ​

ModelEssential fields/constraintsResponsibility
PayoutRunUUID, scheduled-for/start/finish timestamps, cadence version, status, worker ID/lease, counts/totals, error summary; unique scheduled slotDurable record of one automatic sweep.
PayoutItemUUID, payoutRunId, webUserId, whopPayoutIdentityId, currency, reservedMicros, transferMinorUnits, carryMicrosAtReservation, status, attempt counters, next-attempt time, failure code/reason, provider-confirmed/paid/reconciled timestamps; unique (payoutRunId, webUserId, currency)One aggregated creator/currency transfer in a run. reservedMicros equals transferMinorUnits * 10_000; carry remains payable and is informational here. The item is not an earning source.
EarningAllocationPositionone-to-one earningEntryId, availableMicros, reservedMicros, paidMicros, version; checks for nonnegative components and conservation against the entry's allocatable positive amountRow-lockable, rebuildable per-entry projection that prevents allocating the same entitlement twice while still permitting a sub-cent remainder to enter a later payout.
PayoutEarningAllocationid, payoutItemId, earningEntryId, allocatedMicros, state, unique (payoutItemId, earningEntryId) and unique reservation command keyImmutable allocation fact. Allocations may use a partial final entry; the transaction locks and decrements its EarningAllocationPosition. Sum of item allocations must equal item reservedMicros.
ProviderOperationUUID, payoutItemId, provider/operation type/environment/API pin, origin/destination IDs, exact amount/currency, unique idempotencyKey, unique nullable providerTransferId, canonical request hash, state, latest provider status, failure code/reason, first/last request and reconciliation timestampsOne semantic external transfer. Its key/request never change.
ProviderOperationAttemptUUID, operation ID, attempt number, request/response correlation, start/end, outcome class, HTTP status, timeout flag, sanitized response hash, error code; unique (operationId, attemptNumber)Audit of transport attempts using the same semantic operation/key. Do not store secrets.
ProviderWebhookEventWhop msg_… primary/unique key, event type, object ID, account ID, API version/date, received/processed timestamps, payload hash, protected payload, processing status/errorSignature-verified durable inbox and at-least-once deduplication.
PayoutAuditEventUUID, payout/run/item/operation IDs as applicable, command key, from/to state, actor type/id, reason, correlation ID, structured non-secret metadata, timestampMandatory financial audit written in the same local transaction as every state change.

Do not overload the current Payout/PayoutItem tables. Existing data is development-only, so use clearly new models or a clean schema replacement in the later cutover; avoid a fragile semantic migration.

Balance definitions ​

For a creator/currency, all API balances are projections with fixed definitions:

  • held: net positive earning amounts whose latest state is held, less pre-payment reversals;
  • payable/available: finalized positive amounts not held, reserved, paid, or consumed by recovery; this is the only input to the transfer threshold;
  • reserved: sum of active payout allocations whose external outcome is not safely canceled or paid;
  • paid: sum of allocations backed by provider-success evidence, before any separately recorded recovery;
  • recovery due: post-payment negative correction awaiting offset/write-off/recovery under owner policy.

At reservation, let T be locked payable micros after recovery offsets. Set P = floor(T / 10_000) * 10_000; reserve exactly P, allocate FIFO across entries (partially allocating the final entry if necessary), and leave T - P payable. This remainder is not a new earning and is not discarded. Every state transition updates the creator and per-entry projections in the same transaction; a reconciliation query must be able to rebuild both entirely from immutable facts.

Rate precedence and snapshot policy ​

For each accepted positive view delta, resolve exactly once:

text
active CampaignCreatorRpmOverride
  else active ClipperGroup membership + active group rate version
  else active CampaignRateVersion.defaultRpm

Recommended constraint: one active group membership per creator per campaign. If overlapping group memberships are allowed, an explicit numeric priority and deterministic tie-breaker become required; this audit recommends avoiding that complexity.

CampaignRateVersion.cpm independently sets campaign budget debit. A lower group/individual RPM lowers creator liability and creates BloxClips margin; it does not lower CPM budget depletion. If RPM exceeds CPM, campaign budget still debits CPM and BloxClips subsidizes the difference. For MVP, reject RPM > CPM at configuration time unless a finance-admin performs an explicit audited exception and treasury coverage is demonstrated.

Rate, source type, source/version IDs, CPM, RPM, caps, fee policy, currency, and relevant timestamps are copied into the immutable earning entry. A policy change affects only accepted metric deltas observed under the new effective version. It never rewrites prior entries.

Earnings and budget rules ​

Metric source and accrual ​

  1. metric-scraper obtains technical facts only and completes ScrapeJob.
  2. Backend reconciliation locks the campaign and submission, validates job identity/status, converts the new total into a nonnegative bounded accepted-view delta, and marks the job applied exactly once.
  3. Only an approved submission in an earning-eligible campaign interval can create positive accrual.
  4. In the same database transaction, resolve policy, apply caps, create the unique EarningLedgerEntry, create the corresponding CPM CampaignBudgetEntry, and update non-authoritative projections.
  5. If remaining campaign budget funds only part of the delta, compute the maximum whole eligible views under CPM using integer arithmetic, accrue only that partial delta, debit the exact CPM target, and mark the campaign depleted. Do not silently use RPM to fit more views.

The source of accrual is the accepted view delta, while total accepted views provide the monotonic/cumulative idempotency check. Replaying the same ScrapeJob or accepted interval creates no second entry.

Minimums, caps, and flat fee ​

  • Campaign minimum views should be an eligibility gate: views accrue technically, but no creator accrual is posted until the submission reaches the configured threshold and is approved. On first eligibility, post the cumulative eligible views once; later posts are deltas.
  • A per-video cap should be stored as a monetary creator cap in micros or a view cap with an immutable policy version. Use one canonical cap unit. The existing short/long/custom cap fields should be mapped deliberately, not combined implicitly.
  • Optional flat fee: disabled for MVP by default. If enabled, create exactly one FLAT_FEE earning and matching budget debit keyed to (submission, approval-version). It finalizes under the same hold and consumes campaign budget 1:1; it is not multiplied by CPM.
  • Aggregate automatic transfer threshold is separate from submission minimum. Recommended safe launch default is USD 100 until actual Whop fees/operations are known; owner may select another value in 07-open-decisions.md.
  • An additional percentage “clipper fee” should default to zero; group/individual RPM already represents the promised creator rate and margin. If retained, version/snapshot fee basis points and show it separately.

Campaign lifecycle ​

Recommended states are DRAFT, FUNDING_READY, LIVE, DEPLETED, ENDED, and ARCHIVED.

  • LIVE requires valid rate/cap/finality policy and a locally committed budget. The single Whop treasury source is BloxClips' business balance; its available funds are a separate aggregate treasury gate, not a per-campaign accounting balance.
  • Manual end records an immutable cutoff. Scheduled end does the same.
  • On end, campaign groups auto-archive; manual group archive remains available. Archive stops future membership/rate selection but does not affect prior earnings.
  • Permit one final technical scrape whose metric observation time is at/before the cutoff and whose result arrives within a recommended 24-hour ingestion grace. Ignore later positive views. The owner can choose a different grace, but it must be fixed and visible.
  • DEPLETED never auto-reopens merely because a source is edited. A valid budget correction/top-up is a journal entry. If the campaign has not ended, an authorized top-up can explicitly return it to LIVE.
  • Late provider/scraper corrections append signed entries. Do not mutate snapshots or delete a paid source.

Rejection and correction policy ​

  • Before approval: no earning accrual; technical views may continue for review analytics.
  • Approval revoked while earnings are HELD or PAYABLE: append an equal negative creator reversal and CPM budget credit with actor/reason. Do not edit the original.
  • Once any related earning is RESERVED: block routine rejection and send to finance/risk review; either cancel the unsent reservation atomically and then reverse, or finish reconciliation first.
  • After provider-confirmed payment: never automatically claw back Whop funds or make the creator balance negative at Whop. Record a local recovery/negative adjustment according to the owner-selected policy; legal/support handling is explicit.
  • Submission deletion becomes soft deletion and cannot remove journal/audit/provider evidence.

State models ​

Earning lifecycle ​

Economic entries are immutable; EarningStateEvent records state changes.

text
ACCRUED/HELD --hold expires + review clear--> PAYABLE --reserved--> RESERVED
      |                                      |                    |
      +--risk hold remains-------------------+                    +--provider success--> PAID
      |                                                           +--safe cancellation--> PAYABLE
      +--pre-payment correction/rejection--> CANCELED/REVERSED

PAID --post-payment correction--> RECOVERY_DUE --resolved/write-off--> RECOVERED
  • ACCRUED is the immutable economic creation event; its initial disposition is HELD.
  • HELD, PAYABLE, RESERVED, and RECOVERY_DUE are nonterminal.
  • PAID, CANCELED (proved no external movement), and RECOVERED are terminal for that entry/allocation.
  • A negative correction is itself an immutable entry; “reversed” never means deleting the original.

Payout item/provider operation lifecycle ​

text
RESERVED -> DISPATCHING -> PROVIDER_CONFIRMED -> PAID
   |             |                 |
   |             +-> UNKNOWN ------+--retrieve/webhook--> PROVIDER_CONFIRMED/PAID
   |             |                 +--proved no movement--> FAILED_RETRYABLE
   |             +-> FAILED_NEEDS_REVIEW
   +-> CANCELED (only before any possible provider movement)

provider later reverses or an overpayment is established:
PAID -> REVERSED -> RECOVERY_DUE -> RECOVERED

PROVIDER_CONFIRMED means Whop accepted/created the transfer and it may still be processing; PAID requires Whop succeeded/completed evidence. UNKNOWN is mandatory after a network timeout, connection reset, ambiguous 409, crash after request, malformed success response, or a missing local commit after a possible call.

FAILED_RETRYABLE is legal only when evidence shows no transfer moved and the same semantic operation/key can be retried. Validation, permission, identity, insufficient available funds, changed-request/idempotency conflict, exhausted attempts, and unexpected provider state are FAILED_NEEDS_REVIEW.

Because Whop documents that a failed transfer may later succeed under the same ID, provider failed is not automatically a permanent terminal state. Reconciliation must retrieve it and preserve the same operation ID/key.

Conflict authority ​

  • BloxClips ledger wins on why/how much the creator was entitled to.
  • Whop retrieve response wins on the current external transfer state. A webhook is a durable trigger/evidence item; retrieve resolves ordering or schema ambiguity.
  • If Whop says succeeded but local state says failed/unknown, mark the allocation paid through an audited reconciliation transaction. Never send again.
  • If local state says paid but Whop later says reversed, append reversal/recovery state; never erase the paid history.
  • If local entitlement differs from a successful provider amount, preserve provider fact, raise a financial incident, and post an explicit adjustment/recovery. Do not rewrite either source.

Automatic scheduling ​

  • Eligibility/finality: each accrual finalizes at the later of submissionApprovedAt + 7 days and metricObservedAt + 7 days, unless a risk hold remains. This rolling hold is the recommended default; owner choice is recorded in the open decisions.
  • Sweep: daily at 02:00 UTC, with an admin “run eligibility scan” command that uses the same scheduled-run machinery and cannot target arbitrary amounts.
  • Batching: one PayoutItem and one Whop transfer per creator/currency per run, aggregating whole eligible entries. Do not aggregate creators or mix currencies.
  • Threshold: default USD 100 at launch, evaluated on available/payable micros after negative corrections. Lower only after provider fees and operational load are measured.

Locking and reservation transaction ​

One scheduler instance obtains a PostgreSQL advisory lock keyed to payout scheduling and inserts the unique scheduled slot. Multiple app instances may call the tick safely; losers exit. Within a run:

  1. Select eligible creators/earnings in stable order with row locks (FOR UPDATE SKIP LOCKED via safe SQL where Prisma cannot express it).
  2. Lock the creator balance, relevant earning allocation positions, and active payment identity. Recheck finality, holds, recovery balance, threshold, currency, and remaining allocatable amounts.
  3. Create PayoutItem, exact whole-cent allocations (the final source entry may be partial), one ProviderOperation with its immutable UUID-derived idempotency key and canonical request hash, projection changes, and audit events.
  4. Commit. The database unique allocation/source constraints are the final duplicate guard.

Never hold a database transaction open across a Whop call.

Dispatch transaction boundaries ​

  1. Prepare transaction: reserve immutable earnings and create provider operation as above.
  2. Claim transaction: atomically change only PREPARED/due operation to DISPATCHING, assign a lease/attempt number, and write attempt/audit. A concurrent worker affects zero rows.
  3. External call: use the exact stored request, API pin, body key, header key, destination, and amount. No mutable inputs are re-read.
  4. Result transaction: persist sanitized response, provider ID/status, attempt outcome, payout state, and audit. If this commit fails after a possible call, lease expiry transitions the operation to UNKNOWN, not retry-ready.
  5. Webhook transaction: verify raw signature/timestamp, insert unique inbox event, return quickly. An async processor locks the operation, retrieves current transfer when needed, and applies a monotonic/audited provider transition.

Retry and ambiguity policy ​

  • Retry known-safe 429 and transient 5xx using the exact operation/key/body after approximately 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours, with bounded jitter; maximum five transport attempts in the first 24 hours.
  • A timeout or uncertain 409 goes directly to UNKNOWN. Reconcile at the same intervals by provider ID or constrained metadata/list lookup. If found, bind the provider ID and follow it. If not found, replay only the same operation/key/body within the documented window and continue reconciliation.
  • After 24 hours, never assume the header key still protects the request. Do not call create again until provider retrieval/list and Whop support establish no movement. Body-key behavior must be confirmed before production.
  • 400, 401, 403, identity/recipient invalid, insufficient available balance, and deterministic capability failures need review; no blind retry.
  • Alert immediately on permission/security/identity or amount mismatch; after 30 minutes unknown; after 6 hours processing; after five attempts; or when origin available funds are below the next due liability plus the owner-selected buffer.

Admin “retry” must mean reconcile first, then re-dispatch the same provider operation only if permitted. No UI can generate a new idempotency key for an existing item. Exceptional replacement operations require finance-admin authorization, proof the original cannot move, a linked supersession record, reason, and preferably dual control.

Identity and authorization ​

Authoritative recipient mapping ​

WhopPayoutIdentity is the authoritative local mapping after explicit payment verification. WebUser remains the BloxClips actor. Existing WhopIdentity can seed a suggestion only.

Recommended user flow:

  1. Creator selects “Connect Whop for payouts.”
  2. Use Whop's supported OAuth/account-link/onboarding flow, with state/PKCE and server-side callback as applicable; do not identify by typed ID or email alone.
  3. Server extracts the signed/granted Whop user_… subject and, using the BloxClips origin credential where supported, verifies it is a valid transfer recipient.
  4. Show a non-sensitive Whop username/display summary and require confirmation that this is the intended balance.
  5. Insert/rotate the unique active mapping with verification method/time/evidence. Revalidate before first payout and periodically.

Do not infer that a Google email equals a Whop user, that a child-company owner belongs to the same human, or that chat provisioning consented to payments.

Failure behavior:

  • absent/unverified identity: earnings remain payable but unscheduled, with creator action required;
  • stale/deleted/ineligible provider identity: disable mapping, put item/earnings on identity hold, notify creator, and never substitute another ID automatically;
  • same Whop user linked to two BloxClips users: database rejects the second active mapping and opens staff review;
  • returned Whop subject differs from current mapping: require explicit replacement confirmation, risk review if money is reserved, and retain old history;
  • payment identity belonging to another person: revoke/hold; no staff “fix” by editing the raw ID.

Roles ​

ActorAllowedForbidden
CreatorView own earning/budget-source explanation, holds, payment identity, scheduled/paid history; connect/revoke identity when nothing is reservedRequest arbitrary payout, choose amount, mark payable/paid, trigger retry/reconciliation, edit provider ID or rate history
Support/reviewerView redacted states and review submission/risk evidence; add reasoned hold/escalation within roleDispatch, replace identity, edit amounts/IDs, override successful/unknown provider state
Payout operatorView queue and run reconciliation; resolve documented retryable operational failures through constrained commandsChange entitlement/amount, create new key, force paid, reverse successful transfer
Finance adminApprove exceptional cancellation/recovery/supersession, configuration and funding gates; view full financial auditDirectly mutate journal/history or bypass reconcile-before-retry
Scheduler/webhook serviceNarrow state-machine commands onlyGeneral admin/application access

Use a least-privilege Whop Account API key scoped to the owner-confirmed BloxClips business source. Resolve and pin its exact biz_…/ldgr_… identifiers during validation; do not accept an arbitrary origin per payout. Exact permissions remain a provider gate; likely documented candidates are discussed in 02-whop-money-api-research.md. Store secrets in deployment secret management/server environment, keep sandbox and production credentials/origins/webhook secrets distinct, never log them, and never expose them through Next.js/browser variables.

Provider reliability and security controls ​

Required invariants ​

  1. Every metric source posts at most one economic effect.
  2. Sum of campaign budget debits minus credits never exceeds funded budget for a live campaign.
  3. Every positive earning has an immutable CPM/RPM/cap/policy snapshot and source.
  4. Allocated amount across payout items never exceeds the earning's positive remaining amount.
  5. One payout item has at most one active semantic provider operation; idempotency key and provider transfer ID are globally unique per environment.
  6. PAID requires stored provider success evidence; a provider success is applied at most once.
  7. Every financial state transition has an audit event in the same transaction.
  8. No financially referenced source row can be hard-deleted.

Outbound correlation and logs ​

Use operation UUID as the base for header/body idempotency and metadata. Structured logs include environment, run/item/operation IDs, attempt number, provider transfer ID when known, request hash, API pin, state, duration, and classified result. Redact authorization, webhook secret/signature, email, tax data, wallet/payout destination, full response payload, and unnecessary user PII.

Metrics/alerts should cover accrual count/value, budget remaining, payable/reserved/paid totals, payout run lag, identity holds, provider outcomes/latency, unknown age, webhook signature failures/lag/duplicates, reconciliation mismatches, available provider funds, and invariant failures.

Webhook security ​

  • Capture raw bytes only on the dedicated webhook route before JSON parsing.
  • Use SDK Standard Webhooks verification with the endpoint-specific ws_… secret.
  • Enforce five-minute timestamp tolerance and expected current API date/account/environment.
  • Insert event ID under a unique constraint before acknowledging; duplicates return 2xx without reapplying.
  • Keep handlers side-effect-light and asynchronous; retrieve transfer on out-of-order or inconsistent event.
  • Rotate secrets with an explicit overlap procedure and test endpoint; log no signatures/secrets.

Reconciliation jobs ​

  • Near-real-time worker processes webhook inbox.
  • Every 15 minutes reconcile DISPATCHING, UNKNOWN, PROVIDER_CONFIRMED, and recent failed transfers.
  • Daily reconcile all transfers changed in the preceding seven days by origin plus all locally open operations; compare amount, currency, destination, metadata, and state.
  • Weekly sample closed operations and reconcile financial aggregates: journal payable/reserved/paid versus allocations and Whop success/fee totals.
  • A mismatch creates an incident and blocks affected creator/new dispatch when duplication or amount uncertainty exists.

Required test coverage ​

Automated tests must include:

  • duplicate and out-of-order scrape results, rate changes between scrapes, late campaign-end results, partial final CPM allocation, and simultaneous campaign accruals;
  • group versus individual precedence, archived group, duplicate membership, zero/invalid override, RPM below/equal/above CPM, caps/minimum/flat fee;
  • exact micro rounding, many one-view deltas versus one total delta, sub-cent carry, negative corrections, and aggregate invariants;
  • duplicate scheduled ticks and two workers reserving the same creator/earning;
  • creator versus staff authorization for every command;
  • duplicate/out-of-order webhook, invalid signature, stale timestamp, wrong account/environment, and event parse/version changes;
  • crash before call, during timeout, after provider success before DB commit, and after DB commit before acknowledgment;
  • same-key replay, changed-body 400, in-flight 409, 429/5xx backoff, 24-hour expiry, insufficient funds, stale recipient, provider failed-then-succeeded, and reversal;
  • admin reconcile/retry cannot produce a second key or transfer; exceptional supersession requires evidence and audit;
  • UI totals/statuses equal backend projections and never call legacy cashout/send endpoints.

Fallback ​

If Whop confirms direct user_… ledger credit is unsupported, use only this fallback: enroll each creator as a Whop connected biz_… account, verify/track that account, ledger-transfer to it, and direct the creator to Whop's hosted payout flow. The local ledger, states, schedule, idempotency, webhook, audit, and reconciliation design remains unchanged; only WhopPayoutIdentity.destinationType, onboarding UI, and capability checks change.

Do not fall back to Payouts, wallet sends, claim links, Stripe, manual off-platform payment, or staff-entered Whop IDs.