Banking and extraction phase notifications were being sent 24 hours before the stages actually changed, causing users to receive misleading messages (e.g., "banking phase has ended" while the phase was still open). Updated job schedules to fire at stage boundaries: - bankingPhaseEndingNotification: day L-4 → L-3 - extractionPhaseStartedNotification: day L-3 → L-2 Updated documentation to clarify the relationship between scheduled jobs and actual stage transitions.
12 KiB
Creator Program - Developer Guide
Overview
The Creator Program lets eligible creators bank their earned Buzz into a monthly compensation pool and receive cash payouts proportional to their contribution. Both Yellow (.com) and Green (.green) Buzz feed into a single unified pool.
Lifecycle (Monthly)
Day 1 ──────────────────────────────── Day L-3 ──── Day L
│ Banking Phase │ Extraction │
│ Users bank yellow/green buzz │ Users can │
│ into the pool │ withdraw │
│ │ their buzz │
└──────────────────────────────────────┴─────────────┘
↓
Last day of month:
Distribution job runs
(allocates cash to participants)
↓
15th of next month:
Settlement job runs
(cashPending → cashSettled)
↓
User withdraws via Tipalti
Key Concepts
Buzz Types & Accounts
| Account | Purpose |
|---|---|
yellow |
Main .com buzz (earned, purchased) |
green |
Green buzz (.green domain) |
creatorProgramBank |
Unified bank account for all banked buzz |
cashPending |
Cash allocated but not yet settled |
cashSettled |
Cash ready for withdrawal |
Both yellow and green buzz bank into the same creatorProgramBank account. The source type is preserved in ClickHouse transaction records (fromAccountType), so extraction can refund each type correctly.
Relevant file: src/shared/constants/buzz.constants.ts
buzzBankTypes=['green', 'yellow'](order matters for iteration)BuzzSpendType,BuzzCreatorProgramType,BuzzAccountType- type definitions
Month Account
Each month gets a numeric account ID in the format YYYYMM (e.g., 202504). This is used as the accountId for the creatorProgramBank to separate months.
// src/server/services/creator-program.service.ts
export function getMonthAccount(month?: Date) {
return Number(dayjs(month).format('YYYYMM'));
}
Phases
Determined by getPhases() in src/server/utils/creator-program.utils.ts:
- Banking Phase: Month start to 3 days before month end (UTC)
- Extraction Phase: Last 3 days of the month (UTC)
A Redis key REDIS_SYS_KEYS.CREATOR_PROGRAM.FLIP_PHASES can swap these for testing.
Compensation Pool
The pool value is calculated from the previous month's revenue:
poolValue = (purchases + redeemable codes) / 1000
- taxes (CREATOR_POOL_TAXES%)
* portion (CREATOR_POOL_PORTION%)
Fallback: $35,000 if env vars or data are missing.
Pool size = total buzz currently banked in creatorProgramBank for the month.
Architecture
Service Layer
src/server/services/creator-program.service.ts - Core business logic.
Banking
bankBuzz(userId, amount, buzzType: 'yellow' | 'green')
- Validates: not banned, has active membership, in banking phase
- Checks unified cap (sum of all banked types vs cap)
- Creates
TransactionType.Bankfrom user's buzz account tocreatorProgramBank - Busts caches, signals pool update
Extraction
extractBuzz(userId)
All-or-nothing across all buzz types:
- Validates: not banned, in extraction phase
- Gets banked amounts per type via
getBanked(userId) - Extracts each type back to its original account (green -> green, yellow -> yellow)
- Calculates fee on combined total, distributes proportionally across types
- Fee uses
Math.floorfor all but last type (last gets remainder to avoid rounding errors)
Getting Banked Amounts
getBanked(userId) → { perType: { yellow: number, green: number }, total: number, cap: UserCapCacheItem }
- Queries
getCounterPartyBuzzTransactionsfor each buzz type againstcreatorProgramBank - The counterparty filter preserves type separation even with a unified bank account
- Cap is unified across all types based on highest membership tier
Caps
src/shared/constants/creator-program.constants.ts
| Tier | Cap | Peak Earning Multiplier |
|---|---|---|
| Founder | 100,000 | - |
| Bronze | 100,000 | - |
| Silver | 1,000,000 | 1.25x |
| Gold | No fixed limit | 1.5x |
- Minimum cap: 100,000 for all tiers
- Silver/Gold caps scale with peak monthly earnings over a 12-month rolling window
- The highest tier across all active subscriptions is used
Relevant code: createUserCapCache() in the service queries CustomerSubscription joined with Product.metadata.tier.
Extraction Fees
src/shared/constants/creator-program.constants.ts
| Buzz Amount | Fee Rate |
|---|---|
| 0 - 100,000 | 0% |
| 100,001 - 1,000,000 | 5% |
| 1,000,001 - 5,000,000 | 10% |
| 5,000,001+ | 15% |
Tiered: e.g., 200,000 buzz = 0% on first 100k + 5% on next 100k = 5,000 fee.
Cash & Withdrawals
Flow:
- Distribution job allocates pool value to
cashPendingaccounts (last day of month) - Settlement job moves
cashPendingtocashSettled(15th of next month) - User calls
withdrawCash()which creates a Tipalti payment
Withdrawal fees (from Tipalti payment method):
- ACH: $2.00 fixed
- PayPal: 5%
- Check: $4.00 fixed
Minimum withdrawal: $50 (MIN_WITHDRAWAL_AMOUNT = 5000 in cents)
Jobs
src/server/jobs/creators-program-jobs.ts
| Job | Cron | What it does |
|---|---|---|
creatorsProgramDistribute |
2 23 L * * |
Allocates pool value to participants as cashPending |
creatorsProgramInviteTipalti |
50 23 L * * |
Creates Tipalti payee for users above $50 threshold |
creatorsProgramRollover |
0 0 1 * * |
Flushes all caches for new month |
creatorsProgramSettleCash |
0 0 15 * * |
Moves cashPending -> cashSettled, notifies users |
bankingPhaseEndingNotification |
0 0 L-3 * * |
Notifies users on the last banking day (banking closes end of L-3) |
extractionPhaseStartedNotification |
0 0 L-2 * * |
Notifies users on the first extraction day (L-2) |
extractionPhaseEndingNotification |
0 0 L * * |
Notifies extraction phase ending |
Distribution Logic
- Get unified compensation pool (value + size)
- Get all participants from
creatorProgramBankfor the month - For each participant:
share = floor(poolValue * (userBanked / totalBanked) * 100) - Cap per-buzz value at
CAPPED_BUZZ_VALUE($0.001/buzz) - Create
cashPendingtransactions - Advance month counter in
dbKV
Caching
Redis Keys (REDIS_KEYS.CREATOR_PROGRAM)
| Key | Content | TTL |
|---|---|---|
CAPS |
User cap data (tier, peak earnings, cap amount) | 1 day |
BANKED:{userId} |
Per-type banked amounts for user | 1 day |
CASH |
User cash balance (pending, ready, withdrawn) | 1 day |
POOL_VALUE |
Monthly pool dollar value | 1 day |
POOL_SIZE |
Current total banked buzz | Not cached (live) |
POOL_FORECAST |
Forecasted pool size | 1 day |
PREV_MONTH_STATS |
Previous month statistics | 1 month |
Cache busting: After bank/extract/withdraw operations, relevant caches are busted and signals sent for realtime UI updates.
Legacy Cache Cleanup
The bustCompensationPoolCache() function also clears old per-type cache keys (:yellow, :green suffixes) from before the pool unification.
Data Storage
ClickHouse (buzzTransactions table)
Used for:
- Peak earnings calculation (compensation, tips, early access)
- Pool value calculation (purchases, redeemable codes)
- Pool forecast (projected earnings)
- Pool participants (bank/extract transactions)
PostgreSQL
| Table | Purpose |
|---|---|
User |
onboarding bitwise flags for program membership/bans |
CustomerSubscription + Product |
Membership tier for cap calculation |
CashWithdrawal |
Withdrawal records (status, amount, fee, Tipalti metadata) |
UserPaymentConfiguration |
Tipalti account setup, payment method, status |
Realtime Updates
Signal topic: SignalTopic.CreatorProgram ('creators-program')
| Signal | When | Data |
|---|---|---|
CompensationPoolUpdate |
After bank/extract | Full pool object |
CashInvalidator |
After withdraw/settlement | Empty (triggers refetch) |
Frontend hooks in src/components/Buzz/CreatorProgramV2/CreatorProgram.util.ts listen via useCreatorPoolListener().
Access Control
Joining Requirements
- Creator Score >= 40,000 (
MIN_CREATOR_SCORE) - calculated from model/article/image/user scores - Active membership (Bronze, Silver, or Gold - NOT Founder or Free)
- Not banned (
OnboardingSteps.BannedCreatorProgramflag)
Onboarding Flags
Bitwise flags on User.onboarding:
| Flag | Value | Meaning |
|---|---|---|
CreatorProgram |
16 | User has joined the creator program |
BannedCreatorProgram |
32 | User is banned from the creator program |
Tipalti Integration
src/server/http/tipalti/tipalti.caller.ts - HTTP client for Tipalti API.
Flow
- User reaches $50 in
cashSettled->creatorsProgramInviteTipaltijob creates payee - User completes Tipalti onboarding (via email link or dashboard URL)
- Tipalti webhook updates
UserPaymentConfigurationwith account status and payment method - User calls
withdrawCash()-> creates payment batch via Tipalti API - Tipalti processes payment -> webhook updates withdrawal status
Key Functions
createTipaltiPayee()- Creates payee, sends invite emailpayToTipaltiAccount()- Submits payment batch (amount in dollars, not cents!)getTipaltiDashboardUrl()- Signed URL for user's payment dashboard
Frontend
Components
| Component | File | Purpose |
|---|---|---|
CreatorProgramV2 |
src/components/Buzz/CreatorProgramV2/CreatorProgramV2.tsx |
Main dashboard |
BankBuzzCard |
Same file | Buzz type selector + amount input for banking |
ExtractBuzzCard |
Same file | All-or-nothing extraction button |
CompensationPoolCard |
Same file | Unified pool value and size |
EstimatedEarningsCard |
Same file | Per-type banked breakdown + value estimate |
WithdrawCashCard |
Same file | Cash withdrawal interface |
Hooks (CreatorProgram.util.ts)
| Hook | Returns |
|---|---|
useCompensationPool() |
Unified pool data |
useBankedBuzz() |
{ perType, total, cap } |
useCreatorProgramPhase() |
Current phase ('bank' or 'extraction') |
useCreatorProgramMutate() |
Bank, extract, withdraw mutations |
useCreatorPoolListener() |
Subscribes to realtime pool/cash signals |
Pages
/creator-program- Landing page with how-it-works, FAQ, stats (src/pages/creator-program/index.tsx)/user/buzz-dashboard- Main dashboard withCreatorProgramV2component (src/pages/user/buzz-dashboard.tsx)
Testing
Test file: src/server/services/__tests__/creator-program.service.test.ts
Covers: getCreatorRequirements, joinCreatorsProgram, getBanked, bankBuzz, extractBuzz, getCompensationPool, withdrawCash, unified pool invariants.
Run tests:
pnpm run test:unit -- src/server/services/__tests__/creator-program.service.test.ts
Shared utility tests: src/shared/utils/__tests__/creator-program.utils.test.ts (cap calculations).
Environment Variables
| Variable | Purpose |
|---|---|
CREATOR_POOL_TAXES |
Tax percentage to deduct from gross revenue |
CREATOR_POOL_PORTION |
Percentage of post-tax revenue allocated to pool |
CREATOR_POOL_FORECAST_PORTION |
Percentage for forecasted pool size |
TIPALTI_API_URL |
Tipalti API base URL |
TIPALTI_* |
Various Tipalti auth credentials |