Appearance
Epic 13 — CasTars Internal Currency
CasTars is the platform's internal virtual currency. It has two sources: purchased with real money (via Stripe) and earned through platform activity. It is spent to unlock premium features, boost visibility, and claim Bounty rewards. This makes CasTars a full virtual economy, not just a gamification points system.
Overview — CasTars Economy
| Flow | Description |
|---|---|
| Buy | Users purchase CasTars bundles with a credit/debit card via Stripe. Real money in → CasTars credited to balance. |
| Earn | CasTars awarded automatically for platform actions (profile completion, applications, hires, referrals). |
| Spend | CasTars deducted when users activate paid features: profile boosts, featured job listings, reel highlights, unlocking advanced filters. |
| Bounty | Every 500 CasTars accumulated (bought + earned) triggers a redeemable Bounty reward. |
BE-CASTARS-001 — Core CasTars ledger model
- [x] Implemented
Files:
- Edit:
prisma/schema.prisma— addCasTarsLedger,CasTarsBundle,CasTarsPurchasemodels - Edit:
src/graphql/schema/index.ts - Create:
src/services/castars/index.ts— balance read, award, deduct, history
Schema:
prisma
model CasTarsLedger {
id String @id @default(cuid())
userId String
delta Int // positive = credit, negative = debit
reason String // human-readable: "Profile section completed", "Bundle purchase", etc.
source CasTarsSource // PURCHASED | EARNED | SPENT | REFUNDED
refId String? // optional FK to purchase or activity ID
balance Int // running balance after this entry
createdAt DateTime @default(now())
@@map("castars_ledger")
}
enum CasTarsSource {
PURCHASED
EARNED
SPENT
REFUNDED
}
model CasTarsBundle {
id String @id @default(cuid())
name String // "Starter", "Popular", "Pro"
stars Int // CasTars amount
priceCents Int // price in cents (USD)
bonusStars Int @default(0) // bonus on top (e.g. 10% extra on larger bundles)
isActive Boolean @default(true)
createdAt DateTime @default(now())
@@map("castars_bundles")
}
model CasTarsPurchase {
id String @id @default(cuid())
userId String
bundleId String
bundle CasTarsBundle @relation(fields: [bundleId], references: [id])
starsAwarded Int
priceCents Int
currency String @default("USD")
stripePaymentId String @unique
status PurchaseStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("castars_purchases")
}
enum PurchaseStatus {
PENDING
COMPLETED
FAILED
REFUNDED
}Earn triggers (called from other services):
- Profile section completed: +50 per section (max 6 sections)
- Application submitted: +10
- Profile viewed by producer: +5 (max 3 per day)
- Hired for a job: +200
- Referral signup: +100
Queries: myStarBalance: Int!, myCasTarsHistory(first: Int, after: String): CasTarsLedgerConnection!
BE-CASTARS-002 — Stripe payment integration for bundle purchases
- [x] Implemented
Files:
- Create:
src/services/castars/stripe.ts— Stripe SDK client, create PaymentIntent, handle webhook - Create:
src/services/castars/bundles.ts— bundle CRUD for admins, active bundle list for users - Edit:
src/graphql/schema/index.ts— addCasTarsBundletype,casTarsBundlesquery,createCasTarsPaymentIntentmutation,confirmCasTarsPurchasemutation - Edit:
src/graphql/resolvers/index.ts - Create:
src/graphql/resolvers/castars.ts - Create:
src/index.ts(add webhook route) —POST /webhooks/stripe→ handlespayment_intent.succeededandpayment_intent.payment_failed
Purchase flow:
- Client calls
createCasTarsPaymentIntent(bundleId)→ server creates Stripe PaymentIntent, returnsclientSecret - Client confirms payment on Stripe (frontend uses Stripe.js / Elements)
- On success, Stripe fires
payment_intent.succeededwebhook → server credits CasTars, createsCasTarsPurchaserecord withCOMPLETEDstatus - Client polls or receives notification that balance was updated
Security rules:
- CasTars are only credited from the webhook handler, never from a client-side call — prevents double-crediting
- Webhook endpoint verifies Stripe signature (
stripe.webhooks.constructEvent) - Idempotency: check
stripePaymentIdunique constraint before crediting
Environment variables needed: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PUBLISHABLE_KEY (passed to frontend via config endpoint)
BE-CASTARS-003 — CasTars spending (feature unlocks)
- [x] Implemented
Files:
- Edit:
src/services/castars/index.ts— addspendCasTars(userId, amount, reason, refId)with balance check - Edit:
src/graphql/schema/index.ts— addSpendableFeatureenum,spendCasTarsmutation - Create
src/services/castars/features.ts— feature cost registry
Spendable features (costs in CasTars):
| Feature | Cost | Effect |
|---|---|---|
| Profile Boost | 200 | Talent appears at top of discover feed for 24 h |
| Featured Job | 300 | Job pinned to top of listings for 48 h |
| Reel Highlight | 150 | Reel shown in "Featured Reels" section for 24 h |
| Unlock Advanced Filters | 500 | Producer unlocks premium search filters for 30 days |
| Direct Message Credit | 50 | Send a direct message outside a job context |
| Application Priority | 100 | Application flagged as "Priority" to the producer |
Guard: spendCasTars throws INSUFFICIENT_BALANCE GraphQL error if currentBalance < cost. All spends are atomic: deduct + activate feature in a single DB transaction.
Add to schemas: ProfileBoost, FeaturedJob, ReelHighlight expiry tracking models (or JSON field on the relevant parent model with boostedUntil DateTime?).
BE-CASTARS-004 — Bounty system
- [x] Implemented
Files:
- Edit:
prisma/schema.prisma— addBountyRedemptionmodel - Edit:
src/graphql/schema/index.ts— addBountyTypeenum,availableBountiesquery,redeemBountymutation - Create:
src/services/castars/bounty.ts
Description: Every 500 CasTars of lifetime accumulation (bought + earned combined, tracked via a lifetimeStars counter on the user) unlocks one Bounty. Available bounty types:
| Bounty | Effect |
|---|---|
BADGE_EARLY_ADOPTER | Permanent badge on profile |
PROFILE_BOOST_3DAY | 3-day profile boost (no CasTars spend required) |
FEATURED_JOB_CREDIT | One free featured job listing |
PREMIUM_FILTER_WEEK | 7-day advanced filter unlock |
Each bounty can only be redeemed once per milestone (milestone = every 500 lifetime stars). Schema:
prisma
model BountyRedemption {
id String @id @default(cuid())
userId String
milestone Int // 500, 1000, 1500, etc.
bountyType String
redeemedAt DateTime @default(now())
@@unique([userId, milestone])
@@map("bounty_redemptions")
}BE-CASTARS-005 — Admin: CasTars management
- [x] Implemented
Files:
- Edit:
src/graphql/schema/index.ts— add admin queries/mutations for CasTars - Edit:
src/graphql/resolvers/admin.ts - Create:
src/services/castars/admin.ts
Admin capabilities:
adminCasTarsBundles: list, create, update, deactivate bundles (price/star amounts)adminCasTarsPurchases(filter): view all purchases, filter by user/status/dateadminGrantCasTars(userId, amount, reason): manually credit CasTars (e.g. for support, promotions)adminRevokeBoost(userId, featureType): remove an active feature boostadminCasTarsStats: total CasTars purchased (revenue), total earned, total spent, active boosts count
FE-CASTARS-001 — CasTars balance widget (header)
- [x] Implemented
Files:
- Create:
apps/app/src/components/CaStarsWidget.tsx— uses DS components - Edit:
apps/app/src/components/AppShell.tsx— add CaStarsWidget to header - Create:
apps/app/src/lib/queries/castars.ts—MY_BALANCE_QUERY,MY_HISTORY_QUERY - Create:
apps/app/src/hooks/useCasTars.ts
Description: Star icon + balance number in app header. Tapping opens a mini-panel with: current balance, "Buy CasTars" CTA, and shortcut to Rewards page. Glowing animation when a Bounty milestone is reached (every 500 lifetime stars). Built from DS Card, Badge, Button.
FE-CASTARS-002 — CasTars purchase flow (buy with real money)
- [x] Implemented
Files:
- Create:
apps/app/src/pages/castars/BuyCasTarsPage.tsx - Create:
apps/app/src/components/BundleCard.tsx(DSCardbased) - Create:
apps/app/src/lib/stripe.ts— Stripe.js init (loadStripe) - Create:
apps/app/src/components/StripeCheckout.tsx— Stripe Elements card form - Create:
apps/app/src/hooks/usePurchaseCasTars.ts - Edit:
apps/app/src/App.tsx— add route/castars/buy
Purchase UX flow:
- User opens
/castars/buy - Page loads
casTarsBundlesquery — shows bundle cards (Starter / Popular / Pro with bonus %) - User selects a bundle → calls
createCasTarsPaymentIntent(bundleId) - Stripe Elements card form appears with the returned
clientSecret - User submits → Stripe confirms payment
- On success: show confirmation, balance updates automatically (webhook → notification → React Query invalidation)
- On failure: show Stripe error message inline
Bundle display example:
- Starter: 500 CasTars — $4.99
- Popular: 1,500 CasTars + 150 bonus — $12.99 ⭐ Best Value
- Pro: 4,000 CasTars + 600 bonus — $29.99
Note: Never store card details. All payment data handled by Stripe Elements (PCI compliant).
FE-CASTARS-003 — CasTars spending UI (feature store)
- [x] Implemented
Files:
- Create:
apps/app/src/pages/castars/FeatureStorePage.tsx - Create:
apps/app/src/components/FeatureStoreCard.tsx(DSCard) - Create:
apps/app/src/hooks/useSpendCasTars.ts - Edit:
apps/app/src/App.tsx— add route/castars/store
Description: "Feature Store" page — grid of purchasable feature cards, each showing: feature name, description, cost in CasTars, duration, and "Activate" button. Button disabled if balance is insufficient (shows "Need X more CasTars" tooltip). After purchase: show confirmation toast, update active feature status on the relevant page (profile, job, reel). Uses DS Card, Button, Badge, Toast.
FE-CASTARS-004 — Transaction history & rewards page
- [x] Implemented
Files:
- Create:
apps/app/src/pages/castars/CasTarsPage.tsx— tabbed: History | Bounties | Store - Edit:
apps/app/src/App.tsx— route/castars
Description: Tabbed CasTars hub.
- History tab: Ledger of all credits and debits. Each row: date, reason, source (PURCHASED/EARNED/SPENT), delta (green + / red -), running balance.
- Bounties tab: Milestone progress bar (X / 500 lifetime stars to next Bounty). Unlocked bounties with "Redeem" buttons. Redeemed bounties greyed out.
- Store tab: Links to Feature Store page (
/castars/store).