Skip to content
1,153 linesCodeBlameRaw
1# Billing operations: costs, margin and prices
2
3How g1t checks what it charges against what Cloudflare charges it, keeps
4prices at cost plus 20%, and tells staff when the margin slips. Internal.
5Code: `services/billing/src/costs.rs` (reading the bill), `margin.rs`
6(reconciliation, drift, alerts), `pricing.rs` (versions, proposals,
7notice), `keeper.rs` (sandbox and Workers for Platforms measurements),
8`budget.rs` (what g1t pays for itself, and its caps; see
9[Spend caps](#spend-caps)).
10Page: sudo **Costs & margin** (`/costs`).
11
12Several Cloudflare products g1t runs on are new. Artifacts bills
13"operations" from 2026-10-14 without defining them (see
14[ARTIFACTS.md](ARTIFACTS.md), M1). So nothing here hard-codes a product
15list or a unit: every line Cloudflare bills is kept, and how a line maps
16to what g1t sells is data you change from sudo, without a deploy.
17
18## Data sources
19
20| Source | What | Where it lands |
21| --- | --- | --- |
22| Billable usage, `GET /accounts/{account}/billable-usage?from=&to=` | One row per service per day in FOCUS columns: `ServiceFamilyName`, `ServiceName`, `ChargePeriodStart`, `PricingQuantity`, `ContractedCost` / `BilledCost` / `ListCost`. Every product g1t uses appears once it is used: Workers, Workers for Platforms, D1, KV, R2, Queues, Containers, Durable Objects, Artifacts, Browser Rendering, Workers AI, Vectorize, Cloudflare for SaaS, Email. Inside an included amount the cost is 0. | `cost_lines`, source `billable_usage` |
23| GraphQL `artifactsEventsAdaptiveGroups` | Artifacts' own count by `date`, `eventType` and `repositoryName`. Operations are `create`, `fork`, `push`, `pull`, `delete`; errors (`rateLimited`, `serverError`, …) are kept but not counted. | `cost_lines`, source `artifacts_events`; per workspace (from the store key `<workspace>--<repo>`; a pull request's working copy, `pulls--<id>`, is its repository's workspace's, from repos' `pull_owners`) in `own_counts` as `cloudflare_git` |
24| GraphQL `aiGatewayRequestsAdaptiveGroups`, filtered to `AI_GATEWAY_ID` | What AI Gateway priced g1t's own provider traffic at, by `date`, `provider` and `model`: `count`, `sum.cost` (dollars), `sum.tokensIn`/`tokensOut`/`cacheReadTokens`/`cacheWriteTokens`; asked twice in one query, filtered `wholesale: 0` and `wholesale: 1`. `wholesale` is never a dimension: grouped by it, Cloudflare answers no rows and no error (until 2026-10-08 that left the gateway's side empty while it had logged $11.11). Read over its own window: the last 31 days until it has answered with a line, then the last few. Field names checked against Cloudflare's schema (introspection of `AccountAiGatewayRequestsAdaptiveGroups{Sum,Dimensions,Filter_InputObject}`). An adaptive (sampled) dataset: an estimate, close at g1t's volumes. Only g1t's hosted models go through this gateway: a workspace's own provider is called at its own address, never here. | `cost_lines`, source `ai_gateway`, product `ai_gateway_requests`: per day and model a line `<provider>_<model>` (requests, at the gateway's cost), and at no cost `…__tokens`, `…__cache_read_tokens`, `…__cache_write_tokens`; Cloudflare-billed (unified billing) requests are prefixed `wholesale__`. Mapped to `models` (migration 0036). A re-read day replaces all its gateway lines. |
25| The ledger | Every charge: its cost at the price book's cost, what it was charged at price, what paid for it. | read, never written |
26| `pending_usage` | Month-end meters (git, storage, scans, embeddings, the cache) as they stand. | snapshotted daily into `pending_days` |
27| `plan_payments` | The plan's $20. | read |
28| repos `git_operations` | Operations customers are charged for, per workspace, counted by repos through its `operation_mapping`. | `own_counts` meter `git_operations` |
29| Subscriptions, `GET /accounts/{account}/subscriptions` | What g1t pays each month whatever it uses (Workers Paid, add-ons): each subscription that is paid, trialing or awaiting payment, at its price over its frequency. Not on the billable-usage bill. Read in the daily run with the bill's token; a failure is logged and the last read stays. | `cf_subscriptions` (one row) |
30| repos `artifacts_usage` | Every raw meter of the git store (`git.fetch`, `git.receive_pack`, `binding.*`, …) per day and workspace, with repos' `operation_mapping`. | `own_counts` meters `artifacts_<raw meter>`, and `cost_operations` (raw counts × the mapping's `cost_operations`: what g1t expects Cloudflare to bill) |
31
32A meter's slug is Cloudflare's name lower-cased with words joined by `_`
33and the "(First … included)" note dropped: `Workers for Platforms CPU ms
34(First 60M ms are included)` under `Workers` is product `workers`, meter
35`workers_for_platforms_cpu_ms`. Several rows of the same day and meter
36(regions, tiers) are added together before they are stored.
37
38## Credentials
39
40| Secret on g1t-billing | Permissions | Used for |
41| --- | --- | --- |
42| `CLOUDFLARE_BILLING_TOKEN` (optional) | Account: **Billing Read**, Account: **Account Analytics Read**, for the g1t account only | Reading the bill, the Artifacts events and the subscriptions |
43| `CLOUDFLARE_USAGE_TOKEN` (exists) | Billing Read, Account Analytics Read, AI Gateway Read | The keeper (settling runs from the gateway's logs); also the bill when `CLOUDFLARE_BILLING_TOKEN` is not set. AI Gateway's analytics are read with this token first and, if that is refused, with the bill's (a token without Account Analytics Read gets an error). When the answer has no rows, billing asks the REST API for the gateway with the same token: 403 means the token lacks AI Gateway Read, 404 that `AI_GATEWAY_ID` names no gateway, 200 that nothing went through it; the `models` drift says which |
44
45With neither, the daily run reconciles only what g1t counted itself, and
46the page says the bill cannot be read. Nothing fails. To set the scoped one:
47
481. Cloudflare dashboard → My Profile → API Tokens → Create Token → Custom token.
492. Permissions: Account · Billing · Read; Account · Account Analytics · Read.
503. Account resources: Include · the g1t account. No zone permissions.
514. `cd services/billing && npx wrangler secret put CLOUDFLARE_BILLING_TOKEN`.
525. In sudo, Costs & margin → **Run the analysis now**.
53
54Alerts are emailed through the `EMAIL` binding (Cloudflare Email Sending)
55to `COSTS_ALERT_EMAIL` (`hey@flagon.io`). An empty value sends none.
56
57## Schedule
58
59The daily cron (`17 4 * * *`, `keeper::DAILY`) runs, in order:
60
611. The keeper's measurements (sandbox seconds, app requests and CPU), each
62 a proposal now, not a direct change.
632. `costs_daily`:
64 1. Read the bill, the Artifacts events and AI Gateway's analytics. The first run reads the last
65 31 days (GraphQL keeps 31); later runs the last 4, since Cloudflare
66 restates recent days, or back to the last day read after a gap.
67 Lines are upserted on `(day, source, product, meter)`, so a re-read
68 replaces, never adds.
69 2. g1t's own counts for the same days (replaced per day).
70 3. Snapshot `pending_usage` into `pending_days`.
71 4. Reconcile the last 31 days (further back after a gap) into
72 `margin_days` and `workspace_costs`
73 (replaced per day).
74 5. Drift over the last 7 days into `cost_drift`.
75 6. Unit costs over the last 30 days, proposed to the price book.
76 7. Apply price versions whose date has come.
77 8. Open, update and close margin alerts; email new ones.
78 9. Email owners on the plan about rises to come.
79
80**Run the analysis now** at the top of sudo's Costs & margin and Bill & pricing pages
81(`admin_run_costs`) runs all of step 2 at once, alerts included, with no
82need to wait for 04:17 UTC. Running it twice is safe: every step replaces
83what it wrote.
84
85## Reconciliation math
86
87Every Cloudflare line goes to one of g1t's products ("buckets") by
88`cost_map`: the row for its product with the longest matching meter
89prefix, `*` last. A line no row claims goes to `unmapped`.
90
91| Bucket | Cloudflare | Paid for by (`revenue_map`) |
92| --- | --- | --- |
93| `sandboxes` | Containers, Durable Objects compute duration | `sandbox`, `self_hosted`, `builds` |
94| `deployments` | Workers for Platforms | `deployments` |
95| `git` | Artifacts operations (and its events, as counts) | `git` |
96| `repo_storage` | Artifacts storage | `storage` |
97| `actions_cache` | R2 | `cache` |
98| `embeddings` | Workers AI, Vectorize | `context` |
99| `security` | (Workers CPU, under `platform`) | `security` |
100| `domains` | Cloudflare for SaaS | `domains` |
101| `models` | not Cloudflare: AI Gateway's settled cost on the ledger; AI Gateway's own daily total (`ai_gateway_requests`) beside it, to check it | every other task (agent runs). A planning run's ledger task is `plan`, the same as the plan's payments' key, so it is reconciled as `planning` (`margin::PLANNING_KEY`), which has no `revenue_map` row: models. Before 2026-10-08 its model cost landed in `platform`, where Cloudflare's bill is the cost, and was lost |
102| `platform` | Workers, D1, KV, Queues, Email, Browser Rendering, other Durable Objects | the plan's price |
103
104For each day and bucket:
105
106- **Cloudflare cost** = Σ the bucket's lines' cost, as billed: after the
107 included allowances, so a month inside them costs $0 here as on
108 Cloudflare's Billable usage page. `models` uses the ledger's cost of the
109 tokens instead; that is paid to the model providers and is not on
110 Cloudflare's bill. Its "Cloudflare" column is what AI Gateway priced the
111 same traffic at, which drift compares with the ledger (below); it is
112 never added to the cost.
113- **Own cost** = Σ the ledger's `cost_micros` for the bucket's keys (the
114 price book's cost when charged), plus month-end deltas. A workspace's own
115 model provider is no cost to g1t.
116- **Value** = what customers were charged at price: `-amount_micros` plus
117 what the plan's included usage, a trial, the open-source pool or g1t paid.
118 g1t's own (comped) workspaces are valued at cost plus the margin.
119- **Cash** = what workspaces paid: `-amount_micros`, and the plan's price.
120 Never tax or card fees: a payment credits the balance, and
121 `plan_payments`, without them (see [Tax and the card fee](#tax-and-the-card-fee)).
122- **Given away** = the part of the cost that went on usage g1t paid for
123 itself on purpose, by why:
124 - **comped**: all of a comped workspace's cost, every bucket;
125 - **free use**: a free period's usage, the overruns g1t covered
126 (`ledger.given_micros`), and all of a workspace's cost on a day it had
127 nothing priced (free allowances);
128 - **trial** and **open-source pool**: what `trial_micros` and
129 `oss_micros` paid;
130 - **discount**: what a discount on an account's custom terms took below
131 cost plus the margin (`ledger.discount_micros`, see
132 [Margin floor](#margin-floor)). The usage is valued at its price, so a
133 discounted sale never reads as margin lost;
134 - **promotional credit** and **goodwill credit**: what credit staff gave
135 paid for, when it is spent (`given_credit_promotional_micros`,
136 `given_credit_goodwill_micros`, migration 0038). That usage's charge is
137 taken out of cash, so it is never money in. A refund is not here: see
138 [Credits from g1t](#credits-from-g1t);
139 - **testing resets**: what a workspace's usage cost g1t before staff
140 reset its billing (`reset_costs`, `given_reset_micros`, migration
141 0046). The model calls and Cloudflare usage still happened, so the
142 reconciliation reads the kept rows back as that workspace's usage on
143 their days: valued as before, no cash, all of it given. See
144 [Resetting a test workspace](#resetting-a-test-workspace).
145
146 Otherwise a workspace's day is split by those shares of its value at
147 price, and the same shares of each of its buckets' cost are given, its
148 part of running g1t included. The Team plan's included usage is sold:
149 the plan's price paid for it. Stored on `margin_days` (`given_micros`
150 and `given_<why>_micros`, `given_discount_micros` from migration 0036)
151 and `workspace_costs` (`given_micros`). Sudo's Costs & margin page lists
152 each why by name, testing resets included (`givenResetMicros`).
153- **Month-end meters**: a day's figure is that day's `pending_days`
154 snapshot less the day before's, within a month. Their month-end ledger
155 entries are left out, so nothing is counted twice. On a 100%-discount
156 workspace the snapshot's charge is valued and given (comped), never cash:
157 the month's close takes all of it off (`margin::comped_meter`).
158- **Product margin** = (value − cost) / value: a share of the price, so
159 cost plus 20% is a 16.7% margin. Sudo labels every margin "of price".
160- **Sudo's statement** keeps apart:
161 - **Usage sold**: cash for usage against the cost of the usage buckets
162 less what was given. Its margin is the headline; at cost plus 20% it
163 sits near 16.7%.
164 - **Running g1t**: the plan's price against `platform` less its given
165 share.
166 - **Cloudflare subscriptions**: what Cloudflare lists, a month, over
167 the range (`cf_subscriptions`); until a read has worked,
168 `CLOUDFLARE_FIXED_MONTHLY_MICROS`, an estimate.
169 - **Not mapped**: billed, charged for by nothing.
170 - **Given away**: by why. A budget, watched under g1t's own spend, never
171 shown as a loss.
172 - **All in**: money in against all of it, with the figure without what
173 was given beside it. **Who g1t paid** is All in's cost, split into
174 Cloudflare's usage, Cloudflare's subscriptions over the range, and the
175 model providers.
176
177 The overall alert is (Σ cash − (Σ cost − Σ given)) / Σ cash.
178- **Quantities**: where a mapping names an `own_meter`, Cloudflare's
179 billed quantity of those lines (or, without one, Artifacts' operation
180 events) against g1t's own count.
181
182**Shared costs to workspaces.** A bucket's cost is shared in proportion
183to, first available: Cloudflare's own per-workspace count
184(`cloudflare_<bucket>`, today the Artifacts events by repository), g1t's
185own count, what its usage cost (so free use carries its own cost), what
186each was charged for it. `platform`
187and `unmapped` are shared by each workspace's share of all usage that
188day. Shares are whole micros that add up to the bill exactly (largest
189remainder).
190
191## Drift (last 7 days)
192
193| Kind | When | What to do |
194| --- | --- | --- |
195| Count | g1t's count and Cloudflare's differ by more than the mapping's `drift_percent` (10%) | Find out what Cloudflare counts: compare its events with `own_counts` `artifacts_*` and `cost_operations`. If it counts more (binding reads, `ls-refs`), either change repos' `operation_mapping` so customers are charged for what Cloudflare counts, or leave it and let the per-unit cost rise (below). |
196| Cost | Cloudflare charged more than `drift_percent` away from the price book's cost of the same usage, with at least `min_daily_cost` | A price is stale: check the proposals. |
197| Cost, on `models` | What AI Gateway priced g1t's own provider traffic at over the 7 days, against the ledger's model cost for the same days (billed to g1t: comped, free and trial use included, a workspace's own provider not) plus the model cost testing resets kept for those days (`reset_costs`), more than the `ai_gateway_requests` mapping's `drift_percent` (10%) apart, with at least `min_daily_cost`. A ledger with none of the gateway's cost is drift too, and so is a gateway that priced nothing against a ledger with at least `min_daily_cost` of model cost (no percentage): that is not agreement. Its detail says why as far as the run could tell: requests logged with no price (add the models' prices), a token Cloudflare refuses for the gateway (give it AI Gateway Read, or fix `AI_GATEWAY_ID`), a gateway the token sees with no requests (calls went around it), or a read that failed | The gateway higher: model calls g1t paid for and charged no one: runs not settled yet (they catch up within the hour), runs with no session, a run started without a billing ticket, or something else on g1t's gateway. The ledger higher: runs that reached a provider without the gateway. The detail adds why the gateway's own figure may be off: prompt-cache read and write tokens (the gateway prices them at its rates for cache tokens, which can lag the provider's; check against the provider's invoice), requests Cloudflare billed itself (unified billing: on Cloudflare's bill, not a provider's), and models with no price. A testing reset in the window is named in the detail: one that kept its cost says how much of the ledger's side it is; one from before resets kept their cost (the audit log has it, `reset_costs` does not) says the gateway's figure includes usage the ledger no longer has, so that part is not a leak, and the day it leaves the 7 days; while such a reset is in the window the `models` leak is not raised. Days are UTC by when a request ran (gateway) and when a charge was entered (ledger), so a run across midnight shifts a little between days; the 7-day sum absorbs it. |
198| Unpriced | Over the 7 days, a model in AI Gateway's analytics with tokens and $0 cost, or runs settled with `runs.gateway_note` (the gateway could not price all of a run) | The gateway has no price for a model g1t runs: add it in the gateway (custom cost) or route away from it. Until then those runs are charged no less than the sandbox reported (Claude Code's own price table), never $0 silently. |
199| Leak | Cost of at least `min_daily_cost` and nothing charged for it (never for `platform`), or a meter in `unmapped` | Map the meter (below), or decide it is overhead (`platform`). |
200
201## Prices: versions, proposals, notice
202
203- `price_versions` holds every price ever, never edited. `prices` is the
204 version in force. The daily run applies a version once its
205 `effective_at` has come, and adds the public `price_changes` record.
206 Ledger entries made from the price book carry `price_version` (the
207 version ids, comma-separated), so a past statement is always explained
208 by the prices of its day.
209- Proposals come from the keeper (sandbox seconds, app requests and CPU)
210 and the reconciler (mappings with `scale_to_own`: today git operations).
211 For git operations: Cloudflare's rate per its own operation (the median
212 over charged days of cost ÷ quantity) × (Cloudflare's operations ÷ g1t's)
213 × 1,000. If Cloudflare counts three for each one g1t counts, the per-1,000
214 price triples. At least 1,000 of g1t's operations are needed.
215- Decision (`pricing::decide`): under 2% is noise; more than 4× either way
216 is suspect and waits for staff; within `auto_apply_percent` (25%) it is
217 applied on its own when `auto_apply` is on; anything else waits.
218- Notice: a fall applies at once. A rise applies `notice_days` (14) after
219 the decision, and for a monthly meter (git, storage, cache, domains,
220 embeddings, scans) at the start of the month after that, so no month
221 is charged at two prices. Owners of workspaces on the plan are emailed
222 once per rise (`price_notices`), and the pricing page lists it with
223 "takes effect". Rises are never retroactive; margin protection is for
224 new usage once notice has run.
225- A newer measurement before a rise's date replaces its version, with
226 the new rise's own full notice; the proposal behind the replaced version
227 is marked `superseded` (it never took effect, and nothing was charged at
228 it). Sudo says "applied by guardrail, then replaced by a later
229 measurement". Migration 0049 marks the ones from before: sandbox and
230 build seconds' rises of 2026-10-07, replaced on 10-08.
231- Staff approve or reject in sudo. A rejection needs a note.
232- **Price versions** in sudo show each version's cost and price, except
233 where the cost column is not a cost (`PriceVersion.basis`,
234 `pricing::basis_of`): the agent rate and security activation are rates
235 g1t sets (`rate`, no cost shown), and the agent rate's token weights are
236 multipliers (`weight`, shown as ×0.1). Dates show with their time: a
237 version from 00:00 UTC is the evening before in the Americas.
238
239## Changing a mapping
240
241In sudo, Costs & margin → Bill & pricing → **Mappings**: Cloudflare's product and meter
242prefix (as **Cloudflare's lines** lists them; `*` for the rest of the
243product), g1t's product, and optionally:
244
245- **Price meter**: the price book meter the line measures.
246- **Own meter**: g1t's count of the same units (`own_counts.meter`).
247- **Scale to g1t's count**: price one of g1t's units at as many of
248 Cloudflare's as it took (proposals as above).
249- **Drift threshold**.
250
251It applies from the next run; **Run the analysis now** applies it at once.
252Every change is in the audit log (`cost_mapping`).
253
254## Which raw meters are operations
255
256There is one mapping, and the repos service owns it: `operation_mapping`
257in g1t-repos' database, one row per raw meter with `cost_operations` (how
258many operations Cloudflare bills for it) and `billable_operations` (how
259many the customer is charged for). Change it with repos'
260`set_operation_mapping` RPC (services only), or
261`npx wrangler d1 execute g1t-repos --remote` until sudo has a form. A
262change applies to counts from then on, never to what was counted.
263Billing keeps no mapping of its own: it reads repos' `git_operations`
264(already mapped) for what customers are charged, and `artifacts_usage`
265(raw counts with the mapping) for `cost_operations`. Migration 0023 drops
266the `billable_units` table 0022 made for this, which was never written.
267
268**Charged at price** (a product's value) is what each day's usage was paid:
269charged to a card or credit, or drawn from the plan's included usage, a
270trial, a pool or a gift. Usage nothing paid for, as in a free period, is
271valued at price (cost plus the margin), since it was given away at its price
272rather than sold for nothing; so are g1t's own workspaces. Runs on a
273workspace's own model provider have no cost to g1t. Every daily run
274reconciles the whole 31-day window from what is already kept, so a change in
275how a day is valued reaches every day sudo shows.
276
277## Alerts runbook
278
279| Alert | Raised when | First steps |
280| --- | --- | --- |
281| Margin under the floor | A product's value against cost under `margin_floor_percent` (10%) for `alert_days` (3) days running, each with at least `min_daily_cost` | Open the product on Costs & margin. Cost up? Check proposals (approve a rise; it waits out the notice). Value down? A mapping or `revenue_map` may have moved. |
282| All of g1t under the floor | The same for money in against the cost of what was sold: every cost less what was given away (comped workspaces, free periods, the trial, the pools), which is a budget watched in budget.rs. While less than $1 a day comes in, it says the dollars, not a percentage | Look at which products moved; check `platform` (it has no revenue of its own and grows with traffic). Before launch, with little paid usage, expect it. |
283| Leak | Drift of kind leak | Map the meter, or decide it is overhead. |
284| Drift | Count drift | See Drift above. Cloudflare's definitions change in beta: ask them in writing ([ARTIFACTS.md](ARTIFACTS.md), §7). |
285| Costs more than it pays | A workspace's shared cost over 30 days above what its usage was priced at (`value_micros`, whoever paid: card, trial, gift or included usage) × `anomaly_factor`, at least `anomaly_floor`; not comped workspaces | Shown on Reach out as "Costs more than it pays": its usage is priced below what it costs. Abuse (Abuse & fraud page) or a gap in pricing. Not emailed. A trial or gift paying for usage does not raise it. |
286
287Alerts close on their own when the condition clears. Open ones are
288emailed again weekly. The red bar on every sudo page shows margin,
289overall and leak alerts.
290
291## Model costs
292
293Every model call g1t pays for is an agent run's (the `claude` CLI in the
294sandbox, `crates/runner`) or a customer's AI Gateway request (a workspace
295token with `models:write` at `models.g1t.sh/anthropic` or
296`models.g1t.sh/openai/v1`, `gateway.rs`, to Claude on Anthropic or to open
297models on Workers AI through the same AI Gateway); the only other model is
298Workers AI's embeddings for g1t's own search, which are on Cloudflare's bill
299(`embeddings`). How each reaches the ledger:
300
301| Call | Who pays | Run and session | Ledger cost | Settled to the gateway |
302| --- | --- | --- | --- | --- |
303| Agent run through the model proxy (`services/models`) on g1t's hosted models | g1t | `runs` row; session `ms_…` in `cf-aig-metadata` | On finish, the sandbox's figure (Claude Code's `total_cost_usd`, at its own price table, cache tokens included) | Yes, every 15 minutes |
304| Agent run straight to the gateway (no `MODELS_URL`) | g1t | `runs` row; session `rs_…` in `cf-aig-metadata` (`services/runner` `gatewaySession`) | As above | Yes |
305| Agent run with no gateway (`AI_GATEWAY_ID` empty, self-hosting) | g1t's key | `runs` row, no session | The sandbox's figure | No: nothing to settle against |
306| Agent run on a workspace's own provider | The workspace | `runs` row, `billed_to = 'workspace'`, session `ms_…` (the proxy counts its tokens by it) | No model cost (none to g1t); the agent rate on `agent_tokens_own`, line `<run>/agent-own` | No; never on g1t's gateway. Closed by the cron's `settle_own_runs`, which charges tokens counted late |
307| AI Gateway request on g1t's key | The workspace, from included usage and AI credit (`gateway_admit` refuses at $0, over the limit, or with no plan) | No run; `gateway_requests` row; session `gw_<token>_<YYYYMMDDHH>` in `cf-aig-metadata` (`task: gateway`) | Per request: its tokens at `gateway_models` (the table, migrations 0045 and 0047: list price per million by kind, five-minute and one-hour cache writes apart, and for a model priced by prompt length such as Claude Haiku 5.5 a `threshold` above which the whole request is at the `over_` prices) plus the `gateway_models` meter's markup (0 in beta); a `usage` line with task `gateway`, reference the request id, quantity 1 | No, not yet: the session tag is there for it. **Cost** drift on `models` compares the gateway's total with these lines too. Requests billed other than by tokens (fast mode, `inference_geo`, fallbacks, server tools, containers) are refused by the proxy |
308| AI Gateway request on a workspace's own provider (any model connection whose `gateway_models` takes the model) | The workspace's provider | `gateway_requests` row, `own_key = 1`, `provider` and `connection` | None | No; never on g1t's gateway |
309| A sandbox that died before reporting | g1t | as its route | Charged from the gateway when settled | Yes |
310| Embeddings (indexing) | g1t | none (Workers AI) | Month-end `context` meter | No: Cloudflare's bill, `embeddings` bucket |
311| Embeddings (queries, search and agent context) | g1t | none | None: not charged, by design | No: in Cloudflare's `embeddings` line, shared out |
312
313**Settling.** A run's charge is corrected to what AI Gateway priced its
314session's requests at (`settled_cost` in `keeper.rs`). The gateway's
315figure is trusted in full: it is not held to the $100 cap on a sandbox's
316own report. It is never taken below what the sandbox reported when it
317cannot be the whole cost: a request with tokens and no cost (a model the
318gateway has no price for) or more logs than are read (2,000). Such a run
319keeps `runs.gateway_note`, its correction says why, and it raises the
320**Unpriced** drift. For agent runs g1t keeps no token rates of its own:
321the first figure is Claude Code's, the final one the gateway's. AI Gateway
322requests are the exception: they are charged from `gateway_models` (the
323model catalogue, below), which has to follow the provider's price list by
324hand until they are settled like runs. It is not part of the price book's
325`price_versions`: a new model's prices are confirmed when staff approve it
326in sudo, and a changed price on a model already offered is a migration that
327updates the row and its `updated_at` (as 0047 did for Sonnet 5.5's cache
328reads). The `gateway_models` meter's markup is the only price-book number
329on it. Open
330models need `WORKERS_AI_TOKEN` (a Cloudflare API token with Workers AI on
331g1t's account) on the model proxy; without it, and without
332`AI_GATEWAY_TOKEN` holding that permission, they are refused with `503`.
333
334**The daily total.** AI Gateway's analytics for the day (above) against
335the ledger's model cost is the check that nothing slips past: a model call
336with no run, or a run never settled, shows as **Cost** drift on `models`.
337The gateway's per-request `cost` is its estimate from its own price list:
338it can be off for prompt-cache tokens, for requests Cloudflare bills
339itself, and for models it has no price for. The drift's detail says when
340any of those were in the window; the provider's invoice is the last word.
341
342### Margin floor
343
344A sold charge is cost × (1 + `MARGIN_PERCENT`), rounded up (`margin_on`;
345`charge_micros` for a sandbox's own report). Terms change it only as
346follows (`Terms::discounted`, `Billing::charged`):
347
348- **Standard**: charged in full.
349- **A 100% discount** (what was "comped"; see [Discounts](#discounts)),
350 `FREE_WHILE_BUILDING`, the plan's included usage, the trial,
351 the open-source pool, and overruns g1t covers: given, and counted by why
352 (above).
353- **Custom, with a discount**: the discount comes off, and what it took
354 below cost plus the margin is written on the entry as
355 `ledger.discount_micros` and counted as given (**discount**), so the sale
356 is valued at its price and charged plus given is never under cost plus the
357 margin. On a settlement correction it moves with the charge (less than
358 nothing when the charge comes down).
359- **Goodwill credits** (overages) are separate, given by staff on purpose:
360 their margin part first, the cost only up to the cap, each audited.
361
362Every usage path goes through this: `finish_run`, settling, sandbox time,
363features and builds (`charge_feature`), and the month-end meters.
364
365## The model catalogue
366
367Every model g1t can use is one row of `gateway_models` (migrations 0045,
3680047 and `0048_model_catalogue.sql`; code in `catalogue.rs`): the agents'
369tiers, the AI Gateway's Claude and open models, and the embeddings model.
370Billing keeps it because billing owns prices: the gateway charges from the
371same row, so there is one price per model, not two that drift. Page: sudo
372**Agents & models** (`/agents`).
373
374| Column | What |
375| --- | --- |
376| `model`, `name`, `provider`, `kind` | The provider's id, the name people see, `anthropic` or `workers-ai`, `chat` or `embeddings` |
377| `aliases` | Other ids the provider lists it by, comma separated (a dated id such as `claude-haiku-4-5-20251001`) |
378| `family`, `tier_hint` | `haiku`, `sonnet`, `opus`, `fable` (or a Workers AI author); the agent tier it suits: `small`, `large`, `frontier` |
379| `context_window`, `max_output`, `capabilities`, `dimensions` | From the provider's list where it gives them: `effort`, `thinking`, `tools`, `vision`, `embeddings`; an embeddings model's vector length |
380| Prices | Per million tokens in millionths: input, output, cache read, five-minute and hour-long cache writes, and for a model priced by prompt length the `threshold` and `over_` prices |
381| `status` | `available` (routed to and offered), `new` (found, not approved), `deprecated` (the provider stopped listing it), `retired` (staff took it out) |
382| `priced` | 1 when its prices are known |
383| `source`, `first_seen_at`, `last_seen_at`, `missing_since`, `approved_by`, `approved_at`, `note` | Where it came from (`discovered` or `staff`) and its history |
384
385What each status allows:
386
387| Status | Default for a purpose | Offered on the AI Gateway | Charged |
388| --- | --- | --- | --- |
389| `available`, priced | Yes | Yes | Yes |
390| `new`, or unpriced | No | No: refused before it reaches a provider | No |
391| `deprecated` | No: a default that chose it falls back | Yes, to whoever names it | Yes |
392| `retired` | No: a default that chose it falls back | No | No |
393
394### Discovery
395
396The models service lists each provider once a day (`29 5 * * *`, after
397billing's daily run) and whenever staff press **Check for new models**
398(`services/models/src/discover.ts`, through its `Discovery` entrypoint,
399which only sudo binds). Listing models is free; nothing calls a model.
400
401| Provider | How it is listed | Credentials (on g1t-models) |
402| --- | --- | --- |
403| Anthropic | `GET /v1/models` through g1t's AI Gateway (`…/anthropic/v1/models`, tagged `task: discovery`), or straight to Anthropic with g1t's key when there is no gateway | `AI_GATEWAY_TOKEN` (the gateway holds Anthropic's key), or `ANTHROPIC_API_KEY` |
404| Workers AI | `GET /accounts/{account}/ai/models/search` | `WORKERS_AI_TOKEN` (Workers AI Read), else `AI_GATEWAY_TOKEN` |
405
406Each provider's list goes to billing's `record_discovery`, which compares
407it with the catalogue:
408
409- **An id it has never seen** is added as `new`. Chat and embeddings
410 models only. Anthropic models are priced from the price table in
411 `catalogue.rs` (`ANTHROPIC_PRICES`: Anthropic's list prices by model, a
412 dated id priced as its model) when it has them; Workers AI models from
413 the price their listing gives, with cached tokens at the input price.
414 Anything else is added unpriced. Its family and tier hint come from its
415 id (`claude-haiku-*` is fast, `claude-sonnet-*` standard,
416 `claude-opus-*` and `claude-fable-*` most capable).
417- **A dated id of a model it has** (`claude-sonnet-5-5-20261001`) is that
418 model: the id is added to its aliases.
419- **A model it has that is listed** gets `last_seen_at`, and its context
420 window, output limit and capabilities from the list.
421- **A model it has that is not listed** (available or new) becomes
422 `deprecated`, with `missing_since`. Listed again, it goes back to
423 `available` if it was ever approved, else to `new`.
424- **An empty list or a failed one** changes nothing: it is recorded as a
425 failed check with the provider's answer (never a key).
426
427Every check is kept 90 days in `model_checks` and shown on the page. When a
428check adds, deprecates or restores anything, it is in the audit log
429(`models_discovered`, as `schedule` or the staff member) and staff are
430emailed at `COSTS_ALERT_EMAIL` with a link to Agents & models.
431
432When Anthropic publishes a new model's price, add a row to
433`ANTHROPIC_PRICES`, so the next one of its kind arrives priced. Until then
434staff enter the prices when they approve it.
435
436### Approving, retiring and restoring
437
438On Agents & models, **New models** lists every `new` model with its prices
439filled in where they are known. To approve one:
440
4411. Check the name people see and the tier it suits.
4422. Check or enter its prices per million tokens against the provider's
443 price page: input, output, cache read, cache writes (five-minute and
444 hour-long; an hour-long price left empty is the five-minute one), and
445 under **Priced by prompt length** the threshold and the prices above it.
4463. Say why (for example where the prices came from), and **Approve**
447 (`admin_decide_model`, `approve`).
448
449It is `available` at once: the AI Gateway offers it within five minutes
450(the proxy keeps the catalogue that long) and it can be chosen as a
451default. **Retire** takes a model out of routing and the gateway; any
452default that chose it falls back to the next suitable model until staff
453choose another, and the audit line names those defaults. **Restore** puts
454a retired or deprecated model back: `available` if it was ever approved,
455else `new`. Each needs a reason and is in the audit log (`model_approved`,
456`model_retired`, `model_restored`).
457
458### Defaults
459
460`model_defaults` holds staff's choice per purpose, each with when, who and
461why (`admin_set_model_default`; the audit log, `model_default`, has the old
462value, the new one and why):
463
464| Purpose | What it chooses | Read by |
465| --- | --- | --- |
466| `tier_small`, `tier_large`, `tier_frontier` | The model behind Auto's fast, standard and most capable tiers | The runner |
467| `background` | The harness's own small tasks in every run on g1t's tiers (`ANTHROPIC_SMALL_FAST_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`) | The runner |
468| `gateway_first` | The Claude listed first by `GET /openai/v1/models` | Billing's `gateway_models` |
469| `job_implement`, `job_revise`, `job_answer`, `job_review`, `job_update`, `job_plan` | Each kind of job's starting tier (`small`, `large`, `frontier`; a review also `change`, sized by its change) and effort (`low` to `max`, or none for the harness's own) | The runner |
470
471A model purpose takes only an available, priced Claude chat model (the
472harness speaks Anthropic's API). The migration seeded each with what
473`AGENT_ROUTING` had.
474
475Before a default is saved, sudo shows it beside the current one with what
476a **typical run** would cost on each: 40 requests of 2,000 input, 1,500
477output, 45,000 cache-read and 4,000 cache-write tokens, each request priced
478on its own at the catalogue's prices (`catalogue::TYPICAL`; on 2026-10-08,
479$0.076 on Claude Haiku 5.5, $1.34 on Sonnet 5.5, $2.68 on Opus 5.5). It is
480an estimate for comparing models, never a charge.
481
482**How the runner reads them.** `model_defaults` (the RPC) returns each
483purpose's model as it applies now, with its prices and capabilities, and
484each job's tier and effort. The runner reads it at most once a minute per
485isolate and puts it over `AGENT_ROUTING` (`withDefaults` in
486`services/runner/src/model-env.ts`), so a change reaches runs within a
487minute. When billing cannot be read, the runner uses `AGENT_ROUTING` (and
488`DEFAULT_ROUTING` under it) alone and asks again ten seconds later. The
489labels, change sizes, `frontierAfter` and learning always come from
490`AGENT_ROUTING`. Effort is not sent on a model whose catalogue entry lacks
491`effort` (Claude Haiku 4.5).
492
493**Never a retired model.** A chosen model that is deprecated, retired or
494unpriced is not handed out: the purpose falls back to the first available,
495priced Claude with the same tier hint (any Claude for `gateway_first`), in
496the catalogue's order, with a sentence such as *Claude Haiku 5.5 is
497retired; using Claude Haiku 4.5 instead.* The runner adds that sentence to
498the run's reason line, and sudo shows it beside the default. With no model
499left for a purpose, the runner keeps `AGENT_ROUTING`'s.
500
501**Not a default:** the context hub's embeddings model
502(`@cf/baai/bge-base-en-v1.5`, in `services/context`). Its vectors are
503only comparable with others from the same model, so changing it means a
504new index, rebuilt; it is pinned in code, and listed in the catalogue with
505its price. No other g1t service calls a model.
506
507Customers never choose among these: they keep **Auto**, or pin a tier per
508kind of work, and nothing customer-facing names the catalogue.
509
510## Discounts
511
512An account's terms are standard, or custom: a **discount** from 1 to 100%,
513a limit of its own, or both, with a reason (the terms' note) and an
514optional end date. What used to be "comped" is a **100% discount**
515(`Terms::full_discount`; migration `0039_discounts_not_comped.sql` moved
516every `comped` row to `custom` at 100%, and code reads a leftover `comped`
517row as 100%). In SQL, `sales::FULL_DISCOUNT_SQL`.
518
519- **Charging.** Every charge records what the discount took off it
520 (`ledger.discount_micros`), 100% included: the entry is charged nothing
521 and the discount is its whole price. Migration 0039 backfilled the
522 discount on a 100%-discounted workspace's earlier entries that were
523 charged nothing and paid by nothing, at cost plus 20%.
524- **What a 100% discount still does as "comped" did.** The plan is on
525 without its price (`PlanKind::Internal`), trust is `internal` (no limit
526 on unpaid usage), nothing is invoiced or closed, and g1t's own spend on
527 it is held to the monthly budget (the terms' limit, at cost; see
528 [Spend caps](#spend-caps)).
529- **The statement and Usage.** The customer sees every usage line at its
530 price (`StatementLine.price_micros`: charged, plus what paid for it, plus
531 the discount), the discount per day or project and in the totals
532 (`StatementTotals.price_micros`, `discount_micros`, `discount_percent`),
533 and the CSV has price and discount columns. The Usage page measures at
534 price for a discounted account (`Usage.discount_micros`,
535 `discount_percent`).
536- **Margin.** A 100% discount's usage is given away as before, in the
537 bucket still named `comped` (`given_comped_micros`); sudo calls it
538 **100% discounts**. A partial discount's part below cost plus the margin
539 is `given_discount_micros` (**partial discounts**). Both are kept apart
540 from margin on what was sold.
541- **sudo.** The workspace's **Terms** form takes a discount (None, 25%,
542 50%, 100%, or Custom, a whole percent; the custom field shows by CSS
543 alone), a limit, an end date and the reason. Badges and filters say
544 *100% discount* or *N% off*. Each change is audited (`terms`), such as
545 `standard → 100% discount, monthly budget $150.00: g1t's own`.
546
547## Credits from g1t
548
549Staff give a workspace credit from sudo; the code is
550`services/billing/src/grants.rs`, the tables `credit_grants` and
551`ledger.credit_kind` (migration `0038_staff_credits.sql`).
552
553### Giving credit
554
555sudo → the workspace (or an enterprise, choosing one of its workspaces) →
556**Give credit**:
557
5581. **Amount**: $10, $20, $25, $50, $100, or **Custom** (up to $10,000).
559 Up to $100 it is one step; over $100, type the workspace's slug as well.
5602. **Kind**: promotional (a welcome, a referral, an event), goodwill (an
561 apology), or refund (money back for something that went wrong: say what
562 it refunds and, optionally, the day).
5633. **Expires**: never, 30, 90 or 365 days, or the end of a chosen day
564 (UTC). A refund never expires.
5654. **Note**: required. It is on the statement and in the owners' email.
566
567The form needs no JavaScript: the fields for one choice (the custom amount,
568a refund's details, the expiry date) show by CSS alone, and all show where
569`:has()` is not supported. `admin_credit` checks everything again.
570
571A grant is a `crd_…` ledger line (kind `top_up`, so never a payment) with
572`credit_kind`, and a `credit_grants` row. The balance rises at once. The
573owners are emailed through identity's `notify_owners` (the same path as
574limit notices). It is audited as `credit`. The Overages queue's one-click
575goodwill credit is a grant too, of kind goodwill.
576
577The inbox is not told: its items are threads on a repository, built from
578events, and a credit is a workspace's. That needs a workspace-level inbox
579thread first.
580
581### How it is spent
582
583Credit is spent before anything prepaid, the soonest-expiring grant first
584(never-expiring last, then the oldest). Given while the workspace owes, it
585pays what is owed first, the most recent usage first. What each grant paid
586for is never stored: `grants::replay` works it out from the ledger in order,
587so the charge paths do not know about credit and the answer is always what
588the ledger says. A charge that comes down (a settled run) gives back to
589the grant that paid last, while it can still be spent.
590
591### Expiry and revoking
592
593- **Expiry.** The daily run (`expire_credits`, before the reconciliation)
594 closes grants past `expires_at` and enters what was left as a negative
595 `crd…_expired` line; audited as `credit_expired`. A grant past its expiry
596 pays for nothing even before the run.
597- **Revoke.** sudo → the workspace's **Credits** (or **Credits & refunds**)
598 → **Revoke unused**, with why (`admin_revoke_credit`): what is left, as a
599 `crd…_revoked` line, audited as `credit_revoked`. What was spent stays
600 spent.
601
602Neither takes the balance below zero: at most the balance, if a refunded
603payment left less there than the credit.
604
605### How margin treats them
606
607| Kind | When spent | On the day it was given |
608| --- | --- | --- |
609| Promotional | The usage is valued at its price, its charge comes out of cash and is given (`given_credit_promotional_micros`) | Nothing |
610| Goodwill | The same, as `given_credit_goodwill_micros` | Nothing |
611| Refund | Paid for: cash, as any usage | Its amount (less what was revoked) comes off cash on the day it refunds, shared over that day's paid usage |
612
613A refund gives back money already collected, so counting it as given would
614make it look like a budget g1t chose to spend. Taking it off cash for the
615day it refunds says that day's sale was worth less, and counting what it
616later pays for as cash keeps money in equal to what was collected. The
617refund's day is clamped to the last 30 days, the days the reconciliation
618recomputes; an older one lands on the oldest. Refunds never expire, so
619cash taken back is never stranded.
620
621What credit paid of a month-end meter (storage, git, scans) is its own
622row on the day it was charged, since those meters are reconciled from
623snapshots. Sudo's Costs & margin lists promotional and goodwill credit
624under **Given away**, and below the statement the range's credits given,
625spent, and refunded. **Credits & refunds** (`/credits`, `admin_credits`)
626lists every grant (by kind, month, staff and workspace) and the last 12
627months by kind: given, spent, expired, revoked.
628
629### Purchased and scoped credit (prepaid AI)
630
631`credit_grants` also has `scope` (`all`, or `models`: model usage only,
632`grants::is_model_usage`, which includes the agent rate) and `source`
633(`staff`, `purchase`, `promo_code`, `upgrade`), and `CreditKind::Purchased`.
634Spending takes credit scoped to models first, then the soonest-expiring. A
635grant scoped to models given while the workspace owes pays only what models
636owed, never other usage (`replay`). Purchased credit is money paid in: its
637ledger line is a payment (Stripe's id, never `crd…`, `credit_kind`
638`purchased`, statement kind *AI credit*), and the usage it pays for stays
639money in, never given. Staff cannot give it (`admin_credit` refuses the
640kind). Code: `services/billing/src/ai.rs`; migration `0040_ai_credit.sql`.
641
642- **Buying.** `buy_ai_credit` opens Stripe Checkout (payment mode, $10 to
643 $1,000, a second line *Card processing fee* when the `card_fee` cost
644 setting is on, `setup_future_usage=off_session`), recorded in `checkouts`
645 with `feature = 'ai_credit'`, `amount_cents` the credit and `fee_cents`
646 the fee. The credit is entered by whichever comes first, the person coming
647 back (`confirm_ai_credit`, `?ai_credit=cs_…`) or
648 `checkout.session.completed`: both claim the row `open → paid`, the
649 grant's id is the session's id (`INSERT OR IGNORE`) and the ledger's
650 reference is unique, so a payment is credited exactly once. Expires 365
651 days after purchase (the daily `expire_credits`).
652- **Owed.** AI credit props up the balance but is money only for models, so
653 what is owed is `max(0, AI credit left − balance)` (`owed_with`; at a
654 month's close, `models_left_before` the month's start).
655- **Auto-reload.** `ai_reload` (settings; off by default) and `ai_reloads`
656 (one row per attempt). Each cron run (and a run that would be refused)
657 calls `reload_now`: below the threshold, it charges the customer's default
658 payment method off-session for the target less the balance (whole
659 dollars, at least $10, within the month's maximum), with the idempotency
660 key `reload/<workspace>/<YYYY-MM>/<n>` (a retry after a crash is the same
661 PaymentIntent), and grants purchased credit with the PaymentIntent's id. A
662 decline or a payment needing the person turns auto-reload off
663 (`failed_at`, `error`), emails the owners and audits `ai_reload_failed`.
664- **At $0.** `start_run` on g1t's models refuses with `payment_required`
665 when the workspace is on the paid plan (not a 100% discount, not an
666 enterprise), its included usage is used, and AI credit is $0 or less.
667- **Upgrade credit.** The first time a plan subscription is recorded active
668 (`features::record`), $5 of promotional credit scoped to models, id
669 `crd_upgrade_<workspace>`, expiring in a year: given, never revenue. Never
670 for a workspace with a 100% discount.
671
672### The agent rate and models' markup
673
674Price-book meters (migration 0040, each with versions and a public change):
675`agent_models` (per provider dollar; markup 20% until 2026-10-08, then 0,
676a fall applied at once), `agent_tokens` ($0 until 2026-10-22, then $0.25 a
677million tokens: a rise, after the 14 days' notice, emailed to owners on the
678plan by `tell_owners_of_rises`), `gateway_models` (markup 0 during beta),
679`card_fee_percent` (29,000 micros per dollar) and `card_fee_fixed`
680(300,000). Changing any is a price-book change, never a deploy. `finish_run`
681and `settle` charge models at `agent_models`' markup; `charge_agent_rate`
682charges the weighted tokens of the run's session since it was last charged
683(`runs.agent_tokens`, the weighted tokens charged so far, claimed with a
684compare-and-set), on a line `<run>/agent` (later `<run>/agent/<tokens>`),
685with `quantity` the weighted tokens. What it counts is the more of what
686`token_usage` holds for the session and what the sandbox reported with its
687cost (`finish_run`'s `tokens`, from Claude Code's closing `usage`), each
688weighted by kind. **Card fee switch:** sudo → Costs → Guardrails → *Card fee
689on card payments* (`cost_settings.card_fee`, `on`/`off`, on by default). It
690covers every card payment now, not only AI credit: see
691[Tax and the card fee](#tax-and-the-card-fee).
692
693**On a workspace's own model key** (migration `0041_agent_rate_own_key.sql`):
694the run keeps its model session (`runs.session_id`, `ms_…`) so the proxy's
695counts reach it, and the agent rate is charged at `agent_tokens_own` ($0 until
6962026-10-22, then $0.25 a million, a rise from nothing with its notice), on
697`<run>/agent-own` (later `<run>/agent-own/<tokens>`), `billed_to = 'g1t'`
698(g1t's own charge: it counts toward limits and spend), named *Agent rate,
699your own model key* on Usage and the statement. The model is never charged.
700`settle_runs` skips these runs (nothing on g1t's gateway); the cron's
701`settle_own_runs` closes them 5 minutes after they finish (3 hours after
702they start, for a sandbox that never reported) and charges tokens counted
703late. Runs from before have no session and are never charged the rate.
704
705### The agent rate's token weights
706
707How much each kind of token counts toward the agent rate, on g1t's models
708and own keys alike, is four price-book meters (migration
709`0042_agent_rate_weights.sql`): `agent_token_weight_input`, `_output`,
710`_cache_read` and `_cache_write`, each a weight in millionths in
711`cost_micros` (1,000,000 counts a token once). They started at 1, which is
712what the rate always counted; since 2026-10-08 cache reads count a tenth
713(100,000; migration `0044_cache_reads_count_a_tenth.sql`), as model providers
714price them. Input, output and cache writes count once. A cached agent run reads most of its context from
715cache (about 90% of its tokens on a typical Sonnet implement run), so the
716cache-read weight is the lever: at 1 the rate adds about 44% to such a run's
717model cost; at 0.1, far less.
718
719To change a weight (cache reads went to a tenth this way):
720
7211. Add the version, effective at once (a lower weight is a fall):
722
723 ```sql
724 INSERT INTO price_versions (id, meter, version, cost_micros, markup_percent, effective_at, reason, created_by, created_at)
725 VALUES ('pv_agent_token_weight_cache_read_2', 'agent_token_weight_cache_read', 2, 100000, 0,
726 '2026-10-08T00:00:00Z', 'Cache reads count a tenth toward the agent rate', 'staff', '2026-10-08T00:00:00Z');
727 ```
728
729 in a migration, or with `npx wrangler d1 execute g1t-billing --remote`
730 until sudo has a form.
7312. The daily run applies it once `effective_at` has come and writes the
732 public `price_changes` record (**Run the analysis now** applies it at
733 once). Raising a weight later is a rise: give it an `effective_at` 14
734 days out, and owners on the plan are emailed.
7353. Check `/pricing`: the agent rate's row lists the weights, and Usage's
736 agent-rate lines name them.
737
738`weighted` (`ai.rs`) rounds down to a whole token; a weight is never below 0.
739
740### Budgets
741
742The owners' spend limit is the budget. `limits.alert_levels` (comma
743separated, default every level), `limits.pause_at_limit` (default 1; off,
744100% is a warning, never a stop; the trust ceiling still stops work) and
745`limits.budget_webhook` (an https address, not g1t's; posted once per alert
746level a month by `warn_limits`). `set_budget` sets them, with `keepLimit`
747to leave the limit itself alone. A free workspace (trust `new`) has no
748spend limit to set: `set_spend_limit` and `set_budget` say so.
749
750### Earlier credits
751
752Migration 0038 makes every earlier `Credit from g1t:` line a goodwill
753grant with no expiry: the old form asked for "a refund or goodwill" with
754no way to tell them apart, and goodwill never reads as money in.
755
756## Token usage
757
758The model proxy (`services/models`) reads Anthropic's `usage` from every
759`/v1/messages` answer, streamed or whole, on g1t's models and on a
760workspace's own provider alike (OpenAI-shaped providers are translated
761first). Count-tokens requests are not answers and are skipped. After the
762answer, it calls `record_tokens`, which adds input, output, cache reads and
763cache writes to one row per day, workspace, person, session and model in
764`token_usage` (migration `0030_token_usage.sql`). The person is who the run
765was for, from the model session's `requested_by`; never g1t's agent. A
766report that fails is dropped and never affects the answer.
767
768`token_usage` reads a window (42 days by default, 366 at most) for the
769workspace or one person: totals, every day's tokens and the active days,
770with `costMicros` the window's run charges from the ledger, measured as
771`usage` measures them. Usage's report also lists tokens by model
772(`UsageReport.models`). The agent rate is charged on these counts (above);
773a run's model is still priced from AI Gateway's logs, never from them.
774
775## Tables (migration `0022_costs_and_margin.sql`)
776
777`cost_lines`, `cost_map`, `revenue_map`, `own_counts`,
778`pending_days`, `margin_days`, `workspace_costs`, `cost_drift`,
779`margin_alerts`, `price_versions` (seeded with every current price as
780version 1), `price_proposals`, `price_notices`, `cost_settings` (the
781guardrails, seeded), the `actions_cache` price, and `ledger.price_version`.
782Every create is `IF NOT EXISTS` and every seed `INSERT OR IGNORE`; the one
783`ALTER` is applied once by D1's migration tracking. Migration
784`0023_one_operation_mapping.sql` drops `billable_units` (see above).
785Migration `0036_model_costs_in_full.sql` adds `ledger.discount_micros`,
786`margin_days.given_discount_micros`, `runs.gateway_note` and the
787`ai_gateway_requests` → `models` mapping. Migration
788`0038_staff_credits.sql` adds `credit_grants`, `ledger.credit_kind` and
789`margin_days.given_credit_{promotional,goodwill}_micros`, and backfills
790earlier credits (see [Credits from g1t](#credits-from-g1t)). Migration
791`0046_reset_costs.sql` adds `reset_costs` and `margin_days.given_reset_micros`
792(see [Resetting a test workspace](#resetting-a-test-workspace)).
793
794## Spend caps
795
796Two caps keep what g1t pays for itself bounded while billing takes no
797real money. Both are measured at **cost** (what Cloudflare and the model
798providers charge g1t), never at price. Code: `services/billing/src/budget.rs`.
799Page: sudo **Costs & margin** → **g1t's own spend** (`/costs#spend`).
800
801### What counts as g1t's own spend
802
803Every charge that settles (an agent run's model cost from `finish_run` or
804AI Gateway's settlement, sandbox time from `record_sandbox`, a build from
805`charge_feature`) is split by what paid for it and g1t's part is added to
806`g1t_spend` (day, bucket, billing account):
807
808| Bucket | What |
809| --- | --- |
810| `comped` | All of a comped account's work (flagon-io) |
811| `trial` | The trial credit's share |
812| `oss` | The open-source pool's share |
813| `given` | A free workspace's overrun past its last bit of trial |
814| `unpaid` | Charged, but with no real money behind it: Stripe's test key, or `FREE_WHILE_BUILDING` |
815
816The plan's included usage and on-demand charges count as revenue only
817with live payments; in test mode they are `unpaid`, at what the usage cost
818g1t, never what was charged. A workspace's own
819model provider costs g1t nothing and is not counted. Month-end meters
820(git, storage, scans, embeddings, the cache) are not counted here; the
821daily reconciliation covers them. Migration `0024_spend_caps.sql`
822backfills the current month from the ledger.
823
824**Why it differs from the statement.** The section is the calendar month
825so far, today included, counted as each charge settled: the model
826providers' cost and the price book's cost of sandboxes and builds. The
827statement is the range, reconciled against Cloudflare's bill, where
828sandboxes inside Cloudflare's included usage cost $0. The section shows
829Cloudflare's subscriptions for the whole month, the statement over the
830range. And `g1t_spend` is never wiped: spend on a workspace a testing reset
831wiped later stays here (sudo names it, `SpendCaps.reset_micros`), while the
832statement has it only where the reset kept it (`reset_costs`, given away as
833testing resets). syntaqx's $7.41 of 2026-10-02 to 10-05, reset on 10-07
834before resets kept their cost, is that case.
835
836### Caps
837
838| Cap | Variable (g1t-billing) | Default | At the cap |
839| --- | --- | --- | --- |
840| A comped account's monthly budget | `COMPED_MONTHLY_CEILING_MICROS`, or the account's own **Limit** in its terms | $150 a month | New work on the account (agents, checks, workflows, builds) is refused with "<name>'s monthly budget for g1t's own agents is used up … Staff can raise it in sudo". Runs already going finish; the per-run cap still applies to them. It lifts when staff raise the budget or the month turns (UTC). |
841| The daily breaker | `PLATFORM_DAILY_SPEND_CAP_MICROS` | $75 a day (UTC) | New agent runs on g1t's hosted models that g1t would pay for are refused until 00:00 UTC. Not paused: agents on the workspace's own model provider, checks and builds, and workspaces paying with live payments on the plan (not given by staff) or an enterprise contract. In test mode that exemption covers no one. |
842
843`0` turns either off. Cloudflare's subscriptions are read from Cloudflare
844each day (`cf_subscriptions`) and shown on the page only;
845`CLOUDFLARE_FIXED_MONTHLY_MICROS` ($30) stands in until a read works.
846
847The checks are cheap: `reserve` reads today's total (one indexed sum) and,
848for a comped account, its month's comped rows. Refusals come back as
849`paused`, which the compute gate honours for every plan, internal and
850enterprise included (`packages/contracts/src/compute.ts`). The runner tells
851billing whether an agent run is on hosted models (`hostedModel` on
852`reserve`); a caller that does not say is treated as hosted.
853
854### Alerts
855
856All to `COSTS_ALERT_EMAIL` (`hey@flagon.io`), through the `EMAIL` binding:
857
858- **Comped budget**: at 50, 75, 90 and 100%, once each per account and month
859 (`budget_alerts`), checked every 15 minutes. A jump past several levels
860 sends only the highest.
861- **Breaker**: at once, from the charge that trips it; if that email fails,
862 the 15-minute cron sends it (`spend_breaker.told_at`).
863
864While the breaker is open or a comped budget is used up, every sudo page
865shows a red **Spend cap** bar.
866
867### Raising and lifting
868
869- **Raise a comped budget**: sudo → the workspace → Billing → **Terms**, set
870 **Limit $** to the new monthly budget (blank goes back to the default),
871 with a note. It applies to the next start; nothing to deploy. The change
872 is in the account's audit log.
873- **Lift the breaker for today**: sudo → Costs & margin → **g1t's own
874 spend** → **Lift for today**, with why (`admin_lift_breaker`; audit action
875 `breaker_lifted`). It resets by itself at 00:00 UTC.
876- **Change a default**: edit the variable in `services/billing/wrangler.jsonc`
877 and deploy g1t-billing.
878
879## Resetting a test workspace
880
881sudo → the workspace → **Reset billing (testing)** (`admin_reset_billing`)
882returns a workspace used for testing to how a new customer starts. It
883deletes the workspace's rows from every billing table: ledger and balance,
884plan and plan payments, limits and limit requests, trial grant, invoices,
885holds, card checks, alerts sent, price notices, month-end snapshots and
886closes, storage meters, token usage, spikes, sales records and
887notes, `workspace_costs`, its workspace margin alert and its own billing
888account. It keeps `own_counts` (what Cloudflare's bill is compared with)
889and the audit log, which records the reset with the note, the number of
890rows and what g1t had paid for. The workspace, its members and its
891repositories are identity's and repos' and stay.
892
893**What g1t paid for is kept.** The wiped usage still happened: AI Gateway
894still prices its model calls and Cloudflare still bills its sandboxes. So,
895in the same batch as the deletes, the reset writes `reset_costs`: a row per
896day and bucket the workspace had cost on (the ledger's cost, month-end
897meters' cost, and the value the reconciliation gave it), plus one row for
898the reset itself (bucket `''`, nothing in it) so every reset is on record.
899`reset_at` is the same instant as the reset's `admin_actions` entry. The
900costs run reads the rows back as the workspace's usage on their days, all
901of it given away as **testing resets**: `models` drift compares AI
902Gateway with the ledger's model cost plus what resets kept, the statement
903lists it under **Given away**, and the workspace stays in **Who g1t paid**
904with its cost given. The rows are never wiped by a later reset, and a
905rename moves them. Re-running the analysis reads the same rows and gives
906the same answer.
907
908Resets before migration 0046 kept nothing; their wiped rows are gone and
909nothing is made up for them. The costs run finds them in the audit log
910(`admin_actions`, action `reset`) with no `reset_costs` at the same
911instant, and the `models` drift detail says AI Gateway's figure includes
912usage wiped by a testing reset of that workspace on that day, rather than
913calling it a leak. syntaqx was reset on 2026-10-07 after about $8.60 of
914model usage from 2026-10-02 to 2026-10-07; that usage is in the 7-day
915window until the run of 2026-10-13 and leaves it on 2026-10-14.
916
917Billing refuses it while `STRIPE_SECRET_KEY` is a live key, for comped
918workspaces, and for a workspace an enterprise pays for. It then runs the
919costs analysis again (as **Run the analysis now** does), so the margin
920figures drop the workspace's past usage at once; if that run does not
921finish, the page says so and the button does it.
922
923## Stripe
924
925Billing keeps what it needs from Stripe so reads never wait on it, and
926hears of changes three ways (`webhooks.rs`, `stripe_sync.rs`).
927
928**API version.** Every request sends `Stripe-Version: 2025-02-24.acacia`
929(`stripe::STRIPE_VERSION`), the version billing's field reads are written
930for; without it Stripe answers at the account's default. Webhook events
931come at the destination's own version: billing reads an invoice's
932subscription from `subscription` or `parent.subscription_details.subscription`.
933Raising the version is a code change: read Stripe's upgrade notes for every
934field billing reads.
935
936**Failures.** No Stripe failure reaches a page as a 500: each payment page
937(`page_opened`), the portal, confirmations and plan changes turn it into
938`stripe::friendly` (Stripe's own message, never the request or a key), and
939log the full error with the workspace. Every payment page is recorded
940through one insert (`CHECKOUT_INSERT`), checked against the migrations by
941`every_checkout_insert_fills_the_table`, and has an idempotency key
942(`page/<purpose>/<workspace>/…/<10-minute bucket>`).
943
944**The webhook.** A destination made in Stripe's dashboard (Developers →
945Webhooks → Add destination) with the endpoint URL
946`https://api.g1t.sh/stripe/webhook`, in the mode of billing's key (at
947launch, make one in live mode and put its secret). Its signing secret
948(`whsec_…`: the destination, Signing secret, Reveal) is the billing
949Worker's secret:
950
951```sh
952cd services/billing && npx wrangler secret put STRIPE_WEBHOOK_SECRET
953```
954
955Without it every event is refused with 400. After rolling the secret in
956Stripe, put the new one; during the roll Stripe signs with both, so there
957is no gap. The event list need not be exact: billing adds any event it
958handles that the destination does not send (daily, or **Fix destination**
959in sudo → Stripe), and enables it again if Stripe disabled it. It never
960changes the secret. sudo → Stripe shows whether the secret is set, the
961destination and its status, missing events, and the latest events.
962Events are claimed once each in `stripe_events`; a handler that fails
963forgets its claim, and Stripe retries.
964
965**What is kept, and how it stays current**
966
967| Kept | Where | Refreshed by |
968| --- | --- | --- |
969| Saved card (brand, last 4, expiry) | `accounts.card_*`, `card_synced_at` | `customer.updated`, `payment_method.*`, `setup_intent.succeeded`; forgotten when g1t sets a default card, so the next read asks once; the daily pass for cards older than 7 days |
970| Plans | `subscriptions` | `customer.subscription.*`, `invoice.*`; a read asks Stripe at most hourly once a period is over; the daily pass for rows older than a day |
971| Payments, refunds, disputes | ledger, `checkouts`, invoices | their events |
972
973**Every cron run (every 15 minutes)** replays missed events: Stripe's event
974list from an hour before `stripe_sync.through`, oldest first, through the
975same once-only claim. `through` moves to 5 minutes before now when all were
976handled, back to the first failure otherwise, and stays when more than
9771,000 events were listed. Claims stuck at `handling` for 10 minutes are
978dropped so the replay retries them. The first run reads 3 days back.
979
980**Daily** (`keeper::DAILY`): the destination at billing's address is
981enabled again if Stripe disabled it and given any missing event, audited as
982`stripe`/`webhook`; then up to 25 stale cards and 25 stale plans are read
983again.
984
985**What still calls Stripe on a request**: starting a payment page, a plan
986or a card check; opening the billing portal; settling a page the person
987came back from; renaming a workspace (the customer's name). Nothing a page
988view reads.
989
990## Tax and the card fee
991
992Owner decision 2026-10-08. Code: `services/billing/src/tax.rs` (what is
993kept, the address hold), `stripe.rs` (every request's tax fields),
994`invoices.rs`, `webhooks.rs`, `ai.rs`; migration
995`0043_tax_and_card_fees.sql`.
996
997### What Stripe is asked
998
999Every price excludes tax (`tax_behavior=exclusive`) and carries tax code
1000`txcd_10103001` (software as a service, business use; the card fee too,
1001since a fee for paying for a sale follows the sale). Fields are valid for
1002`2025-02-24.acacia`.
1003
1004| Request | Tax | Card fee |
1005| --- | --- | --- |
1006| Checkout, plan or Security (`subscription_fields`) | `automatic_tax[enabled]`, `billing_address_collection=required`, `tax_id_collection[enabled]`, with a customer `customer_update[address]=auto` and `[name]=auto`; the subscription keeps automatic tax for every renewal | A second recurring line, *Card processing fee* |
1007| Subscription on a saved card (`saved_subscription_fields`) | `automatic_tax[enabled]`; a customer Stripe Tax cannot place fails, and the person is sent to Checkout, which asks for the address | `items[1]`, the `card_fee` product |
1008| Checkout, prepay (`prepay_fields`) | As above | By card only; none by bank transfer |
1009| Checkout, AI credit (`credit_fields`) | As above | Its own line |
1010| Checkout, card check (`card_check_fields`) | Setup mode: nothing charged, nothing taxed. `billing_address_collection=required`, and the card's address is copied onto a customer with none (`fill_address`) | — |
1011| Auto-reload (`charge_saved`) | `POST /tax/calculations` first (credit and fee as lines), the PaymentIntent for the total, then `POST /tax/transactions/create_from_calculation` with the PaymentIntent as reference; a refund reverses its share (`create_reversal`, `mode=partial`) | In the calculation and the amount |
1012| Workspace invoice, month close and threshold (`invoice_workspace`) | `automatic_tax[enabled]` on the draft; each item `tax_behavior`, `tax_code` | An item *Card processing fee*, when the default payment method is a card |
1013| Enterprise invoice (`invoice_enterprise`) | The same | Never |
1014| Products (`Stripe::product`) | Made with `tax_code`; one found without it is given it | The `card_fee` product, `metadata[g1t]=card_fee` |
1015
1016### What is kept
1017
1018A payment credits the balance with what it paid for, never its tax or fee:
1019prepay credits the page's `amount_subtotal` less the fee line
1020(`credit_prepayment`), a workspace invoice `amount_paid − tax − fee`
1021(`credit_invoice`), AI credit its credit amount, and `plan_payments` the
1022plan's invoice less its tax and *Card processing fee* lines
1023(`stripe::invoice_split`). Each payment's tax and fee are rows in
1024`tax_and_fees` (`<reference>/tax`, `<reference>/card_fee`; the enterprise's
1025account id in `workspace` for its invoices), with the PaymentIntent, so a
1026refund (`charge.refunded`) gives back the balance, tax and fee in
1027proportion (`tax::refund_split`, negative rows under `refund/<charge>/…`).
1028`workspace_invoices.fee_micros` and `tax_micros` sit beside each usage
1029invoice.
1030
1031- **Statement.** *Tax* and *Card processing fees* are their own lines per
1032 day (`StatementLine.passed_micros`), never in `charged_micros`; totals
1033 `tax_micros`, `card_fee_micros`. The CSV has a row a day for each.
1034- **Margin.** Cash never holds them, so margin is untouched. sudo → Costs
1035 shows **Tax collected** and **Card fees passed on** for the range
1036 (`OverallMargin.tax_collected_micros`, `card_fees_micros`). Tax is owed to
1037 the authorities: file it from Stripe Tax's reports, never from g1t's.
1038
1039### No address
1040
1041Stripe Tax needs a country (in the US a ZIP code, in Canada a postal code
1042or province: `stripe::address_places_customer`). Before a workspace
1043invoice is drafted, g1t checks the customer; without an address, or when
1044Stripe leaves the draft at `requires_location_inputs` or refuses with
1045`customer_tax_location_invalid`, nothing is charged:
1046`accounts.tax_address_needed_at` is set, the owners are emailed once
1047(`notify_owners`), and Billing shows **Add a billing address**. Saving
1048Invoice details with an address Stripe Tax can use clears it, as does a
1049charge that goes through. Work is not stopped for it; the limits still
1050apply. Auto-reload without an address fails like a declined card (turned
1051off, owners told). An enterprise's invoice is not sent without an address:
1052sudo → the enterprise → Invoices → **Billing address** (`admin_enterprise_address`,
1053with its tax ID; audited `billing_address`).
1054
1055### The card fee
1056
1057`card_fee_cents` grosses Stripe's fee up so the amount paid for is left
1058after it: `(amount + 30¢) / (1 − 2.9%)`, rounded up; $0.91 on $20, $1.06
1059on $25. It is worked out on the amount before tax, so Stripe's fee on the
1060tax itself (a few cents) is g1t's. It is shown before paying: the plan card
1061and pricing page (`Plan.card_fee_cents`), AI credit (*Card processing fee
1062$1.06, plus tax where it applies*), Prepay. Never on a bank transfer or an
1063enterprise's (`send_invoice`) invoice. Meters stay at cost + 20% and models
1064at the provider's price plus the agent rate (price versions in migration
10650040); the fee is passed through, not margin.
1066
1067### Tax-exempt customers and tax IDs
1068
1069g1t never sets `tax_exempt`. For a customer who sends an exemption
1070certificate, set it in Stripe's dashboard (Customers → the customer → Tax
1071status: Exempt, or Reverse charge); Billing then says so. Tax IDs come
1072from Checkout (`tax_id_collection`) or Invoice details (`set_billing_details`,
1073validated against Stripe's types in `details::TAX_ID_TYPES`); Stripe checks
1074EU VAT numbers and Stripe Tax applies a reverse charge where it should.
1075Billing shows Stripe's `verification.status`.
1076
1077### In Stripe's dashboard (not done by g1t)
1078
10791. **Settings → Tax → Get started**: turn on Stripe Tax in live and test
1080 mode.
10812. **Origin address**: Flagon, Inc.'s head office address.
10823. **Default tax code**: Software as a service, business use
1083 (`txcd_10103001`); **default tax behavior**: exclusive.
10844. **Registrations**: add each jurisdiction where Flagon is registered to
1085 collect (its home state at least; then states as thresholds are
1086 crossed, which Stripe Tax's monitoring flags; the EU's OSS and the UK
1087 if selling there). Stripe collects only where a registration exists.
10885. **Customer portal**: tax ID and address updates are already allowed
1089 (`portal_configuration`).
10906. Refund an invoice through a **credit note**, so its tax is reversed in
1091 Stripe Tax.
1092
1093## Free workspaces
1094
1095Owner decision 2026-10-08: one free workspace per person, and a free
1096workspace adds no one. Billing answers one question, `free_workspaces`
1097(`credits.rs`): which of the given workspaces are on no paid plan
1098(`plan_kind_for` is `Free`). The plan, an enterprise's terms and a 100%
1099discount (flagon-io) count as paid; with payments off nothing is free.
1100Identity asks it (`services/identity/src/paid.rs`) and refuses with
1101`payment_required`:
1102
1103| Where | Refused when |
1104| --- | --- |
1105| `create_workspace` | The person owns any free workspace. Several from before are kept (grandfathered); none can be added until each is paid for or deleted. |
1106| `add_member`, `invite_member` | The workspace is free. |
1107| `add_collaborator` | Someone outside a free workspace (a username who is not a member, or an address). Members' roles are fine. |
1108| `accept_invite`, `respond_repo_invitation` | The invite's workspace (or repository's) is free now: it waits. A sign-up with such an invite makes the account without joining. |
1109
1110`@g1t` is never counted as someone added. If billing cannot be asked, the
1111change is refused for now ("try again"), never let through. The site says
1112so first (New workspace, People, a repository's Access, from the same RPC);
1113the API and MCP pass identity's refusal on as `402`.
1114
1115## The Security and quality activation
1116
1117A second monthly subscription a workspace can hold beside the plan
1118(`Feature::Security`, `feature = 'security'` in `subscriptions`). It turns
1119on the security suite's paid features for the workspace's private
1120repositories: custom secret patterns, validity checks, delegated bypass,
1121code scanning, dependency review and the security overview. Public
1122repositories have them free; secret scanning, push protection,
1123vulnerability alerts and security updates are free everywhere.
1124
1125- **Price.** The price book's `security_activation` meter (unit
1126 `workspace-month`, `source` `list`, markup 0): 10,000,000 micros, $10,
1127 from migration `0037_security_activation.sql` with its first
1128 `price_versions` row and a public `price_changes` record. `plan()` and the
1129 `prices` RPC read it (`features::security_plan_at`); if the price book
1130 cannot be read, $10. Nothing in the web app hard-codes it. A change is a
1131 new price version, like any other: noticed on the pricing page and
1132 applied from its `effective_at` to new subscriptions. Subscriptions
1133 already running keep the amount Stripe has until they are changed in
1134 Stripe.
1135- **Stripe.** Its own subscription and its own product, tagged
1136 `metadata[g1t]=security` (the plan's is `plan`). Started from the Billing
1137 page with `subscribe` (`feature: security`) on the checked card, or
1138 through Checkout; ended with `cancel_subscription` (`feature: security`)
1139 at the period's end. Its invoices count in `plan_payments` like the
1140 plan's, as paid revenue.
1141- **Who has it.** `has_feature(workspace, security)`: on with an active
1142 subscription, with comped terms or as an enterprise's workspace, or when
1143 Stripe is not configured. The plan's allowance (`allowances.plan`) does
1144 not include it. Refusals are `PaymentRequired` with the price from the
1145 price book and the Billing page's address.
1146- **Where it is checked.** The security service, on each paid call for a
1147 private repository (`suite::entitled`) and before using custom patterns
1148 in a push (`patterns_for`). When billing cannot be reached it is taken
1149 as off: a paid feature waits rather than running unpaid.
1150- **Fixes.** "Fix with g1t" runs g1t's agent, charged as agent usage, never
1151 to the activation.
1152- **Sales figures.** MRR in sudo counts `feature = 'plan'` only; the
1153 activation's subscriptions are not in it yet.