Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ echo '{ "token": "crp_…" }' > ~/.crawlproof.json && chmod 600 ~/.crawlproof.js

crawlproof dashboard # five live screens: ROI, Traffic, Ads, Money, Spend
crawlproof dashboard --range=1m --json | jq .roi.derived
crawlproof dashboard --json | jq '.sites[] | {site, score: .score.score}' # ranked properties
crawlproof stats example.com # sources, referrers, top pages as text
crawlproof ad https://example.com/launch
crawlproof ads pause crawlproof-ad-144
Expand All @@ -154,12 +155,21 @@ esbuild-bundles `lib/dashboard/*` and `cli/dashboard.ts` where they already
live and where the test suite already covers them, leaving hqtui and the
CoinPay SDK external. Rebuild with `node packages/cli/build.mjs`.

On the Traffic screen, `↑`/`↓` pick a property and `Enter` (or a click) opens
it: that domain's traffic and money on their own, and its **risk-to-viral
score** taken apart into the components it was built from —
`100 × viral × (1 − risk/2)`, where viral is momentum, discovery, humanity and
money, and risk is volatility, channel concentration, bot dependence and being
unmonetised. A component with no data is dropped rather than counted as a zero.
The full table is in [`packages/cli/README.md`](packages/cli/README.md) and the
formula lives in [`lib/dashboard/score.ts`](lib/dashboard/score.ts).

The endpoints underneath are the same ones anything else can call:

| Route | Answers |
| --- | --- |
| `GET /api/tracker/v1/sites` | the projects this token can read |
| `GET /api/tracker/v1/stats?site=&range=&who=` | who arrived and from where |
| `GET /api/tracker/v1/stats?site=&range=&who=&detail=1` | who arrived and from where; `detail=1` adds the series and the unfiltered human / bot mix |
| `GET /api/ads/v1/earnings?days=` | ad delivery and money, both sides |
| `GET/POST /api/ads/v1/campaigns` | list and run campaigns |
| `GET/POST /api/ads/v1/slots` | list and create publisher slots |
Expand Down
69 changes: 69 additions & 0 deletions app/(marketing)/docs/cli/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,75 @@ crawlproof dashboard --sites=example.com,blog.example.com`}
crawlproof dashboard --json | jq '.sites[] | {site, visitors}'`}</Cmd>
</section>

<section className="mt-10 space-y-3">
<h2 className="text-2xl font-bold">One property at a time</h2>
<p className="text-sm leading-relaxed">
On <strong>Traffic</strong>, <kbd className="font-mono">↑</kbd>/
<kbd className="font-mono">↓</kbd> pick a site and{" "}
<kbd className="font-mono">Enter</kbd> — or a click on the row — opens
it. That screen is only that domain: pageviews, visits, humans against
bots, AI referrals and where they arrived from, next to the burn
prorated onto it, its ad earnings and spend, and the commission when
there is exactly one merchant business to attribute it to.{" "}
<kbd className="font-mono">Esc</kbd> comes back.
</p>
<Cmd note="s cycles the order: score, visitors, pageviews.">
{`crawlproof dashboard --sort=score
crawlproof dashboard --json | jq '.sites[] | {site, score: .score.score}'`}
</Cmd>
</section>

<section className="mt-10 space-y-3">
<h2 className="text-2xl font-bold">The risk-to-viral score</h2>
<p className="text-sm leading-relaxed">
Every property is scored out of 100, shown as a column on the Traffic
list and taken apart on the domain screen. It is arithmetic over
numbers already on the screen, not a model, so a high score can always
be traced to the figure that caused it.
</p>
<pre className="overflow-x-auto rounded border border-[var(--color-border)] bg-[#0b0d10] p-3 font-mono text-xs leading-relaxed">{`score = 100 × viral × (1 − risk/2)

viral = momentum .40 + discovery .30 + humanity .20 + money .10
risk = volatility .40 + concentration .30 + bot dependence .20 + unmonetised .10`}</pre>
<ul className="list-disc space-y-1 pl-5 text-sm leading-relaxed">
<li>
<strong>momentum</strong> — human visits in the recent half of the
window against the earlier half. Flat is 0.5, doubling is 1.
</li>
<li>
<strong>discovery</strong> — the share arriving through search,
social, an AI assistant, an ad or another site&apos;s link, rather
than direct.
</li>
<li>
<strong>humanity</strong> — humans over humans plus bots, read
unfiltered. A filtered read has a zero bot column by construction
and would call a crawler farm 100% human.
</li>
<li>
<strong>money</strong> — revenue per 1,000 human visits against a $2
target.
</li>
<li>
<strong>volatility</strong> — the coefficient of variation of the
human series; scale-free, so a small site is not penalised for being
small.
</li>
<li>
<strong>concentration</strong> — the largest single arrival
channel&apos;s share. An even spread is not a risk; one channel
being everything is.
</li>
</ul>
<p className="text-sm leading-relaxed">
A component with no data behind it is dropped and its weight
redistributed, never counted as a zero. A trailing{" "}
<code className="font-mono">~</code> marks fewer than 25 human visits
in the window, and a site whose stats call failed is not scored at
all — missing is not the same as bad.
</p>
</section>

<section className="mt-10 space-y-3">
<h2 className="text-2xl font-bold">Traffic, as text</h2>
<p className="text-sm leading-relaxed">
Expand Down
13 changes: 12 additions & 1 deletion app/api/tracker/v1/stats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import { DEFAULT_WHO, parseWho, WHO_PARAM, whoToKind } from "@/lib/tracker/who";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";

/** Anything but an affirmative is off, so a typo cannot buy an extra query. */
export function parseDetail(raw: string | null): boolean {
return raw === "1" || raw === "true" || raw === "yes";
}

export async function GET(req: NextRequest) {
const auth = await authenticateBearer(req);
if (!auth.ok) return NextResponse.json({ error: auth.error }, { status: auth.status });
Expand All @@ -36,7 +41,13 @@ export async function GET(req: NextRequest) {
const resolved = await resolveProject(sb, auth.userId, sp.get("site"));
if (!resolved.ok) return NextResponse.json({ error: resolved.error }, { status: resolved.status });

// `detail=1` adds the series and the unfiltered human / bot mix, which is
// what the dashboard's per-domain screen and its risk-to-viral score are
// built from. Off by default: `crawlproof stats` prints neither, and a
// fleet-wide fan-out should not pay for a panel nobody renders.
const detail = parseDetail(sp.get("detail"));

const range = trackerRange(sp.get("range"));
const stats = await projectStats(sb, resolved.project, range, whoToKind(who), who);
const stats = await projectStats(sb, resolved.project, range, whoToKind(who), who, detail);
return NextResponse.json(stats);
}
Loading
Loading