Skip to main content
Learn how to build a complete ride-hailing platform or super app using Yabetoo, inspired by Gozem, Africa’s super app operating across Francophone Africa. This guide covers ride payments, driver payouts, digital wallets, and multi-service integrations.

Overview

Ride-hailing and super apps need to handle:
  • Real-time ride fare calculation and payment
  • Multiple vehicle types (motorcycles, cars, tricycles)
  • Driver earnings and instant payouts
  • Digital wallet top-ups and payments
  • Multi-service transactions (rides, deliveries, groceries)
  • Commission management

Gozem’s Model

Gozem is Africa’s super app operating since 2018, offering:
  • Transportation: Motorcycle taxis (zémidjans), cars, tricycles
  • Delivery: Food, groceries, e-commerce logistics
  • Financial services: Digital wallet, cashless payments
  • Coverage: 9+ countries including Togo, Benin, Cameroon, Congo
In Congo Brazzaville, Gozem uses Yabetoo for mobile money payment processing.

Architecture

Implementation

1. Data Models

Define ride and vehicle structures:
// types.ts
interface Ride {
  id: string;
  passengerId: string;
  driverId?: string;
  vehicleType: 'motorcycle' | 'car' | 'tricycle';
  pickup: Location;
  dropoff: Location;
  distance: number; // in km
  duration: number; // estimated minutes
  fare: number;
  platformFee: number;
  driverEarnings: number;
  status: 'searching' | 'accepted' | 'arrived' | 'in_progress' | 'completed' | 'cancelled';
  paymentMethod: 'wallet' | 'mobile_money' | 'cash';
  paymentStatus: 'pending' | 'paid' | 'failed';
  createdAt: Date;
  completedAt?: Date;
}

interface Location {
  address: string;
  lat: number;
  lng: number;
}

interface Driver {
  id: string;
  firstName: string;
  lastName: string;
  phone: string;
  operatorName: 'mtn' | 'airtel';
  vehicleType: 'motorcycle' | 'car' | 'tricycle';
  vehiclePlate: string;
  rating: number;
  totalRides: number;
  walletBalance: number;
  status: 'online' | 'offline' | 'busy';
  currentLocation?: Location;
}

interface Wallet {
  id: string;
  userId: string;
  userType: 'passenger' | 'driver';
  balance: number;
  currency: string;
}

// Pricing configuration
const PRICING = {
  motorcycle: { baseFare: 500, perKm: 200, perMinute: 25 },
  car: { baseFare: 1000, perKm: 350, perMinute: 50 },
  tricycle: { baseFare: 750, perKm: 250, perMinute: 35 }
};

const PLATFORM_COMMISSION = 0.20; // 20% platform commission

2. Fare Calculation

Calculate dynamic fares based on distance and time:
interface FareEstimate {
  baseFare: number;
  distanceFare: number;
  timeFare: number;
  totalFare: number;
  platformFee: number;
  driverEarnings: number;
  currency: string;
}

function calculateFare(
  vehicleType: 'motorcycle' | 'car' | 'tricycle',
  distanceKm: number,
  durationMinutes: number,
  surgeMultiplier: number = 1.0
): FareEstimate {
  const pricing = PRICING[vehicleType];

  const baseFare = pricing.baseFare;
  const distanceFare = Math.round(distanceKm * pricing.perKm);
  const timeFare = Math.round(durationMinutes * pricing.perMinute);

  const subtotal = baseFare + distanceFare + timeFare;
  const totalFare = Math.round(subtotal * surgeMultiplier);

  const platformFee = Math.round(totalFare * PLATFORM_COMMISSION);
  const driverEarnings = totalFare - platformFee;

  return {
    baseFare,
    distanceFare,
    timeFare,
    totalFare,
    platformFee,
    driverEarnings,
    currency: 'XAF'
  };
}

// Example: 5km ride, 15 minutes, by motorcycle
// calculateFare('motorcycle', 5, 15)
// → { baseFare: 500, distanceFare: 1000, timeFare: 375, totalFare: 1875, ... }

3. Wallet Top-Up

Allow passengers to top up their digital wallet:
import Yabetoo from '@yabetoo/sdk-js';

const yabetoo = new Yabetoo(process.env.YABETOO_SECRET_KEY!);

async function topUpWallet(userId: string, amount: number) {
  if (amount < 500) {
    throw new Error('Minimum top-up amount is 500 XAF');
  }

  const user = await db.users.findById(userId);

  const intent = await yabetoo.payments.create({
    amount,
    currency: 'XAF',
    description: `Wallet top-up - ${user.firstName} ${user.lastName}`,
    metadata: {
      userId,
      userType: 'passenger',
      type: 'wallet_topup'
    }
  });

  await db.walletTransactions.create({
    id: generateTransactionId(),
    walletId: user.walletId,
    type: 'topup',
    amount,
    paymentIntentId: intent.id,
    status: 'pending',
    createdAt: new Date()
  });

  return intent;
}

// Webhook handler for wallet top-up
async function handleWalletTopUp(data: any) {
  const { userId, amount } = data.metadata;

  await db.wallets.incrementBalance(userId, amount);

  await db.walletTransactions.updateByPaymentIntent(data.id, {
    status: 'completed'
  });

  await sendNotification(userId, {
    type: 'topup_success',
    title: 'Wallet Top-Up Successful',
    message: `${amount} XAF has been added to your wallet.`
  });
}

4. Ride Booking and Payment

Handle ride requests and payments:
async function createRideRequest(
  passengerId: string,
  pickup: Location,
  dropoff: Location,
  vehicleType: 'motorcycle' | 'car' | 'tricycle',
  paymentMethod: 'wallet' | 'mobile_money'
) {
  const passenger = await db.users.findById(passengerId);

  // Calculate route
  const route = await calculateRoute(pickup, dropoff);
  const fareEstimate = calculateFare(vehicleType, route.distanceKm, route.durationMinutes);

  // Check wallet balance if paying with wallet
  if (paymentMethod === 'wallet') {
    const wallet = await db.wallets.findByUser(passengerId);
    if (wallet.balance < fareEstimate.totalFare) {
      throw new Error('Insufficient wallet balance. Please top up or use mobile money.');
    }
  }

  // Create ride
  const ride: Ride = {
    id: generateRideId(),
    passengerId,
    vehicleType,
    pickup,
    dropoff,
    distance: route.distanceKm,
    duration: route.durationMinutes,
    fare: fareEstimate.totalFare,
    platformFee: fareEstimate.platformFee,
    driverEarnings: fareEstimate.driverEarnings,
    status: 'searching',
    paymentMethod,
    paymentStatus: 'pending',
    createdAt: new Date()
  };

  await db.rides.create(ride);

  // Find nearby drivers
  await broadcastToNearbyDrivers(ride);

  return { ride, fareEstimate };
}

async function driverAcceptRide(rideId: string, driverId: string) {
  const ride = await db.rides.findById(rideId);
  const driver = await db.drivers.findById(driverId);

  await db.rides.update(rideId, {
    driverId,
    status: 'accepted'
  });

  await db.drivers.update(driverId, { status: 'busy' });

  // Notify passenger
  await sendNotification(ride.passengerId, {
    type: 'driver_assigned',
    title: 'Driver Found!',
    message: `${driver.firstName} is on the way. ${driver.vehicleType} - ${driver.vehiclePlate}`,
    driverInfo: {
      name: `${driver.firstName} ${driver.lastName}`,
      phone: driver.phone,
      vehiclePlate: driver.vehiclePlate,
      rating: driver.rating
    }
  });

  return { ride, driver };
}

5. Ride Completion and Payment Processing

Process payment when ride is completed:
async function completeRide(rideId: string, driverId: string) {
  const ride = await db.rides.findById(rideId);

  if (ride.driverId !== driverId) {
    throw new Error('Unauthorized');
  }

  // Update ride status
  await db.rides.update(rideId, {
    status: 'completed',
    completedAt: new Date()
  });

  // Process payment based on method
  if (ride.paymentMethod === 'wallet') {
    await processWalletPayment(ride);
  } else if (ride.paymentMethod === 'mobile_money') {
    await processMobileMoneyPayment(ride);
  }

  // Update driver status
  await db.drivers.update(driverId, { status: 'online' });

  return ride;
}

async function processWalletPayment(ride: Ride) {
  // Deduct from passenger wallet
  await db.wallets.decrementBalance(ride.passengerId, ride.fare);

  // Record transaction
  await db.walletTransactions.create({
    walletId: await db.wallets.findByUser(ride.passengerId).id,
    type: 'ride_payment',
    amount: -ride.fare,
    rideId: ride.id,
    status: 'completed'
  });

  // Credit driver earnings
  await db.drivers.incrementWalletBalance(ride.driverId!, ride.driverEarnings);

  // Update payment status
  await db.rides.update(ride.id, { paymentStatus: 'paid' });

  // Notify both parties
  await sendRideCompletionNotifications(ride);
}

async function processMobileMoneyPayment(ride: Ride) {
  const passenger = await db.users.findById(ride.passengerId);

  // Create payment intent for mobile money
  const intent = await yabetoo.payments.create({
    amount: ride.fare,
    currency: 'XAF',
    description: `Ride #${ride.id} - ${ride.vehicleType}`,
    metadata: {
      rideId: ride.id,
      passengerId: ride.passengerId,
      driverId: ride.driverId,
      driverEarnings: ride.driverEarnings.toString(),
      type: 'ride_payment'
    }
  });

  // Store pending payment
  await db.ridePayments.create({
    rideId: ride.id,
    paymentIntentId: intent.id,
    status: 'pending'
  });

  // Send payment link to passenger
  await sendNotification(ride.passengerId, {
    type: 'payment_required',
    title: 'Complete Your Payment',
    message: `Please pay ${ride.fare} XAF for your ride.`,
    paymentIntent: intent
  });

  return intent;
}

6. Driver Payout System

Process driver payouts:
// Instant payout on demand
async function requestDriverPayout(driverId: string, amount?: number) {
  const driver = await db.drivers.findById(driverId);

  const payoutAmount = amount || driver.walletBalance;

  if (payoutAmount < 1000) {
    throw new Error('Minimum payout amount is 1,000 XAF');
  }

  if (payoutAmount > driver.walletBalance) {
    throw new Error('Insufficient balance');
  }

  const disbursement = await yabetoo.disbursements.create({
    amount: payoutAmount,
    currency: 'XAF',
    firstName: driver.firstName,
    lastName: driver.lastName,
    paymentMethodData: {
      type: 'momo',
      momo: {
        msisdn: driver.phone,
        country: 'cg',
        operatorName: driver.operatorName
      }
    }
  });

  // Deduct from driver wallet
  await db.drivers.decrementWalletBalance(driverId, payoutAmount);

  // Record payout
  await db.driverPayouts.create({
    driverId,
    amount: payoutAmount,
    disbursementId: disbursement.id,
    status: 'processing',
    createdAt: new Date()
  });

  await sendNotification(driverId, {
    type: 'payout_initiated',
    title: 'Payout Processing',
    message: `${payoutAmount} XAF is being sent to your mobile money account.`
  });

  return disbursement;
}

// Daily automatic payouts for drivers with balance > threshold
async function processDailyPayouts() {
  const drivers = await db.drivers.findMany({
    where: { walletBalance: { gte: 5000 } }
  });

  for (const driver of drivers) {
    try {
      await requestDriverPayout(driver.id, driver.walletBalance);
    } catch (error) {
      console.error(`Payout failed for driver ${driver.id}:`, error);
    }
  }
}

7. Webhook Handler

Process all payment events:
app.post('/webhooks/yabetoo', async (req, res) => {
  const event = req.body;

  switch (event.type) {
    case 'payment_intent.succeeded':
      const { type } = event.data.metadata;

      if (type === 'wallet_topup') {
        await handleWalletTopUp(event.data);
      } else if (type === 'ride_payment') {
        await handleRidePaymentSuccess(event.data);
      }
      break;

    case 'payment_intent.failed':
      await handlePaymentFailure(event.data);
      break;

    case 'disbursement.completed':
      await handlePayoutComplete(event.data);
      break;

    case 'disbursement.failed':
      await handlePayoutFailure(event.data);
      break;
  }

  res.json({ received: true });
});

async function handleRidePaymentSuccess(data: any) {
  const { rideId, driverId, driverEarnings } = data.metadata;

  // Update ride payment status
  await db.rides.update(rideId, { paymentStatus: 'paid' });

  // Credit driver earnings
  await db.drivers.incrementWalletBalance(driverId, parseInt(driverEarnings));

  // Send notifications
  const ride = await db.rides.findById(rideId);
  await sendRideCompletionNotifications(ride);
}

8. Multi-Service Support (Super App)

Extend to support multiple services like Gozem:
type ServiceType = 'ride' | 'food_delivery' | 'grocery' | 'package';

interface Order {
  id: string;
  userId: string;
  serviceType: ServiceType;
  details: RideDetails | DeliveryDetails;
  amount: number;
  status: string;
  paymentMethod: 'wallet' | 'mobile_money';
}

async function createOrder(
  userId: string,
  serviceType: ServiceType,
  details: any,
  paymentMethod: 'wallet' | 'mobile_money'
) {
  let amount: number;

  switch (serviceType) {
    case 'ride':
      const fareEstimate = calculateFare(details.vehicleType, details.distance, details.duration);
      amount = fareEstimate.totalFare;
      break;
    case 'food_delivery':
      amount = details.foodTotal + details.deliveryFee;
      break;
    case 'grocery':
      amount = details.groceryTotal + details.deliveryFee;
      break;
    case 'package':
      amount = calculatePackageDeliveryFee(details.weight, details.distance);
      break;
  }

  const order: Order = {
    id: generateOrderId(),
    userId,
    serviceType,
    details,
    amount,
    status: 'pending',
    paymentMethod
  };

  await db.orders.create(order);

  // Process based on payment method
  if (paymentMethod === 'wallet') {
    await processWalletPaymentForOrder(order);
  } else {
    return await createMobileMoneyPaymentForOrder(order);
  }

  return order;
}

Ride Flow

1

Request Ride

Passenger selects pickup/dropoff locations and vehicle type.
2

View Fare Estimate

App displays estimated fare based on distance and time.
3

Confirm & Pay

Passenger confirms and pays via wallet or mobile money.
4

Driver Matched

Nearby driver accepts the ride request.
5

Track Ride

Real-time GPS tracking until completion.
6

Rate & Review

Both parties rate each other after the ride.
7

Driver Payout

Driver receives earnings in their wallet, can withdraw anytime.

Pricing Table Example

Vehicle TypeBase FarePer KmPer Minute
Motorcycle500 XAF200 XAF25 XAF
Car1,000 XAF350 XAF50 XAF
Tricycle750 XAF250 XAF35 XAF

Best Practices

Implement dynamic pricing during peak hours to balance supply and demand.
Offer bonuses for completing X rides per day or during off-peak hours.
Include emergency button, trip sharing, and driver verification.
Cache recent transactions and sync when connection is restored.