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

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1//! The g1t plan: one monthly price per workspace, never per person, that
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2//! 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.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look10//!
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.
Plan: Projects, the home of everything about running software26
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas27use g1t_contracts::billing::deployment_costs as costs;
Plan: Projects, the home of everything about running software28use g1t_contracts::billing::*;
29use g1t_contracts::time::rfc3339;
Merge main (membership, two-factor, GitHub repo roles) into tokens30use g1t_contracts::{FailureCode, Outcome};
Plan: Projects, the home of everything about running software31use g1t_kit::now_ms;
32use serde::Deserialize;
33use worker::Result;
34
Project dependencies: addresses, preview stacks, Affects, and agents who know35use crate::stripe::{StripeSubscription, is_missing};
Plan: Projects, the home of everything about running software36use 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,
Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index46 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()
Plan: Projects, the home of everything about running software61}
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
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put98/// `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
Plan: Projects, the home of everything about running software109/// 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.
Usage limits: unpaid usage can only go so far111pub(crate) fn dollars(micros: i64) -> String {
Plan: Projects, the home of everything about running software112 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
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure118/// 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
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily124/// `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 ],
150 overage: "Fixes by g1t's agent are charged as agent usage, like any other agent run. Secret scanning, push protection, vulnerability alerts and security updates stay free.".to_owned(),
151 }
152}
153
Plan: Projects, the home of everything about running software154impl 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 {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily167 /// 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> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily171 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 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily182 }
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)
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily187 }
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 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look192 let p = &self.plans;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily193 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 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look196 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,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look201 includes: vec![
202 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.203 "{} of usage each month, used first: models at the provider's price with the g1t agent rate, everything else g1t runs at cost plus {}%",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look204 dollars(p.plan_included_micros),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put205 self.margin_percent
206 ),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas207 "Everyone in the workspace at one price, never per person".to_owned(),
208 "Unlimited projects, previews and repositories".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.209 "Agents, checks, workflows, the merge queue, deployments, semantic search, and Security and quality on private repositories".to_owned(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look210 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",
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas212 dollars(p.plan_included_micros),
Plan: Projects, the home of everything about running software213 ),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look214 ],
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.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look217 self.margin_percent,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily218 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),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas222 bytes(p.free_storage_bytes),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily223 price_of("private_storage", crate::storage::STORAGE_MICROS_PER_GB_MONTH, 1.0),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas224 thousands(p.git_included),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily225 price_of("git_operations", crate::storage::GIT_MICROS_PER_THOUSAND, 1.0),
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.226 ) + " Models are billed at the provider's price, with no markup, plus the g1t agent rate on their tokens, on your own model keys too. Your own runners cost nothing.",
Plan: Projects, the home of everything about running software227 }
228 }
229
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look230 /// 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
Plan: Projects, the home of everything about running software242 async fn subscription_row(&self, workspace: &str, feature: Feature) -> Result<Option<SubscriptionRow>> {
243 self.db
244 .prepare(
Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index245 "SELECT feature, subscription_id, status, period_end, started_by, started_at, updated_at
Plan: Projects, the home of everything about running software246 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.
Stripe webhooks, enterprise invoices, and sudo for both254 pub(crate) async fn record(
Plan: Projects, the home of everything about running software255 &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));
Usage, Billing settings and prepaid AI credit; fixes from the UX audit263 // Whether this plan was on already: the upgrade credit is for starting it.
264 let was_on = self
265 .subscription_row(workspace, feature)
266 .await?
267 .is_some_and(|row| row.subscription_id == subscription.id && status_from(&row.status).on());
Plan: Projects, the home of everything about running software268 self.db
269 .prepare(
270 "INSERT INTO subscriptions
271 (workspace, feature, subscription_id, status, period_end, started_by, started_at, updated_at)
272 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7)
273 ON CONFLICT (workspace, feature) DO UPDATE SET
274 subscription_id = ?3, status = ?4, period_end = ?5, updated_at = ?7,
275 started_by = CASE WHEN subscription_id = ?3 THEN started_by ELSE ?6 END,
276 started_at = CASE WHEN subscription_id = ?3 THEN started_at ELSE ?7 END",
277 )
278 .bind(&[
279 workspace.into(),
280 feature.as_str().into(),
281 subscription.id.as_str().into(),
282 status_text(status_of(subscription)).into(),
283 optional(period_end.as_deref()),
284 started_by.into(),
285 now.as_str().into(),
286 ])?
287 .run()
288 .await?;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit289 // Starting the paid plan comes with $5 of AI credit, once (ai.rs).
290 if feature == Feature::Plan && !was_on && status_of(subscription) == SubscriptionStatus::Active {
291 self.grant_upgrade_credit(workspace).await?;
292 }
Plan: Projects, the home of everything about running software293 Ok(())
294 }
295
296 /// A workspace's plan for a feature, asking the processor again once
Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index297 /// the period it last knew of is over, at most once an hour.
Plan: Projects, the home of everything about running software298 async fn current(&self, workspace: &str, feature: Feature) -> Result<Option<SubscriptionRow>> {
299 let Some(row) = self.subscription_row(workspace, feature).await? else {
300 return Ok(None);
301 };
Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index302 let stale = needs_refresh(&row.status, row.period_end.as_deref(), &row.updated_at, now_ms());
Plan: Projects, the home of everything about running software303 if let (true, Some(stripe)) = (stale, &self.stripe) {
Project dependencies: addresses, preview stacks, Affects, and agents who know304 match stripe.subscription(&row.subscription_id).await {
305 Ok(subscription) => self.record(workspace, feature, &subscription, &row.started_by).await?,
306 // A plan from another Stripe account: it has ended here.
307 Err(error) if is_missing(&error) => {
308 self.db
309 .prepare("UPDATE subscriptions SET status = 'canceled', updated_at = ? WHERE workspace = ? AND feature = ?")
310 .bind(&[rfc3339(now_ms()).into(), workspace.into(), feature.as_str().into()])?
311 .run()
312 .await?;
313 }
314 Err(error) => return Err(error),
315 }
Plan: Projects, the home of everything about running software316 return self.subscription_row(workspace, feature).await;
317 }
318 Ok(Some(row))
319 }
320
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look321 /// 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 bar322 /// before the plan shows as the plan until its period ends. The
323 /// Security and quality activation is its own subscription.
324 async fn state(&self, workspace: &str, feature: Feature) -> Result<FeatureState> {
325 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.326 // It comes with the plan; an activation still running until
327 // billing ends it keeps it on meanwhile.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar328 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.329 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 bar330 return Ok(FeatureState {
331 plan: self.plan(Feature::Security).await?,
332 on: included || self.stripe.is_none() || subscription.as_ref().is_some_and(|s| s.status.on()),
333 subscription,
334 included,
335 });
336 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look337 let subscription = match self.current(workspace, Feature::Plan).await?.and_then(|row| row.subscription()) {
338 Some(plan) if plan.status.on() => Some(plan),
339 plan => self
340 .current(workspace, Feature::Deployments)
341 .await?
342 .and_then(|row| row.subscription())
343 .filter(|legacy| legacy.status.on())
344 .or(plan),
345 };
346 let included = self.included(workspace).await?;
Plan: Projects, the home of everything about running software347 Ok(FeatureState {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily348 plan: self.plan(Feature::Plan).await?,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put349 on: included || self.stripe.is_none() || subscription.as_ref().is_some_and(|s| s.status.on()),
Plan: Projects, the home of everything about running software350 subscription,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put351 included,
Plan: Projects, the home of everything about running software352 })
353 }
354
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look355 /// Whether the plan is on without its price: comped terms, an
356 /// enterprise's workspaces, or given by g1t staff.
357 async fn included(&self, workspace: &str) -> Result<bool> {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put358 let account = self.account_of(workspace).await?;
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging359 Ok(account.terms.full_discount()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look360 || account.kind == g1t_contracts::billing::AccountKind::Enterprise
361 || account.allowances.plan)
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put362 }
363
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar364 /// Whether the Security and quality activation is on without its
365 /// price: comped terms, or an enterprise's workspace. Giving the plan
366 /// as an allowance does not give the activation.
367 async fn security_included(&self, workspace: &str) -> Result<bool> {
368 let account = self.account_of(workspace).await?;
369 Ok(account.terms.kind == g1t_contracts::billing::TermsKind::Comped
370 || account.kind == g1t_contracts::billing::AccountKind::Enterprise)
371 }
372
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look373 /// Sets every Deployments subscription from before the plan to end
374 /// with its period, once, so no one pays for it and the plan both.
375 /// Until then it counts as the plan.
376 pub(crate) async fn retire_deployments_plans(&self) -> Result<()> {
377 let Some(stripe) = &self.stripe else { return Ok(()) };
378 #[derive(Deserialize)]
379 struct Legacy {
380 workspace: String,
381 subscription_id: String,
382 started_by: String,
383 period_end: Option<String>,
384 }
385 let legacy = self
386 .db
387 .prepare(
388 "SELECT workspace, subscription_id, started_by, period_end FROM subscriptions
389 WHERE feature = 'deployments' AND status = 'active' LIMIT 20",
390 )
391 .all()
392 .await?
393 .results::<Legacy>()?;
394 for plan in legacy {
395 match stripe.cancel_at_period_end(&plan.subscription_id, true).await {
396 Ok(subscription) => {
397 self.record(&plan.workspace, Feature::Deployments, &subscription, &plan.started_by).await?;
398 let account = self.account_of(&plan.workspace).await?;
399 self.audit(
400 &account.id,
401 "migration",
402 &format!(
403 "{}: the Deployments plan ends {} and is not renewed; deployments come with the g1t plan now",
404 plan.workspace,
405 plan.period_end.as_deref().map_or("at the end of its period", |end| &end[..10])
406 ),
407 "billing",
408 )
409 .await?;
410 }
411 Err(error) if is_missing(&error) => {
412 self.db
413 .prepare("UPDATE subscriptions SET status = 'canceled', updated_at = ? WHERE workspace = ? AND feature = 'deployments'")
414 .bind(&[rfc3339(now_ms()).into(), plan.workspace.as_str().into()])?
415 .run()
416 .await?;
417 }
418 Err(error) => worker::console_error!("could not end {}'s Deployments plan: {error}", plan.workspace),
419 }
420 }
421 Ok(())
422 }
423
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.424 /// Ends every Security and quality activation at once: it has no price
425 /// any more and comes with the plan. Stripe ends each subscription now,
426 /// without proration or refund; its row is recorded as canceled and the
427 /// account's audit log says why. Once none is left, the activation's
428 /// product at Stripe is archived, so no price of it can be sold again.
429 pub(crate) async fn retire_security_activations(&self) -> Result<()> {
430 let Some(stripe) = &self.stripe else { return Ok(()) };
431 #[derive(Deserialize)]
432 struct Live {
433 workspace: String,
434 subscription_id: String,
435 started_by: String,
436 }
437 let live = self
438 .db
439 .prepare(
440 "SELECT workspace, subscription_id, started_by FROM subscriptions
441 WHERE feature = 'security' AND status <> 'canceled' LIMIT 20",
442 )
443 .all()
444 .await?
445 .results::<Live>()?;
446 let mut left = false;
447 for activation in &live {
448 match stripe.cancel_now(&activation.subscription_id).await {
449 Ok(subscription) => {
450 self.record(&activation.workspace, Feature::Security, &subscription, &activation.started_by).await?;
451 let account = self.account_of(&activation.workspace).await?;
452 self.audit(
453 &account.id,
454 "migration",
455 &format!(
456 "{}: the Security and quality activation ended; it comes with the g1t plan now, with no price of its own",
457 activation.workspace
458 ),
459 "billing",
460 )
461 .await?;
462 }
463 Err(error) if is_missing(&error) => {
464 self.db
465 .prepare("UPDATE subscriptions SET status = 'canceled', updated_at = ? WHERE workspace = ? AND feature = 'security'")
466 .bind(&[rfc3339(now_ms()).into(), activation.workspace.as_str().into()])?
467 .run()
468 .await?;
469 }
470 Err(error) => {
471 left = true;
472 worker::console_error!("could not end {}'s Security and quality activation: {error}", activation.workspace);
473 }
474 }
475 }
476 if !left && live.len() < 20 {
477 if let Err(error) = stripe.archive_product("security").await {
478 worker::console_error!("could not archive the Security and quality product: {error}");
479 }
480 }
481 Ok(())
482 }
483
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put484 /// Whether the workspace's plan for the feature is paid up.
485 pub(crate) async fn plan_on(&self, workspace: &str, feature: Feature) -> Result<bool> {
486 Ok(self
487 .current(workspace, feature)
488 .await?
489 .and_then(|row| row.subscription())
490 .is_some_and(|s| s.status.on()))
491 }
492
Plan: Projects, the home of everything about running software493 pub(crate) async fn features(&self, a: FeaturesArgs) -> Result<Outcome<Vec<FeatureState>>> {
494 let workspace = a.workspace.to_lowercase();
495 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
496 return Ok(members_only());
497 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar498 let plan = self.state(&workspace, Feature::Plan).await?;
499 let security = self.state(&workspace, Feature::Security).await?;
500 Ok(Outcome::Ok(vec![plan, security]))
Plan: Projects, the home of everything about running software501 }
502
503 pub(crate) async fn subscribe(&self, a: SubscribeArgs) -> Result<Outcome<Checkout>> {
504 let workspace = a.workspace.to_lowercase();
Merge main (membership, two-factor, GitHub repo roles) into tokens505 if !a.actor.manages_billing(&workspace) {
Plan: Projects, the home of everything about running software506 return Ok(Outcome::fail(
507 FailureCode::Forbidden,
Merge main (membership, two-factor, GitHub repo roles) into tokens508 "Only an owner or a billing manager can turn on a paid feature.",
Plan: Projects, the home of everything about running software509 ));
510 }
511 let Some(stripe) = &self.stripe else {
512 return Ok(Outcome::fail(
513 FailureCode::Conflict,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look514 "Payments are not set up on this g1t, so the plan is already on.",
Plan: Projects, the home of everything about running software515 ));
516 };
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.517 // Deployments and Security and quality come with the plan: there is
518 // nothing of their own to start.
519 if a.feature == Feature::Security {
520 return Ok(Outcome::fail(
521 FailureCode::Conflict,
522 format!("Security and quality comes with the g1t plan, with no price of its own. Start the plan at /{workspace}/-/billing."),
523 ));
524 }
525 let feature = Feature::Plan;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar526 let name = if feature == Feature::Security { "The Security and quality activation" } else { "The g1t plan" };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look527 let state = self.state(&workspace, feature).await?;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put528 if state.included {
529 return Ok(Outcome::fail(
530 FailureCode::Conflict,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar531 format!("{name} is included for {workspace} already, at no charge."),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put532 ));
533 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar534 if self.plan_on(&workspace, feature).await? {
535 return Ok(Outcome::fail(FailureCode::Conflict, format!("{name} is already on for {workspace}.")));
Plan: Projects, the home of everything about running software536 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily537 let plan = self.plan(feature).await?;
Plan: Projects, the home of everything about running software538 let customer = self.row(&workspace).await?.and_then(|row| row.customer_id);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit539 // The card from the card check, or else the customer's default
540 // payment method (one added on Stripe's billing page counts): the
541 // plan starts on it at once, with no second page. A card that needs
542 // the bank's approval again goes through Stripe's page instead.
543 let saved = match (customer.as_deref(), self.checked_card(&workspace).await?) {
544 (Some(_), Some(method)) => Some(method),
545 (Some(customer), None) => match stripe.default_payment_method(customer).await {
546 Ok(method) => method.map(|m| m.id),
547 Err(error) => {
548 worker::console_log!("{workspace}: the default payment method could not be read: {error}");
549 None
550 }
551 },
552 (None, _) => None,
553 };
554 if let (Some(customer), Some(method)) = (customer.as_deref(), saved) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look555 match stripe
Merge Stripe Tax, the card fee on card payments, and one free workspace per person556 .subscribe_with_card(&workspace, feature.as_str(), &plan.title, plan.monthly_cents, plan.card_fee_cents, customer, &method)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look557 .await
558 {
559 Ok(subscription) if matches!(subscription.status.as_str(), "active" | "trialing") => {
560 self.record(&workspace, feature, &subscription, &a.actor.username).await?;
561 let account = self.account_of(&workspace).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar562 self.audit(
563 &account.id,
564 "plan",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit565 &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 bar566 &a.actor.username,
567 )
568 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look569 let separator = if a.return_url.contains('?') { '&' } else { '?' };
570 return Ok(Outcome::Ok(Checkout { url: format!("{}{separator}plan=started", a.return_url) }));
571 }
572 Ok(subscription) => {
573 // Incomplete: let it lapse, and use the page.
574 let _ = stripe.cancel_now(&subscription.id).await;
575 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit576 Err(error) => worker::console_log!("{workspace}: the plan could not start on the saved card: {error}"),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look577 }
578 }
Project dependencies: addresses, preview stacks, Affects, and agents who know579 let start = |customer: Option<String>| {
580 let plan = &plan;
581 let workspace = &workspace;
582 let return_url = &a.return_url;
583 async move {
584 stripe
585 .start_subscription(
586 workspace,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look587 feature.as_str(),
Project dependencies: addresses, preview stacks, Affects, and agents who know588 &plan.title,
589 plan.monthly_cents,
Merge Stripe Tax, the card fee on card payments, and one free workspace per person590 plan.card_fee_cents,
Project dependencies: addresses, preview stacks, Affects, and agents who know591 customer.as_deref(),
592 return_url,
593 )
594 .await
595 }
596 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit597 let started = match start(customer.clone()).await {
Project dependencies: addresses, preview stacks, Affects, and agents who know598 // A customer saved under another Stripe account: start afresh.
599 Err(error) if customer.is_some() && is_missing(&error) => {
600 self.forget_customer(&workspace).await?;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit601 start(None).await
Project dependencies: addresses, preview stacks, Affects, and agents who know602 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit603 other => other,
Project dependencies: addresses, preview stacks, Affects, and agents who know604 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit605 let session = match started {
606 Ok(session) => session,
607 Err(error) => {
608 worker::console_error!("{workspace}: Stripe refused the plan's page: {error}");
609 return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error)));
610 }
Plan: Projects, the home of everything about running software611 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit612 let Some(url) = session.url.clone() else {
613 return Ok(Outcome::fail(FailureCode::Conflict, "Stripe returned no payment page. Try again in a minute."));
614 };
615 if let Err(error) = self
616 .record_checkout(&crate::NewCheckout {
617 id: &session.id,
618 workspace: &workspace,
619 amount_cents: plan.monthly_cents,
Merge Stripe Tax, the card fee on card payments, and one free workspace per person620 fee_cents: plan.card_fee_cents,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit621 created_by: &a.actor.username,
622 feature: Some(feature.as_str()),
623 })
624 .await
625 {
626 worker::console_error!("{workspace}: the plan's page could not be recorded: {error}");
627 return Ok(Outcome::fail(FailureCode::Conflict, "g1t could not keep track of the payment page. Nothing was charged; try again."));
628 }
Plan: Projects, the home of everything about running software629 Ok(Outcome::Ok(Checkout { url }))
630 }
631
632 pub(crate) async fn confirm_subscription(
633 &self,
634 a: ConfirmSubscriptionArgs,
635 ) -> Result<Outcome<FeatureState>> {
636 let workspace = a.workspace.to_lowercase();
637 if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
638 return Ok(members_only());
639 }
640 let checkout = self
641 .db
642 .prepare(
643 "SELECT workspace, created_by, feature FROM checkouts
644 WHERE id = ? AND workspace = ? AND status = 'open' AND feature IS NOT NULL",
645 )
646 .bind(&[a.session.as_str().into(), workspace.as_str().into()])?
647 .first::<PlanCheckoutRow>(None)
648 .await?;
649 let (Some(stripe), Some(checkout)) = (&self.stripe, checkout) else {
650 // Unknown, someone else's, or already done: show where it stands.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look651 return Ok(Outcome::Ok(self.state(&workspace, Feature::Plan).await?));
Plan: Projects, the home of everything about running software652 };
653 let Some(feature) = Feature::parse(&checkout.feature) else {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look654 return Ok(Outcome::fail(FailureCode::NotFound, "No such plan."));
Plan: Projects, the home of everything about running software655 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit656 let session = match stripe.session(&a.session).await {
657 Ok(session) => session,
658 Err(error) => return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error))),
659 };
Plan: Projects, the home of everything about running software660 if let (Some(subscription_id), true) = (&session.subscription, session.payment_status == "paid") {
661 let claimed = self
662 .db
663 .prepare("UPDATE checkouts SET status = 'paid' WHERE id = ? AND status = 'open' RETURNING id")
664 .bind(&[a.session.as_str().into()])?
665 .first::<Touched>(None)
666 .await?;
667 if claimed.is_some() {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit668 let subscription = match stripe.subscription(subscription_id).await {
669 Ok(subscription) => subscription,
670 Err(error) => {
671 // Let the next look (or the webhook) settle it.
672 self.db.prepare("UPDATE checkouts SET status = 'open' WHERE id = ?").bind(&[a.session.as_str().into()])?.run().await?;
673 return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error)));
674 }
675 };
Plan: Projects, the home of everything about running software676 self.record(&checkout.workspace, feature, &subscription, &checkout.created_by)
677 .await?;
678 // Keep the card's customer, so later payments need no retyping.
679 self.db
680 .prepare(
681 "INSERT INTO accounts (workspace, balance_micros, customer_id, created_at)
682 VALUES (?1, 0, ?2, ?3)
683 ON CONFLICT (workspace) DO UPDATE SET customer_id = COALESCE(customer_id, ?2)",
684 )
685 .bind(&[
686 checkout.workspace.as_str().into(),
687 optional(session.customer.as_deref()),
688 rfc3339(now_ms()).into(),
689 ])?
690 .run()
691 .await?;
692 }
693 }
694 Ok(Outcome::Ok(self.state(&workspace, feature).await?))
695 }
696
697 pub(crate) async fn cancel_subscription(
698 &self,
699 a: CancelSubscriptionArgs,
700 ) -> Result<Outcome<FeatureState>> {
701 let workspace = a.workspace.to_lowercase();
Merge main (membership, two-factor, GitHub repo roles) into tokens702 if !a.actor.manages_billing(&workspace) {
Plan: Projects, the home of everything about running software703 return Ok(Outcome::fail(
704 FailureCode::Forbidden,
Merge main (membership, two-factor, GitHub repo roles) into tokens705 "Only an owner or a billing manager can change the workspace's plan.",
Plan: Projects, the home of everything about running software706 ));
707 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar708 // The activation; or the plan, or a Deployments subscription from
709 // before it.
710 let row = if a.feature == Feature::Security {
711 self.current(&workspace, Feature::Security).await?.map(|row| (Feature::Security, row))
712 } else {
713 match self.current(&workspace, Feature::Plan).await? {
714 Some(row) if status_from(&row.status) != SubscriptionStatus::Canceled => Some((Feature::Plan, row)),
715 _ => self.current(&workspace, Feature::Deployments).await?.map(|row| (Feature::Deployments, row)),
716 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look717 };
718 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 bar719 let name = if a.feature == Feature::Security { "The Security and quality activation" } else { "The g1t plan" };
720 return Ok(Outcome::fail(FailureCode::NotFound, format!("{name} is not on for {workspace}.")));
Plan: Projects, the home of everything about running software721 };
Usage, Billing settings and prepaid AI credit; fixes from the UX audit722 let subscription = match stripe.cancel_at_period_end(&row.subscription_id, !a.resume).await {
723 Ok(subscription) => subscription,
724 Err(error) => return Ok(Outcome::fail(FailureCode::Conflict, crate::stripe::friendly(&error))),
725 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look726 self.record(&workspace, feature, &subscription, &row.started_by)
Plan: Projects, the home of everything about running software727 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar728 let shown = if feature == Feature::Security { Feature::Security } else { Feature::Plan };
729 Ok(Outcome::Ok(self.state(&workspace, shown).await?))
Plan: Projects, the home of everything about running software730 }
731
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar732 /// Whether the workspace has the plan, which deployments come with, or
733 /// the Security and quality activation.
Plan: Projects, the home of everything about running software734 pub(crate) async fn has_feature(&self, a: HasFeatureArgs) -> Result<Outcome<bool>> {
735 let workspace = a.workspace.to_lowercase();
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar736 if a.feature == Feature::Security {
737 let state = self.state(&workspace, Feature::Security).await?;
738 if state.on {
739 return Ok(Outcome::Ok(true));
740 }
741 return Ok(Outcome::fail(
742 FailureCode::PaymentRequired,
743 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.744 "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 bar745 ),
746 ));
747 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look748 if self.has_plan(&workspace).await? {
Plan: Projects, the home of everything about running software749 return Ok(Outcome::Ok(true));
750 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look751 let what = match a.feature {
752 Feature::Deployments => "Deployments come with the g1t plan",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar753 Feature::Plan | Feature::Security => "This needs the g1t plan",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look754 };
Plan: Projects, the home of everything about running software755 Ok(Outcome::fail(
756 FailureCode::PaymentRequired,
757 format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look758 "{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."
Plan: Projects, the home of everything about running software759 ),
760 ))
761 }
762
763 pub(crate) async fn charge_feature(&self, a: ChargeFeatureArgs) -> Result<Outcome<bool>> {
764 if self.stripe.is_none() || a.cost_micros <= 0 {
765 return Ok(Outcome::Ok(false));
766 }
767 let workspace = a.workspace.to_lowercase();
768 let seen = self
769 .db
770 .prepare("SELECT id FROM ledger WHERE reference = ?")
771 .bind(&[a.reference.as_str().into()])?
772 .first::<Touched>(None)
773 .await?;
774 if seen.is_some() {
775 return Ok(Outcome::Ok(false));
776 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put777 let timestamp = rfc3339(now_ms());
778 let month = crate::credits::month_of(&timestamp);
779 let mut description = a.description.clone();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas780 // A build: every second is metered, at the price book's build
781 // second, which the keeper keeps at what Cloudflare bills, rather
782 // than at what the caller worked out. The month's build time is
783 // tallied for the Billing page.
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put784 let cost_micros = match a.build_seconds.filter(|s| *s > 0 && a.feature == Feature::Deployments) {
785 Some(seconds) => {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas786 self.tally("build_seconds", &workspace, &month, seconds.into()).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look787 let measured = self.price("build_second").await?.map(|(cost, _)| (f64::from(seconds) * cost).ceil() as i64);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas788 measured.unwrap_or(a.cost_micros)
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put789 }
790 None => a.cost_micros,
791 };
Billing accounts, terms and enterprises; g1t is no longer free792 // Never free: the margin applies whatever FREE_WHILE_BUILDING says,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look793 // and only the account's terms change it. The plan's included usage
794 // pays what it can; the trial and the open-source pool never pay for
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put795 // deployments.
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging796 let (charge, discount) = self.terms_of(&workspace).await?.discounted(crate::margin_on(cost_micros, self.margin_percent));
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put797 let drawn = self.draw(&workspace, charge, &month, &crate::credits::Eligible::default()).await?;
798 description.push_str(&drawn.note());
799 self.post_usage(crate::storage::UsageLine {
800 workspace: &workspace,
801 charged: charge - drawn.total(),
802 description: &description,
803 repo: a.repo.as_deref(),
804 task: a.feature.as_str(),
805 cost: cost_micros,
806 reference: &a.reference,
807 created_at: &timestamp,
808 drawn,
809 })
810 .await?;
Merge branch 'worktree-agent-a633ac0f7f66d419d'811 self.record_discount(&a.reference, discount).await?;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays812 self.count_spend(&workspace, cost_micros, charge - drawn.total(), &drawn).await;
Plan: Projects, the home of everything about running software813 Ok(Outcome::Ok(true))
814 }
815}
816
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas817/// `50,000`: a count as the plan reads it.
818pub(crate) fn thousands(n: u64) -> String {
819 let digits = n.to_string();
820 let mut out = String::new();
821 for (i, c) in digits.chars().enumerate() {
822 if i > 0 && (digits.len() - i).is_multiple_of(3) {
823 out.push(',');
824 }
825 out.push(c);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put826 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas827 out
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put828}
829
Plan: Projects, the home of everything about running software830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 #[test]
Billing answers every page in a few round trips, not forty: its reads run together, Stripe is asked at most hourly, and the ledger has a time index835 fn an_ended_plan_is_asked_about_at_most_once_an_hour() {
836 let now = 1_791_000_000_000;
837 let at = |ago_ms: u64| rfc3339(now - ago_ms);
838 let ended = at(24 * 60 * 60 * 1000);
839 // Ended, and last written a day ago: ask.
840 assert!(needs_refresh("active", Some(&ended), &ended, now));
841 // Ended, but written ten minutes ago: believe the row.
842 assert!(!needs_refresh("active", Some(&ended), &at(10 * 60 * 1000), now));
843 // An hour on, ask again.
844 assert!(needs_refresh("active", Some(&ended), &at(REFRESH_MS), now));
845 // No period known is the same as ended.
846 assert!(needs_refresh("past_due", None, &ended, now));
847 // A period still running, or a canceled plan, is never asked about.
848 assert!(!needs_refresh("active", Some(&rfc3339(now + 1000)), &ended, now));
849 assert!(!needs_refresh("canceled", Some(&ended), &ended, now));
850 }
851
852 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily853 fn the_plan_text_quotes_a_build_minute_as_the_table_does() {
854 // The price book's build second (16.44 millionths at cost, plus
855 // 20%) is 19.73 millionths: a minute is 1,184 millionths, $0.0012,
856 // as the pricing page's table says. The old fixed cost (15) gave
857 // $0.0011.
858 let each = Price::price_for(16.439_893_610_418_67, 20);
859 assert_eq!(per_units(each, 60.0), "$0.0012");
860 assert_eq!(per_units(Price::price_for(15.0, 20), 60.0), "$0.0011");
861 assert_eq!(per_units(Price::price_for(150_000.0, 20), 1.0), "$0.18");
862 }
863
864 #[test]
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas865 fn every_build_second_is_metered_at_cost_plus_the_margin() {
866 // A 5-minute build at 15 millionths a second costs g1t 4,500, and
867 // is charged at cost plus 20%, from the first second: there are no
868 // included build minutes, only the plan's included usage.
869 let cost = 300 * costs::MICROS_PER_BUILD_SECOND;
870 assert_eq!(cost, 4_500);
871 assert_eq!(crate::credits::with_margin(cost, 20), 5_400);
872 }
873
874 #[test]
875 fn counts_read_with_thousands_separators() {
876 assert_eq!(thousands(0), "0");
877 assert_eq!(thousands(999), "999");
878 assert_eq!(thousands(50_000), "50,000");
879 assert_eq!(thousands(1_234_567), "1,234,567");
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put880 }
881
882 #[test]
883 fn storage_reads_in_gigabytes() {
884 assert_eq!(bytes(1_000_000_000), "1 GB");
885 assert_eq!(bytes(50_000_000_000), "50 GB");
886 assert_eq!(bytes(1_500_000_000), "1.5 GB");
887 assert_eq!(bytes(500_000_000), "500 MB");
888 }
889
890 #[test]
Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure891 fn amounts_of_money_read_to_the_cent() {
892 assert_eq!(cents(3_986_990), "$3.99");
893 assert_eq!(cents(5_000_000), "$5.00");
894 assert_eq!(cents(4_000), "$0.00");
895 }
896
897 #[test]
Plan: Projects, the home of everything about running software898 fn prices_under_a_cent_keep_their_digits() {
899 assert_eq!(dollars(1512), "$0.0015");
900 assert_eq!(dollars(24_000), "$0.024");
901 assert_eq!(dollars(360_000), "$0.36");
902 assert_eq!(dollars(5_000_000), "$5.00");
903 }
904}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar905
906#[cfg(test)]
907mod security_activation {
908 use super::*;
909
910 #[test]
911 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.912 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 bar913 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.914 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 bar915 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.916 // The price book unreadable: no price, as the book says.
917 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 bar918 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.919 assert!(plan.includes.iter().any(|line| line.contains("cost plus 20%")));
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar920 assert!(plan.overage.contains("agent usage"));
921 }
922}

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