feat(server): remove hard paywall — credits gate scans, guests stay blocked, free tier gets pro model

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Timo Knuth
2026-07-06 12:38:12 +02:00
parent 3ebcc94349
commit e36628d49d

View File

@@ -203,17 +203,15 @@ const resolveIdempotencyKey = (request) => {
return ''; return '';
}; };
const createHardPaywallError = (requiredCredits) => { const ensureNotGuest = (userId, requiredCredits) => {
const error = new Error('Active Pro or trial entitlement required.'); // Guests use the client-side demo scan; the shared 'guest' billing account
error.code = 'INSUFFICIENT_CREDITS'; // must never consume real credits or run free AI analyses.
error.status = 402; if (isGuest(userId)) {
error.metadata = { required: requiredCredits, available: 0 }; const error = new Error('Sign in to use scan credits.');
return error; error.code = 'INSUFFICIENT_CREDITS';
}; error.status = 402;
error.metadata = { required: requiredCredits, available: 0 };
const ensureActiveProEntitlement = (accountSnapshot, requiredCredits) => { throw error;
if (!accountSnapshot || accountSnapshot.plan !== 'pro') {
throw createHardPaywallError(requiredCredits);
} }
}; };
@@ -731,7 +729,7 @@ app.post('/v1/scan', async (request, response) => {
getAccountSnapshot(db, userId), getAccountSnapshot(db, userId),
getCachedCatalogEntries(db), getCachedCatalogEntries(db),
]); ]);
ensureActiveProEntitlement(accountSnapshot, SCAN_PRIMARY_COST); ensureNotGuest(userId, SCAN_PRIMARY_COST);
creditsCharged += await consumeCreditsWithIdempotency( creditsCharged += await consumeCreditsWithIdempotency(
db, db,
userId, userId,
@@ -739,7 +737,8 @@ app.post('/v1/scan', async (request, response) => {
SCAN_PRIMARY_COST, 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 result = pickCatalogFallback(catalogEntries, imageUri, false, { silent: true });
let usedOpenAi = false; let usedOpenAi = false;
let rawPrimaryResult = null; let rawPrimaryResult = null;
@@ -903,7 +902,7 @@ app.post('/v1/search/semantic', async (request, response) => {
} }
const accountSnapshot = await getAccountSnapshot(db, userId); const accountSnapshot = await getAccountSnapshot(db, userId);
ensureActiveProEntitlement(accountSnapshot, SEMANTIC_SEARCH_COST); ensureNotGuest(userId, SEMANTIC_SEARCH_COST);
const creditsCharged = await consumeCreditsWithIdempotency( const creditsCharged = await consumeCreditsWithIdempotency(
db, db,
@@ -943,7 +942,7 @@ app.post('/v1/health-check', async (request, response) => {
} }
const accountSnapshot = await getAccountSnapshot(db, userId); const accountSnapshot = await getAccountSnapshot(db, userId);
ensureActiveProEntitlement(accountSnapshot, HEALTH_CHECK_COST); ensureNotGuest(userId, HEALTH_CHECK_COST);
if (!isOpenAiConfigured()) { if (!isOpenAiConfigured()) {
const error = new Error('OpenAI health check is unavailable. Please configure OPENAI_API_KEY.'); const error = new Error('OpenAI health check is unavailable. Please configure OPENAI_API_KEY.');