Revenue was a lifetime total pretending to be a rate - #243
Merged
Conversation
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
ThreatCrush Security Scan39 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 28 | LOW: 9
Snippets are redacted; ThreatCrush never prints matched credential material. |
Comment on lines
+235
to
+239
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| Accept: "application/json", | ||
| ...(body ? { "content-type": "application/json" } : {}), | ||
| }, |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
1dwindow a merchant volume of $126.8K/mo. Neither was real.CoinPay's
earningsblock does not move when the window changes. Asked for 7 days and for 30 it returns byte-identicalgrossVolumeUsd,commissionUsdandtransactions, 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:
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 definerand filterwhere owner_id = auth.uid(). The service client this route passes has noauth.uid(), so they returned nothing and every impression count arrived as a confident zero. The money columns survived only because #241 added explicitowner_idfilters to the direct selects.The stats views are
security_invokerand granted toservice_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
clicksis already every valid click andfree_clicksis 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-impressionsand--target-ctroverride 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, withads list|show|pause|resume|budget|deletebehind 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 --noEmitclean.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