McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Build a Solar Pay-As-You-Go Payment System
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Build a Solar Pay-As-You-Go Payment System

Build a solar PAYG system by tracking each device's credit balance (in days of usage), generating a unique numeric unlock token on every Paystack payment (charge.success webhook), sending the token to the customer via SMS, and having the device firmware validate and apply the token to extend its active period. When the customer's total payments reach the ownership threshold, issue a final "unlock forever" token.

In this article

  1. 1. Device Registration and Token Generation
  2. 2. Device Status Management and Disable Flow
  3. 3. Learn More

Device Registration and Token Generation

Tables: devices (serial_number, customer_id, model, install_date, total_cost, total_paid, status, credit_days_remaining, last_token_used), payments (device_id, amount, paystack_ref, token, token_days, sent_at, applied_at).

var crypto = require('crypto');

// Generate a deterministic but unpredictable 12-digit unlock token
function generatePaygToken(deviceSerial, paymentRef, secretKey) {
  var hmac = crypto.createHmac('sha256', secretKey);
  hmac.update(deviceSerial + '|' + paymentRef);
  var hash = hmac.digest('hex');
  // Convert first 12 hex chars to decimal — device firmware validates same way
  var tokenNum = parseInt(hash.slice(0, 12), 16);
  return (tokenNum % 900000000000 + 100000000000).toString(); // 12-digit token
}

function calculateTokenDays(amountKobo) {
  // NGN 200/day of credit
  return Math.floor(amountKobo / 20000);
}

async function initializeTopup(customerPhone, deviceId, amount) {
  var device = await db.devices.findById(deviceId);
  var customer = await db.customers.findById(device.customer_id);
  var reference = 'PAYG-' + device.serial_number + '-' + Date.now();

  var res = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: customer.email,
      amount,
      currency: 'NGN',
      reference,
      metadata: { device_id: deviceId, serial: device.serial_number, customer_phone: customerPhone },
    }),
  });
  return (await res.json()).data.authorization_url;
}

// Webhook: generate token and send via SMS
if (event.event === 'charge.success') {
  var meta = event.data.metadata;
  if (!meta.device_id) return res.sendStatus(200);

  var device = await db.devices.findById(meta.device_id);
  var tokenDays = calculateTokenDays(event.data.amount);
  var token = generatePaygToken(meta.serial, event.data.reference, process.env.PAYG_TOKEN_SECRET);

  await db.payments.create({
    device_id: meta.device_id,
    amount: event.data.amount,
    paystack_ref: event.data.reference,
    token,
    token_days: tokenDays,
    sent_at: new Date(),
  });

  // Update device credit
  var newTotal = device.total_paid + event.data.amount;
  var newCredit = device.credit_days_remaining + tokenDays;
  await db.devices.update(meta.device_id, { total_paid: newTotal, credit_days_remaining: newCredit });

  // Send token via SMS
  await sendSMS(meta.customer_phone, 'Solar token: ' + token + ' (' + tokenDays + ' days). Enter on device keypad. Balance: NGN ' + (newTotal / 100));

  // Check ownership milestone
  if (newTotal >= device.total_cost) {
    var ownershipToken = generatePaygToken(meta.serial, 'FOREVER', process.env.PAYG_TOKEN_SECRET);
    await db.devices.update(meta.device_id, { status: 'owned' });
    await sendSMS(meta.customer_phone, 'Congratulations! Your solar system is now fully paid. Final unlock code: ' + ownershipToken + '. Your system is yours forever.');
  }
}

Device Status Management and Disable Flow

// Cron: track credit expiry and flag inactive devices
async function checkDeviceCredit() {
  var devices = await db.devices.findAll({ status: 'active' });
  var today = new Date();

  for (var device of devices) {
    // Deduct one day of credit per day
    var newCredit = device.credit_days_remaining - 1;

    if (newCredit <= 0) {
      await db.devices.update(device.id, { credit_days_remaining: 0, status: 'disabled' });
      var customer = await db.customers.findById(device.customer_id);
      await sendSMS(customer.phone, 'Your solar credit has expired. Top up to restore power: yourapp.com/topup');
    } else {
      await db.devices.update(device.id, { credit_days_remaining: newCredit });
      if (newCredit <= 3) { // low credit warning
        var customer = await db.customers.findById(device.customer_id);
        await sendSMS(customer.phone, 'Low solar credit: ' + newCredit + ' days remaining. Top up: yourapp.com/topup');
      }
    }
  }
}

// Field agent: register a new customer and device
async function registerCustomerDevice(customerData, serialNumber, model, totalCostNGN) {
  var customer = await db.customers.create(customerData);
  var device = await db.devices.create({
    serial_number: serialNumber,
    customer_id: customer.id,
    model,
    install_date: new Date(),
    total_cost: totalCostNGN * 100, // store in kobo
    total_paid: 0,
    status: 'active',
    credit_days_remaining: 3, // 3-day free trial on installation
  });
  await sendSMS(customer.phone, 'Welcome! Your solar system is active for 3 free days. Top up at yourapp.com/topup using device ID: ' + device.id);
  return { customer, device };
}

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Learn More

See build a savings goal app with scheduled debits for card tokenization auto-debit — useful for customers who prefer automatic weekly PAYG topups.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Generate a cryptographically derived numeric token per payment — the device validates it without internet access.
  • ✓Send the token via SMS immediately after charge.success — the customer cannot use their device until they receive it.
  • ✓Track cumulative payments per device to determine ownership milestone.
  • ✓Support M-Pesa payments via Paystack (Kenya) for the unbanked customer base typical of PAYG solar.
  • ✓Devices can be remotely disabled (via the next token request) if a customer stops paying — no repossession needed.

Frequently Asked Questions

How does the device validate the token without internet connectivity?
PAYG solar devices use a technique called HMAC-based token validation (the same principle as TOTP). The device's firmware knows the same secret key and can independently compute whether a given token is valid for that device serial number. The server and device use the same algorithm — no connectivity required. Tokens are one-time-use to prevent replay attacks.
What if the customer pays but their SMS is delayed or lost?
Store the token in your database immediately. Build a token lookup portal: the customer visits yourapp.com/token and enters their phone number or device ID to retrieve their latest token without needing to call support. Also set up a USSD shortcode (*123*deviceID#) for feature phone users who cannot browse the web.
How do I handle device transfers (customer sells to someone else)?
Add a device transfer flow: current owner initiates a transfer, new owner pays a transfer fee and registers their details. The device is unlinked from the old customer and linked to the new one. The ownership progress (total_paid) stays with the device, not the customer — the new owner benefits from previous payments made toward ownership. Document this clearly to prevent disputes.

Learn Paystack Integration

KES 2,999

The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.

Start Paystack Course

Not ready? Create Free Account

Keep reading

Things You Can Build with Paystack: Project Guides

A complete map of real-world projects you can build using the Paystack API. From online store checkouts and subscription platforms to savings apps and solar pay-as-you-go systems, each category links to step-by-step build guides with working code.

Read more

Build a Parking Payment App

A build guide for a parking payment app where drivers scan a QR code on entry, pay for their session via Paystack (card or M-Pesa), and the system validates payment before the barrier lifts for exit. Covers time-based pricing, session management, and overstay handling.

Read more

Build a Referral Payout System

A build guide for a referral payout system where users share referral links, earn commissions when referrals convert, and withdraw their balance via Paystack Transfers to their bank account or M-Pesa wallet.

Read more

Build a Veterinary Clinic Booking and Payment System

A build guide for a veterinary clinic booking and payment system where pet owners book appointments online, pay a deposit via Paystack to confirm the slot, and pay the balance at the clinic after the consultation. Includes pet health records, vaccination schedules, and automated reminders.

Read more

M-Pesa Through Paystack vs Direct Daraja STK Push

A side-by-side comparison of how M-Pesa payments work through Paystack versus direct Daraja STK Push. Covers the user flow differences, redirect versus inline experience, timeout handling, error messages, customer confusion points, and conversion rate implications for Kenyan developers.

Read more

Splitting Payments Between a School and Its Departments

When a student pays school fees, the money needs to be split across multiple departments — tuition to the main account, library fee to the library department, sports levy to sports. This guide covers routing a single student payment to multiple department subaccounts using Paystack Transaction Splits.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.