Skip to content

Documentation

Connect your sources, read the numbers, and see how data is handled.

Updated

Quickstart

Install the Python SDK, set two environment variables, and wrap AI calls with track() to attribute them to a customer and feature. Create the ingestion key under Integrations → SDK & API.

quickstart
pip install spendbook

export SPENDBOOK_API_KEY=afk_your_key        # ingestion key, shown once
export SPENDBOOK_ENDPOINT=https://your-api   # your AI Spendbook API URL

# in your app
from spendbook import track
  • SPENDBOOK_API_KEY: the ingestion key. It starts with afk_ and is shown once.
  • SPENDBOOK_ENDPOINT: the base URL of your AI Spendbook API.
  • Billing data comes from the provider connectors below. The SDK adds who and what each request was for.

Getting started

  1. Create an account and a workspace. Choose your main providers so the app can suggest connectors. To look around first, pick See a sample workspace. Every sample figure is labelled as sample data.
  2. Connect billing: Anthropic and OpenAI admin keys, or the Google Cloud Billing export for Gemini. This gives billed cost and provider-reported usage.
  3. Connect attribution: map the table where your app logs AI calls (Supabase, Postgres or Firestore), import a CSV, or send events with the SDK or ingestion API. This tells AI Spendbook who and what each request was for.
  4. Optionally connect revenue (Stripe or CSV) to see AI cost as a share of each customer's revenue.
  5. Set budgets, explore the dashboards and ask the AI Analyst.

Roles: viewers can read everything. Analysts can also create scenarios and exports and use the AI Analyst. Admins manage connections, keys, budgets, members and settings. Owners can also delete the workspace.

Anthropic & OpenAI

Both connectors use admin API keys, because organization usage and cost reports are only available to admin keys. The keys are used only to read those reports. Regular inference keys are rejected.

Anthropic

  • An organization admin creates an Admin key (it starts with sk-ant-admin) in the Claude Console under organization settings, Admin keys.
  • Endpoints read: the Messages usage report, the cost report, and workspace and API key names for labels.
  • Usage usually appears within minutes. Cost data can be revised for several days, so recent days are fetched again on each sync and corrected.
  • The cost report doesn't include Priority Tier charges. Those days are shown as estimates.

OpenAI

  • An organization owner creates an Admin key in the OpenAI platform under organization settings, Admin keys.
  • Endpoints read: the organization Usage API (completions, embeddings and other usage types) and the Costs API.
  • Costs are reported per day and per project or line item. Usage provides model-level detail.

After you save a key, AI Spendbook validates it, shows the permissions it detected, and runs an initial sync (90 days by default). After that it syncs incrementally. Keys are stored encrypted and never shown again. You can rotate them from the connection page.

Google Gemini

Gemini spend comes from the Cloud Billing export to BigQuery. Turn on the export for the billing account first, then give AI Spendbook a dedicated service account that can only read the export.

  • Roles: roles/bigquery.dataViewer on the billing export dataset, and roles/bigquery.jobUser on the project that runs the queries.
  • Create a JSON key for the service account and upload it in the Google connector. It is stored encrypted and never shown again.
  • Rows read: Cloud Billing export rows for Gemini and Vertex AI services, with SKU, project, usage amounts and billed cost after credits.
  • Export rows can arrive with a delay, so the newest days may show as estimates until the export catches up.

Supabase / Postgres

Create a dedicated read-only role that can only SELECT from the tables that log AI requests. Don't use the postgres user or a service-role key.

restricted role
-- Run in the Supabase SQL editor (or psql) as an admin.
-- 1. A login role that can only read what you grant.
create role spendbook_reader with login password 'use-a-long-random-password';
alter role spendbook_reader set statement_timeout = '30s';
alter role spendbook_reader set default_transaction_read_only = on;

-- 2. Grant SELECT on the specific tables that log AI requests (not the whole schema).
grant usage on schema public to spendbook_reader;
grant select on public.ai_requests to spendbook_reader;
grant select on public.customers to spendbook_reader;   -- optional: for customer attribution

-- Optional: limit to specific columns instead of whole tables.
-- revoke select on public.ai_requests from spendbook_reader;
-- grant select (id, created_at, model, input_tokens, output_tokens, customer_id, feature)
--   on public.ai_requests to spendbook_reader;

-- 3. If Row Level Security is enabled on those tables, allow this role to read them.
create policy spendbook_read on public.ai_requests for select to spendbook_reader using (true);
  • Connect with the host, port, database name and the spendbook_reader credentials. TLS is required.
  • The wizard lists the schemas and tables this role can see. You choose tables and columns. Columns that look sensitive (emails, names, prompts, tokens, addresses) are flagged and left unselected by default.
  • Map columns to canonical fields: timestamp, id, model and token counts are required. customer_id, end_user_id and feature enable attribution. Relationships can pull a field such as customer_id from a joined table.
  • Preview and test the mapping before the first sync. Only the columns you approved are ever read.

Firebase / Firestore

Use a dedicated service account with read-only Firestore access (roles/datastore.viewer). Don't use the default App Engine or Firebase Admin SDK account.

least-privilege service account
# Create a dedicated, read-only service account (Google Cloud CLI).
PROJECT_ID=your-firebase-project
gcloud iam service-accounts create spendbook-reader \
  --project "$PROJECT_ID" --display-name "AI Spendbook (read-only)"

# Cloud Datastore Viewer = read-only access to Firestore documents.
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member "serviceAccount:spendbook-reader@$PROJECT_ID.iam.gserviceaccount.com" \
  --role "roles/datastore.viewer"

# Create a key and upload/paste the JSON in AI Spendbook, then delete the local file.
gcloud iam service-accounts keys create spendbook-reader.json \
  --iam-account "spendbook-reader@$PROJECT_ID.iam.gserviceaccount.com"
  • Upload or paste the JSON key in the Firestore wizard. It is stored encrypted and never shown again.
  • Choose collections, then allowlist the fields to read. Fields that look sensitive start unchecked.
  • Map fields to canonical fields (nested fields use dotted paths like usage.input_tokens), test the mapping, then sync.

Stripe revenue

Create a restricted key with read access to the resources the connector lists. Don't paste a full secret key. Revenue is matched to usage through customer_id, so use the same customer identifiers in both places or import a mapping.

CSV & JSON imports

Upload a file, choose its type, confirm the suggested column mapping, and review validation results. Rejected rows are listed with their row numbers before anything is written. Importing the same rows again doesn't create duplicates, because rows are keyed by event_id or a stable hash.

Provider costs / invoices

Provider cost exports or invoice lines. Stored as billed spend, which is the source of truth for totals.

ColumnRequiredMeaning
dateyesBilling day or period start.
provideryesanthropic, openai, google, …
costyesCharged amount (in the row's currency).
currencyISO code; defaults to USD.
modelIf the line is model-specific.
line_itemInvoice description / SKU.
cost_typetokens, web_search, code_execution, …
project_idProvider project id.
workspace_idProvider workspace id.
end_datePeriod end, if a line spans several days.
event_idStable id; otherwise a hash of the row is used.
provider_costs.csv example

Provider token usage

Token usage aggregates per day, model and project, as exported from a provider console.

ColumnRequiredMeaning
dateyesUsage day.
provideryesanthropic, openai, …
modelyesModel id.
input_tokensUncached input tokens.
output_tokensOutput tokens.
cache_read_tokensPrompt-cache reads.
cache_write_tokensPrompt-cache writes.
request_countNumber of requests.
project_idProvider project id.
service_tierstandard, batch, priority, …
event_idStable id; otherwise a hash of the row is used.
token_usage.csv example

Model usage summary

Requests, tokens and reported cost per model and period.

ColumnRequiredMeaning
dateyesPeriod day.
modelyesModel id.
request_countyesNumber of requests.
providerInferred from the model when missing.
input_tokensInput tokens.
output_tokensOutput tokens.
costReported cost.
currencyISO code; defaults to USD.
project_idProject id.
event_idStable id; otherwise a hash of the row is used.
model_usage.csv example

Request-level records

One row per model call, for example a log export. Enables attribution by customer, feature and user.

ColumnRequiredMeaning
timestampyesWhen the call happened (with a time zone, or set one in options).
modelyesModel id.
providerInferred from the model when missing.
input_tokensUncached input tokens.
output_tokensOutput tokens.
cache_read_tokensPrompt-cache reads.
cache_write_tokensPrompt-cache writes.
customer_idYour customer/account id.
end_user_idIndividual user id.
featureFeature / workflow that made the call.
categoryUsage category.
request_idProvider request id (de-duplication).
costYour recorded cost, if any.
currencyISO code; defaults to USD.
statussuccess / error / HTTP status.
latency_msLatency in milliseconds.
event_idStable id; otherwise a hash of the row is used.
request_data.csv example

Customer mappings

Maps external ids (for example Stripe customer ids) to your customer_id.

ColumnRequiredMeaning
external_idyesId in the other system.
customer_idyesYour customer id used on usage.
customer_nameDisplay name.
sourceSystem the external id comes from.
customer_mappings.csv example

Customer revenue

Monthly revenue per customer, used for AI cost as a share of revenue and margin after AI.

ColumnRequiredMeaning
customer_idyesMust match customer_id on usage.
monthyesYYYY-MM (or a date in the month).
revenueyesRevenue for the month.
currencyISO code; defaults to USD.
revenue.csv example
customer_id,month,revenue_usd,customer_name
acme,2026-10,12000.00,Acme Corp
globex,2026-10,4500.00,Globex

Timestamps need a time zone (ISO-8601 with an offset, or epoch seconds or milliseconds). If yours have none, set “assume time zone” in the mapping options. Money is in US dollars.

Python SDK

Install with pip install spendbook and configure it with SPENDBOOK_API_KEY and SPENDBOOK_ENDPOINT. Wrap AI calls with track() from from spendbook import track to attribute them. It works as a context manager, an async context manager or a decorator, and nested scopes merge.

python
# pip install spendbook
# Configure with environment variables (never hard-code keys):
#   export SPENDBOOK_API_KEY=afk_your_key
#   export SPENDBOOK_ENDPOINT=http://localhost:3000
import anthropic
import spendbook
from spendbook import track

spendbook.init()        # reads SPENDBOOK_API_KEY / SPENDBOOK_ENDPOINT
spendbook.instrument()  # records usage from the anthropic and openai clients
client = anthropic.Anthropic()

with track(customer_id="acme", feature="support-bot", user_id="u_123"):
    client.messages.create(
        model="claude-opus-5-5",
        max_tokens=512,
        messages=[{"role": "user", "content": "Hello"}],
    )
# Token counts, model, latency and attribution are sent. Prompt content is not.
  • Events are queued in memory and sent in background batches. If AI Spendbook can't be reached, your AI calls are never blocked.
  • Content capture is off by default. Set SPENDBOOK_DISABLED=1 to turn the SDK off completely.

Ingestion API

Send events from any language. Use POST http://localhost:3000/api/v1/ingest/events for a single event or /ingest/batch with {"events": [...]} for up to 500, with Authorization: Bearer afk_…. The response reports accepted, duplicate and rejected events.

typescript
// Send one event per AI request from any backend (Node 18+).
const SPENDBOOK_URL = "http://localhost:3000/api/v1/ingest/events";

export async function recordAiUsage(e: {
  requestId: string; model: string; inputTokens: number; outputTokens: number;
  customerId?: string; feature?: string; latencyMs?: number;
}) {
  const res = await fetch(SPENDBOOK_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SPENDBOOK_API_KEY}`, // afk_… ingestion key
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      event_id: e.requestId,
      occurred_at: new Date().toISOString(),
      provider: "anthropic",
      model: e.model,
      input_tokens: e.inputTokens,
      output_tokens: e.outputTokens,
      customer_id: e.customerId,
      feature: e.feature,
      request_id: e.requestId,
      latency_ms: e.latencyMs,
      status: "success",
    }),
  });
  if (!res.ok) console.warn("AI Spendbook ingest failed", res.status);
}
// Batch up to 500 events with POST /api/v1/ingest/batch {"events": [...]}.
curl test
curl -sS -X POST "http://localhost:3000/api/v1/ingest/events" \
  -H "Authorization: Bearer $SPENDBOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "test-'"$(date +%s)"'",
    "occurred_at": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
    "provider": "anthropic",
    "model": "claude-haiku-5-5",
    "input_tokens": 1200,
    "output_tokens": 300,
    "feature": "connection-test"
  }'
FieldTypeDescription
event_id *stringUnique per request; re-sending the same id is a no-op (idempotent).
occurred_at *ISO-8601 timestampWhen the request happened, with a time zone (UTC recommended).
provider *stringanthropic, openai, google, azure_openai, aws_bedrock, …
model *stringModel id as sent to the provider.
input_tokens *integerUncached input tokens.
output_tokens *integerOutput tokens (including reasoning tokens if billed as output).
cache_read_tokensintegerPrompt-cache read tokens.
cache_write_tokensintegerPrompt-cache write tokens.
customer_idstringYour customer or account ID. Powers customer economics.
end_user_idstringIndividual end user (hash it if it's personal data).
featurestringProduct feature that made the call.
categorystringUsage category; otherwise rules / classification apply.
project_idstringProject or environment.
request_idstringProvider request id; used to de-duplicate against provider data.
latency_msintegerEnd-to-end latency.
statusstringsuccess | error | cancelled.
metadataobjectSmall, allow-listed key/values. Never put prompts or secrets here.

The full schema is in the API's OpenAPI document at http://localhost:3000/api/v1/openapi.json.

Reconciliation

AI Spendbook keeps three ledgers and never adds them together:

  • Billed: charges from provider cost reports, billing exports or invoices. When billed data exists for a day, it is the total for that day.
  • Provider usage estimate: token usage reported by the provider, priced at published (or your contract) rates. Used for days the provider hasn't billed yet.
  • Telemetry estimate: request-level events from your app or SDK, priced the same way. Used where there's no provider data.

Request-level data explains the total by customer, feature and user. The part of billed spend that request-level data doesn't cover is shown as unattributed. It isn't spread across customers. The reconciliation report shows, per day and provider, the billed amount, each estimate, and how far they differ.

“Billing complete through” marks the last day for which provider billing is final. Days after it are estimates and can change. If the same request arrives from two sources (for example the SDK and a database mapping), it's matched by provider request id and only counted once.

AI Analyst

  • It answers by calling tools that query your workspace's ledger, and each answer cites the tool calls and data it used.
  • When the data can't answer a question, it says what it couldn't determine instead of guessing.
  • Analysts, admins and owners can use it. It only sees the workspace you are in.

Security & telemetry

  • Credentials are stored in a secret manager and never returned by the API. You only see a masked hint.
  • Every request checks your workspace membership and role on the server. Non-members get “not found”.
  • Connector and settings changes, key creation and member changes are recorded in the audit log (Settings → Audit log).
  • Data policy (Settings → Data & privacy), per workspace: store request metadata (on by default), allow AI classification of usage categories (off by default; sends request metadata to the configured model provider when on), allow content capture (off by default), and retention in days.

What the SDK sends

Model, provider, token counts, latency, status, your attribution fields (customer, user, feature, category, project) and metadata keys you allowlist. Prompt and completion text are sent only if both the SDK option and the workspace content-capture policy are enabled. To turn the SDK off, set SPENDBOOK_DISABLED=1 or revoke the ingestion key.

Questions? Contact support.