Order Anomaly Detection
order_anomaliesIdentify fraud and revenue spikes by detecting statistical anomalies in recent orders, including high-value, velocity spikes, unusual quantities, off-hours, and new-customer high-value orders.
Instructions
Statistical anomaly detection on recent orders. Flags high-value orders (>3σ from mean), velocity spikes (customer ordering unusually fast), unusual quantities, off-hours purchases (2am-5am), and new-customer high-value orders. Returns an array of anomalies with order_id, anomaly_type, severity (low/medium/high), reason, and recommended_action. Useful for fraud detection and revenue spike investigation.
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/index.ts:266-285 (registration)Registration of the 'order_anomalies' tool with the MCP server, including schema (store_id UUID), license check, and delegation to getOrderAnomalies handler.
// ── Tool: order_anomalies ───────────────────────────────────────── server.registerTool( 'order_anomalies', { title: 'Order Anomaly Detection', description: 'Statistical anomaly detection on recent orders. Flags high-value orders (>3σ from mean), velocity spikes (customer ordering unusually fast), unusual quantities, off-hours purchases (2am-5am), and new-customer high-value orders. Returns an array of anomalies with order_id, anomaly_type, severity (low/medium/high), reason, and recommended_action. Useful for fraud detection and revenue spike investigation.', 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 reject = await ensureProOrReject(LICENSE_CONFIG, 'order_anomalies'); if (reject) return reject; const result = await getOrderAnomalies(store_id); return { content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }] }; } catch (e) { return handleToolError(e); } } ); - src/tools/orders.ts:6-33 (handler)Handler function getOrderAnomalies() that validates store, fetches orders, and calls detectAnomalies() to produce an AnomalySummary with counts per severity level.
export interface AnomalySummary { store_id: string; total_anomalies: number; critical: number; high: number; medium: number; low: number; anomalies: AnomalyResult[]; } export async function getOrderAnomalies(storeId: string): Promise<AnomalySummary> { validateUUID(storeId, 'store'); const store = await storage.getStoreById(storeId); if (!store) throw new NotFoundError('Store', storeId); const orders = await storage.getOrders(storeId); const anomalies = detectAnomalies(orders); return { store_id: storeId, total_anomalies: anomalies.length, critical: anomalies.filter((a) => a.risk_level === 'critical').length, high: anomalies.filter((a) => a.risk_level === 'high').length, medium: anomalies.filter((a) => a.risk_level === 'medium').length, low: anomalies.filter((a) => a.risk_level === 'low').length, anomalies, }; } - src/services/anomaly.ts:169-186 (helper)detectAnomalies() — the core anomaly detection engine: computes baseline stats, analyzes recent orders for high value (>3σ), unusual quantities, velocity spikes, off-hours, and new-customer high-value patterns.
export function detectAnomalies(orders: Order[]): AnomalyResult[] { if (orders.length < 5) return []; // Need baseline data const stats = computeOrderStats(orders); const results: AnomalyResult[] = []; // Only analyze recent orders (last 30 days) const cutoff = Date.now() - 30 * MS_PER_DAY; const recentOrders = orders.filter((o) => safeTimestamp(o.created_at) >= cutoff); for (const order of recentOrders) { if (order.status === 'cancelled' || order.status === 'refunded') continue; const result = analyzeOrder(order, orders, stats); if (result) results.push(result); } return results.sort((a, b) => b.risk_score - a.risk_score); } - src/services/anomaly.ts:87-164 (helper)analyzeOrder() — evaluates a single order against 6 anomaly checks, computes a risk score (0-100), maps to risk level (low/medium/high/critical), and produces recommendation action text.
function analyzeOrder(order: Order, allOrders: Order[], stats: OrderStats): AnomalyResult | null { const flags: string[] = []; const anomalyTypes: AnomalyType[] = []; let riskScore = 0; // 1. High value — more than 3 standard deviations above mean if (stats.stdDevTotal > 0 && order.total > stats.avgTotal + 3 * stats.stdDevTotal) { anomalyTypes.push('high_value'); flags.push(`Order total $${order.total.toFixed(2)} is ${((order.total - stats.avgTotal) / stats.stdDevTotal).toFixed(1)}σ above average`); riskScore += 30; } // 2. Unusual item quantity const totalQty = order.items.reduce((sum, i) => sum + i.quantity, 0); if (stats.stdDevItemQty > 0 && totalQty > stats.avgItemQty + 3 * stats.stdDevItemQty) { anomalyTypes.push('unusual_quantity'); flags.push(`${totalQty} items ordered — ${((totalQty - stats.avgItemQty) / stats.stdDevItemQty).toFixed(1)}σ above average`); riskScore += 25; } // 3. Velocity spike if (checkVelocitySpike(order, allOrders, stats)) { anomalyTypes.push('velocity_spike'); flags.push('Unusual order volume detected in a short time window'); riskScore += 20; } // 4. Off-hours ordering if (checkOffHours(order)) { anomalyTypes.push('off_hours'); flags.push('Order placed during off-hours (00:00-05:00 UTC)'); riskScore += 10; } // 5. New customer + high value if (checkNewCustomerHighValue(order, allOrders, stats)) { anomalyTypes.push('new_customer_high_value'); flags.push('First-time customer with unusually high order value'); riskScore += 25; } // 6. Missing customer info if (!order.customer_email && !order.customer_id) { flags.push('No customer email or ID associated'); riskScore += 10; } if (anomalyTypes.length === 0 && riskScore < 15) return null; riskScore = Math.min(100, riskScore); const riskLevel = riskScore <= 25 ? 'low' as const : riskScore <= 50 ? 'medium' as const : riskScore <= 75 ? 'high' as const : 'critical' as const; let action: string; if (riskLevel === 'critical') { action = 'Hold order for manual review. Verify payment method and contact customer before fulfilling.'; } else if (riskLevel === 'high') { action = 'Flag for review. Verify shipping address matches billing. Consider additional verification.'; } else if (riskLevel === 'medium') { action = 'Monitor closely. No immediate action needed but track for patterns.'; } else { action = 'Low risk. Standard processing.'; } return { order_id: order.id, order_number: order.order_number, anomaly_types: anomalyTypes, risk_score: riskScore, risk_level: riskLevel, total: order.total, customer_email: order.customer_email, flags, recommended_action: action, }; } - src/models/store.ts:165-183 (schema)Zod schemas and TypeScript types for AnomalyType (five anomaly categories) and AnomalyResult (order_id, anomaly_types, risk_score, risk_level, flags, recommended_action).
// ── Anomaly Detection ───────────────────────────────────────────── export const AnomalyTypeSchema = z.enum([ 'high_value', 'velocity_spike', 'unusual_quantity', 'off_hours', 'new_customer_high_value', ]); export type AnomalyType = z.infer<typeof AnomalyTypeSchema>; export const AnomalyResultSchema = z.object({ order_id: z.string(), order_number: z.string(), anomaly_types: z.array(AnomalyTypeSchema), risk_score: z.number().min(0).max(100), risk_level: z.enum(['low', 'medium', 'high', 'critical']), total: z.number(), customer_email: z.string().nullable(), flags: z.array(z.string()), recommended_action: z.string(), }); export type AnomalyResult = z.infer<typeof AnomalyResultSchema>;