From a721a5cc40a587b2f8ba5c7a0fbd4c6572510950 Mon Sep 17 00:00:00 2001
From: 0xNyk <0xnykcd@googlemail.com>
Date: Wed, 16 Sep 2026 16:53:08 +0700
Subject: [PATCH] fix(landing): drop the first-100 free Pro offer
The offer never worked. Its claim store was never provisioned in
production, so every claim answered 500 missing_redis_config, and the
app treats a 5xx as a failed Pro check and records a verification error.
- Remove the three places the page promised it: hero, Pro plan, FAQ.
- Answer 404 early_access_closed instead of 500. Installed apps read 404
as "no grant" and keep working, so older installs stop erroring.
- Drop the now-unused styles and the Upstash and early-access env docs.
Claude-Session: https://claude.ai/code/session_01CvRJfFeFvF96jhBFujxvHN
---
landing/README.md | 9 +-
landing/api/pro/early-access/claim.js | 221 +++-----------------------
landing/index.html | 13 --
landing/styles.css | 17 --
4 files changed, 20 insertions(+), 240 deletions(-)
diff --git a/landing/README.md b/landing/README.md
index e798928..09de9bd 100644
--- a/landing/README.md
+++ b/landing/README.md
@@ -20,7 +20,7 @@ Attach `dictx.splitlabs.io` to this Vercel project.
- `/buy/success?session_id=cs_live_...` shows the license key, fetched from `/api/pro/license`
- `/api/pro/license?session_id=cs_live_...` issues the `dxp-` license key for a verified Stripe purchase
- `/api/pro/verify` validates `dxp-` keys against Stripe. Keys from the retired Polar checkout (`lk_...`, `polar_cl_...`) answer `410`, so apps that already activated one keep Pro
-- `/api/pro/early-access/claim` grants free Pro for the first 100 unique installs
+- `/api/pro/early-access/claim` answers `404 early_access_closed`. The "first 100 installs get Pro free" offer is retired: its claim store was never provisioned, so it only ever returned 500. Installed apps read 404 as "no grant" and carry on, so the route stays instead of 404ing as a missing file
- `/js/script.cookieless.js` and `/api/events` proxy DataFast first-party; `middleware.ts` reports AI crawler requests (Edge runtime: the Node.js runtime served every page as 500 on this project)
## Stripe Managed Payments
@@ -53,15 +53,10 @@ DataFast:
- `DATAFAST_WEBSITE_ID`: the public website id; enables crawler tracking in `middleware.ts`
-Rate limits and early access:
+Rate limits:
- `PRO_VERIFY_RATE_LIMIT_WINDOW_MS`: optional API rate-limit window
- `PRO_VERIFY_RATE_LIMIT_MAX`: optional API rate-limit max requests per client per window
-- `UPSTASH_REDIS_REST_URL`: Upstash REST URL for early-access claim counter
-- `UPSTASH_REDIS_REST_TOKEN`: Upstash REST token for early-access claim counter
-- `DICTX_PRO_EARLY_ACCESS_LIMIT`: optional free-claim cap (defaults to `100`)
-- `PRO_EARLY_ACCESS_RATE_LIMIT_WINDOW_MS`: optional rate-limit window for claim API
-- `PRO_EARLY_ACCESS_RATE_LIMIT_MAX`: optional rate-limit max for claim API
## Tests
diff --git a/landing/api/pro/early-access/claim.js b/landing/api/pro/early-access/claim.js
index cd97b34..bf44b4e 100644
--- a/landing/api/pro/early-access/claim.js
+++ b/landing/api/pro/early-access/claim.js
@@ -1,131 +1,26 @@
-const UPSTASH_REDIS_REST_URL = process.env.UPSTASH_REDIS_REST_URL || "";
-const UPSTASH_REDIS_REST_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN || "";
-const EARLY_ACCESS_LIMIT = Number.parseInt(
- process.env.DICTX_PRO_EARLY_ACCESS_LIMIT || "100",
- 10,
-);
-const RATE_LIMIT_WINDOW_MS = Number.parseInt(
- process.env.PRO_EARLY_ACCESS_RATE_LIMIT_WINDOW_MS || "60000",
- 10,
-);
-const RATE_LIMIT_MAX = Number.parseInt(
- process.env.PRO_EARLY_ACCESS_RATE_LIMIT_MAX || "30",
- 10,
-);
-
-const INSTALL_ID_PATTERN = /^[A-Za-z0-9._:-]{8,160}$/;
-const MAX_APP_VERSION_LENGTH = 32;
-const requestBuckets = new Map();
-
-const CLAIMS_SET_KEY = "dictx:pro:early_access:installs";
-const CLAIMS_RANK_KEY = "dictx:pro:early_access:ranks";
-
-const CLAIM_SCRIPT = `
-local set_key = KEYS[1]
-local rank_key = KEYS[2]
-local install_id = ARGV[1]
-local limit = tonumber(ARGV[2])
-local now = ARGV[3]
-
-if redis.call("SISMEMBER", set_key, install_id) == 1 then
- local rank = redis.call("ZSCORE", rank_key, install_id)
- return {1, tostring(rank or "0")}
-end
-
-local count = redis.call("SCARD", set_key)
-if count >= limit then
- return {0, tostring(count)}
-end
-
-redis.call("SADD", set_key, install_id)
-local claimed = redis.call("SCARD", set_key)
-redis.call("ZADD", rank_key, claimed, install_id)
-redis.call("HSET", "dictx:pro:early_access:meta:" .. install_id, "claimed_at", now)
-return {1, tostring(claimed)}
-`;
-
-const getHeader = (req, name) => {
- if (!req || !req.headers) return "";
- if (typeof req.headers.get === "function") {
- return req.headers.get(name) || "";
- }
- const lower = name.toLowerCase();
- return req.headers[lower] || req.headers[name] || "";
-};
-
-const readBody = async (req) => {
- if (!req) return {};
-
- if (req.body !== undefined) {
- if (typeof req.body === "string") {
- try {
- return JSON.parse(req.body);
- } catch (_error) {
- return {};
- }
- }
-
- if (typeof req.body === "object" && req.body !== null) {
- return req.body;
- }
- }
-
- if (typeof req.json === "function") {
- try {
- return await req.json();
- } catch (_error) {
- return {};
- }
- }
-
- return {};
-};
-
-const getClientId = (req) => {
- const forwarded = getHeader(req, "x-forwarded-for");
- if (Array.isArray(forwarded)) {
- return forwarded[0] || "unknown";
- }
- if (forwarded.length > 0) {
- const [first] = forwarded.split(",");
- return first.trim() || "unknown";
- }
- return req?.socket?.remoteAddress || "unknown";
-};
-
-const isRateLimited = (clientId) => {
- const now = Date.now();
- if (requestBuckets.size > 5000) {
- for (const [key, bucket] of requestBuckets.entries()) {
- if (now > bucket.resetAt) {
- requestBuckets.delete(key);
- }
- }
- }
-
- const existing = requestBuckets.get(clientId);
- if (!existing || now > existing.resetAt) {
- requestBuckets.set(clientId, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
- return false;
- }
-
- existing.count += 1;
- requestBuckets.set(clientId, existing);
- return existing.count > RATE_LIMIT_MAX;
-};
-
+/**
+ * POST /api/pro/early-access/claim
+ *
+ * The early-access offer ("first 100 installs get Pro free") is retired.
+ *
+ * It never worked: the claim store (Upstash Redis) was never provisioned in
+ * production, so every claim answered 500 missing_redis_config. Installed apps
+ * treat a 5xx as a failure and record a verification error on the Pro check,
+ * while 404 means "not granted" and lets the app carry on silently
+ * (src-tauri/src/commands/pro.rs, claim_early_access). So this endpoint stays
+ * in place and answers 404 rather than disappearing: older installs keep
+ * calling it, and they must not be shown an error for an offer we withdrew.
+ */
const sendJson = (res, statusCode, payload) => {
+ const body = JSON.stringify(payload);
+
if (res && typeof res.status === "function") {
- if (typeof res.setHeader === "function") {
- res.setHeader("cache-control", "no-store");
- res.setHeader("pragma", "no-cache");
- res.setHeader("expires", "0");
- }
+ res.setHeader("cache-control", "no-store");
res.status(statusCode).json(payload);
return null;
}
- return new Response(JSON.stringify(payload), {
+ return new Response(body, {
status: statusCode,
headers: {
"content-type": "application/json",
@@ -134,92 +29,12 @@ const sendJson = (res, statusCode, payload) => {
});
};
-const callRedisEval = async (installId) => {
- const response = await fetch(`${UPSTASH_REDIS_REST_URL}/eval`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${UPSTASH_REDIS_REST_TOKEN}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- script: CLAIM_SCRIPT,
- keys: [CLAIMS_SET_KEY, CLAIMS_RANK_KEY],
- args: [installId, String(EARLY_ACCESS_LIMIT), new Date().toISOString()],
- }),
- });
-
- if (!response.ok) {
- const detail = await response.text();
- throw new Error(`redis_eval_failed:${response.status}:${detail}`);
- }
-
- const body = await response.json();
- const result = Array.isArray(body?.result) ? body.result : [];
- const isActive = Number.parseInt(String(result[0] ?? "0"), 10) === 1;
- const rank = Number.parseInt(String(result[1] ?? "0"), 10);
- return { isActive, rank };
-};
-
const handler = async (req, res) => {
if (req.method !== "POST") {
return sendJson(res, 405, { error: "method_not_allowed" });
}
- if (!UPSTASH_REDIS_REST_URL || !UPSTASH_REDIS_REST_TOKEN) {
- return sendJson(res, 500, { error: "missing_redis_config" });
- }
-
- const clientId = getClientId(req);
- if (isRateLimited(clientId)) {
- return sendJson(res, 429, { error: "rate_limited" });
- }
-
- const body = await readBody(req);
- const installId = String(body.installId || "").trim();
- const appVersion = String(body.appVersion || "").trim();
-
- if (!installId) {
- return sendJson(res, 400, { error: "installId_required" });
- }
-
- if (!INSTALL_ID_PATTERN.test(installId)) {
- return sendJson(res, 400, { error: "invalid_install_id" });
- }
-
- if (appVersion.length > MAX_APP_VERSION_LENGTH) {
- return sendJson(res, 400, { error: "invalid_app_version" });
- }
-
- try {
- const { isActive, rank } = await callRedisEval(installId);
- const remainingRaw = EARLY_ACCESS_LIMIT - Math.max(rank, 0);
- const remaining = remainingRaw > 0 ? remainingRaw : 0;
-
- if (!isActive) {
- return sendJson(res, 200, {
- active: false,
- limit: EARLY_ACCESS_LIMIT,
- remaining,
- });
- }
-
- return sendJson(res, 200, {
- active: true,
- licenseKey: `EARLY-${installId}`,
- rank,
- limit: EARLY_ACCESS_LIMIT,
- remaining,
- });
- } catch (error) {
- console.warn("pro_early_access_claim_error", {
- clientId,
- detail: error instanceof Error ? error.message : String(error),
- });
- return sendJson(res, 500, {
- error: "internal_error",
- detail: error instanceof Error ? error.message : String(error),
- });
- }
+ return sendJson(res, 404, { error: "early_access_closed" });
};
module.exports = handler;
diff --git a/landing/index.html b/landing/index.html
index 50dda77..4cb63f0 100644
--- a/landing/index.html
+++ b/landing/index.html
@@ -96,9 +96,6 @@
Transcription you can trust in real work.
>Download Latest macOS DMG
-
- First 100 installs unlock Dictx Pro for free automatically in-app.
-
$29 one-time • signed binaries • auto-updates • macOS / Windows /
Linux
@@ -165,9 +162,6 @@
Free or Pro
Pro
$29 one-time
-
- Early access: first 100 installs get Pro free
-
- Signed desktop binaries
- In-app auto-updater
@@ -244,13 +238,6 @@ FAQ
workflow.
-
- How does the first-100 free Pro claim work?
-
- The app claims it automatically on first activation. If your install
- lands in the first 100, Pro is enabled instantly at no cost.
-
-
How do I activate Pro inside the app?
diff --git a/landing/styles.css b/landing/styles.css
index ca28d2f..8ebd77c 100644
--- a/landing/styles.css
+++ b/landing/styles.css
@@ -216,17 +216,6 @@ h1 {
font-size: 13px;
}
-.offer-pill {
- margin: 14px 0 0;
- display: inline-flex;
- align-items: center;
- border: 1px solid color-mix(in srgb, var(--color-logo-primary) 45%, transparent);
- background: color-mix(in srgb, var(--color-logo-primary) 18%, transparent);
- border-radius: 999px;
- padding: 7px 11px;
- font-size: 12px;
- color: color-mix(in srgb, var(--color-text) 88%, transparent);
-}
.hero-proof {
margin-top: 16px;
@@ -335,12 +324,6 @@ h1 {
color-mix(in srgb, var(--color-logo-primary) 18%, transparent);
}
-.plan-offer {
- margin: 6px 0 0;
- color: color-mix(in srgb, var(--color-logo-primary) 84%, white 16%);
- font-size: 12px;
- font-weight: 600;
-}
.plan-name {
margin: 0;