Build an Affiliate Commission Engine
Build an affiliate commission engine by generating unique affiliate links with tracking codes, recording clicks and conversions, attributing commissions to the last-click affiliate when a Paystack webhook fires, maintaining a pending commission ledger, and paying out via Paystack Transfers on withdrawal request.
Click Tracking and Conversion Attribution
Tables: affiliates (name, email, tracking_code, commission_rate, pending_balance, paid_balance, paystack_recipient_code), affiliate_clicks (affiliate_id, visitor_ip, visitor_session, clicked_at, converted), affiliate_conversions (affiliate_id, order_id, paystack_ref, order_amount, commission_amount, status, hold_until).
// When visitor clicks affiliate link: yoursite.com/ref?code=ABC123
async function trackClick(affiliateCode, visitorIp, visitorSession) {
var affiliate = await db.affiliates.findByCode(affiliateCode);
if (!affiliate) return;
await db.affiliateClicks.create({
affiliate_id: affiliate.id,
visitor_ip: visitorIp,
visitor_session: visitorSession,
clicked_at: new Date(),
});
// Store attribution in session/cookie (30-day window)
return affiliate.id; // save to cookie on the response
}
// When initializing Paystack checkout — read affiliate from session/cookie
async function initializeCheckoutWithAttribution(customerEmail, amount, orderId, affiliateId) {
var reference = 'ORD-' + 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: customerEmail, amount, currency: 'NGN', reference,
metadata: {
order_id: orderId,
affiliate_id: affiliateId || null, // null if no affiliate
},
}),
});
return (await res.json()).data.authorization_url;
}
Commission Ledger and Payout
// Webhook: credit commission on successful payment
if (event.event === 'charge.success') {
var meta = event.data.metadata;
if (!meta.affiliate_id) return res.sendStatus(200);
var affiliate = await db.affiliates.findById(meta.affiliate_id);
var commissionAmount = Math.floor(event.data.amount * affiliate.commission_rate);
var holdUntil = new Date();
holdUntil.setDate(holdUntil.getDate() + 30); // 30-day hold for refund window
await db.affiliateConversions.create({
affiliate_id: affiliate.id,
order_id: meta.order_id,
paystack_ref: event.data.reference,
order_amount: event.data.amount,
commission_amount: commissionAmount,
status: 'pending', // becomes 'available' after hold period
hold_until: holdUntil,
});
// Mark the click as converted
await db.affiliateClicks.markConverted(affiliate.id, meta.order_id);
}
// Cron: release commissions after hold period
async function releaseHeldCommissions() {
var conversions = await db.affiliateConversions.findReadyToRelease(new Date());
for (var conversion of conversions) {
await db.affiliateConversions.update(conversion.id, { status: 'available' });
await db.affiliates.increment(conversion.affiliate_id, 'pending_balance', conversion.commission_amount);
}
}
// Affiliate requests withdrawal
async function withdrawCommission(affiliateId, amount) {
var affiliate = await db.affiliates.findById(affiliateId);
if (amount > affiliate.pending_balance) throw new Error('Insufficient available balance');
await db.affiliates.decrement(affiliateId, 'pending_balance', amount);
await fetch('https://api.paystack.co/transfer', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ source: 'balance', amount, recipient: affiliate.paystack_recipient_code, reason: 'Affiliate commission payout' }),
});
}
Learn More
See build a referral payout system for a simpler referral-only version of this commission architecture.
Key Takeaways
- ✓Use last-click attribution: the affiliate who drove the final click before conversion gets the commission.
- ✓Store affiliate_id in the Paystack transaction metadata for clean commission attribution in webhooks.
- ✓Credit commissions to a pending_balance only after the charge.success webhook — never on click.
- ✓Implement a 30-day hold on commissions to allow for refunds before paying affiliates.
- ✓Pay affiliates via Paystack Transfers (bank or M-Pesa) on explicit withdrawal request with minimum threshold.
Frequently Asked Questions
- What is the difference between an affiliate system and a referral system?
- Referral systems are user-to-user (your existing customers invite their friends). Affiliate systems involve external publishers, bloggers, or influencers who promote your product to their audience in exchange for commission. Referral commissions are typically small (NGN 500 or 5%). Affiliate commissions are typically percentage-based (10-30%) on sales. This engine supports both models — the architecture is the same.
- How do I prevent affiliate fraud (fake conversions)?
- The 30-day hold before commissions become available gives you time to identify and reverse fraudulent orders before paying out. Additionally: verify that conversions come from unique IPs/devices, not the affiliate's own device. Flag self-referrals (affiliate using their own code). Monitor for conversion velocity spikes. Require email verification before affiliate accounts can withdraw.
- Can affiliates track their own performance?
- Build a simple affiliate dashboard showing: total clicks, total conversions, conversion rate, pending commissions (in hold period), available commissions (ready to withdraw), and paid commissions (historical). This transparency keeps affiliates motivated and reduces support requests about their earnings.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon