Skip to content

Revenue was a lifetime total pretending to be a rate - #243

Merged
ralyodio merged 1 commit into
masterfrom
vendor-window-labels
Sep 6, 2026
Merged

Revenue was a lifetime total pretending to be a rate#243
ralyodio merged 1 commit into
masterfrom
vendor-window-labels

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Four bugs, three of them mine, found by looking at numbers that did not smell right.

Revenue was a lifetime total pretending to be a rate

The ROI screen reported $131/mo revenue, and on a 1d window a merchant volume of $126.8K/mo. Neither was real.

CoinPay's earnings block does not move when the window changes. Asked for 7 days and for 30 it returns byte-identical grossVolumeUsd, commissionUsd and transactions, while the bank half of the same response does change. So it is a balance, not a rate, and dividing it by a window it never covered is how a banned merchant's historical card volume became a six-figure run rate.

Revenue now comes from the day series, which is windowed. Against production:

Before After
Commission $131.06/mo $3.82/mo
Gross volume $29,591/mo $653/mo
ROI −97% −99.9%

And it no longer changes when you change the traffic range. The lifetime totals are still shown, underneath and labelled, so the big number stays visible without being read as income.

Delivery was zero everywhere, for a different reason

Also mine. The windowed delivery RPCs are security definer and filter where owner_id = auth.uid(). The service client this route passes has no auth.uid(), so they returned nothing and every impression count arrived as a confident zero. The money columns survived only because #241 added explicit owner_id filters to the direct selects.

The stats views are security_invoker and granted to service_role, so they can be read directly. Chunking at 50 matters: in.(…) for 180-odd campaign uuids is a ~7KB query string that comes back empty rather than erroring, which looks exactly like a network with no delivery. That is how the first attempt at this fix still read zero.

Production actually has 220,154 impressions, 202,848 of them free.

The views also do not mean what their column names suggest, which is now written down where it is used: impressions split paid/free and add up, but clicks is already every valid click and free_clicks is invalid free clicks. Adding those in would fold click fraud into the CTR, which is the one number it would show up in.

The Ads screen was built for the wrong stage

It led with paid delivery on a network running entirely on free backfill, which reports a working network as a dead one. It now leads with free, shows the paid/free split, surfaces invalid clicks (currently 9,762 against 83 valid), and measures progress toward 3,000,000 impressions a month at 5% CTR--target-impressions and --target-ctr override both.

It also projects what a month at target would earn, at the price actually charged. Where nothing has ever been charged it says so rather than projecting from an invented price.

Unlabelled time bases

Three different windows met on the ROI screen and only some said so. The bank window is not the traffic range in the header (bank data has no hourly resolution), the burn is a 180 day average, and the ad figures are lifetime. Every panel now names its own.

crawlproof ad <url>

Ad creation never made it into the published package, so it could not reach a box. ad <url> runs one, with ads list|show|pause|resume|budget|delete behind it.

Verified

Revenue and labels against production. Delivery against a local server on the production environment, since the route change is not deployed yet. Full suite green: 2,134 tests (24 in the ROI module), tsc --noEmit clean.

The seven queued announcements for the original launch were cancelled before they published the wrong revenue figure.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HvWJ4336pxTFRdRbvsTQeD

The dashboard reported $131/mo of revenue and, on a 1d window, a merchant
volume of $126.8K/mo. Neither was real.

CoinPay's `earnings` block does not move when the window changes. Asked for 7
days and for 30 it returns byte-identical grossVolumeUsd, commissionUsd and
transaction counts, while the bank half of the same response does change. So it
is a balance, not a rate, and dividing it by a window it never covered is how a
banned merchant's historical card volume became a six-figure run rate.

Revenue now comes from the day series, which is windowed. Against production
that moves commission from $131.06/mo to $3.82/mo and gross volume from
$29,591/mo to $653/mo, and the figure stops changing when you change the
traffic range. The lifetime totals are still shown, underneath and labelled, so
the big number is visible without being read as income.

Delivery was zero everywhere for a different reason, and also mine. The
windowed delivery RPCs are `security definer` and filter on `auth.uid()`, which
is NULL for the service client this route passes, so they returned nothing and
every impression count arrived as a confident zero. The stats views are
`security_invoker` and granted to service_role, so they can be read directly.
Reading them in chunks of 50 matters: `in.(…)` for 180-odd campaign uuids is a
7KB query string that comes back empty rather than erroring, which looks
exactly like a network with no delivery. Production actually has 220,154
impressions, 202,848 of them free.

The views do not mean what their column names suggest. Impressions split
paid/free and add up; `clicks` is already every valid click and `free_clicks`
is invalid free clicks. Adding those in would fold click fraud into the CTR.

The Ads screen led with paid delivery on a network that runs entirely on free
backfill, which reported a working network as a dead one. It now leads with
free, and measures progress toward 3,000,000 impressions a month at 5% CTR
(--target-impressions, --target-ctr), projecting what a month at target earns
at the price actually charged. Where nothing has ever been charged it says so
rather than projecting from an invented price.

And every panel that mixes time bases now names its own: the bank window is not
the traffic range, the burn is a 180 day average, the ad figures are lifetime.

`crawlproof ad <url>` runs an ad from the box, with the rest of `ads` behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvWJ4336pxTFRdRbvsTQeD
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

39 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 28 | LOW: 9

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:48
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment thread packages/cli/src/cli.ts
Comment on lines +235 to +239
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
...(body ? { "content-type": "application/json" } : {}),
},
@ralyodio
ralyodio merged commit ed9f1c0 into master Sep 6, 2026
10 checks passed
@ralyodio ralyodio mentioned this pull request Sep 6, 2026
ralyodio added a commit that referenced this pull request Sep 6, 2026
The revenue fix, the delivery fix and `crawlproof ad <url>` landed in #243, but
the published version did not move with them: `crawlproof update` fetches
@latest, so until this ships every box keeps reinstalling 0.1.0 and the
dashboard keeps reporting a lifetime total as a monthly rate.

This was pushed to #243's branch before the merge and the squash did not carry
it, the same way #240 dropped bin/crawlproof.mjs. Landing it on its own.


Claude-Session: https://claude.ai/code/session_01HvWJ4336pxTFRdRbvsTQeD

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants