Appearance
Financial and observation contract
Date: 2026-09-06. Architectural contract for planned analytics, not a claim that its source tables exist. Logical fact names below are responsibilities, not mandated Prisma model names. The additive executable v2 interfaces and reconciliation fixture are frozen for review in backend #78; the pure observation projection is in stacked draft backend #79. Upstream facts remain unavailable until their owning issues land. See readiness and decisions.
Amounts, identity, and provenance
- MVP scope is USD; aggregate only one currency at a time. Keep the landed
DECIMAL(20,6)rate schema and exact micro-dollar calculation utilities. Do not rewrite rates to the older payout document's proposed BigInt schema. Economic amounts must be exact micros or equivalently exact fixed decimals; rates are USD per 1,000 views, not money balances. - Money DTOs use canonical decimal strings, with explicit
currencyand gross/net basis. Sum exact amounts before display rounding. Frontend display rounding must never feed accounting. Large counts must have a documented safe-integer boundary or string serialization. - Every new economic fact needs stable creator
webUserId, campaign, submission where applicable, source observation/command identity, effective policy versions, currency, amount, and economic/recorded timestamps. Legacy Discord identity is resolved through a verified mapping, never by display name. Unmapped facts stay in an explicit unattributed bucket. - Landed rate policy: highest numeric applicable campaign/group/individual RPM wins, with deterministic selected source and all tied winners. Membership end is exclusive. Group persistence/injection is still #26/#27. Current individual overrides are submission-scoped; do not invent campaign-wide creator overrides.
- Earning/spend writers resolve rates once for an accepted eligible interval, record its policy and source, and post idempotently. Analytics sums those postings. It must not multiply lifetime views by today's rate, rescrape, allocate budget, change payout state, or repair a journal while answering a GET.
Canonical displayed values
| Display / suggested v2 field | Definition and persisted authority | Important exclusions |
|---|---|---|
Received funding / receivedFunding | Verified settled payment credits less explicit payment reversals, from #31/#33. Receipt time is provider evidence, recorded separately from reconciliation time. | Invoice creation, invoice face value, pending/unknown payments, mutable Campaign.budget, aggregate Whop treasury balance. |
Allocated/committed funding / committedFunding | Net verified funding allocations assigned to this campaign, with source links; supports multiple receipts/allocations. | Received funds that have not been assigned to this campaign; duplicated invoice/event rows. |
CPM spend / cpmSpend | Net posted campaign-consumption debits minus explicit consumption correction credits, from #53/#54. CPM is independent of RPM and cashout fees. | Creator payouts are settlement of liability, not another campaign CPM debit. Display config is not a debit. |
RPM expense / rpmExpenseGross | Signed gross earning/accrual postings plus explicit economic adjustments attributable to this campaign. The journal must distinguish unpaid reversals from post-payment recovery assessments. | Not all-time views times RPM; not the amount sent to a bank; not CPM spend. |
Accrued earnings / accruedGross | Posted creator compensation before separately identified fee/withholding deductions. Entitlement and correction history survive later review/rate changes. | Unapproved views that have no earning posting are performance only. |
Unfinalized / unfinalizedGross | Unsettled posted entitlement still awaiting the earning policy's finality/approval conditions, not on an active blocking hold or reservation. | A current Submission.status alone cannot establish financial finality. |
Held / heldGross | Unsettled entitlement blocked by one or more active durable holds. Aggregate each affected amount once using hold-to-entry links. | Adding one amount per risk signal would duplicate liability. A badge string is not a hold. |
Approved/available / availableGross, availableNet | Final, explicitly eligible, unheld, unreserved, unpaid entitlement after applicable adjustments. Net includes only persisted policy deductions. | Payout review approval is not money movement. Tax/destination/threshold gates may separately prevent dispatch. |
In process / reservedGross, reservedNet | Active entry allocations owned by an operation whose outcome is unsent, processing, or uncertain. Preserve these until success or proven release. | Never call AWAITING_SEND or an ambiguous provider timeout paid. |
Pending / pendingGross | For the new UI, a subtotal of unfinalized + held + available, excluding reserved and paid. Show these components. This is an explicit proposed replacement for the ambiguous old label. | It is not the available-to-withdraw balance. Preserve old field semantics until the frontend switches to v2. |
Outstanding / outstandingGross | pendingGross + reservedGross on the same entry basis. Expose net liability separately if supported. | Do not subtract net transfers from gross accruals. |
Paid earnings / paidGross, paidNet | Finalized allocations backed by confirmed provider success, attributed through earning entries to campaign/submission/creator. Gross is the settled earning basis; net is creator credit after recorded deductions. #59 plus #56–#58 owns this mapping. | paidOut, paidViewsTotal, paidAmountTotal, approval timestamps, requested totals, or mutable current moderation status are not canonical paid evidence. |
Remaining campaign funds / remainingCommitted | committedFunding - cpmSpend, after each side's explicit adjustments; funding owner must expose any separate commitment reservations as a further availableToCommit deduction. | Do not subtract RPM expense and payouts again. This is campaign budget capacity, not provider cash liquidity. |
Campaign contribution / contributionGross | cpmSpend - rpmExpenseGross, with same scope and recognition basis, if required by the final DTO. Label before operating/provider costs. | Not net profit; RPM may exceed CPM. No new prohibition or hidden fee rule is selected here. |
| Payout history | Paginated operations with amount basis, status/events, actual initiated/completed times, gross/net/fee breakdown and campaign/submission allocations. Failed/released/unknown operations remain visible but do not add to paid. | A single campaign must never receive the whole amount of a cross-campaign transfer or its unrelated referral bonus. |
| Recovery due | Separate attributable post-payment assessments less evidenced recovery, approved offsets, or write-offs. Original paid allocations stay paid. | Never rewrite a completed transfer or silently net recovery against the paid-history total. Collection policy belongs to #55/#58. |
Fees, withholding, referral inclusion, finality duration, and historical migration policy remain explicit upstream decisions. Until they land, return unavailable/partial financial components, not invented zeroes or a guessed 7%/0% fee. Pure fixtures may choose an explicitly named synthetic zero-fee policy to isolate gross arithmetic; that is not a product decision.
Reconciliation rules
netVerifiedReceipts = assignedFunding + unassignedFundingat funding-source scope. For a campaign without separately reserved commitments,committedFunding = cpmSpend + remainingCommitted. Never claim received equals consumed plus remaining unless all received money is assigned and adjustments are included.- Each accepted accounting source posts at most once, with paired CPM and RPM facts using the recorded funded view interval. Source identity and campaign serialization belong to #53/#54, including approval and payout-refresh paths.
- For each positive entitlement position after unpaid reversals: its amount equals unfinalized + held + available + reserved + paid + explicitly extinguished/offset amount, all on the same gross or net basis. Holds are disjoint amount buckets even with multiple reasons. Use the upstream position/recovery state contract for negative entries; do not force
expense = paid + pendingacross fees, reservations, or post-payment corrections. - An operation's allocated net total equals its confirmed creator credit; any gross-to-net difference reconciles to recorded fee/withholding allocations. Referral or unrelated credits use separate earning classes. Provider fees borne by BloxClips are separately recorded costs.
- Partial entry allocation is permitted only if #59 explicitly models disjoint remaining amounts; never allocate the same portion twice. Proposed cent settlement floors payable micros to cents and retains sub-cent carry as existing payable entitlement, not a new earning. Confirm this in the payout contract before implementation.
- Aggregate base facts before joining one-to-many rates, memberships, signals, or provider attempts. Campaign union totals de-duplicate entry/observation IDs; top-N rows and page totals are not all-time totals.
- Financial history is independent of present submission acceptance, campaign activity, membership, or soft deletion. Performance reports may use a current-accepted cohort, but that cohort must not silently filter already-paid facts.
Time and filter boundaries
The v2 contract uses UTC half-open windows [from, to) and an explicit asOf/snapshot watermark. Date-only to selection becomes the next UTC midnight on the server. Validate allowed platform/status values, positive IDs, ordering, maximum window (initially 366 days), and pagination (default 20, max 100). A time filter always names its basis:
| Measure | Basis |
|---|---|
| Observation change | Persisted observedAt with source time and ingestion time; legacy ViewSnapshot.snapshotDate must be labelled legacy recording-time coverage. |
| New submissions / posts | Submission.createdAt; platform postedAt is a distinct metric, never a fallback. |
| Review activity | Committed review event occurrence time, actor and from/to status. A current status count is a separate stock. |
| Earnings/spend | Economic effective/posting interval fixed by #53; recordedAt supports late-arrival reconciliation. Never use payout creation time. |
| Funding | Verified receipt/allocation effective time as supplied by funding domain. |
| Paid | Provider-confirmed completion time. If a legacy completed row lacks completedAt, include only the supported all-time amount and mark its dated coverage incomplete. |
| Holds/reservations at cutoff | Latest persisted state events at or before asOf, not current booleans projected backward. |
Observed views are signed changes between consecutive observations for each submission, with a deterministic order and the last baseline before the requested window. A true first observation contributes its first recorded total, matching the sponsor report's observation-change convention, with first-observation coverage disclosed. No snapshot on a day means no recorded change, not proof of zero platform traffic. Preserve negative corrections; do not clamp them away or distribute growth across missing days. Do not use live current totals or upload-date inference to fill historical gaps. Existing tracking snapshots write clamp.finalCurrentViews, so legacy series can already reflect budget clamping; label that provenance and do not call them raw platform observations. #37 must preserve raw observations separately from payable/budget adjustments.
Daily cumulative series equals the opening observed level plus signed in-window changes. The opening level, first-observation convention, missing coverage, and latest observation time travel in the DTO. Current effective display views (manual ?? frozen ?? current) remain a distinct stock; they need not equal raw historical observation totals. An adjustment series requires persisted adjustment evidence first.
The existing sponsor scope is current ACCEPTED submissions, all-time effective current metrics, and 90 UTC days of observation changes. Preserve that scope explicitly as current_accepted; it is a present-day cohort report and may change after moderation. New historical status filters must use #41/#42 committed transitions at observation time (status_at_observation) or at an explicitly fixed cutoff (status_at_asOf). A committed transition is effective at its exact occurredAt; an observation at the same instant sees the transition's toStatus. A02 implements those named modes from persisted events, with incomplete coverage before reliable history begins; it must not reconstruct them from today's status or first acceptedAt. Compute each submission's observation delta before applying the historical status filter, so a status transition cannot restart its baseline. Public sharing controls and visibility do not expand with this contract.
Group attribution
Use membership intervals at the fact's time: submission creation for submitted counts, committed review event for accepted counts, observation time for performance changes, earning attribution time for earnings; payout amounts follow their earning allocations even after a member leaves. Archive does not erase history. Membership or identity unknown at that time is an explicit unattributed bucket.
Overlapping groups are owner-approved. Group membership performance is therefore a cohort measure: one fact can belong to multiple groups, so summing group totals is invalid. Each group's member rows reconcile to that group's de-duplicated facts; the union across groups plus unattributed facts reconciles to campaign scope. Rate-winning-source attribution is a separate optional breakdown using the recorded selected source and ties, not a substitute for membership performance. No equal split or arbitrary primary group is invented. If the product requires additive group totals, resolve D-04 before that variant ships.
API compatibility and security
Prefer additive analyticsV2 fields on existing protected responses, or dedicated protected read endpoints where shapes differ substantially. All v2 responses declare contractVersion, scope, currency/basis, window/time basis, asOf, and per-measure coverage (complete, partial, unavailable, reason and known/unattributed counts). Backend first, frontend per audience second; old numeric fields remain until measured consumer retirement. Do not silently reinterpret old totalSpent/balance fields or add sensitive finance data to presently public /api/campaigns routes.
Authorization is checked before cache lookup. Cache keys include authorized subject/scope, filters, contract version, and data watermark; responses disclose freshness. Use a consistent DB snapshot for reconciled totals. Preserve sponsor's post-query token validity recheck. See system map for route protections and required TODO(RBAC) attachment points.