Commit Graph

2 Commits

Author SHA1 Message Date
briant 0913576b69 feat(monetization): replace tier price caps with a monthly pricing allowance
Membership stops governing how much a creator may charge and starts governing how
often they may put a new price on something — article 33749, plus the decisions
recorded in docs/licensing-fee-revamp-questions.md.

- Price caps deleted. The licensing fee ceiling is a flat 100/generation for every
  creator (500 on a video model); paid access has no ceiling at all. Every clamp
  goes with them, including the cap-tier Redis cache — getViewerMonetization, the
  purchase path and the orchestrator-facing mini endpoint no longer resolve a
  subscription tier at all.
- Membership instead grants a monthly allowance of NEW prices: free 3, bronze 10,
  silver 25, gold unlimited, counted in a new PricingSlot ledger keyed like
  PaidAccess. One slot per entity, spent on first pricing, never returned.
- A creator score of 10,000 is required to apply a price to something that has
  none. Moderators are not exempt from the floor or the allowance; they remain
  exempt from the fee ceiling.
- Both rules gate only the unpriced → priced transition. Editing, lowering or
  clearing a price that is already set is always free, at any score — which
  grandfathers everything priced before these rules existed, and is why the
  cutover needs no backfill.

Creator Studio enforces both rules itself: its fee and gate writes are direct SQL
that never reaches the service layer, so a rule not applied there is a rule the
spoke can bypass. The rules themselves live in @civitai/buzz so the two
implementations cannot drift on a threshold or a message.

Also reverts the unshipped lapse-grace work: it mitigated lapse-repricing, and a
lapse no longer changes any price.

MIGRATION — 20260821120000_pricing_slot is applied by hand, per environment, and
must land BEFORE this code deploys: until the table exists every first-time pricing
write throws on a missing relation, and the Creator Studio models page 500s
outright. Apply it under a short lock_timeout — the inline owner FK takes SHARE ROW
EXCLUSIVE on "User". See the migration header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 16:14:19 -06:00
briant 9ceb92e0d6 feat(creator-studio): edit usage control, and stop direct writes going stale
Creator Studio could set a version's pricing but not whether that version was
downloadable at all — usage control was read-only context, echoed into the
paid-access form as a hidden field. It's now an editable picker with its own
save, sitting above Early & paid access because it describes the version rather
than its gate: a creator can flip a version to generation-only without also
filling in pricing.

Written the way the spoke already writes licensingFee -- kysely, with ownership
enforced in the WHERE so a version you don't own updates zero rows. That skips
the main app's cross-validation, so the rule it would have applied is enforced
here instead: a version that isn't downloadable can't be charging for downloads.
When the picker is saved alongside a gate, usage control is persisted FIRST,
because the main-app endpoint validates terms against the stored value.

Placing the picker inside the paid-access form would also have hidden it behind
that section's two banner branches -- a creator at their permanent-access limit,
or past publish, could never have reached it.

Those direct kysely writes were invisible to the main app's caches, so a fee or
usage-control change could take up to a day to surface on-site. bustMvCache
spans six caches, a CDN purge and the search-index queue, and a second copy of
that list in the spoke would drift the first time one is added -- so it stays
where it's defined, behind a narrow ownership-scoped endpoint the spoke calls
after writing. Wired into all four direct-write paths, which fixes the
pre-existing licensingFee staleness alongside the new field. Fire-and-forget: a
failed bust must not turn a saved change into an error.

Fixes the drawer's unreadable help text. The palette runs light-to-dark, so
text-dark-3 reads as "muted" but is #5C5F66 on a #25262B card -- 2.36:1, against
the 4.5:1 WCAG AA needs, and all of it 12px explanatory copy. text-dark-2 clears
it at 4.73:1 while still reading as secondary. 44 occurrences across the drawer,
the models page and the caps table.

Adds two plan docs: the monetization cap API cleanup, and the bulk grid that
replaces the CSV round-trip for multi-field editing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:05:31 -06:00