Skip to content

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

FlowDescription
BuyUsers purchase CasTars bundles with a credit/debit card via Stripe. Real money in → CasTars credited to balance.
EarnCasTars awarded automatically for platform actions (profile completion, applications, hires, referrals).
SpendCasTars deducted when users activate paid features: profile boosts, featured job listings, reel highlights, unlocking advanced filters.
BountyEvery 500 CasTars accumulated (bought + earned) triggers a redeemable Bounty reward.

BE-CASTARS-001 — Core CasTars ledger model

  • [x] Implemented

Files:

  • Edit: prisma/schema.prisma — add CasTarsLedger, CasTarsBundle, CasTarsPurchase models
  • 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 — add CasTarsBundle type, casTarsBundles query, createCasTarsPaymentIntent mutation, confirmCasTarsPurchase mutation
  • Edit: src/graphql/resolvers/index.ts
  • Create: src/graphql/resolvers/castars.ts
  • Create: src/index.ts (add webhook route) — POST /webhooks/stripe → handles payment_intent.succeeded and payment_intent.payment_failed

Purchase flow:

  1. Client calls createCasTarsPaymentIntent(bundleId) → server creates Stripe PaymentIntent, returns clientSecret
  2. Client confirms payment on Stripe (frontend uses Stripe.js / Elements)
  3. On success, Stripe fires payment_intent.succeeded webhook → server credits CasTars, creates CasTarsPurchase record with COMPLETED status
  4. 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 stripePaymentId unique 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 — add spendCasTars(userId, amount, reason, refId) with balance check
  • Edit: src/graphql/schema/index.ts — add SpendableFeature enum, spendCasTars mutation
  • Create src/services/castars/features.ts — feature cost registry

Spendable features (costs in CasTars):

FeatureCostEffect
Profile Boost200Talent appears at top of discover feed for 24 h
Featured Job300Job pinned to top of listings for 48 h
Reel Highlight150Reel shown in "Featured Reels" section for 24 h
Unlock Advanced Filters500Producer unlocks premium search filters for 30 days
Direct Message Credit50Send a direct message outside a job context
Application Priority100Application 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 — add BountyRedemption model
  • Edit: src/graphql/schema/index.ts — add BountyType enum, availableBounties query, redeemBounty mutation
  • 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:

BountyEffect
BADGE_EARLY_ADOPTERPermanent badge on profile
PROFILE_BOOST_3DAY3-day profile boost (no CasTars spend required)
FEATURED_JOB_CREDITOne free featured job listing
PREMIUM_FILTER_WEEK7-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/date
  • adminGrantCasTars(userId, amount, reason): manually credit CasTars (e.g. for support, promotions)
  • adminRevokeBoost(userId, featureType): remove an active feature boost
  • adminCasTarsStats: 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.tsMY_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 (DS Card based)
  • 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:

  1. User opens /castars/buy
  2. Page loads casTarsBundles query — shows bundle cards (Starter / Popular / Pro with bonus %)
  3. User selects a bundle → calls createCasTarsPaymentIntent(bundleId)
  4. Stripe Elements card form appears with the returned clientSecret
  5. User submits → Stripe confirms payment
  6. On success: show confirmation, balance updates automatically (webhook → notification → React Query invalidation)
  7. 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 (DS Card)
  • 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).