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
1,393 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.

The Packages page names every registry it has1//! The billing service: what g1t's compute costs, charged to the workspace
2//! it ran for at what g1t pays plus 20%.
3//!
4//! One paid plan, "g1t" (see `features`): $20 a month per workspace, never
5//! per person, with $10 of usage included, deployments, and more private
6//! storage. The forge is free for everyone; compute needs the plan or a
7//! card check (see `compute` and `cards`). Before anything that costs money
8//! starts, the service that starts it reserves its estimate here; when it
9//! is done, what it cost goes on the ledger plus the margin, drawn first
10//! from what the plan includes, a trial or g1t's pools (see `credits`).
11//! Every change is a ledger entry, and a balance is always the sum of its
12//! ledger.
13//!
14//! Without a card processor configured the service says so and charges
15//! nothing, so that g1t still runs where billing has not been set up.
16//!
17//! Reached only through service bindings; see `g1t_contracts::billing` for
18//! the methods and their arguments.
19
20mod accounts;
21mod budget;
Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted.22mod charged;
The Packages page names every registry it has23mod cards;
24mod closing;
25mod compute;
26mod costs;
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)27mod cycle;
The Packages page names every registry it has28mod margin;
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)29mod platform;
The Packages page names every registry it has30mod pricing;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit31mod report;
32mod details;
The Packages page names every registry it has33mod credits;
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging34mod grants;
The Packages page names every registry it has35mod overages;
36mod requests;
37mod storage;
38mod invoices;
39mod sales;
40mod statement;
41mod webhooks;
42mod features;
43mod keeper;
44mod limits;
45mod rename;
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's46mod reset;
The Packages page names every registry it has47mod retention;
48mod stripe;
49mod stripe_sync;
Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing50mod subscriptions;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person51mod tax;
The Packages page names every registry it has52
53use g1t_contracts::billing::*;
54use g1t_contracts::time::rfc3339;
Merge main (membership, two-factor, GitHub repo roles) into tokens55use g1t_contracts::{FailureCode, Outcome, new_id};
The Packages page names every registry it has56use g1t_kit::{args, now_ms, reply, rpc_method};
57use serde::Deserialize;
58use worker::wasm_bindgen::JsValue;
59use worker::{Context, D1Database, Env, MessageBatch, MessageExt, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
60use futures_util::future::{try_join, try_join5};
61
62use stripe::Stripe;
63
64/// Prepaying: $25 at the least; by card up to $10,000 at a time, and by
65/// bank transfer from $1,000 to $100,000.
66const MIN_TOP_UP_CENTS: u32 = 2_500;
67const MAX_TOP_UP_CENTS: u32 = 1_000_000;
68const MIN_BANK_TRANSFER_CENTS: u32 = 100_000;
69const MAX_BANK_TRANSFER_CENTS: u32 = 10_000_000;
70const LEDGER_PAGE: u32 = 100;
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.71/// What a cost g1t trusts (the price book's) is charged at: plus the
72/// margin, rounded up to a whole millionth.
Merge branch 'worktree-agent-a633ac0f7f66d419d'73pub fn margin_on(cost_micros: i64, margin_percent: u32) -> i64 {
74 let cost = i128::from(cost_micros.max(0));
75 let charge = (cost * i128::from(100 + margin_percent) + 99) / 100;
76 i64::try_from(charge).unwrap_or(i64::MAX)
The Packages page names every registry it has77}
78
79fn optional(value: Option<&str>) -> JsValue {
80 value.map_or(JsValue::NULL, JsValue::from)
81}
82
83#[derive(Deserialize)]
84struct AccountRow {
85 balance_micros: i64,
86 customer_id: Option<String>,
87}
88
89#[derive(Deserialize)]
90struct LedgerRow {
91 id: String,
92 kind: EntryKind,
93 amount_micros: i64,
94 description: String,
95 repo: Option<String>,
96 number: Option<u32>,
97 task: Option<String>,
98 model: Option<String>,
99 created_by: Option<String>,
100 created_at: String,
101 billed_to: Option<String>,
102 #[serde(default)]
103 workspace: Option<String>,
104 #[serde(default)]
105 credit_micros: Option<i64>,
106 #[serde(default)]
107 trial_micros: Option<i64>,
108 #[serde(default)]
109 oss_micros: Option<i64>,
110 #[serde(default)]
111 given_micros: Option<i64>,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging112 #[serde(default)]
113 credit_kind: Option<String>,
114 #[serde(default)]
115 discount_micros: Option<i64>,
The Packages page names every registry it has116}
117
118impl From<LedgerRow> for LedgerEntry {
119 fn from(row: LedgerRow) -> Self {
120 LedgerEntry {
121 id: row.id,
122 kind: row.kind,
123 amount_micros: row.amount_micros,
124 description: row.description,
125 repo: row.repo,
126 number: row.number,
127 task: row.task,
128 model: row.model,
129 billed_to: row.billed_to.unwrap_or_else(|| "g1t".to_owned()),
130 created_by: row.created_by,
131 created_at: row.created_at,
132 workspace: row.workspace,
133 credit_micros: row.credit_micros.unwrap_or(0),
134 trial_micros: row.trial_micros.unwrap_or(0),
135 oss_micros: row.oss_micros.unwrap_or(0),
136 given_micros: row.given_micros.unwrap_or(0),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging137 credit_kind: row.credit_kind.as_deref().and_then(CreditKind::parse),
138 discount_micros: row.discount_micros.unwrap_or(0),
The Packages page names every registry it has139 }
140 }
141}
142
143#[derive(Deserialize)]
144struct CheckoutRow {
145 workspace: String,
146 created_by: String,
Merge Stripe Tax, the card fee on card payments, and one free workspace per person147 #[serde(default)]
148 fee_cents: Option<u32>,
The Packages page names every registry it has149}
150
Usage, Billing settings and prepaid AI credit; fixes from the UX audit151/// A payment page started, as `checkouts` keeps it.
152pub(crate) struct NewCheckout<'a> {
153 pub id: &'a str,
154 pub workspace: &'a str,
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.155 /// What it pays for, in cents: the prepayment or the plan's price; 0
156 /// for a card check.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit157 pub amount_cents: u32,
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.158 /// The card fee on top.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit159 pub fee_cents: u32,
160 pub created_by: &'a str,
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.161 /// None for a prepayment; `plan`, `security` or `card_check` otherwise
162 /// (`ai_credit` on pages from before AI credit was retired).
Usage, Billing settings and prepaid AI credit; fixes from the UX audit163 pub feature: Option<&'a str>,
164}
165
166/// The one insert every payment page goes through, every column named, so
167/// a column added to `checkouts` with no default is caught by
168/// `tests::every_checkout_insert_fills_the_table` rather than by a 500.
169pub(crate) const CHECKOUT_INSERT: &str = "INSERT INTO checkouts (id, workspace, amount_cents, fee_cents, created_by, created_at, feature, status)
170 VALUES (?, ?, ?, ?, ?, ?, ?, 'open')";
171
172impl Billing {
173 pub(crate) async fn record_checkout(&self, c: &NewCheckout<'_>) -> Result<()> {
174 self.db
175 .prepare(CHECKOUT_INSERT)
176 .bind(&[
177 c.id.into(),
178 c.workspace.into(),
179 c.amount_cents.into(),
180 c.fee_cents.into(),
181 c.created_by.into(),
182 rfc3339(now_ms()).into(),
183 optional(c.feature),
184 ])?
185 .run()
186 .await?;
187 Ok(())
188 }
189}
190
The Packages page names every registry it has191/// A row an `UPDATE … RETURNING` touched.
192#[derive(Deserialize)]
193struct Touched {
194 #[allow(dead_code)]
195 id: String,
196}
197
198struct Billing {
199 db: D1Database,
200 /// Absent when no card processor is configured.
201 stripe: Option<Stripe>,
202 /// The destination's signing secret from Stripe (`STRIPE_WEBHOOK_SECRET`);
203 /// without it no event is believed.
204 webhook_secret: Option<String>,
205 margin_percent: u32,
206 /// While g1t is being built out, nothing is charged (`FREE_WHILE_BUILDING`).
207 free: bool,
208 /// Whether new workspaces get trial credit (`TRIAL_WORKSPACE_MICROS`
209 /// and `TRIAL_MONTHLY_POOL_MICROS` both above zero).
210 trials_on: bool,
211 /// The plans' and pools' numbers; see `credits`.
212 plans: credits::Config,
213 /// The repos service: which repositories are public, what private ones
214 /// hold, and their git operations. Absent where it is not bound.
215 repos: Option<worker::Fetcher>,
216 /// The packages service: what each workspace's packages hold.
217 packages: Option<worker::Fetcher>,
218 /// The identity service, which emails owners. Absent where it is not
219 /// bound.
220 identity: Option<worker::Fetcher>,
221 /// How far unpaid usage may go; see `limits`.
222 ceilings: limits::Ceilings,
223 /// The caps on what g1t pays for itself; see `budget`.
224 caps: budget::Caps,
225 /// The worker's bindings, for emailing staff (`EMAIL`).
226 env: Env,
227}
228
229impl Billing {
230 fn status(&self) -> Status {
231 Status {
232 enabled: self.stripe.is_some(),
233 live: self.stripe.as_ref().is_some_and(Stripe::live),
234 free: self.free,
235 }
236 }
237
238 async fn row(&self, workspace: &str) -> Result<Option<AccountRow>> {
239 self.db
240 .prepare("SELECT balance_micros, customer_id FROM accounts WHERE workspace = ?")
241 .bind(&[workspace.into()])?
242 .first::<AccountRow>(None)
243 .await
244 }
245
246 async fn standing(&self, workspace: &str) -> Result<Account> {
247 // The card as last synced (stripe_sync.rs), not asked of Stripe.
248 let (row, card) = try_join(self.row(workspace), self.saved_card(workspace)).await?;
249 Ok(Account {
250 workspace: workspace.to_owned(),
251 balance_micros: row.map_or(0, |row| row.balance_micros),
252 status: self.status(),
253 margin_percent: self.margin_percent,
254 card,
255 })
256 }
257
258 /// The workspace's customer at Stripe, made the first time one is needed.
259 pub(crate) async fn customer_for(&self, workspace: &str) -> Result<String> {
260 let Some(stripe) = &self.stripe else {
261 return Err(worker::Error::RustError("payments are not set up".into()));
262 };
263 if let Some(customer) = self.row(workspace).await?.and_then(|row| row.customer_id) {
264 return Ok(customer);
265 }
266 let customer = stripe.create_customer(workspace).await?;
267 self.db
268 .prepare(
269 "INSERT INTO accounts (workspace, balance_micros, customer_id, created_at) VALUES (?1, 0, ?2, ?3)
270 ON CONFLICT (workspace) DO UPDATE SET customer_id = ?2",
271 )
272 .bind(&[workspace.into(), customer.as_str().into(), rfc3339(now_ms()).into()])?
273 .run()
274 .await?;
275 Ok(customer)
276 }
277
278 /// Stripe's hosted billing page for the workspace. Owners only.
279 async fn billing_portal(&self, a: BillingPortalArgs) -> Result<Outcome<Checkout>> {
280 let workspace = a.workspace.to_lowercase();
Merge main (membership, two-factor, GitHub repo roles) into tokens281 if !a.actor.manages_billing(&workspace) {
282 return Ok(Outcome::fail(FailureCode::Forbidden, "Only an owner or a billing manager can manage the workspace's billing."));
The Packages page names every registry it has283 }
284 let Some(stripe) = &self.stripe else {
285 return Ok(Outcome::fail(FailureCode::Conflict, "Payments are not set up on this g1t."));
286 };
287 let customer = match self.customer_for(&workspace).await {
288 Ok(customer) => customer,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit289 Err(error) => return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error))),
The Packages page names every registry it has290 };
291 match stripe.portal_session(&customer, &a.return_url).await {
292 Ok(url) => Ok(Outcome::Ok(Checkout { url })),
293 Err(error) if stripe::is_missing(&error) => {
294 // The customer was removed at Stripe: a new one next time.
295 self.forget_customer(&workspace).await?;
296 Ok(Outcome::fail(FailureCode::Conflict, "Stripe no longer had this workspace's customer. Try again."))
297 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit298 Err(error) => Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error))),
The Packages page names every registry it has299 }
300 }
301
302 /// Adds a ledger entry and moves the balance by the same amount, as
303 /// one write.
304 #[allow(clippy::too_many_arguments)]
305 async fn enter(
306 &self,
307 workspace: &str,
308 kind: EntryKind,
309 amount_micros: i64,
310 description: &str,
311 reference: &str,
312 cost_micros: Option<i64>,
313 created_by: Option<&str>,
314 customer: Option<&str>,
315 ) -> Result<()> {
316 let now = now_ms();
317 let timestamp = rfc3339(now);
318 let kind = match kind {
319 EntryKind::TopUp => "top_up",
320 EntryKind::Usage => "usage",
321 };
322 self.db
323 .batch(vec![
324 self.db
325 .prepare(
326 "INSERT INTO ledger
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.327 (id, workspace, kind, amount_micros, description, cost_micros, reference, created_by, created_at, billed_to)
328 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'g1t')",
The Packages page names every registry it has329 )
330 .bind(&[
331 new_id("led", now).into(),
332 workspace.into(),
333 kind.into(),
334 // D1 takes numbers as doubles, which hold every
335 // amount this service will see exactly.
336 (amount_micros as f64).into(),
337 description.into(),
338 cost_micros.map_or(JsValue::NULL, |cost| (cost as f64).into()),
339 reference.into(),
340 optional(created_by),
341 timestamp.as_str().into(),
342 ])?,
343 self.db
344 .prepare(
345 "INSERT INTO accounts (workspace, balance_micros, customer_id, created_at)
346 VALUES (?1, ?2, ?3, ?4)
347 ON CONFLICT (workspace) DO UPDATE SET
348 balance_micros = balance_micros + ?2,
349 customer_id = COALESCE(?3, customer_id)",
350 )
351 .bind(&[
352 workspace.into(),
353 (amount_micros as f64).into(),
354 optional(customer),
355 timestamp.as_str().into(),
356 ])?,
357 ])
358 .await?;
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging359 // Money in clears a card declined at the limit. A refund or a lost
360 // dispute is a top-up of less than nothing: money out, which never
361 // clears it.
362 if kind == "top_up" && amount_micros > 0 {
The Packages page names every registry it has363 self.db
364 .prepare("UPDATE limits SET autopay_failed_at = NULL, autopay_error = NULL WHERE workspace = ?")
365 .bind(&[workspace.into()])?
366 .run()
367 .await?;
368 }
369 Ok(())
370 }
371
372 async fn account(&self, a: AccountArgs) -> Result<Outcome<Account>> {
373 let workspace = a.workspace.to_lowercase();
374 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
375 return Ok(members_only());
376 }
377 Ok(Outcome::Ok(self.standing(&workspace).await?))
378 }
379
380 async fn ledger(&self, a: AccountArgs) -> Result<Outcome<Vec<LedgerEntry>>> {
381 let workspace = a.workspace.to_lowercase();
382 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
383 return Ok(members_only());
384 }
385 let rows = self
386 .db
387 .prepare("SELECT * FROM ledger WHERE workspace = ? ORDER BY id DESC LIMIT ?")
388 .bind(&[workspace.into(), LEDGER_PAGE.into()])?
389 .all()
390 .await?
391 .results::<LedgerRow>()?;
392 Ok(Outcome::Ok(
393 rows.into_iter().map(LedgerEntry::from).collect(),
394 ))
395 }
396
397 async fn usage(&self, a: UsageArgs) -> Result<Outcome<Usage>> {
398 let workspace = a.workspace.to_lowercase();
399 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
400 return Ok(members_only());
401 }
402 #[derive(serde::Deserialize)]
403 struct SliceRow {
404 key: Option<String>,
405 micros: Option<i64>,
406 runs: Option<u32>,
407 }
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging408 // While g1t is free, what was used at cost is what there is to show.
409 // With a discount, usage at its price, so a 100% discount still
410 // shows what the workspace would pay.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit411 // Every slice measures usage at price (statement::PRICE_SQL), the one
412 // figure every page shows as usage, whatever paid for it.
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging413 let discount_percent = self.terms_of(&workspace).await?.percent_off();
Usage, Billing settings and prepaid AI credit; fixes from the UX audit414 let measure = if self.free { "COALESCE(cost_micros, 0)" } else { crate::statement::PRICE_SQL };
The Packages page names every registry it has415 let slices = |key: &str, limit: u32| {
416 format!(
417 "SELECT {key} AS key, SUM({measure}) AS micros, COUNT(*) AS runs FROM ledger
418 WHERE workspace = ?1 AND kind = 'usage' AND created_at >= ?2
419 GROUP BY 1 ORDER BY micros DESC LIMIT {limit}"
420 )
421 };
422 let query = |sql: String| {
423 let db = &self.db;
424 let workspace = workspace.clone();
425 let since = a.since.clone();
426 async move {
427 let rows = db
428 .prepare(sql)
429 .bind(&[workspace.into(), since.into()])?
430 .all()
431 .await?
432 .results::<SliceRow>()?;
433 Ok::<Vec<UsageSlice>, worker::Error>(
434 rows.into_iter()
435 .map(|row| UsageSlice {
436 key: row.key.unwrap_or_else(|| "other".to_owned()),
437 micros: row.micros.unwrap_or_default(),
438 runs: row.runs.unwrap_or_default(),
439 })
440 .collect(),
441 )
442 }
443 };
444 #[derive(serde::Deserialize)]
445 struct Totals {
446 spent: Option<i64>,
447 cost: Option<i64>,
448 provider: Option<i64>,
449 runs: Option<u32>,
450 added: Option<i64>,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging451 discount: Option<i64>,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit452 covered: Option<i64>,
453 price: Option<i64>,
The Packages page names every registry it has454 }
455 let totals = async {
456 self.db
Usage, Billing settings and prepaid AI credit; fixes from the UX audit457 .prepare(format!(
The Packages page names every registry it has458 "SELECT
459 -SUM(CASE WHEN kind = 'usage' THEN amount_micros END) AS spent,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit460 SUM(CASE WHEN kind = 'usage' THEN COALESCE(credit_micros, 0) + COALESCE(trial_micros, 0)
461 + COALESCE(oss_micros, 0) + COALESCE(given_micros, 0) END) AS covered,
462 SUM(CASE WHEN kind = 'usage' THEN {price} END) AS price,",
463 price = crate::statement::PRICE_SQL
464 ) + "
The Packages page names every registry it has465 SUM(CASE WHEN kind = 'usage' AND COALESCE(billed_to, 'g1t') = 'g1t' THEN cost_micros END) AS cost,
466 SUM(CASE WHEN kind = 'usage' AND billed_to = 'workspace' THEN cost_micros END) AS provider,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit467 SUM(CASE WHEN kind = 'usage' AND " + crate::statement::RUN_SQL + " THEN 1 ELSE 0 END) AS runs,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging468 SUM(CASE WHEN kind = 'top_up' THEN amount_micros END) AS added,
469 SUM(CASE WHEN kind = 'usage' THEN discount_micros END) AS discount
The Packages page names every registry it has470 FROM ledger WHERE workspace = ?1 AND created_at >= ?2",
471 )
472 .bind(&[workspace.as_str().into(), a.since.as_str().into()])?
473 .first::<Totals>(None)
474 .await
475 };
476 // The totals and the five slices read the same rows independently,
477 // so they go to D1 at once: one round trip of waiting, not six.
478 let (totals, (by_day, by_task, by_repo, by_pull, by_model)) = try_join(
479 totals,
480 try_join5(
481 query(slices("substr(created_at, 1, 10) || '/' || COALESCE(task, 'other')", 400)),
482 query(slices("task", 20)),
483 query(slices("repo", 20)),
484 query(slices("repo || '#' || number", 10)),
485 query(slices("model", 10)),
486 ),
487 )
488 .await?;
489 let totals = totals.unwrap_or(Totals {
490 spent: None,
491 cost: None,
492 provider: None,
493 runs: None,
494 added: None,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging495 discount: None,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit496 covered: None,
497 price: None,
The Packages page names every registry it has498 });
499 Ok(Outcome::Ok(Usage {
500 spent_micros: totals.spent.unwrap_or_default(),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit501 // What the included usage, the trial, a pool or g1t paid, as the
502 // entries record it.
503 covered_micros: totals.covered.unwrap_or_default(),
504 price_micros: if self.free { totals.cost.unwrap_or_default() } else { totals.price.unwrap_or_default() },
The Packages page names every registry it has505 cost_micros: totals.cost.unwrap_or_default(),
506 provider_micros: totals.provider.unwrap_or_default(),
507 used_micros: totals.cost.unwrap_or_default() + totals.provider.unwrap_or_default(),
508 free: self.free,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging509 discount_micros: totals.discount.unwrap_or_default(),
510 discount_percent: (discount_percent > 0).then_some(discount_percent),
The Packages page names every registry it has511 runs: totals.runs.unwrap_or_default(),
512 added_micros: totals.added.unwrap_or_default(),
513 by_day,
514 by_task,
515 by_repo,
516 by_pull,
517 by_model,
518 since: a.since,
519 }))
520 }
521
522 /// `checkout`: prepays usage, by card or (from $1,000) bank transfer.
523 async fn checkout(&self, a: CheckoutArgs) -> Result<Outcome<Checkout>> {
524 let workspace = a.workspace.to_lowercase();
Merge main (membership, two-factor, GitHub repo roles) into tokens525 if !a.actor.manages_billing(&workspace) {
The Packages page names every registry it has526 return Ok(Outcome::fail(
527 FailureCode::Forbidden,
Merge main (membership, two-factor, GitHub repo roles) into tokens528 "Only an owner or a billing manager can prepay for a workspace.",
The Packages page names every registry it has529 ));
530 }
531 let Some(stripe) = &self.stripe else {
532 return Ok(Outcome::fail(
533 FailureCode::Conflict,
534 "Payments are not set up on this g1t yet.",
535 ));
536 };
537 let bank_transfer = a.method.as_deref() == Some("bank_transfer");
538 if let Err(why) = prepay_amount(a.amount_cents, bank_transfer) {
539 return Ok(Outcome::fail(FailureCode::Invalid, why));
540 }
541 // A bank transfer needs a customer for its account details.
542 let customer = if bank_transfer {
543 match self.customer_for(&workspace).await {
544 Ok(customer) => Some(customer),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit545 Err(error) => return Ok(Outcome::fail(FailureCode::Conflict, stripe::friendly(&error))),
The Packages page names every registry it has546 }
547 } else {
548 self.row(&workspace).await?.and_then(|row| row.customer_id)
549 };
Merge Stripe Tax, the card fee on card payments, and one free workspace per person550 // By card, Stripe's fee is its own line; a bank transfer has none.
551 let fee = if bank_transfer { 0 } else { self.card_fee_on(i64::from(a.amount_cents)).await? as u32 };
552 let started = match stripe.start_checkout(&workspace, a.amount_cents, fee, customer.as_deref(), &a.return_url, bank_transfer).await {
The Packages page names every registry it has553 // A customer saved under another Stripe account: start afresh.
554 Err(error) if customer.is_some() && stripe::is_missing(&error) => {
555 self.forget_customer(&workspace).await?;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit556 let customer = if bank_transfer { self.customer_for(&workspace).await.ok() } else { None };
Merge Stripe Tax, the card fee on card payments, and one free workspace per person557 stripe.start_checkout(&workspace, a.amount_cents, fee, customer.as_deref(), &a.return_url, bank_transfer).await
The Packages page names every registry it has558 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit559 other => other,
The Packages page names every registry it has560 };
Merge Stripe Tax, the card fee on card payments, and one free workspace per person561 self.page_opened(started, &workspace, a.amount_cents, fee, &a.actor.username, None).await
Usage, Billing settings and prepaid AI credit; fixes from the UX audit562 }
563
564 /// A payment page Stripe started (or refused), recorded in `checkouts`
565 /// and handed back; any failure as a sentence for the page, never a
566 /// raw error.
567 pub(crate) async fn page_opened(
568 &self,
569 started: Result<stripe::Session>,
570 workspace: &str,
571 amount_cents: u32,
572 fee_cents: u32,
573 created_by: &str,
574 feature: Option<&str>,
575 ) -> Result<Outcome<Checkout>> {
576 let session = match started {
577 Ok(session) => session,
578 Err(error) => {
579 worker::console_error!("{workspace}: Stripe refused a payment page ({}): {error}", feature.unwrap_or("prepay"));
580 return Ok(Outcome::fail(FailureCode::Conflict, stripe::friendly(&error)));
581 }
The Packages page names every registry it has582 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit583 let Some(url) = session.url.clone() else {
584 return Ok(Outcome::fail(FailureCode::Conflict, "Stripe returned no payment page. Try again in a minute."));
585 };
586 let recorded = self
587 .record_checkout(&NewCheckout { id: &session.id, workspace, amount_cents, fee_cents, created_by, feature })
588 .await;
589 if let Err(error) = recorded {
590 worker::console_error!("{workspace}: a payment page could not be recorded: {error}");
591 return Ok(Outcome::fail(FailureCode::Conflict, "g1t could not keep track of the payment page. Nothing was charged; try again."));
592 }
The Packages page names every registry it has593 Ok(Outcome::Ok(Checkout { url }))
594 }
595
596 /// Credits a payment if the processor says it was made and it has not
597 /// been credited before. The amount credited is what the processor
598 /// says was paid, not what anyone here remembers asking for.
599 async fn confirm(&self, a: ConfirmArgs) -> Result<Outcome<Account>> {
600 let workspace = a.workspace.to_lowercase();
601 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
602 return Ok(members_only());
603 }
604 let (Some(stripe), Some(checkout)) = (
605 &self.stripe,
606 self.db
607 .prepare(
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging608 // A prepayment only: a plan's or a card check's page is
609 // settled where it was started, never credited as money
610 // paid in advance.
Merge Stripe Tax, the card fee on card payments, and one free workspace per person611 "SELECT workspace, created_by, fee_cents FROM checkouts
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging612 WHERE id = ? AND workspace = ? AND status = 'open' AND feature IS NULL",
The Packages page names every registry it has613 )
614 .bind(&[a.session.as_str().into(), workspace.as_str().into()])?
615 .first::<CheckoutRow>(None)
616 .await?,
617 ) else {
618 // Unknown, someone else's, or already credited: nothing to do.
619 return Ok(Outcome::Ok(self.standing(&workspace).await?));
620 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit621 let session = match stripe.session(&a.session).await {
622 Ok(session) => session,
623 Err(error) => return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error))),
624 };
Merge Stripe Tax, the card fee on card payments, and one free workspace per person625 if session.payment_status == "paid" && session.amount_total.is_some() {
The Packages page names every registry it has626 // Only whoever flips it from open to paid enters the credit.
627 let claimed = self
628 .db
629 .prepare(
630 "UPDATE checkouts SET status = 'paid' WHERE id = ? AND status = 'open'
631 RETURNING id",
632 )
633 .bind(&[a.session.as_str().into()])?
634 .first::<Touched>(None)
635 .await?;
636 if claimed.is_some() {
Merge Stripe Tax, the card fee on card payments, and one free workspace per person637 self.credit_prepayment(&checkout.workspace, &session, checkout.fee_cents.unwrap_or(0), &checkout.created_by).await?;
The Packages page names every registry it has638 }
639 }
640 Ok(Outcome::Ok(self.standing(&workspace).await?))
641 }
642
Merge Stripe Tax, the card fee on card payments, and one free workspace per person643 /// Enters a paid prepayment page: what its lines came to before tax,
644 /// less the card fee, as credit; its tax and fee kept apart. Once the
645 /// page is claimed `open → paid`. The credit in cents.
646 pub(crate) async fn credit_prepayment(&self, workspace: &str, session: &stripe::Session, fee_cents: u32, by: &str) -> Result<i64> {
647 let fee = i64::from(fee_cents);
648 let cents = tax::prepay_credit_cents(session.before_tax_cents(), fee);
649 let tax_cents = session.tax_cents();
650 let description = if tax_cents > 0 || fee > 0 {
651 format!(
652 "Paid in advance (tax {} and card fee {} paid with it)",
653 features::cents(tax_cents * 10_000),
654 features::cents(fee * 10_000)
655 )
656 } else {
657 "Paid in advance".to_owned()
658 };
659 self.enter(
660 workspace,
661 EntryKind::TopUp,
662 cents * MICROS_PER_DOLLAR / 100,
663 &description,
664 &session.id,
665 None,
666 Some(by),
667 session.customer.as_deref(),
668 )
669 .await?;
670 self.record_extras(workspace, &session.id, session.payment_intent.as_deref(), tax::Extras { tax_cents, fee_cents: fee }, None)
671 .await?;
672 Ok(cents)
673 }
674
The Packages page names every registry it has675 /// Drops a saved customer the card processor no longer knows.
676 pub(crate) async fn forget_customer(&self, workspace: &str) -> Result<()> {
677 self.db
678 .prepare(
679 "UPDATE accounts SET customer_id = NULL, card_brand = NULL, card_last4 = NULL, card_exp_month = NULL,
680 card_exp_year = NULL, card_synced_at = NULL WHERE workspace = ?",
681 )
682 .bind(&[workspace.into()])?
683 .run()
684 .await?;
685 Ok(())
686 }
687}
688
689impl Billing {
690 /// Records how long a sandbox ran, with its cost and its charge: every
691 /// second, from the first, at the price book's price; on its own CPU
692 /// when it reports it. Settles its reservation, if it names one.
693 async fn record_sandbox(&self, a: RecordSandboxArgs) -> Result<Outcome<bool>> {
694 // Self-hosted time is recorded wherever g1t runs, for its minutes;
695 // anything else only where there is a bill to put it on.
696 if (self.stripe.is_none() && !a.self_hosted) || a.seconds == 0 {
697 return Ok(Outcome::Ok(false));
698 }
699 let workspace = a.workspace.to_lowercase();
700 let seen = self
701 .db
702 .prepare("SELECT id FROM ledger WHERE reference = ?")
703 .bind(&[a.reference.as_str().into()])?
704 .first::<Touched>(None)
705 .await?;
706 if seen.is_some() {
707 return Ok(Outcome::Ok(false));
708 }
709 let now = now_ms();
710 let timestamp = rfc3339(now);
711 let seconds = i64::from(a.seconds);
712 // On the workspace's own machine: its minutes go on usage, at $0,
713 // and whatever was reserved for it is given back.
714 if a.self_hosted {
715 let description = format!("{}: {} of self-hosted runner time, $0", a.description, duration(seconds));
716 self.db
717 .prepare(
718 "INSERT INTO ledger
719 (id, workspace, kind, amount_micros, description, repo, task,
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.720 cost_micros, reference, created_at, billed_to, credit_micros, trial_micros, oss_micros, given_micros, quantity)
721 VALUES (?, ?, 'usage', 0, ?, ?, 'self_hosted', 0, ?, ?, 'workspace', 0, 0, 0, 0, ?)",
The Packages page names every registry it has722 )
723 .bind(&[
724 new_id("led", now).into(),
725 workspace.as_str().into(),
726 description.as_str().into(),
727 optional(a.repo.as_deref()),
728 a.reference.as_str().into(),
729 timestamp.as_str().into(),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit730 (seconds as f64).into(),
The Packages page names every registry it has731 ])?
732 .run()
733 .await?;
734 if let Some(reservation) = &a.reservation_id {
735 self.settle_reservation(SettleArgs { reservation_id: reservation.clone(), actual_micros: 0 }).await?;
736 }
737 return Ok(Outcome::Ok(true));
738 }
739 // From the price book, which follows what Cloudflare bills g1t. A
740 // sandbox is the same container as a build, so without a row it is
741 // a build second's cost plus the margin.
742 let (cost_per_second, _) = self.price("sandbox_second").await?.unwrap_or_else(|| {
743 let cost = deployment_costs::MICROS_PER_BUILD_SECOND as f64;
744 (cost, Price::price_for(cost, self.margin_percent))
745 });
746 // A larger machine (`runs-on: g1t-4core`): its memory and disk
747 // cost more each second, and without its own CPU it is priced at
748 // its vCPUs as busy as the standard machine's.
749 let instance = a.instance.as_deref().and_then(g1t_contracts::actions::instance_named);
750 let base_scale = instance.map_or(1.0, |i| keeper::base_scale(i.memory_gib, i.disk_gb));
751 let price_scale = instance.map_or(1.0, |i| i.price_scale);
752 let vcpus = instance.map_or(4.0, |i| i.vcpu.max(4.0));
753 // Its own CPU when the sandbox reports it; otherwise the average.
754 let parts = match a.cpu_seconds.filter(|cpu| cpu.is_finite() && *cpu >= 0.0) {
755 Some(cpu) => match (self.price("sandbox_base_second").await?, self.price("sandbox_cpu_second").await?) {
756 (Some((base, _)), Some((vcpu, _))) => {
757 Some((keeper::run_cost(seconds, cpu.min(seconds as f64 * vcpus), base * base_scale, vcpu), cpu))
758 }
759 _ => None,
760 },
761 None => None,
762 };
763 let cost = parts.map_or(seconds as f64 * cost_per_second * price_scale, |(cost, _)| cost).ceil() as i64;
Merge branch 'worktree-agent-a633ac0f7f66d419d'764 let (charge, terms_note, discount) = self.charged(&workspace, credits::with_margin(cost, self.margin_percent)).await?;
The Packages page names every registry it has765 let eligible = credits::eligible_for(a.kind, a.repo.as_deref());
766 let drawn = self.draw(&workspace, charge, &credits::month_of(&timestamp), &eligible).await?;
767 let charge = charge - drawn.total();
768 let cpu_note = parts.map_or(String::new(), |(_, cpu)| format!(", {cpu:.0} vCPU-seconds"));
769 let description = format!("{}: {} of sandbox time{cpu_note}{terms_note}{}", a.description, duration(seconds), drawn.note());
770 // The price versions it was charged at (pricing.rs).
771 let meters: &[&str] = if parts.is_some() { &["sandbox_base_second", "sandbox_cpu_second"] } else { &["sandbox_second"] };
772 let mut versions = Vec::new();
773 for meter in meters {
774 versions.extend(self.version_now(meter).await?);
775 }
776 let price_version = versions.join(",");
777 self.db
778 .batch(vec![
779 self.db
780 .prepare(
781 "INSERT INTO ledger
782 (id, workspace, kind, amount_micros, description, repo, task,
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.783 cost_micros, reference, created_at, billed_to, credit_micros, trial_micros, oss_micros, given_micros, price_version, quantity, compute)
784 VALUES (?, ?, 'usage', ?, ?, ?, 'sandbox', ?, ?, ?, 'g1t', ?, ?, ?, ?, ?, ?, ?)",
The Packages page names every registry it has785 )
786 .bind(&[
787 new_id("led", now).into(),
788 workspace.as_str().into(),
789 (-(charge as f64)).into(),
790 description.as_str().into(),
791 optional(a.repo.as_deref()),
792 (cost as f64).into(),
793 a.reference.as_str().into(),
794 timestamp.as_str().into(),
795 (drawn.credit as f64).into(),
796 (drawn.trial as f64).into(),
797 (drawn.oss as f64).into(),
798 (drawn.given as f64).into(),
799 optional(Some(price_version.as_str()).filter(|v| !v.is_empty())),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit800 (seconds as f64).into(),
801 optional(a.kind.map(ComputeKind::as_str)),
The Packages page names every registry it has802 ])?,
803 self.db
804 .prepare(
805 "INSERT INTO accounts (workspace, balance_micros, created_at)
806 VALUES (?1, ?2, ?3)
807 ON CONFLICT (workspace) DO UPDATE SET balance_micros = balance_micros + ?2",
808 )
809 .bind(&[
810 workspace.as_str().into(),
811 (-(charge as f64)).into(),
812 timestamp.as_str().into(),
813 ])?,
814 ])
815 .await?;
Merge branch 'worktree-agent-a633ac0f7f66d419d'816 self.record_discount(&a.reference, discount).await?;
The Packages page names every registry it has817 self.count_spend(&workspace, cost, charge, &drawn).await;
818 if let Some(reservation) = &a.reservation_id {
819 self.settle_reservation(SettleArgs { reservation_id: reservation.clone(), actual_micros: cost }).await?;
820 }
821 Ok(Outcome::Ok(true))
822 }
823}
824
825/// Whether an amount may be prepaid: $25 at the least by card, and from
826/// $1,000 by bank transfer.
827pub(crate) fn prepay_amount(cents: u32, bank_transfer: bool) -> std::result::Result<(), String> {
828 let (min, max) = if bank_transfer { (MIN_BANK_TRANSFER_CENTS, MAX_BANK_TRANSFER_CENTS) } else { (MIN_TOP_UP_CENTS, MAX_TOP_UP_CENTS) };
829 if (min..=max).contains(&cents) {
830 return Ok(());
831 }
832 Err(if bank_transfer {
833 format!("Prepay between ${} and ${} by bank transfer.", min / 100, group(max / 100))
834 } else {
835 format!("Prepay between ${} and ${} by card; from $1,000, a bank transfer works too.", min / 100, group(max / 100))
836 })
837}
838
839/// `10,000` for 10000.
840fn group(n: u32) -> String {
841 let digits = n.to_string();
842 let mut out = String::new();
843 for (i, c) in digits.chars().enumerate() {
844 if i > 0 && (digits.len() - i).is_multiple_of(3) {
845 out.push(',');
846 }
847 out.push(c);
848 }
849 out
850}
851
852#[cfg(test)]
853/// What `seconds` of sandbox time are charged at `price_per_second`, in
854/// millionths of a dollar: every second, rounded up to the next millionth.
855fn sandbox_charge(seconds: i64, price_per_second: f64) -> i64 {
856 (seconds as f64 * price_per_second).ceil() as i64
857}
858
859/// `1h 2m`, `3m 12s` or `40s`.
860fn duration(seconds: i64) -> String {
861 let (h, m, s) = (seconds / 3600, seconds % 3600 / 60, seconds % 60);
862 if h > 0 {
863 format!("{h}h {m}m")
864 } else if m > 0 {
865 format!("{m}m {s}s")
866 } else {
867 format!("{s}s")
868 }
869}
870
871impl Billing {
872 /// What a workspace is charged for something that would be `base`:
873 /// nothing while g1t is free, or as its account's terms say. With a
874 /// note for the statement when it differs.
Merge branch 'worktree-agent-a633ac0f7f66d419d'875 /// A charge at cost plus the margin (`base`) on the account's terms:
876 /// what is charged, the note for the statement, and what a discount
877 /// gave away below `base`. That last is written on the entry
878 /// (`record_discount`) so the reconciliation counts it as given, never
879 /// as margin lost: a sold charge is worth at least its cost plus the
880 /// margin.
881 pub(crate) async fn charged(&self, workspace: &str, base: i64) -> Result<(i64, String, i64)> {
The Packages page names every registry it has882 if self.free {
Merge branch 'worktree-agent-a633ac0f7f66d419d'883 return Ok((0, " (free while g1t is being built out)".to_owned(), 0));
The Packages page names every registry it has884 }
885 let terms = self.terms_of(workspace).await?;
Merge branch 'worktree-agent-a633ac0f7f66d419d'886 let (charge, discount) = terms.discounted(base);
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging887 let note = match terms.discount_label() {
888 Some(label) if base > 0 => format!(" ({label})"),
The Packages page names every registry it has889 _ => String::new(),
890 };
Merge branch 'worktree-agent-a633ac0f7f66d419d'891 Ok((charge, note, discount))
892 }
893
894 /// What a discount gave away on an entry, below cost plus the margin
895 /// (less than nothing on a correction down).
896 pub(crate) async fn record_discount(&self, reference: &str, micros: i64) -> Result<()> {
897 if micros == 0 {
898 return Ok(());
899 }
900 self.db
901 .prepare("UPDATE ledger SET discount_micros = ? WHERE reference = ?")
902 .bind(&[(micros as f64).into(), reference.into()])?
903 .run()
904 .await?;
905 Ok(())
The Packages page names every registry it has906 }
907}
908
909fn members_only<T>() -> Outcome<T> {
910 Outcome::fail(
911 FailureCode::Forbidden,
912 "Only members can see a workspace's billing.",
913 )
914}
915
916impl Billing {
917 fn from_env(env: &Env) -> Result<Self> {
918 Ok(Billing {
919 db: env.d1("DB")?,
920 stripe: env
921 .secret("STRIPE_SECRET_KEY")
922 .ok()
923 .map(|key| key.to_string())
924 .filter(|key| !key.is_empty())
925 .map(Stripe::new),
926 webhook_secret: env
927 .secret("STRIPE_WEBHOOK_SECRET")
928 .ok()
929 .map(|secret| secret.to_string().trim().to_owned())
930 .filter(|secret| !secret.is_empty()),
931 margin_percent: env
932 .var("MARGIN_PERCENT")
933 .ok()
934 .and_then(|percent| percent.to_string().parse().ok())
935 .unwrap_or(20),
936 free: env.var("FREE_WHILE_BUILDING").is_ok_and(|v| v.to_string() == "true"),
937 ceilings: limits::Ceilings::from_env(env),
938 trials_on: {
939 let plans = credits::Config::from_env(env);
940 plans.trial_workspace_micros > 0 && plans.trial_monthly_pool_micros > 0
941 },
942 plans: credits::Config::from_env(env),
943 repos: env.service("REPOS").ok(),
944 packages: env.service("PACKAGES").ok(),
945 identity: env.service("IDENTITY").ok(),
946 caps: budget::Caps::from_env(env),
947 env: env.clone(),
948 })
949 }
950}
951
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails952/// The fifteen-minute steps, each on its own: one failing never skips the
953/// rest.
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.954async fn tick(billing: &Billing, env: &Env) {
The Packages page names every registry it has955 // Stripe events billing never received, handled now.
956 match billing.replay_events().await {
957 Ok(done) => worker::console_log!("stripe replay: {done}"),
958 Err(error) => worker::console_error!("replaying Stripe events failed: {error}"),
959 }
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails960 // This month's users, read once for autopay and the warnings.
961 let users = if billing.stripe.is_some() {
962 match billing.month_users().await {
963 Ok(users) => Some(users),
964 Err(error) => {
965 worker::console_error!("reading this month's users failed: {error}");
966 None
967 }
968 }
969 } else {
970 None
971 };
972 if let Some(users) = &users
973 && let Err(error) = billing.autopay(users).await {
974 worker::console_error!("paying at the limit failed: {error}");
975 }
The Packages page names every registry it has976 // Last month's metered usage (scans, embeddings, storage) goes on the
977 // ledger before the month is closed and invoiced.
978 if let Err(error) = billing.charge_pending().await {
979 worker::console_error!("charging last month's metered usage failed: {error}");
980 }
981 if let Err(error) = billing.close_months().await {
982 worker::console_error!("closing the month failed: {error}");
983 }
984 if let Err(error) = billing.invoice_enterprises().await {
985 worker::console_error!("invoicing enterprises failed: {error}");
986 }
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails987 if let Some(users) = &users
988 && let Ok(identity) = env.service("IDENTITY")
989 && let Err(error) = billing.warn_limits(&identity, users).await {
990 worker::console_error!("warning owners failed: {error}");
991 }
992}
993
994/// Every fifteen minutes: runs settled, Stripe replayed, cards charged at
995/// the limit, months closed, owners warned. Once a day, at
996/// [`keeper::DAILY`], in an invocation of its own: the slower checks
997/// against Stripe and Cloudflare, and measuring.
998///
999/// Each step logs its own failure and the next still runs. The spend
1000/// breaker and budget alerts go first: a step that runs out of CPU ends
1001/// the invocation, and must never take the alerts with it.
1002#[event(scheduled)]
1003async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
1004 let Ok(billing) = Billing::from_env(&env) else {
1005 return;
1006 };
The Packages page names every registry it has1007 // Comped budgets' alerts, and a tripped breaker staff were not told of.
1008 if let Err(error) = billing.watch_spend().await {
1009 worker::console_error!("watching g1t's own spend failed: {error}");
1010 }
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails1011 let keeper = keeper::Keeper::from_env(&env);
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)1012 // Once an hour, at the quarter past: what Cloudflare counted for the
1013 // whole platform in the hour before, against its thresholds
1014 // (platform.rs).
1015 if event.cron() == keeper::QUARTER_HOURLY && platform::hourly_due(now_ms()) {
1016 match billing.watch_platform(&keeper).await {
1017 Ok((read, breached)) => worker::console_log!("platform watch: {read} metrics, {breached} breaches"),
1018 Err(error) => worker::console_error!("watching platform usage failed: {error}"),
1019 }
1020 }
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails1021 let daily = event.cron() == keeper::DAILY;
1022 if !daily {
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.1023 tick(&billing, &env).await;
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails1024 }
The Packages page names every registry it has1025 // Once a day: Stripe's endpoint kept listening to billing's events and
1026 // enabled, and saved cards and plans not read in a while read again.
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails1027 if daily {
The Packages page names every registry it has1028 match billing.keep_endpoint("billing").await {
1029 Ok(done) => worker::console_log!("stripe endpoint: {done}"),
1030 Err(error) => worker::console_error!("keeping Stripe's endpoint failed: {error}"),
1031 }
1032 match billing.refresh_from_stripe().await {
1033 Ok(done) => worker::console_log!("stripe refresh: {done}"),
1034 Err(error) => worker::console_error!("refreshing from Stripe failed: {error}"),
1035 }
1036 }
1037 // Once a day, and at once if the costs were never checked: check every
1038 // cost against what Cloudflare billed.
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails1039 if (daily || billing.never_checked().await.unwrap_or(false))
The Packages page names every registry it has1040 && let Err(error) = billing.reconcile(&keeper).await {
1041 worker::console_error!("checking costs against Cloudflare failed: {error}");
1042 }
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging1043 // Once a day: credit from g1t past its expiry stops counting
1044 // (grants.rs), before the day is reconciled.
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails1045 if daily {
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging1046 match billing.expire_credits().await {
1047 Ok(closed) => worker::console_log!("credits expired: {closed}"),
1048 Err(error) => worker::console_error!("expiring credits failed: {error}"),
1049 }
1050 }
The Packages page names every registry it has1051 // Once a day: what Cloudflare charged, reconciled against what g1t
1052 // counted and charged; prices whose day has come; margin alerts
1053 // (margin.rs). After the keeper, so its proposals are in.
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails1054 if daily {
The Packages page names every registry it has1055 match billing.costs_daily(&env, &keeper).await {
1056 Ok(run) => worker::console_log!("costs: {} lines, {} days, {} proposals, {} alerts", run.lines, run.days, run.proposals, run.alerts),
1057 Err(error) => worker::console_error!("reconciling costs failed: {error}"),
1058 }
1059 }
1060 // Once a day: what each workspace's private repositories hold, its git
1061 // operations, Deployments plans from before the g1t plan set to end,
1062 // and old reservations cleared.
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails1063 if daily {
The Packages page names every registry it has1064 if let Err(error) = billing.measure_packages().await {
1065 worker::console_error!("measuring package storage failed: {error}");
1066 }
1067 if let Err(error) = billing.measure_storage().await {
1068 worker::console_error!("measuring storage failed: {error}");
1069 }
1070 if let Err(error) = billing.measure_git().await {
1071 worker::console_error!("measuring git operations failed: {error}");
1072 }
1073 if let Err(error) = billing.retire_deployments_plans().await {
1074 worker::console_error!("ending Deployments plans failed: {error}");
1075 }
Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how.1076 if let Err(error) = billing.retire_security_activations().await {
1077 worker::console_error!("ending Security and quality activations failed: {error}");
1078 }
The Packages page names every registry it has1079 if let Err(error) = billing.sweep_reservations().await {
1080 worker::console_error!("clearing reservations failed: {error}");
1081 }
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.1082 // The AI Gateway's request log, from when there was one, still
1083 // keeps only its last 30 days until it is empty.
1084 let cutoff = rfc3339(now_ms().saturating_sub(30 * 24 * 60 * 60 * 1000));
1085 let cleared = match billing.db.prepare("DELETE FROM gateway_requests WHERE created_at < ?").bind(&[cutoff.into()]) {
1086 Ok(statement) => statement.run().await.map(|_| ()),
1087 Err(error) => Err(error),
1088 };
1089 if let Err(error) = cleared {
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1090 worker::console_error!("clearing old AI Gateway requests failed: {error}");
1091 }
The Packages page names every registry it has1092 }
1093}
1094
1095/// Events from the bus, on billing's own queue: only a workspace's rename
1096/// matters here (see `rename`).
1097#[event(queue)]
1098async fn queue(batch: MessageBatch<g1t_contracts::events::Event>, env: Env, _ctx: Context) -> Result<()> {
1099 let billing = Billing::from_env(&env)?;
1100 let identity = env.service("IDENTITY").ok();
1101 for message in batch.messages()? {
1102 // A repository transferred: its share of the open-source pool this
1103 // month follows it. What it was charged stays with the workspace
1104 // it was charged to; usage from now on is charged to the new one.
1105 if g1t_kit::transfer::on_event(&env, &billing.db, message.body(), closing::TRANSFERRED).await? {
1106 message.ack();
1107 continue;
1108 }
1109 billing.on_event(identity.as_ref(), message.body()).await?;
1110 message.ack();
1111 }
1112 Ok(())
1113}
1114
1115#[event(fetch)]
1116async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
1117 let Some(method) = rpc_method(&request) else {
1118 return Response::error("Not found", 404);
1119 };
1120 // A replica near the caller when it asks for one (crates/kit/src/d1.rs);
1121 // what other services call (can_start, start_run) asks for none.
1122 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
1123 let body: serde_json::Value = request.json().await?;
1124 let mut billing = Billing::from_env(&env)?;
1125 billing.db = db;
1126 let answered = match method.as_str() {
1127 "status" => reply(&billing.status()),
1128 "account" => reply(&billing.account(args(body)?).await?),
1129 "ledger" => reply(&billing.ledger(args(body)?).await?),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging1130 "credits" => reply(&billing.credits(args(body)?).await?),
The Packages page names every registry it has1131 "usage" => reply(&billing.usage(args(body)?).await?),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1132 "usage_report" => reply(&billing.usage_report(args(body)?).await?),
1133 "set_budget" => reply(&billing.set_budget(args(body)?).await?),
1134 "billing_details" => reply(&billing.billing_details(args(body)?).await?),
1135 "set_billing_details" => reply(&billing.set_billing_details(args(body)?).await?),
The Packages page names every registry it has1136 "checkout" => reply(&billing.checkout(args(body)?).await?),
1137 "confirm" => reply(&billing.confirm(args(body)?).await?),
1138 "trial" => reply(&billing.trial(args(body)?).await?),
1139 "features" => reply(&billing.features(args(body)?).await?),
1140 "subscribe" => reply(&billing.subscribe(args(body)?).await?),
1141 "confirm_subscription" => reply(&billing.confirm_subscription(args(body)?).await?),
1142 "cancel_subscription" => reply(&billing.cancel_subscription(args(body)?).await?),
1143 "close_workspace" => reply(&billing.close_workspace(args(body)?).await?),
1144 "has_feature" => reply(&billing.has_feature(args(body)?).await?),
Merge Stripe Tax, the card fee on card payments, and one free workspace per person1145 "free_workspaces" => reply(&billing.free_workspaces(args(body)?).await?),
The Packages page names every registry it has1146 "charge_feature" => reply(&billing.charge_feature(args(body)?).await?),
1147 "record_sandbox" => reply(&billing.record_sandbox(args(body)?).await?),
1148 "limit" => reply(&billing.limit(args(body)?).await?),
1149 "check_limit" => reply(&billing.check_limit(args(body)?).await?),
1150 "set_spend_limit" => reply(&billing.set_spend_limit(args(body)?).await?),
1151 "prices" => reply(&billing.prices().await?),
1152 "billing_portal" => reply(&billing.billing_portal(args(body)?).await?),
1153 "admin_billing_link" => reply(&billing.admin_billing_link(args(body)?).await?),
1154 "admin_stripe" => reply(&billing.admin_stripe(args(body)?).await?),
1155 "admin_enterprise_billing" => reply(&billing.admin_enterprise_billing(args(body)?).await?),
Merge Stripe Tax, the card fee on card payments, and one free workspace per person1156 "admin_enterprise_address" => reply(&billing.admin_enterprise_address(args(body)?).await?),
The Packages page names every registry it has1157 "admin_invoice_enterprise" => reply(&billing.admin_invoice_enterprise(args(body)?).await?),
1158 "stripe_webhook" => reply(&billing.stripe_webhook(args(body)?).await?),
1159 "invoices" => reply(&billing.invoices(args(body)?).await?),
1160 "statement" => reply(&billing.statement(args(body)?).await?),
1161 "statement_entries" => reply(&billing.statement_entries(args(body)?).await?),
1162 "usage_meters" => reply(&billing.usage_meters(args(body)?).await?),
1163 "admin_workspace_invoices" => {
1164 let a: AdminWorkspaceInvoicesArgs = args(body)?;
1165 reply(&billing.workspace_invoices(&a.workspace.to_lowercase()).await?)
1166 }
1167 "admin_signals" => reply(&billing.admin_signals(args(body)?).await?),
1168 "admin_overview" => reply(&billing.admin_overview(args(body)?).await?),
1169 "admin_sales" => reply(&billing.admin_sales(args(body)?).await?),
1170 "admin_set_sales" => reply(&billing.admin_set_sales(args(body)?).await?),
1171 "admin_add_note" => reply(&billing.admin_add_note(args(body)?).await?),
1172 "admin_invoices" => reply(&billing.admin_invoices(args(body)?).await?),
1173 "admin_audit" => reply(&billing.admin_audit(args(body)?).await?),
1174 // A staff change made in another service, for sudo's audit log:
1175 // identity's restores and purges of deleted workspaces.
1176 "admin_log" => {
1177 let a: AdminLogArgs = args(body)?;
1178 billing
1179 .audit(&accounts::own_account(&a.workspace), &a.action, &a.detail, &a.by)
1180 .await?;
1181 reply(&true)
1182 }
1183 "note_pending" => reply(&billing.note_pending(args(body)?).await?),
1184 "admin_accounts" => reply(&billing.admin_accounts(args(body)?).await?),
1185 "admin_account" => reply(&billing.admin_account(args(body)?).await?),
1186 "admin_set_terms" => reply(&billing.admin_set_terms(args(body)?).await?),
1187 "admin_create_enterprise" => reply(&billing.admin_create_enterprise(args(body)?).await?),
1188 "admin_attach" => reply(&billing.admin_attach(args(body)?).await?),
1189 "admin_credit" => reply(&billing.admin_credit(args(body)?).await?),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging1190 "admin_credits" => reply(&billing.admin_credits(args(body)?).await?),
1191 "admin_revoke_credit" => reply(&billing.admin_revoke_credit(args(body)?).await?),
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 sold1192 "admin_reset_billing" => reply(&billing.admin_reset_billing(&env, args(body)?).await?),
The Packages page names every registry it has1193 "admin_set_allowances" => reply(&billing.admin_set_allowances(args(body)?).await?),
1194 "entitlements" => reply(&billing.entitlements(args(body)?).await?),
1195 "audit_retention" => reply(&billing.audit_retention(args(body)?).await?),
1196 "reserve" => reply(&billing.reserve(args(body)?).await?),
1197 "settle" => reply(&billing.settle_reservation(args(body)?).await?),
1198 "card_check" => reply(&billing.card_check(args(body)?).await?),
1199 "confirm_card_check" => reply(&billing.confirm_card_check(args(body)?).await?),
1200 "request_limit" => reply(&billing.request_limit(args(body)?).await?),
1201 "limit_requests" => reply(&billing.limit_requests(args(body)?).await?),
1202 "confirm_spike" => reply(&billing.confirm_spike(args(body)?).await?),
1203 "set_caps" => reply(&billing.set_caps(args(body)?).await?),
1204 "admin_limit_requests" => reply(&billing.admin_limit_requests(args(body)?).await?),
1205 "admin_decide_limit_request" => reply(&billing.admin_decide_limit_request(args(body)?).await?),
1206 "admin_overages" => reply(&billing.admin_overages(args(body)?).await?),
1207 "admin_goodwill" => reply(&billing.admin_goodwill(args(body)?).await?),
1208 "admin_velocity" => reply(&billing.admin_velocity(args(body)?).await?),
1209 "admin_record_payment" => reply(&billing.admin_record_payment(args(body)?).await?),
1210 "admin_costs" => reply(&billing.admin_costs(args(body)?, keeper::Keeper::from_env(&env).can_read_bill()).await?),
1211 "admin_cost_alerts" => reply(&billing.admin_cost_alerts(args(body)?).await?),
1212 "admin_spend_caps" => reply(&billing.spend_caps().await?),
1213 "admin_lift_breaker" => reply(&billing.admin_lift_breaker(args(body)?).await?),
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)1214 // Platform pauses (src/platform.rs): read by every service that
1215 // honours one, kept 30 seconds in each isolate.
1216 "platform_pause" => reply(&billing.pause_now().await),
1217 "admin_platform_guard" => reply(&billing.platform_guard(&keeper::Keeper::from_env(&env)).await?),
1218 "admin_set_pause" => reply(&billing.admin_set_pause(args(body)?, &keeper::Keeper::from_env(&env)).await?),
The Packages page names every registry it has1219 "admin_decide_proposal" => reply(&billing.admin_decide_proposal(args(body)?).await?),
1220 "admin_set_cost_settings" => reply(&billing.admin_set_cost_settings(args(body)?).await?),
1221 "admin_set_cost_mapping" => reply(&billing.admin_set_cost_mapping(args(body)?).await?),
1222 "admin_run_costs" => reply(&billing.admin_run_costs(&env, args(body)?).await?),
1223 _ => Response::error("Unknown method", 404),
1224 };
1225 served.finish(answered)
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230 use super::*;
1231
1232 #[test]
1233 fn sandbox_time_is_charged_from_the_first_second() {
1234 // 21 millionths a second at cost, plus 20%.
1235 let price = Price::price_for(21.0, 20);
1236 assert_eq!(sandbox_charge(1, price), 26);
1237 assert_eq!(sandbox_charge(60, price), 1_512);
1238 assert_eq!(sandbox_charge(0, price), 0);
1239 }
1240
1241 #[test]
1242 fn prepaying_starts_at_twenty_five_dollars_and_bank_transfers_at_a_thousand() {
1243 assert!(prepay_amount(2_500, false).is_ok());
1244 assert!(prepay_amount(2_499, false).is_err());
1245 assert!(prepay_amount(10_000, false).is_ok());
1246 assert!(prepay_amount(1_000_000, false).is_ok());
1247 assert_eq!(prepay_amount(1_000_001, false).unwrap_err(), "Prepay between $25 and $10,000 by card; from $1,000, a bank transfer works too.");
1248 assert!(prepay_amount(99_999, true).is_err());
1249 assert!(prepay_amount(100_000, true).is_ok());
1250 }
1251
1252 #[test]
1253 fn durations_read_plainly() {
1254 assert_eq!(duration(40), "40s");
1255 assert_eq!(duration(192), "3m 12s");
1256 assert_eq!(duration(3720), "1h 2m");
1257 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1258
1259 /// Every migration, in order, as D1 applies them.
1260 const MIGRATIONS: &[&str] = &[
1261 include_str!("../migrations/0001_init.sql"),
1262 include_str!("../migrations/0002_own_provider.sql"),
1263 include_str!("../migrations/0003_subscriptions.sql"),
1264 include_str!("../migrations/0004_sandbox_time.sql"),
1265 include_str!("../migrations/0005_limits.sql"),
1266 include_str!("../migrations/0006_prices.sql"),
1267 include_str!("../migrations/0007_pending_usage.sql"),
1268 include_str!("../migrations/0008_accounts.sql"),
1269 include_str!("../migrations/0009_autopay.sql"),
1270 include_str!("../migrations/0010_month_close.sql"),
1271 include_str!("../migrations/0011_limit_warnings.sql"),
1272 include_str!("../migrations/0012_stripe_webhooks.sql"),
1273 include_str!("../migrations/0013_invoices_trust_sales.sql"),
1274 include_str!("../migrations/0014_custom_domain_price.sql"),
1275 include_str!("../migrations/0015_sandbox_at_cost.sql"),
1276 include_str!("../migrations/0016_plans_and_pools.sql"),
1277 include_str!("../migrations/0017_flagon_comped.sql"),
1278 include_str!("../migrations/0018_one_plan.sql"),
1279 include_str!("../migrations/0019_closed_workspaces.sql"),
1280 include_str!("../migrations/0020_syntaqx_standard.sql"),
1281 include_str!("../migrations/0021_no_quotas.sql"),
1282 include_str!("../migrations/0022_costs_and_margin.sql"),
1283 include_str!("../migrations/0023_one_operation_mapping.sql"),
1284 include_str!("../migrations/0024_spend_caps.sql"),
1285 include_str!("../migrations/0025_run_tier.sql"),
1286 include_str!("../migrations/0026_ledger_by_workspace_time.sql"),
1287 include_str!("../migrations/0027_stripe_cache.sql"),
1288 include_str!("../migrations/0028_drop_stripe_webhooks.sql"),
1289 include_str!("../migrations/0029_audit_retention.sql"),
1290 include_str!("../migrations/0030_token_usage.sql"),
1291 include_str!("../migrations/0031_package_storage.sql"),
1292 include_str!("../migrations/0032_workspace_value.sql"),
1293 include_str!("../migrations/0033_given_away.sql"),
1294 include_str!("../migrations/0034_given_by_why.sql"),
1295 include_str!("../migrations/0035_cf_subscriptions.sql"),
1296 include_str!("../migrations/0036_model_costs_in_full.sql"),
1297 include_str!("../migrations/0037_security_activation.sql"),
1298 include_str!("../migrations/0038_staff_credits.sql"),
1299 include_str!("../migrations/0039_discounts_not_comped.sql"),
1300 include_str!("../migrations/0040_ai_credit.sql"),
Merge branch 'model-routing'1301 include_str!("../migrations/0041_agent_rate_own_key.sql"),
1302 include_str!("../migrations/0042_agent_rate_weights.sql"),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1303 include_str!("../migrations/0043_tax_and_card_fees.sql"),
1304 include_str!("../migrations/0044_cache_reads_count_a_tenth.sql"),
1305 include_str!("../migrations/0045_gateway.sql"),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971306 include_str!("../migrations/0046_reset_costs.sql"),
1307 include_str!("../migrations/0047_gateway_formats.sql"),
Merge branch 'main' into actions-toolkit-oidc-artifacts1308 include_str!("../migrations/0048_model_catalogue.sql"),
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)1309 include_str!("../migrations/0049_superseded_proposals.sql"),
1310 include_str!("../migrations/0050_ledger_usage_by_time.sql"),
1311 include_str!("../migrations/0051_platform_guardrails.sql"),
Spend adds up on one ledger. Charged this month has one definition, price less discount, included usage and credit, shared by the plan card, the spend limit, Spend and the top bar; the limit had counted usage not yet closed at full price before the discount, so a comped workspace read as charged a cent. Every agent line on the ledger names the agent and who asked, repository runs by g1t included, so Spent is the sum of its products, Agents is the agent product, and by agent adds up to it; the billing API returns by_agent and by_person. The usage and billing guide says how spend is counted.1312 include_str!("../migrations/0052_cloudflare_cycle.sql"),
1313 include_str!("../migrations/0053_models_at_cost.sql"),
1314 include_str!("../migrations/0054_agent_attribution.sql"),
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.1315 include_str!("../migrations/0055_app_containers.sql"),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1316 ];
1317
1318 /// The columns of `table` after the migrations: each with whether an
1319 /// insert must give it (NOT NULL, no default).
1320 fn columns(table: &str) -> Vec<(String, bool)> {
1321 let mut out = vec![];
1322 for sql in MIGRATIONS {
1323 let sql: String = sql.lines().map(|l| l.split("--").next().unwrap_or("")).collect::<Vec<_>>().join("\n");
1324 if let Some(start) = sql.find(&format!("CREATE TABLE {table} (")) {
1325 let body = &sql[start + format!("CREATE TABLE {table} (").len()..];
1326 let body = &body[..body.find(");").unwrap()];
1327 for column in body.split(",\n") {
1328 let column = column.trim();
1329 let name = column.split_whitespace().next().unwrap_or("").to_owned();
1330 if name.is_empty() || name == "PRIMARY" {
1331 continue;
1332 }
1333 let required = column.contains("NOT NULL") && !column.contains("DEFAULT") && !column.contains("PRIMARY KEY");
1334 out.push((name, required || column.contains("PRIMARY KEY")));
1335 }
1336 }
1337 for statement in sql.split(';') {
1338 let s = statement.trim();
1339 if let Some(rest) = s.strip_prefix(&format!("ALTER TABLE {table} ADD COLUMN ")) {
1340 let name = rest.split_whitespace().next().unwrap().to_owned();
1341 out.push((name, rest.contains("NOT NULL") && !rest.contains("DEFAULT")));
1342 }
1343 }
1344 }
1345 out
1346 }
1347
1348 /// The columns an `INSERT INTO table (…)` names.
1349 fn inserted(sql: &str, table: &str) -> Vec<String> {
1350 let start = sql.find(&format!("INSERT INTO {table} (")).unwrap() + format!("INSERT INTO {table} (").len();
1351 sql[start..start + sql[start..].find(')').unwrap()].split(',').map(|c| c.trim().to_owned()).collect()
1352 }
Merge branch 'model-routing'1353
Chat and workspace agents: channels, DMs and named agents you talk to1354
1355 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1356 fn every_checkout_insert_fills_the_table() {
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.1357 // The plan's, the activation's, a card check's and a prepayment's
1358 // pages all go through one insert, and it names every
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1359 // column the table needs and none it lacks: a missing NOT NULL
1360 // column, or one from a migration not yet applied, was a 500.
1361 let table = columns("checkouts");
1362 let names: Vec<&str> = table.iter().map(|(n, _)| n.as_str()).collect();
1363 assert_eq!(names, ["id", "workspace", "amount_cents", "created_by", "status", "created_at", "feature", "fee_cents"]);
1364 let given = inserted(CHECKOUT_INSERT, "checkouts");
1365 for column in &given {
1366 assert!(names.contains(&column.as_str()), "the insert names {column}, which checkouts does not have");
1367 }
1368 for (column, required) in &table {
1369 assert!(!required || given.contains(column), "checkouts needs {column}, which the insert leaves out");
1370 }
1371 // As many values as columns.
1372 let values = CHECKOUT_INSERT.split("VALUES").nth(1).unwrap();
1373 assert_eq!(values.matches('?').count() + values.matches("'open'").count(), given.len());
1374 // And no page writes a row of its own any more.
1375 for (file, source) in [
1376 ("lib.rs", include_str!("lib.rs")),
1377 ("features.rs", include_str!("features.rs")),
1378 ("cards.rs", include_str!("cards.rs")),
1379 ] {
1380 let inserts = source.matches(concat!("INSERT INTO ", "checkouts")).count();
1381 assert!(inserts <= usize::from(file == "lib.rs"), "{file} inserts into checkouts on its own");
1382 }
1383 }
1384
1385 #[test]
1386 fn a_new_ledger_column_has_a_default() {
1387 // The ledger is written from a dozen places; a column without a
1388 // default would break every one that does not name it.
1389 for (column, required) in columns("ledger") {
1390 assert!(!required || ["id", "workspace", "kind", "amount_micros", "description", "reference", "created_at"].contains(&column.as_str()), "{column}");
1391 }
1392 }
The Packages page names every registry it has1393}

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