Skip to content

g1t/docs/BILLING_OPERATIONS.md

752 lines48,334 bytesCodeBlame

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.
5Code: `services/billing/src/costs.rs` (reading the bill), `margin.rs`
6(reconciliation, drift, alerts), `pricing.rs` (versions, proposals,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays7notice), `keeper.rs` (sandbox and Workers for Platforms measurements),
8`budget.rs` (what g1t pays for itself, and its caps; see
9[Spend caps](#spend-caps)).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily10Page: sudo **Costs & margin** (`/costs`).
11
12Several Cloudflare products g1t runs on are new. Artifacts bills
13"operations" from 2026-10-14 without defining them (see
14[ARTIFACTS.md](ARTIFACTS.md), M1). So nothing here hard-codes a product
15list or a unit: every line Cloudflare bills is kept, and how a line maps
16to what g1t sells is data you change from sudo, without a deploy.
17
18## Data sources
19
20| Source | What | Where it lands |
21| --- | --- | --- |
22| Billable usage, `GET /accounts/{account}/billable-usage?from=&to=` | One row per service per day in FOCUS columns: `ServiceFamilyName`, `ServiceName`, `ChargePeriodStart`, `PricingQuantity`, `ContractedCost` / `BilledCost` / `ListCost`. Every product g1t uses appears once it is used: Workers, Workers for Platforms, D1, KV, R2, Queues, Containers, Durable Objects, Artifacts, Browser Rendering, Workers AI, Vectorize, Cloudflare for SaaS, Email. Inside an included amount the cost is 0. | `cost_lines`, source `billable_usage` |
Costs: Cloudflare's count for a pull request's working copy is shared out to its repository's workspace (repos pull_owners)23| GraphQL `artifactsEventsAdaptiveGroups` | Artifacts' own count by `date`, `eventType` and `repositoryName`. Operations are `create`, `fork`, `push`, `pull`, `delete`; errors (`rateLimited`, `serverError`, …) are kept but not counted. | `cost_lines`, source `artifacts_events`; per workspace (from the store key `<workspace>--<repo>`; a pull request's working copy, `pulls--<id>`, is its repository's workspace's, from repos' `pull_owners`) in `own_counts` as `cloudflare_git` |
Merge branch 'worktree-agent-a633ac0f7f66d419d'24| GraphQL `aiGatewayRequestsAdaptiveGroups`, filtered to `AI_GATEWAY_ID` | What AI Gateway priced g1t's own provider traffic at, by `date`, `provider`, `model` and `wholesale`: `count`, `sum.cost` (dollars), `sum.tokensIn`/`tokensOut`/`cacheReadTokens`/`cacheWriteTokens`. 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 daily25| The ledger | Every charge: its cost at the price book's cost, what it was charged at price, what paid for it. | read, never written |
26| `pending_usage` | Month-end meters (git, storage, scans, embeddings, the cache) as they stand. | snapshotted daily into `pending_days` |
27| `plan_payments` | The plan's $20. | read |
One operation mapping, owned by repos; billing reads it instead of keeping its own28| repos `git_operations` | Operations customers are charged for, per workspace, counted by repos through its `operation_mapping`. | `own_counts` meter `git_operations` |
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing29| Subscriptions, `GET /accounts/{account}/subscriptions` | What g1t pays each month whatever it uses (Workers Paid, add-ons): each subscription that is paid, trialing or awaiting payment, at its price over its frequency. Not on the billable-usage bill. Read in the daily run with the bill's token; a failure is logged and the last read stays. | `cf_subscriptions` (one row) |
One operation mapping, owned by repos; billing reads it instead of keeping its own30| 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 daily31
32A meter's slug is Cloudflare's name lower-cased with words joined by `_`
33and the "(First … included)" note dropped: `Workers for Platforms CPU ms
34(First 60M ms are included)` under `Workers` is product `workers`, meter
35`workers_for_platforms_cpu_ms`. Several rows of the same day and meter
36(regions, tiers) are added together before they are stored.
37
38## Credentials
39
40| Secret on g1t-billing | Permissions | Used for |
41| --- | --- | --- |
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing42| `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 |
Billing: AI Gateway's analytics are read with the token that can see them, and a gateway that priced nothing is said43| `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: Cloudflare answers a token without AI Gateway Read with no rows rather than an error, so the bill's token would read as a gateway that priced nothing |
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily44
45With neither, the daily run reconciles only what g1t counted itself, and
46the page says the bill cannot be read. Nothing fails. To set the scoped one:
47
481. Cloudflare dashboard → My Profile → API Tokens → Create Token → Custom token.
492. Permissions: Account · Billing · Read; Account · Account Analytics · Read.
503. Account resources: Include · the g1t account. No zone permissions.
514. `cd services/billing && npx wrangler secret put CLOUDFLARE_BILLING_TOKEN`.
sudo: the costs run button is named for what it does, the whole nightly analysis525. In sudo, Costs & margin → **Run the analysis now**.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily53
54Alerts are emailed through the `EMAIL` binding (Cloudflare Email Sending)
55to `COSTS_ALERT_EMAIL` (`hey@flagon.io`). An empty value sends none.
56
57## Schedule
58
59The daily cron (`17 4 * * *`, `keeper::DAILY`) runs, in order:
60
611. The keeper's measurements (sandbox seconds, app requests and CPU), each
62 a proposal now, not a direct change.
632. `costs_daily`:
Merge branch 'worktree-agent-a633ac0f7f66d419d'64 1. Read the bill, the Artifacts events and AI Gateway's analytics. The first run reads the last
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily65 31 days (GraphQL keeps 31); later runs the last 4, since Cloudflare
66 restates recent days, or back to the last day read after a gap.
67 Lines are upserted on `(day, source, product, meter)`, so a re-read
68 replaces, never adds.
69 2. g1t's own counts for the same days (replaced per day).
70 3. Snapshot `pending_usage` into `pending_days`.
sudo: the costs run button is named for what it does, the whole nightly analysis71 4. Reconcile the last 31 days (further back after a gap) into
72 `margin_days` and `workspace_costs`
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily73 (replaced per day).
74 5. Drift over the last 7 days into `cost_drift`.
75 6. Unit costs over the last 30 days, proposed to the price book.
76 7. Apply price versions whose date has come.
77 8. Open, update and close margin alerts; email new ones.
78 9. Email owners on the plan about rises to come.
79
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing80**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 analysis81(`admin_run_costs`) runs all of step 2 at once, alerts included, with no
82need to wait for 04:17 UTC. Running it twice is safe: every step replaces
83what it wrote.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily84
85## Reconciliation math
86
87Every Cloudflare line goes to one of g1t's products ("buckets") by
88`cost_map`: the row for its product with the longest matching meter
89prefix, `*` last. A line no row claims goes to `unmapped`.
90
91| Bucket | Cloudflare | Paid for by (`revenue_map`) |
92| --- | --- | --- |
93| `sandboxes` | Containers, Durable Objects compute duration | `sandbox`, `self_hosted`, `builds` |
94| `deployments` | Workers for Platforms | `deployments` |
95| `git` | Artifacts operations (and its events, as counts) | `git` |
96| `repo_storage` | Artifacts storage | `storage` |
97| `actions_cache` | R2 | `cache` |
98| `embeddings` | Workers AI, Vectorize | `context` |
99| `security` | (Workers CPU, under `platform`) | `security` |
100| `domains` | Cloudflare for SaaS | `domains` |
Merge branch 'worktree-agent-a633ac0f7f66d419d'101| `models` | not Cloudflare: AI Gateway's settled cost on the ledger; AI Gateway's own daily total (`ai_gateway_requests`) beside it, to check it | every other task (agent runs) |
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily102| `platform` | Workers, D1, KV, Queues, Email, Browser Rendering, other Durable Objects | the plan's price |
103
104For each day and bucket:
105
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 running106- **Cloudflare cost** = Σ the bucket's lines' cost, as billed: after the
107 included allowances, so a month inside them costs $0 here as on
108 Cloudflare's Billable usage page. `models` uses the ledger's cost of the
109 tokens instead; that is paid to the model providers and is not on
Merge branch 'worktree-agent-a633ac0f7f66d419d'110 Cloudflare's bill. Its "Cloudflare" column is what AI Gateway priced the
111 same traffic at, which drift compares with the ledger (below); it is
112 never added to the cost.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily113- **Own cost** = Σ the ledger's `cost_micros` for the bucket's keys (the
114 price book's cost when charged), plus month-end deltas. A workspace's own
115 model provider is no cost to g1t.
116- **Value** = what customers were charged at price: `-amount_micros` plus
117 what the plan's included usage, a trial, the open-source pool or g1t paid.
118 g1t's own (comped) workspaces are valued at cost plus the margin.
119- **Cash** = what workspaces paid: `-amount_micros`, and the plan's price.
Costs: margin is measured on what was sold; comped workspaces, free periods, the trial and the pools are given away, a budget shown beside it120- **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 running121 itself on purpose, by why:
122 - **comped**: all of a comped workspace's cost, every bucket;
123 - **free use**: a free period's usage, the overruns g1t covered
124 (`ledger.given_micros`), and all of a workspace's cost on a day it had
125 nothing priced (free allowances);
126 - **trial** and **open-source pool**: what `trial_micros` and
Merge branch 'worktree-agent-a633ac0f7f66d419d'127 `oss_micros` paid;
128 - **discount**: what a discount on an account's custom terms took below
129 cost plus the margin (`ledger.discount_micros`, see
130 [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 charging131 discounted sale never reads as margin lost;
132 - **promotional credit** and **goodwill credit**: what credit staff gave
133 paid for, when it is spent (`given_credit_promotional_micros`,
134 `given_credit_goodwill_micros`, migration 0038). That usage's charge is
135 taken out of cash, so it is never money in. A refund is not here: see
136 [Credits from g1t](#credits-from-g1t).
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 running137
138 Otherwise a workspace's day is split by those shares of its value at
139 price, and the same shares of each of its buckets' cost are given, its
140 part of running g1t included. The Team plan's included usage is sold:
141 the plan's price paid for it. Stored on `margin_days` (`given_micros`
Merge branch 'worktree-agent-a633ac0f7f66d419d'142 and `given_<why>_micros`, `given_discount_micros` from migration 0036)
143 and `workspace_costs` (`given_micros`). Sudo's Bill & pricing page lists
144 comped, free use, trial and pool by name; the discount part is in the
145 total until the page names it (`givenDiscountMicros`).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily146- **Month-end meters**: a day's figure is that day's `pending_days`
147 snapshot less the day before's, within a month. Their month-end ledger
148 entries are left out, so nothing is counted twice.
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 running149- **Product margin** = (value − cost) / value.
150- **Sudo's statement** keeps apart:
151 - **Usage sold**: cash for usage against the cost of the usage buckets
152 less what was given. Its margin is the headline; at cost plus 20% it
153 sits near 16.7%.
154 - **Running g1t**: the plan's price against `platform` less its given
155 share.
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing156 - **Cloudflare subscriptions**: what Cloudflare lists, a month, over
157 the range (`cf_subscriptions`); until a read has worked,
158 `CLOUDFLARE_FIXED_MONTHLY_MICROS`, an 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 running159 - **Not mapped**: billed, charged for by nothing.
160 - **Given away**: by why. A budget, watched under g1t's own spend, never
161 shown as a loss.
162 - **All in**: money in against all of it, with the figure without what
163 was given beside it. **Who g1t paid** splits the cost into Cloudflare
164 and the model providers.
165
166 The overall alert is (Σ cash − (Σ cost − Σ given)) / Σ cash.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily167- **Quantities**: where a mapping names an `own_meter`, Cloudflare's
168 billed quantity of those lines (or, without one, Artifacts' operation
169 events) against g1t's own count.
170
171**Shared costs to workspaces.** A bucket's cost is shared in proportion
172to, first available: Cloudflare's own per-workspace count
173(`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 running174own count, what its usage cost (so free use carries its own cost), what
175each was charged for it. `platform`
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily176and `unmapped` are shared by each workspace's share of all usage that
177day. Shares are whole micros that add up to the bill exactly (largest
178remainder).
179
180## Drift (last 7 days)
181
182| Kind | When | What to do |
183| --- | --- | --- |
One operation mapping, owned by repos; billing reads it instead of keeping its own184| 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). |
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily185| Cost | Cloudflare charged more than `drift_percent` away from the price book's cost of the same usage, with at least `min_daily_cost` | A price is stale: check the proposals. |
Billing: AI Gateway's analytics are read with the token that can see them, and a gateway that priced nothing is said186| 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), 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, it is a token that cannot see AI Gateway, or calls that went around it | 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. 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'187| 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 daily188| 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`). |
189
190## Prices: versions, proposals, notice
191
192- `price_versions` holds every price ever, never edited. `prices` is the
193 version in force. The daily run applies a version once its
194 `effective_at` has come, and adds the public `price_changes` record.
195 Ledger entries made from the price book carry `price_version` (the
196 version ids, comma-separated), so a past statement is always explained
197 by the prices of its day.
198- Proposals come from the keeper (sandbox seconds, app requests and CPU)
199 and the reconciler (mappings with `scale_to_own`: today git operations).
200 For git operations: Cloudflare's rate per its own operation (the median
201 over charged days of cost ÷ quantity) × (Cloudflare's operations ÷ g1t's)
202 × 1,000. If Cloudflare counts three for each one g1t counts, the per-1,000
203 price triples. At least 1,000 of g1t's operations are needed.
204- Decision (`pricing::decide`): under 2% is noise; more than 4× either way
205 is suspect and waits for staff; within `auto_apply_percent` (25%) it is
206 applied on its own when `auto_apply` is on; anything else waits.
207- Notice: a fall applies at once. A rise applies `notice_days` (14) after
208 the decision, and for a monthly meter (git, storage, cache, domains,
209 embeddings, scans) at the start of the month after that, so no month
210 is charged at two prices. Owners of workspaces on the plan are emailed
211 once per rise (`price_notices`), and the pricing page lists it with
212 "takes effect". Rises are never retroactive; margin protection is for
213 new usage once notice has run.
214- Staff approve or reject in sudo. A rejection needs a note.
215
216## Changing a mapping
217
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing218In 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 daily219prefix (as **Cloudflare's lines** lists them; `*` for the rest of the
220product), g1t's product, and optionally:
221
222- **Price meter**: the price book meter the line measures.
223- **Own meter**: g1t's count of the same units (`own_counts.meter`).
224- **Scale to g1t's count**: price one of g1t's units at as many of
225 Cloudflare's as it took (proposals as above).
226- **Drift threshold**.
227
sudo: the costs run button is named for what it does, the whole nightly analysis228It 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 own229Every change is in the audit log (`cost_mapping`).
230
231## Which raw meters are operations
232
233There is one mapping, and the repos service owns it: `operation_mapping`
234in g1t-repos' database, one row per raw meter with `cost_operations` (how
235many operations Cloudflare bills for it) and `billable_operations` (how
236many the customer is charged for). Change it with repos'
237`set_operation_mapping` RPC (services only), or
238`npx wrangler d1 execute g1t-repos --remote` until sudo has a form. A
239change applies to counts from then on, never to what was counted.
240Billing keeps no mapping of its own: it reads repos' `git_operations`
241(already mapped) for what customers are charged, and `artifacts_usage`
242(raw counts with the mapping) for `cost_operations`. Migration 0023 drops
243the `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 daily244
Models' margin read -14%: usage nothing paid for is valued at price, not $0245**Charged at price** (a product's value) is what each day's usage was paid:
246charged to a card or credit, or drawn from the plan's included usage, a
247trial, a pool or a gift. Usage nothing paid for, as in a free period, is
248valued at price (cost plus the margin), since it was given away at its price
249rather than sold for nothing; so are g1t's own workspaces. Runs on a
250workspace's own model provider have no cost to g1t. Every daily run
251reconciles the whole 31-day window from what is already kept, so a change in
252how a day is valued reaches every day sudo shows.
253
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily254## Alerts runbook
255
256| Alert | Raised when | First steps |
257| --- | --- | --- |
258| 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 it259| 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 daily260| Leak | Drift of kind leak | Map the meter, or decide it is overhead. |
261| 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 mislead262| 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 daily263
264Alerts close on their own when the condition clears. Open ones are
265emailed again weekly. The red bar on every sudo page shows margin,
266overall and leak alerts.
267
Merge branch 'worktree-agent-a633ac0f7f66d419d'268## Model costs
269
270Every model call g1t pays for is an agent run's (the `claude` CLI in the
271sandbox, `crates/runner`); the only other model is Workers AI's embeddings,
272which are on Cloudflare's bill (`embeddings`). How each reaches the ledger:
273
274| Call | Who pays | Run and session | Ledger cost | Settled to the gateway |
275| --- | --- | --- | --- | --- |
276| 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 |
277| Agent run straight to the gateway (no `MODELS_URL`) | g1t | `runs` row; session `rs_…` in `cf-aig-metadata` (`services/runner` `gatewaySession`) | As above | Yes |
278| 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 |
279| Agent run on a workspace's own provider | The workspace | `runs` row, `billed_to = 'workspace'`, no session | None (no cost to g1t) | No; never on g1t's gateway |
280| A sandbox that died before reporting | g1t | as its route | Charged from the gateway when settled | Yes |
281| Embeddings (indexing) | g1t | none (Workers AI) | Month-end `context` meter | No: Cloudflare's bill, `embeddings` bucket |
282| Embeddings (queries, search and agent context) | g1t | none | None: not charged, by design | No: in Cloudflare's `embeddings` line, shared out |
283
284**Settling.** A run's charge is corrected to what AI Gateway priced its
285session's requests at (`settled_cost` in `keeper.rs`). The gateway's
286figure is trusted in full: it is not held to the $100 cap on a sandbox's
287own report. It is never taken below what the sandbox reported when it
288cannot be the whole cost: a request with tokens and no cost (a model the
289gateway has no price for) or more logs than are read (2,000). Such a run
290keeps `runs.gateway_note`, its correction says why, and it raises the
291**Unpriced** drift. g1t keeps no token rates of its own: the first figure
292is Claude Code's, the final one the gateway's.
293
294**The daily total.** AI Gateway's analytics for the day (above) against
295the ledger's model cost is the check that nothing slips past: a model call
296with no run, or a run never settled, shows as **Cost** drift on `models`.
297The gateway's per-request `cost` is its estimate from its own price list:
298it can be off for prompt-cache tokens, for requests Cloudflare bills
299itself, and for models it has no price for. The drift's detail says when
300any of those were in the window; the provider's invoice is the last word.
301
302### Margin floor
303
304A sold charge is cost × (1 + `MARGIN_PERCENT`), rounded up (`margin_on`;
305`charge_micros` for a sandbox's own report). Terms change it only as
306follows (`Terms::discounted`, `Billing::charged`):
307
308- **Standard**: charged in full.
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging309- **A 100% discount** (what was "comped"; see [Discounts](#discounts)),
310 `FREE_WHILE_BUILDING`, the plan's included usage, the trial,
Merge branch 'worktree-agent-a633ac0f7f66d419d'311 the open-source pool, and overruns g1t covers: given, and counted by why
312 (above).
313- **Custom, with a discount**: the discount comes off, and what it took
314 below cost plus the margin is written on the entry as
315 `ledger.discount_micros` and counted as given (**discount**), so the sale
316 is valued at its price and charged plus given is never under cost plus the
317 margin. On a settlement correction it moves with the charge (less than
318 nothing when the charge comes down).
319- **Goodwill credits** (overages) are separate, given by staff on purpose:
320 their margin part first, the cost only up to the cap, each audited.
321
322Every usage path goes through this: `finish_run`, settling, sandbox time,
323features and builds (`charge_feature`), and the month-end meters.
324
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging325## Discounts
326
327An account's terms are standard, or custom: a **discount** from 1 to 100%,
328a limit of its own, or both, with a reason (the terms' note) and an
329optional end date. What used to be "comped" is a **100% discount**
330(`Terms::full_discount`; migration `0039_discounts_not_comped.sql` moved
331every `comped` row to `custom` at 100%, and code reads a leftover `comped`
332row as 100%). In SQL, `sales::FULL_DISCOUNT_SQL`.
333
334- **Charging.** Every charge records what the discount took off it
335 (`ledger.discount_micros`), 100% included: the entry is charged nothing
336 and the discount is its whole price. Migration 0039 backfilled the
337 discount on a 100%-discounted workspace's earlier entries that were
338 charged nothing and paid by nothing, at cost plus 20%.
339- **What a 100% discount still does as "comped" did.** The plan is on
340 without its price (`PlanKind::Internal`), trust is `internal` (no limit
341 on unpaid usage), nothing is invoiced or closed, and g1t's own spend on
342 it is held to the monthly budget (the terms' limit, at cost; see
343 [Spend caps](#spend-caps)).
344- **The statement and Usage.** The customer sees every usage line at its
345 price (`StatementLine.price_micros`: charged, plus what paid for it, plus
346 the discount), the discount per day or project and in the totals
347 (`StatementTotals.price_micros`, `discount_micros`, `discount_percent`),
348 and the CSV has price and discount columns. The Usage page measures at
349 price for a discounted account (`Usage.discount_micros`,
350 `discount_percent`).
351- **Margin.** A 100% discount's usage is given away as before, in the
352 bucket still named `comped` (`given_comped_micros`); sudo calls it
353 **100% discounts**. A partial discount's part below cost plus the margin
354 is `given_discount_micros` (**partial discounts**). Both are kept apart
355 from margin on what was sold.
356- **sudo.** The workspace's **Terms** form takes a discount (None, 25%,
357 50%, 100%, or Custom, a whole percent; the custom field shows by CSS
358 alone), a limit, an end date and the reason. Badges and filters say
359 *100% discount* or *N% off*. Each change is audited (`terms`), such as
360 `standard → 100% discount, monthly budget $150.00: g1t's own`.
361
362## Credits from g1t
363
364Staff give a workspace credit from sudo; the code is
365`services/billing/src/grants.rs`, the tables `credit_grants` and
366`ledger.credit_kind` (migration `0038_staff_credits.sql`).
367
368### Giving credit
369
370sudo → the workspace (or an enterprise, choosing one of its workspaces) →
371**Give credit**:
372
3731. **Amount**: $10, $20, $25, $50, $100, or **Custom** (up to $10,000).
374 Up to $100 it is one step; over $100, type the workspace's slug as well.
3752. **Kind**: promotional (a welcome, a referral, an event), goodwill (an
376 apology), or refund (money back for something that went wrong: say what
377 it refunds and, optionally, the day).
3783. **Expires**: never, 30, 90 or 365 days, or the end of a chosen day
379 (UTC). A refund never expires.
3804. **Note**: required. It is on the statement and in the owners' email.
381
382The form needs no JavaScript: the fields for one choice (the custom amount,
383a refund's details, the expiry date) show by CSS alone, and all show where
384`:has()` is not supported. `admin_credit` checks everything again.
385
386A grant is a `crd_…` ledger line (kind `top_up`, so never a payment) with
387`credit_kind`, and a `credit_grants` row. The balance rises at once. The
388owners are emailed through identity's `notify_owners` (the same path as
389limit notices). It is audited as `credit`. The Overages queue's one-click
390goodwill credit is a grant too, of kind goodwill.
391
392The inbox is not told: its items are threads on a repository, built from
393events, and a credit is a workspace's. That needs a workspace-level inbox
394thread first.
395
396### How it is spent
397
398Credit is spent before anything prepaid, the soonest-expiring grant first
399(never-expiring last, then the oldest). Given while the workspace owes, it
400pays what is owed first, the most recent usage first. What each grant paid
401for is never stored: `grants::replay` works it out from the ledger in order,
402so the charge paths do not know about credit and the answer is always what
403the ledger says. A charge that comes down (a settled run) gives back to
404the grant that paid last, while it can still be spent.
405
406### Expiry and revoking
407
408- **Expiry.** The daily run (`expire_credits`, before the reconciliation)
409 closes grants past `expires_at` and enters what was left as a negative
410 `crd…_expired` line; audited as `credit_expired`. A grant past its expiry
411 pays for nothing even before the run.
412- **Revoke.** sudo → the workspace's **Credits** (or **Credits & refunds**)
413 → **Revoke unused**, with why (`admin_revoke_credit`): what is left, as a
414 `crd…_revoked` line, audited as `credit_revoked`. What was spent stays
415 spent.
416
417Neither takes the balance below zero: at most the balance, if a refunded
418payment left less there than the credit.
419
420### How margin treats them
421
422| Kind | When spent | On the day it was given |
423| --- | --- | --- |
424| Promotional | The usage is valued at its price, its charge comes out of cash and is given (`given_credit_promotional_micros`) | Nothing |
425| Goodwill | The same, as `given_credit_goodwill_micros` | Nothing |
426| 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 |
427
428A refund gives back money already collected, so counting it as given would
429make it look like a budget g1t chose to spend. Taking it off cash for the
430day it refunds says that day's sale was worth less, and counting what it
431later pays for as cash keeps money in equal to what was collected. The
432refund's day is clamped to the last 30 days, the days the reconciliation
433recomputes; an older one lands on the oldest. Refunds never expire, so
434cash taken back is never stranded.
435
436What credit paid of a month-end meter (storage, git, scans) is its own
437row on the day it was charged, since those meters are reconciled from
438snapshots. Sudo's Costs & margin lists promotional and goodwill credit
439under **Given away**, and below the statement the range's credits given,
440spent, and refunded. **Credits & refunds** (`/credits`, `admin_credits`)
441lists every grant (by kind, month, staff and workspace) and the last 12
442months by kind: given, spent, expired, revoked.
443
Usage, Billing settings and prepaid AI credit; fixes from the UX audit444### Purchased and scoped credit (prepaid AI)
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging445
Usage, Billing settings and prepaid AI credit; fixes from the UX audit446`credit_grants` also has `scope` (`all`, or `models`: model usage only,
447`grants::is_model_usage`, which includes the agent rate) and `source`
448(`staff`, `purchase`, `promo_code`, `upgrade`), and `CreditKind::Purchased`.
449Spending takes credit scoped to models first, then the soonest-expiring. A
450grant scoped to models given while the workspace owes pays only what models
451owed, never other usage (`replay`). Purchased credit is money paid in: its
452ledger line is a payment (Stripe's id, never `crd…`, `credit_kind`
453`purchased`, statement kind *AI credit*), and the usage it pays for stays
454money in, never given. Staff cannot give it (`admin_credit` refuses the
455kind). Code: `services/billing/src/ai.rs`; migration `0040_ai_credit.sql`.
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging456
Usage, Billing settings and prepaid AI credit; fixes from the UX audit457- **Buying.** `buy_ai_credit` opens Stripe Checkout (payment mode, $10 to
458 $1,000, a second line *Card processing fee* when the `card_fee` cost
459 setting is on, `setup_future_usage=off_session`), recorded in `checkouts`
460 with `feature = 'ai_credit'`, `amount_cents` the credit and `fee_cents`
461 the fee. The credit is entered by whichever comes first, the person coming
462 back (`confirm_ai_credit`, `?ai_credit=cs_…`) or
463 `checkout.session.completed`: both claim the row `open → paid`, the
464 grant's id is the session's id (`INSERT OR IGNORE`) and the ledger's
465 reference is unique, so a payment is credited exactly once. Expires 365
466 days after purchase (the daily `expire_credits`).
467- **Owed.** AI credit props up the balance but is money only for models, so
468 what is owed is `max(0, AI credit left − balance)` (`owed_with`; at a
469 month's close, `models_left_before` the month's start).
470- **Auto-reload.** `ai_reload` (settings; off by default) and `ai_reloads`
471 (one row per attempt). Each cron run (and a run that would be refused)
472 calls `reload_now`: below the threshold, it charges the customer's default
473 payment method off-session for the target less the balance (whole
474 dollars, at least $10, within the month's maximum), with the idempotency
475 key `reload/<workspace>/<YYYY-MM>/<n>` (a retry after a crash is the same
476 PaymentIntent), and grants purchased credit with the PaymentIntent's id. A
477 decline or a payment needing the person turns auto-reload off
478 (`failed_at`, `error`), emails the owners and audits `ai_reload_failed`.
479- **At $0.** `start_run` on g1t's models refuses with `payment_required`
480 when the workspace is on the paid plan (not a 100% discount, not an
481 enterprise), its included usage is used, and AI credit is $0 or less.
482- **Upgrade credit.** The first time a plan subscription is recorded active
483 (`features::record`), $5 of promotional credit scoped to models, id
484 `crd_upgrade_<workspace>`, expiring in a year: given, never revenue. Never
485 for a workspace with a 100% discount.
486
487### The agent rate and models' markup
488
489Price-book meters (migration 0040, each with versions and a public change):
490`agent_models` (per provider dollar; markup 20% until 2026-10-08, then 0,
491a fall applied at once), `agent_tokens` ($0 until 2026-10-22, then $0.25 a
492million tokens: a rise, after the 14 days' notice, emailed to owners on the
493plan by `tell_owners_of_rises`), `gateway_models` (markup 0 during beta),
494`card_fee_percent` (29,000 micros per dollar) and `card_fee_fixed`
495(300,000). Changing any is a price-book change, never a deploy. `finish_run`
496and `settle` charge models at `agent_models`' markup; `charge_agent_rate`
497charges the tokens `token_usage` counted for the run's session since it was
498last charged (`runs.agent_tokens`, claimed with a compare-and-set), on a
499line `<run>/agent` (later `<run>/agent/<tokens>`), with `quantity` the
500tokens. Runs on a workspace's own provider have no session here and are not
501charged the rate. **Card fee switch:** sudo → Costs → Guardrails → *Card fee
502on AI credit bought by card* (`cost_settings.card_fee`, `on`/`off`).
503
504### Budgets
505
506The owners' spend limit is the budget. `limits.alert_levels` (comma
507separated, default every level), `limits.pause_at_limit` (default 1; off,
508100% is a warning, never a stop; the trust ceiling still stops work) and
509`limits.budget_webhook` (an https address, not g1t's; posted once per alert
510level a month by `warn_limits`). `set_budget` sets them, with `keepLimit`
511to leave the limit itself alone. A free workspace (trust `new`) has no
512spend limit to set: `set_spend_limit` and `set_budget` say so.
513
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging514### Earlier credits
515
516Migration 0038 makes every earlier `Credit from g1t:` line a goodwill
517grant with no expiry: the old form asked for "a refund or goodwill" with
518no way to tell them apart, and goodwill never reads as money in.
519
Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix520## Token usage
521
522The model proxy (`services/models`) reads Anthropic's `usage` from every
523`/v1/messages` answer, streamed or whole, on g1t's models and on a
524workspace's own provider alike (OpenAI-shaped providers are translated
525first). Count-tokens requests are not answers and are skipped. After the
526answer, it calls `record_tokens`, which adds input, output, cache reads and
527cache writes to one row per day, workspace, person, session and model in
528`token_usage` (migration `0030_token_usage.sql`). The person is who the run
529was for, from the model session's `requested_by`; never g1t's agent. A
530report that fails is dropped and never affects the answer.
531
532`token_usage` reads a window (42 days by default, 366 at most) for the
533workspace or one person: totals, every day's tokens and the active days,
534with `costMicros` the window's run charges from the ledger, measured as
535`usage` measures them. These counts are for views only: runs are still
536priced from AI Gateway's logs, never from `token_usage`.
537
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily538## Tables (migration `0022_costs_and_margin.sql`)
539
One operation mapping, owned by repos; billing reads it instead of keeping its own540`cost_lines`, `cost_map`, `revenue_map`, `own_counts`,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily541`pending_days`, `margin_days`, `workspace_costs`, `cost_drift`,
542`margin_alerts`, `price_versions` (seeded with every current price as
543version 1), `price_proposals`, `price_notices`, `cost_settings` (the
544guardrails, seeded), the `actions_cache` price, and `ledger.price_version`.
545Every 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 own546`ALTER` is applied once by D1's migration tracking. Migration
547`0023_one_operation_mapping.sql` drops `billable_units` (see above).
Merge branch 'worktree-agent-a633ac0f7f66d419d'548Migration `0036_model_costs_in_full.sql` adds `ledger.discount_micros`,
549`margin_days.given_discount_micros`, `runs.gateway_note` and the
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging550`ai_gateway_requests` → `models` mapping. Migration
551`0038_staff_credits.sql` adds `credit_grants`, `ledger.credit_kind` and
552`margin_days.given_credit_{promotional,goodwill}_micros`, and backfills
553earlier credits (see [Credits from g1t](#credits-from-g1t)).
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays554
555## Spend caps
556
557Two caps keep what g1t pays for itself bounded while billing takes no
558real money. Both are measured at **cost** (what Cloudflare and the model
559providers charge g1t), never at price. Code: `services/billing/src/budget.rs`.
560Page: sudo **Costs & margin** → **g1t's own spend** (`/costs#spend`).
561
562### What counts as g1t's own spend
563
564Every charge that settles (an agent run's model cost from `finish_run` or
565AI Gateway's settlement, sandbox time from `record_sandbox`, a build from
566`charge_feature`) is split by what paid for it and g1t's part is added to
567`g1t_spend` (day, bucket, billing account):
568
569| Bucket | What |
570| --- | --- |
571| `comped` | All of a comped account's work (flagon-io) |
572| `trial` | The trial credit's share |
573| `oss` | The open-source pool's share |
574| `given` | A free workspace's overrun past its last bit of trial |
575| `unpaid` | Charged, but with no real money behind it: Stripe's test key, or `FREE_WHILE_BUILDING` |
576
577The plan's included usage and on-demand charges count as revenue only
578with live payments; in test mode they are `unpaid`. A workspace's own
579model provider costs g1t nothing and is not counted. Month-end meters
580(git, storage, scans, embeddings, the cache) are not counted here; the
581daily reconciliation covers them. Migration `0024_spend_caps.sql`
582backfills the current month from the ledger.
583
584### Caps
585
586| Cap | Variable (g1t-billing) | Default | At the cap |
587| --- | --- | --- | --- |
588| 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). |
589| 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. |
590
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing591`0` turns either off. Cloudflare's subscriptions are read from Cloudflare
592each day (`cf_subscriptions`) and shown on the page only;
593`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 pays594
595The checks are cheap: `reserve` reads today's total (one indexed sum) and,
596for a comped account, its month's comped rows. Refusals come back as
597`paused`, which the compute gate honours for every plan, internal and
598enterprise included (`packages/contracts/src/compute.ts`). The runner tells
599billing whether an agent run is on hosted models (`hostedModel` on
600`reserve`); a caller that does not say is treated as hosted.
601
602### Alerts
603
604All to `COSTS_ALERT_EMAIL` (`hey@flagon.io`), through the `EMAIL` binding:
605
606- **Comped budget**: at 50, 75, 90 and 100%, once each per account and month
607 (`budget_alerts`), checked every 15 minutes. A jump past several levels
608 sends only the highest.
609- **Breaker**: at once, from the charge that trips it; if that email fails,
610 the 15-minute cron sends it (`spend_breaker.told_at`).
611
612While the breaker is open or a comped budget is used up, every sudo page
613shows a red **Spend cap** bar.
614
615### Raising and lifting
616
617- **Raise a comped budget**: sudo → the workspace → Billing → **Terms**, set
618 **Limit $** to the new monthly budget (blank goes back to the default),
619 with a note. It applies to the next start; nothing to deploy. The change
620 is in the account's audit log.
621- **Lift the breaker for today**: sudo → Costs & margin → **g1t's own
622 spend** → **Lift for today**, with why (`admin_lift_breaker`; audit action
623 `breaker_lifted`). It resets by itself at 00:00 UTC.
624- **Change a default**: edit the variable in `services/billing/wrangler.jsonc`
625 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 kept626
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's627## Resetting a test workspace
628
629sudo → the workspace → **Reset billing (testing)** (`admin_reset_billing`)
630returns a workspace used for testing to how a new customer starts. It
631deletes the workspace's rows from every billing table: ledger and balance,
632plan and plan payments, limits and limit requests, trial grant, invoices,
633holds, 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 page634closes, 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's635notes, `workspace_costs`, its workspace margin alert and its own billing
636account. It keeps `own_counts` (what Cloudflare's bill is compared with)
637and the audit log, which records the reset with the note and the number of
638rows. The workspace, its members and its repositories are identity's and
639repos' and stay.
640
641Billing 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 sold642workspaces, and for a workspace an enterprise pays for. It then runs the
643costs analysis again (as **Run the analysis now** does), so the margin
644figures drop the workspace's past usage at once; if that run does not
645finish, 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's646
Billing keeps Stripe's view itself: the saved card on the account, missed events replayed every 15 minutes, and the endpoint kept647## Stripe
648
649Billing keeps what it needs from Stripe so reads never wait on it, and
650hears of changes three ways (`webhooks.rs`, `stripe_sync.rs`).
651
Usage, Billing settings and prepaid AI credit; fixes from the UX audit652**API version.** Every request sends `Stripe-Version: 2025-02-24.acacia`
653(`stripe::STRIPE_VERSION`), the version billing's field reads are written
654for; without it Stripe answers at the account's default. Webhook events
655come at the destination's own version: billing reads an invoice's
656subscription from `subscription` or `parent.subscription_details.subscription`.
657Raising the version is a code change: read Stripe's upgrade notes for every
658field billing reads.
659
660**Failures.** No Stripe failure reaches a page as a 500: each payment page
661(`page_opened`), the portal, confirmations and plan changes turn it into
662`stripe::friendly` (Stripe's own message, never the request or a key), and
663log the full error with the workspace. Every payment page is recorded
664through one insert (`CHECKOUT_INSERT`), checked against the migrations by
665`every_checkout_insert_fills_the_table`, and has an idempotency key
666(`page/<purpose>/<workspace>/…/<10-minute bucket>`).
667
Stripe's webhook secret is a Worker secret, STRIPE_WEBHOOK_SECRET, from a destination made in Stripe's dashboard668**The webhook.** A destination made in Stripe's dashboard (Developers →
669Webhooks → Add destination) with the endpoint URL
670`https://api.g1t.sh/stripe/webhook`, in the mode of billing's key (at
671launch, make one in live mode and put its secret). Its signing secret
672(`whsec_…`: the destination, Signing secret, Reveal) is the billing
673Worker's secret:
674
675```sh
676cd services/billing && npx wrangler secret put STRIPE_WEBHOOK_SECRET
677```
678
679Without it every event is refused with 400. After rolling the secret in
680Stripe, put the new one; during the roll Stripe signs with both, so there
681is no gap. The event list need not be exact: billing adds any event it
682handles that the destination does not send (daily, or **Fix destination**
683in sudo → Stripe), and enables it again if Stripe disabled it. It never
684changes the secret. sudo → Stripe shows whether the secret is set, the
685destination and its status, missing events, and the latest events.
686Events are claimed once each in `stripe_events`; a handler that fails
687forgets 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 kept688
689**What is kept, and how it stays current**
690
691| Kept | Where | Refreshed by |
692| --- | --- | --- |
693| 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 |
694| 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 |
695| Payments, refunds, disputes | ledger, `checkouts`, invoices | their events |
696
697**Every cron run (every 15 minutes)** replays missed events: Stripe's event
698list from an hour before `stripe_sync.through`, oldest first, through the
699same once-only claim. `through` moves to 5 minutes before now when all were
700handled, back to the first failure otherwise, and stays when more than
7011,000 events were listed. Claims stuck at `handling` for 10 minutes are
702dropped so the replay retries them. The first run reads 3 days back.
703
Stripe's webhook secret is a Worker secret, STRIPE_WEBHOOK_SECRET, from a destination made in Stripe's dashboard704**Daily** (`keeper::DAILY`): the destination at billing's address is
705enabled again if Stripe disabled it and given any missing event, audited as
706`stripe`/`webhook`; then up to 25 stale cards and 25 stale plans are read
707again.
Billing keeps Stripe's view itself: the saved card on the account, missed events replayed every 15 minutes, and the endpoint kept708
709**What still calls Stripe on a request**: starting a payment page, a plan
710or a card check; opening the billing portal; settling a page the person
711came back from; renaming a workspace (the customer's name). Nothing a page
712view reads.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar713
714## The Security and quality activation
715
716A second monthly subscription a workspace can hold beside the plan
717(`Feature::Security`, `feature = 'security'` in `subscriptions`). It turns
718on the security suite's paid features for the workspace's private
719repositories: custom secret patterns, validity checks, delegated bypass,
720code scanning, dependency review and the security overview. Public
721repositories have them free; secret scanning, push protection,
722vulnerability alerts and security updates are free everywhere.
723
724- **Price.** The price book's `security_activation` meter (unit
725 `workspace-month`, `source` `list`, markup 0): 10,000,000 micros, $10,
726 from migration `0037_security_activation.sql` with its first
727 `price_versions` row and a public `price_changes` record. `plan()` and the
728 `prices` RPC read it (`features::security_plan_at`); if the price book
729 cannot be read, $10. Nothing in the web app hard-codes it. A change is a
730 new price version, like any other: noticed on the pricing page and
731 applied from its `effective_at` to new subscriptions. Subscriptions
732 already running keep the amount Stripe has until they are changed in
733 Stripe.
734- **Stripe.** Its own subscription and its own product, tagged
735 `metadata[g1t]=security` (the plan's is `plan`). Started from the Billing
736 page with `subscribe` (`feature: security`) on the checked card, or
737 through Checkout; ended with `cancel_subscription` (`feature: security`)
738 at the period's end. Its invoices count in `plan_payments` like the
739 plan's, as paid revenue.
740- **Who has it.** `has_feature(workspace, security)`: on with an active
741 subscription, with comped terms or as an enterprise's workspace, or when
742 Stripe is not configured. The plan's allowance (`allowances.plan`) does
743 not include it. Refusals are `PaymentRequired` with the price from the
744 price book and the Billing page's address.
745- **Where it is checked.** The security service, on each paid call for a
746 private repository (`suite::entitled`) and before using custom patterns
747 in a push (`patterns_for`). When billing cannot be reached it is taken
748 as off: a paid feature waits rather than running unpaid.
749- **Fixes.** "Fix with g1t" runs g1t's agent, charged as agent usage, never
750 to the activation.
751- **Sales figures.** MRR in sudo counts `feature = 'plan'` only; the
752 activation's subscriptions are not in it yet.

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