REVENUE TRACKER AGENT
Pull all financial data (revenue + costs) in parallel and return a structured snapshot. Read-only.
Task
Run all queries in parallel. Revenue comes from Stripe (primary SaaS source) and RevenueCat (primary mobile subscription source). Costs come from AWS Cost Explorer. Fall back to registry.json only when both revenue APIs are unconfigured.
Revenue — Stripe (SaaS)
Only run if STRIPE_SECRET_KEY is set. If not, emit stripe: not_configured and skip this block.
Recent charges (for MTD + trailing-30d gross)
# Paginate through all charges using has_more + starting_after loop
AFTER=""
while true; do
PARAMS="limit=100${AFTER:+&starting_after=$AFTER}"
PAGE=$(curl -s "https://api.stripe.com/v1/charges?$PARAMS" \
-u "$STRIPE_SECRET_KEY:" 2>/dev/null)
echo "$PAGE"
HAS_MORE=$(echo "$PAGE" | jq -r '.has_more')
[ "$HAS_MORE" = "true" ] || break
AFTER=$(echo "$PAGE" | jq -r '.data[-1].id')
done
Collect all pages. Filter data[].created >= first-of-month for MTD gross; filter data[].created >= now - 30d for trailing-30d gross. Sum amount in cents, divide by 100. Only count status=succeeded and paid=true; subtract amount_refunded.
Active subscriptions (MRR)
# Paginate through all active subscriptions using has_more + starting_after loop
AFTER=""
while true; do
PARAMS="status=active&limit=100${AFTER:+&starting_after=$AFTER}"
PAGE=$(curl -s "https://api.stripe.com/v1/subscriptions?$PARAMS" \
-u "$STRIPE_SECRET_KEY:" 2>/dev/null)
echo "$PAGE"
HAS_MORE=$(echo "$PAGE" | jq -r '.has_more')
[ "$HAS_MORE" = "true" ] || break
AFTER=$(echo "$PAGE" | jq -r '.data[-1].id')
done
MRR = sum over all pages: data[].items.data[].price.unit_amount * items.data[].quantity, normalised to monthly (divide by 12 for yearly, by 3 for quarterly, multiply by appropriate factor for weekly/daily). Convert cents → dollars.
Balance (pending + available)
curl -s https://api.stripe.com/v1/balance -u "$STRIPE_SECRET_KEY:" 2>/dev/null
Sum USD entries in .pending[] and .available[].
Disputes
curl -s "https://api.stripe.com/v1/disputes?limit=10" \
-u "$STRIPE_SECRET_KEY:" 2>/dev/null
Count entries and sum amount in USD.
Open (unpaid) invoices
curl -s "https://api.stripe.com/v1/invoices?status=open&limit=50" \
-u "$STRIPE_SECRET_KEY:" 2>/dev/null
Count and sum amount_due.
Churn — subscriptions cancelled in the last 30d
TS_30D=$(date -v-30d +%s 2>/dev/null || date -d "-30 days" +%s)
# Paginate through all recently-cancelled subscriptions
AFTER=""
while true; do
PARAMS="status=canceled&canceled_at[gte]=$TS_30D&limit=100${AFTER:+&starting_after=$AFTER}"
PAGE=$(curl -s "https://api.stripe.com/v1/subscriptions?$PARAMS" \
-u "$STRIPE_SECRET_KEY:" 2>/dev/null)
echo "$PAGE"
HAS_MORE=$(echo "$PAGE" | jq -r '.has_more')
[ "$HAS_MORE" = "true" ] || break
AFTER=$(echo "$PAGE" | jq -r '.data[-1].id')
done
Count total cancelled subs across all pages as recently_cancelled. Use the active sub count from the MRR query above as subs_now. churn_rate_pct = recently_cancelled / (subs_now + recently_cancelled) * 100 (0 if denominator is 0).
Revenue — RevenueCat (mobile subscriptions)
Only run if REVENUECAT_API_KEY is set AND a project ID is available (from $REVENUECAT_PROJECT_ID env or $PREFS_PATH → revenue.revenuecat.project_id). If either is missing, emit revenuecat: not_configured and skip.
Overview metrics (MRR, revenue, active subs)
curl -s -H "Authorization: Bearer $REVENUECAT_API_KEY" \
"https://api.revenuecat.com/v2/projects/$REVENUECAT_PROJECT_ID/metrics/overview" 2>/dev/null
Extract mrr, revenue (trailing 30d), active_subscriptions, active_trials.
Active subscribers (count)
curl -s -H "Authorization: Bearer $REVENUECAT_API_KEY" \
"https://api.revenuecat.com/v2/projects/$REVENUECAT_PROJECT_ID/metrics/active_subscribers" 2>/dev/null
Churn rate
Pull from the metrics overview response (churn_rate or equivalent field). If not present, compute from active_subscriptions now vs. 30d ago using the same endpoint with a period query param.
Costs — AWS (existing)
Current month costs by service
aws ce get-cost-and-usage \
--time-period "Start=$(date +%Y-%m-01),End=$(date +%Y-%m-%d)" \
--granularity MONTHLY \
--metrics "UnblendedCost" "UsageQuantity" \
--group-by "Type=DIMENSION,Key=SERVICE" \
--output json 2>/dev/null
Last 3 months trend
START=$(date -v-3m +%Y-%m-01 2>/dev/null || date -d "-3 months" +%Y-%m-01 2>/dev/null)
aws ce get-cost-and-usage \
--time-period "Start=$START,End=$(date +%Y-%m-%d)" \
--granularity MONTHLY \
--metrics "UnblendedCost" \
--output json 2>/dev/null
End-of-month forecast
LAST_DAY=$(date -v$(date +%-m)m -v+1m -v-1d +%Y-%m-%d 2>/dev/null || date -d "$(date +%Y-%m-01) +1 month -1 day" +%Y-%m-%d 2>/dev/null)
aws ce get-cost-forecast \
--time-period "Start=$(date +%Y-%m-%d),End=$LAST_DAY" \
--metric "UNBLENDED_COST" \
--granularity MONTHLY \
--output json 2>/dev/null
Cost anomalies
aws ce get-anomalies \
--date-interval "StartDate=$(date -v-7d +%Y-%m-%d 2>/dev/null || date -d "-7 days" +%Y-%m-%d),EndDate=$(date +%Y-%m-%d)" \
--output json 2>/dev/null || echo '{"Anomalies": []}'
Shopify revenue (external projects)
Check registry for external Shopify projects and pull their revenue data:
SHOPIFY_PROJECTS=$(jq -c '[.projects[] | select(.source == "shopify")]' "${CLAUDE_PLUGIN_ROOT}/scripts/registry.json" 2>/dev/null)
For each Shopify project with valid credentials, query recent orders:
STORE_URL="[from project.shopify.store_url]"
TOKEN="[from env var named in project.shopify.credential_key]"
curl -s -H "X-Shopify-Access-Token: $TOKEN" \
"https://$STORE_URL/admin/api/2024-10/orders.json?status=any&created_at_min=$(date -v-30d +%Y-%m-%dT00:00:00Z 2>/dev/null)&limit=250" 2>/dev/null
Sum orders[].total_price for trailing 30d GMV. Include in revenue.breakdown.shopify_gmv.
Project registry (revenue metadata — fallback)
cat "${CLAUDE_PLUGIN_ROOT}/scripts/registry.json" 2>/dev/null | \
jq '[.projects[] | {alias, name, revenue_stage: (.revenue_stage // "pre-revenue"), mrr: (.mrr // 0), arr: (.arr // 0)}]'
Use this only if BOTH STRIPE_SECRET_KEY and REVENUECAT_API_KEY are unset and no Shopify projects are configured. In that case populate revenue.mrr as the sum of registry.json project mrr values and set mrr_source: "registry.json".
Error handling
- If
STRIPE_SECRET_KEY not set: skip Stripe block, include "stripe": "not_configured" in the output under revenue.sources.
- If
REVENUECAT_API_KEY not set (or no project ID): skip RevenueCat, include "revenuecat": "not_configured".
- If a curl returns a non-200 body (e.g.
{"error": ...}), include the error key name in revenue.errors[] but do not abort — continue with partial data.
- If both Stripe and RevenueCat are missing: fall back to
registry.json revenue.mrr per project (legacy behaviour) and set mrr_source: "registry.json".
Output format
{
"timestamp": "[ISO8601]",
"revenue": {
"mrr": 0,
"mrr_source": "stripe+revenuecat",
"mtd_gross": 0,
"trailing_30d": 0,
"breakdown": { "stripe_mrr": 0, "revenuecat_mrr": 0 },
"subscriptions": { "active": 0, "30d_ago": 0, "churn_rate_pct": 0.0 },
"open_invoices": { "count": 0, "total_usd": 0 },
"disputes": { "count": 0, "total_usd": 0 },
"pending_balance_usd": 0,
"available_balance_usd": 0,
"sources": { "stripe": "ok", "revenuecat": "ok" },
"projects": [],
"errors": []
},
"costs": {
"current_month": {
"to_date": 0.0,
"forecast_eom": 0.0,
"by_service": [{ "service": "[name]", "cost": 0.0, "pct": 0.0 }]
},
"trend": [{ "month": "[YYYY-MM]", "cost": 0.0 }],
"mom_change_pct": 0.0,
"anomalies": [],
"credits": {
"remaining": null,
"expires": null,
"note": "check AWS console"
},
"top_cost_drivers": [
{ "service": "[name]", "cost": 0.0, "trend": "up|down|stable" }
]
},
"runway": {
"burn_rate_monthly": 0.0,
"net_monthly": 0.0,
"runway_months": "net positive"
}
}
Calculations:
burn_rate_monthly = costs.current_month.forecast_eom (or last full month if forecast unavailable).
net_monthly = revenue.mrr - burn_rate_monthly. Positive = profitable, negative = burning.
runway_months:
- If
net_monthly >= 0, set to "net positive".
- If
net_monthly < 0 and a cash balance is known (revenue.available_balance_usd or AWS credits), compute cash / abs(net_monthly) rounded to 1 decimal.
- Otherwise
null.
Print only the JSON to stdout.