Finance reporting
Four primitives — product, subscription, invoice, expense — plus four read-only finance views give you MRR, ARR, ROAS, CAC, and blended-CAC over any window. Everything is agent-recorded and lives in one local SQLite database.
Primitives
product— what you sell. Captured once, referenced by subscriptions and invoices.subscription— recurring revenue, optionally tied to a counterparty. StoresmrrCentsdenormalized soSUM(mrrCents) WHERE status IN ('active','trialing')is O(1).invoice— a single bill. Tied to asubscription(recurring bill) or standalone, with an optional free-textcounterparty.expense— money out. Fields:amountCents,category(ads|infra|contractor|software|tax|fees|salary|...),vendor,source(manual|stripe|bank|google_ads|meta_ads|...),sourceRef(import dedup key).
Summary · MRR · ARR
krabs finance summary --from 2026-05-01T00:00:00Z --to 2026-05-31T23:59:59Z krabs finance mrr krabs finance expenses-by-category --from 2026-05-01T00:00:00Z --to 2026-05-31T23:59:59Z
Same three calls over HTTP: GET /v1/finance/summary, GET /v1/finance/mrr, GET /v1/finance/expenses-by-category. Over MCP: finance_summary, finance_mrr, finance_expenses_by_category.
Funnel · ROAS · CAC
finance.funnel aggregates revenue and ad spend over a window and computes:
- ROAS = paid revenue ÷ ad spend (return on ad spend). Null when ad spend is zero.
- CAC = ad spend ÷ new customers (customer acquisition cost from paid). Null when nothing converted.
- Blended-CAC = total expenses ÷ new customers. Includes infra, contractor, software, etc. — the all-in cost per customer.
$ krabs finance funnel --from 2026-05-01T00:00:00Z --to 2026-05-31T23:59:59Z
{
"period": { "from": "2026-05-01T00:00:00Z", "to": "2026-05-31T23:59:59Z" },
"revenue": { "paid_cents": 4821000, "currency": "USD" },
"ad_spend": {
"total_cents": 850000, "currency": "USD",
"by_source": [
{ "source": "meta_ads", "total_cents": 620000 },
{ "source": "google_ads", "total_cents": 230000 }
]
},
"new_customers": 14,
"roas": 5.67,
"cac_cents": 60714,
"blended_cac_cents": 89821
}counterparty whose first paid invoice or active subscription falls inside the window. If nothing converted in the window, CAC stays null.Ingesting ad spend
Ad spend lives in the expense table. Two fields decide where it shows up in the funnel breakdown:
categorymust be"ads".sourceselects the platform:meta_ads,google_ads,tiktok_ads, etc. Usemanualfor one-off entries.
krabs expense create \ --amount-cents 4500 \ --currency USD \ --category ads \ --source meta_ads \ --source-ref "act_1234567:2026-05-16" \ --vendor "Meta Ads · Campaign X" \ --occurred-at 2026-05-16T00:00:00Z
sourceRef is the import dedup key. Re-running the ingestion on the same day with the same key returns the existing row instead of inserting a duplicate. That makes the daily-cron pattern safe.
Meta Ads CLI
Meta shipped an official Ads CLI on 2026-04-29, designed for AI agents — predictable commands, JSON output, defined exit codes. It ships as a Python package (Python 3.12+). krabs does not bundle a Meta-specific adapter; instead the agent (or a small daily cron) calls Meta's CLI, pipes the insights through jq, and records each line as a krabs expense.
# 0. Install Meta's official Ads CLI (one-time) pip install meta-ads-cli meta --version # 1. Pull last 7 days of spend per campaign as JSON meta ads insights get \ --date-preset last_7d \ --fields spend,impressions,campaign_id,date_start,date_stop \ --format json > /tmp/meta.json
# 2. For each row, record it in krabs (Bash loop, or have your agent do it)
jq -c '.[]' /tmp/meta.json | while read -r row; do
# Meta returns spend as a decimal-string in account currency — convert to cents
spend_cents=$(echo "$row" | jq -r '(.spend | tonumber * 100 | floor)')
campaign=$(echo "$row" | jq -r '.campaign_id')
date=$(echo "$row" | jq -r '.date_start')
krabs expense create \
--amount-cents "$spend_cents" \
--currency USD \
--category ads \
--source meta_ads \
--source-ref "${campaign}:${date}" \
--vendor "Meta Ads · ${campaign}" \
--occurred-at "${date}T00:00:00Z"
donesourceRef = campaignId:date is the dedup key. Re-running the same window is safe — krabs returns the existing row on collision instead of inserting a duplicate.
Same recipe for Google (google-ads-python), TikTok, LinkedIn — change --source to google_ads | tiktok_ads | linkedin_ads. As long as the row lands with category="ads" and a non-manual source, the funnel breakdown lights up automatically.
Open source, self-hosted
All finance endpoints, MCP tools, and CLI commands are part of the open-source core. You run the single account on your own box — same primitives over MCP, HTTP, and CLI, same agent skill. There is no cloud version and no third-party billing integration.