Payments Architecture
How money flows through Yebo.
Overview
All money in Yebo flows through YeboSafe:
- User wallets
- Mobile money in/out
- Escrow for transactions
- Internal transfers
System Architecture
┌────────────────────────────────────────────────────────────────┐
│ MOBILE MONEY │
│ M-Pesa • MTN MoMo • Airtel • EcoCash │
└──────────────────────────┬─────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ PAYMENT GATEWAY │
│ Deposits (STK Push) • Withdrawals (B2C) • Webhooks │
└──────────────────────────┬─────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ YEBOSAFE │
│ Wallets • Transactions • Escrows • Ledger │
└──────────────────────────┬─────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│YeboShops│ │YeboJobs │ │ YeboNa │
│ Escrow │ │ Payroll │ │ Trade │
└─────────┘ └─────────┘ └─────────┘Wallet System
One Wallet Per User Per Currency
sql
CREATE TABLE wallets (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
currency VARCHAR(3) NOT NULL, -- KES, NGN, USD
balance DECIMAL(15,2) DEFAULT 0,
available_balance DECIMAL(15,2) DEFAULT 0, -- minus pending
created_at TIMESTAMP
);Balance Types
| Type | Description |
|---|---|
balance | Total funds |
available_balance | Minus pending withdrawals/escrows |
pending_in | Deposits not yet confirmed |
pending_out | Withdrawals not yet confirmed |
Transaction Ledger
Every money movement is a transaction:
sql
CREATE TABLE transactions (
id UUID PRIMARY KEY,
wallet_id UUID NOT NULL,
type VARCHAR(20) NOT NULL,
amount DECIMAL(15,2) NOT NULL,
balance_before DECIMAL(15,2),
balance_after DECIMAL(15,2),
reference VARCHAR(100),
metadata JSONB,
status VARCHAR(20),
created_at TIMESTAMP
);Transaction Types
| Type | Description | Direction |
|---|---|---|
deposit | Mobile money in | + |
withdrawal | Mobile money out | - |
escrow_hold | Hold for transaction | - |
escrow_release | Release to seller | + |
escrow_refund | Refund to buyer | + |
transfer_out | Send to another user | - |
transfer_in | Receive from user | + |
fee | Platform fee | - |
refund | Dispute refund | + |
Mobile Money Integration
M-Pesa (Kenya)
Deposit (STK Push):
javascript
async function initiateDeposit(walletId, amount, phone) {
// 1. Create pending transaction
const tx = await db.transactions.create({
walletId,
type: 'deposit',
amount,
status: 'pending'
});
// 2. Initiate STK Push
const mpesa = await daraja.stkPush({
phoneNumber: phone,
amount,
accountReference: tx.id,
transactionDesc: 'Yebo Wallet Deposit'
});
// 3. Return pending status
return { transactionId: tx.id, status: 'pending', checkoutRequestId: mpesa.CheckoutRequestID };
}
// Webhook handler
async function handleMpesaCallback(data) {
const { ResultCode, CheckoutRequestID } = data.Body.stkCallback;
const tx = await db.transactions.findByCheckoutId(CheckoutRequestID);
if (ResultCode === 0) {
// Success
await db.transactions.update(tx.id, { status: 'completed' });
await db.wallets.increment(tx.walletId, tx.amount);
await notifyUser(tx.userId, `KES ${tx.amount} deposited!`);
} else {
// Failed
await db.transactions.update(tx.id, { status: 'failed' });
}
}Withdrawal (B2C):
javascript
async function initiateWithdrawal(walletId, amount, phone) {
const wallet = await db.wallets.get(walletId);
// 1. Check balance
if (wallet.available_balance < amount) {
throw new Error('Insufficient balance');
}
// 2. Create pending transaction and deduct
const tx = await db.transactions.create({
walletId,
type: 'withdrawal',
amount: -amount,
status: 'pending'
});
await db.wallets.decrement(walletId, amount);
// 3. Initiate B2C
const mpesa = await daraja.b2c({
phoneNumber: phone,
amount,
occasion: tx.id
});
return { transactionId: tx.id, status: 'processing' };
}Escrow System
Flow
BUYER PAYS
│
▼
┌─────────────────────────────────────────┐
│ ESCROW CREATED │
│ buyer_wallet: -amount │
│ escrow: +amount │
└─────────────────────────────────────────┘
│
▼
SELLER SHIPS
│
▼
BUYER CONFIRMS
│
▼
┌─────────────────────────────────────────┐
│ ESCROW RELEASED │
│ escrow: -amount │
│ seller_wallet: +(amount - fee) │
│ platform: +fee │
└─────────────────────────────────────────┘States
CREATED ──► FUNDED ──► SHIPPED ──► DELIVERED ──► RELEASED
│ │
│ └──► DISPUTED ──► RESOLVED
│
└──► CANCELLED ──► REFUNDEDImplementation
javascript
async function createEscrow(orderId, buyerWalletId, sellerWalletId, amount) {
const buyerWallet = await db.wallets.get(buyerWalletId);
if (buyerWallet.available_balance < amount) {
throw new Error('Insufficient balance');
}
// Atomic transaction
return await db.transaction(async (tx) => {
// Hold from buyer
await tx.wallets.decrement(buyerWalletId, amount);
await tx.transactions.create({
walletId: buyerWalletId,
type: 'escrow_hold',
amount: -amount,
reference: orderId
});
// Create escrow record
const escrow = await tx.escrows.create({
orderId,
buyerWalletId,
sellerWalletId,
amount,
status: 'funded'
});
return escrow;
});
}
async function releaseEscrow(escrowId) {
const escrow = await db.escrows.get(escrowId);
if (escrow.status !== 'funded') {
throw new Error('Escrow not in releasable state');
}
const fee = escrow.amount * 0.05; // 5% fee
const sellerAmount = escrow.amount - fee;
return await db.transaction(async (tx) => {
// Release to seller
await tx.wallets.increment(escrow.sellerWalletId, sellerAmount);
await tx.transactions.create({
walletId: escrow.sellerWalletId,
type: 'escrow_release',
amount: sellerAmount,
reference: escrow.orderId
});
// Collect fee
await tx.wallets.increment(PLATFORM_WALLET_ID, fee);
// Update escrow
await tx.escrows.update(escrowId, {
status: 'released',
releasedAt: new Date()
});
});
}Multi-Currency
Supported Currencies
| Currency | Countries |
|---|---|
| KES | Kenya |
| NGN | Nigeria |
| GHS | Ghana |
| TZS | Tanzania |
| UGX | Uganda |
| USD | Cross-border |
Conversion
javascript
async function convert(amount, fromCurrency, toCurrency) {
if (fromCurrency === toCurrency) return amount;
const rate = await getExchangeRate(fromCurrency, toCurrency);
return amount * rate;
}
// Example: KES to USD
// const usd = await convert(10000, 'KES', 'USD');
// => ~77 (at 130 KES/USD)Security
Fraud Prevention
- Velocity limits: Max transactions per hour
- Amount limits: Per transaction and daily
- Device fingerprinting: Flag new devices
- Anomaly detection: Unusual patterns
Reconciliation
javascript
// Daily reconciliation job
async function reconcile() {
const wallets = await db.wallets.findAll();
for (const wallet of wallets) {
const calculatedBalance = await db.transactions.sum({
walletId: wallet.id,
status: 'completed'
});
if (calculatedBalance !== wallet.balance) {
await alertOps('Balance mismatch', { wallet, calculated, actual: wallet.balance });
}
}
}Reporting
Available Reports
- Transaction history
- Daily summary
- Fee collection
- Escrow status
- Mobile money success rates
Dashboard Metrics
| Metric | Description |
|---|---|
| GMV | Gross merchandise value |
| TPV | Total payment volume |
| Success rate | Successful transactions % |
| Avg transaction | Average transaction size |