Commerce & Wallet
Store products, the Play Coin wallet, and subscriptions. All endpoints require authentication.
Money is always integer cents. Play Coins are whole units.
Products
| Method | Path | Purpose |
|---|---|---|
GET | /products | List products |
GET | /products/featured | Featured products |
GET | /products/:id | One product |
POST | /products/:id/buy | Buy with Play Coins |
The store sells profile customisation — avatars, avatar frames, banners, banner frames, profile backgrounds, themes. Buying creates an Order with OrderItem rows, an Inventory row for ownership, and a COIN_SPEND_STORE ledger entry. Equipping an owned item is recorded as a UserAsset.
Wallet
| Method | Path | Purpose |
|---|---|---|
GET | /wallet | Balances and, where implemented, ledger history |
GET | /me/coins | Coin balance |
Wallet carries two balances:
| Field | Unit |
|---|---|
coinBalance | Whole Play Coins |
balance | USD cents (creator redeemable balance) |
Two coin balances exist
User.playcoin_balance duplicates Wallet.coinBalance with nothing keeping them in sync. Wallet is backed by the Ledger and is the one to trust. Check which column a given code path reads before debugging a "wrong balance" report.
Every balance change should have a matching Ledger row carrying a unique idempotencyKey. See Monetization.
Subscriptions
| Method | Path | Purpose |
|---|---|---|
GET | /me/subscription | Your platform subscription status |
GET | /me/vip-subscriptions | Creators you subscribe to |
POST | /vip-subscriptions | Record a creator subscription after purchase |
Two kinds of subscription
| Platform subscription | Creator (VIP) subscription | |
|---|---|---|
| Model | Subscription | CreatorSubscription |
| Endpoint | GET /me/subscription | GET /me/vip-subscriptions |
| Meaning | PlayPalz premium tier | Access to one creator's channel |
| Price | Fixed tiers | The creator's monthlyPrice |
| Source | RevenueCat webhook | RevenueCat webhook plus the client call below |
POST /vip-subscriptions
{ "vipUserId": "clx…", "productId": "vip_9_99" }Returns 201.
This exists to work around a gap in the webhook: RevenueCat tells the server that a VIP product was purchased but not which creator it was for, because the target is a customer attribute the handler does not read. So the mobile client calls this endpoint immediately after a successful purchase.
A purchase can be lost
If the app is backgrounded or killed between the store confirming the purchase and this call completing, the fan has paid and the subscription is not recorded. Resolving vip_user_id from the RevenueCat subscriber attributes inside the webhook would remove the dependency on the client. Partial
What a subscription unlocks
An active CreatorSubscription is what canAccessRoom checks for vip rooms:
const sub = await prisma.creatorSubscription.findFirst({
where: {
fanId: userId,
creatorId: room.channel.userId,
status: "active",
OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
},
});Channel membership alone is not enough.
Buying coins
Coins are bought as a consumable in-app purchase, not through this API. The flow is:
Mobile → RevenueCat SDK → App Store / Play → RevenueCat
→ POST /api/v1/webhooks/revenuecat → creditCoins() → Ledger COIN_TOPUPProduct-id-to-coin-amount mapping lives in COIN_PRODUCT_MAP (apps/api/src/modules/revenuecat/RevenueCatTypes.ts). Adding a coin SKU means editing that map.
Creator payouts
Not exposed on this API. Payouts are calculated and executed by anansi on a monthly cycle, behind its own admin API.
