Revenue Truth — Live Data Before Any Money Claim
Why this exists
On 2026-05-26 the CTO answered "why didn't we make money?" three times in a row by quoting planning documents and stale March snapshots as if they were current telemetry. The CEO had to push back with "are you sure?" three times before the CTO admitted he had never queried the live billing endpoint. This skill exists so that never happens again.
The first version of this skill (written same day) referenced an env var named THUMBGATE_ADMIN_KEY that does not exist anywhere in the codebase — a name I invented without grepping. The CEO caught it with another "are you sure?". The skill now defers to the canonical scripts/revenue-status.js pipeline so it cannot drift again.
Hard rule
Before claiming any revenue, visitor, conversion, or funnel number, you MUST run node scripts/revenue-status.js and quote its output. If no credentials are configured, say so — do not fall back to planning docs.
Canonical command
node scripts/revenue-status.js
Reads credentials in this exact priority order (same as scripts/operational-summary.js and scripts/operational-dashboard.js):
$THUMBGATE_OPERATOR_KEY— read-only billing-summary access, recommended for agents~/.config/thumbgate/operator.json— created bynode bin/cli.js billing:setup$THUMBGATE_API_KEY— full admin, only when operator unavailable
Output sections (quote these directly, never paraphrase):
Source:—hosted-billing-summary(live) vslocal-fallback(no creds)Today / 30d / Lifetime:— visitors, pageViews, checkoutStarts, paidOrders, bookedRevenue30d attribution coverage— channel telemetry qualityGaps:— exact phrases describing what's missing
Quick lookups against the same endpoint
If you need a single field rather than the full report, query directly:
KEY="${THUMBGATE_OPERATOR_KEY:-${THUMBGATE_API_KEY:-}}"
[ -z "$KEY" ] && { echo "no operator/api key configured"; exit 1; }
curl -fsS \
-H "Authorization: Bearer ${KEY}" \
"https://thumbgate-production.up.railway.app/v1/billing/summary?window=30d" \
| jq '{
window,
booked_cents: .revenue.bookedRevenueCents,
paid_orders: .revenue.paidOrders,
checkout_starts: .funnel.checkoutStarts,
visitors: .funnel.uniqueVisitors,
acquisition: .funnel.acquisitionBySource
}'
Stripe direct (cross-check only)
curl -fsS https://api.stripe.com/v1/charges?limit=20 \
-u "${STRIPE_SECRET_KEY:?set STRIPE_SECRET_KEY in env}:" \
| jq '[.data[] | select(.paid==true and .refunded==false)
| {amount, created, description, customer}]
| {count: length, total_cents: (map(.amount) | add)}'
Note the var is STRIPE_SECRET_KEY (matches src/api/server.js:2597), not STRIPE_SECRET. If the two sources disagree, /v1/billing/summary is truth for booked-revenue claims; Stripe disagreement is an attribution bug worth filing.
What counts as truth vs. noise
| Source | Truth? | Notes |
|---|---|---|
scripts/revenue-status.js output where Source: hosted-billing-summary |
YES | Backed by Stripe-reconciled ledger |
scripts/revenue-status.js output where Source: local-fallback |
NO | Means no key configured; numbers are zero-state local |
Stripe API /v1/charges |
YES | Cross-check only |
reports/gtm/*/operator-close-packet.md numbers labeled "revenue plan" |
NO | These are forecasts |
docs/VERIFICATION_EVIDENCE.md snapshot dates |
YES at that date, NO as current | Always check the date in the section heading |
docs/COMMERCIAL_TRUTH.md cumulative line |
YES as of file's "Updated:" date | Stale if git log -1 -- docs/COMMERCIAL_TRUTH.md is >7 days old |
If no credentials are configured
State this exactly:
"No operator key in this session's env and no
~/.config/thumbgate/operator.jsonon this container.scripts/revenue-status.jsreportsSource: local-fallback. The last verified production snapshot in the repo is<date>: <numbers>fromdocs/VERIFICATION_EVIDENCE.md. Any number labeled '30d' in operator packets without a corresponding live curl is a forecast, not measured traffic."
Then offer one specific resolution:
"To unlock live numbers for this session AND future sessions: paste a
THUMBGATE_OPERATOR_KEYvalue into the harness env (the agent container's env, not Railway's server env). Generate one withnode bin/cli.js billing:setupon a machine where the admin key is already configured, or copy the existing operator key from Railway's variables."
Then stop. Do not speculate. Do not quote forecasts as actuals.
Channel-attribution sanity check
When booked revenue is low, also pull acquisition breakdown:
KEY="${THUMBGATE_OPERATOR_KEY:-${THUMBGATE_API_KEY:-}}"
curl -fsS \
-H "Authorization: Bearer ${KEY}" \
"https://thumbgate-production.up.railway.app/v1/billing/summary?window=30d" \
| jq '.funnel.acquisitionBySource'
This tells you whether traffic came from channels we actually posted to (Bluesky, Threads, Reddit) or from background ai_search / direct. Channel mismatch is usually the real story.
If you're running from a cloud Claude Code session and seeing 401
Use the wrapper, not the raw script:
npm run revenue:truth # or: bash bin/revenue-truth.sh
The wrapper handles three branches:
- Operator key configured AND fresh. Runs the canonical pipeline. Output starts with
Source: hosted-billing-summary. - Operator key configured but STALE (env var or
~/.config/thumbgate/operator.jsonexists but no longer authenticates against Railway after a rotation). Runs the pipeline, then prints a loudWARNING — configured operator key authenticated against the LOCAL fallbackblock with the exact fix command. This is what you want to see when describing the 401 to the CEO instead of guessing. - No operator key AND shell looks like a cloud session (
$CI,$CODESPACES,$GITHUB_ACTIONS,$CLAUDE_CODE_REMOTE, or/home/user/...Linux container path). Prints a one-paragraph "revenue truth is local-only by design, run from your laptop, do NOT paste the key here" message and exits0. This is the expected posture for cloud sessions — not a bug, not a blocker, not something to keep reporting back to the CEO.
The wrapper refuses to accept the operator key as a CLI argument (exits 64). Pasting the key on the command line would leak it to shell history; pasting it into the Claude transcript would leak it to the model context. Per CLAUDE.md hard-block rule #2.
Anti-pattern this section exists to prevent: an agent in a cloud session running node scripts/revenue-status.js directly, hitting 401, then repeatedly telling the CEO "I can't see hosted revenue" across multiple turns as if it were news. After the first observation, route to npm run revenue:truth so the diagnostic message is the response, not a back-and-forth.