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

Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1//! The price book as versions, and how it changes.
2//!
3//! A price is its cost plus the markup, and costs move: Cloudflare's
4//! rates, how much CPU sandboxes use, what Artifacts turns out to count as
5//! an operation. So prices must move too, without surprising anyone and
6//! without g1t selling at a loss.
7//!
8//! - **Versions.** Every price is a row in `price_versions`, never changed
9//! once written. `prices` holds the version in force; a version waiting
10//! for its date is applied by the daily run, and recorded in
11//! `price_changes` (the public record on the pricing page). A charge
12//! records the version it was made at (`ledger.price_version`), so what a
13//! past statement says is always explained by the prices of then.
14//! - **Proposals.** The keeper and the reconciler measure costs; what they
15//! find is proposed (`price_proposals`). A move under 2% is noise. One
16//! within the guardrail (`auto_apply_percent`, 25%) is applied on its own
17//! when `auto_apply` is on. Anything larger, or more than four times off
18//! (suspect), waits for staff to approve or reject in sudo.
19//! - **Notice.** A fall applies at once: customers only gain. A rise
20//! applies `notice_days` (14) after it is decided, and a monthly meter's
21//! at the start of the month after that, so no month is charged at two
22//! prices. Owners of workspaces on the plan are emailed once per rise.
23//! Cost-plus means the rise does come: margin protection is for new usage
24//! after the notice, never retroactive.
25
26use g1t_contracts::billing::*;
27use g1t_contracts::time::{parse_rfc3339, rfc3339};
28use g1t_contracts::{FailureCode, Outcome, new_id};
29use g1t_kit::now_ms;
30use serde::Deserialize;
31use serde_json::Value;
32use worker::Result;
33
34use crate::Billing;
35
36/// Smaller moves are noise.
37pub(crate) const MIN_CHANGE: f64 = 0.02;
38/// A measurement outside this factor of the current cost is suspect: it
39/// is proposed for a person to look at, never applied on its own.
40pub(crate) const MAX_FACTOR: f64 = 4.0;
41
42/// Meters charged once a month from the month's total (`storage`). A rise
43/// in one takes effect at the start of a month.
44pub(crate) const MONTHLY: [&str; 7] = ["git_operations", "private_storage", "actions_cache", "custom_domain_month", "embedding_tokens", "scan_cpu", "scan_rows"];
45
46/// The price meters a month-end source is charged at, for the ledger's
47/// `price_version`.
48pub(crate) fn meters_of(source: &str) -> &'static [&'static str] {
49 match source {
50 "git" => &["git_operations"],
51 "storage" => &["private_storage"],
52 "context" => &["embedding_tokens"],
53 "security" => &["scan_cpu", "scan_rows"],
54 "domains" => &["custom_domain_month"],
55 "cache" => &["actions_cache"],
56 _ => &[],
57 }
58}
59
60/// Rises smaller than this, in percent, are listed on the pricing page
61/// with their date but not emailed: the keeper moves sandbox seconds by a
62/// percent or two at a time, and an email for each would be noise.
63const EMAIL_RISE_PERCENT: f64 = 5.0;
64
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises65/// What a meter's `cost_micros` is (`PriceVersion::basis`): a weight of the
66/// agent rate's tokens is a multiplier; the agent rate and security
67/// activation are prices g1t sets, with no per-unit cost behind them (the
68/// models a run uses are charged apart, at cost); everything else is a
69/// cost g1t pays.
70pub(crate) fn basis_of(meter: &str) -> &'static str {
71 if meter.starts_with("agent_token_weight_") {
72 "weight"
73 } else if matches!(meter, "agent_tokens" | "agent_tokens_own") || meter == crate::features::SECURITY_METER {
74 "rate"
75 } else {
76 "cost"
77 }
78}
79
80/// Marks the proposals behind a meter's versions still waiting for their
81/// date as superseded, before a newer version replaces them; who decided
82/// it, and when, stay. Parameter: the meter.
83pub(crate) const SUPERSEDE_WAITING_SQL: &str = "UPDATE price_proposals SET status = 'superseded'
84 WHERE version_id IN (SELECT id FROM price_versions WHERE meter = ?1 AND applied_at IS NULL)";
85
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily86/// Owners told of rises per run, at most; the rest on the next run.
87const NOTICES_PER_RUN: usize = 40;
88
89/// What may change prices without a person.
90#[derive(Clone, Copy, Debug, PartialEq)]
91pub(crate) struct Guard {
92 pub auto_apply: bool,
93 /// The largest move applied on its own, in percent either way.
94 pub auto_percent: f64,
95 /// Days between deciding a rise and charging it.
96 pub notice_days: u32,
97}
98
99impl From<&CostSettings> for Guard {
100 fn from(s: &CostSettings) -> Self {
101 Guard { auto_apply: s.auto_apply, auto_percent: s.auto_apply_percent, notice_days: s.notice_days }
102 }
103}
104
105#[derive(Clone, Debug, PartialEq)]
106pub(crate) enum Decision {
107 /// Too small to matter.
108 Nothing,
109 /// Applied without a person, from `effective_ms`.
110 Auto { effective_ms: u64 },
111 /// Waits for staff. `suspect` when the measurement is wildly off.
112 Approve { suspect: bool },
113}
114
115/// When a new cost takes effect: a fall at once; a rise after the notice
116/// period, and for a monthly meter at the start of the first month after
117/// it.
118pub(crate) fn effective_ms(current: f64, new: f64, now_ms: u64, notice_days: u32, monthly: bool) -> u64 {
119 if new <= current {
120 return now_ms;
121 }
122 let after = now_ms + u64::from(notice_days) * crate::costs::DAY_MS;
123 if !monthly {
124 return after;
125 }
126 let stamp = rfc3339(after);
127 if &stamp[8..] == "01T00:00:00.000Z" {
128 return after;
129 }
130 parse_rfc3339(&crate::credits::next_month_start(&stamp[..7])).unwrap_or(after)
131}
132
133/// What to do with a measured cost against the one in force (or the one
134/// already scheduled).
135pub(crate) fn decide(current: f64, measured: f64, guard: Guard, now_ms: u64, monthly: bool) -> Decision {
136 if !measured.is_finite() || measured <= 0.0 || !current.is_finite() || current <= 0.0 {
137 return Decision::Nothing;
138 }
139 let ratio = measured / current;
140 if (ratio - 1.0).abs() < MIN_CHANGE {
141 return Decision::Nothing;
142 }
143 if !(1.0 / MAX_FACTOR..=MAX_FACTOR).contains(&ratio) {
144 return Decision::Approve { suspect: true };
145 }
146 if guard.auto_apply && (ratio - 1.0).abs() * 100.0 <= guard.auto_percent {
147 return Decision::Auto { effective_ms: effective_ms(current, measured, now_ms, guard.notice_days, monthly) };
148 }
149 Decision::Approve { suspect: false }
150}
151
152/// A version of one meter's price.
153#[derive(Clone, Debug, PartialEq, Deserialize)]
154pub(crate) struct Version {
155 pub id: String,
156 pub meter: String,
157 pub version: u32,
158 pub cost_micros: f64,
159 pub markup_percent: u32,
160 pub effective_at: String,
161 pub reason: String,
162 pub created_by: String,
163 pub applied_at: Option<String>,
164}
165
166/// The version in force for `meter` at `at`: the newest whose date has
167/// come. Old versions never change, so a past month's charges are always
168/// explained by the version of then.
169pub(crate) fn in_force<'a>(versions: &'a [Version], meter: &str, at: &str) -> Option<&'a Version> {
170 versions
171 .iter()
172 .filter(|v| v.meter == meter && v.effective_at.as_str() <= at)
173 .max_by(|a, b| a.effective_at.cmp(&b.effective_at).then(a.version.cmp(&b.version)))
174}
175
176/// Settings from `cost_settings` rows, defaults for anything missing or
177/// unreadable.
178pub(crate) fn settings_from(rows: &[(String, String)]) -> CostSettings {
179 let mut s = CostSettings::default();
180 for (key, value) in rows {
181 let value = value.trim();
182 match key.as_str() {
183 "auto_apply" => s.auto_apply = value == "true",
184 "auto_apply_percent" => s.auto_apply_percent = value.parse().unwrap_or(s.auto_apply_percent),
185 "notice_days" => s.notice_days = value.parse().unwrap_or(s.notice_days),
186 "margin_floor_percent" => s.margin_floor_percent = value.parse().unwrap_or(s.margin_floor_percent),
187 "alert_days" => s.alert_days = value.parse().unwrap_or(s.alert_days),
188 "min_daily_cost_micros" => s.min_daily_cost_micros = value.parse().unwrap_or(s.min_daily_cost_micros),
189 "anomaly_factor" => s.anomaly_factor = value.parse().unwrap_or(s.anomaly_factor),
190 "anomaly_floor_micros" => s.anomaly_floor_micros = value.parse().unwrap_or(s.anomaly_floor_micros),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit191 "card_fee" => s.card_fee = value != "off",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily192 _ => {}
193 }
194 }
195 s
196}
197
198/// Why settings cannot be saved, if they cannot.
199pub(crate) fn check_settings(s: &CostSettings) -> std::result::Result<(), String> {
200 if !(0.0..=100.0).contains(&s.auto_apply_percent) {
201 return Err("The guardrail is a percentage from 0 to 100.".into());
202 }
203 if s.notice_days > 90 {
204 return Err("Notice is at most 90 days.".into());
205 }
206 if !(-100.0..=100.0).contains(&s.margin_floor_percent) {
207 return Err("The margin floor is a percentage.".into());
208 }
209 if s.alert_days == 0 || s.alert_days > 30 {
210 return Err("Alert after 1 to 30 days.".into());
211 }
212 if s.min_daily_cost_micros < 0 || s.anomaly_floor_micros < 0 || !(s.anomaly_factor > 0.0 && s.anomaly_factor.is_finite()) {
213 return Err("Amounts and the factor must be positive.".into());
214 }
215 Ok(())
216}
217
218#[derive(Deserialize)]
219struct ProposalRow {
220 id: String,
221 meter: String,
222 current_cost_micros: f64,
223 proposed_cost_micros: f64,
224 reason: String,
225 source: String,
226 suspect: i64,
227 status: String,
228 created_at: String,
229 decided_at: Option<String>,
230 decided_by: Option<String>,
231 note: Option<String>,
232 version_id: Option<String>,
233 title: Option<String>,
234 unit: Option<String>,
235 markup_percent: Option<u32>,
236 effective_at: Option<String>,
237}
238
239impl From<ProposalRow> for PriceProposal {
240 fn from(r: ProposalRow) -> Self {
241 let change = if r.current_cost_micros > 0.0 { (r.proposed_cost_micros / r.current_cost_micros - 1.0) * 100.0 } else { 0.0 };
242 PriceProposal {
243 title: r.title.unwrap_or_else(|| r.meter.clone()),
244 unit: r.unit.unwrap_or_default(),
245 markup_percent: r.markup_percent.unwrap_or(20),
246 id: r.id,
247 meter: r.meter,
248 current_cost_micros: r.current_cost_micros,
249 proposed_cost_micros: r.proposed_cost_micros,
250 change_percent: change,
251 reason: r.reason,
252 source: r.source,
253 suspect: r.suspect != 0,
254 status: r.status,
255 created_at: r.created_at,
256 decided_at: r.decided_at,
257 decided_by: r.decided_by,
258 note: r.note,
259 effective_at: r.version_id.and(r.effective_at),
260 }
261 }
262}
263
264const PROPOSAL_SQL: &str = "SELECT p.*, pr.title, pr.unit, pr.markup_percent, v.effective_at
265 FROM price_proposals p
266 LEFT JOIN prices pr ON pr.meter = p.meter
267 LEFT JOIN price_versions v ON v.id = p.version_id";
268
269impl Billing {
270 pub(crate) async fn cost_settings(&self) -> Result<CostSettings> {
271 #[derive(Deserialize)]
272 struct Row {
273 key: String,
274 value: String,
275 }
276 let rows = self.db.prepare("SELECT key, value FROM cost_settings").all().await?.results::<Row>()?;
277 Ok(settings_from(&rows.into_iter().map(|r| (r.key, r.value)).collect::<Vec<_>>()))
278 }
279
280 pub(crate) async fn admin_set_cost_settings(&self, a: AdminSetCostSettingsArgs) -> Result<Outcome<CostSettings>> {
281 if let Err(why) = check_settings(&a.settings) {
282 return Ok(Outcome::fail(FailureCode::Invalid, why));
283 }
284 let s = &a.settings;
285 let now = rfc3339(now_ms());
286 let pairs = [
287 ("auto_apply", s.auto_apply.to_string()),
288 ("auto_apply_percent", s.auto_apply_percent.to_string()),
289 ("notice_days", s.notice_days.to_string()),
290 ("margin_floor_percent", s.margin_floor_percent.to_string()),
291 ("alert_days", s.alert_days.to_string()),
292 ("min_daily_cost_micros", s.min_daily_cost_micros.to_string()),
293 ("anomaly_factor", s.anomaly_factor.to_string()),
294 ("anomaly_floor_micros", s.anomaly_floor_micros.to_string()),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit295 ("card_fee", if s.card_fee { "on" } else { "off" }.to_owned()),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily296 ];
297 let mut statements = Vec::new();
298 for (key, value) in &pairs {
299 statements.push(
300 self.db
301 .prepare(
302 "INSERT INTO cost_settings (key, value, updated_at, updated_by) VALUES (?1, ?2, ?3, ?4)
303 ON CONFLICT (key) DO UPDATE SET value = ?2, updated_at = ?3, updated_by = ?4",
304 )
305 .bind(&[(*key).into(), value.as_str().into(), now.as_str().into(), a.by.as_str().into()])?,
306 );
307 }
308 self.db.batch(statements).await?;
309 let detail = pairs.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join(", ");
310 self.audit("costs", "cost_settings", &detail, &a.by).await?;
311 Ok(Outcome::Ok(self.cost_settings().await?))
312 }
313
314 /// The cost a new measurement is judged against: the newest version
315 /// scheduled, else the one in force.
316 async fn latest_cost(&self, meter: &str) -> Result<Option<(f64, u32)>> {
317 #[derive(Deserialize)]
318 struct Row {
319 cost_micros: f64,
320 markup_percent: u32,
321 }
322 let scheduled = self
323 .db
324 .prepare("SELECT cost_micros, markup_percent FROM price_versions WHERE meter = ? AND applied_at IS NULL ORDER BY version DESC LIMIT 1")
325 .bind(&[meter.into()])?
326 .first::<Row>(None)
327 .await?;
328 if let Some(row) = scheduled {
329 return Ok(Some((row.cost_micros, row.markup_percent)));
330 }
331 Ok(self
332 .db
333 .prepare("SELECT cost_micros, markup_percent FROM prices WHERE meter = ?")
334 .bind(&[meter.into()])?
335 .first::<Row>(None)
336 .await?
337 .map(|r| (r.cost_micros, r.markup_percent)))
338 }
339
340 /// Proposes a measured cost for a meter. Returns what happened, in
341 /// words, or None when there was nothing to do.
342 pub(crate) async fn propose(&self, meter: &str, measured: f64, reason: &str, source: &str) -> Result<Option<String>> {
343 let Some((current, markup)) = self.latest_cost(meter).await? else {
344 return Ok(None);
345 };
346 // Ten-thousandths of a micro are plenty; floating-point tails are not.
347 let measured = (measured * 10_000.0).round() / 10_000.0;
348 let settings = self.cost_settings().await?;
349 let now = now_ms();
350 let decision = decide(current, measured, Guard::from(&settings), now, MONTHLY.contains(&meter));
351 if decision == Decision::Nothing {
352 return Ok(None);
353 }
354 let stamp = rfc3339(now);
355 let id = new_id("ppr", now);
356 // A newer measurement replaces an open one.
357 self.db
358 .prepare("UPDATE price_proposals SET status = 'superseded', decided_at = ? WHERE meter = ? AND status = 'open'")
359 .bind(&[stamp.as_str().into(), meter.into()])?
360 .run()
361 .await?;
362 let (status, suspect) = match decision {
363 Decision::Auto { .. } => ("applied", false),
364 Decision::Approve { suspect } => ("open", suspect),
365 Decision::Nothing => unreachable!(),
366 };
367 self.db
368 .prepare(
369 "INSERT INTO price_proposals (id, meter, current_cost_micros, proposed_cost_micros, reason, source, suspect, status, created_at, decided_at, decided_by)
370 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
371 )
372 .bind(&[
373 id.as_str().into(),
374 meter.into(),
375 current.into(),
376 measured.into(),
377 reason.into(),
378 source.into(),
379 i32::from(suspect).into(),
380 status.into(),
381 stamp.as_str().into(),
382 crate::optional((status == "applied").then_some(stamp.as_str())),
383 crate::optional((status == "applied").then_some("guardrail")),
384 ])?
385 .run()
386 .await?;
387 if status == "open" {
388 return Ok(Some(format!(
389 "proposed {measured:.4} against {current:.4}{}, waiting for staff",
390 if suspect { " (far off: look before approving)" } else { "" }
391 )));
392 }
393 let Decision::Auto { effective_ms } = decision else { unreachable!() };
394 let version = self.schedule_version(meter, measured, markup, effective_ms, reason, source, Some(&id)).await?;
395 self.apply_due_versions().await?;
396 Ok(Some(format!("applied {measured:.4} (was {current:.4}) from {} as {version}", rfc3339(effective_ms))))
397 }
398
399 /// Writes the next version of a meter's price, to take effect at
400 /// `effective_ms`. Returns its id.
401 #[allow(clippy::too_many_arguments)]
402 pub(crate) async fn schedule_version(
403 &self,
404 meter: &str,
405 cost: f64,
406 markup: u32,
407 effective_ms: u64,
408 reason: &str,
409 by: &str,
410 proposal: Option<&str>,
411 ) -> Result<String> {
412 #[derive(Deserialize)]
413 struct Top {
414 version: Option<u32>,
415 }
416 let next = self
417 .db
418 .prepare("SELECT MAX(version) AS version FROM price_versions WHERE meter = ?")
419 .bind(&[meter.into()])?
420 .first::<Top>(None)
421 .await?
422 .and_then(|t| t.version)
423 .unwrap_or(0)
424 + 1;
425 let id = format!("pv_{meter}_{next}");
426 let now = rfc3339(now_ms());
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises427 // A newer decision replaces a rise still waiting for its date. The
428 // proposal behind it never took effect: superseded, not "applied"
429 // with no date it is in force from.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily430 self.db
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises431 .batch(vec![
432 self.db.prepare(SUPERSEDE_WAITING_SQL).bind(&[meter.into()])?,
433 self.db.prepare("DELETE FROM price_versions WHERE meter = ? AND applied_at IS NULL").bind(&[meter.into()])?,
434 ])
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily435 .await?;
436 self.db
437 .prepare(
438 "INSERT INTO price_versions (id, meter, version, cost_micros, markup_percent, effective_at, reason, created_by, proposal_id, created_at)
439 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
440 )
441 .bind(&[
442 id.as_str().into(),
443 meter.into(),
444 next.into(),
445 cost.into(),
446 markup.into(),
447 rfc3339(effective_ms).into(),
448 reason.into(),
449 by.into(),
450 crate::optional(proposal),
451 now.as_str().into(),
452 ])?
453 .run()
454 .await?;
455 if let Some(proposal) = proposal {
456 self.db
457 .prepare("UPDATE price_proposals SET version_id = ? WHERE id = ?")
458 .bind(&[id.as_str().into(), proposal.into()])?
459 .run()
460 .await?;
461 }
462 Ok(id)
463 }
464
465 /// Puts every version whose date has come into the price book, oldest
466 /// first, each once, with a public record of the change.
467 pub(crate) async fn apply_due_versions(&self) -> Result<u32> {
468 let now = rfc3339(now_ms());
469 let due = self
470 .db
471 .prepare("SELECT * FROM price_versions WHERE applied_at IS NULL AND effective_at <= ? ORDER BY effective_at, version")
472 .bind(&[now.as_str().into()])?
473 .all()
474 .await?
475 .results::<Version>()?;
476 let mut applied = 0;
477 for v in due {
478 let claimed = self
479 .db
480 .prepare("UPDATE price_versions SET applied_at = ? WHERE id = ? AND applied_at IS NULL RETURNING id")
481 .bind(&[now.as_str().into(), v.id.as_str().into()])?
482 .first::<Value>(None)
483 .await?;
484 if claimed.is_none() {
485 continue;
486 }
487 let reason = if v.created_by.contains('@') { format!("{} (approved by g1t staff)", v.reason) } else { v.reason.clone() };
488 self.db
489 .batch(vec![
490 self.db
491 .prepare(
492 "INSERT INTO price_changes (id, meter, old_cost_micros, new_cost_micros, markup_percent, old_markup_percent, reason, created_at)
493 SELECT ?, meter, cost_micros, ?, ?, CASE WHEN markup_percent <> ? THEN markup_percent END, ?, ? FROM prices WHERE meter = ?",
494 )
495 .bind(&[
496 new_id("prc", now_ms()).into(),
497 v.cost_micros.into(),
498 v.markup_percent.into(),
499 v.markup_percent.into(),
500 reason.as_str().into(),
501 now.as_str().into(),
502 v.meter.as_str().into(),
503 ])?,
504 self.db
505 .prepare("UPDATE prices SET cost_micros = ?, markup_percent = ?, source = 'cloudflare', updated_at = ? WHERE meter = ?")
506 .bind(&[v.cost_micros.into(), v.markup_percent.into(), now.as_str().into(), v.meter.as_str().into()])?,
507 ])
508 .await?;
509 applied += 1;
510 }
511 Ok(applied)
512 }
513
514 /// The id of the price version in force for `meter` now, for the
515 /// ledger.
516 pub(crate) async fn version_now(&self, meter: &str) -> Result<Option<String>> {
517 let versions = self
518 .db
519 .prepare("SELECT * FROM price_versions WHERE meter = ? AND applied_at IS NOT NULL")
520 .bind(&[meter.into()])?
521 .all()
522 .await?
523 .results::<Version>()?;
524 Ok(in_force(&versions, meter, &rfc3339(now_ms())).map(|v| v.id.clone()))
525 }
526
527 pub(crate) async fn proposals(&self) -> Result<Vec<PriceProposal>> {
528 Ok(self
529 .db
530 .prepare(format!(
531 "{PROPOSAL_SQL} ORDER BY CASE WHEN p.status = 'open' THEN 0 ELSE 1 END, p.created_at DESC LIMIT 40"
532 ))
533 .all()
534 .await?
535 .results::<ProposalRow>()?
536 .into_iter()
537 .map(PriceProposal::from)
538 .collect())
539 }
540
541 pub(crate) async fn versions(&self) -> Result<Vec<PriceVersion>> {
542 Ok(self
543 .db
544 .prepare("SELECT * FROM price_versions ORDER BY CASE WHEN applied_at IS NULL THEN 0 ELSE 1 END, effective_at DESC, version DESC LIMIT 60")
545 .all()
546 .await?
547 .results::<Version>()?
548 .into_iter()
549 .map(|v| PriceVersion {
550 price_micros: Price::price_for(v.cost_micros, v.markup_percent),
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises551 basis: basis_of(&v.meter).to_owned(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily552 id: v.id,
553 meter: v.meter,
554 version: v.version,
555 cost_micros: v.cost_micros,
556 markup_percent: v.markup_percent,
557 effective_at: v.effective_at,
558 reason: v.reason,
559 created_by: v.created_by,
560 applied_at: v.applied_at,
561 })
562 .collect())
563 }
564
565 /// Prices not in force yet, for the pricing page's "coming" changes.
566 pub(crate) async fn coming_changes(&self) -> Result<Vec<PriceChange>> {
567 #[derive(Deserialize)]
568 struct Row {
569 meter: String,
570 old_cost_micros: Option<f64>,
571 cost_micros: f64,
572 markup_percent: u32,
573 reason: String,
574 created_at: String,
575 effective_at: String,
576 }
577 Ok(self
578 .db
579 .prepare(
580 "SELECT v.meter, p.cost_micros AS old_cost_micros, v.cost_micros, v.markup_percent, v.reason, v.created_at, v.effective_at
581 FROM price_versions v LEFT JOIN prices p ON p.meter = v.meter
582 WHERE v.applied_at IS NULL ORDER BY v.effective_at",
583 )
584 .all()
585 .await?
586 .results::<Row>()?
587 .into_iter()
588 .map(|r| PriceChange {
589 old_cost_micros: r.old_cost_micros.unwrap_or(r.cost_micros),
590 meter: r.meter,
591 new_cost_micros: r.cost_micros,
592 markup_percent: r.markup_percent,
593 old_markup_percent: None,
594 reason: r.reason,
595 created_at: r.created_at,
596 effective_at: Some(r.effective_at),
597 })
598 .collect())
599 }
600
601 /// `admin_decide_proposal`: an approved fall applies now; an approved
602 /// rise after the notice period.
603 pub(crate) async fn admin_decide_proposal(&self, a: AdminDecideProposalArgs) -> Result<Outcome<PriceProposal>> {
604 let found = self
605 .db
606 .prepare(format!("{PROPOSAL_SQL} WHERE p.id = ?"))
607 .bind(&[a.id.as_str().into()])?
608 .first::<ProposalRow>(None)
609 .await?;
610 let Some(found) = found else {
611 return Ok(Outcome::fail(FailureCode::NotFound, "No such proposal."));
612 };
613 if found.status != "open" {
614 return Ok(Outcome::fail(FailureCode::Conflict, format!("This proposal is already {}.", found.status)));
615 }
616 let approve = match a.decision.as_str() {
617 "approve" => true,
618 "reject" => false,
619 _ => return Ok(Outcome::fail(FailureCode::Invalid, "Approve or reject.")),
620 };
621 if !approve && a.note.trim().is_empty() {
622 return Ok(Outcome::fail(FailureCode::Invalid, "Say why it is rejected, for whoever measures it next."));
623 }
624 let now = now_ms();
625 let stamp = rfc3339(now);
626 self.db
627 .prepare("UPDATE price_proposals SET status = ?, decided_at = ?, decided_by = ?, note = ? WHERE id = ? AND status = 'open'")
628 .bind(&[
629 if approve { "approved" } else { "rejected" }.into(),
630 stamp.as_str().into(),
631 a.by.as_str().into(),
632 a.note.trim().into(),
633 a.id.as_str().into(),
634 ])?
635 .run()
636 .await?;
637 if approve {
638 let settings = self.cost_settings().await?;
639 let (current, markup) = self.latest_cost(&found.meter).await?.unwrap_or((found.current_cost_micros, 20));
640 let effective = effective_ms(current, found.proposed_cost_micros, now, settings.notice_days, MONTHLY.contains(&found.meter.as_str()));
641 self.schedule_version(&found.meter, found.proposed_cost_micros, markup, effective, &found.reason, &a.by, Some(&found.id))
642 .await?;
643 self.apply_due_versions().await?;
644 }
645 self.audit(
646 "costs",
647 if approve { "price_approved" } else { "price_rejected" },
648 &format!("{}: {:.4} to {:.4}. {}", found.meter, found.current_cost_micros, found.proposed_cost_micros, a.note.trim()),
649 &a.by,
650 )
651 .await?;
652 let row = self
653 .db
654 .prepare(format!("{PROPOSAL_SQL} WHERE p.id = ?"))
655 .bind(&[a.id.as_str().into()])?
656 .first::<ProposalRow>(None)
657 .await?;
658 Ok(match row {
659 Some(row) => Outcome::Ok(row.into()),
660 None => Outcome::fail(FailureCode::NotFound, "No such proposal."),
661 })
662 }
663
664 /// Emails owners of workspaces on the plan about each rise still to
665 /// come, once per rise per workspace.
666 pub(crate) async fn tell_owners_of_rises(&self, identity: &worker::Fetcher) -> Result<()> {
667 #[derive(Deserialize)]
668 struct Rise {
669 id: String,
670 title: Option<String>,
671 old_cost: f64,
672 cost_micros: f64,
673 markup_percent: u32,
674 unit: Option<String>,
675 effective_at: String,
676 reason: String,
677 }
678 let rises = self
679 .db
680 .prepare(
681 "SELECT v.id, p.title, p.cost_micros AS old_cost, v.cost_micros, v.markup_percent, p.unit, v.effective_at, v.reason
682 FROM price_versions v JOIN prices p ON p.meter = v.meter
683 WHERE v.applied_at IS NULL AND v.cost_micros > p.cost_micros * ?",
684 )
685 .bind(&[(1.0 + EMAIL_RISE_PERCENT / 100.0).into()])?
686 .all()
687 .await?
688 .results::<Rise>()?;
689 let mut sent = 0;
690 for rise in rises {
691 #[derive(Deserialize)]
692 struct Workspace {
693 workspace: String,
694 }
695 let workspaces = self
696 .db
697 .prepare(
698 "SELECT DISTINCT workspace FROM subscriptions WHERE feature = 'plan' AND status IN ('active', 'canceling')
699 AND workspace NOT IN (SELECT workspace FROM price_notices WHERE version_id = ?) LIMIT ?",
700 )
701 .bind(&[rise.id.as_str().into(), ((NOTICES_PER_RUN - sent) as u32).into()])?
702 .all()
703 .await?
704 .results::<Workspace>()?;
705 let title = rise.title.clone().unwrap_or_default();
706 let unit = rise.unit.clone().unwrap_or_default();
707 let price = |cost: f64| crate::features::dollars(Price::price_for(cost, rise.markup_percent).round() as i64);
708 let intro = format!(
709 "From {}, {title} on g1t goes from {} to {} per {unit}. It is what g1t pays Cloudflare plus 20%, and the cost moved: {} Nothing already charged changes.",
710 &rise.effective_at[..10],
711 price(rise.old_cost),
712 price(rise.cost_micros),
713 rise.reason
714 );
715 for Workspace { workspace } in workspaces {
716 let args = g1t_contracts::identity::NotifyOwnersArgs {
717 workspace: workspace.clone(),
718 subject: format!("g1t: {title} costs more from {}", &rise.effective_at[..10]),
719 intro: intro.clone(),
720 action: "See prices".to_owned(),
721 link: "https://g1t.sh/pricing".to_owned(),
722 footer: "You get this because you own a workspace on the g1t plan. How prices follow costs: https://docs.g1t.sh/guides/usage-and-billing/#how-prices-are-set".to_owned(),
723 };
724 // Recorded whether or not anyone was there to tell.
725 if let Err(error) = g1t_kit::call::<_, u32>(identity, "notify_owners", &args).await {
726 worker::console_error!("could not tell {workspace} of a price rise: {error}");
727 continue;
728 }
729 self.db
730 .prepare("INSERT OR IGNORE INTO price_notices (version_id, workspace, sent_at) VALUES (?, ?, ?)")
731 .bind(&[rise.id.as_str().into(), workspace.as_str().into(), rfc3339(now_ms()).into()])?
732 .run()
733 .await?;
734 sent += 1;
735 if sent >= NOTICES_PER_RUN {
736 return Ok(());
737 }
738 }
739 }
740 Ok(())
741 }
742}
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747
748 const NOW: &str = "2026-10-06T04:17:00.000Z";
749
750 fn now() -> u64 {
751 parse_rfc3339(NOW).unwrap()
752 }
753
754 fn guard() -> Guard {
755 Guard { auto_apply: true, auto_percent: 25.0, notice_days: 14 }
756 }
757
758 #[test]
759 fn small_moves_are_noise_and_wild_ones_wait_for_a_person() {
760 assert_eq!(decide(21.0, 21.2, guard(), now(), false), Decision::Nothing);
761 assert_eq!(decide(21.0, 0.0, guard(), now(), false), Decision::Nothing);
762 assert_eq!(decide(21.0, 200.0, guard(), now(), false), Decision::Approve { suspect: true });
763 assert_eq!(decide(21.0, 2.0, guard(), now(), false), Decision::Approve { suspect: true });
764 }
765
766 #[test]
767 fn moves_inside_the_guardrail_apply_themselves_falls_at_once_rises_after_notice() {
768 // Down 19%: at once.
769 assert_eq!(decide(21.0, 17.0, guard(), now(), false), Decision::Auto { effective_ms: now() });
770 // Up 19%: after 14 days.
771 let Decision::Auto { effective_ms } = decide(21.0, 25.0, guard(), now(), false) else { panic!() };
772 assert_eq!(rfc3339(effective_ms), "2026-10-20T04:17:00.000Z");
773 // A monthly meter's rise waits for the month after the notice.
774 let Decision::Auto { effective_ms } = decide(150_000.0, 180_000.0, guard(), now(), true) else { panic!() };
775 assert_eq!(rfc3339(effective_ms), "2026-11-01T00:00:00.000Z");
776 let late = parse_rfc3339("2026-10-25T00:00:00Z").unwrap();
777 assert_eq!(rfc3339(effective_ms_for(late)), "2026-12-01T00:00:00.000Z");
778 }
779
780 fn effective_ms_for(now: u64) -> u64 {
781 effective_ms(1.0, 2.0, now, 14, true)
782 }
783
784 #[test]
Merge costs and margin review: gateway query, own spend, discount meters, superseded rises785 fn a_versions_cost_says_what_it_is() {
786 // The agent rate's $0.25 a million tokens is g1t's price, not what
787 // tokens cost g1t; the weights are multipliers.
788 assert_eq!(basis_of("agent_tokens"), "rate");
789 assert_eq!(basis_of("agent_tokens_own"), "rate");
790 assert_eq!(basis_of("security_activation"), "rate");
791 assert_eq!(basis_of("agent_token_weight_cache_read"), "weight");
792 // A provider's dollar passed on at cost, and Cloudflare's units, are costs.
793 for meter in ["agent_models", "gateway_models", "sandbox_second", "git_operations", "card_fee_percent"] {
794 assert_eq!(basis_of(meter), "cost", "{meter}");
795 }
796 }
797
798 #[test]
799 fn a_rise_replaced_before_its_date_is_superseded_not_applied() {
800 // 2026-10-07's sandbox rise (v2, due 10-21) was replaced by
801 // 10-08's (v3, due 10-22): v2's proposal never took effect.
802 let sql = SUPERSEDE_WAITING_SQL;
803 assert_eq!(crate::rename::parameters(sql), 1);
804 assert!(sql.contains("status = 'superseded'") && sql.contains("applied_at IS NULL") && !sql.contains("decided_"), "{sql}");
805 // A rise still waits its notice: the replacement is no shortcut.
806 let Decision::Auto { effective_ms } = decide(18.1817, 19.5181, guard(), now(), false) else { panic!() };
807 assert_eq!(rfc3339(effective_ms), "2026-10-20T04:17:00.000Z");
808 }
809
810 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily811 fn past_the_guardrail_or_with_auto_off_staff_decide() {
812 // Up 50%: staff.
813 assert_eq!(decide(150_000.0, 225_000.0, guard(), now(), true), Decision::Approve { suspect: false });
814 // Down 30%: staff too; the guardrail is either way.
815 assert_eq!(decide(100.0, 70.0, guard(), now(), false), Decision::Approve { suspect: false });
816 let off = Guard { auto_apply: false, ..guard() };
817 assert_eq!(decide(21.0, 22.0, off, now(), false), Decision::Approve { suspect: false });
818 }
819
820 #[test]
821 fn a_past_statement_keeps_the_price_of_its_time() {
822 let v = |version: u32, cost: f64, at: &str| Version {
823 id: format!("pv_git_operations_{version}"),
824 meter: "git_operations".into(),
825 version,
826 cost_micros: cost,
827 markup_percent: 20,
828 effective_at: at.into(),
829 reason: String::new(),
830 created_by: "keeper".into(),
831 applied_at: None,
832 };
833 let versions = vec![v(1, 150_000.0, "2026-10-05T00:00:00Z"), v(2, 450_000.0, "2026-11-01T00:00:00.000Z"), v(3, 400_000.0, "2026-12-01T00:00:00.000Z")];
834 // October's charges were at version 1, whatever came later.
835 assert_eq!(in_force(&versions, "git_operations", "2026-10-31T23:59:59Z").unwrap().version, 1);
836 assert_eq!(in_force(&versions, "git_operations", "2026-11-15T00:00:00Z").unwrap().version, 2);
837 assert_eq!(in_force(&versions, "git_operations", "2027-01-01T00:00:00Z").unwrap().cost_micros, 400_000.0);
838 assert!(in_force(&versions, "git_operations", "2026-01-01T00:00:00Z").is_none());
839 assert!(in_force(&versions, "sandbox_second", "2027-01-01T00:00:00Z").is_none());
840 }
841
842 #[test]
843 fn settings_read_with_defaults_and_are_checked() {
844 let s = settings_from(&[("auto_apply".into(), "false".into()), ("notice_days".into(), "30".into()), ("anomaly_factor".into(), "x".into())]);
845 assert!(!s.auto_apply);
846 assert_eq!(s.notice_days, 30);
847 assert_eq!(s.anomaly_factor, 1.0);
848 assert_eq!(s.auto_apply_percent, 25.0);
849 assert!(check_settings(&s).is_ok());
850 assert!(check_settings(&CostSettings { notice_days: 120, ..s.clone() }).is_err());
851 assert!(check_settings(&CostSettings { alert_days: 0, ..s.clone() }).is_err());
852 assert!(check_settings(&CostSettings { auto_apply_percent: 150.0, ..s }).is_err());
853 }
854}

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