Pricing & Membership Restructure - April 2026
Status: Planning / Impact Analysis Date: 2026-04-08 Source:
z-2-reqirements/20260405 会员和积分 价格调整 包含IOS第一版本.docx
Table of Contents
- 1. Executive Summary
- 2. Current vs New Pricing (Side-by-Side)
- 3. Impact Analysis by Area
- 4. Detailed File-by-File Change Plan
- 5. Architecture Challenges & Solutions
- 6. Mobile Store Plan
- 7. Migration & Existing User Considerations
- 8. Testing Plan
- 9. Implementation Checklist
- 10. Open Questions for Boss
1. Executive Summary
This change restructures the MysticX pricing across two platforms:
Web: Increase monthly credit grants (Gold 4,000 -> 6,000, Diamond 24,000 -> 30,000), introduce yearly plans with one-time lump-sum credit grants (Gold $89.99/100K credits, Diamond $299.99/1M credits), update lowest-price upsell text.
iOS (new): Launch with Gold-only subscriptions (Weekly $7.99/1,500 credits, Monthly $19.99/6,000 credits, Yearly $89.99/100,000 credits), 3-day free trial on all plans, no credit packs, no Diamond tier.
Google Play / Android: Android will follow the same mobile monetization direction as iOS: Gold-only subscriptions sold through Google Play Billing, with RevenueCat as the shared mobile subscription layer.
Key Risks
- Yearly one-time grant - The current system grants credits on every
invoice.payment_succeededevent. Yearly plans need a single lump-sum of 100K/1M credits instead of monthly recurring. - Credit reclamation on yearly cancel - The requirements doc says "if cancel plan, credits reclaimed" for yearly subscribers. This is a new concept that doesn't exist in the current system.
- iOS platform separation - The iOS product catalog is deliberately different from web. We need platform-aware logic in the subscription flow.
- Weekly subscription - A new billing period that doesn't exist in the current system.
- iOS Apple IAP + RevenueCat integration - iOS billing is now confirmed to use Apple IAP with RevenueCat. The main risk is no longer choosing a provider, but implementing entitlement sync, user identity mapping, trial behavior, and grant logic correctly.
- Google Play Billing + RevenueCat integration - Android is now confirmed to use Google Play Billing with the same Gold-only mobile catalog as iOS. The main risk is correct product mapping, entitlement sync, renewal/cancel/refund handling, and cross-platform account linkage.
2. Current vs New Pricing (Side-by-Side)
2a. Web Subscriptions
| Plan | Current Monthly | NEW Monthly | Current Yearly | NEW Yearly |
|---|---|---|---|---|
| Gold | $19.99, 4,000 SE/mo | $19.99, 6,000 SE/mo | $167.92/yr ($13.99/mo), 4,000 SE/mo | $89.99/yr, 100,000 SE one-time |
| Diamond | $69.99, 24,000 SE/mo | $69.99, 30,000 SE/mo | $587.92/yr ($48.99/mo), 24,000 SE/mo | $299.99/yr, 1,000,000 SE one-time |
Key changes:
- Monthly credit grants increased (Gold +50%, Diamond +25%)
- Yearly prices drastically reduced (Gold: $167.92 -> $89.99 = 46% cheaper; Diamond: $587.92 -> $299.99 = 49% cheaper)
- Yearly credits are now one-time lump-sum, not monthly recurring
- Effective yearly cash discount becomes ~62% for Gold and ~64% for Diamond; if the UI keeps a "75% off" badge for Diamond, treat that as marketing copy instead of a computed discount value
- Default recommended plan: Gold Yearly
- Diamond yearly shows "75% off" badge
2b. Web Credit Packs (UNCHANGED)
| Pack | Credits | Price |
|---|---|---|
| Taster | 250 | $1.99 |
| Mini | 600 | $3.99 |
| Starter | 2,000 | $9.99 |
| Best Value | 4,000 | $14.99 |
No changes to credit pack pricing or amounts.
2c. iOS Subscriptions (NEW)
| Plan | Price | Credits | Notes |
|---|---|---|---|
| Gold Weekly | $7.99 | 1,500 SE | New billing period |
| Gold Monthly | $19.99 | 6,000 SE | Same as web |
| Gold Yearly | $89.99 | 100,000 SE one-time | Same as web |
- All plans include 3-day free trial
- No credit packs on iOS
- No Diamond tier on iOS
- Default recommended: Gold Yearly
- Push notifications for daily credit claiming
2d. Google Play / Android Plan
Android is confirmed to follow the same first-version mobile catalog as iOS:
- Android uses the same mobile subscription catalog as iOS: Gold Weekly, Gold Monthly, Gold Yearly
- Android also does not offer credit packs or Diamond in the first mobile version
- Android uses Google Play Billing inside the app, with RevenueCat as the subscription layer
- Free trial and pricing labels still need separate configuration in Google Play Console even when the commercial design matches iOS
2e. SE Unit Price Comparison (New Structure)
| Product | Price | SE | Unit Price ($/SE) |
|---|---|---|---|
| Taster pack | $1.99 | 250 | $0.00796 |
| Mini pack | $3.99 | 600 | $0.00665 |
| Starter pack | $9.99 | 2,000 | $0.00500 |
| iOS Gold Weekly | $7.99 | 1,500 | $0.00533 |
| Best Value pack | $14.99 | 4,000 | $0.00375 |
| Gold Monthly | $19.99 | 6,000 | $0.00333 |
| Diamond Monthly | $69.99 | 30,000 | $0.00233 |
| Gold Yearly | $89.99 | 100,000 | $0.00090 |
| Diamond Yearly | $299.99 | 1,000,000 | $0.00030 |
The staircase is clean and self-consistent: every higher-commitment product gives a lower unit price.
The new cheapest SE is Diamond Yearly at $0.0003/SE. For context, the current cheapest (displayed in the CreditPurchaseModal upsell) is the Best Value pack with Diamond bonus: $14.99 / 4,600 = ~$0.0033/SE. This is a 91% reduction in the lowest per-SE price.
Note: The requirements doc says the lowest price is $0.0002/SE for Diamond Yearly. But $299.99 / 1,000,000 = $0.0002999, which rounds to $0.0003. The doc was likely using a round number. We should display the computed value via
bestPerCredit.toFixed(4)which yields"0.0003".
Scope note: This unit-price comparison uses the headline credit grant only. It does not include daily rewards, member discounts, or other membership perks.
3. Impact Analysis by Area
3a. Configuration Layer
| File | What Changes | Complexity |
|---|---|---|
config/stripe.ts | Update web yearly pricing; add GOLD_WEEKLY only if weekly is sold through Stripe; otherwise keep weekly in mobile billing config | Medium |
config/stripe-credits.ts | MONTHLY_GRANT_AMOUNTS Gold 4000->6000, Diamond 24000->30000 | Low |
env.ts / billing env | Add STRIPE_PRICE_GOLD_WEEKLY only if weekly is sold via Stripe; otherwise add Apple / Google / RevenueCat product identifiers | Medium |
3b. Backend / Business Logic
| File | What Changes | Complexity |
|---|---|---|
lib/stripe-handlers.ts | Distinguish yearly vs monthly grants (one-time lump sum vs recurring); implement credit reclamation on yearly cancel | HIGH |
lib/auth.tsx | Keep Stripe subscription config web-scoped; iOS subscriptions are handled outside Better Auth Stripe via Apple IAP + RevenueCat | Medium |
| Prisma schema | May need billingInterval field on Subscription model (or detect from Stripe metadata) | Medium |
| Mobile billing integration | Add Apple IAP and Google Play Billing entitlement sync path through RevenueCat | HIGH |
3c. UI Components
| File | What Changes | Complexity |
|---|---|---|
PricingSection.tsx | monthlyCredits values (4000->6000, 24000->30000); yearly pricing display; yearly credit one-time display; default selection to Gold Yearly | Medium |
ComparePlansSection.tsx | Monthly SE values, max monthly potential recalculation | Medium |
CreditPurchaseModal.tsx | bestPerCredit changes (now based on Diamond Yearly); upsell text in 12 locales | Medium |
MembershipContent.tsx | MONTHLY_GRANTS constant (4000->6000, 24000->30000) | Low |
3d. Mobile Store-Specific
| Area | What's Needed | Complexity |
|---|---|---|
| Product catalog config | Mobile store config for iOS + Google Play, or a shared mobile catalog with per-store product IDs | Medium |
| Free trial config | App Store / Google Play trial period setup | Medium |
| Mobile billing validation | RevenueCat webhook / entitlement sync for Apple and Google products, plus store-side console setup | HIGH |
| Push notifications | iOS / Android app feature for daily credit claiming reminder | Out of scope (app team) |
| No credit packs | Mobile UI should hide credit pack purchase flow | Low (app-side) |
3e. Documentation
| File | What Changes |
|---|---|
docs/credit-system.md | Update all pricing tables, add yearly plans section |
docs/credit-system.zh-CN.md | Same, Chinese version |
3f. Tests
| File | What Changes |
|---|---|
__tests__/stripe/credit-grants.test.ts | Update MONTHLY_GRANT_AMOUNTS expected values; add yearly one-time grant tests |
__tests__/stripe/webhook-handlers.test.ts | Update differential credit calculations; add yearly grant tests |
__tests__/stripe/integration-handlers.test.ts | Update expected grant amounts (4000->6000, 24000->30000); add yearly scenarios |
4. Detailed File-by-File Change Plan
4.1 config/stripe-credits.ts
// BEFORE
export const MONTHLY_GRANT_AMOUNTS: Record<string, number> = {
FREE: 0,
GOLD: 4_000,
DIAMOND: 24_000,
};
// AFTER
export const MONTHLY_GRANT_AMOUNTS: Record<string, number> = {
FREE: 0,
GOLD: 6_000,
DIAMOND: 30_000,
};
// NEW: One-time yearly grant amounts (lump sum on subscription start)
export const YEARLY_GRANT_AMOUNTS: Record<string, number> = {
FREE: 0,
GOLD: 100_000,
DIAMOND: 1_000_000,
};4.2 config/stripe.ts
// BEFORE
export const STRIPE_PRICES = {
GOLD_MONTHLY: process.env.STRIPE_PRICE_GOLD_MONTHLY ?? 'price_gold_monthly',
GOLD_YEARLY: process.env.STRIPE_PRICE_GOLD_YEARLY ?? 'price_gold_yearly',
DIAMOND_MONTHLY: process.env.STRIPE_PRICE_DIAMOND_MONTHLY ?? 'price_diamond_monthly',
DIAMOND_YEARLY: process.env.STRIPE_PRICE_DIAMOND_YEARLY ?? 'price_diamond_yearly',
} as const;
// AFTER - add GOLD_WEEKLY only if weekly is sold through Stripe
export const STRIPE_PRICES = {
GOLD_WEEKLY: process.env.STRIPE_PRICE_GOLD_WEEKLY ?? 'price_gold_weekly',
GOLD_MONTHLY: process.env.STRIPE_PRICE_GOLD_MONTHLY ?? 'price_gold_monthly',
GOLD_YEARLY: process.env.STRIPE_PRICE_GOLD_YEARLY ?? 'price_gold_yearly',
DIAMOND_MONTHLY: process.env.STRIPE_PRICE_DIAMOND_MONTHLY ?? 'price_diamond_monthly',
DIAMOND_YEARLY: process.env.STRIPE_PRICE_DIAMOND_YEARLY ?? 'price_diamond_yearly',
} as const;
// BEFORE
export const SUBSCRIPTION_PRICES = {
GOLD: { monthly: 19.99, yearlyPerMonth: 13.99, yearlyTotal: 167.92, discount: 30 },
DIAMOND: { monthly: 69.99, yearlyPerMonth: 48.99, yearlyTotal: 587.92, discount: 30 },
} as const;
// AFTER
export const SUBSCRIPTION_PRICES = {
GOLD: {
weekly: 7.99, // mobile only
monthly: 19.99,
yearlyPerMonth: 7.50, // $89.99 / 12
yearlyTotal: 89.99,
discount: 62, // (1 - 89.99 / (19.99*12)) * 100 ≈ 62%
},
DIAMOND: {
monthly: 69.99,
yearlyPerMonth: 25.00, // $299.99 / 12
yearlyTotal: 299.99,
discount: 75, // per requirements: "75% off"
},
} as const;Important: if Gold Weekly is mobile-store-only via Apple IAP / Google Play Billing, do not put it in STRIPE_PRICES. In that case, keep config/stripe.ts web-focused and store Apple / Google / RevenueCat product IDs in a separate mobile billing config.
4.3 lib/stripe-handlers.ts - Major Changes
This file requires the most significant changes. Currently, grantSubscriptionCredits() uses MONTHLY_GRANT_AMOUNTS[tier] for every invoice. The new logic needs to:
- Detect billing interval (monthly vs yearly) from the Stripe subscription/invoice
- Monthly plans: Continue granting
MONTHLY_GRANT_AMOUNTS[tier]per invoice - Yearly plans: Grant
YEARLY_GRANT_AMOUNTS[tier]as a one-time lump sum on initial payment only - Yearly cancel: Reclaim remaining credits (NEW concept)
- Weekly plans: Grant weekly credit amount per invoice (1,500 for Gold Weekly)
Proposed approach for interval detection:
- Read
subscription.items.data[0].price.recurring.intervalfrom the Stripe subscription object - This returns
'week','month', or'year' - Pass this info through to
grantSubscriptionCredits()
New credit amounts config:
export const WEEKLY_GRANT_AMOUNTS: Record<string, number> = {
FREE: 0,
GOLD: 1_500,
};Yearly credit reclamation (on cancel):
- This is the most complex new feature
- When a yearly subscriber cancels, we need to "reclaim" unearned credits
- Question: How many credits to reclaim? Options:
- A) Reclaim ALL remaining credits (user.credits = 0)
- B) Reclaim prorated amount (e.g., if 3 months used out of 12, reclaim 9/12 of grant)
- C) Reclaim only the original grant amount minus what was "earned" (requires tracking)
- Question: What if the user has already spent credits below the reclaim amount?
- Question: Does "credit reclaimed" mean the balance goes negative, or capped at 0?
4.4 lib/auth.tsx - Better Auth Stripe Plugin
Better Auth's Stripe plugin should stay scoped to web Stripe plans unless the business decides the weekly plan is also sold on the web.
// Web Stripe plans only
plans: [
{
name: 'gold',
priceId: STRIPE_PRICES.GOLD_MONTHLY,
annualDiscountPriceId: STRIPE_PRICES.GOLD_YEARLY,
},
{
name: 'diamond',
priceId: STRIPE_PRICES.DIAMOND_MONTHLY,
annualDiscountPriceId: STRIPE_PRICES.DIAMOND_YEARLY,
},
]Gold Weekly is confirmed to be an iOS Apple IAP / RevenueCat product, so it should not be modeled here as a Stripe plan.
Question: Does the 3-day free trial apply to web plans too, or only iOS? The requirements doc only mentions it for iOS.
Note: Better Auth Stripe
freeTrialverification is only relevant for web Stripe plans. iOS trial configuration belongs in App Store Connect and RevenueCat.
4.5 PricingSection.tsx
- monthlyCredits: 4_000, // GOLD
+ monthlyCredits: 6_000, // GOLD
- monthlyCredits: 24_000, // DIAMOND
+ monthlyCredits: 30_000, // DIAMOND
// Yearly credit display: Need to show "100,000 SE Instantly!" and "1,000,000 SE Instantly!"
// Default selection: Gold Yearly instead of current default4.6 ComparePlansSection.tsx
// Monthly SE Grant row
- gold: '4,000', diamond: '24,000'
+ gold: '6,000', diamond: '30,000'
// Max Monthly Potential (Gold: 6000 grant + 80*30 daily = 8,400; Diamond: 30000 + 100*30 = 33,000)
- gold: '~6,400', diamond: '~27,000'
+ gold: '~8,400', diamond: '~33,000'
// Need to add yearly pricing rows4.7 CreditPurchaseModal.tsx
The bestPerCredit calculation currently uses:
const bestPerCredit = Math.min(...CREDIT_PACKS.map((p) => p.priceUsd / (p.credits + calculateBonusCredits(p.credits, 'DIAMOND'))));This only considers credit packs. With the new pricing, the cheapest SE is Diamond Yearly ($299.99 / 1,000,000 = $0.0003). The upsell text should reference this instead.
Proposed change: Hardcode or calculate the yearly Diamond per-SE price and use it in the upsell copy. The upsell text exists in 12 locales inline (lines ~468-534) and each needs updating.
4.8 MembershipContent.tsx
- const MONTHLY_GRANTS: Record<string, number> = { FREE: 0, GOLD: 4_000, DIAMOND: 24_000 };
+ const MONTHLY_GRANTS: Record<string, number> = { FREE: 0, GOLD: 6_000, DIAMOND: 30_000 };Note: This is a duplicate of
MONTHLY_GRANT_AMOUNTSinconfig/stripe-credits.ts. During implementation, consider importing from the config instead.
5. Architecture Challenges & Solutions
Challenge 1: Yearly One-Time Grant vs Monthly Recurring Grant
Problem: The current grantSubscriptionCredits() function uses MONTHLY_GRANT_AMOUNTS[tier] for every invoice.payment_succeeded event. Stripe yearly subscriptions only fire one invoice per year, so technically this "works" - but the grant amount would be wrong (it'd grant 6,000 instead of 100,000 for Gold Yearly).
Solution:
// In grantSubscriptionCredits(), detect the billing interval:
export async function grantSubscriptionCredits(
deps: Pick<TStripeHandlerDeps, 'prisma' | 'addCredits'>,
userId: string,
plan: string,
invoiceId: string,
billingInterval: 'week' | 'month' | 'year' = 'month' // NEW parameter
) {
const tier = tierFromPlan(plan);
let amount: number;
switch (billingInterval) {
case 'year':
amount = YEARLY_GRANT_AMOUNTS[tier];
break;
case 'week':
amount = WEEKLY_GRANT_AMOUNTS[tier] ?? 0;
break;
default:
amount = MONTHLY_GRANT_AMOUNTS[tier];
}
// ... rest of idempotent grant logic
}We need to extract the billing interval from the Stripe subscription object in handleStripeEvent() and handleSubscriptionComplete().
Challenge 2: Credit Reclamation on Yearly Cancel
Problem: Requirements say "if cancel plan, credits reclaimed" for yearly subscribers. This is a completely new concept. The current handleSubscriptionCancel() only sets tier: 'FREE'.
Proposed Solution:
- When a yearly subscription is canceled (not at period end, but immediately):
- Calculate the original grant amount
- Deduct it from the user's balance (capped at 0 - never go negative)
- Create a
SUBSCRIPTION_RECLAIMcredit transaction for audit trail
- When canceled at period end: No reclamation needed (user paid for the full year)
Open question: Does "cancel" here mean immediate cancellation (refund scenario), or cancel-at-period-end? Stripe supports both. The typical flow is:
- User clicks "Cancel" -> Stripe sets
cancel_at_period_end = true-> subscription stays active until period ends -> then auto-cancels - In this case, no reclamation is needed since they paid for the full year
If we're talking about refund scenarios (admin-initiated or dispute), then reclamation makes sense.
Challenge 3: Weekly Subscription (New Billing Period)
Problem: The current system only has monthly and yearly plans. A weekly plan is new.
Solution:
- If weekly is sold through Stripe, create a new Stripe price with
recurring.interval = 'week' - If weekly is iOS-only, model it in Apple IAP / RevenueCat instead of Stripe
- Add
GOLD_WEEKLYtoSTRIPE_PRICESonly for the Stripe path - Add
gold-weeklyto Better Auth only for the Stripe path - Add
WEEKLY_GRANT_AMOUNTSconfig - The
PLAN_TO_TIERmapping needs to handle'gold-weekly' -> 'GOLD'only on the Stripe path; Apple IAP should use a product mapping table instead - Weekly grants (1,500 SE) are deposited on each billing cycle, same as monthly
Challenge 4: iOS Platform Separation
Problem: iOS has a different product catalog (no credit packs, no Diamond, has Weekly plan). The web must not show the weekly plan, and iOS must not show Diamond or credit packs.
Solution options:
- Option A: Platform parameter in API calls. The mobile app sends
platform: 'ios'and the server returns filtered plan options. - Option B: All filtering is client-side. The iOS app simply doesn't render Diamond or credit pack UI. The backend accepts any valid subscription regardless of platform.
- Recommended: Option B. Keep it simple. The iOS app controls its own UI. The backend doesn't need to know which platform the user subscribed from.
Note: If a user subscribes to Gold Weekly via iOS, then visits the web, the web should still show their active Gold subscription correctly. The tier is
GOLDregardless of billing interval.
Challenge 5: Free Trial (3-Day)
Problem: iOS plans should offer a 3-day free trial.
Solution:
- If the product is sold through Stripe, configure
trial_period_days: 3in Stripe or at subscription creation - If the product is sold through Apple IAP, configure the introductory offer / free trial in App Store Connect or RevenueCat
- Better Auth's Stripe plugin may support a
freeTrialconfig on web Stripe plans - During trial, user should still get their credit grant (or only after trial ends?)
Question: Should credits be granted during the free trial period, or only after the first payment?
Challenge 6: Upgrade Differential Credits
Problem: grantUpgradeDifferentialCredits() calculates MONTHLY_GRANT_AMOUNTS[newTier] - MONTHLY_GRANT_AMOUNTS[oldTier]. With different billing intervals, this gets complicated.
Scenarios:
| From | To | Differential | Notes |
|---|---|---|---|
| Gold Monthly (6K/mo) | Diamond Monthly (30K/mo) | 24,000 | Works as before |
| Gold Weekly (1.5K/wk) | Gold Monthly (6K/mo) | 0 | Same tier, no differential. Correct. |
| Gold Yearly (100K upfront) | Diamond Yearly (1M upfront) | 900,000? | Extremely large. Needs decision. |
| Gold Monthly (6K/mo) | Gold Yearly (100K one-time) | ??? | Interval change within same tier. |
The last two scenarios need explicit business rules. The current differential logic only compares tiers, not intervals.
Question: What happens if a Gold Yearly user (who received 100K upfront) upgrades to Diamond Yearly? Do they get 900K more credits? Or only a prorated difference based on remaining subscription time?
Challenge 7: iOS Apple IAP + RevenueCat Implementation
Problem: Apple requires all digital goods purchased within iOS apps to use Apple's In-App Purchase (IAP) system. MysticX credits are digital goods. This means iOS subscriptions cannot go through Stripe directly — they must use StoreKit / Apple IAP.
The current architecture assumes all subscriptions flow through Stripe webhooks. iOS subscriptions would follow a completely different path:
- User subscribes via Apple IAP (StoreKit 2)
- Apple processes payment and takes 30% cut
- iOS app sends the transaction/receipt to our backend
- Backend validates the receipt with Apple's App Store Server API
- Backend grants subscription tier and credits
This is architecturally very different from the Stripe webhook flow.
Confirmed approach:
- iOS subscriptions use Apple IAP
- RevenueCat is the subscription abstraction and entitlement sync layer
- The backend should trust RevenueCat webhook / entitlement events as the primary integration point, rather than building a direct StoreKit receipt-validation path first
Implementation requirements:
- Create iOS products in App Store Connect
- Mirror those products in RevenueCat and map them to internal tiers / billing intervals
- Choose a stable
appUserIdstrategy so RevenueCat customers can be matched to MysticX users reliably - Process RevenueCat webhook events to activate, renew, cancel, refund, and expire entitlements
- Reuse the same internal credit-grant engine after the entitlement event is validated
- Support restore purchases correctly when a user signs back into the app on a new device
Recommended architecture if we want the cleanest rollout:
- Web: Keep Stripe + Better Auth for Gold Monthly, Gold Yearly, Diamond Monthly, Diamond Yearly
- Mobile stores: Use RevenueCat for Apple IAP and Google Play Billing for Gold Weekly, Gold Monthly, Gold Yearly
- Backend: Unify everything into one entitlement model with
provider,productId,billingInterval,tier, and grant strategy
Challenge 8: Google Play Billing vs Web Stripe
Problem: Android digital subscriptions sold inside the app must go through Google Play Billing, not Stripe checkout embedded in the app. This creates the same category of store-billing split as iOS, but with different console setup, validation APIs, and webhook/event handling.
Confirmed approach:
- Use the same mobile entitlement model for both iOS and Android
- Use RevenueCat so Apple and Google purchases arrive through one normalized integration path
- Keep the mobile product catalog aligned across iOS and Android
- Validate free trial, introductory offer, renewal, cancellation, refund, and entitlement expiration on both stores separately
6. Mobile Store Plan
6.1 Backend Plan by Billing Path
Confirmed iOS path: Apple IAP + RevenueCat
- Keep web subscriptions on Stripe + Better Auth
- Create iOS products in App Store Connect and Android products in Google Play Console, not Stripe
- Map Apple / Google / RevenueCat products to internal tiers and billing intervals
- Add server-side entitlement sync / webhook handling for mobile purchases
- Reuse the same credit-grant engine after entitlement validation
- Bind RevenueCat customer identity to MysticX user identity so purchases restore to the correct account
6.2 What's Mobile App Only (Not Our Scope)
- Push notifications for daily credit claiming
- UI: Only show Gold plans (Weekly/Monthly/Yearly), no Diamond, no credit packs
- Payment wall design: "Founder's Pass" badge on yearly plan
- Credit arrival animation: "Star explosion" effect when 100K credits arrive
- Store-specific pricing presentation: App Store / Play Store pricing labels and trials need store console configuration and app-side display review
6.3 App Store Pricing & Apple IAP Consideration
Apple requires In-App Purchase (IAP) for digital goods in iOS apps. This means iOS subscriptions do NOT go through Stripe — they go through Apple's payment system with RevenueCat as the subscription layer. See Challenge 7 for architecture implications.
Apple takes 30% (15% for Small Business Program). Actual revenue per iOS subscription:
| Plan | Price | Apple's Cut (30%) | Net Revenue |
|---|---|---|---|
| Gold Weekly | $7.99 | $2.40 | $5.59 |
| Gold Monthly | $19.99 | $6.00 | $13.99 |
| Gold Yearly | $89.99 | $27.00 | $62.99 |
Compare to web (Stripe takes ~2.9% + $0.30):
| Plan | Price | Stripe Fee | Net Revenue |
|---|---|---|---|
| Gold Monthly | $19.99 | ~$0.88 | ~$19.11 |
| Gold Yearly | $89.99 | ~$2.91 | ~$87.08 |
6.4 Google Play Billing Consideration
Google Play Store requires Google Play Billing for in-app digital subscriptions on Android. The operational implications are similar to iOS, but the setup differs:
- Products and trials are configured in Google Play Console
- Validation uses Google Play purchase tokens / Developer API, or RevenueCat as the abstraction layer
- Subscription lifecycle events can be consumed directly or via RevenueCat and Play RTDN integration
- Finance should verify the actual Google Play fee rate in the current Play Console program settings before using it in margin models
6.5 Mobile vs Web Subscription Interop
If a user subscribes via iOS or Android and later visits the web:
- Their
tierisGOLD(same as web Gold) - The web should show their active subscription status correctly
- The web should NOT show "upgrade to weekly" or other iOS-only options
- If they want Diamond, they must subscribe via web
If a user subscribes to Diamond on the web and later logs into iOS or Android:
- The app should still recognize them as
DIAMONDand unlock Diamond benefits - The app should show Diamond as the current active plan, labeled as managed outside the App Store / Play Store
- The app should NOT offer App Store or Google Play purchase actions for Diamond, because Diamond is not sold in the first mobile catalog
- The app should avoid showing Gold free-trial or Gold upsell purchase cards as if the user were unsubscribed
Apple anti-steering warning (App Store Review Guideline 3.1.3(b)): Apple's guidelines allow apps to surface content the user bought elsewhere, but explicitly forbid apps from using any link, button, or copy to direct users away from IAP to make a purchase. This means:
- Do NOT add an external link or CTA that routes to the web membership page. Even a "Manage on website" button is a violation if that page contains any pricing or purchase UI.
- Instead, show a read-only informational label only — e.g. "Your Diamond subscription is active (managed outside the App Store)" — with no tappable link attached.
- If a URL is genuinely unavoidable, it must deep-link to a dedicated status-only page where all pricing and purchase UI is hidden for users arriving from iOS.
Android / Google Play: Google Play's policies on external links are less restrictive than Apple's. A link to a subscription management page (not a new purchase flow) is generally permitted. However, the same read-only label approach is the safest default for the first mobile release. An Android-specific "Manage on website" link can be considered after the app is live and stable.
To make this work cleanly, the backend should store more than tier alone. It should also retain provider (stripe, apple, revenuecat), product identifier, and billing interval so the management UI can render the correct state and allowed upgrade paths.
Recommended rule: entitlement beats storefront. The mobile app can sell only Gold while still honoring a web-originated Diamond entitlement in a read-only state. The mobile app must never expose any UI that Apple could interpret as directing the user away from IAP for a purchase decision.
7. Migration & Existing User Considerations
7.1 Existing Monthly Subscribers
Gold Monthly subscribers currently get 4,000 SE/month. After the change, they'll get 6,000 SE/month. This is a free upgrade - no action needed for them. The change takes effect on their next billing cycle when invoice.payment_succeeded fires with the new MONTHLY_GRANT_AMOUNTS.
Diamond Monthly subscribers currently get 24,000 SE/month. After the change, they'll get 30,000 SE/month. Same - free upgrade.
Note: We should probably send a notification to existing subscribers: "Great news! Your monthly SE grant has been increased!"
7.2 Existing Yearly Subscribers
Current yearly subscribers are on the OLD pricing ($167.92/yr for Gold, $587.92/yr for Diamond). Stripe yearly subscriptions only fire invoice.payment_succeeded once per year at renewal.
Critical edge case: After deploying the new code, the grant logic will use billingInterval to determine amounts. When an existing yearly subscriber renews:
- Stripe charges the old price ($167.92 / $587.92) because the price ID on their subscription hasn't changed
- But the new code would grant the new yearly lump sum (100K / 1M credits)
- This means existing yearly subscribers who paid MORE ($167.92 vs $89.99) would get the same 100K credits as new subscribers paying less
This is actually favorable to existing users (they get a massive credit increase for free), but may not be the intended behavior.
Question: Do we need to create NEW Stripe price IDs for the new yearly pricing ($89.99/$299.99), or update the existing ones? If we create new ones, existing subscribers keep their old price ID and we need a mapping to determine their grant amount.
Recommendation: Create new Stripe prices. For existing yearly subscribers, two options:
- (A) Let them keep old prices and still receive old-style monthly grants (requires keeping the old grant path in code)
- (B) Auto-migrate them to new prices at their next renewal (requires Stripe subscription update)
- (C) Let them keep old prices but grant new yearly lump sum on renewal (generous but simple)
7.3 Notification
Requirements mention: "Send a membership benefits upgrade notification."
We should create a one-time notification for all active subscribers informing them of the increased benefits.
8. Testing Plan
8.1 Unit Tests to Update
| Test File | Changes Needed |
|---|---|
__tests__/stripe/credit-grants.test.ts | Update MONTHLY_GRANT_AMOUNTS assertions (4000->6000, 24000->30000); add YEARLY_GRANT_AMOUNTS tests; add WEEKLY_GRANT_AMOUNTS tests |
__tests__/stripe/webhook-handlers.test.ts | Update differential calculations; add yearly one-time grant scenarios; add weekly grant scenarios |
__tests__/stripe/integration-handlers.test.ts | Update "grants 4000 credits for gold plan" -> 6000; update "grants 24000 credits for diamond plan" -> 30000; add yearly grant test |
8.2 New Test Scenarios Needed
- Yearly subscription creation -> One-time lump sum grant of correct amount
- Yearly subscription renewal -> Confirm the annual lump sum repeats according to the active product / price mapping
- Weekly subscription invoice -> Correct weekly grant amount
- Yearly cancel with credit reclamation -> Credits deducted correctly
- Yearly cancel when user has fewer credits than reclaim amount -> Capped at 0
- Upgrade from Gold Monthly to Gold Yearly mid-cycle
- Upgrade from Gold Yearly to Diamond Yearly -> Differential calculation
- iOS free trial start -> No credits granted (or granted?)
- iOS free trial conversion to paid -> Credits granted
- Google Play purchase start -> Entitlement and credits granted correctly
- Google Play renewal / cancellation / refund -> Entitlement and grant state stay correct
8.3 Manual Testing Checklist
- [ ] Web: Gold Monthly shows $19.99, 6,000 SE
- [ ] Web: Diamond Monthly shows $69.99, 30,000 SE
- [ ] Web: Gold Yearly shows $89.99, 100,000 SE (one-time)
- [ ] Web: Diamond Yearly shows $299.99, 1,000,000 SE (one-time)
- [ ] Web: Diamond Yearly shows "75% off" badge
- [ ] Web: Default selection is Gold Yearly
- [ ] Web: CreditPurchaseModal upsell text shows updated lowest price
- [ ] Web: ComparePlansSection shows updated SE values
- [ ] Web: MembershipContent shows correct grant amounts
- [ ] Web: Subscribing to Gold Monthly grants 6,000 SE
- [ ] Web: Subscribing to Gold Yearly grants 100,000 SE one-time
- [ ] Web: Subscribing to Diamond Yearly grants 1,000,000 SE one-time
- [ ] Web: Canceling yearly subscription reclaims credits (if implemented)
- [ ] iOS: Only Gold plans visible (Weekly/Monthly/Yearly)
- [ ] iOS: No credit packs visible
- [ ] iOS: 3-day free trial works
- [ ] iOS: Weekly subscription grants 1,500 SE per cycle
- [ ] Android / Google Play: Same mobile catalog is shown if product confirms parity
- [ ] Android / Google Play: Trial, renewal, cancellation, and entitlement sync work correctly
9. Implementation Checklist
Phase 1: Configuration & Backend (Do First)
- [ ] Create new Stripe prices for web yearly products ($89.99 Gold Yearly, $299.99 Diamond Yearly)
- [ ] Create
GOLD_WEEKLYin Stripe only if weekly is also sold via Stripe - [ ] Add
STRIPE_PRICE_GOLD_WEEKLYonly if needed; otherwise add Apple / Google / RevenueCat product identifiers to env and config - [ ] Update
config/stripe-credits.ts- monthly grants (6000/30000), add yearly/weekly grants - [ ] Update
config/stripe.ts- prices, add weekly, update yearly amounts - [ ] Update
lib/stripe-handlers.ts- interval-aware grants, yearly lump sum logic - [ ] Update
lib/auth.tsx- keep Stripe plans web-scoped unless weekly is sold via Stripe - [ ] Update tier / product mapping to support provider-specific products
- [ ] Add iOS RevenueCat entitlement sync / webhook integration
- [ ] Define RevenueCat
appUserIdmapping and restore-purchase behavior - [ ] Run
npx prisma generateif schema changes
Phase 2: UI Updates
- [ ] Update
PricingSection.tsx- credit values, yearly display, default selection - [ ] Update
ComparePlansSection.tsx- monthly SE values, max potential - [ ] Update
CreditPurchaseModal.tsx- upsell text (12 locales), bestPerCredit calculation - [ ] Update
MembershipContent.tsx- MONTHLY_GRANTS values
Phase 3: Tests
- [ ] Update
__tests__/stripe/credit-grants.test.ts - [ ] Update
__tests__/stripe/webhook-handlers.test.ts - [ ] Update
__tests__/stripe/integration-handlers.test.ts - [ ] Add new test scenarios for yearly/weekly/trial
Phase 4: Documentation
- [ ] Update
docs/credit-system.md - [ ] Update
docs/credit-system.zh-CN.md
Phase 5: iOS Support
- [ ] Set up RevenueCat products, entitlements, and server webhooks for iOS
- [ ] Define the iOS
appUserId/ account-linking strategy in RevenueCat - [ ] Verify restore purchases behavior across reinstall / sign-in changes
- [ ] Coordinate with iOS app team on product catalog
- [ ] Set up App Store Connect in-app purchases (Gold Weekly/Monthly/Yearly)
- [ ] Configure free trial in App Store Connect
Phase 6: Google Play Support
- [ ] Set up Google Play Console subscriptions (Gold Weekly/Monthly/Yearly)
- [ ] Configure free trial / introductory offers in Google Play Console
- [ ] Connect Google Play products and entitlement mapping in RevenueCat
- [ ] Configure Play test users / internal testing and verify cross-platform entitlement sync
10. Open Questions for Boss
Critical (Need answers before implementation)
Yearly credit reclamation - The doc says "if cancel plan, credits reclaimed." Does this mean:
- (a) When user clicks "Cancel" and subscription ends at period end -> No reclamation (they paid for the full year)?
- (b) When user gets a refund (dispute/admin) -> Reclaim credits?
- (c) Immediate cancellation mid-year -> Reclaim prorated credits?
- What if the user has already spent most of the credits? Do we allow negative balance, or cap at 0?
Free trial credits - During the 3-day free trial on iOS, should users receive their credit grant (1,500/6,000/100,000)? Or only after trial converts to paid?
- If credits are granted during trial and user cancels before trial ends, do we reclaim?
Yearly upgrade differential - If a Gold Yearly user (received 100K credits) upgrades to Diamond Yearly ($299.99), should they receive 900K additional credits? That's a very large differential.
Existing yearly subscribers - Do we create new Stripe Price IDs for the new yearly pricing, keeping existing subscribers on old prices? Or update the existing prices (affecting all renewals)?
Weekly plan on web? - The doc mentions the weekly plan for iOS only. The web membership page section says "Weekly, Monthly, Yearly toggle (keep current for now)." Should the web show a weekly option, or is it iOS-exclusive?
"75% off" calculation for Diamond Yearly - $299.99 vs $69.99*12 = $839.88. Actual discount: 64% off. The doc says "Diamond shows 75% off yearly." Should we display 75% as stated, or the mathematically accurate ~64%?
Yearly membership perks - Does the upfront yearly credit grant come in addition to the normal membership perks already tied to tier, such as daily credits, discounts, and premium features? The docs currently assume yes.
RevenueCat identity strategy - Should RevenueCat
appUserIdbe bound 1:1 to our user ID, and how should we handle anonymous usage, login changes, and restore-purchase merging?
Nice To Have (Can decide later)
Yearly plan display format - Should the pricing toggle stay as "Monthly / Yearly" or become a different UI? The doc says "keep current Monthly/Yearly toggle for now."
"Get 100,000 Energy Instantly!" copy - The doc suggests this text for yearly plans. Should this be shown on the pricing card, the checkout page, or both?
Push notification text - The doc suggests: "Allen, your daily 80 energy is ready to align with the stars." Should we use the user's actual name, and adjust the number (80/100) based on their tier? (mobile app team question)
Credit arrival animation for yearly - "Star explosion effect" when 100K/1M credits arrive. Is this needed on web too, or mobile only?
"Founder's Pass" badge - Is this just for the mobile paywall, or should it appear on web pricing too?
Membership upgrade notification - Should we send an email, in-app notification, or both to existing subscribers about the increased benefits?
Appendix A: Changes That Do NOT Need to Happen
These areas remain unchanged:
- Credit pack prices and amounts (Taster/Mini/Starter/Best Value)
- Daily credit amounts (Free: 50, Gold: 80, Diamond: 100)
- Registration bonus (300 SE)
- Tier bonuses on credit packs (Gold +10%, Diamond +15%)
- Credit costs (readings: 200, follow-ups: 50/100/200/400, etc.)
- Referral rewards
- Card skin / reader discounts (Gold 40%, Diamond 60%)
Appendix B: Billing Platform Setup Tasks
Before code deployment, these need to be created in the relevant billing platforms:
Stripe (Web)
- New Price: Gold Yearly - $89.99/year, recurring (replaces old $167.92/year price)
- New Price: Diamond Yearly - $299.99/year, recurring (replaces old $587.92/year price)
- Optional: Gold Weekly - $7.99/week only if weekly is sold through Stripe
- Update env vars: Add new Stripe price IDs to
.env.production,.env.development
App Store Connect (iOS)
- Create Gold Weekly / Monthly / Yearly subscriptions
- Configure the 3-day free trial or introductory offer
- Connect products to RevenueCat
- Configure RevenueCat webhook / entitlement mapping and customer identity strategy
Google Play Console (Android)
- Create Gold Weekly / Monthly / Yearly subscriptions
- Configure free trial / introductory offers for Play subscriptions
- Connect products to RevenueCat
- Configure internal testing and test accounts