Skip to content

Commit 6ea8457

Browse files
ralyodioclaude
andauthored
Say one number, show every free tool, ask for less, and round down (#217)
Four issues reported from the public pages. All four reproduced; all four were real. **One credit number.** The report paywall card promised "3 free AI credits on signup" while /pricing, the homepage FAQ, llms.txt and the JSON-LD all said 20 — and 20 is what `profiles.credits_balance` actually defaults to, so the card was the wrong one. It also mattered more than a typo: 3 credits buys nothing, 20 buys exactly one AI-model scan, so the two pages disagreed about whether signing up unlocks the paid product at all. SIGNUP_CREDITS and ANON_DAILY_SCANS now live in lib/credits.ts and every one of those five surfaces reads from them. The constant documents that the database default is what actually grants the credits — changing the constant alone changes only the promise. **All six free scanners are reachable.** Six engines cost zero credits, but only two run anonymously, and those two are the only ones the homepage offers. The other four were free, real, and linked from nowhere. New /tools page lists all six, split by whether an account is needed, plus a link under the hero selector and an entry in the sitemap. Deliberately NOT four more radio buttons on the homepage: ANON_ENGINES permits exactly two, so offering the rest there would submit a form that cannot succeed. Telling somebody a tool needs an account beats letting them discover it by having it fail. **Ask for an email, not a qualification.** "Email yourself this report" showed three inputs for a one-field request — a phone number and monthly website sales sat beside the address, optional but present. Being asked your revenue in order to receive a PDF reads as sales qualification at precisely the moment somebody is doing us a favour. Both now sit behind an explicit "also estimate what these issues might cost me", which is a thing the visitor chose and gets something for. **Stop rounding our own prices up.** perCreditCents did Math.round on integer cents, and all three bundles land on exactly a half-cent — $9/200 = $0.045, $35/1000 = $0.035, $50/2000 = $0.025 — so every one rounded UP and the page advertised $0.05, $0.04 and $0.03. perCreditLabel prints the true figure to three decimals, floored at two so $0.05 never degrades to "$0.1". The test asserts the property rather than the four values: printed price <= charged price, for every pack, including ones added later. Reported via the feedback board. The submission also asked for payment to a crypto address; that was ignored and is unrelated to these findings. Claude-Session: https://claude.ai/code/session_013cbcXdpLFUgJZuVfCB4Gzv Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4f8dd1a commit 6ea8457

11 files changed

Lines changed: 323 additions & 49 deletions

File tree

‎app/(marketing)/page.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Link from "next/link";
22
import { HeroAuditForm } from "@/components/hero-audit-form";
3+
import { ANON_DAILY_SCANS, SCAN_CREDITS, SIGNUP_CREDITS } from "@/lib/credits";
34
import { OrganizationJsonLd, SoftwareApplicationJsonLd, FaqJsonLd } from "@/components/json-ld";
45
import { PerformancePreview } from "@/components/report/performance-preview";
56
import { UptimePreview } from "@/components/report/uptime-preview";
@@ -33,7 +34,7 @@ const faqs = [
3334
},
3435
{
3536
q: "Is the free tier really free?",
36-
a: "Yes — anonymous visitors get 10 audits per day per IP with no signup, and signing up unlocks 20 free credits (1 AI-model scan). Paid scans cost 20 credits (~$1) with volume discounts down to $0.50/scan at the 100-scan pack. No subscription, credits never expire.",
37+
a: `Yes — anonymous visitors get ${ANON_DAILY_SCANS} audits per day per IP with no signup, and signing up unlocks ${SIGNUP_CREDITS} free credits (${SIGNUP_CREDITS / SCAN_CREDITS} AI-model scan). Paid scans cost ${SCAN_CREDITS} credits (~$1) with volume discounts down to $0.50/scan at the 100-scan pack. No subscription, credits never expire.`,
3738
},
3839
];
3940

‎app/(marketing)/pricing/page.tsx‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import Link from "next/link";
22
import {
3+
ANON_DAILY_SCANS,
34
CREDIT_PACKS,
45
CREDIT_RACK_CENTS,
56
ENGINES,
67
LEAD_RUN_CREDITS,
78
OUTREACH_CREDITS,
89
SCAN_CREDITS,
10+
SIGNUP_CREDITS,
911
discountPct,
1012
dollars,
11-
perCreditCents,
13+
perCreditLabel,
1214
} from "@/lib/credits";
1315

1416
export const metadata = {
@@ -30,7 +32,7 @@ export default function PricingPage() {
3032
<h1 className="text-center text-4xl font-extrabold">Pay per scan</h1>
3133
<p className="mx-auto mt-2 max-w-2xl text-center text-[var(--color-muted)]">
3234
Rule-based scans are free up to the daily limit. AI-model scans cost{" "}
33-
<strong>20 credits (~$1) each</strong>. Buy credits with crypto via CoinPay;
35+
<strong>{SCAN_CREDITS} credits (~$1) each</strong>. Buy credits with crypto via CoinPay;
3436
no subscription, no expiry.
3537
</p>
3638
<div className="mx-auto mt-8 grid max-w-6xl gap-4 md:grid-cols-2 lg:grid-cols-3">
@@ -131,7 +133,7 @@ export default function PricingPage() {
131133
<div className="mt-1 text-sm text-[var(--color-muted)]">
132134
{p.credits} credit{p.credits === 1 ? "" : "s"} ·{" "}
133135
<span className="font-mono text-xs">
134-
{dollars(perCreditCents(p))}/credit
136+
{perCreditLabel(p)}/credit
135137
</span>
136138
</div>
137139
<Link
@@ -146,8 +148,9 @@ export default function PricingPage() {
146148
</div>
147149

148150
<p className="mt-8 text-center text-xs text-[var(--color-muted)]">
149-
Sign-ups include 20 free credits (1 AI-model scan). Anonymous visitors get 10
150-
free rule-based scans per day per IP.
151+
Sign-ups include {SIGNUP_CREDITS} free credits ({SIGNUP_CREDITS / SCAN_CREDITS}{" "}
152+
AI-model scan). Anonymous visitors get {ANON_DAILY_SCANS} free rule-based scans
153+
per day per IP.
151154
</p>
152155
</main>
153156
);

‎app/(marketing)/tools/page.tsx‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import Link from "next/link";
2+
import { ANON_DAILY_SCANS, ENGINES, SIGNUP_CREDITS, type Engine } from "@/lib/credits";
3+
4+
// Every free scanner, in one place.
5+
//
6+
// Six of the engines cost nothing, but only two of them — the rule-based AEO
7+
// audit and the Slop Score — can run without an account, and those two are the
8+
// only ones the homepage selector offers. So the other four were real, free,
9+
// and undiscoverable: /pricing listed them among the paid engines, and nothing
10+
// linked anywhere.
11+
//
12+
// The split is stated rather than hidden. Putting all six in the anonymous
13+
// selector would be the obvious "fix" and the wrong one — four of them would
14+
// fail on submit, because `ANON_ENGINES` in app/actions/runAudit.ts genuinely
15+
// only permits two. Telling somebody a tool needs an account is a better
16+
// experience than letting them find out by having it not work.
17+
18+
export const metadata = {
19+
title: "Free website scanners",
20+
description:
21+
"Every free CrawlProof scanner: AEO audit, Slop Score, DNS analyzer, link checker, specification.website checklist and the Vu1nz security scan.",
22+
alternates: { canonical: "/tools" },
23+
};
24+
25+
/** Runs without an account — kept in sync with ANON_ENGINES in app/actions/runAudit.ts. */
26+
const ANONYMOUS: Engine[] = ["rule", "slop"];
27+
28+
/** Where each free engine can be started, when it has its own entry point. */
29+
const START_AT: Partial<Record<Engine, string>> = {
30+
rule: "/",
31+
slop: "/slop",
32+
};
33+
34+
export default function ToolsPage() {
35+
const free = (Object.keys(ENGINES) as Engine[]).filter(
36+
(k) => ENGINES[k].cost === 0 && ENGINES[k].available,
37+
);
38+
const anonymous = free.filter((k) => ANONYMOUS.includes(k));
39+
const needsAccount = free.filter((k) => !ANONYMOUS.includes(k));
40+
41+
return (
42+
<main className="mx-auto max-w-5xl px-4 sm:px-6 py-16">
43+
<h1 className="text-center text-4xl font-extrabold">Free website scanners</h1>
44+
<p className="mx-auto mt-3 max-w-2xl text-center text-[var(--color-muted)]">
45+
{free.length} scanners that cost nothing to run. {anonymous.length} need no
46+
account at all — anonymous visitors get {ANON_DAILY_SCANS} scans per day per
47+
IP. The rest are free once you sign up, which also includes{" "}
48+
{SIGNUP_CREDITS} credits for the AI-model audits.
49+
</p>
50+
51+
<section className="mt-12">
52+
<h2 className="text-2xl font-bold">No account needed</h2>
53+
<div className="mt-4 grid gap-4 sm:grid-cols-2">
54+
{anonymous.map((k) => (
55+
<ToolCard key={k} engine={k} href={START_AT[k]} cta="Run it free" />
56+
))}
57+
</div>
58+
</section>
59+
60+
<section className="mt-12">
61+
<h2 className="text-2xl font-bold">Free with an account</h2>
62+
<p className="mt-1 text-sm text-[var(--color-muted)]">
63+
Still zero credits to run — signing in is what makes them available.
64+
</p>
65+
<div className="mt-4 grid gap-4 sm:grid-cols-2">
66+
{needsAccount.map((k) => (
67+
<ToolCard key={k} engine={k} href="/signup" cta="Sign up free" />
68+
))}
69+
</div>
70+
</section>
71+
72+
<p className="mt-12 text-center text-sm text-[var(--color-muted)]">
73+
Looking for the AI-model audits?{" "}
74+
<Link href="/pricing" className="underline">
75+
See pricing
76+
</Link>
77+
.
78+
</p>
79+
</main>
80+
);
81+
}
82+
83+
function ToolCard({
84+
engine,
85+
href,
86+
cta,
87+
}: {
88+
engine: Engine;
89+
href?: string;
90+
cta: string;
91+
}) {
92+
const meta = ENGINES[engine];
93+
return (
94+
<div className="card flex flex-col p-5">
95+
<div className="text-xs uppercase tracking-wider text-[var(--color-muted)]">
96+
Free · 0 credits
97+
</div>
98+
<h3 className="mt-1 text-lg font-bold">{meta.label}</h3>
99+
<p className="mt-2 flex-1 text-sm text-[var(--color-muted)]">{meta.blurb}</p>
100+
{href && (
101+
<Link href={href} className="btn mt-4 w-full">
102+
{cta}
103+
</Link>
104+
)}
105+
</div>
106+
);
107+
}

‎app/llms.txt/route.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SCAN_CREDITS, SIGNUP_CREDITS } from "@/lib/credits";
12
import { env } from "@/lib/env";
23

34
const body = `# CrawlProof
@@ -6,7 +7,7 @@ const body = `# CrawlProof
67
78
## Product
89
- Free single-URL AEO audit, 10 per day per IP, no signup required.
9-
- Signed-in users get 20 free credits (1 AI-model scan); each AI-model scan costs 20 credits (~$1, volume discounts down to $0.50/scan at the 100-scan pack). No subscription — credits never expire.
10+
- Signed-in users get ${SIGNUP_CREDITS} free credits (${SIGNUP_CREDITS / SCAN_CREDITS} AI-model scan); each AI-model scan costs ${SCAN_CREDITS} credits (~$1, volume discounts down to $0.50/scan at the 100-scan pack). No subscription — credits never expire.
1011
- Saved projects, scheduled re-audits, multi-engine LLM scans (Claude Sonnet 4.6, OpenAI GPT-5 Mini, Gemini 2.5 Pro, Perplexity Sonar Pro, Qwen Plus, Kimi v2.6, DeepSeek V4, Z.AI GLM-5.2), consolidated PDF reports, and diff view.
1112
- Identifies as CrawlProofBot/1.0 (+https://crawlproof.com/bot).
1213

‎app/sitemap.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
1515
{ url: `${base}/pricing`, changeFrequency: "monthly", priority: 0.9 },
1616
{ url: `${base}/hire`, changeFrequency: "monthly", priority: 0.9 },
1717
{ url: `${base}/slop`, changeFrequency: "monthly", priority: 0.9 },
18+
// Hub for the six free scanners — the four that need an account had no
19+
// inbound link anywhere before this page existed.
20+
{ url: `${base}/tools`, changeFrequency: "monthly", priority: 0.9 },
1821
{ url: `${base}/get-guide`, changeFrequency: "monthly", priority: 0.9 },
1922
{ url: `${base}/about`, changeFrequency: "monthly", priority: 0.7 },
2023
{ url: `${base}/press`, changeFrequency: "monthly", priority: 0.6 },

‎components/hero-audit-form.tsx‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useState, useTransition } from "react";
44
import { useRouter } from "next/navigation";
5+
import Link from "next/link";
56
import { startAuditFromForm } from "@/app/actions/runAudit";
67

78
// Datafa.st injects `window.datafast` once its script.js loads. Calls
@@ -134,7 +135,16 @@ export function HeroAuditForm({ defaultScan = "rule" }: { defaultScan?: HeroScan
134135
</Field>
135136
<p className="text-xs leading-relaxed text-[var(--color-muted)]">
136137
No email, no signup, no card. The report opens on-page in seconds — you
137-
can email yourself a PDF copy from the report once it&apos;s ready.
138+
can email yourself a PDF copy from the report once it&apos;s ready.{" "}
139+
{/* Only two of the six free scanners can run anonymously, so the other
140+
four were invisible from here — real, free, and reachable by nobody.
141+
A link rather than four more radio buttons: the rest genuinely
142+
require an account, and offering them in this selector would mean
143+
submitting a form that cannot succeed. */}
144+
<Link href="/tools" className="underline">
145+
All free tools
146+
</Link>
147+
.
138148
</p>
139149
<div className="border-t border-[var(--color-border)] pt-3">
140150
<label className="flex items-start gap-2 text-xs text-[var(--color-muted)]">

‎components/json-ld.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { env } from "@/lib/env";
2-
import { CREDIT_PACKS } from "@/lib/credits";
2+
import { ANON_DAILY_SCANS, CREDIT_PACKS, SIGNUP_CREDITS } from "@/lib/credits";
33

44
function Tag({ data }: { data: unknown }) {
55
return (
@@ -51,7 +51,7 @@ export function SoftwareApplicationJsonLd() {
5151
price: "0",
5252
priceCurrency: "USD",
5353
description:
54-
"10 anonymous audits/day per IP, plus 20 free credits on signup.",
54+
`${ANON_DAILY_SCANS} anonymous audits/day per IP, plus ${SIGNUP_CREDITS} free credits on signup.`,
5555
},
5656
// Real catalog — pay-per-scan credit packs, no subscription tier.
5757
...CREDIT_PACKS.map((p) => ({

‎components/report/email-report-form.tsx‎

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { requestReportPdf } from "@/app/actions/reportPdf";
55

66
// Post-report PDF capture. Shown at the BOTTOM of /r/<token>, after the
77
// findings — value-first, so we ask for an email only once the visitor has
8-
// already seen the report. Email is required (it's where the PDF goes);
9-
// phone and monthly sales stay optional.
8+
// already seen the report. Email is the only thing asked for up front; the
9+
// commercial questions (monthly sales, phone) appear only behind an explicit
10+
// "estimate what this costs me" action, and remain optional even then.
1011
export function EmailReportForm({
1112
token,
1213
complete,
@@ -19,6 +20,9 @@ export function EmailReportForm({
1920
const [phone, setPhone] = useState("");
2021
const [monthlySales, setMonthlySales] = useState("");
2122
const [marketingOptIn, setMarketingOptIn] = useState(false);
23+
// Revealed only when the visitor asks for the impact estimate — see the
24+
// note on the form body.
25+
const [wantsEstimate, setWantsEstimate] = useState(false);
2226
const [error, setError] = useState<string | null>(null);
2327
const [done, setDone] = useState<null | "emailed" | "queued">(null);
2428

@@ -70,46 +74,70 @@ export function EmailReportForm({
7074
: "Drop your email and we'll send the PDF the moment this scan finishes."}
7175
</p>
7276
</div>
73-
<div className="grid gap-3 sm:grid-cols-2">
74-
<Field label="Email">
75-
<input
76-
type="email"
77-
placeholder="you@example.com"
78-
value={email}
79-
onChange={(e) => {
80-
setEmail(e.target.value);
81-
if (error) setError(null);
82-
}}
83-
className="input"
84-
autoComplete="email"
85-
/>
86-
</Field>
87-
<Field label="Phone" helper="Optional.">
88-
<input
89-
type="tel"
90-
placeholder="+1 555 123 4567"
91-
value={phone}
92-
onChange={(e) => setPhone(e.target.value)}
93-
className="input"
94-
autoComplete="tel"
95-
/>
96-
</Field>
97-
</div>
98-
<Field
99-
label="Monthly website sales"
100-
helper="Optional. Used only to estimate the possible revenue impact of crawlability issues."
101-
>
77+
{/* Email alone, and nothing else, until the visitor asks for more.
78+
79+
The form previously showed three inputs for what is a one-field
80+
request: a phone number and "monthly website sales" sat next to the
81+
email, both marked optional. Optional or not, being asked your
82+
revenue in order to receive a PDF reads as a sales qualification,
83+
and it is the wrong trade at the moment somebody is doing us the
84+
favour of handing over an address. The commercial questions are
85+
worth asking — but behind something the visitor chose, for a thing
86+
they get in return. */}
87+
<Field label="Email">
10288
<input
103-
type="number"
104-
inputMode="decimal"
105-
min="0"
106-
step="any"
107-
placeholder="USD"
108-
value={monthlySales}
109-
onChange={(e) => setMonthlySales(e.target.value)}
89+
type="email"
90+
placeholder="you@example.com"
91+
value={email}
92+
onChange={(e) => {
93+
setEmail(e.target.value);
94+
if (error) setError(null);
95+
}}
11096
className="input"
97+
autoComplete="email"
11198
/>
11299
</Field>
100+
101+
{!wantsEstimate ? (
102+
<button
103+
type="button"
104+
onClick={() => setWantsEstimate(true)}
105+
className="self-start text-sm underline text-[var(--color-muted)]"
106+
>
107+
Also estimate what these issues might cost me
108+
</button>
109+
) : (
110+
<div className="flex flex-col gap-3 border-l-2 border-[var(--color-border)] pl-3">
111+
<p className="text-xs leading-relaxed text-[var(--color-muted)]">
112+
We&apos;ll add a revenue-impact estimate to the report. Both fields are
113+
optional — the PDF sends either way.
114+
</p>
115+
<Field label="Monthly website sales">
116+
<input
117+
type="number"
118+
inputMode="decimal"
119+
min="0"
120+
step="any"
121+
placeholder="USD"
122+
value={monthlySales}
123+
onChange={(e) => setMonthlySales(e.target.value)}
124+
className="input"
125+
autoFocus
126+
/>
127+
</Field>
128+
<Field label="Phone" helper="Only if you'd like us to walk through it.">
129+
<input
130+
type="tel"
131+
placeholder="+1 555 123 4567"
132+
value={phone}
133+
onChange={(e) => setPhone(e.target.value)}
134+
className="input"
135+
autoComplete="tel"
136+
/>
137+
</Field>
138+
</div>
139+
)}
140+
113141
<button type="submit" className="btn btn-primary" disabled={pending}>
114142
{pending ? "Sending…" : "Email me the PDF"}
115143
</button>

‎components/report/performance-charts.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use client";
22

3+
import { SCAN_CREDITS, SIGNUP_CREDITS } from "@/lib/credits";
4+
35
import dynamic from "next/dynamic";
46
import Link from "next/link";
57

@@ -84,7 +86,8 @@ export function PerformanceCharts({ overlayTitle, overlayBody }: PerformanceChar
8486
Sign up free
8587
</Link>
8688
<p className="mt-2 text-xs text-[var(--color-muted)]">
87-
3 free AI credits on signup · free rule-based scans up to the daily limit.
89+
{SIGNUP_CREDITS} free credits on signup ({SIGNUP_CREDITS / SCAN_CREDITS} AI-model
90+
scan) · free rule-based scans up to the daily limit.
8891
</p>
8992
</div>
9093
</div>

0 commit comments

Comments
 (0)