diff --git a/src/monetization/upsell.ts b/src/monetization/upsell.ts new file mode 100644 index 0000000..fb049cf --- /dev/null +++ b/src/monetization/upsell.ts @@ -0,0 +1,78 @@ +/** + * Auto-Upsell Trigger — Issue #3 + * Contextual upgrade prompt triggered upon reaching 50% free call threshold + */ + +export interface UpsellTrigger { + userId: string; + triggerType: 'midpoint_free_tier' | 'limit_reached'; + shownAt: Date; + converted: boolean; + variant: 'urgency' | 'value' | 'social_proof'; + promptText: string; +} + +export interface UpsellDatabase { + triggers: Map; +} + +export function createUpsellDatabase(): UpsellDatabase { + return { + triggers: new Map() + }; +} + +export const UPSELL_PROMPTS = { + urgency: "You've used 5 of your 10 free calls! Upgrade now to avoid interruption.", + value: "Unlock unlimited priority execution and lower latency by upgrading to Standard tier.", + social_proof: "Join top AI agents processing thousands of daily API calls with zero rate-limits." +}; + +export function getUpsellPromptVariant(userId: string): { variant: 'urgency' | 'value' | 'social_proof'; text: string } { + const hash = userId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); + const variants: Array<'urgency' | 'value' | 'social_proof'> = ['urgency', 'value', 'social_proof']; + const variant = variants[hash % variants.length]; + return { + variant, + text: UPSELL_PROMPTS[variant] + }; +} + +export function checkAndTriggerUpsell( + userId: string, + currentCallCount: number, + db: UpsellDatabase, + threshold = 5 +): { shouldPrompt: boolean; headers: Record; promptText?: string } { + // Only trigger at threshold crossing + if (currentCallCount < threshold) { + return { shouldPrompt: false, headers: {} }; + } + + // Idempotency: verify if already triggered for this user & triggerType + const triggerKey = `${userId}:midpoint_free_tier`; + if (db.triggers.has(triggerKey)) { + return { shouldPrompt: false, headers: {} }; + } + + const { variant, text } = getUpsellPromptVariant(userId); + + db.triggers.set(triggerKey, { + userId, + triggerType: 'midpoint_free_tier', + shownAt: new Date(), + converted: false, + variant, + promptText: text + }); + + return { + shouldPrompt: true, + headers: { + 'X-Upsell-Prompt': 'true', + 'X-Upsell-Variant': variant, + 'X-Upsell-Message': text + }, + promptText: text + }; +} diff --git a/tests/upsell.test.ts b/tests/upsell.test.ts new file mode 100644 index 0000000..ef18c56 --- /dev/null +++ b/tests/upsell.test.ts @@ -0,0 +1,35 @@ +import { assertEquals } from 'https://deno.land/std@0.224.0/assert/mod.ts'; +import { + createUpsellDatabase, + checkAndTriggerUpsell, + getUpsellPromptVariant +} from '../src/monetization/upsell.ts'; + +Deno.test('Upsell: does not trigger before threshold (e.g. call 4)', () => { + const db = createUpsellDatabase(); + const res = checkAndTriggerUpsell('usr_alpha', 4, db); + assertEquals(res.shouldPrompt, false); + assertEquals(Object.keys(res.headers).length, 0); +}); + +Deno.test('Upsell: triggers exactly at threshold (call 5) with headers', () => { + const db = createUpsellDatabase(); + const res = checkAndTriggerUpsell('usr_alpha', 5, db); + assertEquals(res.shouldPrompt, true); + assertEquals(res.headers['X-Upsell-Prompt'], 'true'); + assertEquals(typeof res.promptText, 'string'); +}); + +Deno.test('Upsell: idempotent — does not trigger twice for same user', () => { + const db = createUpsellDatabase(); + checkAndTriggerUpsell('usr_alpha', 5, db); + const secondCheck = checkAndTriggerUpsell('usr_alpha', 6, db); + assertEquals(secondCheck.shouldPrompt, false); +}); + +Deno.test('Upsell: prompt variant distribution', () => { + const v1 = getUpsellPromptVariant('usr_1'); + const v2 = getUpsellPromptVariant('usr_2'); + assertEquals(typeof v1.text, 'string'); + assertEquals(typeof v2.text, 'string'); +});