From e36628d49d8ebbec9cd0902b66a10d850283842d Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Mon, 6 Jul 2026 12:38:12 +0200 Subject: [PATCH] =?UTF-8?q?feat(server):=20remove=20hard=20paywall=20?= =?UTF-8?q?=E2=80=94=20credits=20gate=20scans,=20guests=20stay=20blocked,?= =?UTF-8?q?=20free=20tier=20gets=20pro=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- server/index.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/server/index.js b/server/index.js index 9c1667b..01ba886 100644 --- a/server/index.js +++ b/server/index.js @@ -203,17 +203,15 @@ const resolveIdempotencyKey = (request) => { return ''; }; -const createHardPaywallError = (requiredCredits) => { - const error = new Error('Active Pro or trial entitlement required.'); - error.code = 'INSUFFICIENT_CREDITS'; - error.status = 402; - error.metadata = { required: requiredCredits, available: 0 }; - return error; -}; - -const ensureActiveProEntitlement = (accountSnapshot, requiredCredits) => { - if (!accountSnapshot || accountSnapshot.plan !== 'pro') { - throw createHardPaywallError(requiredCredits); +const ensureNotGuest = (userId, requiredCredits) => { + // Guests use the client-side demo scan; the shared 'guest' billing account + // must never consume real credits or run free AI analyses. + if (isGuest(userId)) { + const error = new Error('Sign in to use scan credits.'); + error.code = 'INSUFFICIENT_CREDITS'; + error.status = 402; + error.metadata = { required: requiredCredits, available: 0 }; + throw error; } }; @@ -731,7 +729,7 @@ app.post('/v1/scan', async (request, response) => { getAccountSnapshot(db, userId), getCachedCatalogEntries(db), ]); - ensureActiveProEntitlement(accountSnapshot, SCAN_PRIMARY_COST); + ensureNotGuest(userId, SCAN_PRIMARY_COST); creditsCharged += await consumeCreditsWithIdempotency( db, userId, @@ -739,7 +737,8 @@ app.post('/v1/scan', async (request, response) => { SCAN_PRIMARY_COST, ); - const scanPlan = accountSnapshot.plan === 'pro' ? 'pro' : 'free'; + // Free tier gets the same model quality; quantity (3 credits/month) is the differentiator. + const scanPlan = 'pro'; let result = pickCatalogFallback(catalogEntries, imageUri, false, { silent: true }); let usedOpenAi = false; let rawPrimaryResult = null; @@ -903,7 +902,7 @@ app.post('/v1/search/semantic', async (request, response) => { } const accountSnapshot = await getAccountSnapshot(db, userId); - ensureActiveProEntitlement(accountSnapshot, SEMANTIC_SEARCH_COST); + ensureNotGuest(userId, SEMANTIC_SEARCH_COST); const creditsCharged = await consumeCreditsWithIdempotency( db, @@ -943,7 +942,7 @@ app.post('/v1/health-check', async (request, response) => { } const accountSnapshot = await getAccountSnapshot(db, userId); - ensureActiveProEntitlement(accountSnapshot, HEALTH_CHECK_COST); + ensureNotGuest(userId, HEALTH_CHECK_COST); if (!isOpenAiConfigured()) { const error = new Error('OpenAI health check is unavailable. Please configure OPENAI_API_KEY.');