snappy-analytics skill
scorecardread/bot${TELEGRAM_BOT_TOKEN}/sendMessage/bot${TELEGRAM_BOT_TOKEN}/sendMessage$ npx snappy-skills install snappy-analytics
$ npx snappy-skills install --all
$ npx snappy-skills update
typescriptimport { getScorecard } from "../snappy-analytics/api.ts";
CLI:
bashnpx tsx ~/.claude/skills/snappy-analytics/api.ts scorecard
| Function | Purpose |
|---|---|
getScorecard() |
Content pipeline scorecard -- atom counts by status, type, and recent entries |
snappy-analytics is the measurement layer of the Snappy operating system. Terminal consumer skill -- many inputs, zero downstream skills, only delivery channels. Owns no raw data; every number originates in a producer skill. Job: pull, normalize, compare against targets, flag red zones, assemble the synthesized scoreboard. Read-only across the entire system.
| Format | Trigger | Frequency | Destination |
|---|---|---|---|
| Weekly Snappy Dashboard | snappy-ops Friday close |
Weekly | Telegram + optional Slack |
| Monthly Snappy Review | snappy-ops last Friday of month |
Monthly | Notion + Telegram digest |
| Daily Briefing rollup | snappy-ops morning brief |
Mon-Fri | Terminal inline |
| Funnel Report | "funnel report" trigger | On-demand | Terminal + optional Notion |
| Producer | Domain |
|---|---|
snappy-freshbooks |
MRR, revenue, outstanding, profit margin, runway |
snappy-website |
Page views, contact submissions, traffic sources |
snappy-ads |
Paid spend, CPL, CP-Call, ROAS, true ROI |
snappy-youtube |
Views, watch time, CTR, AVD, sub delta |
snappy-linkedin |
Impressions, engagement rate, followers |
snappy-email |
Sends, open rate, click rate, list growth |
snappy-skool |
Members, active 14d, posts, churn |
snappy-sales |
Leads, calls booked/completed, close rate, pipeline value |
snappy-clients |
Active count, lifecycle stage, churn cross-check |
snappy-knowledge |
Contacts, tags, dormant, birthdays |
snappy-pipeline |
Orbiter enrichment health (read-only QA) |
snappy-content |
Content velocity, content -> conversion attribution |
| Metric | Target | Red Flag |
|---|---|---|
| Profit margin | >= 60% | < 50% |
| Client concentration | <= 30% | > 30% |
| Runway | >= 6 months | < 3 months |
| Show rate | 75-85% | < 60% |
| Close rate (high-ticket) | 20-30% | < 10% |
| Community active 14d | > 50% | < 40% |
| Email unsub rate | < 1% per send | > 1% |
(noisy).[snappy-X] source tag.closes / completed_calls, never closes / booked.snappy-telegram -- Friday dashboard self-message, daily briefing rollupsnappy-slack -- Optional #all-snappy post (C09DD2D0S07)snappy-docs (Notion) -- Monthly review archiveweekly-dashboard.sh -- standalone bash script that aggregates metrics from multiple sources into a markdown dashboard. Pulls content pipeline data from the content-engine Fly app, git commit counts via gh CLI, AGENTS.md feedback log stats, and skill file freshness. Run it directly:
bash~/.claude/skills/snappy-analytics/weekly-dashboard.sh
Output is markdown to stdout. Pipe to file or use as input for Telegram/Slack delivery.
snappy-analytics/
SKILL.md <- Full reference (workflow, decision map, anti-patterns)
content-metrics.md <- YouTube / LinkedIn / Blog / Email pull procedures
revenue-metrics.md <- MRR / cash / churn / ad ROI computation
funnel-metrics.md <- Three funnels, attribution, benchmarks
dashboard-formats.md <- Weekly / Monthly / Daily templates + synthesis rules
data-source-map.md <- Producer -> metric index, endpoint catalog, gap log
troubleshooting.md <- Diagnostic procedures
weekly-dashboard.sh <- Metrics aggregation script (content pipeline, git, skill health)
AGENTS.md <- This file
If this loader doesn't cover your case:
bashecho "[$(date -u +%FT%TZ)] snappy-analytics: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
<!-- SKILL-INDEX-START -->
[snappy-analytics Index]|root: ~/.claude/skills/snappy-analytics|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,content-metrics.md,dashboard-formats.md,data-source-map.md,funnel-metrics.md,revenue-metrics.md,troubleshooting.md}
<!-- SKILL-INDEX-END -->
snappy-email<!-- SNAPPY-CONTRACT-VERBS-START -->
Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
scorecard |
— | read |
npx tsx ~/.claude/skills/snappy-analytics/api.ts scorecard |
When an answer carries face_hint, show it with one snappy_present(<answer>) call.
See /snappy-faces for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
---
name: snappy-analytics
role: Central scorecard for the Snappy OS -- pulls metrics from 12 producer skills, synthesizes weekly dashboard, monthly review, daily briefing rollup, and funnel reports.
loaded-by: PreToolUse hook (auto-injected when "snappy-analytics" is mentioned)
Triggers on: analytics, metrics, KPIs, dashboard, weekly review, funnel report
---
## API module
```typescript
import { getScorecard } from "../snappy-analytics/api.ts";
```
CLI:
```bash
npx tsx ~/.claude/skills/snappy-analytics/api.ts scorecard
```
## API functions
| Function | Purpose |
|----------|---------|
| `getScorecard()` | Content pipeline scorecard -- atom counts by status, type, and recent entries |
## Context
snappy-analytics is the measurement layer of the Snappy operating system. Terminal consumer skill -- many inputs, zero downstream skills, only delivery channels. Owns no raw data; every number originates in a producer skill. Job: pull, normalize, compare against targets, flag red zones, assemble the synthesized scoreboard. Read-only across the entire system.
## Four Output Formats
| Format | Trigger | Frequency | Destination |
|--------|---------|-----------|-------------|
| Weekly Snappy Dashboard | `snappy-ops` Friday close | Weekly | Telegram + optional Slack |
| Monthly Snappy Review | `snappy-ops` last Friday of month | Monthly | Notion + Telegram digest |
| Daily Briefing rollup | `snappy-ops` morning brief | Mon-Fri | Terminal inline |
| Funnel Report | "funnel report" trigger | On-demand | Terminal + optional Notion |
## Producer Skills (12 metric sources)
| Producer | Domain |
|----------|--------|
| `snappy-freshbooks` | MRR, revenue, outstanding, profit margin, runway |
| `snappy-website` | Page views, contact submissions, traffic sources |
| `snappy-ads` | Paid spend, CPL, CP-Call, ROAS, true ROI |
| `snappy-youtube` | Views, watch time, CTR, AVD, sub delta |
| `snappy-linkedin` | Impressions, engagement rate, followers |
| `snappy-email` | Sends, open rate, click rate, list growth |
| `snappy-skool` | Members, active 14d, posts, churn |
| `snappy-sales` | Leads, calls booked/completed, close rate, pipeline value |
| `snappy-clients` | Active count, lifecycle stage, churn cross-check |
| `snappy-knowledge` | Contacts, tags, dormant, birthdays |
| `snappy-pipeline` | Orbiter enrichment health (read-only QA) |
| `snappy-content` | Content velocity, content -> conversion attribution |
## Key Red Flag Thresholds
| Metric | Target | Red Flag |
|--------|--------|----------|
| Profit margin | >= 60% | < 50% |
| Client concentration | <= 30% | > 30% |
| Runway | >= 6 months | < 3 months |
| Show rate | 75-85% | < 60% |
| Close rate (high-ticket) | 20-30% | < 10% |
| Community active 14d | > 50% | < 40% |
| Email unsub rate | < 1% per send | > 1% |
## Rules
- Always pull fresh data on Friday morning (never use cached files > 24h).
- Samples < 30: report counts only, never compute rates.
- Samples 30-100: tag rate as `(noisy)`.
- Always normalize comparison windows: 7d-to-7d, 28d-to-28d, full month-to-full month.
- Every metric line gets `[snappy-X]` source tag.
- Always pick THE ONE THING -- single highest-leverage action from the data.
- Trim to 4096 chars for Telegram. Split or post to Slack if longer.
- Booked != completed. Close rate = `closes / completed_calls`, never `closes / booked`.
- Never mix paid + organic numbers in a single funnel rate.
- MRR = paid + sent recurring only. One-time invoices are revenue, never MRR.
## Delivery Channels
- `snappy-telegram` -- Friday dashboard self-message, daily briefing rollup
- `snappy-slack` -- Optional `#all-snappy` post (`C09DD2D0S07`)
- `snappy-docs` (Notion) -- Monthly review archive
## Quick-Run Script
`weekly-dashboard.sh` -- standalone bash script that aggregates metrics from multiple sources into a markdown dashboard. Pulls content pipeline data from the content-engine Fly app, git commit counts via `gh` CLI, AGENTS.md feedback log stats, and skill file freshness. Run it directly:
```bash
~/.claude/skills/snappy-analytics/weekly-dashboard.sh
```
Output is markdown to stdout. Pipe to file or use as input for Telegram/Slack delivery.
## Directory Layout
```
snappy-analytics/
SKILL.md <- Full reference (workflow, decision map, anti-patterns)
content-metrics.md <- YouTube / LinkedIn / Blog / Email pull procedures
revenue-metrics.md <- MRR / cash / churn / ad ROI computation
funnel-metrics.md <- Three funnels, attribution, benchmarks
dashboard-formats.md <- Weekly / Monthly / Daily templates + synthesis rules
data-source-map.md <- Producer -> metric index, endpoint catalog, gap log
troubleshooting.md <- Diagnostic procedures
weekly-dashboard.sh <- Metrics aggregation script (content pipeline, git, skill health)
AGENTS.md <- This file
```
---
If this loader doesn't cover your case:
```bash
echo "[$(date -u +%FT%TZ)] snappy-analytics: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
```
<!-- SKILL-INDEX-START -->
[snappy-analytics Index]|root: ~/.claude/skills/snappy-analytics|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,content-metrics.md,dashboard-formats.md,data-source-map.md,funnel-metrics.md,revenue-metrics.md,troubleshooting.md}
<!-- SKILL-INDEX-END -->
## Used by
- `snappy-email`
<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs
Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `scorecard` | — | `read` | `npx tsx ~/.claude/skills/snappy-analytics/api.ts scorecard` |
## Show the result
When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
Triggers on: snappy-analytics, analytics
The measurement layer of the Snappy operating system. Consumes metrics from every producer skill, synthesizes them into the weekly dashboard / monthly review / funnel report, delivers via Telegram + Slack + Notion. Read-only across the system.
snappy-analytics is the single skill responsible for "where do we stand?" questions. It owns no raw data -- every number originates in a producer skill. Its job is to pull, normalize, compare against targets, flag red zones, and assemble the synthesized scoreboard. The output is always one of four named formats: Weekly Dashboard, Monthly Review, Daily Briefing rollup, or Funnel Report.
Triggers (verbatim or paraphrase):
DO NOT use when:
snappy-pipeline (read-only QA skill, not part of the dashboard pull).snappy-analytics is a terminal CONSUMER skill in the system graph. It has many inputs and zero downstream skills -- only delivery channels.
| producer | metric domain | where in this skill |
|---|---|---|
snappy-website |
page views, contact form submissions, traffic sources | content-metrics.md > Blog, funnel-metrics.md > Visits |
snappy-ads |
paid spend, CPL, CP-Call, ROAS, true ROI | revenue-metrics.md > Ad Spend & ROI |
snappy-youtube |
views, watch time, subs, CTR, AVD | content-metrics.md > YouTube |
snappy-linkedin |
impressions, engagement rate, follower delta | content-metrics.md > LinkedIn |
snappy-email |
sends, open rate, click rate, list growth | content-metrics.md > Email |
snappy-skool |
members, active 14d, posts, churn | data-source-map.md > Community |
snappy-sales |
leads, calls booked / completed, pipeline value | funnel-metrics.md > Pipeline Health |
snappy-clients |
active client count, lifecycle stage, churn | revenue-metrics.md > Churn |
snappy-freshbooks |
MRR, revenue, outstanding, churn, profit margin, runway | revenue-metrics.md |
snappy-knowledge |
total contacts, tag distribution, dormant, birthdays | data-source-map.md > Knowledge graph |
snappy-pipeline |
Orbiter enrichment health (read-only QA) | data-source-map.md > Orbiter |
snappy-content |
content velocity, content -> conversion attribution | content-metrics.md > Cross-Channel Synthesis |
None. This is a terminal consumer. The synthesized output is delivered via channels, not handed to another skill.
| channel | use | frequency |
|---|---|---|
snappy-telegram |
Weekly dashboard self-message to Robert | Friday morning |
snappy-slack |
Optional #all-snappy post (C09DD2D0S07) |
Friday + monthly |
snappy-docs (Notion) |
Monthly review page archive | Last Friday of month |
| Terminal output | Daily briefing inline rollup, funnel reports | On-demand |
snappy-ops is the only skill that calls snappy-analytics. Triggers:
snappy-ops/weekly-rhythm.md > Friday) -> Weekly Snappy Dashboardsnappy-ops/weekly-rhythm.md > Monthly Overlays) -> Monthly Snappy Reviewsnappy-ops/SKILL.md > Daily Briefing) -> Daily Briefing rollupCredentials load from .env.cache via snappy-settings. Per snappy-infra/auth-reference.md:
bashSNAPPY_SETTINGS_QUIET=1 source ~/.claude/skills/snappy-settings/scripts/load-env.sh
# exports XANO, XANO_METADATA_TOKEN, and all other credentials
# Note: ActiveCampaign is deprecated (email goes via Google) -- no AC_API_KEY
Robert says... -> Open this resource
"weekly dashboard" -> dashboard-formats.md > Weekly
"monthly review" / "month close" -> dashboard-formats.md > Monthly
"funnel report" / "show me the funnel" -> funnel-metrics.md
"MRR" / "revenue" / "outstanding" / "churn" -> revenue-metrics.md
"profit margin" / "runway" / "expenses" -> revenue-metrics.md > Profit Margin
"ad ROI" / "ROAS" / "true ROI" -> revenue-metrics.md > Ad Spend & ROI
"YouTube" / "LinkedIn" / "newsletter" -> content-metrics.md
"daily briefing rollup" -> dashboard-formats.md > Daily
"where does metric X come from" -> data-source-map.md
"what's missing / what can't we measure" -> data-source-map.md > What's Missing
revenue-metrics.md > Pull procedurecontent-metrics.mdfunnel-metrics.md > Pull proceduredata-source-map.mddata-source-map.md > Orbiter Xano[snappy-X] source per dashboard-formats.md > Synthesis Rules > Source labeling.dashboard-formats.md > Synthesis Rules > Red flag highlighting.snappy-ops/SKILL.md > Operations Philosophy.dashboard-formats.md > Weekly Snappy Dashboard.snappy-telegram self-message, optionally snappy-slack #all-snappy.~/.claude/skills/snappy-analytics/logs/YYYY-WW.md for trend backref.| situation | action |
|---|---|
| Robert says "dashboard" with no qualifier | Default to Weekly Dashboard, last 7 days |
| Today is the last Friday of the month | Run Monthly Review BEFORE Weekly Dashboard |
| Today is mid-month, Robert asks "this month's revenue" | Disclose partial month + offer 30-day rolling alternative |
| A producer skill's endpoint returns nothing / errors | Mark that line [NO DATA] in the dashboard, don't fabricate |
| Sample size < 30 for a rate | Report counts only -- never compute the rate |
| Sample size 30-100 for a rate | Compute but tag (noisy) per dashboard-formats.md > Sample size & noise |
| A metric crosses a red-flag threshold | Tag [RED] and surface in CONCERNS section. See dashboard-formats.md > Red flag highlighting for thresholds |
Multiple producer skills disagree (e.g. FreshBooks churn vs snappy-clients churn) |
Investigate before synthesizing. Discrepancy = data hygiene issue |
| Robert asks for a metric this skill cannot produce | Check data-source-map.md > What's Missing and explain the gap |
| Robert wants to act on a metric (write an invoice, send an email) | Hand off to producer skill -- this skill is read-only |
The full index lives in data-source-map.md. Here is the producer-skill -> metric-domain index for fast routing:
| producer skill | primary metrics | owner doc |
|---|---|---|
snappy-freshbooks |
MRR, revenue, outstanding, churn, profit margin, runway, ARPU, concentration | snappy-freshbooks/api-reference.md |
snappy-website |
page views, contact submissions, source attribution | snappy-website/conversion-tracking.md |
snappy-ads |
paid spend, CPL, CP-Call, ROAS, true ROI | snappy-ads/SKILL.md Workflow 2 |
snappy-youtube |
views, watch time, CTR, AVD, top videos | snappy-youtube/analytics.md |
snappy-linkedin |
post impressions, engagement rate, profile views, followers | snappy-linkedin/SKILL.md |
snappy-email |
sends, open rate, click rate, unsubs, list size | snappy-email/SKILL.md |
snappy-skool |
members, active 14d, posts, leaderboard, member churn | snappy-skool/SKILL.md |
snappy-sales |
leads, calls booked / completed, close rate, pipeline value | snappy-sales/pipeline.md |
snappy-clients |
active client count, lifecycle stage, dormant, churn cross-check | snappy-clients/lifecycle.md |
snappy-knowledge |
contacts, tags, dormant, birthdays | snappy-knowledge/endpoints.md |
snappy-pipeline |
Orbiter enrichment health (read-only QA) | snappy-pipeline/endpoints.md |
snappy-content |
content velocity, content -> conversion attribution | snappy-content/SKILL.md |
Endpoint catalog, browser-only sources, and the gap log are in data-source-map.md.
bashSNAPPY_SETTINGS_QUIET=1 source ~/.claude/skills/snappy-settings/scripts/load-env.sh
# exports XANO, XANO_METADATA_TOKEN, TELEGRAM_BOT_TOKEN, TELEGRAM_ROBERT_CHAT_ID, and all other credentials
# Note: ActiveCampaign is deprecated (email goes via Google) -- no AC_API_KEY
bash# Telegram weekly dashboard self-message
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\": \"${TELEGRAM_ROBERT_CHAT_ID}\", \"text\": \"$(cat /tmp/dashboard.txt | jq -Rs .)\", \"parse_mode\": \"Markdown\"}"
# Slack #all-snappy post (channel id from snappy-infra/auth-reference.md)
curl -s -X POST "$XANO/api:hZB4Dj0c/slack/bot-message" \
-H "Content-Type: application/json" -H "Authorization: Bearer $XANO_METADATA_TOKEN" \
-d '{"channel_id": "C09DD2D0S07", "text": "Weekly dashboard:\n```\n'$(cat /tmp/dashboard.txt)'\n```"}'
| metric | target | red flag | source skill |
|---|---|---|---|
| Profit margin | >= 60% | < 50% | snappy-freshbooks |
| Single client concentration | <= 30% | > 30% | snappy-freshbooks |
| Runway (months) | >= 6 | < 3 | snappy-freshbooks |
| Show rate | 75-85% | < 60% | snappy-sales |
| Close rate (high-ticket) | 20-30% | < 10% | snappy-sales |
| Active community 14d | > 50% | < 40% | snappy-skool |
| Newsletter sends/week | 3+ | < 3 (yellow) | snappy-email |
| Email open rate | 10-20% baseline | -- | snappy-email |
| Email unsub rate | < 1% per send | > 1% | snappy-email |
| LinkedIn posts/week | 5+ | < 5 (yellow) | snappy-linkedin |
| YouTube AVD | 40-55% (1k-10k subs) | < 30% | snappy-youtube |
| CPL (when ads running) | < $25 | > $50 | snappy-ads |
| CP-Call (when ads running) | < $250 | > $500 | snappy-ads |
| Stale leads | 0 | > 5 (yellow) | snappy-sales |
Full target tables in each producer skill's SKILL.md and synthesized in dashboard-formats.md > Red flag highlighting.
| format | trigger | frequency | destination |
|---|---|---|---|
| Weekly Snappy Dashboard | snappy-ops Friday close |
weekly | Telegram + optional Slack |
| Monthly Snappy Review | snappy-ops last Friday of month |
monthly | Notion + Telegram digest |
| Daily Briefing rollup | snappy-ops morning brief |
Mon-Fri | Terminal inline |
| Funnel Report | "funnel report" trigger | on-demand | Terminal + optional Notion |
Full templates in dashboard-formats.md.
These are the failure modes specific to analytics -- distinct from the format-specific gotchas in dashboard-formats.md and the channel/funnel/revenue/content gotchas in each resource file.
| wrong | right |
|---|---|
| Compute a conversion rate from < 30 samples ("we had 7 visits and 1 optin, that's 14.3%") | If samples < 30, report counts only. Between 30-100 tag the rate as (noisy). Above 100 the rate is reportable. From funnel-metrics.md > What AI Agents Get Wrong. |
| Compare windows of different lengths (last 28d vs last 7d, "this month so far" vs "last month full") | Always normalize: 7d-to-7d, 28d-to-28d, MoM = full month vs full month. Disclose partial months explicitly. From dashboard-formats.md > Window discipline. |
| Default to last-touch attribution when first-touch is recoverable | Priority order: manual sales notes > AC original list > first UTM > last UTM > unknown. Disclose attribution model in every report. From funnel-metrics.md > Attribution priority order. |
| Treat Calendly bookings as completed calls | booked != completed. Show rate is its own metric and a common pipeline gap. Always pull both numbers. From funnel-metrics.md > What AI Agents Get Wrong. |
| Compute close rate against booked calls | Close rate uses closes / completed_calls -- never closes / booked. Show rate is a separate metric. |
| Mix paid + organic numbers in a single funnel rate | Always break down by channel. Paid VSL ~30% optin; organic blog ~5%. Mixing produces meaningless averages. From funnel-metrics.md > What AI Agents Get Wrong. |
| Calculate MRR from sent invoices (treating sent as already-revenue) | MRR = paid + sent recurring invoices, normalized to monthly. Outstanding invoices go in receivables, not MRR. From revenue-metrics.md > What AI Agents Get Wrong. |
| Include one-time / project invoices in MRR | MRR is recurring only. One-time invoices count toward "revenue this month" but never MRR. |
| Forget to normalize quarterly / annual retainers when computing MRR | Quarterly = amount / 3, annual = amount / 12. Per revenue-metrics.md > Recurring Revenue. |
| Compute ROAS as the only ad ROI metric | snappy-ads/SKILL.md mandates true ROI using actual collected FreshBooks revenue, not projected ROAS from Hyros. Pair with snappy-knowledge UTM tags to attribute clients to ads. |
| Project ARR by multiplying MRR by 12 | ARR is misleading for high-ticket consulting where retention is < 12 months. Report MRR + estimated annualized contract value separately. |
Use the FreshBooks client string as a stable join key |
Client names are typed by humans. Use client_id from freshbooks/clients when joining. |
| Hardcode "30%+ open rate" as the email target | snappy-email/SKILL.md actually targets 10-20% as the operational baseline. 30%+ is a stretch goal. From content-metrics.md > Email. |
| Render the dashboard with no source tags | Every metric line gets [snappy-X] so Robert can drill in. Non-negotiable per dashboard-formats.md > Source labeling. |
| Drop the "ONE THING" line because nothing dramatic happened | Always pick one. Even "keep doing what's working -- ship Friday post" is the one thing. |
| Send a 6000-char dashboard to Telegram | Telegram limit is 4096 chars. Split into 2-3 messages or post to Slack instead. |
| Use Charlotte MCP browser tools to scrape Studio / Vercel / LinkedIn | Use agent-browser per CLAUDE.md. Charlotte MCP browser tools are deprecated. |
| Compare engagement rates across channels (LinkedIn ER vs YouTube AVD vs Email open rate) | Different platforms compute engagement differently. Compare each channel against its own baseline over time, never cross-channel. |
| Pull metrics from cached files older than 24h for the Friday dashboard | Always pull fresh on Friday morning. Studio, AC, Vercel, FreshBooks all update continuously. |
| Treat metrics older than 48h as fresh | Daily-cadence metrics (Skool, Vercel) refresh in hours. Pull at most 24h before assembly. |
| Skip the "unattributed" bucket in the funnel report | Always report unknown-source separately. > 20% unattributed = data hygiene issue. |
| Flag every yellow as urgent | Reserve [RED] for must-address. Yellows are watch items, not interrupts. |
| Generate the dashboard then never log it | Save every dashboard to ~/.claude/skills/snappy-analytics/logs/YYYY-WW.md for trend backref. |
| Compute "rates of rates" (engagement rate of engagement rate) | Meaningless. Average raw counts, then compute the rate once. |
| Use mixed currencies in revenue without conversion | FreshBooks supports multi-currency. Filter by currency or convert to base (USD) and disclose conversion rate. |
Hit api:Bd_dCiOz (Orbiter Xano) with the standard curl token |
Orbiter is a separate Xano instance (xh2o-yths-38lt.n7c.xano.io). Call via mcp__xano-mcp__execute only. Never mix with main Snappy Xano endpoints. |
| Pretend a metric exists when the producer endpoint is missing | Check data-source-map.md > What's Missing first. If a metric is gap-flagged, mark it [NO DATA -- see gap log] in the output. |
Progressive disclosure -- start at SKILL.md, drill into the topic file you need.
| file | when to read | key sections |
|---|---|---|
SKILL.md (this file) |
Always -- entry point + workflow | Workflow / Quick Start / Quick Decision Map / What AI Agents Get Wrong |
data-source-map.md |
"Where does metric X come from?" / "What can't we measure?" | Producer Skills / Metric -> Skill Index / Endpoint Catalog / Gap Log |
content-metrics.md |
Pulling YouTube / LinkedIn / Blog / Email numbers | Per-channel pull procedures + target zones + cross-channel synthesis |
revenue-metrics.md |
Pulling MRR / cash / churn / ad ROI | MRR computation rules + churn formulas + true ROI procedure |
funnel-metrics.md |
Funnel report / conversion analysis / attribution | Three funnels visual + stage definitions + attribution rules + benchmarks |
dashboard-formats.md |
Rendering the synthesized scoreboard | Weekly / Monthly / Daily formats + synthesis rules + red flag thresholds |
Producers (this skill consumes from them):
| skill | why |
|---|---|
snappy-website |
Source of traffic + contact form submissions + UTM-tagged optins. Funnel top-of-stack. |
snappy-ads |
Source of ad spend + CPL + CP-Call + ROAS. True ROI is computed by joining ad UTMs to FreshBooks clients. |
snappy-youtube |
Source of YouTube views, AVD, CTR, sub delta. Priority is AVD > CTR > Views per snappy-youtube/analytics.md. |
snappy-linkedin |
Source of organic post impressions and engagement rate (browser-only -- no API for organic LI analytics). |
snappy-email |
Source of newsletter sends, opens, clicks, list growth. Xano emails/list endpoint (Gmail backend). ActiveCampaign is NOT in use. |
snappy-skool |
Source of Skool member counts, posts, classroom, chats, and leaderboards through the measured private frontend API. |
snappy-sales |
Source of leads, calls booked / completed, close rate, pipeline value, stale lead count. |
snappy-clients |
Source of lifecycle stage tags. Cross-check FreshBooks-derived churn against snappy-clients lifecycle changes. |
snappy-freshbooks |
Source of MRR, revenue, outstanding, profit margin, runway. Read-only -- never write here from analytics. |
snappy-knowledge |
Source of total contacts, dormant, birthdays. UTM tags on contacts power attribution. |
snappy-pipeline |
Read-only QA on Orbiter enrichment. Not part of every dashboard pull -- only when Orbiter health is being reviewed. |
snappy-content |
Source of cross-channel content velocity (sends per week per channel) and content -> conversion attribution. |
Channels (this skill delivers through them):
| skill | why |
|---|---|
snappy-telegram |
Primary delivery for the Friday dashboard self-message and the daily briefing rollup. |
snappy-slack |
Optional #all-snappy post for the weekly dashboard and monthly review (C09DD2D0S07). |
snappy-docs |
Notion archive for monthly review pages (long-form, not subject to Telegram char limit). |
Orchestrator (the skill that calls this one):
| skill | why |
|---|---|
snappy-ops |
The only skill that triggers analytics pulls. Friday close -> Weekly Dashboard. Last Friday of month -> Monthly Review. Morning briefing -> Daily Briefing rollup. |
Infrastructure:
| skill | why |
|---|---|
snappy-infra |
Auth reference (.env.cache / Xano token / channel IDs / API groups). |
snappy-browse |
agent-browser CLI used to scrape Studio / Vercel / LinkedIn / Skool. |
snappy-xano-mcp |
mcp__xano-mcp__execute for Orbiter Xano (separate instance). |
Skill Status: COMPLETE
<!-- SNAPPY-NEAR-NEIGHBOURS-START -->
These hands share enough of this one's words that a model can pick the wrong
door. Each row says what the other one is for; open that one instead when its
job is the job.
| Hand | What it is for |
|---|---|
snappy-content |
Interview-driven content production methodology, the writing engine for every Snappy channel: the 4-questio... |
snappy-corpus |
The Krisp transcript corpus + nugget mining pipeline: import calls into the corpus, find quotes, stories, o... |
snappy-freshbooks |
Snappy FreshBooks -- authoritative source for DRAFT invoicing, billing, time tracking, expense logging, rec... |
snappy-image |
Centralized image generation, editing, and capture for Snappy: Nano Banana / Gemini, Grok / xAI edits, gpt... |
snappy-infra |
Snappy infrastructure foundation -- Xano API surface (Slack, email, LinkedIn, FreshBooks, WhatsApp, calenda... |
snappy-linkedin |
LinkedIn operations for Snappy -- posting (text, image, carousel, native video, article, poll, document, co... |
snappy-playbook |
WeTube SS mastermind 6-week curriculum source |
snappy-telegram |
Telegram Bot API channel for Snappy: direct calls to api.telegram.org (no Xano middleware) to send text, ph... |
snappy-testimonials |
Snappy Testimonials -- scans client meeting transcripts (Krisp) and the knowledge graph for positive client... |
snappy-transcripts |
Transcript retrieval, search, and processing for Snappy |
snappy-video |
Video and audio processing pipeline for Snappy, run on the Mac Mini via SSH (caption-video.sh wrapper aroun... |
snappy-xano-mcp |
THE EXISTING, DEPLOYED Snappy MCP server: the Cloudflare Worker exposing the Snappy Xano API (email, calend... |
snappy-youtube |
Organic YouTube content creation and channel management for Snappy |
<!-- SNAPPY-NEAR-NEIGHBOURS-END -->
---
name: snappy-analytics
reports_to: money
head: false
description: Centralized analytics and metrics for the entire Snappy operating system. Pulls performance numbers from every producer skill (website, ads, youtube, linkedin, email, community, sales, clients, freshbooks, knowledge, pipeline, content), synthesizes the weekly Snappy Dashboard, monthly review, and on-demand funnel reports. Use this skill when Robert says "weekly dashboard", "monthly review", "what's our MRR", "pipeline status", "funnel report", "ad ROI", "content scorecard", "are we hitting targets", "show me the numbers", "scorecard", "metrics check", or any cross-skill measurement question.
---
**Triggers on:** snappy-analytics, analytics
# Snappy Analytics -- Central Scorecard
> The measurement layer of the Snappy operating system. Consumes metrics from every producer skill, synthesizes them into the weekly dashboard / monthly review / funnel report, delivers via Telegram + Slack + Notion. **Read-only across the system.**
## Purpose
`snappy-analytics` is the single skill responsible for "where do we stand?" questions. It owns no raw data -- every number originates in a producer skill. Its job is to pull, normalize, compare against targets, flag red zones, and assemble the synthesized scoreboard. The output is always one of four named formats: Weekly Dashboard, Monthly Review, Daily Briefing rollup, or Funnel Report.
## When to Use This Skill
Triggers (verbatim or paraphrase):
- "weekly dashboard" / "Friday dashboard" / "snappy dashboard"
- "monthly review" / "month close" / "monthly numbers"
- "what's our MRR" / "revenue this month" / "outstanding"
- "show me the funnel" / "funnel report" / "where are leads dropping off"
- "what's our visit-to-close rate"
- "ad ROI" / "ROAS" / "are ads paying off"
- "content scorecard" / "newsletter open rate" / "YouTube views this week"
- "pipeline status" / "calls booked this week" / "close rate"
- "community health" / "active members"
- "are we hitting targets" / "scorecard" / "show me the numbers"
- Any "what's our X" question that requires numbers from multiple producer skills
DO NOT use when:
- Robert wants to act on a metric (writing an invoice, sending a newsletter, posting a video). Route to the producer skill instead.
- Robert wants the raw data (one client's invoices, one video's stats). Route to the producer skill.
- Robert is debugging Orbiter enrichment per record. That's `snappy-pipeline` (read-only QA skill, not part of the dashboard pull).
---
## Workflow
snappy-analytics is a **terminal CONSUMER skill** in the system graph. It has many inputs and zero downstream skills -- only delivery channels.
### Inputs (producer skills feeding metrics in)
|producer|metric domain|where in this skill|
|--------|-------------|-------------------|
|`snappy-website`|page views, contact form submissions, traffic sources|`content-metrics.md > Blog`, `funnel-metrics.md > Visits`|
|`snappy-ads`|paid spend, CPL, CP-Call, ROAS, true ROI|`revenue-metrics.md > Ad Spend & ROI`|
|`snappy-youtube`|views, watch time, subs, CTR, AVD|`content-metrics.md > YouTube`|
|`snappy-linkedin`|impressions, engagement rate, follower delta|`content-metrics.md > LinkedIn`|
|`snappy-email`|sends, open rate, click rate, list growth|`content-metrics.md > Email`|
|`snappy-skool`|members, active 14d, posts, churn|`data-source-map.md > Community`|
|`snappy-sales`|leads, calls booked / completed, pipeline value|`funnel-metrics.md > Pipeline Health`|
|`snappy-clients`|active client count, lifecycle stage, churn|`revenue-metrics.md > Churn`|
|`snappy-freshbooks`|MRR, revenue, outstanding, churn, profit margin, runway|`revenue-metrics.md`|
|`snappy-knowledge`|total contacts, tag distribution, dormant, birthdays|`data-source-map.md > Knowledge graph`|
|`snappy-pipeline`|Orbiter enrichment health (read-only QA)|`data-source-map.md > Orbiter`|
|`snappy-content`|content velocity, content -> conversion attribution|`content-metrics.md > Cross-Channel Synthesis`|
### Outputs (downstream skills consuming this)
**None.** This is a terminal consumer. The synthesized output is delivered via channels, not handed to another skill.
### Channels (delivery surfaces for synthesized output)
|channel|use|frequency|
|-------|---|---------|
|`snappy-telegram`|Weekly dashboard self-message to Robert|Friday morning|
|`snappy-slack`|Optional `#all-snappy` post (`C09DD2D0S07`)|Friday + monthly|
|`snappy-docs` (Notion)|Monthly review page archive|Last Friday of month|
|Terminal output|Daily briefing inline rollup, funnel reports|On-demand|
### Orchestrator
`snappy-ops` is the only skill that calls snappy-analytics. Triggers:
- **Friday close** (`snappy-ops/weekly-rhythm.md > Friday`) -> Weekly Snappy Dashboard
- **Last Friday of month** (`snappy-ops/weekly-rhythm.md > Monthly Overlays`) -> Monthly Snappy Review
- **Morning briefing** (`snappy-ops/SKILL.md > Daily Briefing`) -> Daily Briefing rollup
- **Ad-hoc** ("funnel report", "what's our MRR") -> appropriate sub-format
---
## Quick Start
### Auth (always)
Credentials load from `.env.cache` via `snappy-settings`. Per `snappy-infra/auth-reference.md`:
```bash
SNAPPY_SETTINGS_QUIET=1 source ~/.claude/skills/snappy-settings/scripts/load-env.sh
# exports XANO, XANO_METADATA_TOKEN, and all other credentials
# Note: ActiveCampaign is deprecated (email goes via Google) -- no AC_API_KEY
```
### Interview-driven routing (the right resource for the right question)
```
Robert says... -> Open this resource
"weekly dashboard" -> dashboard-formats.md > Weekly
"monthly review" / "month close" -> dashboard-formats.md > Monthly
"funnel report" / "show me the funnel" -> funnel-metrics.md
"MRR" / "revenue" / "outstanding" / "churn" -> revenue-metrics.md
"profit margin" / "runway" / "expenses" -> revenue-metrics.md > Profit Margin
"ad ROI" / "ROAS" / "true ROI" -> revenue-metrics.md > Ad Spend & ROI
"YouTube" / "LinkedIn" / "newsletter" -> content-metrics.md
"daily briefing rollup" -> dashboard-formats.md > Daily
"where does metric X come from" -> data-source-map.md
"what's missing / what can't we measure" -> data-source-map.md > What's Missing
```
### Weekly Dashboard pull (the most common workflow)
1. **Auth** (auth block above).
2. **Pull all numbers in parallel**, never sequentially:
- Revenue/MRR/outstanding -> `revenue-metrics.md > Pull procedure`
- Content velocity (newsletter/LinkedIn/YouTube/blog/Skool) -> `content-metrics.md`
- Pipeline (leads/calls/closes) -> `funnel-metrics.md > Pull procedure`
- Knowledge graph (contacts/dormant/birthdays) -> `data-source-map.md`
- Orbiter enrichment health -> `data-source-map.md > Orbiter Xano`
3. **Compute deltas** vs prior 7d (NEVER 7d-vs-30d, NEVER overlapping windows).
4. **Tag every line** with `[snappy-X]` source per `dashboard-formats.md > Synthesis Rules > Source labeling`.
5. **Apply red flags** per `dashboard-formats.md > Synthesis Rules > Red flag highlighting`.
6. **Pick THE ONE THING** -- the single highest-leverage action drawn from the data. Non-negotiable per `snappy-ops/SKILL.md > Operations Philosophy`.
7. **Render** in the canonical format from `dashboard-formats.md > Weekly Snappy Dashboard`.
8. **Trim to 4096 chars** for Telegram. Split or post to Slack if longer.
9. **Deliver** via `snappy-telegram` self-message, optionally `snappy-slack` `#all-snappy`.
10. **Log** to `~/.claude/skills/snappy-analytics/logs/YYYY-WW.md` for trend backref.
---
## Quick Decision Map
|situation|action|
|---------|------|
|Robert says "dashboard" with no qualifier|Default to Weekly Dashboard, last 7 days|
|Today is the last Friday of the month|Run Monthly Review BEFORE Weekly Dashboard|
|Today is mid-month, Robert asks "this month's revenue"|Disclose partial month + offer 30-day rolling alternative|
|A producer skill's endpoint returns nothing / errors|Mark that line `[NO DATA]` in the dashboard, don't fabricate|
|Sample size < 30 for a rate|Report counts only -- never compute the rate|
|Sample size 30-100 for a rate|Compute but tag `(noisy)` per `dashboard-formats.md > Sample size & noise`|
|A metric crosses a red-flag threshold|Tag `[RED]` and surface in CONCERNS section. See `dashboard-formats.md > Red flag highlighting` for thresholds|
|Multiple producer skills disagree (e.g. FreshBooks churn vs `snappy-clients` churn)|Investigate before synthesizing. Discrepancy = data hygiene issue|
|Robert asks for a metric this skill cannot produce|Check `data-source-map.md > What's Missing` and explain the gap|
|Robert wants to act on a metric (write an invoice, send an email)|Hand off to producer skill -- this skill is read-only|
---
## Cross-Skill Data Sources (compressed index)
The full index lives in `data-source-map.md`. Here is the producer-skill -> metric-domain index for fast routing:
|producer skill|primary metrics|owner doc|
|--------------|---------------|---------|
|`snappy-freshbooks`|MRR, revenue, outstanding, churn, profit margin, runway, ARPU, concentration|`snappy-freshbooks/api-reference.md`|
|`snappy-website`|page views, contact submissions, source attribution|`snappy-website/conversion-tracking.md`|
|`snappy-ads`|paid spend, CPL, CP-Call, ROAS, true ROI|`snappy-ads/SKILL.md` Workflow 2|
|`snappy-youtube`|views, watch time, CTR, AVD, top videos|`snappy-youtube/analytics.md`|
|`snappy-linkedin`|post impressions, engagement rate, profile views, followers|`snappy-linkedin/SKILL.md`|
|`snappy-email`|sends, open rate, click rate, unsubs, list size|`snappy-email/SKILL.md`|
|`snappy-skool`|members, active 14d, posts, leaderboard, member churn|`snappy-skool/SKILL.md`|
|`snappy-sales`|leads, calls booked / completed, close rate, pipeline value|`snappy-sales/pipeline.md`|
|`snappy-clients`|active client count, lifecycle stage, dormant, churn cross-check|`snappy-clients/lifecycle.md`|
|`snappy-knowledge`|contacts, tags, dormant, birthdays|`snappy-knowledge/endpoints.md`|
|`snappy-pipeline`|Orbiter enrichment health (read-only QA)|`snappy-pipeline/endpoints.md`|
|`snappy-content`|content velocity, content -> conversion attribution|`snappy-content/SKILL.md`|
Endpoint catalog, browser-only sources, and the gap log are in `data-source-map.md`.
---
## Quick Reference
### Standard auth block
```bash
SNAPPY_SETTINGS_QUIET=1 source ~/.claude/skills/snappy-settings/scripts/load-env.sh
# exports XANO, XANO_METADATA_TOKEN, TELEGRAM_BOT_TOKEN, TELEGRAM_ROBERT_CHAT_ID, and all other credentials
# Note: ActiveCampaign is deprecated (email goes via Google) -- no AC_API_KEY
```
### Telegram + Slack delivery (channels)
```bash
# Telegram weekly dashboard self-message
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\": \"${TELEGRAM_ROBERT_CHAT_ID}\", \"text\": \"$(cat /tmp/dashboard.txt | jq -Rs .)\", \"parse_mode\": \"Markdown\"}"
# Slack #all-snappy post (channel id from snappy-infra/auth-reference.md)
curl -s -X POST "$XANO/api:hZB4Dj0c/slack/bot-message" \
-H "Content-Type: application/json" -H "Authorization: Bearer $XANO_METADATA_TOKEN" \
-d '{"channel_id": "C09DD2D0S07", "text": "Weekly dashboard:\n```\n'$(cat /tmp/dashboard.txt)'\n```"}'
```
### Targets snapshot (the red flags to scan for every week)
|metric|target|red flag|source skill|
|------|------|--------|------------|
|Profit margin|>= 60%|< 50%|`snappy-freshbooks`|
|Single client concentration|<= 30%|> 30%|`snappy-freshbooks`|
|Runway (months)|>= 6|< 3|`snappy-freshbooks`|
|Show rate|75-85%|< 60%|`snappy-sales`|
|Close rate (high-ticket)|20-30%|< 10%|`snappy-sales`|
|Active community 14d|> 50%|< 40%|`snappy-skool`|
|Newsletter sends/week|3+|< 3 (yellow)|`snappy-email`|
|Email open rate|10-20% baseline|--|`snappy-email`|
|Email unsub rate|< 1% per send|> 1%|`snappy-email`|
|LinkedIn posts/week|5+|< 5 (yellow)|`snappy-linkedin`|
|YouTube AVD|40-55% (1k-10k subs)|< 30%|`snappy-youtube`|
|CPL (when ads running)|< $25|> $50|`snappy-ads`|
|CP-Call (when ads running)|< $250|> $500|`snappy-ads`|
|Stale leads|0|> 5 (yellow)|`snappy-sales`|
Full target tables in each producer skill's SKILL.md and synthesized in `dashboard-formats.md > Red flag highlighting`.
### The four output formats
|format|trigger|frequency|destination|
|------|-------|---------|-----------|
|Weekly Snappy Dashboard|`snappy-ops` Friday close|weekly|Telegram + optional Slack|
|Monthly Snappy Review|`snappy-ops` last Friday of month|monthly|Notion + Telegram digest|
|Daily Briefing rollup|`snappy-ops` morning brief|Mon-Fri|Terminal inline|
|Funnel Report|"funnel report" trigger|on-demand|Terminal + optional Notion|
Full templates in `dashboard-formats.md`.
---
## What AI Agents Get Wrong
These are the failure modes specific to analytics -- distinct from the format-specific gotchas in `dashboard-formats.md` and the channel/funnel/revenue/content gotchas in each resource file.
|wrong|right|
|-----|-----|
|Compute a conversion rate from < 30 samples ("we had 7 visits and 1 optin, that's 14.3%")|If samples < 30, report counts only. Between 30-100 tag the rate as `(noisy)`. Above 100 the rate is reportable. From `funnel-metrics.md > What AI Agents Get Wrong`.|
|Compare windows of different lengths (last 28d vs last 7d, "this month so far" vs "last month full")|Always normalize: 7d-to-7d, 28d-to-28d, MoM = full month vs full month. Disclose partial months explicitly. From `dashboard-formats.md > Window discipline`.|
|Default to last-touch attribution when first-touch is recoverable|Priority order: manual sales notes > AC original list > first UTM > last UTM > unknown. Disclose attribution model in every report. From `funnel-metrics.md > Attribution priority order`.|
|Treat Calendly bookings as completed calls|`booked != completed`. Show rate is its own metric and a common pipeline gap. Always pull both numbers. From `funnel-metrics.md > What AI Agents Get Wrong`.|
|Compute close rate against booked calls|Close rate uses `closes / completed_calls` -- never `closes / booked`. Show rate is a separate metric.|
|Mix paid + organic numbers in a single funnel rate|Always break down by channel. Paid VSL ~30% optin; organic blog ~5%. Mixing produces meaningless averages. From `funnel-metrics.md > What AI Agents Get Wrong`.|
|Calculate MRR from sent invoices (treating sent as already-revenue)|MRR = paid + sent recurring invoices, normalized to monthly. Outstanding invoices go in receivables, not MRR. From `revenue-metrics.md > What AI Agents Get Wrong`.|
|Include one-time / project invoices in MRR|MRR is recurring only. One-time invoices count toward "revenue this month" but never MRR.|
|Forget to normalize quarterly / annual retainers when computing MRR|Quarterly = `amount / 3`, annual = `amount / 12`. Per `revenue-metrics.md > Recurring Revenue`.|
|Compute ROAS as the only ad ROI metric|`snappy-ads/SKILL.md` mandates **true ROI** using actual collected FreshBooks revenue, not projected ROAS from Hyros. Pair with `snappy-knowledge` UTM tags to attribute clients to ads.|
|Project ARR by multiplying MRR by 12|ARR is misleading for high-ticket consulting where retention is < 12 months. Report MRR + estimated annualized contract value separately.|
|Use the FreshBooks `client` string as a stable join key|Client names are typed by humans. Use `client_id` from `freshbooks/clients` when joining.|
|Hardcode "30%+ open rate" as the email target|`snappy-email/SKILL.md` actually targets 10-20% as the operational baseline. 30%+ is a stretch goal. From `content-metrics.md > Email`.|
|Render the dashboard with no source tags|Every metric line gets `[snappy-X]` so Robert can drill in. Non-negotiable per `dashboard-formats.md > Source labeling`.|
|Drop the "ONE THING" line because nothing dramatic happened|Always pick one. Even "keep doing what's working -- ship Friday post" is the one thing.|
|Send a 6000-char dashboard to Telegram|Telegram limit is 4096 chars. Split into 2-3 messages or post to Slack instead.|
|Use Charlotte MCP browser tools to scrape Studio / Vercel / LinkedIn|Use `agent-browser` per CLAUDE.md. Charlotte MCP browser tools are deprecated.|
|Compare engagement rates across channels (LinkedIn ER vs YouTube AVD vs Email open rate)|Different platforms compute engagement differently. Compare each channel against its own baseline over time, never cross-channel.|
|Pull metrics from cached files older than 24h for the Friday dashboard|Always pull fresh on Friday morning. Studio, AC, Vercel, FreshBooks all update continuously.|
|Treat metrics older than 48h as fresh|Daily-cadence metrics (Skool, Vercel) refresh in hours. Pull at most 24h before assembly.|
|Skip the "unattributed" bucket in the funnel report|Always report unknown-source separately. > 20% unattributed = data hygiene issue.|
|Flag every yellow as urgent|Reserve `[RED]` for must-address. Yellows are watch items, not interrupts.|
|Generate the dashboard then never log it|Save every dashboard to `~/.claude/skills/snappy-analytics/logs/YYYY-WW.md` for trend backref.|
|Compute "rates of rates" (engagement rate of engagement rate)|Meaningless. Average raw counts, then compute the rate once.|
|Use mixed currencies in revenue without conversion|FreshBooks supports multi-currency. Filter by currency or convert to base (USD) and disclose conversion rate.|
|Hit `api:Bd_dCiOz` (Orbiter Xano) with the standard curl token|Orbiter is a separate Xano instance (`xh2o-yths-38lt.n7c.xano.io`). Call via `mcp__xano-mcp__execute` only. Never mix with main Snappy Xano endpoints.|
|Pretend a metric exists when the producer endpoint is missing|Check `data-source-map.md > What's Missing` first. If a metric is gap-flagged, mark it `[NO DATA -- see gap log]` in the output.|
---
## Navigation Guide
Progressive disclosure -- start at SKILL.md, drill into the topic file you need.
|file|when to read|key sections|
|----|------------|-------------|
|`SKILL.md` (this file)|Always -- entry point + workflow|Workflow / Quick Start / Quick Decision Map / What AI Agents Get Wrong|
|`data-source-map.md`|"Where does metric X come from?" / "What can't we measure?"|Producer Skills / Metric -> Skill Index / Endpoint Catalog / Gap Log|
|`content-metrics.md`|Pulling YouTube / LinkedIn / Blog / Email numbers|Per-channel pull procedures + target zones + cross-channel synthesis|
|`revenue-metrics.md`|Pulling MRR / cash / churn / ad ROI|MRR computation rules + churn formulas + true ROI procedure|
|`funnel-metrics.md`|Funnel report / conversion analysis / attribution|Three funnels visual + stage definitions + attribution rules + benchmarks|
|`dashboard-formats.md`|Rendering the synthesized scoreboard|Weekly / Monthly / Daily formats + synthesis rules + red flag thresholds|
---
## Related Skills
Producers (this skill consumes from them):
|skill|why|
|-----|---|
|`snappy-website`|Source of traffic + contact form submissions + UTM-tagged optins. Funnel top-of-stack.|
|`snappy-ads`|Source of ad spend + CPL + CP-Call + ROAS. True ROI is computed by joining ad UTMs to FreshBooks clients.|
|`snappy-youtube`|Source of YouTube views, AVD, CTR, sub delta. Priority is AVD > CTR > Views per `snappy-youtube/analytics.md`.|
|`snappy-linkedin`|Source of organic post impressions and engagement rate (browser-only -- no API for organic LI analytics).|
|`snappy-email`|Source of newsletter sends, opens, clicks, list growth. Xano `emails/list` endpoint (Gmail backend). ActiveCampaign is NOT in use.|
|`snappy-skool`|Source of Skool member counts, posts, classroom, chats, and leaderboards through the measured private frontend API.|
|`snappy-sales`|Source of leads, calls booked / completed, close rate, pipeline value, stale lead count.|
|`snappy-clients`|Source of lifecycle stage tags. Cross-check FreshBooks-derived churn against `snappy-clients` lifecycle changes.|
|`snappy-freshbooks`|Source of MRR, revenue, outstanding, profit margin, runway. Read-only -- never write here from analytics.|
|`snappy-knowledge`|Source of total contacts, dormant, birthdays. UTM tags on contacts power attribution.|
|`snappy-pipeline`|Read-only QA on Orbiter enrichment. Not part of every dashboard pull -- only when Orbiter health is being reviewed.|
|`snappy-content`|Source of cross-channel content velocity (sends per week per channel) and content -> conversion attribution.|
Channels (this skill delivers through them):
|skill|why|
|-----|---|
|`snappy-telegram`|Primary delivery for the Friday dashboard self-message and the daily briefing rollup.|
|`snappy-slack`|Optional `#all-snappy` post for the weekly dashboard and monthly review (`C09DD2D0S07`).|
|`snappy-docs`|Notion archive for monthly review pages (long-form, not subject to Telegram char limit).|
Orchestrator (the skill that calls this one):
|skill|why|
|-----|---|
|`snappy-ops`|The only skill that triggers analytics pulls. Friday close -> Weekly Dashboard. Last Friday of month -> Monthly Review. Morning briefing -> Daily Briefing rollup.|
Infrastructure:
|skill|why|
|-----|---|
|`snappy-infra`|Auth reference (.env.cache / Xano token / channel IDs / API groups).|
|`snappy-browse`|`agent-browser` CLI used to scrape Studio / Vercel / LinkedIn / Skool.|
|`snappy-xano-mcp`|`mcp__xano-mcp__execute` for Orbiter Xano (separate instance).|
---
**Skill Status**: COMPLETE
<!-- SNAPPY-NEAR-NEIGHBOURS-START -->
## Near neighbours
These hands share enough of this one's words that a model can pick the wrong
door. Each row says what the other one is for; open that one instead when its
job is the job.
| Hand | What it is for |
|---|---|
| `snappy-content` | Interview-driven content production methodology, the writing engine for every Snappy channel: the 4-questio... |
| `snappy-corpus` | The Krisp transcript corpus + nugget mining pipeline: import calls into the corpus, find quotes, stories, o... |
| `snappy-freshbooks` | Snappy FreshBooks -- authoritative source for DRAFT invoicing, billing, time tracking, expense logging, rec... |
| `snappy-image` | Centralized image generation, editing, and capture for Snappy: Nano Banana / Gemini, Grok / xAI edits, gpt... |
| `snappy-infra` | Snappy infrastructure foundation -- Xano API surface (Slack, email, LinkedIn, FreshBooks, WhatsApp, calenda... |
| `snappy-linkedin` | LinkedIn operations for Snappy -- posting (text, image, carousel, native video, article, poll, document, co... |
| `snappy-playbook` | WeTube SS mastermind 6-week curriculum source |
| `snappy-telegram` | Telegram Bot API channel for Snappy: direct calls to api.telegram.org (no Xano middleware) to send text, ph... |
| `snappy-testimonials` | Snappy Testimonials -- scans client meeting transcripts (Krisp) and the knowledge graph for positive client... |
| `snappy-transcripts` | Transcript retrieval, search, and processing for Snappy |
| `snappy-video` | Video and audio processing pipeline for Snappy, run on the Mac Mini via SSH (caption-video.sh wrapper aroun... |
| `snappy-xano-mcp` | THE EXISTING, DEPLOYED Snappy MCP server: the Cloudflare Worker exposing the Snappy Xano API (email, calend... |
| `snappy-youtube` | Organic YouTube content creation and channel management for Snappy |
<!-- SNAPPY-NEAR-NEIGHBOURS-END -->
#!/usr/bin/env npx tsx
/**
* snappy-analytics/api.ts -- Metrics aggregator for all snappy-* skills.
*
* Pulls from the content engine DB and synthesizes scorecard data.
* Terminal consumer skill -- read-only across the system.
*
* Usage:
* npx tsx api.ts scorecard # content atom counts by status
*
* Or import as module:
* import { getScorecard } from "../snappy-analytics/api.ts";
*/
import { env } from "../snappy-settings/load.ts";
import { realpathSync } from "fs";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
const CONTENT_ENGINE = "https://rb-content-engine.fly.dev/sql";
async function contentEngineQuery(query: string) {
const res = await fetch(CONTENT_ENGINE, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Content engine query failed (${res.status}): ${text}`);
}
return res.json();
}
// --- Public API ---
/**
* Get content pipeline scorecard -- atom counts by status and type.
*/
export async function getScorecard() {
const [byStatus, byType, recent] = await Promise.all([
contentEngineQuery("SELECT status, COUNT(*) as count FROM content_atoms GROUP BY status ORDER BY count DESC"),
contentEngineQuery("SELECT type, status, COUNT(*) as count FROM content_atoms WHERE type IS NOT NULL GROUP BY type, status ORDER BY count DESC"),
contentEngineQuery("SELECT id, type, topic, status, created_at FROM content_atoms ORDER BY created_at DESC LIMIT 10"),
]);
return { byStatus, byType, recent };
}
// --- CLI ---
/** WHAT THIS HAND ANSWERS, and what each verb does to the world.
* Derived from this file's own CLI dispatch by
* `snappy-hands/contract-derive.ts` — a verb the code does not implement is
* never declared here. Snappy's daemon reads it (`api.ts contract`) to
* validate every call, build the argument words in order, decide whether the
* act runs now or stages for the owner, and hand the child exactly the
* environment keys named in `requires` — never a value, never anything else.
*/
export const HAND_CONTRACT = {
skill: "snappy-analytics",
description: "Centralized analytics and metrics for the entire Snappy operating system. Pulls performance numbers from every producer skill (website, ads, youtube, linkedin, email, community, sales, clients, freshbooks, knowledge, pipeline, content), synthesizes the weekly Snappy Dashboard, monthly review, and on-demand funnel reports. Use this skill when Robert says \"weekly dashboard\", \"monthly review\", \"what's our MRR\", \"pipeline status\", \"funnel report\", \"ad ROI\", \"content scorecard\", \"are we hitting targets\", \"show me the numbers\", \"scorecard\", \"metrics check\", or any cross-skill measurement question.",
managed: false,
requires: [] as string[],
refusals: refusalTable("unknown_verb"),
verbs: {
scorecard: {
args: [], effect: "read",
class: "read", execution: "call", openWorld: false,
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
},
},
} as const;
if (import.meta.url === `file://${realpathSync(process.argv[1])}` && process.argv[2] === "contract") {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
if (import.meta.url === `file://${realpathSync(process.argv[1])}`) {
(async () => {
const [, , cmd] = process.argv;
switch (cmd) {
case "scorecard": {
const data = await getScorecard();
console.log("=== CONTENT ATOMS BY STATUS ===");
if (Array.isArray(data.byStatus)) {
for (const row of data.byStatus) {
console.log(` ${row.status || "null"}: ${row.count}`);
}
} else {
console.log(JSON.stringify(data.byStatus, null, 2));
}
console.log("\n=== BY TYPE + STATUS ===");
if (Array.isArray(data.byType)) {
for (const row of data.byType) {
console.log(` ${row.type || "untyped"} (${row.status}): ${row.count}`);
}
} else {
console.log(JSON.stringify(data.byType, null, 2));
}
console.log("\n=== RECENT ATOMS ===");
if (Array.isArray(data.recent)) {
for (const row of data.recent) {
console.log(` [${row.id}] ${row.type || "untyped"} -- ${row.topic || "(no topic)"} (${row.status}) ${row.created_at}`);
}
} else {
console.log(JSON.stringify(data.recent, null, 2));
}
break;
}
default:
console.log("Usage: npx tsx api.ts scorecard");
}
})();
}
#!/usr/bin/env npx tsx
/**
* snappy-analytics/api.ts -- Metrics aggregator for all snappy-* skills.
*
* Pulls from the content engine DB and synthesizes scorecard data.
* Terminal consumer skill -- read-only across the system.
*
* Usage:
* npx tsx api.ts scorecard # content atom counts by status
*
* Or import as module:
* import { getScorecard } from "../snappy-analytics/api.ts";
*/
import { env } from "../snappy-settings/load.ts";
import { realpathSync } from "fs";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
const CONTENT_ENGINE = "https://rb-content-engine.fly.dev/sql";
async function contentEngineQuery(query: string) {
const res = await fetch(CONTENT_ENGINE, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Content engine query failed (${res.status}): ${text}`);
}
return res.json();
}
// --- Public API ---
/**
* Get content pipeline scorecard -- atom counts by status and type.
*/
export async function getScorecard() {
const [byStatus, byType, recent] = await Promise.all([
contentEngineQuery("SELECT status, COUNT(*) as count FROM content_atoms GROUP BY status ORDER BY count DESC"),
contentEngineQuery("SELECT type, status, COUNT(*) as count FROM content_atoms WHERE type IS NOT NULL GROUP BY type, status ORDER BY count DESC"),
contentEngineQuery("SELECT id, type, topic, status, created_at FROM content_atoms ORDER BY created_at DESC LIMIT 10"),
]);
return { byStatus, byType, recent };
}
// --- CLI ---
/** WHAT THIS HAND ANSWERS, and what each verb does to the world.
* Derived from this file's own CLI dispatch by
* `snappy-hands/contract-derive.ts` — a verb the code does not implement is
* never declared here. Snappy's daemon reads it (`api.ts contract`) to
* validate every call, build the argument words in order, decide whether the
* act runs now or stages for the owner, and hand the child exactly the
* environment keys named in `requires` — never a value, never anything else.
*/
export const HAND_CONTRACT = {
skill: "snappy-analytics",
description: "Centralized analytics and metrics for the entire Snappy operating system. Pulls performance numbers from every producer skill (website, ads, youtube, linkedin, email, community, sales, clients, freshbooks, knowledge, pipeline, content), synthesizes the weekly Snappy Dashboard, monthly review, and on-demand funnel reports. Use this skill when Robert says \"weekly dashboard\", \"monthly review\", \"what's our MRR\", \"pipeline status\", \"funnel report\", \"ad ROI\", \"content scorecard\", \"are we hitting targets\", \"show me the numbers\", \"scorecard\", \"metrics check\", or any cross-skill measurement question.",
managed: false,
requires: [] as string[],
refusals: refusalTable("unknown_verb"),
verbs: {
scorecard: {
args: [], effect: "read",
class: "read", execution: "call", openWorld: false,
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
},
},
} as const;
if (import.meta.url === `file://${realpathSync(process.argv[1])}` && process.argv[2] === "contract") {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
if (import.meta.url === `file://${realpathSync(process.argv[1])}`) {
(async () => {
const [, , cmd] = process.argv;
switch (cmd) {
case "scorecard": {
const data = await getScorecard();
console.log("=== CONTENT ATOMS BY STATUS ===");
if (Array.isArray(data.byStatus)) {
for (const row of data.byStatus) {
console.log(` ${row.status || "null"}: ${row.count}`);
}
} else {
console.log(JSON.stringify(data.byStatus, null, 2));
}
console.log("\n=== BY TYPE + STATUS ===");
if (Array.isArray(data.byType)) {
for (const row of data.byType) {
console.log(` ${row.type || "untyped"} (${row.status}): ${row.count}`);
}
} else {
console.log(JSON.stringify(data.byType, null, 2));
}
console.log("\n=== RECENT ATOMS ===");
if (Array.isArray(data.recent)) {
for (const row of data.recent) {
console.log(` [${row.id}] ${row.type || "untyped"} -- ${row.topic || "(no topic)"} (${row.status}) ${row.created_at}`);
}
} else {
console.log(JSON.stringify(data.recent, null, 2));
}
break;
}
default:
console.log("Usage: npx tsx api.ts scorecard");
}
})();
}
Resource for
snappy-analytics. Pull patterns for every content channel that produces engagement data this skill aggregates.
Credentials load from .env.cache via snappy-settings. Per snappy-infra/auth-reference.md:
bashSNAPPY_SETTINGS_QUIET=1 source ~/.claude/skills/snappy-settings/scripts/load-env.sh
# exports XANO, XANO_METADATA_TOKEN, and all other credentials
# Note: ActiveCampaign is deprecated (email goes via Google) -- no AC_API_KEY
Browser auth state files:
| platform | file |
|---|---|
| YouTube Studio | ~/.openclaw/workspace/youtube-studio-auth.json |
~/.openclaw/workspace/linkedin-auth.json |
|
| Vercel | ~/.openclaw/workspace/vercel-auth.json (if exists; create via browser-connect.sh) |
snappy-youtube/analytics.md#YouTube Analytics API OAuth via Xano is NOT yet wired (gap flagged in snappy-youtube/analytics.md). Until then, scrape Studio via agent-browser.
| metric | window | target_zone (0-1K subs) | target_zone (1K-10K) |
|---|---|---|---|
| Views | 7d, 28d, all-time | 100-300/video at 48h | 300-1K/video at 48h |
| Watch time (hours) | 7d, 28d | -- | -- |
| Subscribers gained (net) | 7d, 28d | positive any week | +5/week minimum |
| CTR (impressions -> views) | per video, 48h | 2-4% | 4-7% |
| AVD (% of video) | per video | 30-40% | 40-55% |
| Top videos by views | 28d | top 5 | top 5 |
| Average view duration (seconds) | per video | -- | -- |
Priority order from snappy-youtube/analytics.md: AVD > CTR > Views. Fix retention before packaging before views.
bash# Standard launch sequence -- kill zombies, open Studio
pkill -9 -f "daemon.js" 2>/dev/null
pkill -9 -f "Chrome for Testing" 2>/dev/null
sleep 1
agent-browser --state ~/.openclaw/workspace/youtube-studio-auth.json \
open https://studio.youtube.com
agent-browser wait 5000
# Click Analytics in left sidebar (per CLAUDE.md -- never construct URLs blindly)
agent-browser find text "Analytics" click
agent-browser wait 3000
# Set time range to last 7 days (default is 28d)
agent-browser find text "Last 28 days" click
agent-browser wait 1000
agent-browser find text "Last 7 days" click
agent-browser wait 2000
# Screenshot the overview
agent-browser screenshot /tmp/yt-analytics-7d.png --full
# Repeat for 28 days
agent-browser find text "Last 7 days" click
agent-browser wait 1000
agent-browser find text "Last 28 days" click
agent-browser wait 2000
agent-browser screenshot /tmp/yt-analytics-28d.png --full
Read both screenshots and extract: views, watch time hours, subs gained, CTR overview, top videos.
youtube/video-stats lives#If snappy-youtube/analytics.md upgrades the endpoint to "live", swap to:
bashcurl -s "$XANO/api:PB9UH7b9/youtube/video-stats?video_id=VIDEO_ID" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
Until then agent-browser is the canonical path.
For optimization triggers (CTR < 3%, AVD < 30%), drill into each video via Studio:
bashagent-browser find text "Content" click
agent-browser wait 2000
agent-browser find text "TARGET VIDEO TITLE" click
agent-browser wait 2000
agent-browser find text "Analytics" click
agent-browser wait 3000
agent-browser screenshot /tmp/yt-video-deep-VIDEO_ID.png --full
Read screenshot. Apply triggers from snappy-youtube/analytics.md > Optimization Triggers.
snappy-linkedin/SKILL.md#LinkedIn organic post analytics is browser-only. The LinkedIn API only exposes paid analytics. Use agent-browser against linkedin.com/in/robert-boulos/recent-activity/all/.
| metric | window | target_zone |
|---|---|---|
| Post impressions | 7d, 28d (sum) | >= 1k/week baseline; trending up |
| Engagement rate | per post | > 2% baseline; > 5% strong |
| Reactions + comments + shares | per post | 3-5 comments/post baseline |
| Profile views | 7d, 28d | trending up |
| Followers gained (net) | 7d, 28d | positive any week |
| Top 3 posts by impressions | 28d | top 3 |
Engagement rate formula: (reactions + comments + shares) / impressions * 100. NEVER count clicks as engagement -- LinkedIn doesn't expose click-through reliably for organic.
bashpkill -9 -f "daemon.js" 2>/dev/null
pkill -9 -f "Chrome for Testing" 2>/dev/null
sleep 1
# Open recent activity
agent-browser --state ~/.openclaw/workspace/linkedin-auth.json \
open https://www.linkedin.com/in/robert-boulos/recent-activity/all/
agent-browser wait 3000
# Verify auth (NOT "Sign in")
agent-browser snapshot -i 2>&1 | head -10
# Extract per-post impressions for last 7 days
agent-browser extract "for each post in the feed, get the post text first 60 chars, the impression count, and the reaction + comment + share counts"
For follower delta, navigate to profile -> Analytics & Tools:
bashagent-browser find text "Me" click
agent-browser wait 1000
agent-browser find text "View Profile" click
agent-browser wait 2000
agent-browser find text "Analytics & tools" click
agent-browser wait 2000
agent-browser screenshot /tmp/li-analytics.png --full
Read and extract: impression total (28d), engagement rate, profile views, followers gained.
snappy-linkedin#snappy-linkedin/posting-types.md documents which post format Robert ran on which day. Pair the analytics output with that calendar to attribute engagement to format (text vs carousel vs video).
snappy-website/conversion-tracking.md#Blog hosted on Next.js on Vercel. Page analytics via Vercel Analytics dashboard (web UI only -- no public REST API as of model cutoff).
| metric | window | target_zone |
|---|---|---|
| Total page views | 7d, 28d | trending up |
| Unique visitors | 7d, 28d | trending up |
| Top blog pages | 7d, 28d | top 5 |
| Bounce rate | 7d | < 60% |
| Avg session duration | 7d | > 90s for blog readers |
| Traffic sources breakdown | 7d, 28d | organic vs social vs direct vs referral |
| Blog -> contact form conversion | 28d | >= 1% blog reader -> form submit |
| Blog -> email signup conversion | 28d | >= 5% blog reader -> email |
bashagent-browser --state ~/.openclaw/workspace/vercel-auth.json \
open https://vercel.com
agent-browser wait 3000
# Click into snappy-ai project
agent-browser find text "snappy-ai" click
agent-browser wait 2000
# Navigate to Analytics tab
agent-browser find text "Analytics" click
agent-browser wait 3000
# Default range is last 24h -- switch to 7 days
agent-browser find text "Last 24 hours" click
agent-browser wait 500
agent-browser find text "Last 7 days" click
agent-browser wait 2000
agent-browser screenshot /tmp/vercel-analytics-7d.png --full
For contact form conversions, pair with the Xano contact/submissions pull from revenue-metrics.md (cross-skill data).
UTM parameters captured by the contact form (per snappy-website/conversion-tracking.md) make blog attribution possible:
bashcurl -s "$XANO/api:PB9UH7b9/contact/submissions" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.utm_medium == "blog" or .utm_source == "blog")]'
Count = blog-attributed contact submissions for the period. Compare to blog page views from Vercel for the conversion rate.
ActiveCampaign is NOT in use. All email metrics come from Xano
emails/list(Gmail backend). AC REST API references below are deprecated.
snappy-email/SKILL.md + Xano api:PB9UH7b9/emails/send (Gmail backend)GET api:PB9UH7b9/emails/list| metric | window | target (per snappy-email/SKILL.md) |
|---|---|---|
| Emails sent | weekly | 3+ per week minimum |
| Open rate | per send + weekly avg | 10-20% baseline |
| Click rate | per send + weekly avg | 1-3% baseline |
| Reply rate | per send | track manually until automated |
| Unsubscribe rate | per send | < 1% |
| List size | weekly | growing |
| List growth (net) | weekly | +10/week |
| Deliverability | weekly | > 95% |
| Top 3 campaigns by opens | 28d | top 3 |
| Calls booked from email | weekly | 2+/week (the only metric that matters) |
Note: older sections of
snappy-analyticslisted "30%+ open rate" -- that's an aspirational/general benchmark. Thesnappy-email/SKILL.mdMetrics section actually lists 10-20% as the operational baseline. Use 10-20% as the working target and treat 30%+ as a stretch goal.
ActiveCampaign is NOT in use. The AC REST API endpoints below are historical. Email metrics come from Xano
emails/list(Gmail backend) or Loops.so for bulk sends.
bash# All email send events recorded by Xano
curl -s "$XANO/api:PB9UH7b9/emails/list" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
Filter by date range client-side. Use this for the "emails sent this week" count when AC sync is delayed.
Cross-reference with snappy-sales lead source field:
bashcurl -s "$XANO/api:PB9UH7b9/leads?source=email&created_after=$(date -v-7d +%Y-%m-%d)" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
Or pull from Calendly via snappy-calendar if Calendly source field is set.
Each week, count outputs across all channels:
| channel | target_per_week | actual_this_week | skill |
|---|---|---|---|
| Newsletter sends | 3+ | ? | snappy-email |
| LinkedIn posts | 5+ | ? | snappy-linkedin |
| YouTube videos | 1+ | ? | snappy-youtube |
| Blog posts | 1+ | ? | snappy-blog -> snappy-publish |
| Skool community posts | 3-5 | ? | snappy-skool |
If any channel < target, flag in Friday close report.
Compare engagement rate across channels to find what's working:
| channel | engagement_definition | baseline | good | great |
|---|---|---|---|---|
| YouTube | AVD % | 30% | 40% | 55%+ |
(reactions + comments + shares) / impressions |
2% | 5% | 10%+ | |
opens / sends |
10% | 20% | 30%+ | |
| Blog | session_duration > 90s rate |
-- | 50% | 70%+ |
| Skool | comments / posts |
1 | 3 | 5+ |
NEVER directly compare engagement rates between channels (different definitions). Use this only to track each channel against its own baseline over time.
Build a single view of content -> contact form submission per week:
bash# Get contact submissions for the period
curl -s "$XANO/api:PB9UH7b9/contact/submissions" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.created_at >= "'$(date -v-7d +%Y-%m-%d)'")] | group_by(.utm_source) | map({source: .[0].utm_source, count: length})'
This breaks contact form submissions down by UTM source. Cross-check against the publishing log for each channel to attribute submissions back to specific posts.
| wrong | right | |
|---|---|---|
| Mix 7d and 28d windows in one comparison ("YouTube views are up -- 1.2K this week vs 4.8K last month") | Always compare same window length. Last 7d vs prior 7d. Last 28d vs prior 28d. Disclose window in every line. | |
| Treat raw view counts as the primary signal | YouTube priority is AVD > CTR > Views. LinkedIn priority is engagement rate > impressions. Fix retention/quality before optimizing reach. | |
| Compare engagement rates across channels | Different platforms compute engagement differently. Compare channel against its own baseline over time, never cross-channel. | |
| Hardcode the "30%+ open rate" target from old marketing playbooks | snappy-email/SKILL.md actually targets 10-20% as the baseline. 30%+ is aspirational. |
|
Count status=2 (unsubscribed) or status=3 (bounced) AC contacts in list size |
Filter to status=1 only. |
|
| Use Charlotte MCP browser tools to scrape Studio / Vercel / LinkedIn | Use agent-browser per CLAUDE.md. Charlotte MCP browser tools are deprecated. |
|
| Pull only the latest screenshot and ignore window state | Always set the time range explicitly (Last 7 days vs Last 28 days) before screenshotting. Default is rarely what you want. | |
| Skip auth verification | Always run `agent-browser snapshot -i \ | head -10` after launch and confirm you're logged in (not seeing "Sign in"). |
| Compute "growth" by subtracting two snapshot numbers without normalizing | If you have weekly cadence numbers, growth = (this_week - last_week) / last_week. If your snapshots are 9 days apart, normalize to 7 days first. |
|
| Attribute conversions to last touch when first-touch is unknown | If you only have UTM on the contact form, mark the attribution as "last-touch only" in the report. Don't pretend it's full multi-touch. | |
| Read scorecard data from cached files older than 48h | Always pull fresh. Studio, AC, Vercel all update continuously. | |
| Trust per-post LinkedIn impressions extracted via brittle CSS selectors | Use agent-browser extract "..." (AI-driven) -- it's resilient to UI changes that break selectors. |
|
| Compare YouTube CTR across videos with different impression volumes | Below ~1000 impressions, CTR is noisy. Wait for the threshold per snappy-youtube/analytics.md > Kill/Keep Rules. |
# Content Metrics -- YouTube, LinkedIn, Blog, Email
> Resource for `snappy-analytics`. Pull patterns for every content channel that produces engagement data this skill aggregates.
## Table of Contents
- [Auth (shared)](#auth-shared)
- [YouTube](#youtube)
- [LinkedIn](#linkedin)
- [Blog (snappy.ai)](#blog-snappyai)
- [Email (Xano / Gmail)](#email-xano--gmail)
- [Cross-Channel Synthesis](#cross-channel-synthesis)
- [What AI Agents Get Wrong (content)](#what-ai-agents-get-wrong-content)
---
## Auth (shared)
Credentials load from `.env.cache` via `snappy-settings`. Per `snappy-infra/auth-reference.md`:
```bash
SNAPPY_SETTINGS_QUIET=1 source ~/.claude/skills/snappy-settings/scripts/load-env.sh
# exports XANO, XANO_METADATA_TOKEN, and all other credentials
# Note: ActiveCampaign is deprecated (email goes via Google) -- no AC_API_KEY
```
Browser auth state files:
|platform|file|
|--------|----|
|YouTube Studio|`~/.openclaw/workspace/youtube-studio-auth.json`|
|LinkedIn|`~/.openclaw/workspace/linkedin-auth.json`|
|Vercel|`~/.openclaw/workspace/vercel-auth.json` (if exists; create via browser-connect.sh)|
---
## YouTube
### Authoritative source: `snappy-youtube/analytics.md`
YouTube Analytics API OAuth via Xano is **NOT yet wired** (gap flagged in `snappy-youtube/analytics.md`). Until then, scrape Studio via `agent-browser`.
### Metrics to capture per pull
|metric|window|target_zone (0-1K subs)|target_zone (1K-10K)|
|------|------|---------------------|---------------------|
|Views|7d, 28d, all-time|100-300/video at 48h|300-1K/video at 48h|
|Watch time (hours)|7d, 28d|--|--|
|Subscribers gained (net)|7d, 28d|positive any week|+5/week minimum|
|CTR (impressions -> views)|per video, 48h|2-4%|4-7%|
|AVD (% of video)|per video|30-40%|40-55%|
|Top videos by views|28d|top 5|top 5|
|Average view duration (seconds)|per video|--|--|
Priority order from `snappy-youtube/analytics.md`: **AVD > CTR > Views**. Fix retention before packaging before views.
### Pull procedure (until Xano endpoint exists)
```bash
# Standard launch sequence -- kill zombies, open Studio
pkill -9 -f "daemon.js" 2>/dev/null
pkill -9 -f "Chrome for Testing" 2>/dev/null
sleep 1
agent-browser --state ~/.openclaw/workspace/youtube-studio-auth.json \
open https://studio.youtube.com
agent-browser wait 5000
# Click Analytics in left sidebar (per CLAUDE.md -- never construct URLs blindly)
agent-browser find text "Analytics" click
agent-browser wait 3000
# Set time range to last 7 days (default is 28d)
agent-browser find text "Last 28 days" click
agent-browser wait 1000
agent-browser find text "Last 7 days" click
agent-browser wait 2000
# Screenshot the overview
agent-browser screenshot /tmp/yt-analytics-7d.png --full
# Repeat for 28 days
agent-browser find text "Last 7 days" click
agent-browser wait 1000
agent-browser find text "Last 28 days" click
agent-browser wait 2000
agent-browser screenshot /tmp/yt-analytics-28d.png --full
```
Read both screenshots and extract: views, watch time hours, subs gained, CTR overview, top videos.
### When `youtube/video-stats` lives
If `snappy-youtube/analytics.md` upgrades the endpoint to "live", swap to:
```bash
curl -s "$XANO/api:PB9UH7b9/youtube/video-stats?video_id=VIDEO_ID" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
```
Until then `agent-browser` is the canonical path.
### Per-video deep dive
For optimization triggers (CTR < 3%, AVD < 30%), drill into each video via Studio:
```bash
agent-browser find text "Content" click
agent-browser wait 2000
agent-browser find text "TARGET VIDEO TITLE" click
agent-browser wait 2000
agent-browser find text "Analytics" click
agent-browser wait 3000
agent-browser screenshot /tmp/yt-video-deep-VIDEO_ID.png --full
```
Read screenshot. Apply triggers from `snappy-youtube/analytics.md > Optimization Triggers`.
---
## LinkedIn
### Authoritative source: `snappy-linkedin/SKILL.md`
LinkedIn organic post analytics is **browser-only**. The LinkedIn API only exposes paid analytics. Use `agent-browser` against `linkedin.com/in/robert-boulos/recent-activity/all/`.
### Metrics to capture per pull
|metric|window|target_zone|
|------|------|-----------|
|Post impressions|7d, 28d (sum)|>= 1k/week baseline; trending up|
|Engagement rate|per post|> 2% baseline; > 5% strong|
|Reactions + comments + shares|per post|3-5 comments/post baseline|
|Profile views|7d, 28d|trending up|
|Followers gained (net)|7d, 28d|positive any week|
|Top 3 posts by impressions|28d|top 3|
Engagement rate formula: `(reactions + comments + shares) / impressions * 100`. NEVER count clicks as engagement -- LinkedIn doesn't expose click-through reliably for organic.
### Pull procedure
```bash
pkill -9 -f "daemon.js" 2>/dev/null
pkill -9 -f "Chrome for Testing" 2>/dev/null
sleep 1
# Open recent activity
agent-browser --state ~/.openclaw/workspace/linkedin-auth.json \
open https://www.linkedin.com/in/robert-boulos/recent-activity/all/
agent-browser wait 3000
# Verify auth (NOT "Sign in")
agent-browser snapshot -i 2>&1 | head -10
# Extract per-post impressions for last 7 days
agent-browser extract "for each post in the feed, get the post text first 60 chars, the impression count, and the reaction + comment + share counts"
```
For follower delta, navigate to profile -> Analytics & Tools:
```bash
agent-browser find text "Me" click
agent-browser wait 1000
agent-browser find text "View Profile" click
agent-browser wait 2000
agent-browser find text "Analytics & tools" click
agent-browser wait 2000
agent-browser screenshot /tmp/li-analytics.png --full
```
Read and extract: impression total (28d), engagement rate, profile views, followers gained.
### Cross-link to `snappy-linkedin`
`snappy-linkedin/posting-types.md` documents which post format Robert ran on which day. Pair the analytics output with that calendar to attribute engagement to format (text vs carousel vs video).
---
## Blog (snappy.ai)
### Authoritative source: `snappy-website/conversion-tracking.md`
Blog hosted on Next.js on Vercel. Page analytics via Vercel Analytics dashboard (web UI only -- no public REST API as of model cutoff).
### Metrics to capture per pull
|metric|window|target_zone|
|------|------|-----------|
|Total page views|7d, 28d|trending up|
|Unique visitors|7d, 28d|trending up|
|Top blog pages|7d, 28d|top 5|
|Bounce rate|7d|< 60%|
|Avg session duration|7d|> 90s for blog readers|
|Traffic sources breakdown|7d, 28d|organic vs social vs direct vs referral|
|Blog -> contact form conversion|28d|>= 1% blog reader -> form submit|
|Blog -> email signup conversion|28d|>= 5% blog reader -> email|
### Pull procedure
```bash
agent-browser --state ~/.openclaw/workspace/vercel-auth.json \
open https://vercel.com
agent-browser wait 3000
# Click into snappy-ai project
agent-browser find text "snappy-ai" click
agent-browser wait 2000
# Navigate to Analytics tab
agent-browser find text "Analytics" click
agent-browser wait 3000
# Default range is last 24h -- switch to 7 days
agent-browser find text "Last 24 hours" click
agent-browser wait 500
agent-browser find text "Last 7 days" click
agent-browser wait 2000
agent-browser screenshot /tmp/vercel-analytics-7d.png --full
```
For contact form conversions, pair with the Xano `contact/submissions` pull from `revenue-metrics.md` (cross-skill data).
### Conversion attribution
UTM parameters captured by the contact form (per `snappy-website/conversion-tracking.md`) make blog attribution possible:
```bash
curl -s "$XANO/api:PB9UH7b9/contact/submissions" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.utm_medium == "blog" or .utm_source == "blog")]'
```
Count = blog-attributed contact submissions for the period. Compare to blog page views from Vercel for the conversion rate.
---
## Email (Xano / Gmail)
> **ActiveCampaign is NOT in use.** All email metrics come from Xano `emails/list` (Gmail backend). AC REST API references below are deprecated.
### Authoritative sources
- Transactional sends: `snappy-email/SKILL.md` + Xano `api:PB9UH7b9/emails/send` (Gmail backend)
- Send history: `GET api:PB9UH7b9/emails/list`
### Metrics to capture per pull
|metric|window|target (per `snappy-email/SKILL.md`)|
|------|------|--------------------------------------|
|Emails sent|weekly|3+ per week minimum|
|Open rate|per send + weekly avg|10-20% baseline|
|Click rate|per send + weekly avg|1-3% baseline|
|Reply rate|per send|track manually until automated|
|Unsubscribe rate|per send|< 1%|
|List size|weekly|growing|
|List growth (net)|weekly|+10/week|
|Deliverability|weekly|> 95%|
|Top 3 campaigns by opens|28d|top 3|
|Calls booked from email|weekly|2+/week (the only metric that matters)|
> Note: older sections of `snappy-analytics` listed "30%+ open rate" -- that's an aspirational/general benchmark. The `snappy-email/SKILL.md` Metrics section actually lists 10-20% as the operational baseline. Use 10-20% as the working target and treat 30%+ as a stretch goal.
### ~~Pull via ActiveCampaign REST API~~ (DEPRECATED)
> **ActiveCampaign is NOT in use.** The AC REST API endpoints below are historical. Email metrics come from Xano `emails/list` (Gmail backend) or Loops.so for bulk sends.
### Pull via Xano (primary path)
```bash
# All email send events recorded by Xano
curl -s "$XANO/api:PB9UH7b9/emails/list" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
```
Filter by date range client-side. Use this for the "emails sent this week" count when AC sync is delayed.
### Calls booked from email attribution
Cross-reference with `snappy-sales` lead source field:
```bash
curl -s "$XANO/api:PB9UH7b9/leads?source=email&created_after=$(date -v-7d +%Y-%m-%d)" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
```
Or pull from Calendly via `snappy-calendar` if Calendly source field is set.
---
## Cross-Channel Synthesis
### Content velocity tracker
Each week, count outputs across all channels:
|channel|target_per_week|actual_this_week|skill|
|-------|---------------|----------------|-----|
|Newsletter sends|3+|?|`snappy-email`|
|LinkedIn posts|5+|?|`snappy-linkedin`|
|YouTube videos|1+|?|`snappy-youtube`|
|Blog posts|1+|?|`snappy-blog` -> `snappy-publish`|
|Skool community posts|3-5|?|`snappy-skool`|
If any channel < target, flag in Friday close report.
### Engagement leaderboard (channel comparison)
Compare engagement rate across channels to find what's working:
|channel|engagement_definition|baseline|good|great|
|-------|---------------------|--------|----|-----|
|YouTube|AVD %|30%|40%|55%+|
|LinkedIn|`(reactions + comments + shares) / impressions`|2%|5%|10%+|
|Email|`opens / sends`|10%|20%|30%+|
|Blog|`session_duration > 90s` rate|--|50%|70%+|
|Skool|`comments / posts`|1|3|5+|
NEVER directly compare engagement rates between channels (different definitions). Use this only to track each channel against its own baseline over time.
### Content -> conversion path (attribution)
Build a single view of content -> contact form submission per week:
```bash
# Get contact submissions for the period
curl -s "$XANO/api:PB9UH7b9/contact/submissions" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.created_at >= "'$(date -v-7d +%Y-%m-%d)'")] | group_by(.utm_source) | map({source: .[0].utm_source, count: length})'
```
This breaks contact form submissions down by UTM source. Cross-check against the publishing log for each channel to attribute submissions back to specific posts.
---
## What AI Agents Get Wrong (content)
|wrong|right|
|-----|-----|
|Mix 7d and 28d windows in one comparison ("YouTube views are up -- 1.2K this week vs 4.8K last month")|Always compare same window length. Last 7d vs prior 7d. Last 28d vs prior 28d. Disclose window in every line.|
|Treat raw view counts as the primary signal|YouTube priority is **AVD > CTR > Views**. LinkedIn priority is **engagement rate > impressions**. Fix retention/quality before optimizing reach.|
|Compare engagement rates across channels|Different platforms compute engagement differently. Compare channel against its own baseline over time, never cross-channel.|
|Hardcode the "30%+ open rate" target from old marketing playbooks|`snappy-email/SKILL.md` actually targets 10-20% as the baseline. 30%+ is aspirational.|
|Count `status=2` (unsubscribed) or `status=3` (bounced) AC contacts in list size|Filter to `status=1` only.|
|Use Charlotte MCP browser tools to scrape Studio / Vercel / LinkedIn|Use `agent-browser` per CLAUDE.md. Charlotte MCP browser tools are deprecated.|
|Pull only the latest screenshot and ignore window state|Always set the time range explicitly (Last 7 days vs Last 28 days) before screenshotting. Default is rarely what you want.|
|Skip auth verification|Always run `agent-browser snapshot -i \| head -10` after launch and confirm you're logged in (not seeing "Sign in").|
|Compute "growth" by subtracting two snapshot numbers without normalizing|If you have weekly cadence numbers, growth = `(this_week - last_week) / last_week`. If your snapshots are 9 days apart, normalize to 7 days first.|
|Attribute conversions to last touch when first-touch is unknown|If you only have UTM on the contact form, mark the attribution as "last-touch only" in the report. Don't pretend it's full multi-touch.|
|Read scorecard data from cached files older than 48h|Always pull fresh. Studio, AC, Vercel all update continuously.|
|Trust per-post LinkedIn impressions extracted via brittle CSS selectors|Use `agent-browser extract "..."` (AI-driven) -- it's resilient to UI changes that break selectors.|
|Compare YouTube CTR across videos with different impression volumes|Below ~1000 impressions, CTR is noisy. Wait for the threshold per `snappy-youtube/analytics.md > Kill/Keep Rules`.|
import { test } from "node:test";
import assert from "node:assert/strict";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES, type RefusalCode } from "../snappy-settings/refusal-codes.ts";
/** RULE 33 COVERAGE, AGAINST THE ONE CLOSED TABLE ⟨lane r30, 2026-09-09⟩.
* This file used to restate a refusal row's own properties — that it has a
* `contract_slice`, that it has a `fix`, that it leaks no token — once per
* hand, 48 times, over rows that all come from the SAME object. Forty-eight
* copies of one check is the duplicate road the closed table exists to end:
* `snappy-settings/refusal-codes.test.ts` runs those checks ONCE over every
* row, and the second test below proves this hand carries THE ROW ITSELF and
* not a copy — an identity a drifted duplicate cannot fake. Before this, each
* hand hand-wrote its own row, and the wording had already drifted: the
* inline `unknown_verb` said "Call one of the verbs named in
* HAND_CONTRACT.verbs" while the closed table says "Call one of the verbs the
* contract declares; the refusal lists them."
*
* DECLARED stays a literal list, deliberately. It is this hand's coverage
* manifest and it is what rule 33's lint reads out of the test SOURCE to ask
* "did a person look at this code" — deriving it from Object.keys would make
* the test pass for a hand with no refusals at all. `satisfies readonly
* RefusalCode[]` makes the compiler refuse a name the one table does not
* have. It NAMES codes; it no longer DEFINES them. */
const DECLARED = [
"unknown_verb",
] as const satisfies readonly RefusalCode[];
test("snappy-analytics: the refusal table declares exactly the codes this test names", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals ?? {}).sort(), [...DECLARED].sort());
});
test("snappy-analytics: every declared refusal is the one closed table's own row, never a copy", () => {
const declared = Object.entries(HAND_CONTRACT.refusals ?? {});
assert.ok(declared.length > 0, "HAND_CONTRACT.refusals is empty");
for (const [code, row] of declared) {
assert.ok(code in REFUSAL_CODES, `${code} is not a row of snappy-settings/refusal-codes.ts`);
assert.equal(row, REFUSAL_CODES[code as RefusalCode], `${code} is a copy of the closed table's row, not the row itself`);
}
});
import { test } from "node:test";
import assert from "node:assert/strict";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES, type RefusalCode } from "../snappy-settings/refusal-codes.ts";
/** RULE 33 COVERAGE, AGAINST THE ONE CLOSED TABLE ⟨lane r30, 2026-09-09⟩.
* This file used to restate a refusal row's own properties — that it has a
* `contract_slice`, that it has a `fix`, that it leaks no token — once per
* hand, 48 times, over rows that all come from the SAME object. Forty-eight
* copies of one check is the duplicate road the closed table exists to end:
* `snappy-settings/refusal-codes.test.ts` runs those checks ONCE over every
* row, and the second test below proves this hand carries THE ROW ITSELF and
* not a copy — an identity a drifted duplicate cannot fake. Before this, each
* hand hand-wrote its own row, and the wording had already drifted: the
* inline `unknown_verb` said "Call one of the verbs named in
* HAND_CONTRACT.verbs" while the closed table says "Call one of the verbs the
* contract declares; the refusal lists them."
*
* DECLARED stays a literal list, deliberately. It is this hand's coverage
* manifest and it is what rule 33's lint reads out of the test SOURCE to ask
* "did a person look at this code" — deriving it from Object.keys would make
* the test pass for a hand with no refusals at all. `satisfies readonly
* RefusalCode[]` makes the compiler refuse a name the one table does not
* have. It NAMES codes; it no longer DEFINES them. */
const DECLARED = [
"unknown_verb",
] as const satisfies readonly RefusalCode[];
test("snappy-analytics: the refusal table declares exactly the codes this test names", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals ?? {}).sort(), [...DECLARED].sort());
});
test("snappy-analytics: every declared refusal is the one closed table's own row, never a copy", () => {
const declared = Object.entries(HAND_CONTRACT.refusals ?? {});
assert.ok(declared.length > 0, "HAND_CONTRACT.refusals is empty");
for (const [code, row] of declared) {
assert.ok(code in REFUSAL_CODES, `${code} is not a row of snappy-settings/refusal-codes.ts`);
assert.equal(row, REFUSAL_CODES[code as RefusalCode], `${code} is a copy of the closed table's row, not the row itself`);
}
});
Resource for
snappy-analytics. Output formats for the synthesized scoreboard. Every dashboard pull lands in one of these layouts before going to Telegram / Slack / Notion.
| dashboard | primary destination | secondary | frequency | owner trigger |
|---|---|---|---|---|
| Weekly Snappy Dashboard | Telegram (snappy-telegram self-message) |
snappy-slack #all-snappy (optional) |
Friday morning | snappy-ops Friday close |
| Monthly Snappy Review | Notion page (snappy-docs) |
Telegram digest, Slack #all-snappy |
Last Friday of month | snappy-ops monthly close |
| Daily Briefing rollup | Terminal (during snappy-ops brief) |
-- | Daily Mon-Fri | snappy-ops morning briefing |
| Funnel Report | Terminal output | Notion (if requested) | On-demand | Robert says "funnel report" |
| Ad-hoc metric checks | Terminal | -- | On-demand | Any "what's our X" question |
bash# Telegram delivery (per snappy-telegram pattern)
# Assumes .env.cache already loaded via:
# SNAPPY_SETTINGS_QUIET=1 source ~/.claude/skills/snappy-settings/scripts/load-env.sh
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\": \"${TELEGRAM_ROBERT_CHAT_ID}\", \"text\": \"$(cat /tmp/dashboard.txt | jq -Rs .)\", \"parse_mode\": \"Markdown\"}"
# Slack delivery (per snappy-slack pattern)
curl -s -X POST "$XANO/api:hZB4Dj0c/slack/bot-message" \
-H "Content-Type: application/json" -H "Authorization: Bearer $XANO_METADATA_TOKEN" \
-d '{"channel_id": "C09DD2D0S07", "text": "Weekly dashboard:\n```\n'$(cat /tmp/dashboard.txt)'\n```"}'
#all-snappy channel ID C09DD2D0S07 per snappy-infra/auth-reference.md > Quick Reference Constants.
The flagship output. Pulled every Friday during snappy-ops Friday close.
=== SNAPPY WEEKLY DASHBOARD ===
Week of YYYY-MM-DD (last 7 days)
Generated: YYYY-MM-DD HH:MM (timezone)
REVENUE [snappy-freshbooks]
MRR: $X,XXX (MoM: +X.X%)
Revenue this month: $X,XXX (vs target $Y,YYY)
Cash collected (7d): $X,XXX
Outstanding: $X,XXX (X invoices)
Active clients: X (vs X last week)
Avg revenue/client: $X,XXX
Top concentration: XX% (Client Name)
[Red flag: ___ if any]
CONTENT [snappy-content channels]
Newsletter sends (7d): X (target: 3+)
LinkedIn posts (7d): X (target: 5+)
YouTube videos (7d): X (target: 1+)
Blog posts (7d): X (target: 1+)
Skool posts (7d): X (target: 3-5)
YouTube views (7d): X,XXX (vs prior 7d: +/-X%)
YouTube subs net (7d): +XX
YouTube top video: "Title" -- XXX views
LinkedIn impressions: X,XXX
LinkedIn engagement: X.X%
LinkedIn followers: +XX
Blog pageviews (7d): X,XXX
Blog top page: /blog/slug
EMAIL [snappy-email + Xano/Gmail]
List size: X,XXX (Δ +XX this week)
Open rate (7d avg): XX.X% (target 10-20%)
Click rate (7d avg): X.X% (target 1-3%)
Unsub rate (7d avg): X.X% (target < 1%)
Calls booked from email: X (target 2+)
ADS [snappy-ads -- STATUS: paused / running]
Spend (7d): $XXX (or "no spend this week")
CPL: $XX.XX (target < $25)
CP-Call: $XXX.XX (target < $250)
ROAS: X.Xx (target 3x+)
True ROI (rolling 30d): X.Xx
COMMUNITY [snappy-skool]
Members: XXX (Δ +X this week)
New members: +X
Active 14d: XX% (target > 50%)
Posts (7d): XX
Unanswered posts: X (target < 5)
Top contributor: @handle
PIPELINE [snappy-sales]
Leads in (7d): X
Calls booked (7d): X
Calls completed (7d): X (show rate XX%)
Deals closed (7d): X (close rate XX%)
Pipeline value: $X,XXX
Stale leads (>7d): X (target 0)
KNOWLEDGE [snappy-knowledge]
Total contacts: XXX (Δ +X this week)
VIP contacts: XX
Dormant contacts (30d): XX
Birthdays this week: X
ENRICHMENT [snappy-pipeline / Orbiter only]
Health: GREEN / YELLOW / RED
Stuck records: XX
Dominant failure: Source N (X% of stuck)
THE ONE THING THIS WEEK
[Single highest-leverage action drawn from the above]
NOTABLE WINS
- [Top 3 wins this week]
CONCERNS
- [Top 3 concerns / red flags]
NEXT WEEK FOCUS
- [Top 3 priorities for next week]
## Workflow > Weekly Dashboard Pull).[snappy-X] so Robert knows where to drill in.revenue-metrics.md > Targets and Red Flags, funnel-metrics.md > Benchmarks, content-metrics.md).snappy-ops/SKILL.md > Operations Philosophy.Run last Friday of each month. Extends the weekly with monthly trends and bigger-picture moves.
=== SNAPPY MONTHLY REVIEW ===
Month: YYYY-MM
Generated: YYYY-MM-DD
REVENUE
This Month Last Month Δ MoM YTD
MRR $X,XXX $X,XXX +X.X% $X,XXX
Revenue (paid) $X,XXX $X,XXX +X.X% $X,XXX
Cash collected $X,XXX $X,XXX +X.X% $X,XXX
Active clients X X +X --
ARPU $X,XXX $X,XXX +X.X% --
Top concentration XX% XX% ±X.Xpp --
Churn (monthly) X.X% X.X% ±X.Xpp --
PROFITABILITY
This Month Last Month
Total revenue $X,XXX $X,XXX
Total expenses $X,XXX $X,XXX
Profit margin XX% XX%
Top expense category SaaS $X,XXX Ads $X,XXX
Runway (months) X.X X.X
PIPELINE
This Month Last Month Δ
Leads in XX XX ±X
Calls booked XX XX ±X
Calls completed XX XX ±X
Show rate XX% XX% ±X.Xpp
Closes X X ±X
Close rate XX% XX% ±X.Xpp
Avg deal size $X,XXX $X,XXX ±$X
Pipeline value (open) $X,XXX $X,XXX ±$X
CONTENT
This Month Last Month Trend
Newsletter sends XX XX ↑/↓/→
LinkedIn posts XX XX ↑/↓/→
YouTube videos X X ↑/↓/→
Blog posts X X ↑/↓/→
YouTube views (28d) X,XXX X,XXX ±X%
YouTube subs (28d) +XX +XX ±X
LinkedIn impressions X,XXX X,XXX ±X%
Blog pageviews X,XXX X,XXX ±X%
ADS (if running)
This Month Last Month Δ
Spend $X,XXX $X,XXX ±$X
Leads (paid) XX XX ±X
Calls (paid) X X ±X
Closes (paid) X X ±X
CPL $XX $XX ±$X
CP-Call $XXX $XXX ±$X
CPA $X,XXX $X,XXX ±$X
True ROI (collected) X.Xx X.Xx ±X.X
COMMUNITY
This Month Last Month Trend
Members XXX XXX ↑/↓/→
Net change +X +X
Active 14d XX% XX% ±X.Xpp
Engagement (posts) XX XX ↑/↓/→
KNOWLEDGE
This Month Last Month
Total contacts XXX XXX +X
New contacts +XX +XX
Tagged VIP XX XX
BIGGEST WINS (the 3 metrics that improved most)
1. [Metric] +XX% -- [why we think]
2. [Metric] +XX% -- [why we think]
3. [Metric] +XX% -- [why we think]
BIGGEST CONCERNS (the 3 metrics that declined or hit red flags)
1. [Metric] -XX% -- [hypothesis]
2. [Metric] -XX% -- [hypothesis]
3. [Metric] -XX% -- [hypothesis]
FUNNEL BOTTLENECKS
- [Stage with the worst conversion vs benchmark, e.g. "Show rate is 62% (target 75%) -- Calendly reminder cadence?"]
ACTION ITEMS FOR NEXT MONTH (3 max)
1. [Specific, owner, deadline]
2. [Specific, owner, deadline]
3. [Specific, owner, deadline]
TARGETS / BENCHMARK UPDATES
- [Any thresholds to revise based on this month's data]
±X.Xpp not ±X.X% to avoid the "percent of percent" confusion.funnel-metrics.md > Benchmarks and flag the worst.Lightweight version embedded in snappy-ops morning briefing. NOT a full dashboard -- just the inline metrics that matter today.
[Briefing block, after Calendar/Email/Slack/Invoices/Community]
WEEKLY METRICS PULSE (Mon-Fri only -- not Sat/Sun)
Revenue WTD: $X,XXX (week target: $Y,YYY)
Calls today: X (next: HH:MM with [name])
Outstanding: $X,XXX (X overdue)
Newsletter: X/3 sent (today: [Mon/Wed/Fri = expected send])
LinkedIn: X/5 sent
Active in Skool: X new members yesterday
THE ONE THING TODAY
[Single highest-leverage action]
Render this inline in the morning briefing, not as a separate Telegram digest. The full weekly dashboard is Friday only.
When Robert says "funnel report" or "show me the funnel" -- render this format. See funnel-metrics.md > Funnel Output Format for the canonical structure.
Triggers:
Default window: last 30 days. Confirm window if Robert asks for a different period.
The same rules apply to every dashboard. The format wraps the data; these rules govern how the data is computed.
| rule | enforcement |
|---|---|
| Always disclose the window | Every line includes the window in the label or header |
| Never compare unequal windows | 7d-to-7d, 28d-to-28d, MoM = full-month vs full-month |
| Partial months are flagged | If today is the 7th, "this month" is partial -- note it |
| Same-day-of-week | Pull Friday metrics on Friday, not Saturday -- keeps comparisons honest |
| rule | threshold |
|---|---|
| Don't compute rates from < 30 samples | Report counts only |
| Tag rates from < 100 samples as "noisy" | Add (noisy) suffix |
| Don't compute "rate of rates" | Engagement rate of engagement rate is meaningless |
| Average vs median | Use median for skewed data (deal size, session duration). Average for normal-distributed (open rate, CTR). |
Every metric line MUST have a [snappy-X] tag indicating the producer skill. Lets Robert drill in on any number.
Red flags from each producer skill's targets:
| metric | red flag | source |
|---|---|---|
| Profit margin < 50% | red | snappy-freshbooks/SKILL.md |
| Single client > 30% MRR | red | snappy-freshbooks/SKILL.md |
| Runway < 3 months | red | snappy-freshbooks/SKILL.md |
| Show rate < 60% | red | snappy-sales/SKILL.md |
| Close rate < 10% | red | snappy-sales/SKILL.md |
| Active community < 40% | red | snappy-skool/SKILL.md |
| Unsub rate > 1% per send | red | snappy-email/SKILL.md |
| CPL > $50 (when ads running) | red | snappy-ads/SKILL.md |
| CP-Call > $500 | red | snappy-ads/SKILL.md |
| YouTube AVD < 30% | red | snappy-youtube/analytics.md |
| Newsletter < 3/week | yellow | snappy-email/SKILL.md |
| LinkedIn < 5 posts/week | yellow | snappy-linkedin/SKILL.md |
| Stale leads > 5 | yellow | snappy-sales/SKILL.md |
Use markers in the dashboard:
[RED] -- must address[YELLOW] -- watch[OK] -- within target| wrong | right |
|---|---|
| Render the dashboard with no source tags | Every metric line gets [snappy-X] so Robert can drill in |
| Compute "vs last week" but use 8 days for one and 7 for the other | Lock to exactly 7 days with explicit start_date / end_date |
| Average percentages (open rate avg over 4 sends) | Average is fine for opens (each send is independent) but never for ratios of ratios |
Use +X% for percentage point changes |
Use ±Xpp for percentage points, ±X% for relative changes -- they're different |
| Drop the "ONE THING" line because there's nothing dramatic | Always pick one. Even if it's "keep doing what's working -- ship Friday post" -- that's the one thing. |
| Render in Markdown for Telegram without escaping | Telegram Markdown is finicky. Test with a sample. Underscores and asterisks need escaping. |
| Send a 6000-char dashboard via Telegram | Telegram limit is 4096. Split into 2-3 messages or post to Slack instead. |
| Pull metrics from cached files older than 24h for the weekly dashboard | Always pull fresh on Friday morning |
| Show partial-month revenue alongside full-month historical | Either complete the month first or use 30-day rolling |
| Flag every yellow as urgent | Reserve [RED] for must-address. Yellows are watch items, not interrupts. |
Compute deltas as (this - last) for declining metrics |
Declines should show as negative. Use (this - last) consistently -- sign carries the meaning. |
| Generate the dashboard then never log it | Save every dashboard to ~/.claude/skills/snappy-analytics/logs/YYYY-WW.md for trend backref |
| Skip "concerns" because everything looks fine | If nothing is below baseline, write "no red flags this week" -- explicit absence is informational |
| Use mixed currencies in revenue without conversion | Convert all to base currency (USD) and disclose conversion rate |
| Render a chart in ASCII when a table is clearer | Tables only. ASCII charts are unreliable in Telegram and cluttered in Slack. |
# Dashboard Formats -- Weekly, Monthly, On-Demand
> Resource for `snappy-analytics`. Output formats for the synthesized scoreboard. Every dashboard pull lands in one of these layouts before going to Telegram / Slack / Notion.
## Table of Contents
- [Output Channels](#output-channels)
- [Weekly Snappy Dashboard](#weekly-snappy-dashboard)
- [Monthly Snappy Review](#monthly-snappy-review)
- [Daily Briefing Rollup](#daily-briefing-rollup)
- [Funnel Report (on-demand)](#funnel-report-on-demand)
- [Synthesis Rules](#synthesis-rules)
- [What AI Agents Get Wrong (formats)](#what-ai-agents-get-wrong-formats)
---
## Output Channels
|dashboard|primary destination|secondary|frequency|owner trigger|
|---------|-------------------|---------|---------|-------------|
|Weekly Snappy Dashboard|Telegram (`snappy-telegram` self-message)|`snappy-slack` `#all-snappy` (optional)|Friday morning|`snappy-ops` Friday close|
|Monthly Snappy Review|Notion page (`snappy-docs`)|Telegram digest, Slack `#all-snappy`|Last Friday of month|`snappy-ops` monthly close|
|Daily Briefing rollup|Terminal (during `snappy-ops` brief)|--|Daily Mon-Fri|`snappy-ops` morning briefing|
|Funnel Report|Terminal output|Notion (if requested)|On-demand|Robert says "funnel report"|
|Ad-hoc metric checks|Terminal|--|On-demand|Any "what's our X" question|
### Delivery patterns
```bash
# Telegram delivery (per snappy-telegram pattern)
# Assumes .env.cache already loaded via:
# SNAPPY_SETTINGS_QUIET=1 source ~/.claude/skills/snappy-settings/scripts/load-env.sh
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\": \"${TELEGRAM_ROBERT_CHAT_ID}\", \"text\": \"$(cat /tmp/dashboard.txt | jq -Rs .)\", \"parse_mode\": \"Markdown\"}"
# Slack delivery (per snappy-slack pattern)
curl -s -X POST "$XANO/api:hZB4Dj0c/slack/bot-message" \
-H "Content-Type: application/json" -H "Authorization: Bearer $XANO_METADATA_TOKEN" \
-d '{"channel_id": "C09DD2D0S07", "text": "Weekly dashboard:\n```\n'$(cat /tmp/dashboard.txt)'\n```"}'
```
`#all-snappy` channel ID `C09DD2D0S07` per `snappy-infra/auth-reference.md > Quick Reference Constants`.
---
## Weekly Snappy Dashboard
The flagship output. Pulled every Friday during `snappy-ops` Friday close.
### Format
```
=== SNAPPY WEEKLY DASHBOARD ===
Week of YYYY-MM-DD (last 7 days)
Generated: YYYY-MM-DD HH:MM (timezone)
REVENUE [snappy-freshbooks]
MRR: $X,XXX (MoM: +X.X%)
Revenue this month: $X,XXX (vs target $Y,YYY)
Cash collected (7d): $X,XXX
Outstanding: $X,XXX (X invoices)
Active clients: X (vs X last week)
Avg revenue/client: $X,XXX
Top concentration: XX% (Client Name)
[Red flag: ___ if any]
CONTENT [snappy-content channels]
Newsletter sends (7d): X (target: 3+)
LinkedIn posts (7d): X (target: 5+)
YouTube videos (7d): X (target: 1+)
Blog posts (7d): X (target: 1+)
Skool posts (7d): X (target: 3-5)
YouTube views (7d): X,XXX (vs prior 7d: +/-X%)
YouTube subs net (7d): +XX
YouTube top video: "Title" -- XXX views
LinkedIn impressions: X,XXX
LinkedIn engagement: X.X%
LinkedIn followers: +XX
Blog pageviews (7d): X,XXX
Blog top page: /blog/slug
EMAIL [snappy-email + Xano/Gmail]
List size: X,XXX (Δ +XX this week)
Open rate (7d avg): XX.X% (target 10-20%)
Click rate (7d avg): X.X% (target 1-3%)
Unsub rate (7d avg): X.X% (target < 1%)
Calls booked from email: X (target 2+)
ADS [snappy-ads -- STATUS: paused / running]
Spend (7d): $XXX (or "no spend this week")
CPL: $XX.XX (target < $25)
CP-Call: $XXX.XX (target < $250)
ROAS: X.Xx (target 3x+)
True ROI (rolling 30d): X.Xx
COMMUNITY [snappy-skool]
Members: XXX (Δ +X this week)
New members: +X
Active 14d: XX% (target > 50%)
Posts (7d): XX
Unanswered posts: X (target < 5)
Top contributor: @handle
PIPELINE [snappy-sales]
Leads in (7d): X
Calls booked (7d): X
Calls completed (7d): X (show rate XX%)
Deals closed (7d): X (close rate XX%)
Pipeline value: $X,XXX
Stale leads (>7d): X (target 0)
KNOWLEDGE [snappy-knowledge]
Total contacts: XXX (Δ +X this week)
VIP contacts: XX
Dormant contacts (30d): XX
Birthdays this week: X
ENRICHMENT [snappy-pipeline / Orbiter only]
Health: GREEN / YELLOW / RED
Stuck records: XX
Dominant failure: Source N (X% of stuck)
THE ONE THING THIS WEEK
[Single highest-leverage action drawn from the above]
NOTABLE WINS
- [Top 3 wins this week]
CONCERNS
- [Top 3 concerns / red flags]
NEXT WEEK FOCUS
- [Top 3 priorities for next week]
```
### Synthesis instructions
1. Pull all numbers in parallel (see SKILL.md `## Workflow > Weekly Dashboard Pull`).
2. Compute deltas vs the same window prior period (last 7d vs prior 7d, etc.). Always 7d-to-7d, not 7d-to-30d.
3. Tag each line with the producer skill in brackets `[snappy-X]` so Robert knows where to drill in.
4. Flag any metric that crosses a red-flag threshold (per `revenue-metrics.md > Targets and Red Flags`, `funnel-metrics.md > Benchmarks`, `content-metrics.md`).
5. The "ONE THING" line is non-negotiable per `snappy-ops/SKILL.md > Operations Philosophy`.
6. Trim to fit the Telegram message limit (4096 chars). If overflowing, split into 2-3 messages or post to Slack instead.
---
## Monthly Snappy Review
Run last Friday of each month. Extends the weekly with monthly trends and bigger-picture moves.
### Format
```
=== SNAPPY MONTHLY REVIEW ===
Month: YYYY-MM
Generated: YYYY-MM-DD
REVENUE
This Month Last Month Δ MoM YTD
MRR $X,XXX $X,XXX +X.X% $X,XXX
Revenue (paid) $X,XXX $X,XXX +X.X% $X,XXX
Cash collected $X,XXX $X,XXX +X.X% $X,XXX
Active clients X X +X --
ARPU $X,XXX $X,XXX +X.X% --
Top concentration XX% XX% ±X.Xpp --
Churn (monthly) X.X% X.X% ±X.Xpp --
PROFITABILITY
This Month Last Month
Total revenue $X,XXX $X,XXX
Total expenses $X,XXX $X,XXX
Profit margin XX% XX%
Top expense category SaaS $X,XXX Ads $X,XXX
Runway (months) X.X X.X
PIPELINE
This Month Last Month Δ
Leads in XX XX ±X
Calls booked XX XX ±X
Calls completed XX XX ±X
Show rate XX% XX% ±X.Xpp
Closes X X ±X
Close rate XX% XX% ±X.Xpp
Avg deal size $X,XXX $X,XXX ±$X
Pipeline value (open) $X,XXX $X,XXX ±$X
CONTENT
This Month Last Month Trend
Newsletter sends XX XX ↑/↓/→
LinkedIn posts XX XX ↑/↓/→
YouTube videos X X ↑/↓/→
Blog posts X X ↑/↓/→
YouTube views (28d) X,XXX X,XXX ±X%
YouTube subs (28d) +XX +XX ±X
LinkedIn impressions X,XXX X,XXX ±X%
Blog pageviews X,XXX X,XXX ±X%
ADS (if running)
This Month Last Month Δ
Spend $X,XXX $X,XXX ±$X
Leads (paid) XX XX ±X
Calls (paid) X X ±X
Closes (paid) X X ±X
CPL $XX $XX ±$X
CP-Call $XXX $XXX ±$X
CPA $X,XXX $X,XXX ±$X
True ROI (collected) X.Xx X.Xx ±X.X
COMMUNITY
This Month Last Month Trend
Members XXX XXX ↑/↓/→
Net change +X +X
Active 14d XX% XX% ±X.Xpp
Engagement (posts) XX XX ↑/↓/→
KNOWLEDGE
This Month Last Month
Total contacts XXX XXX +X
New contacts +XX +XX
Tagged VIP XX XX
BIGGEST WINS (the 3 metrics that improved most)
1. [Metric] +XX% -- [why we think]
2. [Metric] +XX% -- [why we think]
3. [Metric] +XX% -- [why we think]
BIGGEST CONCERNS (the 3 metrics that declined or hit red flags)
1. [Metric] -XX% -- [hypothesis]
2. [Metric] -XX% -- [hypothesis]
3. [Metric] -XX% -- [hypothesis]
FUNNEL BOTTLENECKS
- [Stage with the worst conversion vs benchmark, e.g. "Show rate is 62% (target 75%) -- Calendly reminder cadence?"]
ACTION ITEMS FOR NEXT MONTH (3 max)
1. [Specific, owner, deadline]
2. [Specific, owner, deadline]
3. [Specific, owner, deadline]
TARGETS / BENCHMARK UPDATES
- [Any thresholds to revise based on this month's data]
```
### Synthesis instructions
1. Pull weekly dashboards for the trailing 4 weeks AND raw data for the entire month.
2. Compute MoM deltas vs the prior month (full month vs full month -- never partial).
3. For percentage points (rates), use `±X.Xpp` not `±X.X%` to avoid the "percent of percent" confusion.
4. Identify wins/concerns by ranking absolute change and percentage change separately. Use percentage for content/traffic, absolute dollars for revenue.
5. Bottleneck analysis: compare each funnel stage rate against `funnel-metrics.md > Benchmarks` and flag the worst.
6. Action items must be specific (NOT "improve content"), have an owner (default Robert), and a deadline (default end of next month).
---
## Daily Briefing Rollup
Lightweight version embedded in `snappy-ops` morning briefing. NOT a full dashboard -- just the inline metrics that matter today.
### Format
```
[Briefing block, after Calendar/Email/Slack/Invoices/Community]
WEEKLY METRICS PULSE (Mon-Fri only -- not Sat/Sun)
Revenue WTD: $X,XXX (week target: $Y,YYY)
Calls today: X (next: HH:MM with [name])
Outstanding: $X,XXX (X overdue)
Newsletter: X/3 sent (today: [Mon/Wed/Fri = expected send])
LinkedIn: X/5 sent
Active in Skool: X new members yesterday
THE ONE THING TODAY
[Single highest-leverage action]
```
Render this inline in the morning briefing, not as a separate Telegram digest. The full weekly dashboard is Friday only.
---
## Funnel Report (on-demand)
When Robert says "funnel report" or "show me the funnel" -- render this format. See `funnel-metrics.md > Funnel Output Format` for the canonical structure.
Triggers:
- "funnel report"
- "show me the funnel"
- "where are leads dropping off"
- "what's our visit-to-close rate"
Default window: last 30 days. Confirm window if Robert asks for a different period.
---
## Synthesis Rules
The same rules apply to every dashboard. The format wraps the data; these rules govern how the data is computed.
### Window discipline
|rule|enforcement|
|----|-----------|
|Always disclose the window|Every line includes the window in the label or header|
|Never compare unequal windows|7d-to-7d, 28d-to-28d, MoM = full-month vs full-month|
|Partial months are flagged|If today is the 7th, "this month" is partial -- note it|
|Same-day-of-week|Pull Friday metrics on Friday, not Saturday -- keeps comparisons honest|
### Sample size & noise
|rule|threshold|
|----|---------|
|Don't compute rates from < 30 samples|Report counts only|
|Tag rates from < 100 samples as "noisy"|Add `(noisy)` suffix|
|Don't compute "rate of rates"|Engagement rate of engagement rate is meaningless|
|Average vs median|Use median for skewed data (deal size, session duration). Average for normal-distributed (open rate, CTR).|
### Source labeling
Every metric line MUST have a `[snappy-X]` tag indicating the producer skill. Lets Robert drill in on any number.
### Red flag highlighting
Red flags from each producer skill's targets:
|metric|red flag|source|
|------|--------|------|
|Profit margin < 50%|red|`snappy-freshbooks/SKILL.md`|
|Single client > 30% MRR|red|`snappy-freshbooks/SKILL.md`|
|Runway < 3 months|red|`snappy-freshbooks/SKILL.md`|
|Show rate < 60%|red|`snappy-sales/SKILL.md`|
|Close rate < 10%|red|`snappy-sales/SKILL.md`|
|Active community < 40%|red|`snappy-skool/SKILL.md`|
|Unsub rate > 1% per send|red|`snappy-email/SKILL.md`|
|CPL > $50 (when ads running)|red|`snappy-ads/SKILL.md`|
|CP-Call > $500|red|`snappy-ads/SKILL.md`|
|YouTube AVD < 30%|red|`snappy-youtube/analytics.md`|
|Newsletter < 3/week|yellow|`snappy-email/SKILL.md`|
|LinkedIn < 5 posts/week|yellow|`snappy-linkedin/SKILL.md`|
|Stale leads > 5|yellow|`snappy-sales/SKILL.md`|
Use markers in the dashboard:
- `[RED]` -- must address
- `[YELLOW]` -- watch
- `[OK]` -- within target
- No marker -- informational only
---
## What AI Agents Get Wrong (formats)
|wrong|right|
|-----|-----|
|Render the dashboard with no source tags|Every metric line gets `[snappy-X]` so Robert can drill in|
|Compute "vs last week" but use 8 days for one and 7 for the other|Lock to exactly 7 days with explicit start_date / end_date|
|Average percentages (open rate avg over 4 sends)|Average is fine for opens (each send is independent) but never for ratios of ratios|
|Use `+X%` for percentage point changes|Use `±Xpp` for percentage points, `±X%` for relative changes -- they're different|
|Drop the "ONE THING" line because there's nothing dramatic|Always pick one. Even if it's "keep doing what's working -- ship Friday post" -- that's the one thing.|
|Render in Markdown for Telegram without escaping|Telegram Markdown is finicky. Test with a sample. Underscores and asterisks need escaping.|
|Send a 6000-char dashboard via Telegram|Telegram limit is 4096. Split into 2-3 messages or post to Slack instead.|
|Pull metrics from cached files older than 24h for the weekly dashboard|Always pull fresh on Friday morning|
|Show partial-month revenue alongside full-month historical|Either complete the month first or use 30-day rolling|
|Flag every yellow as urgent|Reserve `[RED]` for must-address. Yellows are watch items, not interrupts.|
|Compute deltas as `(this - last)` for declining metrics|Declines should show as negative. Use `(this - last)` consistently -- sign carries the meaning.|
|Generate the dashboard then never log it|Save every dashboard to `~/.claude/skills/snappy-analytics/logs/YYYY-WW.md` for trend backref|
|Skip "concerns" because everything looks fine|If nothing is below baseline, write "no red flags this week" -- explicit absence is informational|
|Use mixed currencies in revenue without conversion|Convert all to base currency (USD) and disclose conversion rate|
|Render a chart in ASCII when a table is clearer|Tables only. ASCII charts are unreliable in Telegram and cluttered in Slack.|
Resource for
snappy-analytics. The complete index of which producer skill owns which metric, what endpoint or browser path it lives behind, how to pull it, and how often.
Every skill that publishes a metric this skill consumes. Lives in Workflow > Inputs of SKILL.md as the canonical list -- this file is the deeper version.
| skill | metric_domain | primary_pull | fallback_pull | owner_doc |
|---|---|---|---|---|
snappy-website |
Traffic, page views, source attribution, contact form submissions, conversion rates | Vercel Analytics + Xano contact/submissions |
agent-browser to vercel.com | snappy-website/conversion-tracking.md |
snappy-ads |
Paid spend, CPL, CP-Call, ROAS, CTR, view rate, true ROI | Xano api:PB9UH7b9/ads/metrics?period=... |
agent-browser to ads.google.com + Hyros | snappy-ads/SKILL.md (Workflow 2) |
snappy-youtube |
Views, watch time, subscribers, CTR, AVD, top videos | Xano api:PB9UH7b9/youtube/video-stats (test) |
agent-browser to studio.youtube.com | snappy-youtube/analytics.md |
snappy-linkedin |
Post impressions, engagement rate, profile views, follower delta | agent-browser to LinkedIn analytics + recent activity | Xano linkedin/profile |
snappy-linkedin/SKILL.md |
snappy-email |
Send count, open rate, click rate, list size, unsub rate, deliverability | Xano emails/list (Gmail backend) |
agent-browser to Gmail | snappy-email/SKILL.md |
snappy-skool |
Members, new members, engagement rate, active members, churn, leaderboard, posts | npx tsx snappy-skool/api.ts <read> with cookie auth |
Private frontend API, mastered in the skill | snappy-skool/SKILL.md |
snappy-sales |
Leads, calls booked, calls completed, deal value, pipeline stage counts, close rate | Xano api:PB9UH7b9/leads/... + calendar/events?days=N |
Manual pipeline review per snappy-sales/pipeline.md |
snappy-sales/pipeline.md |
snappy-clients |
Active client count, client health score, dormant client count, client revenue concentration | Xano api:PB9UH7b9/contacts?tag=client + contacts/dormant?days=N |
-- | snappy-clients/lifecycle.md |
snappy-freshbooks |
MRR, revenue this month, outstanding, cash collected, churn, ARPU, profit margin, runway | Xano api:PB9UH7b9/freshbooks/invoices + api:ACdo1OLG/freshbooks/clients + freshbooks/expenses |
-- | snappy-freshbooks/api-reference.md |
snappy-knowledge |
Total contacts, new contacts (delta), tag distribution, dormant counts | Xano api:PB9UH7b9/contacts?tag=... + contacts/dormant |
-- | snappy-knowledge/endpoints.md |
snappy-pipeline |
Orbiter enrichment health, completeness %, queue depth, crash counts (Orbiter only) | MCP mcp__xano-mcp__execute to api:Bd_dCiOz/qa/batch-overview |
-- | snappy-pipeline/endpoints.md |
snappy-content |
Posts published per channel per week, content velocity, content -> conversion attribution | Joins data from blog/youtube/linkedin/email pulls | -- | snappy-content/SKILL.md |
snappy-blog |
Posts published, pageviews per post, blog -> email conversion | Vercel Analytics + Xano contact source tracking | -- | snappy-blog/SKILL.md |
snappy-publish |
Deploy success rate, time to publish, posts shipped per week | Git log + Vercel deploy events | -- | snappy-publish/SKILL.md |
Reverse lookup: "I need metric X -- where does it come from?"
| metric | skill | endpoint / path | notes |
|---|---|---|---|
| MRR | snappy-freshbooks |
GET api:PB9UH7b9/freshbooks/invoices |
Filter status in (paid, sent) AND recurring -> normalize to monthly -> sum. NEVER count overdue invoices in MRR -- those are receivables, not recurring revenue. NEVER include one-time project invoices. |
| Revenue this month | snappy-freshbooks |
GET api:PB9UH7b9/freshbooks/invoices |
Filter status = paid AND payment_date in current month. Cash-basis only. |
| Outstanding receivables | snappy-freshbooks |
GET api:PB9UH7b9/freshbooks/invoices |
Filter status in (sent, viewed, partial, overdue). Sum amount - paid_amount. |
| Cash collected this month | snappy-freshbooks |
GET api:PB9UH7b9/freshbooks/invoices |
Sum paid_amount where payment_date in current month. |
| ARPU | snappy-freshbooks |
computed | MRR / active_client_count. Active = has at least one paid or sent invoice in last 60 days. |
| Churn (monthly) | snappy-freshbooks + snappy-clients |
computed | (clients_lost_this_month / clients_start_of_month) * 100. Use FreshBooks recurring profile changes for source of truth. |
| Profit margin | snappy-freshbooks |
GET api:PB9UH7b9/freshbooks/expenses + invoices |
(revenue - expenses) / revenue * 100. Target >= 60%. |
| Runway | snappy-freshbooks |
computed | bank_balance / 3_month_avg_expense. Target >= 6 months. |
| Single-client concentration | snappy-freshbooks |
computed | max(client_revenue) / MRR * 100. Red flag > 30%. |
| Ad spend | snappy-ads |
GET api:PB9UH7b9/ads/metrics?period=... |
Server-side aggregation if endpoint live. Otherwise agent-browser to ads.google.com. |
| True ROI (paid funnel) | snappy-ads + snappy-freshbooks |
computed | (collected_revenue_from_ad_attributed_clients - total_ad_spend) / total_ad_spend. NOT projected ROAS. |
| metric | skill | endpoint / path | notes |
|---|---|---|---|
| Page views (snappy.ai) | snappy-website |
Vercel Analytics dashboard | agent-browser to vercel.com -> snappy-ai project -> Analytics tab. Vercel Analytics API has no public REST endpoint as of cutoff. |
| Unique visitors | snappy-website |
Vercel Analytics dashboard | Same source. Track distinctly from page views. |
| Top pages | snappy-website |
Vercel Analytics dashboard | Use to identify what content draws traffic. |
| Traffic sources | snappy-website |
Vercel Analytics dashboard | Organic / social / direct / referral / paid breakdown. Compare to UTM-tagged contact submissions. |
| Contact form submissions | snappy-website |
GET api:PB9UH7b9/contact/submissions |
UTM source captured per submission. Server-side persistence. |
| Top referrers | snappy-website |
Vercel Analytics dashboard | Cross-check against UTM data on contact form submissions. |
| Bounce rate / session duration | snappy-website |
Vercel Analytics dashboard | Watch for traffic-quality issues (high bounce on paid). |
| metric | skill | endpoint / path | notes |
|---|---|---|---|
| YouTube views (7d, 28d) | snappy-youtube |
GET api:PB9UH7b9/youtube/video-stats (test before relying) |
Fallback: agent-browser to studio.youtube.com -> Analytics. NEVER mix 7d and 28d in same comparison without disclosing. |
| YouTube watch time | snappy-youtube |
same | Hours, not minutes. Watch time > views as a quality signal. |
| YouTube subs gained | snappy-youtube |
same | Net delta (gained - lost). The total number is vanity; net delta is the metric. |
| YouTube CTR | snappy-youtube |
same | impressions -> views. Target 4-7% per snappy-youtube/analytics.md. |
| YouTube AVD | snappy-youtube |
same | Average view duration as % of video. Priority signal -- fix retention first. |
| Top videos by views | snappy-youtube |
same | Top 5 with views, CTR, AVD per video. |
| LinkedIn impressions | snappy-linkedin |
agent-browser to recent activity | linkedin.com/in/robert-boulos/recent-activity/all/ -> extract impression counts per post. |
| LinkedIn engagement rate | snappy-linkedin |
computed | (reactions + comments + shares) / impressions * 100. Target > 2% baseline. |
| LinkedIn followers gained | snappy-linkedin |
agent-browser to profile | Net delta only. Total follower count is vanity. |
| Top LinkedIn posts | snappy-linkedin |
same | Top 3 by impressions. |
| Blog page views | snappy-blog |
Vercel Analytics dashboard | Per-post pageview attribution. |
| Blog -> email signup rate | snappy-blog + snappy-email |
Xano contact source tracking | blog_signups / blog_pageviews * 100. Target > 5%. |
| Email send count | snappy-email |
GET api:PB9UH7b9/emails/list |
Target 3+ broadcasts/week per snappy-email/SKILL.md. ActiveCampaign is NOT in use. |
| Email open rate | snappy-email |
Xano emails/list (Gmail tracking) |
Target 10-20% baseline (per snappy-email metrics -- note that 30%+ targets in older docs are aspirational). |
| Email click rate | snappy-email |
Xano emails/list (Gmail tracking) |
Target 1-3% baseline. |
| Email unsub rate | snappy-email |
Xano emails/list |
Target < 1% per send. |
| List size | snappy-email |
Xano contacts + Loops.so | Active subscribers only. |
| List growth rate | snappy-email |
computed | (new_subs - unsubs) / total * 100 per week. |
| metric | skill | endpoint / path | notes |
|---|---|---|---|
| Total members | snappy-skool |
api.ts group and api.ts members |
Compare the group census with paginated rows. |
| New members (this week) | snappy-skool |
api.ts members --json, sort by join date |
Use the returned member timestamps. |
| Active members (14d) | snappy-skool |
api.ts leaderboards --json plus posts |
Posted or commented in last 14 days. |
| Engagement rate | snappy-skool |
computed | active / total * 100. Target > 50%. |
| Posts this week | snappy-skool |
api.ts posts --json |
Trending up = healthy. |
| Unanswered posts | snappy-skool |
api.ts questions --json |
Target < 5 at any time. Snappy retention rule. |
| Leaderboard top 10 | snappy-skool |
api.ts leaderboards --json |
Recognize top contributors weekly. |
| Member churn | snappy-skool + snappy-freshbooks |
computed | members_lost_this_month / members_start * 100. Cross-check against FreshBooks recurring profile cancellations. |
| metric | skill | endpoint / path | notes | ||
|---|---|---|---|---|---|
| Leads this week | snappy-sales |
GET api:PB9UH7b9/leads?stage=... |
Filter on creation date in last 7 days. | ||
| Calls booked | snappy-sales |
GET api:PB9UH7b9/calendar/events?days=7 |
Filter title for `call | demo | discovery (per snappy-sales/SKILL.md`). |
| Calls completed | snappy-sales |
GET api:PB9UH7b9/calendar/events?days=7 |
Past-dated events not marked cancelled. | ||
| Show rate | snappy-sales |
computed | calls_completed / calls_booked * 100. Target > 75%. |
||
| Close rate | snappy-sales |
computed | deals_closed / calls_completed * 100. Target 20-30%. |
||
| Deal value (avg) | snappy-sales + snappy-freshbooks |
computed | Sum of first invoices from closed-won deals / count. | ||
| Pipeline stage distribution | snappy-sales |
GET api:PB9UH7b9/leads |
Group by stage field. |
||
| Stale leads | snappy-sales |
GET api:PB9UH7b9/leads?updated_before=... |
> 7 days no contact = stale. Drives Friday review. |
| metric | skill | endpoint / path | notes |
|---|---|---|---|
| Total contacts | snappy-knowledge |
GET api:PB9UH7b9/contacts |
Total count. |
| Contact growth (week) | snappy-knowledge |
computed from contact created_at |
New contacts last 7 days. |
| Tag distribution | snappy-knowledge |
GET api:PB9UH7b9/contacts?tag=X |
Counts per tag (vip, client, prospect, past_client, advisor). |
| Dormant contacts (30d+) | snappy-knowledge |
GET api:PB9UH7b9/contacts/dormant?days=30 |
Re-engagement candidates. |
| Birthdays this week | snappy-knowledge |
GET api:PB9UH7b9/contacts/birthdays?days_ahead=7 |
Personal pipeline trigger. |
| Orbiter enrichment health | snappy-pipeline |
MCP mcp__xano-mcp__execute -> api:Bd_dCiOz/qa/batch-overview |
Orbiter Xano instance, NOT main. See snappy-pipeline/SKILL.md. |
| Enrichment completeness % | snappy-pipeline |
MCP -> qa/enrichment-completeness?master_person_id=N |
Per-person diagnostic. |
| Stuck records | snappy-pipeline |
MCP -> qa/stuck-analysis |
Dynamic -- dominant failure source surfaces here. |
Single index of every endpoint this skill calls (or could call) -- all canonical, all verified against producer skill source files.
xnwv-v1z6-dvnr.n7c.xano.io)#| group | endpoint | method | metric | verified_in |
|---|---|---|---|---|
api:PB9UH7b9 |
/freshbooks/invoices |
GET | MRR, revenue, outstanding, churn | snappy-freshbooks/api-reference.md |
api:PB9UH7b9 |
/freshbooks/expenses |
GET | Profit margin, ad spend | snappy-freshbooks/api-reference.md |
api:PB9UH7b9 |
/freshbooks/time-entries |
GET | Unbilled hours | snappy-freshbooks/api-reference.md |
api:ACdo1OLG |
/freshbooks/clients |
GET | Client count, ARPU | snappy-freshbooks/api-reference.md |
api:PB9UH7b9 |
/contact/submissions |
GET | Website conversions | snappy-website/conversion-tracking.md |
api:PB9UH7b9 |
/contacts |
GET | Knowledge graph counts, tag distribution | snappy-knowledge/endpoints.md |
api:PB9UH7b9 |
/contacts/dormant?days=N |
GET | Dormant contacts, churn risk | snappy-knowledge/SKILL.md |
api:PB9UH7b9 |
/contacts/birthdays?days_ahead=N |
GET | Personal pipeline | snappy-knowledge/SKILL.md |
api:PB9UH7b9 |
/calendar/events?days=N |
GET | Calls booked / completed | snappy-sales/SKILL.md |
api:PB9UH7b9 |
/leads |
GET | Pipeline stage distribution | snappy-sales/pipeline.md |
api:PB9UH7b9 |
/ads/metrics?period=last_7_days |
GET | Ad spend, CPL, CP-Call, ROAS | snappy-ads/SKILL.md Workflow 2 |
api:PB9UH7b9 |
/ads/metrics?period=last_30_days |
GET | Monthly ad rollup | snappy-ads/SKILL.md |
api:PB9UH7b9 |
/youtube/video-stats |
GET | Per-video stats (test before relying) | snappy-youtube/analytics.md |
api:PB9UH7b9 |
/linkedin/profile |
GET | Follower delta | snappy-linkedin/SKILL.md |
api:PB9UH7b9 |
/emails/list |
GET | Email send history | snappy-email/SKILL.md |
api:OehldiTW)#| endpoint | method | metric | notes |
|---|---|---|---|
/email/smart-inbox |
POST | Triage queue depth | Used by ops for morning brief, not analytics |
ActiveCampaign is NOT in use. Email goes through Xano/Gmail. The AC endpoints below are historical and should not be called.
AC_API_KEYis intentionally empty in.env.cache.
xh2o-yths-38lt.n7c.xano.io, api:Bd_dCiOz)#| endpoint | method | metric | verified_in |
|---|---|---|---|
/qa/batch-overview |
GET | System-wide enrichment health | snappy-pipeline/endpoints.md |
/qa/stuck-analysis |
GET | Stuck record breakdown | snappy-pipeline/endpoints.md |
/qa/enrichment-completeness?master_person_id=N |
GET | Per-person completeness | snappy-pipeline/endpoints.md |
/qa/queue-health |
GET | Queue backlog | snappy-pipeline/endpoints.md |
Call via mcp__xano-mcp__execute only. NEVER mix with main Snappy Xano endpoints.
| source | skill | metric | state file |
|---|---|---|---|
studio.youtube.com |
snappy-youtube |
All YouTube analytics until Xano endpoint exists | ~/.openclaw/workspace/youtube-studio-auth.json |
linkedin.com/in/robert-boulos/recent-activity/all/ |
snappy-linkedin |
Per-post impressions/engagement | ~/.openclaw/workspace/linkedin-auth.json |
vercel.com -> snappy-ai project -> Analytics |
snappy-website |
Page views, sources, top pages | ~/.openclaw/workspace/vercel-auth.json (if exists) |
skool.com/snappy |
snappy-skool |
Member count, leaderboard, posts | ~/.openclaw/workspace/skool-auth.json |
ads.google.com |
snappy-ads |
Ad spend (fallback when Xano endpoint stale) | ~/.openclaw/workspace/google-auth.json |
How often each metric needs to be refreshed and which workflow triggers the pull.
| metric | frequency | trigger | owner workflow |
|---|---|---|---|
| MRR | weekly + monthly | Friday close + 1st of month | Section 7 weekly dashboard |
| Revenue this month | daily during last week of month, weekly otherwise | Friday close | Section 7 weekly dashboard |
| Outstanding | weekly | Friday close | Section 7 + snappy-freshbooks Friday review |
| Churn | monthly | Last Friday of month | Monthly review |
| Profit margin | monthly | Last Friday of month | Monthly review |
| Runway | monthly | 1st of month | Monthly review |
| Ad spend | weekly | Monday ad review (if running) | Section 7 + snappy-ads Workflow 2 |
| CPL / CP-Call / ROAS | weekly | Monday ad review | snappy-ads Workflow 2 |
| YouTube views (7d) | weekly | Friday content review | Section 7 |
| YouTube CTR / AVD | per video, post-publish 48h | snappy-youtube/analytics.md weekly review |
snappy-youtube Friday |
| LinkedIn impressions | weekly | Friday content review | Section 7 |
| Email open / click rate | per send + weekly rollup | After each broadcast + Friday | Section 7 |
| List size | weekly | Friday content review | Section 7 |
| Skool member count | daily | Morning briefing + Friday | snappy-skool daily check + Section 7 |
| Active members 14d | weekly | Friday community summary | snappy-skool Friday |
| Pipeline stage counts | weekly | Monday + Friday pipeline review | snappy-sales Monday |
| Calls booked / completed | weekly | Friday close | Section 7 |
| Close rate (rolling 30d) | weekly | Friday close | Section 7 |
| Knowledge contact growth | weekly | Friday close | Section 7 |
| Orbiter enrichment health | weekly | Friday close (via snappy-client-orbiter) |
snappy-pipeline health pulse |
Where the synthesized output of this skill lives.
| dashboard | location | format | frequency |
|---|---|---|---|
| Weekly Snappy Dashboard | Telegram message to Robert + Slack #all-snappy (optional) + Notion log (optional) |
ASCII table per dashboard-formats.md weekly section |
Friday morning |
| Monthly Snappy Review | Notion page + Telegram digest | Monthly comparison table per dashboard-formats.md monthly section |
Last Friday of month |
| Daily Briefing rollup | Terminal output during snappy-ops morning briefing |
Inline 1-3 lines per metric | Daily Mon-Fri |
| Funnel Report (paid + organic) | On-demand via funnel section | Funnel ASCII table per funnel-metrics.md |
Monthly |
| Internal Xano dashboard (future) | https://xano.snappy.ai |
Web UI surfacing all of the above | Aspirational -- under build |
Honest list of metrics this skill should produce but currently can't because the producer skill or endpoint doesn't exist yet.
| gap | why_missing | fix_path | priority |
|---|---|---|---|
| Vercel Analytics REST API | Vercel does not expose page views via public REST as of model cutoff. @vercel/analytics SDK only writes events. |
agent-browser to vercel.com is the only path. Or self-host Plausible/Umami and pull via their API. | P1 |
| YouTube Analytics API OAuth via Xano | snappy-youtube/analytics.md flags this as a gap. youtube/video-stats exists but is "test before relying". |
Add YouTube Analytics API OAuth to Xano + new endpoint group. Until then use agent-browser to studio.youtube.com. | P1 |
| Hyros true-ROI endpoint | snappy-ads references Hyros for profit attribution but no Xano wrapper exists. |
Add Hyros REST proxy to Xano. Until then check Hyros UI manually monthly. | P2 |
| Skool API | Skool has no public API. Browser-only. | None -- Skool roadmap. Track manually via agent-browser scrapes. | P3 (won't fix) |
| Pre-computed MRR endpoint | snappy-freshbooks/api-reference.md lists GET /freshbooks/dashboards/mrr as aspirational. |
Build server-side MRR aggregation in Xano. | P1 |
| Pipeline stage roll-up endpoint | snappy-sales has /leads but no aggregation endpoint. |
Add GET /leads/pipeline-summary returning counts by stage. |
P2 |
| Content -> conversion attribution join | No single endpoint that ties a YouTube video / blog post / LinkedIn post to a contact form submission via UTM. | Build Xano endpoint that joins contact/submissions UTM with content publication dates. |
P1 |
| Email reply rate | snappy-email/SKILL.md says "track manually" -- no automated reply detection. |
Add Gmail thread-reply detection to email/smart-inbox endpoint. |
P2 |
| LinkedIn impressions API | LinkedIn API only exposes paid analytics, not organic. Browser-only. | Use agent-browser until LinkedIn opens organic analytics API. |
P3 (won't fix) |
When a gap is closed, move the row to the catalog above and update Section 7 (weekly dashboard) workflow accordingly.
# Data Source Map -- Every Metric, Every Skill, Every Endpoint > Resource for `snappy-analytics`. The complete index of which producer skill owns which metric, what endpoint or browser path it lives behind, how to pull it, and how often. ## Table of Contents - [Producer Skills (data sources)](#producer-skills-data-sources) - [Metric -> Skill Index](#metric---skill-index) - [Endpoint Catalog](#endpoint-catalog) - [Pull Frequency Matrix](#pull-frequency-matrix) - [Dashboard Locations](#dashboard-locations) - [What's Missing (gap log)](#whats-missing-gap-log) --- ## Producer Skills (data sources) Every skill that publishes a metric this skill consumes. Lives in `Workflow > Inputs` of SKILL.md as the canonical list -- this file is the deeper version. |skill|metric_domain|primary_pull|fallback_pull|owner_doc| |-----|-------------|-----------|-------------|---------| |`snappy-website`|Traffic, page views, source attribution, contact form submissions, conversion rates|Vercel Analytics + Xano `contact/submissions`|agent-browser to vercel.com|`snappy-website/conversion-tracking.md`| |`snappy-ads`|Paid spend, CPL, CP-Call, ROAS, CTR, view rate, true ROI|Xano `api:PB9UH7b9/ads/metrics?period=...`|agent-browser to ads.google.com + Hyros|`snappy-ads/SKILL.md` (Workflow 2)| |`snappy-youtube`|Views, watch time, subscribers, CTR, AVD, top videos|Xano `api:PB9UH7b9/youtube/video-stats` (test)|agent-browser to studio.youtube.com|`snappy-youtube/analytics.md`| |`snappy-linkedin`|Post impressions, engagement rate, profile views, follower delta|agent-browser to LinkedIn analytics + recent activity|Xano `linkedin/profile`|`snappy-linkedin/SKILL.md`| |`snappy-email`|Send count, open rate, click rate, list size, unsub rate, deliverability|Xano `emails/list` (Gmail backend)|agent-browser to Gmail|`snappy-email/SKILL.md`| |`snappy-skool`|Members, new members, engagement rate, active members, churn, leaderboard, posts|`npx tsx snappy-skool/api.ts <read>` with cookie auth|Private frontend API, mastered in the skill|`snappy-skool/SKILL.md`| |`snappy-sales`|Leads, calls booked, calls completed, deal value, pipeline stage counts, close rate|Xano `api:PB9UH7b9/leads/...` + `calendar/events?days=N`|Manual pipeline review per `snappy-sales/pipeline.md`|`snappy-sales/pipeline.md`| |`snappy-clients`|Active client count, client health score, dormant client count, client revenue concentration|Xano `api:PB9UH7b9/contacts?tag=client` + `contacts/dormant?days=N`|--|`snappy-clients/lifecycle.md`| |`snappy-freshbooks`|MRR, revenue this month, outstanding, cash collected, churn, ARPU, profit margin, runway|Xano `api:PB9UH7b9/freshbooks/invoices` + `api:ACdo1OLG/freshbooks/clients` + `freshbooks/expenses`|--|`snappy-freshbooks/api-reference.md`| |`snappy-knowledge`|Total contacts, new contacts (delta), tag distribution, dormant counts|Xano `api:PB9UH7b9/contacts?tag=...` + `contacts/dormant`|--|`snappy-knowledge/endpoints.md`| |`snappy-pipeline`|Orbiter enrichment health, completeness %, queue depth, crash counts (Orbiter only)|MCP `mcp__xano-mcp__execute` to `api:Bd_dCiOz/qa/batch-overview`|--|`snappy-pipeline/endpoints.md`| |`snappy-content`|Posts published per channel per week, content velocity, content -> conversion attribution|Joins data from blog/youtube/linkedin/email pulls|--|`snappy-content/SKILL.md`| |`snappy-blog`|Posts published, pageviews per post, blog -> email conversion|Vercel Analytics + Xano contact source tracking|--|`snappy-blog/SKILL.md`| |`snappy-publish`|Deploy success rate, time to publish, posts shipped per week|Git log + Vercel deploy events|--|`snappy-publish/SKILL.md`| --- ## Metric -> Skill Index Reverse lookup: "I need metric X -- where does it come from?" ### Revenue & financial |metric|skill|endpoint / path|notes| |------|-----|---------------|-----| |MRR|`snappy-freshbooks`|`GET api:PB9UH7b9/freshbooks/invoices`|Filter `status in (paid, sent)` AND recurring -> normalize to monthly -> sum. NEVER count overdue invoices in MRR -- those are receivables, not recurring revenue. NEVER include one-time project invoices.| |Revenue this month|`snappy-freshbooks`|`GET api:PB9UH7b9/freshbooks/invoices`|Filter `status = paid` AND `payment_date` in current month. Cash-basis only.| |Outstanding receivables|`snappy-freshbooks`|`GET api:PB9UH7b9/freshbooks/invoices`|Filter `status in (sent, viewed, partial, overdue)`. Sum `amount - paid_amount`.| |Cash collected this month|`snappy-freshbooks`|`GET api:PB9UH7b9/freshbooks/invoices`|Sum `paid_amount` where `payment_date` in current month.| |ARPU|`snappy-freshbooks`|computed|`MRR / active_client_count`. Active = has at least one paid or sent invoice in last 60 days.| |Churn (monthly)|`snappy-freshbooks` + `snappy-clients`|computed|`(clients_lost_this_month / clients_start_of_month) * 100`. Use FreshBooks recurring profile changes for source of truth.| |Profit margin|`snappy-freshbooks`|`GET api:PB9UH7b9/freshbooks/expenses` + invoices|`(revenue - expenses) / revenue * 100`. Target >= 60%.| |Runway|`snappy-freshbooks`|computed|`bank_balance / 3_month_avg_expense`. Target >= 6 months.| |Single-client concentration|`snappy-freshbooks`|computed|`max(client_revenue) / MRR * 100`. Red flag > 30%.| |Ad spend|`snappy-ads`|`GET api:PB9UH7b9/ads/metrics?period=...`|Server-side aggregation if endpoint live. Otherwise agent-browser to ads.google.com.| |True ROI (paid funnel)|`snappy-ads` + `snappy-freshbooks`|computed|`(collected_revenue_from_ad_attributed_clients - total_ad_spend) / total_ad_spend`. NOT projected ROAS.| ### Traffic & acquisition |metric|skill|endpoint / path|notes| |------|-----|---------------|-----| |Page views (snappy.ai)|`snappy-website`|Vercel Analytics dashboard|agent-browser to vercel.com -> snappy-ai project -> Analytics tab. Vercel Analytics API has no public REST endpoint as of cutoff.| |Unique visitors|`snappy-website`|Vercel Analytics dashboard|Same source. Track distinctly from page views.| |Top pages|`snappy-website`|Vercel Analytics dashboard|Use to identify what content draws traffic.| |Traffic sources|`snappy-website`|Vercel Analytics dashboard|Organic / social / direct / referral / paid breakdown. Compare to UTM-tagged contact submissions.| |Contact form submissions|`snappy-website`|`GET api:PB9UH7b9/contact/submissions`|UTM source captured per submission. Server-side persistence.| |Top referrers|`snappy-website`|Vercel Analytics dashboard|Cross-check against UTM data on contact form submissions.| |Bounce rate / session duration|`snappy-website`|Vercel Analytics dashboard|Watch for traffic-quality issues (high bounce on paid).| ### Content (channels) |metric|skill|endpoint / path|notes| |------|-----|---------------|-----| |YouTube views (7d, 28d)|`snappy-youtube`|`GET api:PB9UH7b9/youtube/video-stats` (test before relying)|Fallback: agent-browser to studio.youtube.com -> Analytics. NEVER mix 7d and 28d in same comparison without disclosing.| |YouTube watch time|`snappy-youtube`|same|Hours, not minutes. Watch time > views as a quality signal.| |YouTube subs gained|`snappy-youtube`|same|Net delta (gained - lost). The total number is vanity; net delta is the metric.| |YouTube CTR|`snappy-youtube`|same|`impressions -> views`. Target 4-7% per `snappy-youtube/analytics.md`.| |YouTube AVD|`snappy-youtube`|same|Average view duration as % of video. Priority signal -- fix retention first.| |Top videos by views|`snappy-youtube`|same|Top 5 with views, CTR, AVD per video.| |LinkedIn impressions|`snappy-linkedin`|agent-browser to recent activity|`linkedin.com/in/robert-boulos/recent-activity/all/` -> extract impression counts per post.| |LinkedIn engagement rate|`snappy-linkedin`|computed|`(reactions + comments + shares) / impressions * 100`. Target > 2% baseline.| |LinkedIn followers gained|`snappy-linkedin`|agent-browser to profile|Net delta only. Total follower count is vanity.| |Top LinkedIn posts|`snappy-linkedin`|same|Top 3 by impressions.| |Blog page views|`snappy-blog`|Vercel Analytics dashboard|Per-post pageview attribution.| |Blog -> email signup rate|`snappy-blog` + `snappy-email`|Xano contact source tracking|`blog_signups / blog_pageviews * 100`. Target > 5%.| |Email send count|`snappy-email`|`GET api:PB9UH7b9/emails/list`|Target 3+ broadcasts/week per `snappy-email/SKILL.md`. ActiveCampaign is NOT in use.| |Email open rate|`snappy-email`|Xano `emails/list` (Gmail tracking)|Target 10-20% baseline (per snappy-email metrics -- note that 30%+ targets in older docs are aspirational).| |Email click rate|`snappy-email`|Xano `emails/list` (Gmail tracking)|Target 1-3% baseline.| |Email unsub rate|`snappy-email`|Xano `emails/list`|Target < 1% per send.| |List size|`snappy-email`|Xano contacts + Loops.so|Active subscribers only.| |List growth rate|`snappy-email`|computed|`(new_subs - unsubs) / total * 100` per week.| ### Community |metric|skill|endpoint / path|notes| |------|-----|---------------|-----| |Total members|`snappy-skool`|`api.ts group` and `api.ts members`|Compare the group census with paginated rows.| |New members (this week)|`snappy-skool`|`api.ts members --json`, sort by join date|Use the returned member timestamps.| |Active members (14d)|`snappy-skool`|`api.ts leaderboards --json` plus posts|Posted or commented in last 14 days.| |Engagement rate|`snappy-skool`|computed|`active / total * 100`. Target > 50%.| |Posts this week|`snappy-skool`|`api.ts posts --json`|Trending up = healthy.| |Unanswered posts|`snappy-skool`|`api.ts questions --json`|Target < 5 at any time. Snappy retention rule.| |Leaderboard top 10|`snappy-skool`|`api.ts leaderboards --json`|Recognize top contributors weekly.| |Member churn|`snappy-skool` + `snappy-freshbooks`|computed|`members_lost_this_month / members_start * 100`. Cross-check against FreshBooks recurring profile cancellations.| ### Sales pipeline |metric|skill|endpoint / path|notes| |------|-----|---------------|-----| |Leads this week|`snappy-sales`|`GET api:PB9UH7b9/leads?stage=...`|Filter on creation date in last 7 days.| |Calls booked|`snappy-sales`|`GET api:PB9UH7b9/calendar/events?days=7`|Filter title for `call|demo|discovery` (per `snappy-sales/SKILL.md`).| |Calls completed|`snappy-sales`|`GET api:PB9UH7b9/calendar/events?days=7`|Past-dated events not marked cancelled.| |Show rate|`snappy-sales`|computed|`calls_completed / calls_booked * 100`. Target > 75%.| |Close rate|`snappy-sales`|computed|`deals_closed / calls_completed * 100`. Target 20-30%.| |Deal value (avg)|`snappy-sales` + `snappy-freshbooks`|computed|Sum of first invoices from closed-won deals / count.| |Pipeline stage distribution|`snappy-sales`|`GET api:PB9UH7b9/leads`|Group by `stage` field.| |Stale leads|`snappy-sales`|`GET api:PB9UH7b9/leads?updated_before=...`|> 7 days no contact = stale. Drives Friday review.| ### Knowledge graph & enrichment |metric|skill|endpoint / path|notes| |------|-----|---------------|-----| |Total contacts|`snappy-knowledge`|`GET api:PB9UH7b9/contacts`|Total count.| |Contact growth (week)|`snappy-knowledge`|computed from contact `created_at`|New contacts last 7 days.| |Tag distribution|`snappy-knowledge`|`GET api:PB9UH7b9/contacts?tag=X`|Counts per tag (vip, client, prospect, past_client, advisor).| |Dormant contacts (30d+)|`snappy-knowledge`|`GET api:PB9UH7b9/contacts/dormant?days=30`|Re-engagement candidates.| |Birthdays this week|`snappy-knowledge`|`GET api:PB9UH7b9/contacts/birthdays?days_ahead=7`|Personal pipeline trigger.| |Orbiter enrichment health|`snappy-pipeline`|MCP `mcp__xano-mcp__execute` -> `api:Bd_dCiOz/qa/batch-overview`|Orbiter Xano instance, NOT main. See `snappy-pipeline/SKILL.md`.| |Enrichment completeness %|`snappy-pipeline`|MCP -> `qa/enrichment-completeness?master_person_id=N`|Per-person diagnostic.| |Stuck records|`snappy-pipeline`|MCP -> `qa/stuck-analysis`|Dynamic -- dominant failure source surfaces here.| --- ## Endpoint Catalog Single index of every endpoint this skill calls (or could call) -- all canonical, all verified against producer skill source files. ### Main Snappy Xano (`xnwv-v1z6-dvnr.n7c.xano.io`) |group|endpoint|method|metric|verified_in| |-----|--------|------|------|-----------| |`api:PB9UH7b9`|`/freshbooks/invoices`|GET|MRR, revenue, outstanding, churn|`snappy-freshbooks/api-reference.md`| |`api:PB9UH7b9`|`/freshbooks/expenses`|GET|Profit margin, ad spend|`snappy-freshbooks/api-reference.md`| |`api:PB9UH7b9`|`/freshbooks/time-entries`|GET|Unbilled hours|`snappy-freshbooks/api-reference.md`| |`api:ACdo1OLG`|`/freshbooks/clients`|GET|Client count, ARPU|`snappy-freshbooks/api-reference.md`| |`api:PB9UH7b9`|`/contact/submissions`|GET|Website conversions|`snappy-website/conversion-tracking.md`| |`api:PB9UH7b9`|`/contacts`|GET|Knowledge graph counts, tag distribution|`snappy-knowledge/endpoints.md`| |`api:PB9UH7b9`|`/contacts/dormant?days=N`|GET|Dormant contacts, churn risk|`snappy-knowledge/SKILL.md`| |`api:PB9UH7b9`|`/contacts/birthdays?days_ahead=N`|GET|Personal pipeline|`snappy-knowledge/SKILL.md`| |`api:PB9UH7b9`|`/calendar/events?days=N`|GET|Calls booked / completed|`snappy-sales/SKILL.md`| |`api:PB9UH7b9`|`/leads`|GET|Pipeline stage distribution|`snappy-sales/pipeline.md`| |`api:PB9UH7b9`|`/ads/metrics?period=last_7_days`|GET|Ad spend, CPL, CP-Call, ROAS|`snappy-ads/SKILL.md` Workflow 2| |`api:PB9UH7b9`|`/ads/metrics?period=last_30_days`|GET|Monthly ad rollup|`snappy-ads/SKILL.md`| |`api:PB9UH7b9`|`/youtube/video-stats`|GET|Per-video stats (test before relying)|`snappy-youtube/analytics.md`| |`api:PB9UH7b9`|`/linkedin/profile`|GET|Follower delta|`snappy-linkedin/SKILL.md`| |`api:PB9UH7b9`|`/emails/list`|GET|Email send history|`snappy-email/SKILL.md`| ### Email Ops Xano (`api:OehldiTW`) |endpoint|method|metric|notes| |--------|------|------|-----| |`/email/smart-inbox`|POST|Triage queue depth|Used by ops for morning brief, not analytics| ### ~~ActiveCampaign REST API~~ (DEPRECATED -- NOT IN USE) > **ActiveCampaign is NOT in use.** Email goes through Xano/Gmail. The AC endpoints below are historical and should not be called. `AC_API_KEY` is intentionally empty in `.env.cache`. ### Orbiter Xano (`xh2o-yths-38lt.n7c.xano.io`, `api:Bd_dCiOz`) |endpoint|method|metric|verified_in| |--------|------|------|-----------| |`/qa/batch-overview`|GET|System-wide enrichment health|`snappy-pipeline/endpoints.md`| |`/qa/stuck-analysis`|GET|Stuck record breakdown|`snappy-pipeline/endpoints.md`| |`/qa/enrichment-completeness?master_person_id=N`|GET|Per-person completeness|`snappy-pipeline/endpoints.md`| |`/qa/queue-health`|GET|Queue backlog|`snappy-pipeline/endpoints.md`| Call via `mcp__xano-mcp__execute` only. NEVER mix with main Snappy Xano endpoints. ### Browser-only sources (no API) |source|skill|metric|state file| |------|-----|------|---------| |`studio.youtube.com`|`snappy-youtube`|All YouTube analytics until Xano endpoint exists|`~/.openclaw/workspace/youtube-studio-auth.json`| |`linkedin.com/in/robert-boulos/recent-activity/all/`|`snappy-linkedin`|Per-post impressions/engagement|`~/.openclaw/workspace/linkedin-auth.json`| |`vercel.com -> snappy-ai project -> Analytics`|`snappy-website`|Page views, sources, top pages|`~/.openclaw/workspace/vercel-auth.json` (if exists)| |`skool.com/snappy`|`snappy-skool`|Member count, leaderboard, posts|`~/.openclaw/workspace/skool-auth.json`| |`ads.google.com`|`snappy-ads`|Ad spend (fallback when Xano endpoint stale)|`~/.openclaw/workspace/google-auth.json`| --- ## Pull Frequency Matrix How often each metric needs to be refreshed and which workflow triggers the pull. |metric|frequency|trigger|owner workflow| |------|---------|-------|--------------| |MRR|weekly + monthly|Friday close + 1st of month|Section 7 weekly dashboard| |Revenue this month|daily during last week of month, weekly otherwise|Friday close|Section 7 weekly dashboard| |Outstanding|weekly|Friday close|Section 7 + `snappy-freshbooks` Friday review| |Churn|monthly|Last Friday of month|Monthly review| |Profit margin|monthly|Last Friday of month|Monthly review| |Runway|monthly|1st of month|Monthly review| |Ad spend|weekly|Monday ad review (if running)|Section 7 + `snappy-ads` Workflow 2| |CPL / CP-Call / ROAS|weekly|Monday ad review|`snappy-ads` Workflow 2| |YouTube views (7d)|weekly|Friday content review|Section 7| |YouTube CTR / AVD|per video, post-publish 48h|`snappy-youtube/analytics.md` weekly review|`snappy-youtube` Friday| |LinkedIn impressions|weekly|Friday content review|Section 7| |Email open / click rate|per send + weekly rollup|After each broadcast + Friday|Section 7| |List size|weekly|Friday content review|Section 7| |Skool member count|daily|Morning briefing + Friday|`snappy-skool` daily check + Section 7| |Active members 14d|weekly|Friday community summary|`snappy-skool` Friday| |Pipeline stage counts|weekly|Monday + Friday pipeline review|`snappy-sales` Monday| |Calls booked / completed|weekly|Friday close|Section 7| |Close rate (rolling 30d)|weekly|Friday close|Section 7| |Knowledge contact growth|weekly|Friday close|Section 7| |Orbiter enrichment health|weekly|Friday close (via `snappy-client-orbiter`)|`snappy-pipeline` health pulse| --- ## Dashboard Locations Where the synthesized output of this skill lives. |dashboard|location|format|frequency| |---------|--------|------|---------| |Weekly Snappy Dashboard|Telegram message to Robert + Slack `#all-snappy` (optional) + Notion log (optional)|ASCII table per `dashboard-formats.md` weekly section|Friday morning| |Monthly Snappy Review|Notion page + Telegram digest|Monthly comparison table per `dashboard-formats.md` monthly section|Last Friday of month| |Daily Briefing rollup|Terminal output during `snappy-ops` morning briefing|Inline 1-3 lines per metric|Daily Mon-Fri| |Funnel Report (paid + organic)|On-demand via funnel section|Funnel ASCII table per `funnel-metrics.md`|Monthly| |Internal Xano dashboard (future)|`https://xano.snappy.ai`|Web UI surfacing all of the above|Aspirational -- under build| --- ## What's Missing (gap log) Honest list of metrics this skill should produce but currently can't because the producer skill or endpoint doesn't exist yet. |gap|why_missing|fix_path|priority| |---|-----------|--------|--------| |Vercel Analytics REST API|Vercel does not expose page views via public REST as of model cutoff. `@vercel/analytics` SDK only writes events.|agent-browser to vercel.com is the only path. Or self-host Plausible/Umami and pull via their API.|P1| |YouTube Analytics API OAuth via Xano|`snappy-youtube/analytics.md` flags this as a gap. `youtube/video-stats` exists but is "test before relying".|Add YouTube Analytics API OAuth to Xano + new endpoint group. Until then use agent-browser to studio.youtube.com.|P1| |Hyros true-ROI endpoint|`snappy-ads` references Hyros for profit attribution but no Xano wrapper exists.|Add Hyros REST proxy to Xano. Until then check Hyros UI manually monthly.|P2| |Skool API|Skool has no public API. Browser-only.|None -- Skool roadmap. Track manually via agent-browser scrapes.|P3 (won't fix)| |Pre-computed MRR endpoint|`snappy-freshbooks/api-reference.md` lists `GET /freshbooks/dashboards/mrr` as aspirational.|Build server-side MRR aggregation in Xano.|P1| |Pipeline stage roll-up endpoint|`snappy-sales` has `/leads` but no aggregation endpoint.|Add `GET /leads/pipeline-summary` returning counts by stage.|P2| |Content -> conversion attribution join|No single endpoint that ties a YouTube video / blog post / LinkedIn post to a contact form submission via UTM.|Build Xano endpoint that joins `contact/submissions` UTM with content publication dates.|P1| |Email reply rate|`snappy-email/SKILL.md` says "track manually" -- no automated reply detection.|Add Gmail thread-reply detection to `email/smart-inbox` endpoint.|P2| |LinkedIn impressions API|LinkedIn API only exposes paid analytics, not organic. Browser-only.|Use `agent-browser` until LinkedIn opens organic analytics API.|P3 (won't fix)| When a gap is closed, move the row to the catalog above and update Section 7 (weekly dashboard) workflow accordingly.
Resource for
snappy-analytics. End-to-end conversion tracking from first touch to closed deal. Covers paid funnel, organic funnel, and outbound funnel.
Per snappy-website/SKILL.md > Workflow > Funnel chains and snappy-ads/SKILL.md > Workflow > Funnel chain:
PAID FUNNEL
============
snappy-ads (YouTube ads)
-> snappy-website (VSL optin -> VSL video -> application -> Calendly)
-> snappy-pipeline (Orbiter enrichment, ad-attributed leads)
-> snappy-knowledge (contact created with ad UTM)
-> snappy-sales (call booked -> call completed -> close)
-> snappy-clients -> snappy-freshbooks (invoice + collect)
ORGANIC FUNNEL
==============
snappy-youtube (organic video)
-> snappy-website (blog or home via description link)
-> snappy-email (newsletter optin)
-> snappy-knowledge (contact)
-> snappy-sales (call) -> snappy-clients -> snappy-freshbooks
SOCIAL / OUTBOUND FUNNEL
========================
snappy-linkedin (post or outreach DM)
-> snappy-website (blog) OR direct DM thread
-> snappy-email (nurture)
-> snappy-knowledge -> snappy-sales -> snappy-clients -> snappy-freshbooks
(Outbound variant: snappy-linkedin outreach Day 0/3/7/14 -> direct call booking)
| stage | definition | source skill | key | ||
|---|---|---|---|---|---|
| Impressions | Times an ad / post / video was shown | snappy-ads / snappy-youtube / snappy-linkedin |
impressions |
||
| Visits | Unique visitors landing on a snappy.ai page | snappy-website (Vercel) |
unique_visitors |
||
| Optins | Email captured (newsletter or VSL optin) | snappy-website + snappy-email |
contact/submissions count + AC list adds |
||
| Applications | VSL application submitted | snappy-website (planned: applications table) |
/applications (P1 endpoint) |
||
| Calls Booked | Calendly slot reserved | snappy-calendar + snappy-sales |
/calendar/events filter for `call |
demo | discovery` |
| Calls Completed | Call attended (past-dated, not no-show) | snappy-sales |
/leads?stage=call_completed |
||
| Deals Closed | Signed + first invoice sent | snappy-sales -> snappy-freshbooks |
/leads?stage=closed_won cross-checked against first invoice |
||
| Active Clients | Currently retained, paying | snappy-clients + snappy-freshbooks |
contacts?tag=client AND has invoice in last 60d |
For each step in the funnel, compute the conversion rate to the next step over a defined window (default: last 30 days).
| step | formula | target (per snappy-website/conversion-tracking.md) |
|---|---|---|
| Impressions -> Visits | unique_visitors / impressions * 100 |
3-5% (paid CTR), 4-7% (organic YouTube CTR) |
| Visits -> Optins | optins / unique_visitors * 100 |
3% home, 5% blog, 30%+ VSL optin |
| Optins -> Applications | applications / optins * 100 |
10%+ (VSL post-optin) |
| Applications -> Calls Booked | calls_booked / applications * 100 |
50%+ |
| Calls Booked -> Calls Completed (show rate) | calls_completed / calls_booked * 100 |
75-85% (snappy-sales) |
| Calls Completed -> Closes | closes / calls_completed * 100 |
20-30% (snappy-sales) |
| Closes -> Active Clients (retention 60d) | retained / closed * 100 |
> 90% in first 60 days |
End-to-end overall: overall_visit_to_close = closes / unique_visitors * 100 -- usually 0.1-1% for cold paid, 0.5-3% for warm organic.
bash# 1. Visits -- Vercel Analytics dashboard (browser-only)
# Capture from screenshot or manual read
# 2. Contact form submissions (proxy for organic optins + applications)
curl -s "$XANO/api:PB9UH7b9/contact/submissions" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.created_at >= "'$(date -v-30d +%Y-%m-%d)'")] | length'
# 3. Optins by source (UTM breakdown)
curl -s "$XANO/api:PB9UH7b9/contact/submissions" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.created_at >= "'$(date -v-30d +%Y-%m-%d)'")] | group_by(.utm_source) | map({source: .[0].utm_source, count: length})'
# 4. Calls booked (calendar)
curl -s "$XANO/api:PB9UH7b9/calendar/events?days=30" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.summary | test("call|demo|discovery"; "i"))] | length'
# 5. Pipeline stage counts
curl -s "$XANO/api:PB9UH7b9/leads" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.created_at >= "'$(date -v-30d +%Y-%m-%d)'")] | group_by(.stage) | map({stage: .[0].stage, count: length})'
# 6. Closes (cross-check leads + first invoices)
curl -s "$XANO/api:PB9UH7b9/leads?stage=closed_won&created_after=$(date -v-30d +%Y-%m-%d)" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
Beyond raw conversion rates, watch:
| metric | definition | red_flag | owner |
|---|---|---|---|
| Pipeline value | Sum of (probability * deal_value) for each open lead |
< 3x monthly target | snappy-sales |
| Velocity | Median days from lead creation -> close | > 30 days for inbound, > 60d for outbound | snappy-sales |
| Stale leads | Leads not contacted in > 7 days | > 5 stale at any time | snappy-sales |
| Stage distribution | % of leads in each stage | > 50% stuck in same stage | snappy-sales |
| Lead source mix | % leads from paid / organic / outbound | Single source > 70% (concentration) | snappy-knowledge UTM |
| Contact growth rate | Net new contacts per week | < 10/week sustained | snappy-knowledge |
| Enrichment quality (Orbiter only) | % of enriched contacts with all fields populated | < 80% completeness | snappy-pipeline |
The hard part. Most conversions have multiple touches before close.
Default: last-touch UTM captured by the contact form submission.
Why last-touch:
snappy-website doesn't yet emit those events.When to override last-touch:
When multiple sources are known for a single contact:
Even without true multi-touch attribution, you can infer touchpoints:
bash# All contact submissions for a single email -- shows multiple touches
curl -s "$XANO/api:PB9UH7b9/contact/submissions" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.email == "prospect@example.com")] | sort_by(.created_at)'
If a contact submitted twice (once via blog, once via VSL), report both touches but use first-touch as canonical attribution.
Standard ASCII table (for terminal + Telegram digest). Always disclose the window.
=== FUNNEL REPORT (Last 30 days, 2026-03-08 to 2026-04-07) ===
PAID FUNNEL (snappy-ads -> snappy-website -> snappy-sales)
Ad impressions: XX,XXX
Ad clicks: X,XXX (X.X% CTR)
VSL optin visits: X,XXX
Optins: XXX (X.X% optin rate)
Applications: XX (XX% of optins)
Calls Booked: XX (XX% of applications)
Calls Completed: XX (XX% show rate)
Deals Closed: X (XX% close rate)
----------------------------------------
Overall click-to-close: X.XX%
CPL: $XX
CP-Call: $XXX
CPA: $X,XXX
True ROI: X.Xx (collected revenue / spend)
ORGANIC FUNNEL (snappy-youtube + snappy-website -> snappy-sales)
YouTube views: X,XXX
Blog visits (organic): X,XXX
Email optins: XX (X.X% of visits)
Calls Booked: X (XX% of optins)
Calls Completed: X (XX% show rate)
Deals Closed: X (XX% close rate)
----------------------------------------
Overall view-to-close: X.XX%
OUTBOUND FUNNEL (snappy-linkedin)
Connection requests: XXX
Connections accepted: XX (XX% accept rate)
Day 7 soft asks: XX
Calls Booked: X (X% from outbound)
Deals Closed: X (X% from outbound)
UNATTRIBUTED
Unknown-source closes: X (NOTE: investigate if > 20% of total)
NOTES:
- [Bottleneck observation: e.g. "show rate is 60% -- below target. Investigate Calendly reminders."]
- [Concentration risk: e.g. "65% of closes from paid this month -- diversify."]
- [Trend: e.g. "Organic funnel completes at 2x close rate of paid -- bigger budget for content might be high ROI."]
Per-stage benchmarks. Use for "is this number poor/okay/good/great?".
| metric | poor | okay | good | great |
|---|---|---|---|---|
| VSL optin rate | < 15% | 15-25% | 25-35% | > 35% |
| Application rate (post-optin) | < 5% | 5-15% | 15-25% | > 25% |
| Show rate | < 60% | 60-75% | 75-85% | > 85% |
| Close rate (high-ticket) | < 10% | 10-20% | 20-30% | > 30% |
| Visitor-to-close (paid) | < 0.1% | 0.1-0.5% | 0.5-1% | > 1% |
| Visitor-to-close (organic) | < 0.5% | 0.5-1.5% | 1.5-3% | > 3% |
| LTV:CAC | < 2:1 | 2-3:1 | 3-5:1 | > 5:1 |
| Pipeline velocity (inbound, days) | > 60 | 30-60 | 14-30 | < 14 |
| Outbound accept rate | < 20% | 20-35% | 35-50% | > 50% |
| Outbound conversion (DM -> call) | < 1% | 1-3% | 3-5% | > 5% |
These are starting benchmarks for high-ticket consulting funnels. Adjust as Snappy's actual numbers establish a baseline.
| wrong | right |
|---|---|
| Compute conversion rates from incomplete data ("we had 47 visits and 3 optins this week, that's 6.4%") | If sample size < 100 visitors, label the rate as "noisy". If < 30, don't compute a rate at all -- report counts only. |
| Compare windows of different lengths ("optins are up -- 50 last month vs 12 last week") | Always normalize to the same window. Last 7d vs prior 7d. Last 30d vs prior 30d. |
| Attribute every conversion to last touch when first-touch is also known | If first-touch is recoverable, prefer it. Last-touch only when first-touch is unknown. Disclose which model in the output. |
| Mix paid + organic numbers in a single conversion rate | Always break down funnel by channel. Paid VSL conversion is ~30% optin; organic blog is ~5%. Mixing them produces meaningless averages. |
| Treat Calendly bookings as completed calls | booked != completed. Show rate is the most common pipeline gap. Always pull both numbers. |
| Compute close rate against booked instead of completed | Close rate must use closes / completed_calls, NOT closes / booked. Show rate is a separate metric. |
| Count canceled calls as no-shows | Canceled with notice = different from no-show. Calendly distinguishes. Don't fold them. |
Pull lead counts without filtering by stage |
Always filter -- leads in disqualified or nurture stages are not in active pipeline. |
| Compute LTV from a single client | LTV needs >= 5 closed-and-churned clients to be meaningful. Until then report median deal size, not LTV. |
| Compute LTV:CAC using gross revenue instead of collected | Always use collected (per snappy-freshbooks). Outstanding ≠ collected ≠ LTV. |
Use UTM utm_source alone for attribution |
UTM source + medium + campaign + content together. A single source field collapses multiple campaigns. |
| Include test contacts (yourself, contractors) in funnel counts | Filter by email domain -- exclude @snappy.ai, @robertboulos.com, known contractor domains. |
| Project annualized revenue from a single month's closes | Closes are lumpy. Use trailing 3-month average for projections. |
| Compute pipeline value at face value of every open lead | Apply probability weighting by stage (e.g., 25% for discovery, 50% for proposal, 80% for verbal). |
| Skip the "unattributed" bucket | Always report unknown-source separately. > 20% unattributed = data hygiene problem. |
| Compare different attribution windows (Hyros 30d-click vs FreshBooks 60d-paid) | Always disclose the attribution window. Apples to apples only. |
| Treat sales call notes as structured data | They're not. Pull structured fields from /leads, treat call notes as supplementary context only. |
| Hardcode "average sales cycle 30 days" as a target | Snappy is high-ticket consulting. Cycles vary by deal size. Compute median per deal-size bucket instead. |
Forget to cross-reference snappy-clients lifecycle stage when counting "active clients" |
Active = lifecycle tag active AND has invoice in last 60 days. Both conditions, AND. |
# Funnel Metrics -- Pipeline, Conversion, Attribution
> Resource for `snappy-analytics`. End-to-end conversion tracking from first touch to closed deal. Covers paid funnel, organic funnel, and outbound funnel.
## Table of Contents
- [Three Funnels (visual)](#three-funnels-visual)
- [Stage Definitions](#stage-definitions)
- [Conversion Tracking](#conversion-tracking)
- [Pipeline Health Metrics](#pipeline-health-metrics)
- [Attribution Rules](#attribution-rules)
- [Funnel Output Format](#funnel-output-format)
- [Benchmarks](#benchmarks)
- [What AI Agents Get Wrong (funnel)](#what-ai-agents-get-wrong-funnel)
---
## Three Funnels (visual)
Per `snappy-website/SKILL.md > Workflow > Funnel chains` and `snappy-ads/SKILL.md > Workflow > Funnel chain`:
```
PAID FUNNEL
============
snappy-ads (YouTube ads)
-> snappy-website (VSL optin -> VSL video -> application -> Calendly)
-> snappy-pipeline (Orbiter enrichment, ad-attributed leads)
-> snappy-knowledge (contact created with ad UTM)
-> snappy-sales (call booked -> call completed -> close)
-> snappy-clients -> snappy-freshbooks (invoice + collect)
ORGANIC FUNNEL
==============
snappy-youtube (organic video)
-> snappy-website (blog or home via description link)
-> snappy-email (newsletter optin)
-> snappy-knowledge (contact)
-> snappy-sales (call) -> snappy-clients -> snappy-freshbooks
SOCIAL / OUTBOUND FUNNEL
========================
snappy-linkedin (post or outreach DM)
-> snappy-website (blog) OR direct DM thread
-> snappy-email (nurture)
-> snappy-knowledge -> snappy-sales -> snappy-clients -> snappy-freshbooks
(Outbound variant: snappy-linkedin outreach Day 0/3/7/14 -> direct call booking)
```
---
## Stage Definitions
|stage|definition|source skill|key|
|-----|----------|-----------|---|
|Impressions|Times an ad / post / video was shown|`snappy-ads` / `snappy-youtube` / `snappy-linkedin`|`impressions`|
|Visits|Unique visitors landing on a snappy.ai page|`snappy-website` (Vercel)|`unique_visitors`|
|Optins|Email captured (newsletter or VSL optin)|`snappy-website` + `snappy-email`|`contact/submissions` count + AC list adds|
|Applications|VSL application submitted|`snappy-website` (planned: `applications` table)|`/applications` (P1 endpoint)|
|Calls Booked|Calendly slot reserved|`snappy-calendar` + `snappy-sales`|`/calendar/events` filter for `call|demo|discovery`|
|Calls Completed|Call attended (past-dated, not no-show)|`snappy-sales`|`/leads?stage=call_completed`|
|Deals Closed|Signed + first invoice sent|`snappy-sales` -> `snappy-freshbooks`|`/leads?stage=closed_won` cross-checked against first invoice|
|Active Clients|Currently retained, paying|`snappy-clients` + `snappy-freshbooks`|`contacts?tag=client` AND has invoice in last 60d|
---
## Conversion Tracking
For each step in the funnel, compute the conversion rate to the next step over a defined window (default: last 30 days).
|step|formula|target (per `snappy-website/conversion-tracking.md`)|
|----|-------|----------------------------------------------------|
|Impressions -> Visits|`unique_visitors / impressions * 100`|3-5% (paid CTR), 4-7% (organic YouTube CTR)|
|Visits -> Optins|`optins / unique_visitors * 100`|3% home, 5% blog, 30%+ VSL optin|
|Optins -> Applications|`applications / optins * 100`|10%+ (VSL post-optin)|
|Applications -> Calls Booked|`calls_booked / applications * 100`|50%+|
|Calls Booked -> Calls Completed (show rate)|`calls_completed / calls_booked * 100`|75-85% (`snappy-sales`)|
|Calls Completed -> Closes|`closes / calls_completed * 100`|20-30% (`snappy-sales`)|
|Closes -> Active Clients (retention 60d)|`retained / closed * 100`|> 90% in first 60 days|
End-to-end overall: `overall_visit_to_close = closes / unique_visitors * 100` -- usually 0.1-1% for cold paid, 0.5-3% for warm organic.
### Pull procedure
```bash
# 1. Visits -- Vercel Analytics dashboard (browser-only)
# Capture from screenshot or manual read
# 2. Contact form submissions (proxy for organic optins + applications)
curl -s "$XANO/api:PB9UH7b9/contact/submissions" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.created_at >= "'$(date -v-30d +%Y-%m-%d)'")] | length'
# 3. Optins by source (UTM breakdown)
curl -s "$XANO/api:PB9UH7b9/contact/submissions" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.created_at >= "'$(date -v-30d +%Y-%m-%d)'")] | group_by(.utm_source) | map({source: .[0].utm_source, count: length})'
# 4. Calls booked (calendar)
curl -s "$XANO/api:PB9UH7b9/calendar/events?days=30" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.summary | test("call|demo|discovery"; "i"))] | length'
# 5. Pipeline stage counts
curl -s "$XANO/api:PB9UH7b9/leads" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.created_at >= "'$(date -v-30d +%Y-%m-%d)'")] | group_by(.stage) | map({stage: .[0].stage, count: length})'
# 6. Closes (cross-check leads + first invoices)
curl -s "$XANO/api:PB9UH7b9/leads?stage=closed_won&created_after=$(date -v-30d +%Y-%m-%d)" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
```
---
## Pipeline Health Metrics
Beyond raw conversion rates, watch:
|metric|definition|red_flag|owner|
|------|----------|--------|-----|
|Pipeline value|Sum of `(probability * deal_value)` for each open lead|< 3x monthly target|`snappy-sales`|
|Velocity|Median days from lead creation -> close|> 30 days for inbound, > 60d for outbound|`snappy-sales`|
|Stale leads|Leads not contacted in > 7 days|> 5 stale at any time|`snappy-sales`|
|Stage distribution|% of leads in each stage|> 50% stuck in same stage|`snappy-sales`|
|Lead source mix|% leads from paid / organic / outbound|Single source > 70% (concentration)|`snappy-knowledge` UTM|
|Contact growth rate|Net new contacts per week|< 10/week sustained|`snappy-knowledge`|
|Enrichment quality (Orbiter only)|% of enriched contacts with all fields populated|< 80% completeness|`snappy-pipeline`|
---
## Attribution Rules
The hard part. Most conversions have multiple touches before close.
### Snappy attribution model
Default: **last-touch UTM** captured by the contact form submission.
Why last-touch:
- It's what we have. The contact form captures one UTM set per submission.
- Multi-touch attribution requires every touch logged with a session ID -- `snappy-website` doesn't yet emit those events.
When to override last-touch:
- Robert manually notes a referral source on the call ("she heard about me from the Skool community")
- Email system captures the original optin source (different from the form UTM). ActiveCampaign is NOT in use; check Xano contact records instead.
- Calendly source field contains a hint
### Attribution priority order
When multiple sources are known for a single contact:
1. **Manually noted source** (during sales call) -- wins
2. **Original AC list source** (first list a contact joined) -- second
3. **First UTM captured** (earliest contact form submission) -- third
4. **Last UTM captured** (most recent contact form submission) -- fourth
5. **Default to "unknown"** if none of above
### Multi-touch hints
Even without true multi-touch attribution, you can infer touchpoints:
```bash
# All contact submissions for a single email -- shows multiple touches
curl -s "$XANO/api:PB9UH7b9/contact/submissions" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.email == "prospect@example.com")] | sort_by(.created_at)'
```
If a contact submitted twice (once via blog, once via VSL), report both touches but use first-touch as canonical attribution.
---
## Funnel Output Format
Standard ASCII table (for terminal + Telegram digest). Always disclose the window.
```
=== FUNNEL REPORT (Last 30 days, 2026-03-08 to 2026-04-07) ===
PAID FUNNEL (snappy-ads -> snappy-website -> snappy-sales)
Ad impressions: XX,XXX
Ad clicks: X,XXX (X.X% CTR)
VSL optin visits: X,XXX
Optins: XXX (X.X% optin rate)
Applications: XX (XX% of optins)
Calls Booked: XX (XX% of applications)
Calls Completed: XX (XX% show rate)
Deals Closed: X (XX% close rate)
----------------------------------------
Overall click-to-close: X.XX%
CPL: $XX
CP-Call: $XXX
CPA: $X,XXX
True ROI: X.Xx (collected revenue / spend)
ORGANIC FUNNEL (snappy-youtube + snappy-website -> snappy-sales)
YouTube views: X,XXX
Blog visits (organic): X,XXX
Email optins: XX (X.X% of visits)
Calls Booked: X (XX% of optins)
Calls Completed: X (XX% show rate)
Deals Closed: X (XX% close rate)
----------------------------------------
Overall view-to-close: X.XX%
OUTBOUND FUNNEL (snappy-linkedin)
Connection requests: XXX
Connections accepted: XX (XX% accept rate)
Day 7 soft asks: XX
Calls Booked: X (X% from outbound)
Deals Closed: X (X% from outbound)
UNATTRIBUTED
Unknown-source closes: X (NOTE: investigate if > 20% of total)
NOTES:
- [Bottleneck observation: e.g. "show rate is 60% -- below target. Investigate Calendly reminders."]
- [Concentration risk: e.g. "65% of closes from paid this month -- diversify."]
- [Trend: e.g. "Organic funnel completes at 2x close rate of paid -- bigger budget for content might be high ROI."]
```
---
## Benchmarks
Per-stage benchmarks. Use for "is this number poor/okay/good/great?".
|metric|poor|okay|good|great|
|------|----|----|----|-----|
|VSL optin rate|< 15%|15-25%|25-35%|> 35%|
|Application rate (post-optin)|< 5%|5-15%|15-25%|> 25%|
|Show rate|< 60%|60-75%|75-85%|> 85%|
|Close rate (high-ticket)|< 10%|10-20%|20-30%|> 30%|
|Visitor-to-close (paid)|< 0.1%|0.1-0.5%|0.5-1%|> 1%|
|Visitor-to-close (organic)|< 0.5%|0.5-1.5%|1.5-3%|> 3%|
|LTV:CAC|< 2:1|2-3:1|3-5:1|> 5:1|
|Pipeline velocity (inbound, days)|> 60|30-60|14-30|< 14|
|Outbound accept rate|< 20%|20-35%|35-50%|> 50%|
|Outbound conversion (DM -> call)|< 1%|1-3%|3-5%|> 5%|
These are starting benchmarks for high-ticket consulting funnels. Adjust as Snappy's actual numbers establish a baseline.
---
## What AI Agents Get Wrong (funnel)
|wrong|right|
|-----|-----|
|Compute conversion rates from incomplete data ("we had 47 visits and 3 optins this week, that's 6.4%")|If sample size < 100 visitors, label the rate as "noisy". If < 30, don't compute a rate at all -- report counts only.|
|Compare windows of different lengths ("optins are up -- 50 last month vs 12 last week")|Always normalize to the same window. Last 7d vs prior 7d. Last 30d vs prior 30d.|
|Attribute every conversion to last touch when first-touch is also known|If first-touch is recoverable, prefer it. Last-touch only when first-touch is unknown. Disclose which model in the output.|
|Mix paid + organic numbers in a single conversion rate|Always break down funnel by channel. Paid VSL conversion is ~30% optin; organic blog is ~5%. Mixing them produces meaningless averages.|
|Treat Calendly bookings as completed calls|`booked != completed`. Show rate is the most common pipeline gap. Always pull both numbers.|
|Compute close rate against booked instead of completed|Close rate must use `closes / completed_calls`, NOT `closes / booked`. Show rate is a separate metric.|
|Count canceled calls as no-shows|Canceled with notice = different from no-show. Calendly distinguishes. Don't fold them.|
|Pull lead counts without filtering by `stage`|Always filter -- leads in `disqualified` or `nurture` stages are not in active pipeline.|
|Compute LTV from a single client|LTV needs >= 5 closed-and-churned clients to be meaningful. Until then report median deal size, not LTV.|
|Compute LTV:CAC using gross revenue instead of collected|Always use collected (per `snappy-freshbooks`). Outstanding ≠ collected ≠ LTV.|
|Use UTM `utm_source` alone for attribution|UTM source + medium + campaign + content together. A single source field collapses multiple campaigns.|
|Include test contacts (yourself, contractors) in funnel counts|Filter by email domain -- exclude `@snappy.ai`, `@robertboulos.com`, known contractor domains.|
|Project annualized revenue from a single month's closes|Closes are lumpy. Use trailing 3-month average for projections.|
|Compute pipeline value at face value of every open lead|Apply probability weighting by stage (e.g., 25% for discovery, 50% for proposal, 80% for verbal).|
|Skip the "unattributed" bucket|Always report unknown-source separately. > 20% unattributed = data hygiene problem.|
|Compare different attribution windows (Hyros 30d-click vs FreshBooks 60d-paid)|Always disclose the attribution window. Apples to apples only.|
|Treat sales call notes as structured data|They're not. Pull structured fields from `/leads`, treat call notes as supplementary context only.|
|Hardcode "average sales cycle 30 days" as a target|Snappy is high-ticket consulting. Cycles vary by deal size. Compute median per deal-size bucket instead.|
|Forget to cross-reference `snappy-clients` lifecycle stage when counting "active clients"|Active = lifecycle tag `active` AND has invoice in last 60 days. Both conditions, AND.|
Resource for
snappy-analytics. Pull patterns for everything money-related. All canonical FreshBooks endpoints come fromsnappy-freshbooks/api-reference.md.
Credentials load from .env.cache via snappy-settings. Per snappy-infra/auth-reference.md:
bashSNAPPY_SETTINGS_QUIET=1 source ~/.claude/skills/snappy-settings/scripts/load-env.sh
# exports XANO, XANO_METADATA_TOKEN, and all other credentials
This skill is read-only against FreshBooks. All writes (create/send/mark-paid) live in snappy-freshbooks.
MRR = sum of recurring monthly revenue from active client invoices, normalized to a monthly cadence.
| invoice cadence | normalization |
|---|---|
| monthly | amount |
| quarterly | amount / 3 |
| annual | amount / 12 |
| weekly | amount * 4.33 |
| one-time / project | EXCLUDED -- never counted in MRR |
bash# All invoices
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" > /tmp/invoices.json
# All clients (for active determination)
curl -s "$XANO/api:ACdo1OLG/freshbooks/clients" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" > /tmp/clients.json
paid, sent, viewed, partial). Exclude draft, void, disputed.frequency set, OR client has at least 2 invoices in last 90 days at the same amount.(this_month_MRR - last_month_MRR) / last_month_MRR * 100.NEVER include:
closed clients with stale invoicessnappy-freshbooks/api-reference.md flags GET /freshbooks/dashboards/mrr as a P1 build target. When live, use it instead of client-side aggregation.
bash# Filter paid invoices with payment_date in current month
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.status == "paid" and (.payment_date | startswith("'$(date +%Y-%m)'")))] | map(.amount) | add'
bash# Sum of unpaid + partial + overdue
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.status == "sent" or .status == "viewed" or .status == "partial" or .status == "overdue")] | map(.amount) | add'
bash# Sum of paid amounts where payment_date in last 7 days
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.status == "paid" and .payment_date >= "'$(date -v-7d +%Y-%m-%d)'")] | map(.amount) | add'
| bucket | days_overdue | action_owner |
|---|---|---|
| Current | 0 | -- |
| 1-7 days | 1-7 | snappy-freshbooks no action yet (per workflows.md) |
| 8-14 days | 8-14 | snappy-freshbooks first chase email (day 7) |
| 15-30 days | 15-30 | snappy-freshbooks whatsapp follow-up (day 14) |
| 30+ days | 30+ | snappy-freshbooks Robert escalation |
Compute by: due_date - today for each overdue invoice.
bash# Active = client has at least one invoice in status (paid, sent, viewed) in last 60 days
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.issue_date >= "'$(date -v-60d +%Y-%m-%d)'" and (.status == "paid" or .status == "sent" or .status == "viewed"))] | group_by(.client) | length'
MRR / active_client_count. Compute after MRR is known.
bash# Group MRR-eligible invoices by client; find max
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.status == "paid" or .status == "sent")] | group_by(.client) | map({client: .[0].client, total: (map(.amount) | add)}) | max_by(.total)'
Then: max_client_total / MRR * 100 = concentration %. Per snappy-freshbooks/SKILL.md > Targets and Red Flags, > 30% is a red flag.
churn_rate = clients_lost_this_month / clients_at_start_of_month * 100
A "lost" client = had a paid invoice in (this_month - 30 days) but has no paid or sent invoice in last 30 days.
bash# Clients with invoices 30-60 days ago
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq -r '[.[] | select(.issue_date >= "'$(date -v-60d +%Y-%m-%d)'" and .issue_date < "'$(date -v-30d +%Y-%m-%d)'") | .client] | unique[]' \
> /tmp/clients_30_60_days_ago.txt
# Clients with invoices in last 30 days
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq -r '[.[] | select(.issue_date >= "'$(date -v-30d +%Y-%m-%d)'") | .client] | unique[]' \
> /tmp/clients_last_30_days.txt
# Lost = in old set but NOT in new set
comm -23 <(sort /tmp/clients_30_60_days_ago.txt) <(sort /tmp/clients_last_30_days.txt)
snappy-clients#snappy-clients tracks lifecycle stage (active -> wind_down -> past_client). Pull tag changes:
bash# Past clients tagged in last 30 days
curl -s "$XANO/api:PB9UH7b9/contacts?tag=past_client" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.updated_at >= "'$(date -v-30d +%Y-%m-%d)'")]'
Reconcile FreshBooks-derived churn vs snappy-clients tag changes. Discrepancy = data hygiene issue.
bashcurl -s "$XANO/api:PB9UH7b9/freshbooks/expenses" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
profit_margin = (revenue - expenses) / revenue * 100
Use cash-basis revenue (paid invoices in month) and cash-basis expenses (logged expenses in month).
Per snappy-freshbooks/SKILL.md > Targets and Red Flags:
runway_months = bank_balance / 3_month_avg_expense
Bank balance is NOT in FreshBooks -- pull from manual entry or bank export. Until automated, prompt Robert for bank balance during monthly close.
3-month average expense:
bashcurl -s "$XANO/api:PB9UH7b9/freshbooks/expenses" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.date >= "'$(date -v-90d +%Y-%m-%d)'")] | map(.amount) | add / 3'
Per snappy-freshbooks/SKILL.md:
bashcurl -s "$XANO/api:PB9UH7b9/freshbooks/expenses" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.date >= "'$(date -v-30d +%Y-%m-%d)'")] | group_by(.category) | map({category: .[0].category, total: (map(.amount) | add)}) | sort_by(-.total)'
Canonical categories per snappy-freshbooks/recurring-and-expenses.md: software_saas, contractors, ads, tools_infra, professional, travel, office, misc.
snappy-ads/SKILL.md Workflow 2#When ads are running (currently dormant per snappy-ads/SKILL.md > Status), pull weekly metrics:
bashcurl -s "$XANO/api:PB9UH7b9/ads/metrics?period=last_7_days" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
curl -s "$XANO/api:PB9UH7b9/ads/metrics?period=last_30_days" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
| metric | definition | target (per snappy-ads/SKILL.md) |
kill_threshold |
|---|---|---|---|
| Spend | cost |
< $100/day | -- |
| Impressions | impressions |
-- | -- |
| CTR | clicks / impressions * 100 |
> 1% | < 1% after 1k impressions |
| View rate | views_30s / impressions * 100 |
> 20% | < 15% after 1k impressions |
| CPL | spend / opt_ins |
< $25 | > $50 (2x target) after 14d / 20+ leads |
| CP-Call | spend / calls_booked |
< $250 | > $500 after 14d / 5+ calls |
| CPA | spend / clients_closed |
< $2,000 | -- |
| ROAS | revenue / spend |
3x+ | < 2x sustained |
Per snappy-ads/SKILL.md > True ROI Formula:
true_roi = (collected_revenue_from_ad_attributed_clients - total_ad_spend) / total_ad_spend
This is NOT projected ROAS. It uses actual collected revenue from FreshBooks, joined to ad-attributed clients via snappy-knowledge UTM tags.
bash# 1. Get clients tagged with ad source in knowledge graph
AD_CLIENTS=$(curl -s "$XANO/api:PB9UH7b9/contacts?tag=ad_source_youtube" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" | jq -r '.[].name')
# 2. Sum collected revenue from FreshBooks for those clients
TOTAL_REVENUE=$(curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq --argjson clients "$(echo "$AD_CLIENTS" | jq -R -s 'split("\n") | map(select(length > 0))')" \
'[.[] | select(.status == "paid" and (.client | IN($clients[])))] | map(.amount) | add')
# 3. Sum total ad spend
TOTAL_SPEND=$(curl -s "$XANO/api:PB9UH7b9/ads/metrics?period=all_time" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" | jq '.spend')
# 4. Compute true ROI
echo "True ROI: $(echo "scale=2; ($TOTAL_REVENUE - $TOTAL_SPEND) / $TOTAL_SPEND" | bc)"
Run this monthly. Per snappy-ads/SKILL.md, this is paired with snappy-freshbooks and represents the most honest measure of paid funnel performance.
Last Friday of every month (per snappy-ops/weekly-rhythm.md > Monthly Overlays).
| metric | source | frequency |
|---|---|---|
| MRR (this month) | snappy-freshbooks invoices pull |
weekly |
| MRR (last month) | same, with month filter | monthly snapshot |
| MoM MRR growth | computed | monthly |
| Revenue this month | snappy-freshbooks invoices |
weekly |
| Cash collected this month | same | weekly |
| Outstanding receivables | same | weekly |
| Active client count | computed from invoices | weekly |
| Churn (monthly) | computed | monthly |
| ARPU | computed | monthly |
| Top client concentration % | computed | monthly |
| Total expenses (month) | snappy-freshbooks expenses |
monthly |
| Profit margin | computed | monthly |
| Runway | computed (needs bank balance) | monthly |
| Ad spend (month) | snappy-ads metrics |
monthly (if running) |
| True ROI (paid funnel) | computed | monthly |
| Expense category top 3 | computed | monthly |
Run dashboard-formats.md > Monthly Snappy Review to format the synthesized result.
| wrong | right |
|---|---|
| Calculate MRR from sent invoices (treating sent as already-revenue) | MRR = paid + sent recurring invoices, normalized to monthly. Outstanding ≠ revenue until paid (per snappy-freshbooks/SKILL.md What AI Agents Get Wrong). |
| Treat invoice sent as cash received | Cash flow uses paid invoices only. Outstanding receivables tracked separately. |
| Include one-time / project invoices in MRR | MRR is recurring only. One-time invoices go in revenue this month, not MRR. |
Include draft or void invoices in any calculation |
Filter to status in (paid, sent, viewed, partial, overdue). Always. |
| Include overdue invoices in MRR | Overdue ≠ recurring. Move them to receivables, NOT MRR. |
| Compare 28-day windows that overlap month boundaries without normalizing | Always set explicit start_date / end_date. Never compare "last 28 days" if one end is mid-month. |
| Use accrual revenue for runway calculation | Runway is cash basis. Bank balance / monthly cash burn. |
| Skip the bank balance prompt | Runway is impossible without bank balance -- and there's no API for it. Always prompt Robert for current bank balance during monthly close. |
| Compute ROAS as the only ad ROI | snappy-ads/SKILL.md mandates true ROI using actual collected FreshBooks revenue, not projected ROAS from Hyros. |
| Compare ROAS across platforms (Google Ads vs LinkedIn Ads) | Different attribution windows. Always disclose attribution model. ROAS is platform-specific. |
Treat the FreshBooks client string as a stable join key |
Client names are typed by humans and have typos. Use client_id from freshbooks/clients when joining. Fall back to fuzzy match only if client_id missing. |
Compute churn from invoice gaps without checking snappy-clients lifecycle tags |
Cross-reference. A client may pause for a month and resume. Lifecycle tag is the source of truth for "actually churned". |
| Project ARR by multiplying MRR by 12 and reporting it | ARR is misleading for high-ticket consulting where retention is < 12 months. Report MRR + estimated annualized contract value separately. |
| Subtract this month's revenue from last month's without checking for partial months | If today is the 7th of the month, "this month" is incomplete. Either wait until month-end or use a 30-day rolling window. |
| Pull the Hyros ROAS number and report it as gospel | Hyros uses click attribution which can over-attribute. Always reconcile against FreshBooks paid invoices. |
| Sum invoice amounts in different currencies without conversion | FreshBooks supports multi-currency (USD, CAD, etc.). Filter by currency or convert to base currency before summing. |
| Forget to multiply quarterly retainers by 1/3 in MRR normalization | Always normalize. A $15k/quarter retainer is $5k MRR, not $15k. |
| Compare profit margin across months without seasonal context | Q1 ad spend is usually higher than Q3. Note context, don't flag every dip as a problem. |
# Revenue Metrics -- MRR, Cash Flow, ROI, Ad Spend
> Resource for `snappy-analytics`. Pull patterns for everything money-related. All canonical FreshBooks endpoints come from `snappy-freshbooks/api-reference.md`.
## Table of Contents
- [Auth](#auth)
- [Recurring Revenue (MRR)](#recurring-revenue-mrr)
- [Cash Flow & Outstanding](#cash-flow--outstanding)
- [Client Concentration & ARPU](#client-concentration--arpu)
- [Churn](#churn)
- [Profit Margin & Runway](#profit-margin--runway)
- [Ad Spend & ROI](#ad-spend--roi)
- [Monthly Close Synthesis](#monthly-close-synthesis)
- [What AI Agents Get Wrong (revenue)](#what-ai-agents-get-wrong-revenue)
---
## Auth
Credentials load from `.env.cache` via `snappy-settings`. Per `snappy-infra/auth-reference.md`:
```bash
SNAPPY_SETTINGS_QUIET=1 source ~/.claude/skills/snappy-settings/scripts/load-env.sh
# exports XANO, XANO_METADATA_TOKEN, and all other credentials
```
This skill is **read-only against FreshBooks**. All writes (create/send/mark-paid) live in `snappy-freshbooks`.
---
## Recurring Revenue (MRR)
### Definition (operational)
MRR = sum of recurring monthly revenue from active client invoices, normalized to a monthly cadence.
|invoice cadence|normalization|
|--------------|-------------|
|monthly|amount|
|quarterly|amount / 3|
|annual|amount / 12|
|weekly|amount * 4.33|
|one-time / project|EXCLUDED -- never counted in MRR|
### Pull procedure
```bash
# All invoices
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" > /tmp/invoices.json
# All clients (for active determination)
curl -s "$XANO/api:ACdo1OLG/freshbooks/clients" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" > /tmp/clients.json
```
### Computation rules
1. **Filter** invoices to status in (`paid`, `sent`, `viewed`, `partial`). Exclude `draft`, `void`, `disputed`.
2. **Filter** to recurring only -- line item description matches "retainer" / "monthly" or invoice has `frequency` set, OR client has at least 2 invoices in last 90 days at the same amount.
3. **Normalize** each to monthly using the table above.
4. **Sum** = current MRR.
5. **Compute MoM growth**: `(this_month_MRR - last_month_MRR) / last_month_MRR * 100`.
NEVER include:
- One-time project invoices
- Past `closed` clients with stale invoices
- Outstanding/overdue invoices that haven't been paid yet (those go in receivables, not MRR)
- Disputed or voided invoices
- Future-dated draft invoices
### Aspirational endpoint (when it lands)
`snappy-freshbooks/api-reference.md` flags `GET /freshbooks/dashboards/mrr` as a P1 build target. When live, use it instead of client-side aggregation.
---
## Cash Flow & Outstanding
### Revenue this month (cash basis)
```bash
# Filter paid invoices with payment_date in current month
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.status == "paid" and (.payment_date | startswith("'$(date +%Y-%m)'")))] | map(.amount) | add'
```
### Outstanding receivables
```bash
# Sum of unpaid + partial + overdue
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.status == "sent" or .status == "viewed" or .status == "partial" or .status == "overdue")] | map(.amount) | add'
```
### Cash collected this week
```bash
# Sum of paid amounts where payment_date in last 7 days
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.status == "paid" and .payment_date >= "'$(date -v-7d +%Y-%m-%d)'")] | map(.amount) | add'
```
### Aging buckets (overdue tiers)
|bucket|days_overdue|action_owner|
|------|-----------|------------|
|Current|0|--|
|1-7 days|1-7|`snappy-freshbooks` no action yet (per workflows.md)|
|8-14 days|8-14|`snappy-freshbooks` first chase email (day 7)|
|15-30 days|15-30|`snappy-freshbooks` whatsapp follow-up (day 14)|
|30+ days|30+|`snappy-freshbooks` Robert escalation|
Compute by: `due_date - today` for each `overdue` invoice.
---
## Client Concentration & ARPU
### Active client count
```bash
# Active = client has at least one invoice in status (paid, sent, viewed) in last 60 days
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.issue_date >= "'$(date -v-60d +%Y-%m-%d)'" and (.status == "paid" or .status == "sent" or .status == "viewed"))] | group_by(.client) | length'
```
### ARPU (average revenue per client)
`MRR / active_client_count`. Compute after MRR is known.
### Single-client concentration
```bash
# Group MRR-eligible invoices by client; find max
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.status == "paid" or .status == "sent")] | group_by(.client) | map({client: .[0].client, total: (map(.amount) | add)}) | max_by(.total)'
```
Then: `max_client_total / MRR * 100` = concentration %. Per `snappy-freshbooks/SKILL.md > Targets and Red Flags`, > 30% is a red flag.
---
## Churn
### Monthly churn formula
`churn_rate = clients_lost_this_month / clients_at_start_of_month * 100`
A "lost" client = had a paid invoice in (this_month - 30 days) but has no paid or sent invoice in last 30 days.
### Computation
```bash
# Clients with invoices 30-60 days ago
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq -r '[.[] | select(.issue_date >= "'$(date -v-60d +%Y-%m-%d)'" and .issue_date < "'$(date -v-30d +%Y-%m-%d)'") | .client] | unique[]' \
> /tmp/clients_30_60_days_ago.txt
# Clients with invoices in last 30 days
curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq -r '[.[] | select(.issue_date >= "'$(date -v-30d +%Y-%m-%d)'") | .client] | unique[]' \
> /tmp/clients_last_30_days.txt
# Lost = in old set but NOT in new set
comm -23 <(sort /tmp/clients_30_60_days_ago.txt) <(sort /tmp/clients_last_30_days.txt)
```
### Cross-check with `snappy-clients`
`snappy-clients` tracks lifecycle stage (`active` -> `wind_down` -> `past_client`). Pull tag changes:
```bash
# Past clients tagged in last 30 days
curl -s "$XANO/api:PB9UH7b9/contacts?tag=past_client" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.updated_at >= "'$(date -v-30d +%Y-%m-%d)'")]'
```
Reconcile FreshBooks-derived churn vs `snappy-clients` tag changes. Discrepancy = data hygiene issue.
---
## Profit Margin & Runway
### Expense pull
```bash
curl -s "$XANO/api:PB9UH7b9/freshbooks/expenses" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
```
### Profit margin (monthly)
```
profit_margin = (revenue - expenses) / revenue * 100
```
Use cash-basis revenue (paid invoices in month) and cash-basis expenses (logged expenses in month).
Per `snappy-freshbooks/SKILL.md > Targets and Red Flags`:
- Target: >= 60%
- Red flag: < 50%
### Runway
```
runway_months = bank_balance / 3_month_avg_expense
```
Bank balance is NOT in FreshBooks -- pull from manual entry or bank export. Until automated, prompt Robert for bank balance during monthly close.
3-month average expense:
```bash
curl -s "$XANO/api:PB9UH7b9/freshbooks/expenses" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.date >= "'$(date -v-90d +%Y-%m-%d)'")] | map(.amount) | add / 3'
```
Per `snappy-freshbooks/SKILL.md`:
- Target: >= 6 months
- Red flag: < 3 months (freeze non-essential spending)
### Expense category breakdown
```bash
curl -s "$XANO/api:PB9UH7b9/freshbooks/expenses" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq '[.[] | select(.date >= "'$(date -v-30d +%Y-%m-%d)'")] | group_by(.category) | map({category: .[0].category, total: (map(.amount) | add)}) | sort_by(-.total)'
```
Canonical categories per `snappy-freshbooks/recurring-and-expenses.md`: `software_saas`, `contractors`, `ads`, `tools_infra`, `professional`, `travel`, `office`, `misc`.
---
## Ad Spend & ROI
### Authoritative source: `snappy-ads/SKILL.md` Workflow 2
When ads are running (currently dormant per `snappy-ads/SKILL.md > Status`), pull weekly metrics:
```bash
curl -s "$XANO/api:PB9UH7b9/ads/metrics?period=last_7_days" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
curl -s "$XANO/api:PB9UH7b9/ads/metrics?period=last_30_days" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN"
```
### Metrics to capture
|metric|definition|target (per `snappy-ads/SKILL.md`)|kill_threshold|
|------|----------|-----------------------------------|--------------|
|Spend|`cost`|< $100/day|--|
|Impressions|`impressions`|--|--|
|CTR|`clicks / impressions * 100`|> 1%|< 1% after 1k impressions|
|View rate|`views_30s / impressions * 100`|> 20%|< 15% after 1k impressions|
|CPL|`spend / opt_ins`|< $25|> $50 (2x target) after 14d / 20+ leads|
|CP-Call|`spend / calls_booked`|< $250|> $500 after 14d / 5+ calls|
|CPA|`spend / clients_closed`|< $2,000|--|
|ROAS|`revenue / spend`|3x+|< 2x sustained|
### True ROI (the only number that matters)
Per `snappy-ads/SKILL.md > True ROI Formula`:
```
true_roi = (collected_revenue_from_ad_attributed_clients - total_ad_spend) / total_ad_spend
```
This is NOT projected ROAS. It uses **actual collected revenue** from FreshBooks, joined to ad-attributed clients via `snappy-knowledge` UTM tags.
#### Computation
```bash
# 1. Get clients tagged with ad source in knowledge graph
AD_CLIENTS=$(curl -s "$XANO/api:PB9UH7b9/contacts?tag=ad_source_youtube" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" | jq -r '.[].name')
# 2. Sum collected revenue from FreshBooks for those clients
TOTAL_REVENUE=$(curl -s "$XANO/api:PB9UH7b9/freshbooks/invoices" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" \
| jq --argjson clients "$(echo "$AD_CLIENTS" | jq -R -s 'split("\n") | map(select(length > 0))')" \
'[.[] | select(.status == "paid" and (.client | IN($clients[])))] | map(.amount) | add')
# 3. Sum total ad spend
TOTAL_SPEND=$(curl -s "$XANO/api:PB9UH7b9/ads/metrics?period=all_time" \
-H "Authorization: Bearer $XANO_METADATA_TOKEN" | jq '.spend')
# 4. Compute true ROI
echo "True ROI: $(echo "scale=2; ($TOTAL_REVENUE - $TOTAL_SPEND) / $TOTAL_SPEND" | bc)"
```
Run this monthly. Per `snappy-ads/SKILL.md`, this is paired with `snappy-freshbooks` and represents the most honest measure of paid funnel performance.
---
## Monthly Close Synthesis
Last Friday of every month (per `snappy-ops/weekly-rhythm.md > Monthly Overlays`).
### Inputs to gather
|metric|source|frequency|
|------|------|---------|
|MRR (this month)|`snappy-freshbooks` invoices pull|weekly|
|MRR (last month)|same, with month filter|monthly snapshot|
|MoM MRR growth|computed|monthly|
|Revenue this month|`snappy-freshbooks` invoices|weekly|
|Cash collected this month|same|weekly|
|Outstanding receivables|same|weekly|
|Active client count|computed from invoices|weekly|
|Churn (monthly)|computed|monthly|
|ARPU|computed|monthly|
|Top client concentration %|computed|monthly|
|Total expenses (month)|`snappy-freshbooks` expenses|monthly|
|Profit margin|computed|monthly|
|Runway|computed (needs bank balance)|monthly|
|Ad spend (month)|`snappy-ads` metrics|monthly (if running)|
|True ROI (paid funnel)|computed|monthly|
|Expense category top 3|computed|monthly|
### Output
Run `dashboard-formats.md > Monthly Snappy Review` to format the synthesized result.
---
## What AI Agents Get Wrong (revenue)
|wrong|right|
|-----|-----|
|Calculate MRR from sent invoices (treating sent as already-revenue)|MRR = paid + sent recurring invoices, normalized to monthly. Outstanding ≠ revenue until paid (per `snappy-freshbooks/SKILL.md` What AI Agents Get Wrong).|
|Treat invoice sent as cash received|Cash flow uses paid invoices only. Outstanding receivables tracked separately.|
|Include one-time / project invoices in MRR|MRR is recurring only. One-time invoices go in revenue this month, not MRR.|
|Include `draft` or `void` invoices in any calculation|Filter to status in (paid, sent, viewed, partial, overdue). Always.|
|Include overdue invoices in MRR|Overdue ≠ recurring. Move them to receivables, NOT MRR.|
|Compare 28-day windows that overlap month boundaries without normalizing|Always set explicit start_date / end_date. Never compare "last 28 days" if one end is mid-month.|
|Use accrual revenue for runway calculation|Runway is cash basis. Bank balance / monthly cash burn.|
|Skip the bank balance prompt|Runway is impossible without bank balance -- and there's no API for it. Always prompt Robert for current bank balance during monthly close.|
|Compute ROAS as the only ad ROI|`snappy-ads/SKILL.md` mandates **true ROI** using actual collected FreshBooks revenue, not projected ROAS from Hyros.|
|Compare ROAS across platforms (Google Ads vs LinkedIn Ads)|Different attribution windows. Always disclose attribution model. ROAS is platform-specific.|
|Treat the FreshBooks `client` string as a stable join key|Client names are typed by humans and have typos. Use `client_id` from `freshbooks/clients` when joining. Fall back to fuzzy match only if `client_id` missing.|
|Compute churn from invoice gaps without checking `snappy-clients` lifecycle tags|Cross-reference. A client may pause for a month and resume. Lifecycle tag is the source of truth for "actually churned".|
|Project ARR by multiplying MRR by 12 and reporting it|ARR is misleading for high-ticket consulting where retention is < 12 months. Report MRR + estimated annualized contract value separately.|
|Subtract this month's revenue from last month's without checking for partial months|If today is the 7th of the month, "this month" is incomplete. Either wait until month-end or use a 30-day rolling window.|
|Pull the Hyros ROAS number and report it as gospel|Hyros uses click attribution which can over-attribute. Always reconcile against FreshBooks paid invoices.|
|Sum invoice amounts in different currencies without conversion|FreshBooks supports multi-currency (USD, CAD, etc.). Filter by currency or convert to base currency before summing.|
|Forget to multiply quarterly retainers by 1/3 in MRR normalization|Always normalize. A $15k/quarter retainer is $5k MRR, not $15k.|
|Compare profit margin across months without seasonal context|Q1 ad spend is usually higher than Q3. Note context, don't flag every dip as a problem.|
Common failures across the analytics data sources and how to fix them.
| Problem | Fix |
|---|---|
| YouTube Studio won't load | Re-auth: agent-browser --auth google then retry |
| FreshBooks API returns 401 | Re-run auth setup (XANO_METADATA_TOKEN expired) |
| Skool data looks stale | Check agent-browser is using correct auth state file |
| Missing ad data | Confirm Google Ads account has active campaigns |
| Email metrics unavailable | Check Xano emails/list endpoint and XANO_METADATA_TOKEN auth. ActiveCampaign is NOT in use. |
| Vercel analytics empty | Verify analytics is enabled on the snappy-ai Vercel project |
| LinkedIn analytics 403 | Re-run agent-browser --state ~/.openclaw/workspace/linkedin-auth.json --goal "verify logged in" |
| Xano 401 on any read | Refresh XANO_METADATA_TOKEN in ~/.claude/skills/snappy-settings/.env.cache, then re-source load-env.sh |
| Agent-browser times out | Check BROWSER=chrome and headless mode; rerun with --debug |
# snappy-analytics -- Troubleshooting Common failures across the analytics data sources and how to fix them. | Problem | Fix | |---------|-----| | YouTube Studio won't load | Re-auth: `agent-browser --auth google` then retry | | FreshBooks API returns 401 | Re-run auth setup (XANO_METADATA_TOKEN expired) | | Skool data looks stale | Check agent-browser is using correct auth state file | | Missing ad data | Confirm Google Ads account has active campaigns | | Email metrics unavailable | Check Xano `emails/list` endpoint and `XANO_METADATA_TOKEN` auth. ActiveCampaign is NOT in use. | | Vercel analytics empty | Verify analytics is enabled on the snappy-ai Vercel project | | LinkedIn analytics 403 | Re-run `agent-browser --state ~/.openclaw/workspace/linkedin-auth.json --goal "verify logged in"` | | Xano 401 on any read | Refresh `XANO_METADATA_TOKEN` in `~/.claude/skills/snappy-settings/.env.cache`, then re-source `load-env.sh` | | Agent-browser times out | Check `BROWSER=chrome` and headless mode; rerun with `--debug` |
#!/usr/bin/env bash
# weekly-dashboard.sh -- Aggregates metrics from multiple sources into a weekly markdown dashboard.
# Usage: ./weekly-dashboard.sh
# Output: Markdown dashboard to stdout.
set -euo pipefail
WEEK=$(date +%Y-W%V)
TODAY=$(date +%Y-%m-%d)
CONTENT_ENGINE="https://rb-content-engine.fly.dev"
FEEDBACK_LOG="$HOME/.claude/logs/agents-md-feedback.log"
# ── helpers ──────────────────────────────────────────────────────────────────
section() { printf '\n## %s\n\n' "$1"; }
row() { printf '| %-30s | %-40s |\n' "$1" "$2"; }
note() { printf '> %s\n' "$1"; }
# ── header ───────────────────────────────────────────────────────────────────
cat <<EOF
# Snappy Weekly Dashboard -- $WEEK
Generated: $TODAY
---
EOF
# ── 1. Content Pipeline (content-engine) ─────────────────────────────────────
section "Content Pipeline [snappy-content]"
CONTENT_RAW=$(curl -s --max-time 10 -X POST "$CONTENT_ENGINE/sql" \
-H "Content-Type: application/json" \
-d '{"query": "SELECT type, status, COUNT(*) as count FROM content_atoms WHERE mined_at > now() - interval '\''7 days'\'' GROUP BY type, status ORDER BY type"}' 2>/dev/null || echo "ERROR")
if [ "$CONTENT_RAW" = "ERROR" ] || [ -z "$CONTENT_RAW" ]; then
echo "[NO DATA] Content engine unreachable or returned empty."
else
# Try to parse as JSON array
if echo "$CONTENT_RAW" | jq -e '.' >/dev/null 2>&1; then
ROWS=$(echo "$CONTENT_RAW" | jq -r '.rows // .result // . | if type == "array" then . else [.] end | .[] | "\(.type // "--")\t\(.status // "--")\t\(.count // "--")"' 2>/dev/null || echo "")
if [ -n "$ROWS" ]; then
printf '| %-20s | %-15s | %s |\n' "Type" "Status" "Count"
printf '| %-20s | %-15s | %s |\n' "--------------------" "---------------" "-----"
echo "$ROWS" | while IFS=$'\t' read -r type status count; do
printf '| %-20s | %-15s | %s |\n' "$type" "$status" "$count"
done
else
echo "Response received but could not parse rows."
echo '```'
echo "$CONTENT_RAW" | head -20
echo '```'
fi
else
echo "Non-JSON response from content engine:"
echo '```'
echo "$CONTENT_RAW" | head -20
echo '```'
fi
fi
# ── 2. Git Activity [snappy-github] ─────────────────────────────────────────
section "Git Activity (last 7 days) [snappy-github]"
if command -v gh &>/dev/null; then
REPOS=("snappybusiness/snappy-website" "snappybusiness/content-engine" "snappybusiness/total-crm" "snappybusiness/snappy-xano-mcp")
SINCE=$(date -v-7d +%Y-%m-%dT00:00:00Z 2>/dev/null || date -d '7 days ago' +%Y-%m-%dT00:00:00Z 2>/dev/null || echo "")
if [ -n "$SINCE" ]; then
printf '| %-40s | %s |\n' "Repository" "Commits (7d)"
printf '| %-40s | %s |\n' "----------------------------------------" "------------"
TOTAL_COMMITS=0
for REPO in "${REPOS[@]}"; do
COUNT=$(gh api "repos/$REPO/commits?since=$SINCE&per_page=100" --jq 'if type == "array" then length else empty end' 2>/dev/null || echo "--")
if [[ "$COUNT" =~ ^[0-9]+$ ]]; then
TOTAL_COMMITS=$((TOTAL_COMMITS + COUNT))
fi
printf '| %-40s | %s |\n' "$REPO" "$COUNT"
done
echo ""
echo "**Total commits: $TOTAL_COMMITS**"
else
echo "Could not compute date range."
fi
else
echo "[NO DATA] gh CLI not available. Install with: brew install gh"
fi
# ── 3. Skill Usage / AGENTS.md Feedback ──────────────────────────────────────
section "Skill Health [snappy-analytics]"
if [ -f "$FEEDBACK_LOG" ]; then
TOTAL_ENTRIES=$(wc -l < "$FEEDBACK_LOG" | tr -d ' ')
WEEK_START=$(date -v-7d +%Y-%m-%d 2>/dev/null || date -d '7 days ago' +%Y-%m-%d 2>/dev/null || echo "")
if [ -n "$WEEK_START" ]; then
RECENT=$(awk -v start="$WEEK_START" '$0 ~ /^\[/ { d=substr($1,2,10); if (d >= start) print }' "$FEEDBACK_LOG" | wc -l | tr -d ' ')
else
RECENT="?"
fi
FIXED=$(grep -c 'FIXED' "$FEEDBACK_LOG" 2>/dev/null || echo 0)
LOGGED=$(grep -c 'LOGGED' "$FEEDBACK_LOG" 2>/dev/null || echo 0)
echo "- Total feedback entries: **$TOTAL_ENTRIES**"
echo "- Entries this week: **$RECENT**"
echo "- Self-fixed (P-corrections): **$FIXED**"
echo "- Queued for regen (I-corrections): **$LOGGED**"
# Top skills with feedback
echo ""
echo "**Top skills with feedback gaps:**"
echo ""
awk -F': ' '/^\[/{print $2}' "$FEEDBACK_LOG" | awk -F': ' '{print $1}' | sort | uniq -c | sort -rn | head -5 | while read -r count skill; do
echo "- $skill: $count entries"
done
else
echo "[NO DATA] No feedback log found at $FEEDBACK_LOG"
fi
# ── 4. Skill File Freshness ──────────────────────────────────────────────────
section "Skill File Freshness"
SKILLS_DIR="$HOME/.claude/skills"
if [ -d "$SKILLS_DIR" ]; then
SKILL_COUNT=$(find -L "$SKILLS_DIR" -maxdepth 1 -type d | tail -n +2 | wc -l | tr -d ' ')
AGENTS_COUNT=$(find -L "$SKILLS_DIR" -name "AGENTS.md" | wc -l | tr -d ' ')
echo "- Total skill directories: **$SKILL_COUNT**"
echo "- Skills with AGENTS.md: **$AGENTS_COUNT**"
# Recently modified skills (last 7 days)
echo ""
echo "**Recently modified AGENTS.md (last 7 days):**"
echo ""
find -L "$SKILLS_DIR" -name "AGENTS.md" -mtime -7 -exec stat -f '%Sm %N' -t '%Y-%m-%d' {} \; 2>/dev/null | sort -r | head -10 | while read -r date path; do
skill=$(echo "$path" | sed "s|$SKILLS_DIR/||;s|/AGENTS.md||")
echo "- $skill ($date)"
done
fi
# ── footer ───────────────────────────────────────────────────────────────────
cat <<EOF
---
*Dashboard generated by \`snappy-analytics/weekly-dashboard.sh\` at $(date -u +%FT%TZ)*
*Source tags: [snappy-content] [snappy-github] [snappy-analytics]*
EOF
#!/usr/bin/env bash
# weekly-dashboard.sh -- Aggregates metrics from multiple sources into a weekly markdown dashboard.
# Usage: ./weekly-dashboard.sh
# Output: Markdown dashboard to stdout.
set -euo pipefail
WEEK=$(date +%Y-W%V)
TODAY=$(date +%Y-%m-%d)
CONTENT_ENGINE="https://rb-content-engine.fly.dev"
FEEDBACK_LOG="$HOME/.claude/logs/agents-md-feedback.log"
# ── helpers ──────────────────────────────────────────────────────────────────
section() { printf '\n## %s\n\n' "$1"; }
row() { printf '| %-30s | %-40s |\n' "$1" "$2"; }
note() { printf '> %s\n' "$1"; }
# ── header ───────────────────────────────────────────────────────────────────
cat <<EOF
# Snappy Weekly Dashboard -- $WEEK
Generated: $TODAY
---
EOF
# ── 1. Content Pipeline (content-engine) ─────────────────────────────────────
section "Content Pipeline [snappy-content]"
CONTENT_RAW=$(curl -s --max-time 10 -X POST "$CONTENT_ENGINE/sql" \
-H "Content-Type: application/json" \
-d '{"query": "SELECT type, status, COUNT(*) as count FROM content_atoms WHERE mined_at > now() - interval '\''7 days'\'' GROUP BY type, status ORDER BY type"}' 2>/dev/null || echo "ERROR")
if [ "$CONTENT_RAW" = "ERROR" ] || [ -z "$CONTENT_RAW" ]; then
echo "[NO DATA] Content engine unreachable or returned empty."
else
# Try to parse as JSON array
if echo "$CONTENT_RAW" | jq -e '.' >/dev/null 2>&1; then
ROWS=$(echo "$CONTENT_RAW" | jq -r '.rows // .result // . | if type == "array" then . else [.] end | .[] | "\(.type // "--")\t\(.status // "--")\t\(.count // "--")"' 2>/dev/null || echo "")
if [ -n "$ROWS" ]; then
printf '| %-20s | %-15s | %s |\n' "Type" "Status" "Count"
printf '| %-20s | %-15s | %s |\n' "--------------------" "---------------" "-----"
echo "$ROWS" | while IFS=$'\t' read -r type status count; do
printf '| %-20s | %-15s | %s |\n' "$type" "$status" "$count"
done
else
echo "Response received but could not parse rows."
echo '```'
echo "$CONTENT_RAW" | head -20
echo '```'
fi
else
echo "Non-JSON response from content engine:"
echo '```'
echo "$CONTENT_RAW" | head -20
echo '```'
fi
fi
# ── 2. Git Activity [snappy-github] ─────────────────────────────────────────
section "Git Activity (last 7 days) [snappy-github]"
if command -v gh &>/dev/null; then
REPOS=("snappybusiness/snappy-website" "snappybusiness/content-engine" "snappybusiness/total-crm" "snappybusiness/snappy-xano-mcp")
SINCE=$(date -v-7d +%Y-%m-%dT00:00:00Z 2>/dev/null || date -d '7 days ago' +%Y-%m-%dT00:00:00Z 2>/dev/null || echo "")
if [ -n "$SINCE" ]; then
printf '| %-40s | %s |\n' "Repository" "Commits (7d)"
printf '| %-40s | %s |\n' "----------------------------------------" "------------"
TOTAL_COMMITS=0
for REPO in "${REPOS[@]}"; do
COUNT=$(gh api "repos/$REPO/commits?since=$SINCE&per_page=100" --jq 'if type == "array" then length else empty end' 2>/dev/null || echo "--")
if [[ "$COUNT" =~ ^[0-9]+$ ]]; then
TOTAL_COMMITS=$((TOTAL_COMMITS + COUNT))
fi
printf '| %-40s | %s |\n' "$REPO" "$COUNT"
done
echo ""
echo "**Total commits: $TOTAL_COMMITS**"
else
echo "Could not compute date range."
fi
else
echo "[NO DATA] gh CLI not available. Install with: brew install gh"
fi
# ── 3. Skill Usage / AGENTS.md Feedback ──────────────────────────────────────
section "Skill Health [snappy-analytics]"
if [ -f "$FEEDBACK_LOG" ]; then
TOTAL_ENTRIES=$(wc -l < "$FEEDBACK_LOG" | tr -d ' ')
WEEK_START=$(date -v-7d +%Y-%m-%d 2>/dev/null || date -d '7 days ago' +%Y-%m-%d 2>/dev/null || echo "")
if [ -n "$WEEK_START" ]; then
RECENT=$(awk -v start="$WEEK_START" '$0 ~ /^\[/ { d=substr($1,2,10); if (d >= start) print }' "$FEEDBACK_LOG" | wc -l | tr -d ' ')
else
RECENT="?"
fi
FIXED=$(grep -c 'FIXED' "$FEEDBACK_LOG" 2>/dev/null || echo 0)
LOGGED=$(grep -c 'LOGGED' "$FEEDBACK_LOG" 2>/dev/null || echo 0)
echo "- Total feedback entries: **$TOTAL_ENTRIES**"
echo "- Entries this week: **$RECENT**"
echo "- Self-fixed (P-corrections): **$FIXED**"
echo "- Queued for regen (I-corrections): **$LOGGED**"
# Top skills with feedback
echo ""
echo "**Top skills with feedback gaps:**"
echo ""
awk -F': ' '/^\[/{print $2}' "$FEEDBACK_LOG" | awk -F': ' '{print $1}' | sort | uniq -c | sort -rn | head -5 | while read -r count skill; do
echo "- $skill: $count entries"
done
else
echo "[NO DATA] No feedback log found at $FEEDBACK_LOG"
fi
# ── 4. Skill File Freshness ──────────────────────────────────────────────────
section "Skill File Freshness"
SKILLS_DIR="$HOME/.claude/skills"
if [ -d "$SKILLS_DIR" ]; then
SKILL_COUNT=$(find -L "$SKILLS_DIR" -maxdepth 1 -type d | tail -n +2 | wc -l | tr -d ' ')
AGENTS_COUNT=$(find -L "$SKILLS_DIR" -name "AGENTS.md" | wc -l | tr -d ' ')
echo "- Total skill directories: **$SKILL_COUNT**"
echo "- Skills with AGENTS.md: **$AGENTS_COUNT**"
# Recently modified skills (last 7 days)
echo ""
echo "**Recently modified AGENTS.md (last 7 days):**"
echo ""
find -L "$SKILLS_DIR" -name "AGENTS.md" -mtime -7 -exec stat -f '%Sm %N' -t '%Y-%m-%d' {} \; 2>/dev/null | sort -r | head -10 | while read -r date path; do
skill=$(echo "$path" | sed "s|$SKILLS_DIR/||;s|/AGENTS.md||")
echo "- $skill ($date)"
done
fi
# ── footer ───────────────────────────────────────────────────────────────────
cat <<EOF
---
*Dashboard generated by \`snappy-analytics/weekly-dashboard.sh\` at $(date -u +%FT%TZ)*
*Source tags: [snappy-content] [snappy-github] [snappy-analytics]*
EOF