Skip to content
1,271 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1# 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.
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)5Code: `services/billing/src/costs.rs` (reading the bill), `cycle.rs`
6(Cloudflare's billing cycle, list prices and included amounts), `margin.rs`
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily7(reconciliation, drift, alerts), `pricing.rs` (versions, proposals,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays8notice), `keeper.rs` (sandbox and Workers for Platforms measurements),
9`budget.rs` (what g1t pays for itself, and its caps; see
10[Spend caps](#spend-caps)).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily11Page: sudo **Costs & margin** (`/costs`).
12
13Several Cloudflare products g1t runs on are new. Artifacts bills
14"operations" from 2026-10-14 without defining them (see
15[ARTIFACTS.md](ARTIFACTS.md), M1). So nothing here hard-codes a product
16list or a unit: every line Cloudflare bills is kept, and how a line maps
17to what g1t sells is data you change from sudo, without a deploy.
18
19## Data sources
20
21| Source | What | Where it lands |
22| --- | --- | --- |
Merge remote-tracking branch 'origin/main' into workspace-chat23| Billable usage, `GET /accounts/{account}/billable-usage?from=&to=` | One row per service per day in FOCUS columns: `ServiceFamilyName`, `ServiceName`, `ChargePeriodStart`, `ConsumedQuantity` (else `PricingQuantity`), `ContractedCost` / `BilledCost` / `EffectiveCost`. Every page is read (`result_info`: `cursor`, else `total_pages`), over whole billing cycles (see [The billing cycle](#the-billing-cycle)). Every product g1t uses appears here 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. While a cycle is open its rows carry no cost; `ListCost` is never taken as a cost, since it is before the included amounts (the keeper reads it for a unit's rate, below). | `cost_lines`, source `billable_usage`: `quantity` (consumed), `billed_usd` (Cloudflare's own cost), `billable_quantity` and `cost_usd` (over the cycle), `basis`; the read itself in `cost_reads` |
Costs: Cloudflare's count for a pull request's working copy is shared out to its repository's workspace (repos pull_owners)24| 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` |
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises25| 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. |
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily26| 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 |
27| `pending_usage` | Month-end meters (git, storage, scans, embeddings, the cache) as they stand. | snapshotted daily into `pending_days` |
28| `plan_payments` | The plan's $20. | read |
One operation mapping, owned by repos; billing reads it instead of keeping its own29| repos `git_operations` | Operations customers are charged for, per workspace, counted by repos through its `operation_mapping`. | `own_counts` meter `git_operations` |
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)30| 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; and `current_period_start` of the monthly one, the day every billing cycle starts. Not on the billable-usage bill. Read first in the daily run, with the bill's token; a failure is logged and the last read stays. | `cf_subscriptions` (one row; `cycle_start`) |
One operation mapping, owned by repos; billing reads it instead of keeping its own31| 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) |
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily32
33A meter's slug is Cloudflare's name lower-cased with words joined by `_`
34and the "(First … included)" note dropped: `Workers for Platforms CPU ms
35(First 60M ms are included)` under `Workers` is product `workers`, meter
36`workers_for_platforms_cpu_ms`. Several rows of the same day and meter
37(regions, tiers) are added together before they are stored.
38
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)39## The billing cycle
40
41Cloudflare bills usage per billing cycle: a month from the day the
42account's subscription renews. g1t's account renews on the 28th, so a cycle
43runs from the 28th to the 27th (Sep 28 to Oct 27, 2026). Each meter's
44included amount ("first 30M are included") is the account's, once a cycle:
45not a day's, not a calendar month's, and not per product. Code:
46`services/billing/src/cycle.rs`.
47
48- **When a cycle starts**: `current_period_start` of the monthly
49 subscription, from the daily read of the subscriptions
50 (`cf_subscriptions.cycle_start`). Until that has worked,
51 `CLOUDFLARE_BILLING_DAY` on g1t-billing (`28`), else the 1st.
52- **What is read**: the whole current cycle, every run, and the cycle
53 before while it is in its first 4 days (Cloudflare posts a day a day or
54 two late and restates recent days) or when nothing has been read yet.
55 Every page of the answer. The days read replace what was kept for them.
56- **What a meter costs**: where Cloudflare put a cost on any of a meter's
57 lines in the cycle, that cost, line by line (`basis` `cloudflare`).
58 Otherwise the list price past the included amount (`basis` `list`), on
59 the days the cycle's running total passes it: a cycle that crosses the
60 included amount on the 7th has all of its cost on the 7th and after, as
61 on Cloudflare's Billable usage page. Meters priced per million
62 (requests, CPU ms, rows, operations) are billed in whole millions:
63 9.16M CPU ms past the included 30M is 10M, $0.20. A meter with no list
64 price is costed at $0 (`basis` `none`), and sudo lists it.
65- **List prices** (`cycle::LIST_PRICES`), each with its included amount
66 per cycle:
67
68 | Meter | Included | Price |
69 | --- | --- | --- |
70 | Workers standard requests | 10M | $0.30 per million |
71 | Workers CPU ms | 30M | $0.02 per million |
72 | Workers for Platforms requests | 20M | $0.30 per million |
73 | Workers for Platforms CPU ms | 60M | $0.02 per million |
74 | Workers for Platforms scripts | 1,000 | $0.02 each |
75 | Workers Logs events | 20M | $0.60 per million |
76 | D1 rows read / written | 25B / 50M | $0.001 / $1.00 per million |
77 | D1 storage | 5 GB-month | $0.75 per GB-month |
78 | KV reads / writes, lists, deletes | 10M / 1M each | $0.50 / $5.00 per million |
79 | KV storage | 1 GB | $0.50 per GB-month |
80 | R2 Class A / Class B operations | 1M / 10M | $4.50 / $0.36 per million |
81 | Durable Objects requests | 1M | $0.15 per million |
82 | Durable Objects duration | 400,000 GB-s | $12.50 per million GB-s |
83 | Durable Objects SQL rows read / written | 25B / 50M | $0.001 / $1.00 per million |
84 | Durable Objects SQL storage | 5 GB-month | $0.20 per GB-month |
85 | Queues operations | 1M | $0.40 per million |
86 | Containers memory | 25 GiB-hours (90,000 GiB-s) | $0.0000025 per GiB-second |
87 | Containers vCPU | 375 vCPU-minutes (22,500 vCPU-s) | $0.000020 per vCPU-second |
88 | Containers disk | 200 GB-hours (720,000 GB-s) | $0.00000007 per GB-second |
89 | Containers egress, North America and Europe / elsewhere | 1 TB / 500 GB | $0.025 / $0.04 per GB |
90 | Vectorize queried / stored dimensions | 50M / 10M | $0.01 per million / $0.05 per 100 million |
91 | Workers AI neurons | 10,000 a day | $0.011 per 1,000 |
92
93 Not priced yet, so $0 until Cloudflare's lines carry a cost: Artifacts
94 (billed from 2026-10-14), Email Service, Browser Rendering, R2 storage,
95 Cloudflare for SaaS. When Cloudflare changes a price or an included
96 amount, change the table and its test in the same pull request.
97- **Projection**: the cycle's cost so far over the days elapsed, times the
98 cycle's days, as Cloudflare's page projects it ($0.29 over 12 days of a
99 30-day cycle is $0.73).
100- **Subscriptions** (Workers Paid and Workers for Platforms, $30 a month)
101 accrue day by day: each day is its cycle's share of the month's price.
102 The statement's range and g1t's own spend (the calendar month so far) both
103 add up the same days, so they never disagree about a day.
104
105Sudo's Bill & pricing page opens with **This billing cycle**: each meter's
106use, what the cycle includes, what is past it and its cost, the total so
107far, the average day, the projection and the subscriptions. Under it, what
108the last read got back (`cost_reads`): rows, pages, how many rows had a
109consumed quantity and how many only a pricing quantity, and how many carried
110Cloudflare's own cost.
111
112To check the figures against Cloudflare: in the dashboard, Billing →
113Billable usage, for the same cycle. The total so far, the projection and
114each meter's total and billable quantity should agree within a cent. If
115they do not, read the answer itself with a token with Billing Read:
116`GET https://api.cloudflare.com/client/v4/accounts/{account}/billable-usage?from=<cycle start>&to=<today>`,
117and compare, for `Workers CPU ms` and `Container Memory`, the
118`ConsumedQuantity`, `PricingQuantity`, `PricingUnit` and the cost columns,
119and `result_info` for more pages.
120
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily121## Credentials
122
123| Secret on g1t-billing | Permissions | Used for |
124| --- | --- | --- |
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing125| `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 |
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises126| `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 |
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily127
128With neither, the daily run reconciles only what g1t counted itself, and
129the page says the bill cannot be read. Nothing fails. To set the scoped one:
130
1311. Cloudflare dashboard → My Profile → API Tokens → Create Token → Custom token.
1322. Permissions: Account · Billing · Read; Account · Account Analytics · Read.
1333. Account resources: Include · the g1t account. No zone permissions.
1344. `cd services/billing && npx wrangler secret put CLOUDFLARE_BILLING_TOKEN`.
sudo: the costs run button is named for what it does, the whole nightly analysis1355. In sudo, Costs & margin → **Run the analysis now**.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily136
137Alerts are emailed through the `EMAIL` binding (Cloudflare Email Sending)
138to `COSTS_ALERT_EMAIL` (`hey@flagon.io`). An empty value sends none.
139
140## Schedule
141
142The daily cron (`17 4 * * *`, `keeper::DAILY`) runs, in order:
143
1441. The keeper's measurements (sandbox seconds, app requests and CPU), each
145 a proposal now, not a direct change.
1462. `costs_daily`:
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)147 1. Read Cloudflare's subscriptions (and with them when the billing cycle
148 starts), then the bill, the Artifacts events and AI Gateway's
149 analytics. The bill is read over whole billing cycles, every page
150 (see [The billing cycle](#the-billing-cycle)), and priced over each
151 cycle; its days replace what was kept for them. The Artifacts events
152 and the gateway: the first run reads the last 31 days (GraphQL keeps
153 31); later runs the last 4, since Cloudflare restates recent days, or
154 back to the last day read after a gap. Lines are upserted on
155 `(day, source, product, meter)`, so a re-read replaces, never adds.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily156 2. g1t's own counts for the same days (replaced per day).
157 3. Snapshot `pending_usage` into `pending_days`.
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)158 4. Reconcile the last 31 days (further back after a gap, or to the
159 first day of the bill's read) into
sudo: the costs run button is named for what it does, the whole nightly analysis160 `margin_days` and `workspace_costs`
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily161 (replaced per day).
162 5. Drift over the last 7 days into `cost_drift`.
163 6. Unit costs over the last 30 days, proposed to the price book.
164 7. Apply price versions whose date has come.
165 8. Open, update and close margin alerts; email new ones.
166 9. Email owners on the plan about rises to come.
167
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing168**Run the analysis now** at the top of sudo's Costs & margin and Bill & pricing pages
sudo: the costs run button is named for what it does, the whole nightly analysis169(`admin_run_costs`) runs all of step 2 at once, alerts included, with no
170need to wait for 04:17 UTC. Running it twice is safe: every step replaces
171what it wrote.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily172
173## Reconciliation math
174
175Every Cloudflare line goes to one of g1t's products ("buckets") by
176`cost_map`: the row for its product with the longest matching meter
177prefix, `*` last. A line no row claims goes to `unmapped`.
178
179| Bucket | Cloudflare | Paid for by (`revenue_map`) |
180| --- | --- | --- |
181| `sandboxes` | Containers, Durable Objects compute duration | `sandbox`, `self_hosted`, `builds` |
182| `deployments` | Workers for Platforms | `deployments` |
183| `git` | Artifacts operations (and its events, as counts) | `git` |
184| `repo_storage` | Artifacts storage | `storage` |
185| `actions_cache` | R2 | `cache` |
186| `embeddings` | Workers AI, Vectorize | `context` |
187| `security` | (Workers CPU, under `platform`) | `security` |
188| `domains` | Cloudflare for SaaS | `domains` |
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises189| `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 |
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily190| `platform` | Workers, D1, KV, Queues, Email, Browser Rendering, other Durable Objects | the plan's price |
191
192For each day and bucket:
193
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)194- **Cloudflare cost** = Σ the bucket's lines' cost, after the included
195 amounts over the billing cycle (see [The billing cycle](#the-billing-cycle)),
196 so a cycle inside them costs $0 here as on Cloudflare's Billable usage
197 page, and the day its total passes one carries the cost. `models` uses the ledger's cost of the
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running198 tokens instead; that is paid to the model providers and is not on
Merge branch 'worktree-agent-a633ac0f7f66d419d'199 Cloudflare's bill. Its "Cloudflare" column is what AI Gateway priced the
200 same traffic at, which drift compares with the ledger (below); it is
201 never added to the cost.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily202- **Own cost** = Σ the ledger's `cost_micros` for the bucket's keys (the
203 price book's cost when charged), plus month-end deltas. A workspace's own
204 model provider is no cost to g1t.
205- **Value** = what customers were charged at price: `-amount_micros` plus
206 what the plan's included usage, a trial, the open-source pool or g1t paid.
207 g1t's own (comped) workspaces are valued at cost plus the margin.
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)208- **Cash** = what workspaces paid with real money: `-amount_micros`, and
209 the plan's price, from the day payments went live (see "charged without
210 real money" below). Never tax or card fees: a payment credits the
211 balance, and `plan_payments`, without them (see [Tax and the card fee](#tax-and-the-card-fee)).
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it212- **Given away** = the part of the cost that went on usage g1t paid for
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running213 itself on purpose, by why:
214 - **comped**: all of a comped workspace's cost, every bucket;
215 - **free use**: a free period's usage, the overruns g1t covered
216 (`ledger.given_micros`), and all of a workspace's cost on a day it had
217 nothing priced (free allowances);
218 - **trial** and **open-source pool**: what `trial_micros` and
Merge branch 'worktree-agent-a633ac0f7f66d419d'219 `oss_micros` paid;
220 - **discount**: what a discount on an account's custom terms took below
221 cost plus the margin (`ledger.discount_micros`, see
222 [Margin floor](#margin-floor)). The usage is valued at its price, so a
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging223 discounted sale never reads as margin lost;
224 - **promotional credit** and **goodwill credit**: what credit staff gave
225 paid for, when it is spent (`given_credit_promotional_micros`,
226 `given_credit_goodwill_micros`, migration 0038). That usage's charge is
227 taken out of cash, so it is never money in. A refund is not here: see
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97228 [Credits from g1t](#credits-from-g1t);
229 - **testing resets**: what a workspace's usage cost g1t before staff
230 reset its billing (`reset_costs`, `given_reset_micros`, migration
231 0046). The model calls and Cloudflare usage still happened, so the
232 reconciliation reads the kept rows back as that workspace's usage on
233 their days: valued as before, no cash, all of it given. See
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)234 [Resetting a test workspace](#resetting-a-test-workspace);
235 - **charged without real money**: what workspaces were charged, and
236 plans paid, while payments were not live (Stripe's test mode), or
237 before the day they went live (`cost_settings.payments_live_since`,
238 kept the first time the run sees live payments). It brought in no
239 money, so it is taken out of cash and given (`given_unpaid_micros`,
240 migration 0052): never money in, never margin, never what a workspace
241 paid. The plan's included usage counts as money in only from that day
242 too.
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running243
244 Otherwise a workspace's day is split by those shares of its value at
245 price, and the same shares of each of its buckets' cost are given, its
246 part of running g1t included. The Team plan's included usage is sold:
247 the plan's price paid for it. Stored on `margin_days` (`given_micros`
Merge branch 'worktree-agent-a633ac0f7f66d419d'248 and `given_<why>_micros`, `given_discount_micros` from migration 0036)
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97249 and `workspace_costs` (`given_micros`). Sudo's Costs & margin page lists
250 each why by name, testing resets included (`givenResetMicros`).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily251- **Month-end meters**: a day's figure is that day's `pending_days`
252 snapshot less the day before's, within a month. Their month-end ledger
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises253 entries are left out, so nothing is counted twice. On a 100%-discount
254 workspace the snapshot's charge is valued and given (comped), never cash:
255 the month's close takes all of it off (`margin::comped_meter`).
256- **Product margin** = (value − cost) / value: a share of the price, so
257 cost plus 20% is a 16.7% margin. Sudo labels every margin "of price".
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running258- **Sudo's statement** keeps apart:
259 - **Usage sold**: cash for usage against the cost of the usage buckets
260 less what was given. Its margin is the headline; at cost plus 20% it
261 sits near 16.7%.
262 - **Running g1t**: the plan's price against `platform` less its given
263 share.
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)264 - **Cloudflare subscriptions**: what Cloudflare lists, a month
265 (`cf_subscriptions`), accrued over the range day by day as each day's
266 share of its billing cycle (`OverallMargin.subscriptions_micros`);
267 until a read has worked, `CLOUDFLARE_FIXED_MONTHLY_MICROS`, an
268 estimate.
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running269 - **Not mapped**: billed, charged for by nothing.
270 - **Given away**: by why. A budget, watched under g1t's own spend, never
271 shown as a loss.
272 - **All in**: money in against all of it, with the figure without what
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises273 was given beside it. **Who g1t paid** is All in's cost, split into
274 Cloudflare's usage, Cloudflare's subscriptions over the range, and the
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)275 model providers (the ledger's cost); where AI Gateway priced g1t's own
276 provider traffic at more than a cent apart from that
277 (`OverallMargin.gateway_cost_micros`, Cloudflare-billed requests left
278 out), it says so beside it, and the `models` drift says why.
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running279
280 The overall alert is (Σ cash − (Σ cost − Σ given)) / Σ cash.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily281- **Quantities**: where a mapping names an `own_meter`, Cloudflare's
282 billed quantity of those lines (or, without one, Artifacts' operation
283 events) against g1t's own count.
284
285**Shared costs to workspaces.** A bucket's cost is shared in proportion
286to, first available: Cloudflare's own per-workspace count
287(`cloudflare_<bucket>`, today the Artifacts events by repository), g1t's
Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running288own count, what its usage cost (so free use carries its own cost), what
289each was charged for it. `platform`
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily290and `unmapped` are shared by each workspace's share of all usage that
291day. Shares are whole micros that add up to the bill exactly (largest
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)292remainder). On a day no workspace used anything, running g1t has no one to
293share it: it stays no one's, and sudo says how much under **Workspaces that
294cost most** (`CostsReport.unattributed_micros`), so the workspaces' costs
295and it add up to the statement's cost.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily296
297## Drift (last 7 days)
298
299| Kind | When | What to do |
300| --- | --- | --- |
One operation mapping, owned by repos; billing reads it instead of keeping its own301| 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). |
Merge remote-tracking branch 'origin/main' into workspace-chat302| Cost | The price book's cost of a product's usage over the 7 days (the ledger's `cost_micros`) is more than `drift_percent` away from what the same usage comes to at Cloudflare's list prices before the included amounts, with at least `min_daily_cost` either side. The list cost is each billable-usage line mapped to the product, its whole quantity at `cycle::LIST_PRICES` (not rounded to whole millions; `margin::list_costs`). Never what Cloudflare billed: that is net of the cycle's included amounts while the price book costs every unit, so a product whose usage mostly fits in them (sandboxes inside 25 GiB-hours of Containers memory) would read as a stale price when nothing changed; what was billed is still the cost in the margins, and a leak. A product with usage on a meter that has no list price (Artifacts, Cloudflare for SaaS, R2 storage until priced) is not checked, since its usage cannot be priced like for like; add the price to `cycle::LIST_PRICES`. `cost_drift` is replaced every run and an alert whose drift is gone is resolved on the same run, so an alert raised by the old comparison (what was billed against the price book) closes on the next | A price is stale: check the proposals, and `cycle::LIST_PRICES` against Cloudflare's pricing page. |
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises303| 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. |
Merge branch 'worktree-agent-a633ac0f7f66d419d'304| 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. |
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily305| 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`). |
306
307## Prices: versions, proposals, notice
308
309- `price_versions` holds every price ever, never edited. `prices` is the
310 version in force. The daily run applies a version once its
311 `effective_at` has come, and adds the public `price_changes` record.
312 Ledger entries made from the price book carry `price_version` (the
313 version ids, comma-separated), so a past statement is always explained
314 by the prices of its day.
315- Proposals come from the keeper (sandbox seconds, app requests and CPU)
316 and the reconciler (mappings with `scale_to_own`: today git operations).
317 For git operations: Cloudflare's rate per its own operation (the median
Merge remote-tracking branch 'origin/main' into workspace-chat318 over costed days of cost ÷ quantity) × (Cloudflare's operations ÷ g1t's)
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily319 × 1,000. If Cloudflare counts three for each one g1t counts, the per-1,000
320 price triples. At least 1,000 of g1t's operations are needed.
Merge remote-tracking branch 'origin/main' into workspace-chat321- A rate is always a price per unit before the included amounts, like the
322 price book's: never what was billed over all of the quantity, which is
323 net of them and reads as a cheaper unit. The reconciler takes a line's
324 cost at `cycle::LIST_PRICES` where its meter has one (`margin::rate_line`),
325 else Cloudflare's own cost (the median leaves out the day an included
326 amount ran out). The keeper takes the bill's `ListCost` ÷ quantity
327 (`keeper::billed_rate`), and the published rates where there is none.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily328- Decision (`pricing::decide`): under 2% is noise; more than 4× either way
329 is suspect and waits for staff; within `auto_apply_percent` (25%) it is
330 applied on its own when `auto_apply` is on; anything else waits.
331- Notice: a fall applies at once. A rise applies `notice_days` (14) after
332 the decision, and for a monthly meter (git, storage, cache, domains,
333 embeddings, scans) at the start of the month after that, so no month
334 is charged at two prices. Owners of workspaces on the plan are emailed
335 once per rise (`price_notices`), and the pricing page lists it with
336 "takes effect". Rises are never retroactive; margin protection is for
337 new usage once notice has run.
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises338- A newer measurement before a rise's date replaces its version, with
339 the new rise's own full notice; the proposal behind the replaced version
340 is marked `superseded` (it never took effect, and nothing was charged at
341 it). Sudo says "applied by guardrail, then replaced by a later
342 measurement". Migration 0049 marks the ones from before: sandbox and
343 build seconds' rises of 2026-10-07, replaced on 10-08.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily344- Staff approve or reject in sudo. A rejection needs a note.
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises345- **Price versions** in sudo show each version's cost and price, except
346 where the cost column is not a cost (`PriceVersion.basis`,
347 `pricing::basis_of`): the agent rate and security activation are rates
348 g1t sets (`rate`, no cost shown), and the agent rate's token weights are
349 multipliers (`weight`, shown as ×0.1). Dates show with their time: a
350 version from 00:00 UTC is the evening before in the Americas.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily351
352## Changing a mapping
353
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing354In sudo, Costs & margin → Bill & pricing → **Mappings**: Cloudflare's product and meter
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily355prefix (as **Cloudflare's lines** lists them; `*` for the rest of the
356product), g1t's product, and optionally:
357
358- **Price meter**: the price book meter the line measures.
359- **Own meter**: g1t's count of the same units (`own_counts.meter`).
360- **Scale to g1t's count**: price one of g1t's units at as many of
361 Cloudflare's as it took (proposals as above).
362- **Drift threshold**.
363
sudo: the costs run button is named for what it does, the whole nightly analysis364It applies from the next run; **Run the analysis now** applies it at once.
One operation mapping, owned by repos; billing reads it instead of keeping its own365Every change is in the audit log (`cost_mapping`).
366
367## Which raw meters are operations
368
369There is one mapping, and the repos service owns it: `operation_mapping`
370in g1t-repos' database, one row per raw meter with `cost_operations` (how
371many operations Cloudflare bills for it) and `billable_operations` (how
372many the customer is charged for). Change it with repos'
373`set_operation_mapping` RPC (services only), or
374`npx wrangler d1 execute g1t-repos --remote` until sudo has a form. A
375change applies to counts from then on, never to what was counted.
376Billing keeps no mapping of its own: it reads repos' `git_operations`
377(already mapped) for what customers are charged, and `artifacts_usage`
378(raw counts with the mapping) for `cost_operations`. Migration 0023 drops
379the `billable_units` table 0022 made for this, which was never written.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily380
Models' margin read -14%: usage nothing paid for is valued at price, not $0381**Charged at price** (a product's value) is what each day's usage was paid:
382charged to a card or credit, or drawn from the plan's included usage, a
383trial, a pool or a gift. Usage nothing paid for, as in a free period, is
384valued at price (cost plus the margin), since it was given away at its price
385rather than sold for nothing; so are g1t's own workspaces. Runs on a
386workspace's own model provider have no cost to g1t. Every daily run
387reconciles the whole 31-day window from what is already kept, so a change in
388how a day is valued reaches every day sudo shows.
389
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily390## Alerts runbook
391
392| Alert | Raised when | First steps |
393| --- | --- | --- |
394| 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. |
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it395| 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. |
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily396| Leak | Drift of kind leak | Map the meter, or decide it is overhead. |
397| Drift | Count drift | See Drift above. Cloudflare's definitions change in beta: ask them in writing ([ARTIFACTS.md](ARTIFACTS.md), §7). |
Margin alerts measure what is sold, and say dollars when a percentage would mislead398| 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. |
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily399
400Alerts close on their own when the condition clears. Open ones are
401emailed again weekly. The red bar on every sudo page shows margin,
402overall and leak alerts.
403
Merge branch 'worktree-agent-a633ac0f7f66d419d'404## Model costs
405
406Every model call g1t pays for is an agent run's (the `claude` CLI in the
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens407sandbox, `crates/runner`) or a customer's AI Gateway request (a workspace
AI Gateway: OpenAI's format, open models, and your own providers408token with `models:write` at `models.g1t.sh/anthropic` or
409`models.g1t.sh/openai/v1`, `gateway.rs`, to Claude on Anthropic or to open
410models on Workers AI through the same AI Gateway); the only other model is
411Workers AI's embeddings for g1t's own search, which are on Cloudflare's bill
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens412(`embeddings`). How each reaches the ledger:
Merge branch 'worktree-agent-a633ac0f7f66d419d'413
414| Call | Who pays | Run and session | Ledger cost | Settled to the gateway |
415| --- | --- | --- | --- | --- |
416| 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 |
417| Agent run straight to the gateway (no `MODELS_URL`) | g1t | `runs` row; session `rs_…` in `cf-aig-metadata` (`services/runner` `gatewaySession`) | As above | Yes |
418| 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 |
Merge branch 'model-routing'419| 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 |
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97420| 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 |
AI Gateway: OpenAI's format, open models, and your own providers421| 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 |
Merge branch 'worktree-agent-a633ac0f7f66d419d'422| A sandbox that died before reporting | g1t | as its route | Charged from the gateway when settled | Yes |
423| Embeddings (indexing) | g1t | none (Workers AI) | Month-end `context` meter | No: Cloudflare's bill, `embeddings` bucket |
424| Embeddings (queries, search and agent context) | g1t | none | None: not charged, by design | No: in Cloudflare's `embeddings` line, shared out |
425
426**Settling.** A run's charge is corrected to what AI Gateway priced its
427session's requests at (`settled_cost` in `keeper.rs`). The gateway's
428figure is trusted in full: it is not held to the $100 cap on a sandbox's
429own report. It is never taken below what the sandbox reported when it
430cannot be the whole cost: a request with tokens and no cost (a model the
431gateway has no price for) or more logs than are read (2,000). Such a run
432keeps `runs.gateway_note`, its correction says why, and it raises the
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens433**Unpriced** drift. For agent runs g1t keeps no token rates of its own:
434the first figure is Claude Code's, the final one the gateway's. AI Gateway
Merge branch 'main' into actions-toolkit-oidc-artifacts435requests are the exception: they are charged from `gateway_models` (the
436model catalogue, below), which has to follow the provider's price list by
AI Gateway: OpenAI's format, open models, and your own providers437hand until they are settled like runs. It is not part of the price book's
Merge branch 'main' into actions-toolkit-oidc-artifacts438`price_versions`: a new model's prices are confirmed when staff approve it
439in sudo, and a changed price on a model already offered is a migration that
440updates the row and its `updated_at` (as 0047 did for Sonnet 5.5's cache
441reads). The `gateway_models` meter's markup is the only price-book number
442on it. Open
AI Gateway: OpenAI's format, open models, and your own providers443models need `WORKERS_AI_TOKEN` (a Cloudflare API token with Workers AI on
444g1t's account) on the model proxy; without it, and without
445`AI_GATEWAY_TOKEN` holding that permission, they are refused with `503`.
Merge branch 'worktree-agent-a633ac0f7f66d419d'446
447**The daily total.** AI Gateway's analytics for the day (above) against
448the ledger's model cost is the check that nothing slips past: a model call
449with no run, or a run never settled, shows as **Cost** drift on `models`.
450The gateway's per-request `cost` is its estimate from its own price list:
451it can be off for prompt-cache tokens, for requests Cloudflare bills
452itself, and for models it has no price for. The drift's detail says when
453any of those were in the window; the provider's invoice is the last word.
454
455### Margin floor
456
457A sold charge is cost × (1 + `MARGIN_PERCENT`), rounded up (`margin_on`;
458`charge_micros` for a sandbox's own report). Terms change it only as
459follows (`Terms::discounted`, `Billing::charged`):
460
461- **Standard**: charged in full.
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging462- **A 100% discount** (what was "comped"; see [Discounts](#discounts)),
463 `FREE_WHILE_BUILDING`, the plan's included usage, the trial,
Merge branch 'worktree-agent-a633ac0f7f66d419d'464 the open-source pool, and overruns g1t covers: given, and counted by why
465 (above).
466- **Custom, with a discount**: the discount comes off, and what it took
467 below cost plus the margin is written on the entry as
468 `ledger.discount_micros` and counted as given (**discount**), so the sale
469 is valued at its price and charged plus given is never under cost plus the
470 margin. On a settlement correction it moves with the charge (less than
471 nothing when the charge comes down).
472- **Goodwill credits** (overages) are separate, given by staff on purpose:
473 their margin part first, the cost only up to the cap, each audited.
474
475Every usage path goes through this: `finish_run`, settling, sandbox time,
476features and builds (`charge_feature`), and the month-end meters.
477
Merge branch 'main' into actions-toolkit-oidc-artifacts478## The model catalogue
479
480Every model g1t can use is one row of `gateway_models` (migrations 0045,
4810047 and `0048_model_catalogue.sql`; code in `catalogue.rs`): the agents'
482tiers, the AI Gateway's Claude and open models, and the embeddings model.
483Billing keeps it because billing owns prices: the gateway charges from the
484same row, so there is one price per model, not two that drift. Page: sudo
485**Agents & models** (`/agents`).
486
487| Column | What |
488| --- | --- |
489| `model`, `name`, `provider`, `kind` | The provider's id, the name people see, `anthropic` or `workers-ai`, `chat` or `embeddings` |
490| `aliases` | Other ids the provider lists it by, comma separated (a dated id such as `claude-haiku-4-5-20251001`) |
491| `family`, `tier_hint` | `haiku`, `sonnet`, `opus`, `fable` (or a Workers AI author); the agent tier it suits: `small`, `large`, `frontier` |
492| `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 |
493| 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 |
494| `status` | `available` (routed to and offered), `new` (found, not approved), `deprecated` (the provider stopped listing it), `retired` (staff took it out) |
495| `priced` | 1 when its prices are known |
496| `source`, `first_seen_at`, `last_seen_at`, `missing_since`, `approved_by`, `approved_at`, `note` | Where it came from (`discovered` or `staff`) and its history |
497
498What each status allows:
499
500| Status | Default for a purpose | Offered on the AI Gateway | Charged |
501| --- | --- | --- | --- |
502| `available`, priced | Yes | Yes | Yes |
503| `new`, or unpriced | No | No: refused before it reaches a provider | No |
504| `deprecated` | No: a default that chose it falls back | Yes, to whoever names it | Yes |
505| `retired` | No: a default that chose it falls back | No | No |
506
507### Discovery
508
509The models service lists each provider once a day (`29 5 * * *`, after
510billing's daily run) and whenever staff press **Check for new models**
511(`services/models/src/discover.ts`, through its `Discovery` entrypoint,
512which only sudo binds). Listing models is free; nothing calls a model.
513
514| Provider | How it is listed | Credentials (on g1t-models) |
515| --- | --- | --- |
516| 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` |
517| Workers AI | `GET /accounts/{account}/ai/models/search` | `WORKERS_AI_TOKEN` (Workers AI Read), else `AI_GATEWAY_TOKEN` |
518
519Each provider's list goes to billing's `record_discovery`, which compares
520it with the catalogue:
521
522- **An id it has never seen** is added as `new`. Chat and embeddings
523 models only. Anthropic models are priced from the price table in
524 `catalogue.rs` (`ANTHROPIC_PRICES`: Anthropic's list prices by model, a
525 dated id priced as its model) when it has them; Workers AI models from
526 the price their listing gives, with cached tokens at the input price.
527 Anything else is added unpriced. Its family and tier hint come from its
528 id (`claude-haiku-*` is fast, `claude-sonnet-*` standard,
529 `claude-opus-*` and `claude-fable-*` most capable).
530- **A dated id of a model it has** (`claude-sonnet-5-5-20261001`) is that
531 model: the id is added to its aliases.
532- **A model it has that is listed** gets `last_seen_at`, and its context
533 window, output limit and capabilities from the list.
534- **A model it has that is not listed** (available or new) becomes
535 `deprecated`, with `missing_since`. Listed again, it goes back to
536 `available` if it was ever approved, else to `new`.
537- **An empty list or a failed one** changes nothing: it is recorded as a
538 failed check with the provider's answer (never a key).
539
540Every check is kept 90 days in `model_checks` and shown on the page. When a
541check adds, deprecates or restores anything, it is in the audit log
542(`models_discovered`, as `schedule` or the staff member) and staff are
543emailed at `COSTS_ALERT_EMAIL` with a link to Agents & models.
544
545When Anthropic publishes a new model's price, add a row to
546`ANTHROPIC_PRICES`, so the next one of its kind arrives priced. Until then
547staff enter the prices when they approve it.
548
549### Approving, retiring and restoring
550
551On Agents & models, **New models** lists every `new` model with its prices
552filled in where they are known. To approve one:
553
5541. Check the name people see and the tier it suits.
5552. Check or enter its prices per million tokens against the provider's
556 price page: input, output, cache read, cache writes (five-minute and
557 hour-long; an hour-long price left empty is the five-minute one), and
558 under **Priced by prompt length** the threshold and the prices above it.
5593. Say why (for example where the prices came from), and **Approve**
560 (`admin_decide_model`, `approve`).
561
562It is `available` at once: the AI Gateway offers it within five minutes
563(the proxy keeps the catalogue that long) and it can be chosen as a
564default. **Retire** takes a model out of routing and the gateway; any
565default that chose it falls back to the next suitable model until staff
566choose another, and the audit line names those defaults. **Restore** puts
567a retired or deprecated model back: `available` if it was ever approved,
568else `new`. Each needs a reason and is in the audit log (`model_approved`,
569`model_retired`, `model_restored`).
570
571### Defaults
572
573`model_defaults` holds staff's choice per purpose, each with when, who and
574why (`admin_set_model_default`; the audit log, `model_default`, has the old
575value, the new one and why):
576
577| Purpose | What it chooses | Read by |
578| --- | --- | --- |
579| `tier_small`, `tier_large`, `tier_frontier` | The model behind Auto's fast, standard and most capable tiers | The runner |
580| `background` | The harness's own small tasks in every run on g1t's tiers (`ANTHROPIC_SMALL_FAST_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`) | The runner |
581| `gateway_first` | The Claude listed first by `GET /openai/v1/models` | Billing's `gateway_models` |
582| `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 |
583
584A model purpose takes only an available, priced Claude chat model (the
585harness speaks Anthropic's API). The migration seeded each with what
586`AGENT_ROUTING` had.
587
588Before a default is saved, sudo shows it beside the current one with what
589a **typical run** would cost on each: 40 requests of 2,000 input, 1,500
590output, 45,000 cache-read and 4,000 cache-write tokens, each request priced
591on its own at the catalogue's prices (`catalogue::TYPICAL`; on 2026-10-08,
592$0.076 on Claude Haiku 5.5, $1.34 on Sonnet 5.5, $2.68 on Opus 5.5). It is
593an estimate for comparing models, never a charge.
594
595**How the runner reads them.** `model_defaults` (the RPC) returns each
596purpose's model as it applies now, with its prices and capabilities, and
597each job's tier and effort. The runner reads it at most once a minute per
598isolate and puts it over `AGENT_ROUTING` (`withDefaults` in
599`services/runner/src/model-env.ts`), so a change reaches runs within a
600minute. When billing cannot be read, the runner uses `AGENT_ROUTING` (and
601`DEFAULT_ROUTING` under it) alone and asks again ten seconds later. The
602labels, change sizes, `frontierAfter` and learning always come from
603`AGENT_ROUTING`. Effort is not sent on a model whose catalogue entry lacks
604`effort` (Claude Haiku 4.5).
605
606**Never a retired model.** A chosen model that is deprecated, retired or
607unpriced is not handed out: the purpose falls back to the first available,
608priced Claude with the same tier hint (any Claude for `gateway_first`), in
609the catalogue's order, with a sentence such as *Claude Haiku 5.5 is
610retired; using Claude Haiku 4.5 instead.* The runner adds that sentence to
611the run's reason line, and sudo shows it beside the default. With no model
612left for a purpose, the runner keeps `AGENT_ROUTING`'s.
613
614**Not a default:** the context hub's embeddings model
615(`@cf/baai/bge-base-en-v1.5`, in `services/context`). Its vectors are
616only comparable with others from the same model, so changing it means a
617new index, rebuilt; it is pinned in code, and listed in the catalogue with
618its price. No other g1t service calls a model.
619
620Customers never choose among these: they keep **Auto**, or pin a tier per
621kind of work, and nothing customer-facing names the catalogue.
622
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging623## Discounts
624
625An account's terms are standard, or custom: a **discount** from 1 to 100%,
626a limit of its own, or both, with a reason (the terms' note) and an
627optional end date. What used to be "comped" is a **100% discount**
628(`Terms::full_discount`; migration `0039_discounts_not_comped.sql` moved
629every `comped` row to `custom` at 100%, and code reads a leftover `comped`
630row as 100%). In SQL, `sales::FULL_DISCOUNT_SQL`.
631
632- **Charging.** Every charge records what the discount took off it
633 (`ledger.discount_micros`), 100% included: the entry is charged nothing
634 and the discount is its whole price. Migration 0039 backfilled the
635 discount on a 100%-discounted workspace's earlier entries that were
636 charged nothing and paid by nothing, at cost plus 20%.
637- **What a 100% discount still does as "comped" did.** The plan is on
638 without its price (`PlanKind::Internal`), trust is `internal` (no limit
639 on unpaid usage), nothing is invoiced or closed, and g1t's own spend on
640 it is held to the monthly budget (the terms' limit, at cost; see
641 [Spend caps](#spend-caps)).
642- **The statement and Usage.** The customer sees every usage line at its
643 price (`StatementLine.price_micros`: charged, plus what paid for it, plus
644 the discount), the discount per day or project and in the totals
645 (`StatementTotals.price_micros`, `discount_micros`, `discount_percent`),
646 and the CSV has price and discount columns. The Usage page measures at
647 price for a discounted account (`Usage.discount_micros`,
648 `discount_percent`).
649- **Margin.** A 100% discount's usage is given away as before, in the
650 bucket still named `comped` (`given_comped_micros`); sudo calls it
651 **100% discounts**. A partial discount's part below cost plus the margin
652 is `given_discount_micros` (**partial discounts**). Both are kept apart
653 from margin on what was sold.
654- **sudo.** The workspace's **Terms** form takes a discount (None, 25%,
655 50%, 100%, or Custom, a whole percent; the custom field shows by CSS
656 alone), a limit, an end date and the reason. Badges and filters say
657 *100% discount* or *N% off*. Each change is audited (`terms`), such as
658 `standard → 100% discount, monthly budget $150.00: g1t's own`.
659
660## Credits from g1t
661
662Staff give a workspace credit from sudo; the code is
663`services/billing/src/grants.rs`, the tables `credit_grants` and
664`ledger.credit_kind` (migration `0038_staff_credits.sql`).
665
666### Giving credit
667
668sudo → the workspace (or an enterprise, choosing one of its workspaces) →
669**Give credit**:
670
6711. **Amount**: $10, $20, $25, $50, $100, or **Custom** (up to $10,000).
672 Up to $100 it is one step; over $100, type the workspace's slug as well.
6732. **Kind**: promotional (a welcome, a referral, an event), goodwill (an
674 apology), or refund (money back for something that went wrong: say what
675 it refunds and, optionally, the day).
6763. **Expires**: never, 30, 90 or 365 days, or the end of a chosen day
677 (UTC). A refund never expires.
6784. **Note**: required. It is on the statement and in the owners' email.
679
680The form needs no JavaScript: the fields for one choice (the custom amount,
681a refund's details, the expiry date) show by CSS alone, and all show where
682`:has()` is not supported. `admin_credit` checks everything again.
683
684A grant is a `crd_…` ledger line (kind `top_up`, so never a payment) with
685`credit_kind`, and a `credit_grants` row. The balance rises at once. The
686owners are emailed through identity's `notify_owners` (the same path as
687limit notices). It is audited as `credit`. The Overages queue's one-click
688goodwill credit is a grant too, of kind goodwill.
689
690The inbox is not told: its items are threads on a repository, built from
691events, and a credit is a workspace's. That needs a workspace-level inbox
692thread first.
693
694### How it is spent
695
696Credit is spent before anything prepaid, the soonest-expiring grant first
697(never-expiring last, then the oldest). Given while the workspace owes, it
698pays what is owed first, the most recent usage first. What each grant paid
699for is never stored: `grants::replay` works it out from the ledger in order,
700so the charge paths do not know about credit and the answer is always what
701the ledger says. A charge that comes down (a settled run) gives back to
702the grant that paid last, while it can still be spent.
703
704### Expiry and revoking
705
706- **Expiry.** The daily run (`expire_credits`, before the reconciliation)
707 closes grants past `expires_at` and enters what was left as a negative
708 `crd…_expired` line; audited as `credit_expired`. A grant past its expiry
709 pays for nothing even before the run.
710- **Revoke.** sudo → the workspace's **Credits** (or **Credits & refunds**)
711 → **Revoke unused**, with why (`admin_revoke_credit`): what is left, as a
712 `crd…_revoked` line, audited as `credit_revoked`. What was spent stays
713 spent.
714
715Neither takes the balance below zero: at most the balance, if a refunded
716payment left less there than the credit.
717
718### How margin treats them
719
720| Kind | When spent | On the day it was given |
721| --- | --- | --- |
722| Promotional | The usage is valued at its price, its charge comes out of cash and is given (`given_credit_promotional_micros`) | Nothing |
723| Goodwill | The same, as `given_credit_goodwill_micros` | Nothing |
724| 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 |
725
726A refund gives back money already collected, so counting it as given would
727make it look like a budget g1t chose to spend. Taking it off cash for the
728day it refunds says that day's sale was worth less, and counting what it
729later pays for as cash keeps money in equal to what was collected. The
730refund's day is clamped to the last 30 days, the days the reconciliation
731recomputes; an older one lands on the oldest. Refunds never expire, so
732cash taken back is never stranded.
733
734What credit paid of a month-end meter (storage, git, scans) is its own
735row on the day it was charged, since those meters are reconciled from
736snapshots. Sudo's Costs & margin lists promotional and goodwill credit
737under **Given away**, and below the statement the range's credits given,
738spent, and refunded. **Credits & refunds** (`/credits`, `admin_credits`)
739lists every grant (by kind, month, staff and workspace) and the last 12
740months by kind: given, spent, expired, revoked.
741
Usage, Billing settings and prepaid AI credit; fixes from the UX audit742### Purchased and scoped credit (prepaid AI)
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging743
Usage, Billing settings and prepaid AI credit; fixes from the UX audit744`credit_grants` also has `scope` (`all`, or `models`: model usage only,
745`grants::is_model_usage`, which includes the agent rate) and `source`
746(`staff`, `purchase`, `promo_code`, `upgrade`), and `CreditKind::Purchased`.
747Spending takes credit scoped to models first, then the soonest-expiring. A
748grant scoped to models given while the workspace owes pays only what models
749owed, never other usage (`replay`). Purchased credit is money paid in: its
750ledger line is a payment (Stripe's id, never `crd…`, `credit_kind`
751`purchased`, statement kind *AI credit*), and the usage it pays for stays
752money in, never given. Staff cannot give it (`admin_credit` refuses the
753kind). Code: `services/billing/src/ai.rs`; migration `0040_ai_credit.sql`.
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging754
Usage, Billing settings and prepaid AI credit; fixes from the UX audit755- **Buying.** `buy_ai_credit` opens Stripe Checkout (payment mode, $10 to
756 $1,000, a second line *Card processing fee* when the `card_fee` cost
757 setting is on, `setup_future_usage=off_session`), recorded in `checkouts`
758 with `feature = 'ai_credit'`, `amount_cents` the credit and `fee_cents`
759 the fee. The credit is entered by whichever comes first, the person coming
760 back (`confirm_ai_credit`, `?ai_credit=cs_…`) or
761 `checkout.session.completed`: both claim the row `open → paid`, the
762 grant's id is the session's id (`INSERT OR IGNORE`) and the ledger's
763 reference is unique, so a payment is credited exactly once. Expires 365
764 days after purchase (the daily `expire_credits`).
765- **Owed.** AI credit props up the balance but is money only for models, so
766 what is owed is `max(0, AI credit left − balance)` (`owed_with`; at a
767 month's close, `models_left_before` the month's start).
768- **Auto-reload.** `ai_reload` (settings; off by default) and `ai_reloads`
769 (one row per attempt). Each cron run (and a run that would be refused)
770 calls `reload_now`: below the threshold, it charges the customer's default
771 payment method off-session for the target less the balance (whole
772 dollars, at least $10, within the month's maximum), with the idempotency
773 key `reload/<workspace>/<YYYY-MM>/<n>` (a retry after a crash is the same
774 PaymentIntent), and grants purchased credit with the PaymentIntent's id. A
775 decline or a payment needing the person turns auto-reload off
776 (`failed_at`, `error`), emails the owners and audits `ai_reload_failed`.
777- **At $0.** `start_run` on g1t's models refuses with `payment_required`
778 when the workspace is on the paid plan (not a 100% discount, not an
779 enterprise), its included usage is used, and AI credit is $0 or less.
780- **Upgrade credit.** The first time a plan subscription is recorded active
781 (`features::record`), $5 of promotional credit scoped to models, id
782 `crd_upgrade_<workspace>`, expiring in a year: given, never revenue. Never
783 for a workspace with a 100% discount.
784
785### The agent rate and models' markup
786
787Price-book meters (migration 0040, each with versions and a public change):
788`agent_models` (per provider dollar; markup 20% until 2026-10-08, then 0,
789a fall applied at once), `agent_tokens` ($0 until 2026-10-22, then $0.25 a
790million tokens: a rise, after the 14 days' notice, emailed to owners on the
791plan by `tell_owners_of_rises`), `gateway_models` (markup 0 during beta),
792`card_fee_percent` (29,000 micros per dollar) and `card_fee_fixed`
793(300,000). Changing any is a price-book change, never a deploy. `finish_run`
794and `settle` charge models at `agent_models`' markup; `charge_agent_rate`
Merge branch 'model-routing'795charges the weighted tokens of the run's session since it was last charged
796(`runs.agent_tokens`, the weighted tokens charged so far, claimed with a
797compare-and-set), on a line `<run>/agent` (later `<run>/agent/<tokens>`),
798with `quantity` the weighted tokens. What it counts is the more of what
799`token_usage` holds for the session and what the sandbox reported with its
800cost (`finish_run`'s `tokens`, from Claude Code's closing `usage`), each
801weighted by kind. **Card fee switch:** sudo → Costs → Guardrails → *Card fee
Merge Stripe Tax, the card fee on card payments, and one free workspace per person802on card payments* (`cost_settings.card_fee`, `on`/`off`, on by default). It
803covers every card payment now, not only AI credit: see
804[Tax and the card fee](#tax-and-the-card-fee).
Usage, Billing settings and prepaid AI credit; fixes from the UX audit805
Merge branch 'model-routing'806**On a workspace's own model key** (migration `0041_agent_rate_own_key.sql`):
807the run keeps its model session (`runs.session_id`, `ms_…`) so the proxy's
808counts reach it, and the agent rate is charged at `agent_tokens_own` ($0 until
8092026-10-22, then $0.25 a million, a rise from nothing with its notice), on
810`<run>/agent-own` (later `<run>/agent-own/<tokens>`), `billed_to = 'g1t'`
811(g1t's own charge: it counts toward limits and spend), named *Agent rate,
812your own model key* on Usage and the statement. The model is never charged.
813`settle_runs` skips these runs (nothing on g1t's gateway); the cron's
814`settle_own_runs` closes them 5 minutes after they finish (3 hours after
815they start, for a sandbox that never reported) and charges tokens counted
816late. Runs from before have no session and are never charged the rate.
817
818### The agent rate's token weights
819
820How much each kind of token counts toward the agent rate, on g1t's models
821and own keys alike, is four price-book meters (migration
822`0042_agent_rate_weights.sql`): `agent_token_weight_input`, `_output`,
823`_cache_read` and `_cache_write`, each a weight in millionths in
Billing: cache reads count a tenth toward the agent rate824`cost_micros` (1,000,000 counts a token once). They started at 1, which is
825what the rate always counted; since 2026-10-08 cache reads count a tenth
826(100,000; migration `0044_cache_reads_count_a_tenth.sql`), as model providers
827price them. Input, output and cache writes count once. A cached agent run reads most of its context from
Merge branch 'model-routing'828cache (about 90% of its tokens on a typical Sonnet implement run), so the
829cache-read weight is the lever: at 1 the rate adds about 44% to such a run's
830model cost; at 0.1, far less.
831
Billing: cache reads count a tenth toward the agent rate832To change a weight (cache reads went to a tenth this way):
Merge branch 'model-routing'833
8341. Add the version, effective at once (a lower weight is a fall):
835
836 ```sql
837 INSERT INTO price_versions (id, meter, version, cost_micros, markup_percent, effective_at, reason, created_by, created_at)
838 VALUES ('pv_agent_token_weight_cache_read_2', 'agent_token_weight_cache_read', 2, 100000, 0,
839 '2026-10-08T00:00:00Z', 'Cache reads count a tenth toward the agent rate', 'staff', '2026-10-08T00:00:00Z');
840 ```
841
842 in a migration, or with `npx wrangler d1 execute g1t-billing --remote`
843 until sudo has a form.
8442. The daily run applies it once `effective_at` has come and writes the
845 public `price_changes` record (**Run the analysis now** applies it at
846 once). Raising a weight later is a rise: give it an `effective_at` 14
847 days out, and owners on the plan are emailed.
8483. Check `/pricing`: the agent rate's row lists the weights, and Usage's
849 agent-rate lines name them.
850
851`weighted` (`ai.rs`) rounds down to a whole token; a weight is never below 0.
852
Usage, Billing settings and prepaid AI credit; fixes from the UX audit853### Budgets
854
855The owners' spend limit is the budget. `limits.alert_levels` (comma
856separated, default every level), `limits.pause_at_limit` (default 1; off,
857100% is a warning, never a stop; the trust ceiling still stops work) and
858`limits.budget_webhook` (an https address, not g1t's; posted once per alert
859level a month by `warn_limits`). `set_budget` sets them, with `keepLimit`
860to leave the limit itself alone. A free workspace (trust `new`) has no
861spend limit to set: `set_spend_limit` and `set_budget` say so.
862
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging863### Earlier credits
864
865Migration 0038 makes every earlier `Credit from g1t:` line a goodwill
866grant with no expiry: the old form asked for "a refund or goodwill" with
867no way to tell them apart, and goodwill never reads as money in.
868
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix869## Token usage
870
871The model proxy (`services/models`) reads Anthropic's `usage` from every
872`/v1/messages` answer, streamed or whole, on g1t's models and on a
873workspace's own provider alike (OpenAI-shaped providers are translated
874first). Count-tokens requests are not answers and are skipped. After the
875answer, it calls `record_tokens`, which adds input, output, cache reads and
876cache writes to one row per day, workspace, person, session and model in
877`token_usage` (migration `0030_token_usage.sql`). The person is who the run
878was for, from the model session's `requested_by`; never g1t's agent. A
879report that fails is dropped and never affects the answer.
880
881`token_usage` reads a window (42 days by default, 366 at most) for the
882workspace or one person: totals, every day's tokens and the active days,
883with `costMicros` the window's run charges from the ledger, measured as
Merge branch 'model-routing'884`usage` measures them. Usage's report also lists tokens by model
885(`UsageReport.models`). The agent rate is charged on these counts (above);
886a run's model is still priced from AI Gateway's logs, never from them.
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix887
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily888## Tables (migration `0022_costs_and_margin.sql`)
889
One operation mapping, owned by repos; billing reads it instead of keeping its own890`cost_lines`, `cost_map`, `revenue_map`, `own_counts`,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily891`pending_days`, `margin_days`, `workspace_costs`, `cost_drift`,
892`margin_alerts`, `price_versions` (seeded with every current price as
893version 1), `price_proposals`, `price_notices`, `cost_settings` (the
894guardrails, seeded), the `actions_cache` price, and `ledger.price_version`.
895Every create is `IF NOT EXISTS` and every seed `INSERT OR IGNORE`; the one
One operation mapping, owned by repos; billing reads it instead of keeping its own896`ALTER` is applied once by D1's migration tracking. Migration
897`0023_one_operation_mapping.sql` drops `billable_units` (see above).
Merge branch 'worktree-agent-a633ac0f7f66d419d'898Migration `0036_model_costs_in_full.sql` adds `ledger.discount_micros`,
899`margin_days.given_discount_micros`, `runs.gateway_note` and the
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging900`ai_gateway_requests` → `models` mapping. Migration
901`0038_staff_credits.sql` adds `credit_grants`, `ledger.credit_kind` and
902`margin_days.given_credit_{promotional,goodwill}_micros`, and backfills
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97903earlier credits (see [Credits from g1t](#credits-from-g1t)). Migration
904`0046_reset_costs.sql` adds `reset_costs` and `margin_days.given_reset_micros`
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)905(see [Resetting a test workspace](#resetting-a-test-workspace)). Migration
906`0052_cloudflare_cycle.sql` adds `cost_lines.billed_usd`,
907`billable_quantity` and `basis`, `cf_subscriptions.cycle_start`,
908`cost_reads`, `margin_days.given_unpaid_micros` and the
909`payments_live_since` setting (see [The billing cycle](#the-billing-cycle)).
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays910
911## Spend caps
912
913Two caps keep what g1t pays for itself bounded while billing takes no
914real money. Both are measured at **cost** (what Cloudflare and the model
915providers charge g1t), never at price. Code: `services/billing/src/budget.rs`.
916Page: sudo **Costs & margin** → **g1t's own spend** (`/costs#spend`).
917
918### What counts as g1t's own spend
919
920Every charge that settles (an agent run's model cost from `finish_run` or
921AI Gateway's settlement, sandbox time from `record_sandbox`, a build from
922`charge_feature`) is split by what paid for it and g1t's part is added to
923`g1t_spend` (day, bucket, billing account):
924
925| Bucket | What |
926| --- | --- |
927| `comped` | All of a comped account's work (flagon-io) |
928| `trial` | The trial credit's share |
929| `oss` | The open-source pool's share |
930| `given` | A free workspace's overrun past its last bit of trial |
931| `unpaid` | Charged, but with no real money behind it: Stripe's test key, or `FREE_WHILE_BUILDING` |
932
933The plan's included usage and on-demand charges count as revenue only
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises934with live payments; in test mode they are `unpaid`, at what the usage cost
935g1t, never what was charged. A workspace's own
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays936model provider costs g1t nothing and is not counted. Month-end meters
937(git, storage, scans, embeddings, the cache) are not counted here; the
938daily reconciliation covers them. Migration `0024_spend_caps.sql`
939backfills the current month from the ledger.
940
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises941**Why it differs from the statement.** The section is the calendar month
942so far, today included, counted as each charge settled: the model
943providers' cost and the price book's cost of sandboxes and builds. The
944statement is the range, reconciled against Cloudflare's bill, where
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)945sandboxes inside Cloudflare's included usage cost $0. Both count
946Cloudflare's subscriptions the same way: each day its billing cycle's share
947of the month's price (`SpendCaps.fixed_month_micros` here, the month's days
948so far). And `g1t_spend` is never wiped: spend on a workspace a testing reset
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises949wiped later stays here (sudo names it, `SpendCaps.reset_micros`), while the
950statement has it only where the reset kept it (`reset_costs`, given away as
951testing resets). syntaqx's $7.41 of 2026-10-02 to 10-05, reset on 10-07
952before resets kept their cost, is that case.
953
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays954### Caps
955
956| Cap | Variable (g1t-billing) | Default | At the cap |
957| --- | --- | --- | --- |
958| 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). |
959| 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. |
960
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing961`0` turns either off. Cloudflare's subscriptions are read from Cloudflare
Merge Cloudflare's usage over its billing cycle: every page read, included amounts once a cycle, a projection, test-mode charges never money in (billing 0052)962each day (`cf_subscriptions`) and shown on the page only, accrued day by
963day; `CLOUDFLARE_FIXED_MONTHLY_MICROS` ($30) stands in until a read works.
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays964
965The checks are cheap: `reserve` reads today's total (one indexed sum) and,
966for a comped account, its month's comped rows. Refusals come back as
967`paused`, which the compute gate honours for every plan, internal and
968enterprise included (`packages/contracts/src/compute.ts`). The runner tells
969billing whether an agent run is on hosted models (`hostedModel` on
970`reserve`); a caller that does not say is treated as hosted.
971
972### Alerts
973
974All to `COSTS_ALERT_EMAIL` (`hey@flagon.io`), through the `EMAIL` binding:
975
976- **Comped budget**: at 50, 75, 90 and 100%, once each per account and month
977 (`budget_alerts`), checked every 15 minutes. A jump past several levels
978 sends only the highest.
979- **Breaker**: at once, from the charge that trips it; if that email fails,
980 the 15-minute cron sends it (`spend_breaker.told_at`).
981
982While the breaker is open or a comped budget is used up, every sudo page
983shows a red **Spend cap** bar.
984
985### Raising and lifting
986
987- **Raise a comped budget**: sudo → the workspace → Billing → **Terms**, set
988 **Limit $** to the new monthly budget (blank goes back to the default),
989 with a note. It applies to the next start; nothing to deploy. The change
990 is in the account's audit log.
991- **Lift the breaker for today**: sudo → Costs & margin → **g1t's own
992 spend** → **Lift for today**, with why (`admin_lift_breaker`; audit action
993 `breaker_lifted`). It resets by itself at 00:00 UTC.
994- **Change a default**: edit the variable in `services/billing/wrangler.jsonc`
995 and deploy g1t-billing.
Billing keeps Stripe's view itself: the saved card on the account, missed events replayed every 15 minutes, and the endpoint kept996
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's997## Resetting a test workspace
998
999sudo → the workspace → **Reset billing (testing)** (`admin_reset_billing`)
1000returns a workspace used for testing to how a new customer starts. It
1001deletes the workspace's rows from every billing table: ledger and balance,
1002plan and plan payments, limits and limit requests, trial grant, invoices,
1003holds, card checks, alerts sent, price notices, month-end snapshots and
sudo: the billing reset no longer names sandbox_months (dropped in 0015), checked against the migrations by a test; a failed reset says why instead of an error page1004closes, storage meters, token usage, spikes, sales records and
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's1005notes, `workspace_costs`, its workspace margin alert and its own billing
1006account. It keeps `own_counts` (what Cloudflare's bill is compared with)
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971007and the audit log, which records the reset with the note, the number of
1008rows and what g1t had paid for. The workspace, its members and its
1009repositories are identity's and repos' and stay.
1010
1011**What g1t paid for is kept.** The wiped usage still happened: AI Gateway
1012still prices its model calls and Cloudflare still bills its sandboxes. So,
1013in the same batch as the deletes, the reset writes `reset_costs`: a row per
1014day and bucket the workspace had cost on (the ledger's cost, month-end
1015meters' cost, and the value the reconciliation gave it), plus one row for
1016the reset itself (bucket `''`, nothing in it) so every reset is on record.
1017`reset_at` is the same instant as the reset's `admin_actions` entry. The
1018costs run reads the rows back as the workspace's usage on their days, all
1019of it given away as **testing resets**: `models` drift compares AI
1020Gateway with the ledger's model cost plus what resets kept, the statement
1021lists it under **Given away**, and the workspace stays in **Who g1t paid**
1022with its cost given. The rows are never wiped by a later reset, and a
1023rename moves them. Re-running the analysis reads the same rows and gives
1024the same answer.
1025
1026Resets before migration 0046 kept nothing; their wiped rows are gone and
1027nothing is made up for them. The costs run finds them in the audit log
1028(`admin_actions`, action `reset`) with no `reset_costs` at the same
1029instant, and the `models` drift detail says AI Gateway's figure includes
1030usage wiped by a testing reset of that workspace on that day, rather than
1031calling it a leak. syntaqx was reset on 2026-10-07 after about $8.60 of
1032model usage from 2026-10-02 to 2026-10-07; that usage is in the 7-day
1033window until the run of 2026-10-13 and leaves it on 2026-10-14.
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's1034
1035Billing refuses it while `STRIPE_SECRET_KEY` is a live key, for comped
sudo: a billing reset runs the costs analysis again so every figure is fresh; every submit button shows it is working (CSS only); no margin percentage on less than a cent sold1036workspaces, and for a workspace an enterprise pays for. It then runs the
1037costs analysis again (as **Run the analysis now** does), so the margin
1038figures drop the workspace's past usage at once; if that run does not
1039finish, the page says so and the button does it.
sudo: reset a test workspace's billing so it starts again as a new customer; refused on a live Stripe key, for comped workspaces and for an enterprise's1040
Billing keeps Stripe's view itself: the saved card on the account, missed events replayed every 15 minutes, and the endpoint kept1041## Stripe
1042
1043Billing keeps what it needs from Stripe so reads never wait on it, and
1044hears of changes three ways (`webhooks.rs`, `stripe_sync.rs`).
1045
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1046**API version.** Every request sends `Stripe-Version: 2025-02-24.acacia`
1047(`stripe::STRIPE_VERSION`), the version billing's field reads are written
1048for; without it Stripe answers at the account's default. Webhook events
1049come at the destination's own version: billing reads an invoice's
1050subscription from `subscription` or `parent.subscription_details.subscription`.
1051Raising the version is a code change: read Stripe's upgrade notes for every
1052field billing reads.
1053
1054**Failures.** No Stripe failure reaches a page as a 500: each payment page
1055(`page_opened`), the portal, confirmations and plan changes turn it into
1056`stripe::friendly` (Stripe's own message, never the request or a key), and
1057log the full error with the workspace. Every payment page is recorded
1058through one insert (`CHECKOUT_INSERT`), checked against the migrations by
1059`every_checkout_insert_fills_the_table`, and has an idempotency key
1060(`page/<purpose>/<workspace>/…/<10-minute bucket>`).
1061
Stripe's webhook secret is a Worker secret, STRIPE_WEBHOOK_SECRET, from a destination made in Stripe's dashboard1062**The webhook.** A destination made in Stripe's dashboard (Developers →
1063Webhooks → Add destination) with the endpoint URL
1064`https://api.g1t.sh/stripe/webhook`, in the mode of billing's key (at
1065launch, make one in live mode and put its secret). Its signing secret
1066(`whsec_…`: the destination, Signing secret, Reveal) is the billing
1067Worker's secret:
1068
1069```sh
1070cd services/billing && npx wrangler secret put STRIPE_WEBHOOK_SECRET
1071```
1072
1073Without it every event is refused with 400. After rolling the secret in
1074Stripe, put the new one; during the roll Stripe signs with both, so there
1075is no gap. The event list need not be exact: billing adds any event it
1076handles that the destination does not send (daily, or **Fix destination**
1077in sudo → Stripe), and enables it again if Stripe disabled it. It never
1078changes the secret. sudo → Stripe shows whether the secret is set, the
1079destination and its status, missing events, and the latest events.
1080Events are claimed once each in `stripe_events`; a handler that fails
1081forgets its claim, and Stripe retries.
Billing keeps Stripe's view itself: the saved card on the account, missed events replayed every 15 minutes, and the endpoint kept1082
1083**What is kept, and how it stays current**
1084
1085| Kept | Where | Refreshed by |
1086| --- | --- | --- |
1087| 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 |
1088| 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 |
1089| Payments, refunds, disputes | ledger, `checkouts`, invoices | their events |
1090
1091**Every cron run (every 15 minutes)** replays missed events: Stripe's event
1092list from an hour before `stripe_sync.through`, oldest first, through the
1093same once-only claim. `through` moves to 5 minutes before now when all were
1094handled, back to the first failure otherwise, and stays when more than
10951,000 events were listed. Claims stuck at `handling` for 10 minutes are
1096dropped so the replay retries them. The first run reads 3 days back.
1097
Stripe's webhook secret is a Worker secret, STRIPE_WEBHOOK_SECRET, from a destination made in Stripe's dashboard1098**Daily** (`keeper::DAILY`): the destination at billing's address is
1099enabled again if Stripe disabled it and given any missing event, audited as
1100`stripe`/`webhook`; then up to 25 stale cards and 25 stale plans are read
1101again.
Billing keeps Stripe's view itself: the saved card on the account, missed events replayed every 15 minutes, and the endpoint kept1102
1103**What still calls Stripe on a request**: starting a payment page, a plan
1104or a card check; opening the billing portal; settling a page the person
1105came back from; renaming a workspace (the customer's name). Nothing a page
1106view reads.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1107
Merge Stripe Tax, the card fee on card payments, and one free workspace per person1108## Tax and the card fee
1109
1110Owner decision 2026-10-08. Code: `services/billing/src/tax.rs` (what is
1111kept, the address hold), `stripe.rs` (every request's tax fields),
1112`invoices.rs`, `webhooks.rs`, `ai.rs`; migration
1113`0043_tax_and_card_fees.sql`.
1114
1115### What Stripe is asked
1116
1117Every price excludes tax (`tax_behavior=exclusive`) and carries tax code
1118`txcd_10103001` (software as a service, business use; the card fee too,
1119since a fee for paying for a sale follows the sale). Fields are valid for
1120`2025-02-24.acacia`.
1121
1122| Request | Tax | Card fee |
1123| --- | --- | --- |
1124| 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* |
1125| 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 |
1126| Checkout, prepay (`prepay_fields`) | As above | By card only; none by bank transfer |
1127| Checkout, AI credit (`credit_fields`) | As above | Its own line |
1128| 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`) | — |
1129| 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 |
1130| 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 |
1131| Enterprise invoice (`invoice_enterprise`) | The same | Never |
1132| Products (`Stripe::product`) | Made with `tax_code`; one found without it is given it | The `card_fee` product, `metadata[g1t]=card_fee` |
1133
1134### What is kept
1135
1136A payment credits the balance with what it paid for, never its tax or fee:
1137prepay credits the page's `amount_subtotal` less the fee line
1138(`credit_prepayment`), a workspace invoice `amount_paid − tax − fee`
1139(`credit_invoice`), AI credit its credit amount, and `plan_payments` the
1140plan's invoice less its tax and *Card processing fee* lines
1141(`stripe::invoice_split`). Each payment's tax and fee are rows in
1142`tax_and_fees` (`<reference>/tax`, `<reference>/card_fee`; the enterprise's
1143account id in `workspace` for its invoices), with the PaymentIntent, so a
1144refund (`charge.refunded`) gives back the balance, tax and fee in
1145proportion (`tax::refund_split`, negative rows under `refund/<charge>/…`).
1146`workspace_invoices.fee_micros` and `tax_micros` sit beside each usage
1147invoice.
1148
1149- **Statement.** *Tax* and *Card processing fees* are their own lines per
1150 day (`StatementLine.passed_micros`), never in `charged_micros`; totals
1151 `tax_micros`, `card_fee_micros`. The CSV has a row a day for each.
1152- **Margin.** Cash never holds them, so margin is untouched. sudo → Costs
1153 shows **Tax collected** and **Card fees passed on** for the range
1154 (`OverallMargin.tax_collected_micros`, `card_fees_micros`). Tax is owed to
1155 the authorities: file it from Stripe Tax's reports, never from g1t's.
1156
1157### No address
1158
1159Stripe Tax needs a country (in the US a ZIP code, in Canada a postal code
1160or province: `stripe::address_places_customer`). Before a workspace
1161invoice is drafted, g1t checks the customer; without an address, or when
1162Stripe leaves the draft at `requires_location_inputs` or refuses with
1163`customer_tax_location_invalid`, nothing is charged:
1164`accounts.tax_address_needed_at` is set, the owners are emailed once
1165(`notify_owners`), and Billing shows **Add a billing address**. Saving
1166Invoice details with an address Stripe Tax can use clears it, as does a
1167charge that goes through. Work is not stopped for it; the limits still
1168apply. Auto-reload without an address fails like a declined card (turned
1169off, owners told). An enterprise's invoice is not sent without an address:
1170sudo → the enterprise → Invoices → **Billing address** (`admin_enterprise_address`,
1171with its tax ID; audited `billing_address`).
1172
1173### The card fee
1174
1175`card_fee_cents` grosses Stripe's fee up so the amount paid for is left
1176after it: `(amount + 30¢) / (1 − 2.9%)`, rounded up; $0.91 on $20, $1.06
1177on $25. It is worked out on the amount before tax, so Stripe's fee on the
1178tax itself (a few cents) is g1t's. It is shown before paying: the plan card
1179and pricing page (`Plan.card_fee_cents`), AI credit (*Card processing fee
1180$1.06, plus tax where it applies*), Prepay. Never on a bank transfer or an
1181enterprise's (`send_invoice`) invoice. Meters stay at cost + 20% and models
1182at the provider's price plus the agent rate (price versions in migration
11830040); the fee is passed through, not margin.
1184
1185### Tax-exempt customers and tax IDs
1186
1187g1t never sets `tax_exempt`. For a customer who sends an exemption
1188certificate, set it in Stripe's dashboard (Customers → the customer → Tax
1189status: Exempt, or Reverse charge); Billing then says so. Tax IDs come
1190from Checkout (`tax_id_collection`) or Invoice details (`set_billing_details`,
1191validated against Stripe's types in `details::TAX_ID_TYPES`); Stripe checks
1192EU VAT numbers and Stripe Tax applies a reverse charge where it should.
1193Billing shows Stripe's `verification.status`.
1194
1195### In Stripe's dashboard (not done by g1t)
1196
11971. **Settings → Tax → Get started**: turn on Stripe Tax in live and test
1198 mode.
11992. **Origin address**: Flagon, Inc.'s head office address.
12003. **Default tax code**: Software as a service, business use
1201 (`txcd_10103001`); **default tax behavior**: exclusive.
12024. **Registrations**: add each jurisdiction where Flagon is registered to
1203 collect (its home state at least; then states as thresholds are
1204 crossed, which Stripe Tax's monitoring flags; the EU's OSS and the UK
1205 if selling there). Stripe collects only where a registration exists.
12065. **Customer portal**: tax ID and address updates are already allowed
1207 (`portal_configuration`).
12086. Refund an invoice through a **credit note**, so its tax is reversed in
1209 Stripe Tax.
1210
1211## Free workspaces
1212
1213Owner decision 2026-10-08: one free workspace per person, and a free
1214workspace adds no one. Billing answers one question, `free_workspaces`
1215(`credits.rs`): which of the given workspaces are on no paid plan
1216(`plan_kind_for` is `Free`). The plan, an enterprise's terms and a 100%
1217discount (flagon-io) count as paid; with payments off nothing is free.
1218Identity asks it (`services/identity/src/paid.rs`) and refuses with
1219`payment_required`:
1220
1221| Where | Refused when |
1222| --- | --- |
1223| `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. |
1224| `add_member`, `invite_member` | The workspace is free. |
1225| `add_collaborator` | Someone outside a free workspace (a username who is not a member, or an address). Members' roles are fine. |
1226| `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. |
1227
1228`@g1t` is never counted as someone added. If billing cannot be asked, the
1229change is refused for now ("try again"), never let through. The site says
1230so first (New workspace, People, a repository's Access, from the same RPC);
1231the API and MCP pass identity's refusal on as `402`.
1232
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1233## The Security and quality activation
1234
1235A second monthly subscription a workspace can hold beside the plan
1236(`Feature::Security`, `feature = 'security'` in `subscriptions`). It turns
1237on the security suite's paid features for the workspace's private
1238repositories: custom secret patterns, validity checks, delegated bypass,
1239code scanning, dependency review and the security overview. Public
1240repositories have them free; secret scanning, push protection,
1241vulnerability alerts and security updates are free everywhere.
1242
1243- **Price.** The price book's `security_activation` meter (unit
1244 `workspace-month`, `source` `list`, markup 0): 10,000,000 micros, $10,
1245 from migration `0037_security_activation.sql` with its first
1246 `price_versions` row and a public `price_changes` record. `plan()` and the
1247 `prices` RPC read it (`features::security_plan_at`); if the price book
1248 cannot be read, $10. Nothing in the web app hard-codes it. A change is a
1249 new price version, like any other: noticed on the pricing page and
1250 applied from its `effective_at` to new subscriptions. Subscriptions
1251 already running keep the amount Stripe has until they are changed in
1252 Stripe.
1253- **Stripe.** Its own subscription and its own product, tagged
1254 `metadata[g1t]=security` (the plan's is `plan`). Started from the Billing
1255 page with `subscribe` (`feature: security`) on the checked card, or
1256 through Checkout; ended with `cancel_subscription` (`feature: security`)
1257 at the period's end. Its invoices count in `plan_payments` like the
1258 plan's, as paid revenue.
1259- **Who has it.** `has_feature(workspace, security)`: on with an active
1260 subscription, with comped terms or as an enterprise's workspace, or when
1261 Stripe is not configured. The plan's allowance (`allowances.plan`) does
1262 not include it. Refusals are `PaymentRequired` with the price from the
1263 price book and the Billing page's address.
1264- **Where it is checked.** The security service, on each paid call for a
1265 private repository (`suite::entitled`) and before using custom patterns
1266 in a push (`patterns_for`). When billing cannot be reached it is taken
1267 as off: a paid feature waits rather than running unpaid.
1268- **Fixes.** "Fix with g1t" runs g1t's agent, charged as agent usage, never
1269 to the activation.
1270- **Sales figures.** MRR in sudo counts `feature = 'plan'` only; the
1271 activation's subscriptions are not in it yet.

This file's history is long; its oldest lines are credited to the oldest commit read.