Finance · ROAS · CAC
Four primitives — product, subscription, invoice, expense — plus four read-only finance views give you MRR, ARR, ROAS, CAC, and blended-CAC over any window. Stripe-mirrored data and agent-recorded data live in the same tables; you query them the same way.
Primitives
product— what you sell. Captured once, referenced by subscriptions and invoices.subscription— recurring revenue from a contact. StoresmrrCentsdenormalized soSUM(mrrCents) WHERE status IN ('active','trialing')is O(1).invoice— a single bill. Tied to asubscription(recurring bill), adeal(one-shot project bill), or standalone.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).
Business profile
Krabs ships agent-first — on first connect the agent runs a short kickoff to learn how you make money, then calls businessProfile.set. That single record shapes which primitives the agent will default to and how reporting is framed.
krabs account business-profile set \ --revenue-model hybrid \ --cadence monthly \ --currency USD \ --channels meta_ads,google_ads,referral \ --typical-contract-cents 20000
Revenue models: recurring_saas · one_time ·hybrid · freelance · marketplace ·other. Cadences: weekly · monthly · quarterly · yearly · per_project · mixed.
Read it back with krabs account business-profile get or GET /v1/account/business-profile. The kickoff runs once; re-call when the business changes.
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 no contacts 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
}contact whose status moved to customer and whose updatedAt falls inside the window. The agent or your Stripe webhook handler is responsible for moving contacts to customer on first paid invoice. If you skip that step, 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.
Self-host vs cloud — parity
All finance endpoints, MCP tools, and CLI commands are part of the open-source core. Self-host users get the same primitives, the same /v1/finance/funnelendpoint, and the same agent skill. The cloud version adds nothing here — it's the same code path.
Stripe and Resend integrations are also part of the core. Connect them at /dashboard/settings/integrations/<provider> in either deployment.