DocumentationCosts per customerMarkdown
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/v1that belongs to a customer (a visitor's, an operator's or a server API key's) is timed bymeterRequests(server/request-meter.ts); a streamed answer counts until its last byte. Counted per instance and written every 15 seconds astenant_usage.api_requestsandapi_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:
- Fetches the last four days of Vercel charges (recent days are revised) and replaces them in
infra_costs. NeedsSNAKKBAR_VERCEL_TOKEN: a Vercel access token for the team that can read billing. Without it the job skips this step and Kostnader says so. - Measures each customer's data for today.
- Sends the last seven days' report to the cost portal, when
COSTS_PORTAL_URLandCOSTS_PORTAL_TOKENare 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.
{
"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.amountis NOK,amountUsdthe same in USD.providerisvercel,supabase,ai-gateway,domainorother.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.