Customer Segmentation
customers_segmentSegment customers using RFM analysis into categories like Champions, At Risk, and Lost with actionable recommendations to improve retention and targeting.
Instructions
RFM (Recency, Frequency, Monetary) customer segmentation. Categorizes customers into segments: Champions, Loyal, Potential, At Risk, New, Hibernating, Lost — with actionable recommendations.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| store_id | Yes | UUID of a connected store (returned by store_connect with action="connect" or visible in store_connect with action="list" / the store_overview resource) |
Implementation Reference
- src/tools/customers.ts:13-43 (handler)Main handler for customers_segment tool. Validates store, retrieves customers and orders from storage, runs RFM segmentation via segmentCustomers(), and returns a SegmentSummary with aggregated segment stats and per-customer RFM results.
export async function getCustomerSegments(storeId: string): Promise<SegmentSummary> { validateUUID(storeId, 'store'); const store = await storage.getStoreById(storeId); if (!store) throw new NotFoundError('Store', storeId); const customers = await storage.getCustomers(storeId); const orders = await storage.getOrders(storeId); const results = segmentCustomers(customers, orders); // Build segment summary const segments: Record<string, { count: number; total_spent: number; avg_rfm: number }> = {}; for (const r of results) { if (!segments[r.segment]) { segments[r.segment] = { count: 0, total_spent: 0, avg_rfm: 0 }; } segments[r.segment].count++; segments[r.segment].total_spent += r.total_spent; segments[r.segment].avg_rfm += r.rfm_score; } for (const seg of Object.values(segments)) { seg.avg_rfm = seg.count > 0 ? Math.round((seg.avg_rfm / seg.count) * 100) / 100 : 0; seg.total_spent = Math.round(seg.total_spent * 100) / 100; } return { store_id: storeId, total_customers: results.length, segments, customers: results, }; } - src/services/rfm.ts:45-55 (helper)Determines RFM segment based on recency/frequency/monetary quintile scores. Maps to segments: champions, loyal, potential, new, at_risk, hibernating, lost.
export function determineSegment(r: number, f: number, m: number): RFMSegment { const avg = (r + f + m) / 3; if (r >= 4 && f >= 4 && m >= 4) return 'champions'; if (r >= 3 && f >= 4 && m >= 3) return 'loyal'; if (r >= 4 && f <= 2) return 'new'; if (r >= 3 && f >= 2 && avg >= 3) return 'potential'; if (r <= 2 && f >= 3) return 'at_risk'; if (r <= 2 && f <= 2 && m <= 2) return 'lost'; return 'hibernating'; } - src/services/rfm.ts:76-146 (helper)Core RFM segmentation logic. Computes recency (days since last order), frequency (order count), monetary (total spent) for each customer, scores them via percentile quintiles, and returns sorted RFMResult array.
export function segmentCustomers(customers: Customer[], orders: Order[]): RFMResult[] { if (customers.length === 0) return []; const now = Date.now(); // Build per-customer order data from orders const customerOrders = new Map<string, { lastOrderTs: number; orderCount: number; totalSpent: number }>(); for (const order of orders) { if (order.status === 'cancelled' || order.status === 'refunded') continue; if (!order.customer_id) continue; const existing = customerOrders.get(order.customer_id); const orderTs = new Date(order.created_at).getTime(); if (existing) { existing.lastOrderTs = Math.max(existing.lastOrderTs, orderTs); existing.orderCount++; existing.totalSpent += order.total; } else { customerOrders.set(order.customer_id, { lastOrderTs: orderTs, orderCount: 1, totalSpent: order.total, }); } } // Calculate raw RFM values const rfmRaw: Array<{ customer: Customer; recencyDays: number; frequency: number; monetary: number }> = []; for (const customer of customers) { const orderData = customerOrders.get(customer.id); const lastOrderTs = orderData?.lastOrderTs ?? (customer.last_order_at ? new Date(customer.last_order_at).getTime() : 0); const recencyDays = lastOrderTs > 0 ? Math.round((now - lastOrderTs) / MS_PER_DAY) : 999; const frequency = orderData?.orderCount ?? customer.total_orders; const monetary = orderData?.totalSpent ?? customer.total_spent; if (frequency === 0 && monetary === 0) continue; // Skip customers with no orders rfmRaw.push({ customer, recencyDays, frequency, monetary }); } if (rfmRaw.length === 0) return []; // Score each dimension (1-5) const recencyScores = quintileScores(rfmRaw.map((r) => r.recencyDays), false); const frequencyScores = quintileScores(rfmRaw.map((r) => r.frequency), true); const monetaryScores = quintileScores(rfmRaw.map((r) => r.monetary), true); return rfmRaw.map((entry, i) => { const r = recencyScores[i]; const f = frequencyScores[i]; const m = monetaryScores[i]; const segment = determineSegment(r, f, m); return { customer_id: entry.customer.id, customer_name: entry.customer.name, customer_email: entry.customer.email, recency_score: r, frequency_score: f, monetary_score: m, rfm_score: Math.round(((r + f + m) / 3) * 100) / 100, segment, total_orders: entry.frequency, total_spent: Math.round(entry.monetary * 100) / 100, last_order_days_ago: entry.recencyDays, recommended_action: segmentAction(segment), }; }).sort((a, b) => b.rfm_score - a.rfm_score); } - src/models/store.ts:109-130 (schema)Type definitions for RFM segmentation: RFMSegment enum (champions/loyal/potential/at_risk/new/hibernating/lost) and RFMResult schema with score fields and recommended_action.
// ── RFM Segmentation ────────────────────────────────────────────── export const RFMSegmentSchema = z.enum([ 'champions', 'loyal', 'potential', 'at_risk', 'new', 'hibernating', 'lost', ]); export type RFMSegment = z.infer<typeof RFMSegmentSchema>; export const RFMResultSchema = z.object({ customer_id: z.string(), customer_name: z.string(), customer_email: z.string(), recency_score: z.number().int().min(1).max(5), frequency_score: z.number().int().min(1).max(5), monetary_score: z.number().int().min(1).max(5), rfm_score: z.number(), segment: RFMSegmentSchema, total_orders: z.number().int(), total_spent: z.number(), last_order_days_ago: z.number().int(), recommended_action: z.string(), }); export type RFMResult = z.infer<typeof RFMResultSchema>; - src/index.ts:226-243 (registration)Registration of the customers_segment tool with the MCP server, including title, description, inputSchema (store_id UUID), annotations, and handler that calls getCustomerSegments().
// ── Tool: customers_segment ─────────────────────────────────────── server.registerTool( 'customers_segment', { title: 'Customer Segmentation', description: 'RFM (Recency, Frequency, Monetary) customer segmentation. Categorizes customers into segments: Champions, Loyal, Potential, At Risk, New, Hibernating, Lost — with actionable recommendations.', inputSchema: z.object({ store_id: z.string().uuid().describe('UUID of a connected store (returned by store_connect with action="connect" or visible in store_connect with action="list" / the store_overview resource)'), }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async ({ store_id }) => { try { const result = await getCustomerSegments(store_id); return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; } catch (e) { return handleToolError(e); } } );