Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
648 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.

Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1//! Keeps every price current with what g1t actually pays.
2//!
3//! g1t passes its own costs through, so a price is only right while the
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.4//! cost under it is. One job keeps them right, on the billing service's
5//! daily cron:
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request6//!
7//! - **Checking costs** (daily). What Cloudflare billed g1t's account, from
8//! its usage API, is measured against how much was used: Containers
9//! against the seconds containers ran, Workers for Platforms per request
10//! and per CPU millisecond. When a measured cost moves, the price book
11//! moves with it, since each price is its cost plus a set markup, and
12//! the change is recorded where anyone can see it. A measurement goes
13//! through `pricing` as a proposal: a small move is applied on its own (a
14//! rise only after customers have had notice), a large or suspect one
15//! waits for staff in sudo, so one odd day of data cannot reprice
16//! everything.
17
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.18use g1t_contracts::billing::{MICROS_PER_DOLLAR, Price, PriceBook, PriceChange};
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request19
20use g1t_contracts::time::rfc3339;
21use g1t_kit::now_ms;
22use serde::Deserialize;
23use serde_json::{Value, json};
24use worker::{Env, Fetch, Headers, Method, Request, RequestInit, Result};
25
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.26use crate::Billing;
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request27
28/// The cron that also checks costs against Cloudflare's bill.
29pub(crate) const DAILY: &str = "17 4 * * *";
Merge platform pause and the hourly usage watcher: staff can pause compute, schedules, indexing or renders for everyone, the watcher emails on a breach and is never blind quietly, and the models proxy holds each run to its cap (billing 0051, integrations 0006)30/// The quarter-hourly tick: settling, and once an hour the platform watch.
31pub(crate) const QUARTER_HOURLY: &str = "*/15 * * * *";
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request32
33/// Where the keeper reads what g1t pays.
34pub(crate) struct Keeper {
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.35 /// `CLOUDFLARE_USAGE_TOKEN`: Billing and Account Analytics, read only.
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request36 token: Option<String>,
37 /// What reads the bill for `costs`: `CLOUDFLARE_BILLING_TOKEN`
38 /// (Account: Billing Read and Account Analytics Read), or the usage
39 /// token, which has both.
40 billing_token: Option<String>,
41 account: String,
42}
43
44impl Keeper {
45 pub(crate) fn from_env(env: &Env) -> Self {
46 let var = |name: &str| env.var(name).map(|v| v.to_string()).unwrap_or_default();
47 let secret = |name: &str| env.secret(name).ok().map(|v| v.to_string()).filter(|v| !v.is_empty());
48 let token = secret("CLOUDFLARE_USAGE_TOKEN");
49 Keeper {
50 billing_token: secret("CLOUDFLARE_BILLING_TOKEN").or_else(|| token.clone()),
51 token,
52 account: var("CLOUDFLARE_ACCOUNT_ID"),
53 }
54 }
55
56 async fn send(&self, method: Method, url: &str, body: Option<Value>) -> Result<Value> {
57 let Some(token) = &self.token else {
58 return Err(worker::Error::RustError("no CLOUDFLARE_USAGE_TOKEN".into()));
59 };
60 send_with(token, method, url, body).await
61 }
62
63 /// Whether Cloudflare's bill can be read.
64 pub(crate) fn can_read_bill(&self) -> bool {
65 self.billing_token.is_some() && !self.account.is_empty()
66 }
67
68 fn billing_token(&self) -> Result<&str> {
69 self.billing_token
70 .as_deref()
71 .ok_or_else(|| worker::Error::RustError("no CLOUDFLARE_BILLING_TOKEN or CLOUDFLARE_USAGE_TOKEN".into()))
72 }
73
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)74 /// Billable usage from `from` to `to` (dates), every page of it, as one
75 /// answer (`result` holds all the rows), and how many pages it took.
76 /// An answer that says it failed is returned as it is.
77 pub(crate) async fn billable_usage_pages(&self, from: &str, to: &str) -> Result<(Value, u32)> {
78 usage_pages(self.billing_token()?, &self.api(&format!("/billable-usage?from={from}&to={to}"))).await
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request79 }
80
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing81 /// The account's subscriptions (Workers Paid, add-ons), as Cloudflare
82 /// answers them. Needs Account: Billing Read.
83 pub(crate) async fn subscriptions_body(&self) -> Result<Value> {
84 send_with(self.billing_token()?, Method::Get, &self.api("/subscriptions"), None).await
85 }
86
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request87 /// A GraphQL Analytics query, as Cloudflare answers it, errors and all.
88 pub(crate) async fn graphql(&self, body: Value) -> Result<Value> {
89 send_with(self.billing_token()?, Method::Post, "https://api.cloudflare.com/client/v4/graphql", Some(body)).await
90 }
91
92 pub(crate) fn account(&self) -> &str {
93 &self.account
94 }
95
96 fn api(&self, path: &str) -> String {
97 format!("https://api.cloudflare.com/client/v4/accounts/{}{path}", self.account)
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises98 }
99
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request100 /// The account's billable usage, one row per service per day, as
101 /// Cloudflare reports it.
102 async fn billable_usage(&self, from: &str, to: &str) -> Result<Vec<UsageRow>> {
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)103 let Some(token) = &self.token else {
104 return Err(worker::Error::RustError("no CLOUDFLARE_USAGE_TOKEN".into()));
105 };
106 let (body, _) = usage_pages(token, &self.api(&format!("/billable-usage?from={from}&to={to}"))).await?;
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request107 let rows = body["result"].as_array().cloned().unwrap_or_default();
108 Ok(rows.iter().filter_map(UsageRow::from_value).collect())
109 }
110
111 /// What g1t's containers used from `since` to `until` (dates), as
112 /// Cloudflare bills it: memory in byte-seconds, and CPU seconds.
113 async fn container_usage(&self, since: &str, until: &str) -> Result<ContainerUsage> {
114 let query = "query ($account: String!, $since: Date!, $until: Date!) {
115 viewer { accounts(filter: { accountTag: $account }) {
116 containersUsageAdaptiveGroups(limit: 1000, filter: { date_geq: $since, date_leq: $until }) {
117 sum { cpuTimeSec allocatedMemory }
118 }
119 } }
120 }";
121 let body = self
122 .send(
123 Method::Post,
124 "https://api.cloudflare.com/client/v4/graphql",
125 Some(json!({ "query": query, "variables": { "account": self.account, "since": since, "until": until } })),
126 )
127 .await?;
128 let groups = body["data"]["viewer"]["accounts"][0]["containersUsageAdaptiveGroups"]
129 .as_array()
130 .cloned()
131 .unwrap_or_default();
132 Ok(groups.iter().fold(ContainerUsage::default(), |total, g| ContainerUsage {
133 cpu_seconds: total.cpu_seconds + g["sum"]["cpuTimeSec"].as_f64().unwrap_or(0.0),
134 memory_byte_seconds: total.memory_byte_seconds + g["sum"]["allocatedMemory"].as_f64().unwrap_or(0.0),
135 }))
136 }
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises137}
138
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request139/// A request to Cloudflare's API with a bearer token; anything but 200 is
140/// an error with what Cloudflare said.
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)141/// The most pages of billable usage read in one go: far more than a few
142/// months of g1t's meters.
143const MAX_USAGE_PAGES: u32 = 50;
144
145/// Every page of a billable-usage answer, its rows together. Before
146/// 2026-10-09 only the first page was read.
147async fn usage_pages(token: &str, url: &str) -> Result<(Value, u32)> {
148 let mut rows: Vec<Value> = Vec::new();
149 let mut pages = 0;
150 let mut next = url.to_owned();
151 loop {
152 let body = send_with(token, Method::Get, &next, None).await?;
153 if body["success"] == Value::Bool(false) {
154 return Ok((body, pages + 1));
155 }
156 pages += 1;
157 rows.extend(body["result"].as_array().cloned().unwrap_or_default());
158 match crate::costs::next_page(&body, pages).filter(|_| pages < MAX_USAGE_PAGES) {
159 Some(query) => next = format!("{url}&{query}"),
160 None => break,
161 }
162 }
163 Ok((json!({ "success": true, "result": rows }), pages))
164}
165
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request166async fn send_with(token: &str, method: Method, url: &str, body: Option<Value>) -> Result<Value> {
167 let headers = Headers::new();
168 headers.set("authorization", &format!("Bearer {token}"))?;
169 headers.set("content-type", "application/json")?;
170 let mut init = RequestInit::new();
171 init.with_method(method).with_headers(headers);
172 if let Some(body) = body {
173 init.with_body(Some(body.to_string().into()));
174 }
175 let mut response = Fetch::Request(Request::new_with_init(url, &init)?).send().await?;
176 let status = response.status_code();
177 let value: Value = response.json().await.unwrap_or(Value::Null);
178 if status != 200 {
179 return Err(worker::Error::RustError(format!("Cloudflare answered {status}: {value}")));
180 }
181 Ok(value)
182}
183
184#[derive(Debug, Default, Clone, Copy)]
185pub(crate) struct ContainerUsage {
186 cpu_seconds: f64,
187 memory_byte_seconds: f64,
188}
189
190/// g1t's sandboxes: Containers' standard-1, half a vCPU, 4 GiB, 8 GB disk.
191const SANDBOX_GIB: f64 = 4.0;
192const SANDBOX_DISK_GB: f64 = 8.0;
193const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
194/// The Durable Object behind each container is billed for as long as the
195/// container runs, at 128 MB.
196const SANDBOX_DO_GB: f64 = 0.125;
197
198/// Cloudflare's published Containers rates, in dollars, used for any rate
199/// the bill does not show yet (while usage is inside the included amount).
200const LIST_MEMORY_GIB_SECOND: f64 = 0.000_002_5;
201const LIST_DISK_GB_SECOND: f64 = 0.000_000_07;
202const LIST_VCPU_SECOND: f64 = 0.000_02;
203/// Durable Objects duration: $12.50 per million GB-seconds.
204const LIST_DO_GB_SECOND: f64 = 0.000_012_5;
205
206/// What one second of a sandbox costs whatever it does, in millionths of a
207/// dollar: its memory and disk, and the Durable Object behind it, for the
208/// whole second. CPU is billed only while busy, on top.
209pub(crate) fn sandbox_base_micros(memory: f64, disk: f64, durable_object: f64) -> f64 {
210 (SANDBOX_GIB * memory + SANDBOX_DISK_GB * disk + SANDBOX_DO_GB * durable_object) * MICROS_PER_DOLLAR as f64
211}
212
213/// What one second of a sandbox costs on average, in millionths of a
214/// dollar: its base, and the CPU sandboxes actually use per second of
215/// running. Runs that report their own CPU are priced on it instead (see
216/// `run_cost`).
217pub(crate) fn sandbox_second_micros(usage: ContainerUsage, memory: f64, disk: f64, vcpu: f64, durable_object: f64) -> Option<f64> {
218 let instance_seconds = usage.memory_byte_seconds / (SANDBOX_GIB * GIB);
219 if instance_seconds < 3600.0 {
220 return None;
221 }
222 let cpu_share = usage.cpu_seconds / instance_seconds;
223 Some(sandbox_base_micros(memory, disk, durable_object) + cpu_share * vcpu * MICROS_PER_DOLLAR as f64)
224}
225
226/// How much more a second of a larger machine's memory and disk (and the
227/// Durable Object behind it) costs than the standard sandbox's, at
228/// Cloudflare's list rates: 1 for the standard machine.
229pub(crate) fn base_scale(memory_gib: f64, disk_gb: f64) -> f64 {
230 let base = |memory: f64, disk: f64| memory * LIST_MEMORY_GIB_SECOND + disk * LIST_DISK_GB_SECOND + SANDBOX_DO_GB * LIST_DO_GB_SECOND;
231 base(memory_gib, disk_gb) / base(SANDBOX_GIB, SANDBOX_DISK_GB)
232}
233
234/// What a run that reported its own CPU cost g1t: its base for every
235/// second, and its vCPU-seconds at the vCPU rate.
236pub(crate) fn run_cost(seconds: i64, cpu_seconds: f64, base_per_second: f64, per_vcpu_second: f64) -> f64 {
237 seconds.max(0) as f64 * base_per_second + cpu_seconds.max(0.0) * per_vcpu_second
238}
239
Merge remote-tracking branch 'origin/main' into workspace-chat240/// A unit's rate from the bill: the median, over the days with a list
241/// cost, of list cost over quantity. Never what was billed: that is net of
242/// the included amounts (nothing while a cycle is inside them, part of a
243/// day's usage on the day it passes one), so over all of the quantity it
244/// reads as a lower price when nothing changed. None without a list cost;
245/// the published rates stand in.
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request246pub(crate) fn billed_rate(rows: &[&UsageRow]) -> Option<f64> {
247 let mut rates: Vec<f64> = rows
248 .iter()
Merge remote-tracking branch 'origin/main' into workspace-chat249 .filter(|r| r.list_cost > 0.0 && r.quantity > 0.0)
250 .map(|r| r.list_cost / r.quantity)
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request251 .collect();
252 if rates.is_empty() {
253 return None;
254 }
255 rates.sort_by(f64::total_cmp);
256 Some(rates[rates.len() / 2])
257}
258
259/// One line of Cloudflare's billable usage.
260#[derive(Debug, Clone)]
261pub(crate) struct UsageRow {
262 period_start: String,
263 period_end: String,
264 service: String,
265 unit: String,
266 quantity: f64,
267 cost: f64,
Merge remote-tracking branch 'origin/main' into workspace-chat268 /// The quantity at list price, before the included amounts.
269 list_cost: f64,
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request270}
271
272impl UsageRow {
273 /// Read leniently: the API is new, and its field names are FOCUS's.
274 fn from_value(row: &Value) -> Option<Self> {
275 let text = |keys: &[&str]| keys.iter().find_map(|k| row[*k].as_str()).unwrap_or_default().to_owned();
276 let number = |keys: &[&str]| {
277 keys.iter()
278 .find_map(|k| row[*k].as_f64().or_else(|| row[*k].as_str().and_then(|s| s.parse().ok())))
279 .unwrap_or(0.0)
280 };
281 let service = text(&["ServiceName", "service_name", "service"]);
282 if service.is_empty() {
283 return None;
284 }
285 let family = text(&["ServiceFamilyName", "service_family_name"]);
286 Some(UsageRow {
287 period_start: text(&["ChargePeriodStart", "charge_period_start"]),
288 period_end: text(&["ChargePeriodEnd", "charge_period_end"]),
289 service: if family.is_empty() { service } else { format!("{family} / {service}") },
290 unit: text(&["PricingUnit", "ConsumedUnit", "consumed_unit"]),
291 quantity: number(&["PricingQuantity", "ConsumedQuantity", "pricing_quantity"]),
292 // What g1t pays; list price if nothing was contracted.
293 cost: Some(number(&["ContractedCost", "BilledCost", "contracted_cost"]))
294 .filter(|cost| *cost > 0.0)
295 .unwrap_or_else(|| number(&["ListCost", "list_cost"])),
Merge remote-tracking branch 'origin/main' into workspace-chat296 list_cost: number(&["ListCost", "list_cost"]),
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request297 })
298 }
299}
300
301#[derive(Deserialize)]
302struct PriceRow {
303 meter: String,
304 title: String,
305 unit: String,
306 cost_micros: f64,
307 markup_percent: u32,
308 source: String,
309 checked_at: Option<String>,
310 updated_at: String,
311}
312
313#[derive(Deserialize)]
314struct ChangeRow {
315 meter: String,
316 old_cost_micros: f64,
317 new_cost_micros: f64,
318 markup_percent: u32,
319 old_markup_percent: Option<u32>,
320 reason: String,
321 created_at: String,
Merge branch 'worktree-agent-a633ac0f7f66d419d'322}
323
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request324impl Billing {
325 pub(crate) async fn prices(&self) -> Result<PriceBook> {
326 // Three reads at once; the plans are priced from the rows already
327 // read, not one query per meter (status.g1t.sh times this call).
328 let (prices, changes, coming) = futures_util::future::join3(
329 async { self.db.prepare("SELECT * FROM prices ORDER BY rowid").all().await?.results::<PriceRow>() },
330 async {
331 self.db
332 .prepare("SELECT * FROM price_changes ORDER BY created_at DESC LIMIT 20")
333 .all()
334 .await?
335 .results::<ChangeRow>()
336 },
337 // Changes still to come first, so a rise is seen before it is charged.
338 self.coming_changes(),
339 )
340 .await;
341 let (prices, changes, coming) = (prices?, changes?, coming?);
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.342 // Agents' and models' meters, from before g1t stopped running
343 // agents, stay in the table but are no longer prices.
344 let prices: Vec<PriceRow> = prices.into_iter().filter(|row| !crate::pricing::retired(&row.meter)).collect();
345 let changes: Vec<ChangeRow> = changes.into_iter().filter(|row| !crate::pricing::retired(&row.meter)).collect();
346 let coming: Vec<PriceChange> = coming.into_iter().filter(|change| !crate::pricing::retired(&change.meter)).collect();
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request347 let book: std::collections::BTreeMap<&str, f64> = prices
348 .iter()
349 .map(|row| (row.meter.as_str(), Price::price_for(row.cost_micros, row.markup_percent)))
350 .collect();
Merge Stripe Tax, the card fee on card payments, and one free workspace per person351 // With the card fee on top of each monthly price, as it is charged.
352 let card_fee = self.card_fee().await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar353 let plans: Vec<_> = g1t_contracts::billing::Feature::ALL
354 .iter()
Merge Stripe Tax, the card fee on card payments, and one free workspace per person355 .map(|feature| {
356 let mut plan = match feature {
357 g1t_contracts::billing::Feature::Security => crate::features::security_plan_at(&book),
358 _ => self.plan_at(&book),
359 };
360 plan.card_fee_cents = crate::tax::fee_for(i64::from(plan.monthly_cents), &card_fee) as u32;
361 plan
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar362 })
363 .collect();
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request364 Ok(PriceBook {
365 prices: prices
366 .into_iter()
367 .map(|row| Price {
368 price_micros: Price::price_for(row.cost_micros, row.markup_percent),
369 meter: row.meter,
370 title: row.title,
371 unit: row.unit,
372 cost_micros: row.cost_micros,
373 markup_percent: row.markup_percent,
374 source: row.source,
375 checked_at: row.checked_at,
376 updated_at: row.updated_at,
377 })
378 .collect(),
379 changes: coming
380 .into_iter()
381 .chain(changes.into_iter().map(|row| PriceChange {
382 meter: row.meter,
383 old_cost_micros: row.old_cost_micros,
384 new_cost_micros: row.new_cost_micros,
385 markup_percent: row.markup_percent,
386 old_markup_percent: row.old_markup_percent,
387 reason: row.reason,
388 created_at: row.created_at,
389 effective_at: None,
390 }))
391 .collect(),
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.392 // Nothing is charged for models any more; kept for readers.
393 model_margin_percent: self.margin_percent,
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request394 plans,
395 free: Some(g1t_contracts::billing::FreeTier {
396 trial_workspace_micros: if self.trials_on { self.plans.trial_workspace_micros } else { 0 },
397 trial_monthly_pool_micros: if self.trials_on { self.plans.trial_monthly_pool_micros } else { 0 },
398 oss_pool_micros: self.plans.oss_pool_micros,
399 oss_repo_micros: self.plans.oss_repo_micros,
400 free_private_storage_bytes: self.plans.free_storage_bytes,
401 audit_retention_days: self.plans.free_audit_days,
402 plan_audit_retention_days: self.plans.audit_days,
403 min_charge_micros: self.plans.min_charge_micros,
404 git_operations_included: self.plans.git_included,
405 paid_start_ceiling_micros: self.plans.paid_start_micros,
406 overage_forgive_cost_micros: self.plans.forgive_cost_micros,
407 }),
408 })
409 }
410
411 /// Whether the costs have never been checked against Cloudflare's bill.
412 pub(crate) async fn never_checked(&self) -> Result<bool> {
413 Ok(self
414 .db
415 .prepare("SELECT meter FROM prices WHERE checked_at IS NOT NULL LIMIT 1")
416 .first::<Value>(None)
417 .await?
418 .is_none())
419 }
420
421 /// A meter's cost and price per unit, from the book.
422 pub(crate) async fn price(&self, meter: &str) -> Result<Option<(f64, f64)>> {
423 #[derive(Deserialize)]
424 struct Row {
425 cost_micros: f64,
426 markup_percent: u32,
427 }
428 Ok(self
429 .db
430 .prepare("SELECT cost_micros, markup_percent FROM prices WHERE meter = ?")
431 .bind(&[meter.into()])?
432 .first::<Row>(None)
433 .await?
434 .map(|row| (row.cost_micros, Price::price_for(row.cost_micros, row.markup_percent))))
435 }
436
437 pub(crate) async fn reconcile(&self, keeper: &Keeper) -> Result<()> {
438 if keeper.token.is_none() {
439 return Ok(());
440 }
441 let now = rfc3339(now_ms());
442 let today = &now[..10];
443 let since = rfc3339(now_ms() - 30 * 24 * 60 * 60 * 1000);
444 let rows = keeper.billable_usage(&since[..10], today).await?;
445 for row in &rows {
446 self.db
447 .prepare(
448 "INSERT INTO cloudflare_usage (period_start, period_end, service, unit, quantity, cost_usd, fetched_at)
449 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
450 ON CONFLICT (period_start, service, unit) DO UPDATE SET
451 period_end = ?2, quantity = ?5, cost_usd = ?6, fetched_at = ?7",
452 )
453 .bind(&[
454 row.period_start.as_str().into(),
455 row.period_end.as_str().into(),
456 row.service.as_str().into(),
457 row.unit.as_str().into(),
458 row.quantity.into(),
459 row.cost.into(),
460 now.as_str().into(),
461 ])?
462 .run()
463 .await?;
464 }
465
466 let named = |words: &[&str]| -> Vec<&UsageRow> {
467 rows.iter()
468 .filter(|r| {
469 let service = r.service.to_lowercase();
470 words.iter().all(|word| service.contains(word))
471 })
472 .collect()
473 };
474
Merge remote-tracking branch 'origin/main' into workspace-chat475 // Containers: each resource at the list cost the bill shows for it
476 // (before the included amounts), or the published rate without one,
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request477 // over how much CPU g1t's sandboxes really use per second.
478 let memory = billed_rate(&named(&["container memory"]));
479 let disk = billed_rate(&named(&["container disk"]));
480 let vcpu = billed_rate(&named(&["container vcpu"]));
481 let durable_object = billed_rate(&named(&["durable objects", "duration"]));
482 let usage = keeper.container_usage(&since[..10], today).await?;
483 let rates = (
484 memory.unwrap_or(LIST_MEMORY_GIB_SECOND),
485 disk.unwrap_or(LIST_DISK_GB_SECOND),
486 vcpu.unwrap_or(LIST_VCPU_SECOND),
487 durable_object.unwrap_or(LIST_DO_GB_SECOND),
488 );
489 // The parts, for runs that report their own CPU.
Merge remote-tracking branch 'origin/main' into workspace-chat490 let parts_reason = "Cloudflare's Containers and Durable Objects rates, as listed on the bill or published";
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request491 self.measure("sandbox_base_second", sandbox_base_micros(rates.0, rates.1, rates.3), parts_reason).await?;
492 self.measure("sandbox_cpu_second", rates.2 * MICROS_PER_DOLLAR as f64, parts_reason).await?;
493 if let Some(per_second) = sandbox_second_micros(usage, rates.0, rates.1, rates.2, rates.3) {
494 let instance_seconds = usage.memory_byte_seconds / (SANDBOX_GIB * GIB);
495 let billed = [("memory", memory), ("disk", disk), ("vCPU", vcpu), ("Durable Object duration", durable_object)]
496 .iter()
497 .filter(|(_, rate)| rate.is_some())
498 .map(|(name, _)| *name)
499 .collect::<Vec<_>>();
500 let reason = format!(
501 "Sandboxes used {:.2} vCPU per second over {:.0} hours of Cloudflare Containers in the last 30 days, with the Durable Object behind each; {}",
502 usage.cpu_seconds / instance_seconds,
503 instance_seconds / 3600.0,
504 if billed.is_empty() {
505 "rates are Cloudflare's published ones".to_owned()
506 } else {
Merge remote-tracking branch 'origin/main' into workspace-chat507 format!("{} at the list cost on Cloudflare's bill", billed.join(", "))
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request508 },
509 );
510 for meter in ["sandbox_second", "build_second"] {
511 self.measure(meter, per_second, &reason).await?;
512 }
513 }
514 // Apps run as Workers: per million requests and CPU milliseconds,
515 // once the bill shows them charged.
516 // Security scans' CPU follows the same Workers CPU rate.
517 let app_meters: [(&str, &[&str], &str); 3] = [
518 ("app_requests", &["workers", "requests"], "requests"),
519 ("app_cpu", &["workers cpu"], "CPU ms"),
520 ("scan_cpu", &["workers cpu"], "CPU ms"),
521 ];
522 for (meter, words, unit) in app_meters {
523 if let Some(rate) = billed_rate(&named(words)) {
Merge remote-tracking branch 'origin/main' into workspace-chat524 let reason = format!("Cloudflare's bill lists Workers {unit} at ${:.2} per million", rate * 1e6);
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request525 self.measure(meter, rate * 1e6 * MICROS_PER_DOLLAR as f64, &reason).await?;
526 }
527 }
528 self.db
529 .prepare("UPDATE prices SET checked_at = ?")
530 .bind(&[now.as_str().into()])?
531 .run()
532 .await?;
533 Ok(())
534 }
535
536 /// Proposes moving a meter's cost to a measurement (see `pricing`):
537 /// applied on its own when small, after notice when a rise; left for
538 /// staff when large or suspect.
539 async fn measure(&self, meter: &str, measured: f64, reason: &str) -> Result<()> {
540 if let Some(outcome) = self.propose(meter, measured, reason, "keeper").await? {
541 worker::console_log!("{meter}: {outcome}");
542 }
543 Ok(())
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 #[test]
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.552 fn a_cost_plus_the_margin_rounds_up_to_a_whole_micro() {
Merge branch 'worktree-agent-a633ac0f7f66d419d'553 assert_eq!(crate::margin_on(140_000_000, 20), 168_000_000);
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.554 // Exactly cost plus the margin, rounded up, in whole micros, never under it.
Merge branch 'worktree-agent-a633ac0f7f66d419d'555 for cost in [0_i64, 1, 7, 999, 123_457, 99_999_999] {
556 let exact = (cost * 120 + 99) / 100;
557 assert_eq!(crate::margin_on(cost, 20), exact, "{cost}");
558 assert!(crate::margin_on(cost, 20) * 100 >= cost * 120, "{cost}");
559 }
560 }
561
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request562 #[test]
563 fn a_sandbox_second_is_its_memory_and_disk_and_the_cpu_it_uses() {
564 // An hour of sandboxes that kept a fifth of a vCPU busy.
565 let usage = ContainerUsage { cpu_seconds: 720.0, memory_byte_seconds: 3600.0 * 4.0 * GIB };
566 let micros =
567 sandbox_second_micros(usage, LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_VCPU_SECOND, LIST_DO_GB_SECOND).unwrap();
568 // 4 x 2.5 + 8 x 0.07 + 0.125 x 12.5 + 0.2 x 20 = 16.1225
569 assert!((micros - 16.1225).abs() < 1e-9, "{micros}");
570 // The Durable Object adds about 11% to the second it left out.
571 assert!((sandbox_base_micros(LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_DO_GB_SECOND) - 12.1225).abs() < 1e-9);
572 // Too little use to say anything.
573 assert!(sandbox_second_micros(ContainerUsage { cpu_seconds: 1.0, memory_byte_seconds: GIB }, 1.0, 1.0, 1.0, 1.0).is_none());
574 }
575
576 #[test]
577 fn a_run_that_reports_its_cpu_is_priced_on_it() {
578 let base = sandbox_base_micros(LIST_MEMORY_GIB_SECOND, LIST_DISK_GB_SECOND, LIST_DO_GB_SECOND);
579 let vcpu = LIST_VCPU_SECOND * MICROS_PER_DOLLAR as f64;
580 // A 10-minute cargo build that kept its half vCPU busy throughout.
581 let heavy = run_cost(600, 300.0, base, vcpu);
582 assert!((heavy - (600.0 * 12.1225 + 300.0 * 20.0)).abs() < 1e-6);
583 // The same ten minutes, mostly idle, costs less.
584 let light = run_cost(600, 30.0, base, vcpu);
585 assert!(light < heavy);
586 // The average would have under-priced the heavy one.
587 let average = 600.0 * (base + 0.195 * vcpu);
588 assert!(average < heavy && average > light);
589 assert_eq!(run_cost(0, -1.0, base, vcpu), 0.0);
590 // A larger machine's base: its memory and disk, not its CPU.
591 assert!((base_scale(SANDBOX_GIB, SANDBOX_DISK_GB) - 1.0).abs() < 1e-12);
592 assert!((base_scale(12.0, 20.0) - 2.72).abs() < 0.01, "{}", base_scale(12.0, 20.0));
593 assert!((base_scale(8.0, 16.0) - 1.87).abs() < 0.01, "{}", base_scale(8.0, 16.0));
594 }
595
596 #[test]
597 fn a_billed_rate_is_the_median_of_the_charged_days() {
Merge remote-tracking branch 'origin/main' into workspace-chat598 let row = |quantity: f64, list_cost: f64| UsageRow {
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request599 period_start: String::new(),
600 period_end: String::new(),
601 service: "Containers / Container Memory".into(),
602 unit: "Count".into(),
603 quantity,
Merge remote-tracking branch 'origin/main' into workspace-chat604 cost: list_cost,
605 list_cost,
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request606 };
607 let rows = [row(100.0, 0.0), row(100.0, 0.0002), row(100.0, 0.00025), row(100.0, 0.00025)];
608 assert_eq!(billed_rate(&rows.iter().collect::<Vec<_>>()), Some(0.000_002_5));
609 assert_eq!(billed_rate(&[&row(5.0, 0.0)]), None);
610 }
611
612 #[test]
Merge remote-tracking branch 'origin/main' into workspace-chat613 fn a_rate_is_the_list_price_not_what_was_billed_past_the_included_amount() {
614 // 126,870 GiB-seconds, of which the 36,870 past the included 90,000
615 // were billed: $0.09 billed, $0.32 at list. The rate is the list's.
616 let row = UsageRow {
617 period_start: String::new(),
618 period_end: String::new(),
619 service: "Containers / Container Memory".into(),
620 unit: "GiB-seconds".into(),
621 quantity: 126_870.0,
622 cost: 0.092_175,
623 list_cost: 0.317_175,
624 };
625 assert!((billed_rate(&[&row]).unwrap() - 0.000_002_5).abs() < 1e-15);
626 // Billed with no list cost says nothing about the price.
627 assert_eq!(billed_rate(&[&UsageRow { list_cost: 0.0, ..row }]), None);
628 }
629
630 #[test]
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request631 fn usage_rows_are_read_by_their_focus_names() {
632 let row = UsageRow::from_value(&json!({
633 "ServiceFamilyName": "Containers",
634 "ServiceName": "Memory",
635 "PricingUnit": "GiB-seconds",
636 "PricingQuantity": "1200.5",
637 "ContractedCost": 0.003,
Merge remote-tracking branch 'origin/main' into workspace-chat638 "ListCost": 0.0030,
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request639 "ChargePeriodStart": "2026-10-01",
640 }))
641 .unwrap();
642 assert_eq!(row.service, "Containers / Memory");
643 assert_eq!(row.quantity, 1200.5);
644 assert_eq!(row.cost, 0.003);
Merge remote-tracking branch 'origin/main' into workspace-chat645 assert_eq!(row.list_cost, 0.003);
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request646 assert!(UsageRow::from_value(&json!({ "nothing": 1 })).is_none());
647 }
648}

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