docs / finance

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. Stores mrrCents denormalized so SUM(mrrCents) WHERE status IN ('active','trialing') is O(1).
  • invoice — a single bill. Tied to a subscription (recurring bill), a deal (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
}
how ‘new customers’ is computed
A new customer is a 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:

  • category must be "ads".
  • source selects the platform: meta_ads, google_ads, tiktok_ads, etc. Use manual for 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"
done

sourceRef = 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.

Edit this page on GitHub →last updated 2026-05-17 · v0.5.0