Files
civitai__civitai/docs/blue-buzz-paid-access-plan.md
T
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

7.7 KiB

Blue Buzz for early / paid access

Status: proposal, nothing implemented. Ask: let a creator opt in to accepting Blue Buzz for early/paid access purchases.

What this is and isn't

@dev: "Blue and yellow prices would be the same number. Really, we're asking if the creator is open to receiving generation credit (blue buzz). They should still be able to accept green/yellow as they normally would."

So this is one price, an extra accepted currency — not dual pricing, and not a replacement. Green/yellow behaviour is untouched; blue is additive and opt-in.

The trade a creator is accepting: Blue Buzz is not bankable. buzzTypeConfig gives green and yellow bankable: true; blue has neither bankable nor purchasable. It is earned/granted credit — rewards mint blue by default (base.reward.ts:110). A creator opting in is choosing to be paid in generation credits they cannot withdraw. The UI has to say that plainly or it generates support tickets.

The one line that matters

buzz.service.ts:1106:

input.toAccountType = input.toAccountType ?? 'yellow'; // Default to bank if not provided

The destination is yellow regardless of what the buyer spent. Wire blue into paid access without changing this and you have built a free-credit-to-cash conversion: farm blue from rewards, spend it on a cooperating creator's version, creator withdraws yellow. Self-purchase is blocked, two accounts defeat that.

So: when the buyer pays blue, the creator receives blue. toAccountType is settable per call — the ?? is only a default — so this is one argument, not a service change.

What already works

Piece State
Charging a chosen currency earlyAccessPurchase already passes fromAccountTypes: [buzzType]
Per-call destination createMultiAccountBuzzTransaction accepts toAccountType
Client currency UI BuzzTransactionButton takes accountTypes and renders a distribution
Creator Program exclusion Earnings queries filter toAccountType IN ('yellow', 'green') — blue is already excluded (creator-program.service.ts:74, used at 120/317/354/889/894)

The block is a single explicit guard at model-version.service.ts:1908:

if (buzzType === 'blue')
  throw throwBadRequestError('You cannot use Blue Buzz for early access purchases.');

Today it is belt-and-braces — the controller passes getAllowedAccountTypes(ctx.features)[0], which resolves to green or yellow by domain, so blue is never offered in the first place.

Decided: the buyer picks one currency

@dev: "users can select to pay with blue or green on civitai.com, and blue or yellow on civitai.red."

So the choice is blue OR the domain currency — never a blend, never a fallback. Confirmed against the code: getAllowedAccountTypes is purely domain-driven (isGreen ? 'green' : 'yellow', and isGreen is the green color domain), and earlyAccessPurchase already passes a one-element fromAccountTypes. A purchase is single-currency today; blue just adds a second option to choose from.

Domain Buyer may pay with
civitai.com (green) blue or green
civitai.red (red) blue or yellow

This keeps the loophole shut by construction: one source colour in, the same colour out (toAccountType), one transaction. No split payout, no partial conversion.

Two traps this rules out, both worth stating because the obvious implementation hits them:

  • Don't use getAllowedAccountTypes on the charge path. getAllowedAccountTypes(features, ['blue']) returns ['blue', 'green'], and spend drains in array order — every buyer would silently spend blue first, with no opt-in and no choice. It stays the right helper for deciding what the client may offer.
  • BuzzTransactionButton distributes, it doesn't choose. It takes accountTypes, fetches balances and computes a spend distribution across them (getBuzzTypeDistribution) — pass it ['blue','green'] and it drains rather than asks. The purchase modal therefore needs two buttons, each with a one-element accountTypes ("Pay with Blue Buzz" / "Pay with Green Buzz"), which reuses the component's per-currency balance and insufficient-funds handling unchanged. Rendering one button per accepted currency is the whole UI change.

Storage

PaidAccess.terms, as acceptsBlueBuzz?: boolean alongside download / generation.

Chosen for read cost: terms is already loaded by earlyAccessPurchase and already reaches the client via useModelVersionPermission, so the flag is free on both sides. A creator-level column would need a new lookup and a new cache on the purchase path. Nothing rewrites terms on read since the price caps were removed, and gatePrices only reads the two grants — no change.

@ai: Per-version storage, but the stance is catalog-wide ("am I open to generation credit"), so expressing it per-version means editing every gate. Seed it from a creator-level default in Creator Studio settings, with bulk apply — exactly the field shape the bulk grid is being designed for. Storage stays per-version; the default is UX.

Work

  1. Terms + write path. acceptsBlueBuzz on ModelVersionTerms; buildModelVersionTerms takes it; checkbox in both editors (onsite form + Creator Studio drawer) with explicit "not withdrawable" copy.
  2. Purchase input. Add buzzType to modelVersionEarlyAccessPurchase (currently { modelVersionId, type }). Re-validate server-side against the terms — never trust the client's choice.
  3. Charge + payout. Drop the guard; pass toAccountType: 'blue' when the buyer paid blue.
  4. Refund. Return to the same account type the buyer spent.
  5. Client. Offer blue in the purchase modal only when the terms allow it, showing the blue balance.
  6. Accounting sweep — see below.

Accounting

  • Creator Program — already correct. Earnings filter on toAccountType IN ('yellow','green'), so blue income is excluded without further work. Worth an explicit test so it stays that way.
  • Donation goals. earlyAccessPurchase attaches a donation record to the version's EA goal. A blue purchase advancing a goal creators read as cash is probably wrong — decide whether blue counts, and if not, skip the donation write for blue purchases.
  • Creator Studio earnings page. Must separate blue from bankable, or it overstates income for exactly the creators who opted in.

Risks

  • The loophole is the whole risk. Any path where blue enters and yellow leaves converts free credit into withdrawable currency. Worth a regression test asserting toAccountType === fromAccountType for the purchase, not just a careful review.
  • Creator expectation. "Accept Blue Buzz" reads as "get paid" unless the UI says otherwise.
  • Blue is plentiful. It is granted by rewards and bundled with Stripe purchases, so opting in likely shifts real purchase volume from bankable to non-bankable currency. That is the creator's call to make, but the earnings surface should make the split visible before and after.