
Loyalty Points System
- 70 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Implement a points-and-tiers loyalty program where customers earn on purchases and redeem for discounts, using loyalty apps or a custom ledger.
About
Builds a loyalty points program with earning rules, tier progression, and checkout redemption via Smile.io, LoyaltyLion, WooCommerce Points, or a ledger-based custom implementation. A developer uses it to add a structured retention mechanism that increases repeat purchases.
- Append-only points ledger schema with balance, award-after-fulfillment, and redemption code
- Best practices for expiry reminders, refund reversals, and tier thresholds
Loyalty Points System by the numbers
- 70 all-time installs (skills.sh)
- Ranked #3,084 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill loyalty-points-systemAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 70 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Implement a points-and-tiers loyalty program where customers earn on purchases and redeem for discounts, using loyalty apps or a custom ledger.
Files
Loyalty Points System
Overview
A loyalty points program rewards repeat customers with points earned on purchases and other actions (reviews, referrals, social shares), which they can redeem for discounts at checkout. Well-designed programs include tier progression (Bronze → Silver → Gold → Platinum) that unlocks perks like free shipping or higher point multipliers as customers spend more. Most merchants should start with a dedicated loyalty app — the major platforms have excellent options — rather than building from scratch.
When to Use This Skill
- When adding a customer retention mechanism to increase repeat purchase rate
- When launching a tiered VIP program where high-value customers unlock benefits like free shipping or early access
- When replacing an ad-hoc discount system with a structured loyalty program that customers can track
- When integrating points into a mobile app where customers check their balance and redeem at checkout
- When running promotional campaigns that award bonus points for specific actions (reviews, referrals, first purchase)
Core Instructions
Step 1: Determine the merchant's platform and choose the right tool
| Platform | Recommended Tool | Why |
|---|---|---|
| Shopify | Smile.io (formerly Sweet Tooth) or LoyaltyLion | Both integrate natively with Shopify, handle points earning/redemption at checkout, and include tier management and referral programs |
| Shopify (budget) | Rivo Loyalty & Rewards or Bon Loyalty | Lower-cost alternatives with core points and tier features |
| WooCommerce | WooCommerce Points and Rewards (official, ~$79/year) or YITH WooCommerce Points & Rewards | WooCommerce-native integration; the official plugin is well-maintained |
| BigCommerce | Smile.io or LoyaltyLion | Both have BigCommerce integrations |
| Multi-platform / Headless | Loyaltylion API, Smile.io API, or Yotpo Loyalty | All expose APIs for integration with custom storefronts |
| Custom / Headless | Build with ledger-based points tracking | Only when app capabilities are insufficient for your specific requirements |
Step 2: Configure the loyalty program
---
Shopify
Using Smile.io (most popular Shopify loyalty app):
1. Install Smile: Loyalty & Rewards from the Shopify App Store (free tier available; paid plans from ~$49/month) 2. In the Smile dashboard, configure Ways to earn:
- Points for purchase (e.g., 1 point per $1 spent)
- Bonus points for account creation, birthday, product reviews
- Referral points for referring a friend
3. Configure Ways to redeem:
- Points for a discount code (e.g., 100 points = $1 off)
- Set minimum points required for redemption
- Set maximum redemption per order (e.g., max 50% of order value in points)
4. Configure Tiers (VIP program, available on paid plans):
- Define tier thresholds (e.g., Bronze: 0–$500 spend, Silver: $500–$1,000, Gold: $1,000+)
- Set tier benefits: point multipliers, exclusive products, free shipping
5. Configure the Customer-facing widget: the floating points widget appears on your storefront automatically; customize colors and placement
Testing the integration:
- Create a test customer account, place a test order, and verify points are awarded after the order status changes to "fulfilled"
- Attempt a redemption at checkout to verify the discount applies correctly
---
WooCommerce
Using WooCommerce Points and Rewards:
1. Install WooCommerce Points and Rewards from WooCommerce.com 2. Go to WooCommerce → Points and Rewards → Settings:
- Earn points: set the earning ratio (e.g., 1 point per $1 spent)
- Earn points for: purchases, reviews, account registration
- Redeem points: set the redemption ratio (e.g., 100 points = $1 discount)
- Maximum discount: cap the amount that can be redeemed per order
- Points expire: optionally set an expiration period
3. Manage individual customer balances under WooCommerce → Points and Rewards → Manage Points 4. Points are displayed in the customer's account page automatically
For tiers with WooCommerce: The official plugin does not include tier management. Options:
- YITH WooCommerce Points & Rewards: includes tier/VIP functionality
- Gamification for WooCommerce: adds tier levels and achievement badges
- Manually assign WooCommerce customer groups (roles) based on lifetime spend and give group-specific prices or shipping rates
---
BigCommerce
Using Smile.io on BigCommerce:
1. Install Smile.io from the BigCommerce App Marketplace 2. Follow the same Smile.io setup as the Shopify section above — the interface is nearly identical 3. Smile.io integrates with BigCommerce's checkout to apply loyalty discounts as coupon codes
Using LoyaltyLion:
1. Install LoyaltyLion from the BigCommerce App Marketplace 2. Configure earning rules and redemption options in the LoyaltyLion dashboard 3. The app adds a customer loyalty panel to account pages and integrates with BigCommerce checkout
---
Custom / Headless
For headless storefronts or when app capabilities are insufficient, build a ledger-based points system. The ledger pattern (append-only transactions, no balance column) ensures accuracy, enables full audit trails, and makes point reversals trivial.
CREATE TABLE loyalty_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL UNIQUE,
tier VARCHAR(16) NOT NULL DEFAULT 'bronze',
lifetime_spend_cents INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE loyalty_ledger (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES loyalty_accounts(id),
points INTEGER NOT NULL, -- positive = earned, negative = redeemed/expired
type VARCHAR(32) NOT NULL CHECK (type IN ('purchase', 'bonus', 'referral', 'redemption', 'expiration', 'adjustment')),
reference_id UUID, -- order_id, review_id, etc.
description TEXT NOT NULL,
expires_at TIMESTAMPTZ, -- NULL = never expires
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Points balance (excluding expired transactions):
async function getPointsBalance(accountId: string): Promise<number> {
const result = await db.raw(`
SELECT COALESCE(SUM(points), 0) AS balance
FROM loyalty_ledger
WHERE account_id = ?
AND (expires_at IS NULL OR expires_at > NOW())
`, [accountId]);
return Math.max(0, parseInt(result.rows[0].balance, 10));
}Award points after order fulfillment (not at purchase — prevents points fraud from returns):
const TIER_MULTIPLIERS = { bronze: 1, silver: 1.25, gold: 1.5, platinum: 2 };
const POINTS_PER_DOLLAR = 1;
const EXPIRY_MONTHS = 12;
async function awardPurchasePoints(customerId: string, orderId: string, subtotalCents: number) {
const account = await getOrCreateAccount(customerId);
const multiplier = TIER_MULTIPLIERS[account.tier];
const points = Math.round((subtotalCents / 100) * POINTS_PER_DOLLAR * multiplier);
const expiresAt = new Date();
expiresAt.setMonth(expiresAt.getMonth() + EXPIRY_MONTHS);
await db.loyaltyLedger.insert({
account_id: account.id, points, type: 'purchase',
reference_id: orderId, description: `Points for order ${orderId}`, expires_at: expiresAt,
});
// Update lifetime spend and recalculate tier
await db.loyaltyAccounts.update(account.id, {
lifetime_spend_cents: account.lifetime_spend_cents + subtotalCents,
});
await recalculateTier(account.id);
}Redeem points at checkout:
const REDEMPTION_RATE = 0.01; // 100 points = $1
async function redeemPoints(customerId: string, orderId: string, pointsToRedeem: number): Promise<{ discountCents: number }> {
const account = await db.loyaltyAccounts.findByCustomerId(customerId);
const balance = await getPointsBalance(account.id);
if (pointsToRedeem > balance) throw new Error('Insufficient points');
const discountCents = Math.floor(pointsToRedeem * REDEMPTION_RATE * 100);
await db.loyaltyLedger.insert({
account_id: account.id, points: -pointsToRedeem, type: 'redemption',
reference_id: orderId, description: `Redeemed for order ${orderId}`,
});
return { discountCents };
}Best Practices
- Award points after fulfillment, not at purchase — prevents customers from earning points on orders they plan to return; most loyalty apps have a configurable "pending period" for this
- Display points in dollar value on the UI — "You have $5.00 in rewards" converts better than "You have 500 points"; show the dollar value prominently
- Send expiration reminder emails — email customers 30 days before their points expire; this is a proven re-engagement trigger that also drives purchases
- Cap maximum redemption per order — allow redeeming at most 50% of order value in points to protect margin
- Reverse points when orders are refunded — insert a negative adjustment transaction tied to the refund event; do not rely on manual corrections
- Display upcoming tier thresholds — "Spend $47 more to reach Gold" drives incremental spend more than just showing the current tier
- Start simple — launch with a flat earn rate before adding tiers; tiers add complexity and customer service burden; prove the program works first
Common Pitfalls
| Problem | Solution |
|---|---|
| Customer earns points on a returned order | Award points only after the return window closes, or reverse points when a return is processed |
| Tier downgrade confuses customers | Only evaluate tier downgrades at pre-defined calendar dates (e.g., annually), not after each order; communicate the policy clearly |
| Referral fraud — customers referring themselves | Validate referrals by checking that referrer and referee have different email addresses and/or billing addresses |
| App stops awarding points after a platform update | Set up monitoring alerts on your loyalty app; test point awards weekly with an automated test order |
| Points balance shown as zero on checkout due to sync lag | For custom builds, calculate balance in real-time from the ledger; for app-based programs, ensure the app is fully integrated with your checkout |
Related Skills
- @coupon-management
- @gift-cards
- @ab-testing-pricing
- @customer-segmentation
- @discount-engine
{
"context": "Tests whether the agent designs a ledger-based loyalty schema with the correct tables, columns, constraints, and indexes as specified by the skill. The key distinction is using an append-only transaction ledger rather than a mutable balance column.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Append-only ledger table",
"max_score": 12,
"description": "Creates a transactions/ledger table (named loyalty_transactions or similar) rather than storing a mutable balance directly on the accounts table"
},
{
"name": "Accounts table: UUID primary key",
"max_score": 6,
"description": "loyalty_accounts table uses a UUID primary key (e.g., gen_random_uuid() or uuid_generate_v4())"
},
{
"name": "Accounts table: customer_id unique FK",
"max_score": 6,
"description": "loyalty_accounts has a customer_id column that is UNIQUE and references the customers table"
},
{
"name": "Accounts table: tier CHECK constraint",
"max_score": 10,
"description": "tier column has a CHECK constraint restricting values to exactly 'bronze', 'silver', 'gold', 'platinum'"
},
{
"name": "Accounts table: lifetime_spend column",
"max_score": 8,
"description": "loyalty_accounts includes a lifetime_spend column (INTEGER or BIGINT, in cents) used for tier calculation, defaulting to 0"
},
{
"name": "Transactions table: signed points column",
"max_score": 10,
"description": "loyalty_transactions has a points column that is INTEGER (positive for earned, negative for redeemed/expired) — NOT two separate columns or an unsigned type"
},
{
"name": "Transactions table: type CHECK constraint",
"max_score": 10,
"description": "type column has a CHECK constraint that includes at minimum: 'purchase', 'bonus', 'redemption', 'expiration', 'adjustment', 'referral'"
},
{
"name": "Transactions table: expires_at column",
"max_score": 8,
"description": "loyalty_transactions includes an expires_at column (TIMESTAMPTZ or TIMESTAMP, nullable) to support point expiration policies"
},
{
"name": "Transactions table: reference_id column",
"max_score": 6,
"description": "loyalty_transactions includes a reference_id column (UUID, nullable) for linking to orders, referrals, reviews etc."
},
{
"name": "Account-date index",
"max_score": 12,
"description": "Creates an index on loyalty_transactions covering (account_id, created_at DESC) for efficient balance lookups"
},
{
"name": "Partial expiry index",
"max_score": 12,
"description": "Creates a partial index on loyalty_transactions(expires_at) WHERE expires_at IS NOT NULL for efficient expiration job queries"
}
]
}
Loyalty Program Database Schema
Problem/Feature Description
A mid-sized online retailer is launching a customer loyalty program and needs a solid database foundation before any application code is written. The engineering team wants a schema that can support points earned on purchases, bonus actions, redemptions, and automatic expiry — without ever losing the history of how a customer's balance changed over time. Auditing is critical: the finance team needs to be able to trace every points event back to its source. The schema also needs to support tiered membership levels that unlock different earning rates.
The existing database is PostgreSQL and already has a customers table with an id UUID primary key. Your task is to design the tables, constraints, and indexes that will underpin the loyalty system.
Output Specification
Produce a single SQL migration file named migration.sql containing:
- All
CREATE TABLEstatements - All
CREATE INDEXstatements - Any necessary
CHECKconstraints
The file should be runnable against a PostgreSQL database that already has a customers(id UUID) table. Do not include any application code — only SQL.
{
"context": "Tests whether the agent implements the points earning engine with the correct tier multipliers, base earn rate, expiration window, calculation math, transactional logic, tier thresholds, tier upgrade notification, and balance calculation using the skill's exact values.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Tier multipliers",
"max_score": 10,
"description": "Defines tier multipliers as: bronze=1, silver=1.25, gold=1.5, platinum=2 (exact values)"
},
{
"name": "Base earn rate",
"max_score": 8,
"description": "Uses a base rate of 1 point per dollar (POINTS_PER_DOLLAR = 1 or equivalent constant)"
},
{
"name": "12-month expiration",
"max_score": 8,
"description": "Sets points to expire 12 months after earning (not any other duration)"
},
{
"name": "Points math: floor then round",
"max_score": 8,
"description": "Calculates base points with Math.floor(orderSubtotalCents / 100) and applies tier multiplier with Math.round()"
},
{
"name": "Transactional award",
"max_score": 10,
"description": "Inserts the points transaction AND updates lifetime_spend inside a single database transaction (db.transaction or equivalent)"
},
{
"name": "Tier recalculation after award",
"max_score": 6,
"description": "Calls recalculateTier after the transaction commits (not inside the transaction itself)"
},
{
"name": "Tier thresholds",
"max_score": 10,
"description": "Uses tier thresholds: platinum >= 200000 cents ($2,000), gold >= 100000 ($1,000), silver >= 25000 ($250), bronze >= 0"
},
{
"name": "Tier upgrade email",
"max_score": 8,
"description": "Calls sendTierUpgradeEmail when a customer's tier changes to a higher tier"
},
{
"name": "Balance excludes expired",
"max_score": 10,
"description": "getPointsBalance query filters out expired points using: expires_at IS NULL OR expires_at > NOW()"
},
{
"name": "Balance non-negative",
"max_score": 6,
"description": "getPointsBalance returns Math.max(0, balance) or equivalent to prevent negative balance results"
},
{
"name": "getOrCreate account",
"max_score": 8,
"description": "Implements getOrCreateLoyaltyAccount that returns existing account or creates new one with default tier='bronze'"
},
{
"name": "Transaction type: purchase",
"max_score": 8,
"description": "Inserts the purchase transaction with type='purchase' (not 'earn', 'credit', or other)"
}
]
}
Loyalty Points Earning Engine
Problem/Feature Description
A fashion e-commerce brand wants to reward their best customers with accelerated points earning as they reach higher spending milestones. The loyalty program should automatically move customers through membership tiers — Bronze, Silver, Gold, and Platinum — as their total lifetime purchases grow, and each tier should give them a higher earn rate on future purchases. When a customer crosses a tier threshold, they should receive a congratulatory email.
The database schema is already in place with loyalty_accounts and loyalty_transactions tables. The engineering team needs TypeScript functions to handle the earn flow: calculating the correct points for a completed order, updating the customer's lifetime spend, recalculating their tier, and recording the transaction. Points should expire after a set period.
The existing database ORM exposes:
db.loyaltyAccounts.findByCustomerId(customerId)→ account or nulldb.loyaltyAccounts.findById(id)→ accountdb.loyaltyAccounts.insert(data)→ accountdb.loyaltyAccounts.update(id, data)→ voiddb.loyaltyTransactions.insert(data)→ voiddb.transaction(callback)→ runs callback in a DB transactionsendTierUpgradeEmail(customerId, newTier)→ Promise<void>
Output Specification
Produce a TypeScript file named loyalty-earn.ts containing:
- All constants at the top of the file
getOrCreateLoyaltyAccount(customerId: string)functionawardPurchasePoints(customerId, orderId, orderSubtotalCents)function that returns the number of points awardedrecalculateTier(accountId: string)functiongetPointsBalance(accountId: string)function (usesdb.rawwith a SQL query)
Include a brief // Usage example comment at the bottom showing how to call awardPurchasePoints.
{
"context": "Tests whether the agent implements redemption and expiration logic with the correct conversion rate, idempotency check, redemption cap, validation guards, expiration query filters, and safe expiration capping — all per the skill's specific values and patterns.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Conversion rate 100:1",
"max_score": 10,
"description": "Uses a redemption rate of 100 points = $1 (i.e., POINTS_TO_DOLLARS_RATE = 0.01 or equivalent calculation producing discountCents = floor(pointsToRedeem * 0.01 * 100))"
},
{
"name": "Redemption transaction: negative points",
"max_score": 8,
"description": "Inserts redemption transaction with negative points value (e.g., points: -pointsToRedeem)"
},
{
"name": "Redemption transaction: type='redemption'",
"max_score": 6,
"description": "Redemption transaction uses type='redemption' (not 'debit', 'spend', or other)"
},
{
"name": "Redemption transaction: expires_at null",
"max_score": 6,
"description": "Redemption transaction is inserted with expires_at: null"
},
{
"name": "Idempotency check",
"max_score": 12,
"description": "Checks for an existing redemption transaction with the same account_id + orderId (reference_id) + type='redemption' before deducting points, and returns early or skips if found"
},
{
"name": "50% redemption cap",
"max_score": 10,
"description": "Enforces a maximum redemption of 50% of the order value (e.g., caps pointsToRedeem so discountCents <= orderSubtotalCents * 0.5)"
},
{
"name": "Validation guards",
"max_score": 8,
"description": "Validates that: (1) loyalty account exists, (2) pointsToRedeem <= available balance, (3) pointsToRedeem > 0 — throwing errors for each violation"
},
{
"name": "Expiration query filters",
"max_score": 10,
"description": "expirePoints() query filters transactions with: expires_at <= NOW() AND type != 'expiration' AND points > 0"
},
{
"name": "Expiration balance cap",
"max_score": 12,
"description": "In expirePoints(), caps the amount to expire as Math.min(totalExpiring, currentBalance) to avoid expiring more than available"
},
{
"name": "Expiration transaction: type='expiration'",
"max_score": 8,
"description": "Inserts expiration records with type='expiration' and negative points and expires_at: null"
},
{
"name": "Notes comment block",
"max_score": 10,
"description": "The file includes a comment block explaining design decisions (idempotency, expiration logic, redemption cap, and conversion rate)"
}
]
}
Loyalty Points Redemption and Expiration Maintenance
Problem/Feature Description
A retail platform has a working loyalty points earning system and now needs two more pieces: the ability for customers to apply their points as a discount at checkout, and a nightly maintenance job that cleans up expired points.
The checkout team has reported that customers are occasionally charged twice due to payment gateway retries, so the redemption function must be safe to call multiple times for the same order without double-deducting points. The finance team also wants a safeguard so that customers cannot apply so many points that the company loses its entire margin on an order — there should be a ceiling on how much of an order can be paid with points.
The database ORM is the same as the earning system:
db.loyaltyAccounts.findByCustomerId(customerId)→ account or nulldb.loyaltyTransactions.insert(data)→ voiddb.loyaltyTransactions.findOne(filter)→ transaction or nulldb.raw(sql, params)→ query result with.rowsarraygetPointsBalance(accountId)→ Promise<number> (already implemented elsewhere)
Output Specification
Produce a TypeScript file named loyalty-redeem.ts containing:
- All constants at the top of the file
redeemPoints(customerId, orderId, pointsToRedeem, orderSubtotalCents)function returning{ discountCents: number }expirePoints()function (the daily maintenance job)
Add a // Notes: comment block at the top of the file explaining the key design decisions made (idempotency approach, expiration logic, the redemption cap, and the conversion rate used).
{
"name": "finsi/loyalty-points-system",
"version": "0.1.0",
"summary": "Points earning, redemption rules, tier progression, and expiration policies",
"skills": {
"loyalty-points-system": {
"path": "SKILL.md"
}
}
}