Zaptam Billing & Wallet System
Deep dive into membership tiers, wallet functionality, transactions, and payouts.
Business Model
Zaptam uses a gender-differentiated monetization model:
┌────────────────────────────────────────────────────────────┐
│ ZAPTAM ECONOMY │
├────────────────────────────────────────────────────────────┤
│ │
│ MALE USERS (Spenders) FEMALE USERS (Earners) │
│ ───────────────────── ────────────────────── │
│ │
│ • Pay membership tiers • Free access │
│ • Purchase credits • Earn from engagement │
│ • Pay for boosts • Request withdrawals │
│ │
│ $49 / $149 / $499 $0 entry │
│ per month │
│ │
└────────────────────────────────────────────────────────────┘Membership Tiers
File: Landing page src/pages/Membership.tsx
Standard Tier - $49/month
typescript
{
name: 'Standard',
price: 49,
period: 'month',
description: 'Enter the network with full access to verified members.',
features: [
'Worth verification included',
'Browse verified profiles',
'Express interest unlimited',
'Full messaging access',
'Basic privacy controls',
'Standard support',
],
}Premium Tier - $149/month
typescript
{
name: 'Premium',
price: 149,
period: 'month',
description: 'Signal your seriousness with priority placement and verification badge.',
features: [
'Everything in Standard',
'Worth verification badge',
'Priority in discovery',
'See who viewed your profile',
'Advanced privacy controls',
'Read receipts control',
'Priority support',
],
badge: 'Most Popular',
}Elite Tier - $499/month
typescript
{
name: 'Elite',
price: 499,
period: 'month',
description: 'The full experience with personal curation and exclusive perks.',
features: [
'Everything in Premium',
'Top discovery placement',
'Personal curator access',
'Exclusive member events',
'Profile boost monthly',
'Concierge support 24/7',
'Priority match suggestions',
],
}Women's Access - $0
typescript
{
name: 'Women Join Free',
price: 0,
description: 'Full access after passing identity verification and acceptance review.',
features: [
'Identity verification required',
'Full platform access',
'Unlimited browsing',
'Unlimited messaging',
'All privacy features',
'Priority support',
],
}Tier Comparison
| Feature | Standard | Premium | Elite |
|---|---|---|---|
| Worth Verification | ✓ | ✓ | ✓ |
| Browse Profiles | ✓ | ✓ | ✓ |
| Unlimited Messaging | ✓ | ✓ | ✓ |
| Worth Badge | — | ✓ | ✓ |
| Priority Discovery | — | ✓ | ✓ |
| Profile Views | — | ✓ | ✓ |
| Personal Curator | — | — | ✓ |
| Exclusive Events | — | — | ✓ |
| 24/7 Concierge | — | — | ✓ |
Wallet Service
File: src/services/wallet.service.ts
Balance Calculation
typescript
export async function getWalletBalance(userId: string) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { gender: true },
});
// Get all completed transactions
const transactions = await prisma.walletTransaction.findMany({
where: {
userId,
status: 'COMPLETED',
},
});
// Calculate running balance
let balance = new Decimal(0);
for (const tx of transactions) {
if (tx.type === 'EARNING' || tx.type === 'CREDIT_PURCHASE') {
balance = balance.plus(tx.amount); // Credits IN
} else if (tx.type === 'WITHDRAWAL' || tx.type === 'BOOST' || tx.type === 'MEMBERSHIP') {
balance = balance.minus(tx.amount); // Credits OUT
}
}
return {
balance: balance.toNumber(),
currency: 'USD',
userType: user.gender === 'FEMALE' ? 'earner' : 'spender',
};
}Balance Formula:
Balance = Σ(Credits In) - Σ(Credits Out)
Credits In:
- EARNING (female engagement rewards)
- CREDIT_PURCHASE (male credit buys)
Credits Out:
- WITHDRAWAL (female payouts)
- BOOST (profile boost purchases)
- MEMBERSHIP (subscription payments)Transaction Types
prisma
enum TransactionType {
MEMBERSHIP // Monthly subscription payment
CREDIT_PURCHASE // Buying credits (male)
EARNING // Platform earnings (female)
WITHDRAWAL // Cashing out (female)
BOOST // Profile boost purchase
}Transaction Flow
Male Users:
Credit Purchase → CREDIT_PURCHASE (Completed)
↓
Balance +$X
↓
Use credits → BOOST/MEMBERSHIP (Completed)
↓
Balance -$XFemale Users:
Platform engagement → EARNING (Completed)
↓
Balance +$X
↓
Request payout → WITHDRAWAL (Pending)
↓
Admin approves → WITHDRAWAL (Completed)
↓
Balance -$XTransaction Statuses
prisma
enum TransactionStatus {
PENDING // Processing/awaiting action
COMPLETED // Successfully processed
FAILED // Transaction failed
REFUNDED // Reversed/refunded
}Status Transitions
┌─────────┐
│ PENDING │
└────┬────┘
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
┌───────┐ ┌─────────┐ ┌────────┐
│ FAILED│ │COMPLETED│ │REFUNDED│
└───────┘ └─────────┘ └────────┘Credit Purchases (Male)
typescript
export async function purchaseCredits(
userId: string,
amount: number,
paymentReference: string
): Promise<void> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { gender: true },
});
if (!user) {
throw new NotFoundError('User not found');
}
// Gender check
if (user.gender !== 'MALE') {
throw new ForbiddenError('Only male users can purchase credits');
}
await prisma.walletTransaction.create({
data: {
userId,
type: 'CREDIT_PURCHASE',
amount: new Decimal(amount),
status: 'COMPLETED', // In production: PENDING until payment confirmed
reference: paymentReference,
},
});
}API Endpoint:
POST /api/wallet/purchase
{
"amount": 100,
"paymentReference": "stripe_pi_123456"
}Membership Purchases
typescript
export async function purchaseMembership(
userId: string,
tierId: string,
paymentReference: string
): Promise<void> {
const tier = await prisma.membershipTier.findUnique({
where: { id: tierId, isActive: true },
});
if (!tier) {
throw new NotFoundError('Membership tier not found');
}
await prisma.walletTransaction.create({
data: {
userId,
type: 'MEMBERSHIP',
amount: tier.price,
status: 'COMPLETED', // In production: PENDING until payment confirmed
reference: paymentReference,
},
});
}Withdrawals (Female)
Request Withdrawal
typescript
export async function requestWithdrawal(
userId: string,
amount: number,
payoutDetails: string
): Promise<void> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { gender: true },
});
if (!user) {
throw new NotFoundError('User not found');
}
// Gender check
if (user.gender !== 'FEMALE') {
throw new ForbiddenError('Only female users can request withdrawals');
}
// Balance check
const { balance } = await getWalletBalance(userId);
if (balance < amount) {
throw new BadRequestError('Insufficient balance');
}
// Minimum withdrawal
if (amount < 10) {
throw new BadRequestError('Minimum withdrawal amount is $10');
}
await prisma.walletTransaction.create({
data: {
userId,
type: 'WITHDRAWAL',
amount: new Decimal(amount),
status: 'PENDING', // Requires admin approval
reference: payoutDetails, // Payment method info
},
});
}API Endpoint:
POST /api/wallet/withdraw
{
"amount": 50,
"payoutDetails": "PayPal: user@email.com"
}Process Withdrawal (Admin)
typescript
export async function processWithdrawal(
transactionId: string,
approved: boolean,
adminId: string
): Promise<void> {
const transaction = await prisma.walletTransaction.findUnique({
where: { id: transactionId },
});
if (!transaction) {
throw new NotFoundError('Transaction not found');
}
if (transaction.type !== 'WITHDRAWAL' || transaction.status !== 'PENDING') {
throw new BadRequestError('Invalid transaction for processing');
}
await prisma.walletTransaction.update({
where: { id: transactionId },
data: {
status: approved ? 'COMPLETED' : 'FAILED',
reference: `${transaction.reference}|processed_by:${adminId}`,
},
});
}Admin Endpoint:
PATCH /api/admin/payouts/:id
{
"approved": true
}Transaction History
typescript
export async function getTransactionHistory(
userId: string,
page: number,
limit: number
) {
const skip = (page - 1) * limit;
const [transactions, total] = await Promise.all([
prisma.walletTransaction.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
skip,
take: limit,
}),
prisma.walletTransaction.count({ where: { userId } }),
]);
return {
items: transactions,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}API Response:
json
{
"success": true,
"data": {
"items": [
{
"id": "tx-uuid",
"type": "EARNING",
"amount": 25.00,
"currency": "USD",
"status": "COMPLETED",
"reference": "match_bonus_2024-01",
"createdAt": "2024-01-15T10:30:00Z"
},
{
"id": "tx-uuid-2",
"type": "WITHDRAWAL",
"amount": 100.00,
"currency": "USD",
"status": "PENDING",
"reference": "PayPal: user@email.com",
"createdAt": "2024-01-10T14:20:00Z"
}
],
"total": 15,
"page": 1,
"limit": 20,
"totalPages": 1
}
}Database Schema
WalletTransaction Model
prisma
model WalletTransaction {
id String @id @default(uuid()) @db.Uuid
userId String @map("user_id") @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type TransactionType
amount Decimal @db.Decimal(10, 2)
currency String @default("USD")
status TransactionStatus @default(PENDING)
reference String?
createdAt DateTime @default(now()) @map("created_at")
@@index([userId])
@@map("wallet_transactions")
}MembershipTier Model
prisma
model MembershipTier {
id String @id @default(uuid()) @db.Uuid
name String
price Decimal @db.Decimal(10, 2)
currency String @default("USD")
duration Int // Days
features Json // Array of feature strings
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
@@map("membership_tiers")
}Admin Dashboard
Transaction Overview
typescript
// GET /api/admin/transactions
const transactions = await prisma.walletTransaction.findMany({
include: {
user: {
select: { id: true, alias: true, phoneNumber: true },
},
},
orderBy: { createdAt: 'desc' },
skip,
take: limit,
});Pending Payouts
typescript
// GET /api/admin/payouts?status=PENDING
const payouts = await prisma.walletTransaction.findMany({
where: {
type: 'WITHDRAWAL',
status: 'PENDING',
},
include: {
user: {
select: { id: true, alias: true, phoneNumber: true, email: true },
},
},
orderBy: { createdAt: 'desc' },
});UI Components
Balance Card
typescript
// app/src/components/wallet/BalanceCard.tsx
interface BalanceCardProps {
balance: number;
currency: string;
userType: 'earner' | 'spender';
}
export function BalanceCard({ balance, currency, userType }: BalanceCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>
{userType === 'earner' ? 'Your Earnings' : 'Your Credits'}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-4xl font-bold">
${balance.toFixed(2)} {currency}
</p>
</CardContent>
</Card>
);
}Transaction Item
typescript
// app/src/components/wallet/TransactionItem.tsx
const getTypeIcon = (type: string) => {
if (['EARNING', 'CREDIT_PURCHASE'].includes(type)) {
return <ArrowDownRight className="text-green-400" />; // Credit IN
}
return <ArrowUpRight className="text-red-400" />; // Credit OUT
};Withdrawal Modal
typescript
// app/src/components/wallet/WithdrawalModal.tsx
// For female users to request payouts
interface WithdrawalModalProps {
balance: number;
onSubmit: (amount: number, payoutDetails: string) => void;
}Payment Integration Notes
Current State
The current implementation stores transactions with COMPLETED status immediately. In production:
Payment Gateway Integration (Stripe recommended)
- Create PaymentIntent
- Store transaction as
PENDING - Webhook confirms payment → update to
COMPLETED
Subscription Management
- Use Stripe Subscriptions for recurring tiers
- Handle subscription lifecycle events
Payout Processing
- Integrate with payout provider (PayPal, bank transfer)
- Automated payout on admin approval
Recommended Flow
User initiates purchase
│
▼
Create PENDING transaction
│
▼
Redirect to Stripe Checkout
│
▼
Payment succeeds → Webhook
│
▼
Update transaction to COMPLETED
│
▼
Credit user's balanceRevenue Metrics (Admin)
Dashboard Stats
typescript
// Calculate total revenue
const revenue = await prisma.walletTransaction.aggregate({
where: {
type: { in: ['MEMBERSHIP', 'CREDIT_PURCHASE', 'BOOST'] },
status: 'COMPLETED',
},
_sum: {
amount: true,
},
});
// Pending payouts
const pendingPayouts = await prisma.walletTransaction.aggregate({
where: {
type: 'WITHDRAWAL',
status: 'PENDING',
},
_sum: {
amount: true,
},
});FAQ
Why different models for men and women?
"Men bring financial capacity, women bring authentic presence. Both sides are verified and contribute to the ecosystem differently."
What's the minimum withdrawal?
$10 minimum to reduce transaction overhead.
How are earnings calculated?
Currently not implemented. Potential earning triggers:
- Receiving messages
- Getting matches
- Profile engagement
- Referrals