Build a Classifieds Site with Featured Listing Payments
Build a classifieds site with paid features by allowing free listing creation but gating "featured" and "bumped" placement behind a Paystack payment. Create the listing immediately on submission, but only elevate it to featured status after the charge.success webhook confirms payment. Auto-expire featured status after the paid duration.
Listing Creation and Featured Upgrade
Tables: listings (seller_id, category, title, description, price, images[], location, status, is_featured, featured_until, bumped_until, paystack_ref), feature_packages (name, price, duration_days, benefit).
var FEATURE_PACKAGES = {
featured: { price: 500000, duration: 7, label: 'Featured — top of search for 7 days' }, // NGN 5,000
bumped: { price: 200000, duration: 3, label: 'Bumped — pushed to top for 3 days' }, // NGN 2,000
urgent: { price: 150000, duration: 3, label: 'Urgent badge — highlight for 3 days' }, // NGN 1,500
};
// Free listing — goes live immediately
async function createListing(sellerEmail, sellerId, listingData) {
return await db.listings.create({
seller_id: sellerId, ...listingData,
status: 'active',
is_featured: false,
});
}
// Seller wants to feature an existing listing
async function purchaseFeature(sellerEmail, listingId, packageName) {
var pkg = FEATURE_PACKAGES[packageName];
if (!pkg) throw new Error('Invalid package');
var reference = 'FEAT-' + listingId + '-' + Date.now();
await db.listings.update(listingId, { paystack_ref: reference });
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: sellerEmail,
amount: pkg.price,
currency: 'NGN',
reference,
metadata: { listing_id: listingId, package_name: packageName, duration_days: pkg.duration },
}),
});
return (await res.json()).data.authorization_url;
}
// Webhook: activate featured status
if (event.event === 'charge.success') {
var meta = event.data.metadata;
if (!meta.listing_id) return res.sendStatus(200);
var featuredUntil = new Date();
featuredUntil.setDate(featuredUntil.getDate() + meta.duration_days);
var updateData = { is_featured: true };
if (meta.package_name === 'featured') updateData.featured_until = featuredUntil;
if (meta.package_name === 'bumped') updateData.bumped_until = featuredUntil;
await db.listings.update(meta.listing_id, updateData);
}
Featured Expiry and Renewal Reminders
// Cron: expire featured listings and send renewal reminders
async function processFeaturedExpiry() {
var now = new Date();
// Expire featured listings
var expiredFeatured = await db.listings.findAll({ is_featured: true, featured_until_lt: now });
for (var listing of expiredFeatured) {
await db.listings.update(listing.id, { is_featured: false, featured_until: null });
await sendFeaturedExpiredNotification(listing.seller_id, listing.id);
}
// Send renewal reminders 1 day before expiry
var tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
var expiringSoon = await db.listings.findExpiringSoon(tomorrow);
for (var listing of expiringSoon) {
await sendRenewalReminder(listing.seller_id, listing.id, listing.featured_until);
}
}
// Seller dashboard: listing performance
async function getListingStats(listingId) {
var listing = await db.listings.findById(listingId);
var views = await db.listingViews.countByListing(listingId);
var contacts = await db.listingContacts.countByListing(listingId);
return {
views,
contacts,
contact_rate: views > 0 ? (contacts / views * 100).toFixed(1) + '%' : '0%',
is_featured: listing.is_featured,
featured_until: listing.featured_until,
};
}
Learn More
See build a job board with paid listings for the same paid listing model applied specifically to job postings.
Key Takeaways
- ✓Free listings go live immediately; featured status requires Paystack payment first.
- ✓Support multiple upgrade options: featured (7 days), bumped (3 days), urgent badge (3 days).
- ✓Auto-expire featured status via cron and send renewal prompts 1 day before expiry.
- ✓Use the same pattern for any listing category: cars, property, electronics, services.
- ✓Track which featured listings generate more views/contacts to refine your pricing.
Frequently Asked Questions
- Should all listings require payment, or just the featured upgrade?
- Start with free listings to build inventory — a classifieds site with no listings has no value for anyone. Once you have enough active listings (100+), you can test paid features. Some large classifieds (Jiji, OLX) start free, add featured upgrades, and only later gate basic posting. Free listings + paid upgrades is the proven model for early-stage growth.
- How do I prevent the same listing from being posted multiple times by the same seller?
- Add a duplicate detection check: before saving a new listing, query for active listings from the same seller in the same category with a similar title (fuzzy match) or same price. Flag duplicates for admin review or block them outright. For professional sellers (car dealers, agents), allow multiple listings but require account verification first.
- Can I support a subscription model where sellers pay a flat monthly fee for unlimited featured listings?
- Yes. Create a Paystack Plan for the seller subscription. On subscription.create, set a seller_plan: "pro" flag in your sellers table. In the purchaseFeature function, check if the seller has an active pro subscription and skip the Paystack charge if so. This is a good upsell for power sellers who post frequently.
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