# What Snakkbar costs: per customer, and the report for the cost portal

Snakkbar runs on four things that cost money: **Vercel** (the app, its functions and builds),
**Supabase** (database and files), the **AI Gateway** (every model call) and the **domain**. The
system owner sees what each customer costs under **Systemeier → Kunder** (cost and result beside
revenue) and **Systemeier → Kostnader** (a month by service and customer, and the fixed costs). The
same figures go to the cost portal that gathers every product's costs.

## Where each cost comes from

| Cost | Source | Per customer |
| --- | --- | --- |
| AI Gateway | The cost the gateway reports on every call (`providerMetadata.gateway.cost`), metered in `tenant_usage` | **Exact**: the website assistant, Pixel, other calls (translation, portraits) and embeddings |
| Vercel, the project's usage | Vercel's billing API (FOCUS export, `GET /v1/billing/charges`), filtered to the Snakkbar project, fetched nightly into `infra_costs` | Split by each customer's **server time** |
| Vercel, builds | The same, services whose name has «Build» | **Equally** over the customers that exist that day: deploys are no customer's doing |
| Supabase | Fixed costs (plan share, compute), set in Systemeier → Kostnader: Supabase has no invoice API | **Half by server time, half by data** (the customer's rows and files) |
| Domain and other subscriptions | Fixed costs | Equally, unless set otherwise |

Fixed costs have an amount in USD or NOK per month or year, a start and (optionally) an end. A
year counts as 1/365 a day, a month as 12/365. Each says how it is split: `equal`, `usage` (server
time) or `data` (half server time, half data). The migration seeds estimates (Supabase Micro
compute, a quarter of the Pro plan after its compute credit, the domain at Vercel's renewal price, a
quarter of the Vercel Pro seat): adjust them to the invoices.

USD becomes NOK at Norges Bank's latest rate (server/economics.ts, the same rate as the model costs).

### The keys

- **Server time**: every request to `/api/v1` that belongs to a customer (a visitor's, an operator's
  or a server API key's) is timed by `meterRequests` (server/request-meter.ts); a streamed answer
  counts until its last byte. Counted per instance and written every 15 seconds as
  `tenant_usage.api_requests` and `api_ms`. Requests that belong to no customer (the public site,
  the jobs) are not counted, so the project's cost is split over what customers used.
- **Data**: the nightly job measures each customer's rows in every table with a `tenant_id`
  (`pg_column_size`, so embeddings count) and their files in Storage (`tenant_data_bytes`). A day
  without a measurement uses the latest one before it.
- A day with no measured use falls back to equal shares. A day without any customer leaves the cost
  unallocated (customer `null`), so the total always adds up.

The split is in server/cost-allocation.ts, pure and unit-tested (server/test/cost-allocation.test.ts).

## The nightly job

`GET /api/v1/internal/costs` (Vercel Cron, 04:17 UTC, `CRON_SECRET`), also **Hent nå** in
Systemeier → Kostnader and `POST /api/v1/platform/costs/sync` with a platform key:

1. Fetches the last four days of Vercel charges (recent days are revised) and replaces them in
   `infra_costs`. Needs `SNAKKBAR_VERCEL_TOKEN`: a Vercel access token for the team that can read
   billing. Without it the job skips this step and Kostnader says so.
2. Measures each customer's data for today.
3. Sends the last seven days' report to the cost portal, when `COSTS_PORTAL_URL` and
   `COSTS_PORTAL_TOKEN` are set (`POST`, `Authorization: Bearer <token>`, the report as the body).

| Variable | |
| --- | --- |
| `SNAKKBAR_VERCEL_TOKEN` | Vercel token with access to the team's billing |
| `SNAKKBAR_VERCEL_TEAM` | Team id (default the Sevenfold team) |
| `SNAKKBAR_VERCEL_PROJECT` | Project id when `VERCEL_PROJECT_ID` is not set (default the Snakkbar project) |
| `COSTS_PORTAL_URL`, `COSTS_PORTAL_TOKEN` | Where the nightly report goes |

## The report: `sevenfold-costs/v1`

`GET /api/v1/platform/costs?from=2026-11-01&to=2026-11-30` with a platform key
(`Authorization: Bearer sk_platform_…`, made under Systemeier → Tilbakemeldinger) or as a signed-in
system owner. Default: the last 30 days; at most 399 days. The nightly push sends the same body.

```json
{
  "schema": "sevenfold-costs/v1",
  "product": { "id": "snakkbar", "name": "Snakkbar" },
  "from": "2026-11-01",
  "to": "2026-11-30",
  "generatedAt": "2026-12-01T04:17:12.000Z",
  "currency": "NOK",
  "exchangeRate": { "usdNok": 10.12, "date": "2026-11-30", "source": "norges-bank" },
  "lines": [
    { "date": "2026-11-01", "customer": { "id": "amatec", "name": "Amatec" }, "provider": "ai-gateway", "service": "Website assistant", "basis": "direct", "amount": 1.4231, "amountUsd": 0.140623 },
    { "date": "2026-11-01", "customer": { "id": "amatec", "name": "Amatec" }, "provider": "vercel", "service": "Fluid Provisioned Memory", "basis": "usage", "amount": 0.6012, "amountUsd": 0.059407 },
    { "date": "2026-11-01", "customer": { "id": "amatec", "name": "Amatec" }, "provider": "supabase", "service": "Supabase compute (Micro)", "basis": "data", "amount": 1.6633, "amountUsd": 0.164358 },
    { "date": "2026-11-01", "customer": { "id": "amatec", "name": "Amatec" }, "provider": "domain", "service": "Domene snakkbar.app", "basis": "equal", "amount": 0.2079, "amountUsd": 0.020548 }
  ],
  "revenue": [{ "date": "2026-11-01", "customer": { "id": "amatec", "name": "Amatec" }, "amount": 32.26 }],
  "sources": {
    "vercel": { "configured": true, "project": "prj_…", "fetchedThrough": "2026-11-30" },
    "fixedCosts": [{ "name": "Domene snakkbar.app", "provider": "domain", "amount": 15, "currency": "USD", "per": "year", "split": "equal", "startsOn": "2026-10-12", "endsOn": null }]
  }
}
```

- `lines`: one per day, customer, provider and service. `amount` is NOK, `amountUsd` the same in
  USD. `provider` is `vercel`, `supabase`, `ai-gateway`, `domain` or `other`.
- `basis`: `direct` (metered for that customer), `usage`, `data`, `equal` (how a shared cost was
  split), `unallocated` (no customer that day).
- `revenue`: the subscription price accrued per day (NOK), per customer.
- The portal should treat a report as the truth for its product and dates: a later report for the
  same days replaces the earlier lines (Vercel revises recent days).

Other products can send the same format with their own `product.id`; the portal adds what no
single product sees on its own, such as the Vercel team's Pro seat and the Supabase organisation's
plan, and splits them between products.
