Skip to content
913 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.

Topics sit in About on the files pages, under the description1//! The g1t plan: one monthly price per workspace, never per person, that
2//! includes $10 of usage. Everything that costs g1t money is metered from
3//! the first unit at cost plus the margin and drawn from that $10 first;
4//! past it, it is charged, up to the workspace's spend limit. There are no
5//! per-feature quotas: no count of apps, build minutes, requests or
6//! domains ever stops a workspace on the plan. Only its spend limit does
7//! (and g1t's protections against abuse). Projects, previews and
8//! repositories cost g1t next to nothing and are not metered. None of it is
9//! free, whatever `FREE_WHILE_BUILDING` says.
10//!
11//! Deployments were once a plan of their own. They come with the g1t plan
12//! now: `has_feature(deployments)` answers whether the workspace has the
13//! plan, and a Deployments subscription from before keeps working until
14//! its period ends. Billing sets each one to end then, once
15//! (`retire_deployments_plans`), so no one pays for both.
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.16//!
17//! Security and quality was once a $10 monthly activation of its own. It
18//! comes with the g1t plan now, at no price of its own (migration 0053 sets
19//! `security_activation` to $0): its scans are metered at cost plus the
20//! margin like everything else. `has_feature(security)` answers whether
21//! the workspace has the plan (or the activation's old inclusion), and
22//! billing ends every activation subscription at once, daily until none is
23//! left, then archives its product at Stripe
24//! (`retire_security_activations`). No refund is made for the part of a
25//! month already paid.
Topics sit in About on the files pages, under the description26
27use g1t_contracts::billing::deployment_costs as costs;
28use g1t_contracts::billing::*;
29use g1t_contracts::time::rfc3339;
Merge main (membership, two-factor, GitHub repo roles) into tokens30use g1t_contracts::{FailureCode, Outcome};
Topics sit in About on the files pages, under the description31use g1t_kit::now_ms;
32use serde::Deserialize;
33use worker::Result;
34
35use crate::stripe::{StripeSubscription, is_missing};
36use crate::{Billing, Touched, members_only, optional};
37
38#[derive(Deserialize)]
39struct SubscriptionRow {
40 feature: String,
41 subscription_id: String,
42 status: String,
43 period_end: Option<String>,
44 started_by: String,
45 started_at: String,
46 updated_at: String,
47}
48
49/// How long a plan's row is believed after it was last written, once its
50/// period is over, before the processor is asked again.
51const REFRESH_MS: u64 = 60 * 60 * 1000;
52
53/// Whether to ask the processor about a plan again: its period is over (or
54/// unknown) and it is not canceled, and it was not written in the last hour.
55/// Without the hour a plan the processor still shows as ended would be
56/// asked about on every page.
57fn needs_refresh(status: &str, period_end: Option<&str>, updated_at: &str, now_ms: u64) -> bool {
58 let now = rfc3339(now_ms);
59 let over = period_end.is_none_or(|end| end <= now.as_str()) && status != "canceled";
60 over && updated_at <= rfc3339(now_ms.saturating_sub(REFRESH_MS)).as_str()
61}
62
63#[derive(Deserialize)]
64struct PlanCheckoutRow {
65 workspace: String,
66 created_by: String,
67 feature: String,
68}
69
70fn status_from(text: &str) -> SubscriptionStatus {
71 match text {
72 "active" => SubscriptionStatus::Active,
73 "canceling" => SubscriptionStatus::Canceling,
74 "past_due" => SubscriptionStatus::PastDue,
75 _ => SubscriptionStatus::Canceled,
76 }
77}
78
79fn status_text(status: SubscriptionStatus) -> &'static str {
80 match status {
81 SubscriptionStatus::Active => "active",
82 SubscriptionStatus::Canceling => "canceling",
83 SubscriptionStatus::PastDue => "past_due",
84 SubscriptionStatus::Canceled => "canceled",
85 }
86}
87
88/// What the processor's state for a plan means here.
89fn status_of(subscription: &StripeSubscription) -> SubscriptionStatus {
90 match subscription.status.as_str() {
91 "active" | "trialing" if subscription.cancel_at_period_end => SubscriptionStatus::Canceling,
92 "active" | "trialing" => SubscriptionStatus::Active,
93 "past_due" | "unpaid" | "incomplete" | "paused" => SubscriptionStatus::PastDue,
94 _ => SubscriptionStatus::Canceled,
95 }
96}
97
98/// `1 GB`, `50 GB`, or `500 MB`, as storage is priced (powers of ten).
99pub(crate) fn bytes(bytes: i64) -> String {
100 if bytes >= 1_000_000_000 && bytes % 1_000_000_000 == 0 {
101 format!("{} GB", bytes / 1_000_000_000)
102 } else if bytes >= 1_000_000_000 {
103 format!("{:.1} GB", bytes as f64 / 1e9)
104 } else {
105 format!("{} MB", bytes / 1_000_000)
106 }
107}
108
109/// Dollars to the cent, or finer for prices under a cent, so that a
110/// build minute's $0.0015 does not read as nothing.
111pub(crate) fn dollars(micros: i64) -> String {
112 let text = format!("{:.4}", micros as f64 / MICROS_PER_DOLLAR as f64);
113 let (whole, fraction) = text.split_once('.').unwrap_or((&text, ""));
114 let fraction = fraction.trim_end_matches('0');
115 format!("${whole}.{fraction:0<2}")
116}
117
118/// An amount of money to the cent, as balances and amounts owed read:
119/// `$3.99`, never `$3.987`. Prices use [`dollars`].
120pub(crate) fn cents(micros: i64) -> String {
121 format!("${:.2}", micros as f64 / MICROS_PER_DOLLAR as f64)
122}
123
124/// `units` at `each` micros a unit, as the pricing page writes it: a
125/// build second's price times 60 is the build minute both quote.
126pub(crate) fn per_units(each: f64, units: f64) -> String {
127 dollars((each * units).round() as i64)
128}
129
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar130/// The price book's meter for the Security and quality activation.
131pub(crate) const SECURITY_METER: &str = "security_activation";
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.132/// Its price when the price book cannot be read: none, as the book says.
133const SECURITY_FALLBACK_MICROS: f64 = 0.0;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar134
135/// The Security and quality activation at the price book's price.
136pub(crate) fn security_plan_at(book: &std::collections::BTreeMap<&str, f64>) -> Plan {
137 let micros = book.get(SECURITY_METER).copied().unwrap_or(SECURITY_FALLBACK_MICROS);
138 Plan {
139 feature: Feature::Security,
140 title: Feature::Security.title().to_owned(),
141 monthly_cents: (micros / 10_000.0).round().max(0.0) as u32,
Merge Stripe Tax, the card fee on card payments, and one free workspace per person142 card_fee_cents: 0,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar143 includes: vec![
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.144 "With the g1t plan, for every private repository in the workspace; public repositories have it free".to_owned(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar145 "Custom secret patterns, validity checks with issuers, and delegated push protection bypass".to_owned(),
146 "Code scanning from SARIF, with pull request checks that can block merges".to_owned(),
147 "Dependency review on pull requests, and the workspace's security overview".to_owned(),
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.148 "No price of its own: scans are charged at cost plus 20%, like everything g1t runs".to_owned(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar149 ],
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.150 overage: "Secret scanning, push protection, vulnerability alerts and security updates stay free.".to_owned(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar151 }
152}
153
Topics sit in About on the files pages, under the description154impl SubscriptionRow {
155 fn subscription(&self) -> Option<Subscription> {
156 Some(Subscription {
157 feature: Feature::parse(&self.feature)?,
158 status: status_from(&self.status),
159 period_end: self.period_end.clone(),
160 started_by: self.started_by.clone(),
161 started_at: self.started_at.clone(),
162 })
163 }
164}
165
166impl Billing {
167 /// What the g1t plan costs and includes, as it is sold now, at the
168 /// price book's prices (the same figures as the pricing page's table).
Merge Stripe Tax, the card fee on card payments, and one free workspace per person169 /// With the card fee on top of its monthly price, as it is charged.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar170 pub(crate) async fn plan(&self, feature: Feature) -> Result<Plan> {
Topics sit in About on the files pages, under the description171 let mut book = std::collections::BTreeMap::new();
Merge Stripe Tax, the card fee on card payments, and one free workspace per person172 let mut plan = if feature == Feature::Security {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar173 if let Some((_, price)) = self.price(SECURITY_METER).await? {
174 book.insert(SECURITY_METER, price);
175 }
Merge Stripe Tax, the card fee on card payments, and one free workspace per person176 security_plan_at(&book)
177 } else {
178 for meter in ["build_second", "app_requests", "app_cpu", "custom_domain_month", "private_storage", "git_operations"] {
179 if let Some((_, price)) = self.price(meter).await? {
180 book.insert(meter, price);
181 }
Topics sit in About on the files pages, under the description182 }
Merge Stripe Tax, the card fee on card payments, and one free workspace per person183 self.plan_at(&book)
184 };
185 plan.card_fee_cents = self.card_fee_on(i64::from(plan.monthly_cents)).await? as u32;
186 Ok(plan)
Topics sit in About on the files pages, under the description187 }
188
189 /// The plan at the given prices per unit (micros, after the markup);
190 /// the published costs plus the margin for any not given.
191 pub(crate) fn plan_at(&self, book: &std::collections::BTreeMap<&str, f64>) -> Plan {
192 let p = &self.plans;
193 let price_of = |meter: &str, cost: i64, units: f64| {
194 per_units(book.get(meter).copied().unwrap_or_else(|| Price::price_for(cost as f64, self.margin_percent)), units)
195 };
196 Plan {
197 feature: Feature::Plan,
198 title: Feature::Plan.title().to_owned(),
199 monthly_cents: p.plan_monthly_cents,
Merge Stripe Tax, the card fee on card payments, and one free workspace per person200 card_fee_cents: 0,
Topics sit in About on the files pages, under the description201 includes: vec![
202 format!(
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.203 "{} of usage each month, used first: everything g1t runs at cost plus {}%",
Topics sit in About on the files pages, under the description204 dollars(p.plan_included_micros),
205 self.margin_percent
206 ),
207 "Everyone in the workspace at one price, never per person".to_owned(),
208 "Unlimited projects, previews and repositories".to_owned(),
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.209 "Checks, workflows, the merge queue, deployments, semantic search, and Security and quality on private repositories".to_owned(),
Topics sit in About on the files pages, under the description210 format!(
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.211 "Usage past {} is charged the same way, up to your spend limit",
Topics sit in About on the files pages, under the description212 dollars(p.plan_included_micros),
213 ),
214 ],
215 overage: format!(
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.216 "Everything g1t runs is metered from the first unit at what it costs g1t plus {}%: sandbox time and deploy builds by the second ({} a build minute), {} per million app requests, {} per million CPU milliseconds, {} a month per custom domain, private storage past the free {} at {} per GB-month, and git operations past the free {} a month at {} per 1,000. Unused included usage does not roll over.",
Topics sit in About on the files pages, under the description217 self.margin_percent,
218 price_of("build_second", costs::MICROS_PER_BUILD_SECOND, 60.0),
219 price_of("app_requests", costs::MICROS_PER_MILLION_REQUESTS, 1.0),
220 price_of("app_cpu", costs::MICROS_PER_MILLION_CPU_MS, 1.0),
221 price_of("custom_domain_month", costs::MICROS_PER_DOMAIN_MONTH, 1.0),
222 bytes(p.free_storage_bytes),
223 price_of("private_storage", crate::storage::STORAGE_MICROS_PER_GB_MONTH, 1.0),
224 thousands(p.git_included),
225 price_of("git_operations", crate::storage::GIT_MICROS_PER_THOUSAND, 1.0),
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.226 ) + " Your own runners cost nothing.",
Topics sit in About on the files pages, under the description227 }
228 }
229
230 /// When the workspace's plan started, and when the period paid for
231 /// ends: its first billing cycle is the first month.
232 pub(crate) async fn plan_cycle(&self, workspace: &str) -> Result<Option<(String, Option<String>)>> {
233 let row = match self.current(workspace, Feature::Plan).await? {
234 Some(row) => Some(row),
235 None => self.current(workspace, Feature::Deployments).await?,
236 };
237 Ok(row
238 .filter(|row| status_from(&row.status).on())
239 .map(|row| (row.started_at, row.period_end)))
240 }
241
242 async fn subscription_row(&self, workspace: &str, feature: Feature) -> Result<Option<SubscriptionRow>> {
243 self.db
244 .prepare(
245 "SELECT feature, subscription_id, status, period_end, started_by, started_at, updated_at
246 FROM subscriptions WHERE workspace = ? AND feature = ?",
247 )
248 .bind(&[workspace.into(), feature.as_str().into()])?
249 .first::<SubscriptionRow>(None)
250 .await
251 }
252
253 /// Writes down what the processor says about a plan.
254 pub(crate) async fn record(
255 &self,
256 workspace: &str,
257 feature: Feature,
258 subscription: &StripeSubscription,
259 started_by: &str,
260 ) -> Result<()> {
261 let now = rfc3339(now_ms());
262 let period_end = subscription.period_end().map(|seconds| rfc3339(seconds.max(0) as u64 * 1000));
263 self.db
264 .prepare(
265 "INSERT INTO subscriptions
266 (workspace, feature, subscription_id, status, period_end, started_by, started_at, updated_at)
267 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7)
268 ON CONFLICT (workspace, feature) DO UPDATE SET
269 subscription_id = ?3, status = ?4, period_end = ?5, updated_at = ?7,
270 started_by = CASE WHEN subscription_id = ?3 THEN started_by ELSE ?6 END,
271 started_at = CASE WHEN subscription_id = ?3 THEN started_at ELSE ?7 END",
272 )
273 .bind(&[
274 workspace.into(),
275 feature.as_str().into(),
276 subscription.id.as_str().into(),
277 status_text(status_of(subscription)).into(),
278 optional(period_end.as_deref()),
279 started_by.into(),
280 now.as_str().into(),
281 ])?
282 .run()
283 .await?;
284 Ok(())
285 }
286
287 /// A workspace's plan for a feature, asking the processor again once
288 /// the period it last knew of is over, at most once an hour.
289 async fn current(&self, workspace: &str, feature: Feature) -> Result<Option<SubscriptionRow>> {
290 let Some(row) = self.subscription_row(workspace, feature).await? else {
291 return Ok(None);
292 };
293 let stale = needs_refresh(&row.status, row.period_end.as_deref(), &row.updated_at, now_ms());
294 if let (true, Some(stripe)) = (stale, &self.stripe) {
295 match stripe.subscription(&row.subscription_id).await {
296 Ok(subscription) => self.record(workspace, feature, &subscription, &row.started_by).await?,
297 // A plan from another Stripe account: it has ended here.
298 Err(error) if is_missing(&error) => {
299 self.db
300 .prepare("UPDATE subscriptions SET status = 'canceled', updated_at = ? WHERE workspace = ? AND feature = ?")
301 .bind(&[rfc3339(now_ms()).into(), workspace.into(), feature.as_str().into()])?
302 .run()
303 .await?;
304 }
305 Err(error) => return Err(error),
306 }
307 return self.subscription_row(workspace, feature).await;
308 }
309 Ok(Some(row))
310 }
311
312 /// The plan as a workspace sees it. A Deployments subscription from
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar313 /// before the plan shows as the plan until its period ends. The
314 /// Security and quality activation is its own subscription.
315 async fn state(&self, workspace: &str, feature: Feature) -> Result<FeatureState> {
316 if feature == Feature::Security {
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.317 // It comes with the plan; an activation still running until
318 // billing ends it keeps it on meanwhile.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar319 let subscription = self.current(workspace, Feature::Security).await?.and_then(|row| row.subscription());
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.320 let included = self.security_included(workspace).await? || self.has_plan(workspace).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar321 return Ok(FeatureState {
322 plan: self.plan(Feature::Security).await?,
323 on: included || self.stripe.is_none() || subscription.as_ref().is_some_and(|s| s.status.on()),
324 subscription,
325 included,
326 });
327 }
Topics sit in About on the files pages, under the description328 let subscription = match self.current(workspace, Feature::Plan).await?.and_then(|row| row.subscription()) {
329 Some(plan) if plan.status.on() => Some(plan),
330 plan => self
331 .current(workspace, Feature::Deployments)
332 .await?
333 .and_then(|row| row.subscription())
334 .filter(|legacy| legacy.status.on())
335 .or(plan),
336 };
337 let included = self.included(workspace).await?;
338 Ok(FeatureState {
339 plan: self.plan(Feature::Plan).await?,
340 on: included || self.stripe.is_none() || subscription.as_ref().is_some_and(|s| s.status.on()),
341 subscription,
342 included,
343 })
344 }
345
346 /// Whether the plan is on without its price: comped terms, an
347 /// enterprise's workspaces, or given by g1t staff.
348 async fn included(&self, workspace: &str) -> Result<bool> {
349 let account = self.account_of(workspace).await?;
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging350 Ok(account.terms.full_discount()
Topics sit in About on the files pages, under the description351 || account.kind == g1t_contracts::billing::AccountKind::Enterprise
352 || account.allowances.plan)
353 }
354
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar355 /// Whether the Security and quality activation is on without its
356 /// price: comped terms, or an enterprise's workspace. Giving the plan
357 /// as an allowance does not give the activation.
358 async fn security_included(&self, workspace: &str) -> Result<bool> {
359 let account = self.account_of(workspace).await?;
360 Ok(account.terms.kind == g1t_contracts::billing::TermsKind::Comped
361 || account.kind == g1t_contracts::billing::AccountKind::Enterprise)
362 }
363
Topics sit in About on the files pages, under the description364 /// Sets every Deployments subscription from before the plan to end
365 /// with its period, once, so no one pays for it and the plan both.
366 /// Until then it counts as the plan.
367 pub(crate) async fn retire_deployments_plans(&self) -> Result<()> {
368 let Some(stripe) = &self.stripe else { return Ok(()) };
369 #[derive(Deserialize)]
370 struct Legacy {
371 workspace: String,
372 subscription_id: String,
373 started_by: String,
374 period_end: Option<String>,
375 }
376 let legacy = self
377 .db
378 .prepare(
379 "SELECT workspace, subscription_id, started_by, period_end FROM subscriptions
380 WHERE feature = 'deployments' AND status = 'active' LIMIT 20",
381 )
382 .all()
383 .await?
384 .results::<Legacy>()?;
385 for plan in legacy {
386 match stripe.cancel_at_period_end(&plan.subscription_id, true).await {
387 Ok(subscription) => {
388 self.record(&plan.workspace, Feature::Deployments, &subscription, &plan.started_by).await?;
389 let account = self.account_of(&plan.workspace).await?;
390 self.audit(
391 &account.id,
392 "migration",
393 &format!(
394 "{}: the Deployments plan ends {} and is not renewed; deployments come with the g1t plan now",
395 plan.workspace,
396 plan.period_end.as_deref().map_or("at the end of its period", |end| &end[..10])
397 ),
398 "billing",
399 )
400 .await?;
401 }
402 Err(error) if is_missing(&error) => {
403 self.db
404 .prepare("UPDATE subscriptions SET status = 'canceled', updated_at = ? WHERE workspace = ? AND feature = 'deployments'")
405 .bind(&[rfc3339(now_ms()).into(), plan.workspace.as_str().into()])?
406 .run()
407 .await?;
408 }
409 Err(error) => worker::console_error!("could not end {}'s Deployments plan: {error}", plan.workspace),
410 }
411 }
412 Ok(())
413 }
414
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.415 /// Ends every Security and quality activation at once: it has no price
416 /// any more and comes with the plan. Stripe ends each subscription now,
417 /// without proration or refund; its row is recorded as canceled and the
418 /// account's audit log says why. Once none is left, the activation's
419 /// product at Stripe is archived, so no price of it can be sold again.
420 pub(crate) async fn retire_security_activations(&self) -> Result<()> {
421 let Some(stripe) = &self.stripe else { return Ok(()) };
422 #[derive(Deserialize)]
423 struct Live {
424 workspace: String,
425 subscription_id: String,
426 started_by: String,
427 }
428 let live = self
429 .db
430 .prepare(
431 "SELECT workspace, subscription_id, started_by FROM subscriptions
432 WHERE feature = 'security' AND status <> 'canceled' LIMIT 20",
433 )
434 .all()
435 .await?
436 .results::<Live>()?;
437 let mut left = false;
438 for activation in &live {
439 match stripe.cancel_now(&activation.subscription_id).await {
440 Ok(subscription) => {
441 self.record(&activation.workspace, Feature::Security, &subscription, &activation.started_by).await?;
442 let account = self.account_of(&activation.workspace).await?;
443 self.audit(
444 &account.id,
445 "migration",
446 &format!(
447 "{}: the Security and quality activation ended; it comes with the g1t plan now, with no price of its own",
448 activation.workspace
449 ),
450 "billing",
451 )
452 .await?;
453 }
454 Err(error) if is_missing(&error) => {
455 self.db
456 .prepare("UPDATE subscriptions SET status = 'canceled', updated_at = ? WHERE workspace = ? AND feature = 'security'")
457 .bind(&[rfc3339(now_ms()).into(), activation.workspace.as_str().into()])?
458 .run()
459 .await?;
460 }
461 Err(error) => {
462 left = true;
463 worker::console_error!("could not end {}'s Security and quality activation: {error}", activation.workspace);
464 }
465 }
466 }
467 if !left && live.len() < 20 {
468 if let Err(error) = stripe.archive_product("security").await {
469 worker::console_error!("could not archive the Security and quality product: {error}");
470 }
471 }
472 Ok(())
473 }
474
Topics sit in About on the files pages, under the description475 /// Whether the workspace's plan for the feature is paid up.
476 pub(crate) async fn plan_on(&self, workspace: &str, feature: Feature) -> Result<bool> {
477 Ok(self
478 .current(workspace, feature)
479 .await?
480 .and_then(|row| row.subscription())
481 .is_some_and(|s| s.status.on()))
482 }
483
484 pub(crate) async fn features(&self, a: FeaturesArgs) -> Result<Outcome<Vec<FeatureState>>> {
485 let workspace = a.workspace.to_lowercase();
486 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
487 return Ok(members_only());
488 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar489 let plan = self.state(&workspace, Feature::Plan).await?;
490 let security = self.state(&workspace, Feature::Security).await?;
491 Ok(Outcome::Ok(vec![plan, security]))
Topics sit in About on the files pages, under the description492 }
493
494 pub(crate) async fn subscribe(&self, a: SubscribeArgs) -> Result<Outcome<Checkout>> {
495 let workspace = a.workspace.to_lowercase();
Merge main (membership, two-factor, GitHub repo roles) into tokens496 if !a.actor.manages_billing(&workspace) {
Topics sit in About on the files pages, under the description497 return Ok(Outcome::fail(
498 FailureCode::Forbidden,
Merge main (membership, two-factor, GitHub repo roles) into tokens499 "Only an owner or a billing manager can turn on a paid feature.",
Topics sit in About on the files pages, under the description500 ));
501 }
502 let Some(stripe) = &self.stripe else {
503 return Ok(Outcome::fail(
504 FailureCode::Conflict,
505 "Payments are not set up on this g1t, so the plan is already on.",
506 ));
507 };
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.508 // Deployments and Security and quality come with the plan: there is
509 // nothing of their own to start.
510 if a.feature == Feature::Security {
511 return Ok(Outcome::fail(
512 FailureCode::Conflict,
513 format!("Security and quality comes with the g1t plan, with no price of its own. Start the plan at /{workspace}/-/billing."),
514 ));
515 }
516 let feature = Feature::Plan;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar517 let name = if feature == Feature::Security { "The Security and quality activation" } else { "The g1t plan" };
Topics sit in About on the files pages, under the description518 let state = self.state(&workspace, feature).await?;
519 if state.included {
520 return Ok(Outcome::fail(
521 FailureCode::Conflict,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar522 format!("{name} is included for {workspace} already, at no charge."),
Topics sit in About on the files pages, under the description523 ));
524 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar525 if self.plan_on(&workspace, feature).await? {
526 return Ok(Outcome::fail(FailureCode::Conflict, format!("{name} is already on for {workspace}.")));
Topics sit in About on the files pages, under the description527 }
528 let plan = self.plan(feature).await?;
529 let customer = self.row(&workspace).await?.and_then(|row| row.customer_id);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit530 // The card from the card check, or else the customer's default
531 // payment method (one added on Stripe's billing page counts): the
532 // plan starts on it at once, with no second page. A card that needs
533 // the bank's approval again goes through Stripe's page instead.
534 let saved = match (customer.as_deref(), self.checked_card(&workspace).await?) {
535 (Some(_), Some(method)) => Some(method),
536 (Some(customer), None) => match stripe.default_payment_method(customer).await {
537 Ok(method) => method.map(|m| m.id),
538 Err(error) => {
539 worker::console_log!("{workspace}: the default payment method could not be read: {error}");
540 None
541 }
542 },
543 (None, _) => None,
544 };
545 if let (Some(customer), Some(method)) = (customer.as_deref(), saved) {
Topics sit in About on the files pages, under the description546 match stripe
Merge Stripe Tax, the card fee on card payments, and one free workspace per person547 .subscribe_with_card(&workspace, feature.as_str(), &plan.title, plan.monthly_cents, plan.card_fee_cents, customer, &method)
Topics sit in About on the files pages, under the description548 .await
549 {
550 Ok(subscription) if matches!(subscription.status.as_str(), "active" | "trialing") => {
551 self.record(&workspace, feature, &subscription, &a.actor.username).await?;
552 let account = self.account_of(&workspace).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar553 self.audit(
554 &account.id,
555 "plan",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit556 &format!("{workspace}: {} started on the saved card", name.to_lowercase()),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar557 &a.actor.username,
558 )
559 .await?;
Topics sit in About on the files pages, under the description560 let separator = if a.return_url.contains('?') { '&' } else { '?' };
561 return Ok(Outcome::Ok(Checkout { url: format!("{}{separator}plan=started", a.return_url) }));
562 }
563 Ok(subscription) => {
564 // Incomplete: let it lapse, and use the page.
565 let _ = stripe.cancel_now(&subscription.id).await;
566 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit567 Err(error) => worker::console_log!("{workspace}: the plan could not start on the saved card: {error}"),
Topics sit in About on the files pages, under the description568 }
569 }
570 let start = |customer: Option<String>| {
571 let plan = &plan;
572 let workspace = &workspace;
573 let return_url = &a.return_url;
574 async move {
575 stripe
576 .start_subscription(
577 workspace,
578 feature.as_str(),
579 &plan.title,
580 plan.monthly_cents,
Merge Stripe Tax, the card fee on card payments, and one free workspace per person581 plan.card_fee_cents,
Topics sit in About on the files pages, under the description582 customer.as_deref(),
583 return_url,
584 )
585 .await
586 }
587 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit588 let started = match start(customer.clone()).await {
Topics sit in About on the files pages, under the description589 // A customer saved under another Stripe account: start afresh.
590 Err(error) if customer.is_some() && is_missing(&error) => {
591 self.forget_customer(&workspace).await?;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit592 start(None).await
Topics sit in About on the files pages, under the description593 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit594 other => other,
Topics sit in About on the files pages, under the description595 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit596 let session = match started {
597 Ok(session) => session,
598 Err(error) => {
599 worker::console_error!("{workspace}: Stripe refused the plan's page: {error}");
600 return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error)));
601 }
Topics sit in About on the files pages, under the description602 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit603 let Some(url) = session.url.clone() else {
604 return Ok(Outcome::fail(FailureCode::Conflict, "Stripe returned no payment page. Try again in a minute."));
605 };
606 if let Err(error) = self
607 .record_checkout(&crate::NewCheckout {
608 id: &session.id,
609 workspace: &workspace,
610 amount_cents: plan.monthly_cents,
Merge Stripe Tax, the card fee on card payments, and one free workspace per person611 fee_cents: plan.card_fee_cents,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit612 created_by: &a.actor.username,
613 feature: Some(feature.as_str()),
614 })
615 .await
616 {
617 worker::console_error!("{workspace}: the plan's page could not be recorded: {error}");
618 return Ok(Outcome::fail(FailureCode::Conflict, "g1t could not keep track of the payment page. Nothing was charged; try again."));
619 }
Topics sit in About on the files pages, under the description620 Ok(Outcome::Ok(Checkout { url }))
621 }
622
623 pub(crate) async fn confirm_subscription(
624 &self,
625 a: ConfirmSubscriptionArgs,
626 ) -> Result<Outcome<FeatureState>> {
627 let workspace = a.workspace.to_lowercase();
628 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
629 return Ok(members_only());
630 }
631 let checkout = self
632 .db
633 .prepare(
634 "SELECT workspace, created_by, feature FROM checkouts
635 WHERE id = ? AND workspace = ? AND status = 'open' AND feature IS NOT NULL",
636 )
637 .bind(&[a.session.as_str().into(), workspace.as_str().into()])?
638 .first::<PlanCheckoutRow>(None)
639 .await?;
640 let (Some(stripe), Some(checkout)) = (&self.stripe, checkout) else {
641 // Unknown, someone else's, or already done: show where it stands.
642 return Ok(Outcome::Ok(self.state(&workspace, Feature::Plan).await?));
643 };
644 let Some(feature) = Feature::parse(&checkout.feature) else {
645 return Ok(Outcome::fail(FailureCode::NotFound, "No such plan."));
646 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit647 let session = match stripe.session(&a.session).await {
648 Ok(session) => session,
649 Err(error) => return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error))),
650 };
Topics sit in About on the files pages, under the description651 if let (Some(subscription_id), true) = (&session.subscription, session.payment_status == "paid") {
652 let claimed = self
653 .db
654 .prepare("UPDATE checkouts SET status = 'paid' WHERE id = ? AND status = 'open' RETURNING id")
655 .bind(&[a.session.as_str().into()])?
656 .first::<Touched>(None)
657 .await?;
658 if claimed.is_some() {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit659 let subscription = match stripe.subscription(subscription_id).await {
660 Ok(subscription) => subscription,
661 Err(error) => {
662 // Let the next look (or the webhook) settle it.
663 self.db.prepare("UPDATE checkouts SET status = 'open' WHERE id = ?").bind(&[a.session.as_str().into()])?.run().await?;
664 return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error)));
665 }
666 };
Topics sit in About on the files pages, under the description667 self.record(&checkout.workspace, feature, &subscription, &checkout.created_by)
668 .await?;
669 // Keep the card's customer, so later payments need no retyping.
670 self.db
671 .prepare(
672 "INSERT INTO accounts (workspace, balance_micros, customer_id, created_at)
673 VALUES (?1, 0, ?2, ?3)
674 ON CONFLICT (workspace) DO UPDATE SET customer_id = COALESCE(customer_id, ?2)",
675 )
676 .bind(&[
677 checkout.workspace.as_str().into(),
678 optional(session.customer.as_deref()),
679 rfc3339(now_ms()).into(),
680 ])?
681 .run()
682 .await?;
683 }
684 }
685 Ok(Outcome::Ok(self.state(&workspace, feature).await?))
686 }
687
688 pub(crate) async fn cancel_subscription(
689 &self,
690 a: CancelSubscriptionArgs,
691 ) -> Result<Outcome<FeatureState>> {
692 let workspace = a.workspace.to_lowercase();
Merge main (membership, two-factor, GitHub repo roles) into tokens693 if !a.actor.manages_billing(&workspace) {
Topics sit in About on the files pages, under the description694 return Ok(Outcome::fail(
695 FailureCode::Forbidden,
Merge main (membership, two-factor, GitHub repo roles) into tokens696 "Only an owner or a billing manager can change the workspace's plan.",
Topics sit in About on the files pages, under the description697 ));
698 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar699 // The activation; or the plan, or a Deployments subscription from
700 // before it.
701 let row = if a.feature == Feature::Security {
702 self.current(&workspace, Feature::Security).await?.map(|row| (Feature::Security, row))
703 } else {
704 match self.current(&workspace, Feature::Plan).await? {
705 Some(row) if status_from(&row.status) != SubscriptionStatus::Canceled => Some((Feature::Plan, row)),
706 _ => self.current(&workspace, Feature::Deployments).await?.map(|row| (Feature::Deployments, row)),
707 }
Topics sit in About on the files pages, under the description708 };
709 let (Some(stripe), Some((feature, row))) = (&self.stripe, row) else {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar710 let name = if a.feature == Feature::Security { "The Security and quality activation" } else { "The g1t plan" };
711 return Ok(Outcome::fail(FailureCode::NotFound, format!("{name} is not on for {workspace}.")));
Topics sit in About on the files pages, under the description712 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit713 let subscription = match stripe.cancel_at_period_end(&row.subscription_id, !a.resume).await {
714 Ok(subscription) => subscription,
715 Err(error) => return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error))),
716 };
Topics sit in About on the files pages, under the description717 self.record(&workspace, feature, &subscription, &row.started_by)
718 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar719 let shown = if feature == Feature::Security { Feature::Security } else { Feature::Plan };
720 Ok(Outcome::Ok(self.state(&workspace, shown).await?))
Topics sit in About on the files pages, under the description721 }
722
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar723 /// Whether the workspace has the plan, which deployments come with, or
724 /// the Security and quality activation.
Topics sit in About on the files pages, under the description725 pub(crate) async fn has_feature(&self, a: HasFeatureArgs) -> Result<Outcome<bool>> {
726 let workspace = a.workspace.to_lowercase();
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar727 if a.feature == Feature::Security {
728 let state = self.state(&workspace, Feature::Security).await?;
729 if state.on {
730 return Ok(Outcome::Ok(true));
731 }
732 return Ok(Outcome::fail(
733 FailureCode::PaymentRequired,
734 format!(
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.735 "Security and quality on private repositories comes with the g1t plan ($20 a month for the workspace, with $10 of usage included; scans at cost plus 20%), and {workspace} does not have it. An owner can start it at /{workspace}/-/billing."
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar736 ),
737 ));
738 }
Topics sit in About on the files pages, under the description739 if self.has_plan(&workspace).await? {
740 return Ok(Outcome::Ok(true));
741 }
742 let what = match a.feature {
743 Feature::Deployments => "Deployments come with the g1t plan",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar744 Feature::Plan | Feature::Security => "This needs the g1t plan",
Topics sit in About on the files pages, under the description745 };
746 Ok(Outcome::fail(
747 FailureCode::PaymentRequired,
748 format!(
749 "{what} ($20 a month for the workspace, with $10 of usage included), and {workspace} does not have it. An owner can start it at /{workspace}/-/billing."
750 ),
751 ))
752 }
753
754 pub(crate) async fn charge_feature(&self, a: ChargeFeatureArgs) -> Result<Outcome<bool>> {
755 if self.stripe.is_none() || a.cost_micros <= 0 {
756 return Ok(Outcome::Ok(false));
757 }
758 let workspace = a.workspace.to_lowercase();
759 let seen = self
760 .db
761 .prepare("SELECT id FROM ledger WHERE reference = ?")
762 .bind(&[a.reference.as_str().into()])?
763 .first::<Touched>(None)
764 .await?;
765 if seen.is_some() {
766 return Ok(Outcome::Ok(false));
767 }
768 let timestamp = rfc3339(now_ms());
769 let month = crate::credits::month_of(&timestamp);
770 let mut description = a.description.clone();
771 // A build: every second is metered, at the price book's build
772 // second, which the keeper keeps at what Cloudflare bills, rather
773 // than at what the caller worked out. The month's build time is
774 // tallied for the Billing page.
775 let cost_micros = match a.build_seconds.filter(|s| *s > 0 && a.feature == Feature::Deployments) {
776 Some(seconds) => {
777 self.tally("build_seconds", &workspace, &month, seconds.into()).await?;
778 let measured = self.price("build_second").await?.map(|(cost, _)| (f64::from(seconds) * cost).ceil() as i64);
779 measured.unwrap_or(a.cost_micros)
780 }
781 None => a.cost_micros,
782 };
783 // Never free: the margin applies whatever FREE_WHILE_BUILDING says,
784 // and only the account's terms change it. The plan's included usage
785 // pays what it can; the trial and the open-source pool never pay for
786 // deployments.
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging787 let (charge, discount) = self.terms_of(&workspace).await?.discounted(crate::margin_on(cost_micros, self.margin_percent));
Topics sit in About on the files pages, under the description788 let drawn = self.draw(&workspace, charge, &month, &crate::credits::Eligible::default()).await?;
789 description.push_str(&drawn.note());
790 self.post_usage(crate::storage::UsageLine {
791 workspace: &workspace,
792 charged: charge - drawn.total(),
793 description: &description,
794 repo: a.repo.as_deref(),
795 task: a.feature.as_str(),
796 cost: cost_micros,
797 reference: &a.reference,
798 created_at: &timestamp,
799 drawn,
800 })
801 .await?;
Merge branch 'worktree-agent-a633ac0f7f66d419d'802 self.record_discount(&a.reference, discount).await?;
Topics sit in About on the files pages, under the description803 self.count_spend(&workspace, cost_micros, charge - drawn.total(), &drawn).await;
804 Ok(Outcome::Ok(true))
805 }
806}
807
808/// `50,000`: a count as the plan reads it.
809pub(crate) fn thousands(n: u64) -> String {
810 let digits = n.to_string();
811 let mut out = String::new();
812 for (i, c) in digits.chars().enumerate() {
813 if i > 0 && (digits.len() - i).is_multiple_of(3) {
814 out.push(',');
815 }
816 out.push(c);
817 }
818 out
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824
825 #[test]
826 fn an_ended_plan_is_asked_about_at_most_once_an_hour() {
827 let now = 1_791_000_000_000;
828 let at = |ago_ms: u64| rfc3339(now - ago_ms);
829 let ended = at(24 * 60 * 60 * 1000);
830 // Ended, and last written a day ago: ask.
831 assert!(needs_refresh("active", Some(&ended), &ended, now));
832 // Ended, but written ten minutes ago: believe the row.
833 assert!(!needs_refresh("active", Some(&ended), &at(10 * 60 * 1000), now));
834 // An hour on, ask again.
835 assert!(needs_refresh("active", Some(&ended), &at(REFRESH_MS), now));
836 // No period known is the same as ended.
837 assert!(needs_refresh("past_due", None, &ended, now));
838 // A period still running, or a canceled plan, is never asked about.
839 assert!(!needs_refresh("active", Some(&rfc3339(now + 1000)), &ended, now));
840 assert!(!needs_refresh("canceled", Some(&ended), &ended, now));
841 }
842
843 #[test]
844 fn the_plan_text_quotes_a_build_minute_as_the_table_does() {
845 // The price book's build second (16.44 millionths at cost, plus
846 // 20%) is 19.73 millionths: a minute is 1,184 millionths, $0.0012,
847 // as the pricing page's table says. The old fixed cost (15) gave
848 // $0.0011.
849 let each = Price::price_for(16.439_893_610_418_67, 20);
850 assert_eq!(per_units(each, 60.0), "$0.0012");
851 assert_eq!(per_units(Price::price_for(15.0, 20), 60.0), "$0.0011");
852 assert_eq!(per_units(Price::price_for(150_000.0, 20), 1.0), "$0.18");
853 }
854
855 #[test]
856 fn every_build_second_is_metered_at_cost_plus_the_margin() {
857 // A 5-minute build at 15 millionths a second costs g1t 4,500, and
858 // is charged at cost plus 20%, from the first second: there are no
859 // included build minutes, only the plan's included usage.
860 let cost = 300 * costs::MICROS_PER_BUILD_SECOND;
861 assert_eq!(cost, 4_500);
862 assert_eq!(crate::credits::with_margin(cost, 20), 5_400);
863 }
864
865 #[test]
866 fn counts_read_with_thousands_separators() {
867 assert_eq!(thousands(0), "0");
868 assert_eq!(thousands(999), "999");
869 assert_eq!(thousands(50_000), "50,000");
870 assert_eq!(thousands(1_234_567), "1,234,567");
871 }
872
873 #[test]
874 fn storage_reads_in_gigabytes() {
875 assert_eq!(bytes(1_000_000_000), "1 GB");
876 assert_eq!(bytes(50_000_000_000), "50 GB");
877 assert_eq!(bytes(1_500_000_000), "1.5 GB");
878 assert_eq!(bytes(500_000_000), "500 MB");
879 }
880
881 #[test]
882 fn amounts_of_money_read_to_the_cent() {
883 assert_eq!(cents(3_986_990), "$3.99");
884 assert_eq!(cents(5_000_000), "$5.00");
885 assert_eq!(cents(4_000), "$0.00");
886 }
887
888 #[test]
889 fn prices_under_a_cent_keep_their_digits() {
890 assert_eq!(dollars(1512), "$0.0015");
891 assert_eq!(dollars(24_000), "$0.024");
892 assert_eq!(dollars(360_000), "$0.36");
893 assert_eq!(dollars(5_000_000), "$5.00");
894 }
895}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar896
897#[cfg(test)]
898mod security_activation {
899 use super::*;
900
901 #[test]
902 fn the_activation_is_priced_from_the_price_book() {
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.903 let book = std::collections::BTreeMap::from([(SECURITY_METER, 0.0)]);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar904 let plan = security_plan_at(&book);
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.905 assert_eq!((plan.feature, plan.monthly_cents), (Feature::Security, 0));
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar906 assert_eq!(plan.title, "Security and quality");
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.907 // The price book unreadable: no price, as the book says.
908 assert_eq!(security_plan_at(&std::collections::BTreeMap::new()).monthly_cents, 0);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar909 assert!(plan.includes.iter().any(|line| line.contains("public repositories have it free")));
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.910 assert!(plan.includes.iter().any(|line| line.contains("cost plus 20%")));
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.911 assert!(plan.overage.contains("stay free") && !plan.overage.contains("agent"));
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar912 }
913}

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