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.
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.');
}
}
// 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.
See build a savings goal app with scheduled debits for card tokenization auto-debit — useful for customers who prefer automatic weekly PAYG topups.
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