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 '';
};
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.');