Skip to content

What the fleet costs and what it returns, in the terminal - #240

Merged
ralyodio merged 1 commit into
masterfrom
worktree-roi-dashboard
Sep 6, 2026
Merged

What the fleet costs and what it returns, in the terminal#240
ralyodio merged 1 commit into
masterfrom
worktree-roi-dashboard

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

crawlproof dashboard (aliases roi, tui) — a live hqtui dashboard over the three feeds that have never been in the same place: the tracker for who arrived, the ad network for what was delivered, and CoinPay for what the bank actually did. Five screens (ROI, Traffic, Ads, Money, Spend), refreshing on a timer. --json gives the same snapshot without a terminal.

The two rules in the arithmetic

Both exist because breaking either produces a flattering number that is false.

Self-deal is not revenue. We advertise on our own slots, so ad spend and ad earnings are one dollar moving between two pockets. They are reported under Internal and counted as neither cost nor revenue.

Personal money is not fleet cost. The bank feed carries groceries next to servers. Cost is the business scope only, joined from each ledger row's account_id to that account's effective_scope — a ledger row names an account, not a scope, so both halves are required.

Everything is normalised to a monthly rate and prorated onto the traffic window. Burn is a rate, and it is the only way the traffic side can be asked for an hour while the bank side only answers in weeks.

Two token-authed reads to feed it

  • GET /api/ads/v1/earnings?days= — the model /dashboard/ads/earnings already builds. Without it, a client that wants fleet totals needs one request per campaign, and the account is past 170 of them.
  • GET /api/tracker/v1/sites/stats already names every project, but only inside the 400 it returns when the caller does not say which. A client should not have to parse an error message to find the fleet.

Fleet traffic is a client-side fan-out (one /stats call per site, concurrency-capped at 8) rather than a server-side aggregate. Summing 52 projects inside one serverless request is how the tracker RPCs have timed out before; a slow client is a much better failure than a route that 504s for everyone. Measured at ~5s for 52 sites.

Security

loadEarnings filtered nothing by owner and relied on RLS. The new route passes the service client, which has no RLS, so it now filters ad_campaigns, ad_slots, ad_ledger and ad_payouts by owner_id explicitly. For the existing dashboard that is a no-op narrowing of what RLS already allows; for the new route it is the security boundary. ad_ledger is indexed on (owner_id, kind), so the filter is free.

It says what it cannot know

A site that did not answer is missing, not zero. A vendor list built from one page of a longer ledger says so. A fleet whose visits run 200× its pageviews, or whose average is really one busy site, says that next to the number — a "visitor" is any non-crawler hit, which on a site with a machine-readable endpoint runs orders of magnitude above pages anyone read, so a per-pageview figure sits beside the per-visitor one.

Verified against live data

Ran end-to-end against a local server on the production environment, all 52 sites, all three feeds, zero source errors. All five screens captured through a pty harness. Real 1m answer: $4,448/mo business burn against $131/mo revenue, ROI −97%, 1.13 months of cover, $1.94 per pageview. Top vendors: OpenAI $720, DigitalOcean $603, Anthropic $337, Turso $308, Railway $279.

Full suite green: 2,129 tests (2,106 existing + 23 new), tsc --noEmit clean.

Notes

  • COINPAY_API_URL is the site origin in this repo but the CoinPay SDK's base must include /api. The CLI normalises either form — the failure otherwise is an HTML page parsed as JSON, which names neither cause.
  • The finance half needs a CoinPay merchant session (~/.coinpay.json, which coinpay auth login writes, or COINPAY_SESSION_TOKEN). Without one the traffic and ads screens still work and the money panels say what is missing.
  • npm run lint is broken on this branch and on master alike (next lint passes "lint" as a directory). Not touched here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HvWJ4336pxTFRdRbvsTQeD

`crawlproof dashboard` (also roi, tui) is a live hqtui dashboard over three
feeds that have never been in the same place: the tracker for who arrived, the
ad network for what was delivered, and CoinPay for what the bank actually did.
Five screens — ROI, Traffic, Ads, Money, Spend — refreshing on a timer, plus
--json for the same snapshot without a terminal.

Two rules run through the arithmetic, because breaking either produces a
flattering number that is false:

  Self-deal is not revenue. We advertise on our own slots, so ad spend and ad
  earnings are one dollar moving between two pockets. They are reported under
  Internal and counted as neither cost nor revenue.

  Personal money is not fleet cost. The bank feed carries groceries next to
  servers, so cost is the business scope only, joined from each ledger row's
  account to that account's effective_scope.

Everything is normalised to a monthly rate and prorated onto the traffic
window, because burn is a rate and the traffic side can be asked for an hour
while the bank side only answers in weeks.

Two token-authed reads to feed it:

  GET /api/ads/v1/earnings — the model /dashboard/ads/earnings already builds.
  Without it a client wanting fleet totals needs one request per campaign, and
  the account is past 170 of them.

  GET /api/tracker/v1/sites — /stats already names every project, but only
  inside the 400 it returns when the caller does not say which. A client should
  not have to parse an error message to find the fleet.

loadEarnings now filters ad_campaigns, ad_slots, ad_ledger and ad_payouts by
owner_id explicitly rather than leaning on RLS. For the dashboard that is a
no-op narrowing of what RLS already allows; for the new route, which passes the
service client, it is the security boundary.

The dashboard reports what it cannot know as loudly as what it can: a site that
did not answer is missing rather than zero, a vendor list built from one page of
a longer ledger says so, and a fleet whose visits run 200x its pageviews — or
whose average is really one busy site — says that too, next to the number.

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.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​profullstack/​coinpay@​0.9.07810010096100
Added@​profullstack/​hqtui@​0.1.11781009995100

View full report

@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @noble/hashes is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/@profullstack/coinpay@0.9.0npm/@noble/hashes@1.8.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@noble/hashes@1.8.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm @noble/hashes is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: package-lock.jsonnpm/@profullstack/coinpay@0.9.0npm/@noble/hashes@1.8.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@noble/hashes@1.8.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Low adoption: npm @profullstack/coinpay

Location: Package overview

From: package-lock.jsonnpm/@profullstack/coinpay@0.9.0

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@profullstack/coinpay@0.9.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Low adoption: npm @profullstack/hqtui

Location: Package overview

From: package-lock.jsonnpm/@profullstack/hqtui@0.1.11

ℹ Read more on: This package | This alert | What are unpopular packages?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Unpopular packages may have less maintenance and contain other problems.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@profullstack/hqtui@0.1.11. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@ralyodio
ralyodio merged commit d270d24 into master Sep 6, 2026
10 checks passed
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.

1 participant