Skip to content

Commit

Merge a fair price check: the price book against Cloudflare's list price before allowances, not what was billed past them, so usage inside the included amounts is never a stale price

syntaqxcommitted Parentse17babbac4005eBrowse files
4 files+232−500/4 viewed
+10−3
2020
2121 | Source | What | Where it lands |
2222 | --- | --- | --- |
23−| Billable usage, `GET /accounts/{account}/billable-usage?from=&to=` | One row per service per day in FOCUS columns: `ServiceFamilyName`, `ServiceName`, `ChargePeriodStart`, `ConsumedQuantity` (else `PricingQuantity`), `ContractedCost` / `BilledCost` / `EffectiveCost`. Every page is read (`result_info`: `cursor`, else `total_pages`), over whole billing cycles (see [The billing cycle](#the-billing-cycle)). Every product g1t uses appears here once it is used: Workers, Workers for Platforms, D1, KV, R2, Queues, Containers, Durable Objects, Artifacts, Browser Rendering, Workers AI, Vectorize, Cloudflare for SaaS, Email. While a cycle is open its rows carry no cost; `ListCost` is never taken, since it is before the included amounts. | `cost_lines`, source `billable_usage`: `quantity` (consumed), `billed_usd` (Cloudflare's own cost), `billable_quantity` and `cost_usd` (over the cycle), `basis`; the read itself in `cost_reads` |
23+| Billable usage, `GET /accounts/{account}/billable-usage?from=&to=` | One row per service per day in FOCUS columns: `ServiceFamilyName`, `ServiceName`, `ChargePeriodStart`, `ConsumedQuantity` (else `PricingQuantity`), `ContractedCost` / `BilledCost` / `EffectiveCost`. Every page is read (`result_info`: `cursor`, else `total_pages`), over whole billing cycles (see [The billing cycle](#the-billing-cycle)). Every product g1t uses appears here once it is used: Workers, Workers for Platforms, D1, KV, R2, Queues, Containers, Durable Objects, Artifacts, Browser Rendering, Workers AI, Vectorize, Cloudflare for SaaS, Email. While a cycle is open its rows carry no cost; `ListCost` is never taken as a cost, since it is before the included amounts (the keeper reads it for a unit's rate, below). | `cost_lines`, source `billable_usage`: `quantity` (consumed), `billed_usd` (Cloudflare's own cost), `billable_quantity` and `cost_usd` (over the cycle), `basis`; the read itself in `cost_reads` |
2424 | GraphQL `artifactsEventsAdaptiveGroups` | Artifacts' own count by `date`, `eventType` and `repositoryName`. Operations are `create`, `fork`, `push`, `pull`, `delete`; errors (`rateLimited`, `serverError`, …) are kept but not counted. | `cost_lines`, source `artifacts_events`; per workspace (from the store key `<workspace>--<repo>`; a pull request's working copy, `pulls--<id>`, is its repository's workspace's, from repos' `pull_owners`) in `own_counts` as `cloudflare_git` |
2525 | GraphQL `aiGatewayRequestsAdaptiveGroups`, filtered to `AI_GATEWAY_ID` | What AI Gateway priced g1t's own provider traffic at, by `date`, `provider` and `model`: `count`, `sum.cost` (dollars), `sum.tokensIn`/`tokensOut`/`cacheReadTokens`/`cacheWriteTokens`; asked twice in one query, filtered `wholesale: 0` and `wholesale: 1`. `wholesale` is never a dimension: grouped by it, Cloudflare answers no rows and no error (until 2026-10-08 that left the gateway's side empty while it had logged $11.11). Read over its own window: the last 31 days until it has answered with a line, then the last few. Field names checked against Cloudflare's schema (introspection of `AccountAiGatewayRequestsAdaptiveGroups{Sum,Dimensions,Filter_InputObject}`). An adaptive (sampled) dataset: an estimate, close at g1t's volumes. Only g1t's hosted models go through this gateway: a workspace's own provider is called at its own address, never here. | `cost_lines`, source `ai_gateway`, product `ai_gateway_requests`: per day and model a line `<provider>_<model>` (requests, at the gateway's cost), and at no cost `…__tokens`, `…__cache_read_tokens`, `…__cache_write_tokens`; Cloudflare-billed (unified billing) requests are prefixed `wholesale__`. Mapped to `models` (migration 0036). A re-read day replaces all its gateway lines. |
2626 | The ledger | Every charge: its cost at the price book's cost, what it was charged at price, what paid for it. | read, never written |
299299 | Kind | When | What to do |
300300 | --- | --- | --- |
301301 | Count | g1t's count and Cloudflare's differ by more than the mapping's `drift_percent` (10%) | Find out what Cloudflare counts: compare its events with `own_counts` `artifacts_*` and `cost_operations`. If it counts more (binding reads, `ls-refs`), either change repos' `operation_mapping` so customers are charged for what Cloudflare counts, or leave it and let the per-unit cost rise (below). |
302−| Cost | Cloudflare charged more than `drift_percent` away from the price book's cost of the same usage, with at least `min_daily_cost` | A price is stale: check the proposals. |
302+| Cost | The price book's cost of a product's usage over the 7 days (the ledger's `cost_micros`) is more than `drift_percent` away from what the same usage comes to at Cloudflare's list prices before the included amounts, with at least `min_daily_cost` either side. The list cost is each billable-usage line mapped to the product, its whole quantity at `cycle::LIST_PRICES` (not rounded to whole millions; `margin::list_costs`). Never what Cloudflare billed: that is net of the cycle's included amounts while the price book costs every unit, so a product whose usage mostly fits in them (sandboxes inside 25 GiB-hours of Containers memory) would read as a stale price when nothing changed; what was billed is still the cost in the margins, and a leak. A product with usage on a meter that has no list price (Artifacts, Cloudflare for SaaS, R2 storage until priced) is not checked, since its usage cannot be priced like for like; add the price to `cycle::LIST_PRICES`. `cost_drift` is replaced every run and an alert whose drift is gone is resolved on the same run, so an alert raised by the old comparison (what was billed against the price book) closes on the next | A price is stale: check the proposals, and `cycle::LIST_PRICES` against Cloudflare's pricing page. |
303303 | Cost, on `models` | What AI Gateway priced g1t's own provider traffic at over the 7 days, against the ledger's model cost for the same days (billed to g1t: comped, free and trial use included, a workspace's own provider not) plus the model cost testing resets kept for those days (`reset_costs`), more than the `ai_gateway_requests` mapping's `drift_percent` (10%) apart, with at least `min_daily_cost`. A ledger with none of the gateway's cost is drift too, and so is a gateway that priced nothing against a ledger with at least `min_daily_cost` of model cost (no percentage): that is not agreement. Its detail says why as far as the run could tell: requests logged with no price (add the models' prices), a token Cloudflare refuses for the gateway (give it AI Gateway Read, or fix `AI_GATEWAY_ID`), a gateway the token sees with no requests (calls went around it), or a read that failed | The gateway higher: model calls g1t paid for and charged no one: runs not settled yet (they catch up within the hour), runs with no session, a run started without a billing ticket, or something else on g1t's gateway. The ledger higher: runs that reached a provider without the gateway. The detail adds why the gateway's own figure may be off: prompt-cache read and write tokens (the gateway prices them at its rates for cache tokens, which can lag the provider's; check against the provider's invoice), requests Cloudflare billed itself (unified billing: on Cloudflare's bill, not a provider's), and models with no price. A testing reset in the window is named in the detail: one that kept its cost says how much of the ledger's side it is; one from before resets kept their cost (the audit log has it, `reset_costs` does not) says the gateway's figure includes usage the ledger no longer has, so that part is not a leak, and the day it leaves the 7 days; while such a reset is in the window the `models` leak is not raised. Days are UTC by when a request ran (gateway) and when a charge was entered (ledger), so a run across midnight shifts a little between days; the 7-day sum absorbs it. |
304304 | Unpriced | Over the 7 days, a model in AI Gateway's analytics with tokens and $0 cost, or runs settled with `runs.gateway_note` (the gateway could not price all of a run) | The gateway has no price for a model g1t runs: add it in the gateway (custom cost) or route away from it. Until then those runs are charged no less than the sandbox reported (Claude Code's own price table), never $0 silently. |
305305 | Leak | Cost of at least `min_daily_cost` and nothing charged for it (never for `platform`), or a meter in `unmapped` | Map the meter (below), or decide it is overhead (`platform`). |
315315 - Proposals come from the keeper (sandbox seconds, app requests and CPU)
316316 and the reconciler (mappings with `scale_to_own`: today git operations).
317317 For git operations: Cloudflare's rate per its own operation (the median
318− over charged days of cost ÷ quantity) × (Cloudflare's operations ÷ g1t's)
318+ over costed days of cost ÷ quantity) × (Cloudflare's operations ÷ g1t's)
319319 × 1,000. If Cloudflare counts three for each one g1t counts, the per-1,000
320320 price triples. At least 1,000 of g1t's operations are needed.
321+- A rate is always a price per unit before the included amounts, like the
322+ price book's: never what was billed over all of the quantity, which is
323+ net of them and reads as a cheaper unit. The reconciler takes a line's
324+ cost at `cycle::LIST_PRICES` where its meter has one (`margin::rate_line`),
325+ else Cloudflare's own cost (the median leaves out the day an included
326+ amount ran out). The keeper takes the bill's `ListCost` ÷ quantity
327+ (`keeper::billed_rate`), and the published rates where there is none.
321328 - Decision (`pricing::decide`): under 2% is noise; more than 4× either way
322329 is suspect and waits for staff; within `auto_apply_percent` (25%) it is
323330 applied on its own when `auto_apply` is on; anything else waits.
+19−0
203203 LIST_PRICES.iter().filter(|p| p.product == product && meter.starts_with(p.meter)).max_by_key(|p| p.meter.len())
204204 }
205205
206+/// What `quantity` of a meter comes to at its list price before any
207+/// included amount, and not in whole blocks: the price book costs every
208+/// unit, so this, not what Cloudflare billed past the included amounts, is
209+/// what it is checked against. None for a meter with no list price.
210+pub(crate) fn list_cost(product: &str, meter: &str, quantity: f64) -> Option<f64> {
211+ list_price(product, meter).map(|p| quantity.max(0.0) * p.usd / p.per)
212+}
213+
206214 /// Puts a cost on every billable-usage line, cycle by cycle (`anchor`):
207215 /// Cloudflare's own where it put one on any of the meter's lines in the
208216 /// cycle, else the list price past the included amount, landing on the
400408 }
401409
402410 #[test]
411+ fn a_list_cost_is_every_unit_at_the_list_price() {
412+ // 126.87k GiB-seconds is $0.32 at list, though only the 36.87k past
413+ // the included 90k were billed ($0.09).
414+ let memory = list_cost("containers", "container_memory_per_gib_second", 126_870.0).unwrap();
415+ assert!((memory - 0.317_175).abs() < 1e-9);
416+ // Per-million meters are not rounded up to a whole million.
417+ assert!((list_cost("workers", "workers_cpu_ms", 11_160_000.0).unwrap() - 0.2232).abs() < 1e-9);
418+ assert!(list_cost("email", "email_service_emails_sent", 7.0).is_none());
419+ }
420+
421+ #[test]
403422 fn subscriptions_accrue_by_the_cycles_days() {
404423 // A whole cycle is the month's price, whatever its length.
405424 assert_eq!(accrued(30_000_000, "2026-09-28", "2026-10-27", 28), 30_000_000);
+39−11
326326 seconds.max(0) as f64 * base_per_second + cpu_seconds.max(0.0) * per_vcpu_second
327327 }
328328
329−/// A unit's marginal rate from the bill: the median, over the days that
330−/// were charged, of cost over quantity. None while nothing was charged.
329+/// A unit's rate from the bill: the median, over the days with a list
330+/// cost, of list cost over quantity. Never what was billed: that is net of
331+/// the included amounts (nothing while a cycle is inside them, part of a
332+/// day's usage on the day it passes one), so over all of the quantity it
333+/// reads as a lower price when nothing changed. None without a list cost;
334+/// the published rates stand in.
331335 pub(crate) fn billed_rate(rows: &[&UsageRow]) -> Option<f64> {
332336 let mut rates: Vec<f64> = rows
333337 .iter()
334− .filter(|r| r.cost > 0.0 && r.quantity > 0.0)
335− .map(|r| r.cost / r.quantity)
338+ .filter(|r| r.list_cost > 0.0 && r.quantity > 0.0)
339+ .map(|r| r.list_cost / r.quantity)
336340 .collect();
337341 if rates.is_empty() {
338342 return None;
350354 unit: String,
351355 quantity: f64,
352356 cost: f64,
357+ /// The quantity at list price, before the included amounts.
358+ list_cost: f64,
353359 }
354360
355361 impl UsageRow {
376382 cost: Some(number(&["ContractedCost", "BilledCost", "contracted_cost"]))
377383 .filter(|cost| *cost > 0.0)
378384 .unwrap_or_else(|| number(&["ListCost", "list_cost"])),
385+ list_cost: number(&["ListCost", "list_cost"]),
379386 })
380387 }
381388 }
879886 .collect()
880887 };
881888
882− // Containers: each resource at what the bill shows it costs, or
883− // the published rate while the included amount still covers it,
889+ // Containers: each resource at the list cost the bill shows for it
890+ // (before the included amounts), or the published rate without one,
884891 // over how much CPU g1t's sandboxes really use per second.
885892 let memory = billed_rate(&named(&["container memory"]));
886893 let disk = billed_rate(&named(&["container disk"]));
894901 durable_object.unwrap_or(LIST_DO_GB_SECOND),
895902 );
896903 // The parts, for runs that report their own CPU.
897− let parts_reason = "Cloudflare's Containers and Durable Objects rates, as billed or published";
904+ let parts_reason = "Cloudflare's Containers and Durable Objects rates, as listed on the bill or published";
898905 self.measure("sandbox_base_second", sandbox_base_micros(rates.0, rates.1, rates.3), parts_reason).await?;
899906 self.measure("sandbox_cpu_second", rates.2 * MICROS_PER_DOLLAR as f64, parts_reason).await?;
900907 if let Some(per_second) = sandbox_second_micros(usage, rates.0, rates.1, rates.2, rates.3) {
911918 if billed.is_empty() {
912919 "rates are Cloudflare's published ones".to_owned()
913920 } else {
914− format!("{} at what Cloudflare billed", billed.join(", "))
921+ format!("{} at the list cost on Cloudflare's bill", billed.join(", "))
915922 },
916923 );
917924 for meter in ["sandbox_second", "build_second"] {
928935 ];
929936 for (meter, words, unit) in app_meters {
930937 if let Some(rate) = billed_rate(&named(words)) {
931− let reason = format!("Cloudflare billed Workers {unit} at ${:.2} per million", rate * 1e6);
938+ let reason = format!("Cloudflare's bill lists Workers {unit} at ${:.2} per million", rate * 1e6);
932939 self.measure(meter, rate * 1e6 * MICROS_PER_DOLLAR as f64, &reason).await?;
933940 }
934941 }
10711078
10721079 #[test]
10731080 fn a_billed_rate_is_the_median_of_the_charged_days() {
1074− let row = |quantity: f64, cost: f64| UsageRow {
1081+ let row = |quantity: f64, list_cost: f64| UsageRow {
10751082 period_start: String::new(),
10761083 period_end: String::new(),
10771084 service: "Containers / Container Memory".into(),
10781085 unit: "Count".into(),
10791086 quantity,
1080− cost,
1087+ cost: list_cost,
1088+ list_cost,
10811089 };
10821090 let rows = [row(100.0, 0.0), row(100.0, 0.0002), row(100.0, 0.00025), row(100.0, 0.00025)];
10831091 assert_eq!(billed_rate(&rows.iter().collect::<Vec<_>>()), Some(0.000_002_5));
10851093 }
10861094
10871095 #[test]
1096+ fn a_rate_is_the_list_price_not_what_was_billed_past_the_included_amount() {
1097+ // 126,870 GiB-seconds, of which the 36,870 past the included 90,000
1098+ // were billed: $0.09 billed, $0.32 at list. The rate is the list's.
1099+ let row = UsageRow {
1100+ period_start: String::new(),
1101+ period_end: String::new(),
1102+ service: "Containers / Container Memory".into(),
1103+ unit: "GiB-seconds".into(),
1104+ quantity: 126_870.0,
1105+ cost: 0.092_175,
1106+ list_cost: 0.317_175,
1107+ };
1108+ assert!((billed_rate(&[&row]).unwrap() - 0.000_002_5).abs() < 1e-15);
1109+ // Billed with no list cost says nothing about the price.
1110+ assert_eq!(billed_rate(&[&UsageRow { list_cost: 0.0, ..row }]), None);
1111+ }
1112+
1113+ #[test]
10881114 fn usage_rows_are_read_by_their_focus_names() {
10891115 let row = UsageRow::from_value(&json!({
10901116 "ServiceFamilyName": "Containers",
10921118 "PricingUnit": "GiB-seconds",
10931119 "PricingQuantity": "1200.5",
10941120 "ContractedCost": 0.003,
1121+ "ListCost": 0.0030,
10951122 "ChargePeriodStart": "2026-10-01",
10961123 }))
10971124 .unwrap();
10981125 assert_eq!(row.service, "Containers / Memory");
10991126 assert_eq!(row.quantity, 1200.5);
11001127 assert_eq!(row.cost, 0.003);
1128+ assert_eq!(row.list_cost, 0.003);
11011129 assert!(UsageRow::from_value(&json!({ "nothing": 1 })).is_none());
11021130 }
11031131 }
+164−36
495495 pub(crate) enum DriftKind {
496496 /// g1t counted a different number of units than Cloudflare did.
497497 Count,
498− /// What Cloudflare charged differs from what the price book says the
499− /// same usage cost.
498+ /// The price book's cost of a bucket's usage differs from what the
499+ /// same usage comes to at Cloudflare's list prices, before the included
500+ /// amounts (a price may be stale); on `models`, the ledger's model cost
501+ /// differs from what AI Gateway priced the same traffic at.
500502 Cost,
501503 /// Cloudflare charged for something nothing charges customers for.
502504 Leak,
526528 }
527529
528530 /// Drift over a window for one bucket: counts more than `threshold`
529−/// percent apart, a bill that far from the price book's cost of the same
530−/// usage, and cost with nothing charged for it. Under `min_cost_micros`
531−/// in all, cost says nothing.
532−pub(crate) fn drifts(bucket: &str, days: &[ProductDay], threshold: f64, counted: bool, min_cost_micros: i64) -> Vec<Drift> {
531+/// percent apart, the price book's cost of the usage that far from what the
532+/// same usage comes to at Cloudflare's list prices (`list_micros`, from
533+/// `list_costs`), and cost with nothing charged for it. Under
534+/// `min_cost_micros` in all, cost says nothing.
535+///
536+/// The price book is set against the list cost, never against what
537+/// Cloudflare billed: the bill is net of the included amounts and the price
538+/// book's cost is of every unit, so a bucket whose usage mostly fits in
539+/// them would read as a stale price when nothing changed. Without a list
540+/// cost (a meter in the bucket with no list price) the price book is not
541+/// checked. A leak is still what Cloudflare billed: money spent.
542+pub(crate) fn drifts(bucket: &str, days: &[ProductDay], threshold: f64, counted: bool, min_cost_micros: i64, list_micros: Option<f64>) -> Vec<Drift> {
533543 let overhead = OVERHEAD.contains(&bucket);
534544 let sum = |f: &dyn Fn(&ProductDay) -> f64| days.iter().map(f).sum::<f64>();
535545 let cf_cost = sum(&|d| d.cf_cost_micros as f64);
548558 out.push(Drift { bucket: bucket.into(), kind: DriftKind::Count, ours: own_quantity, cloudflare: cf_quantity, delta_percent: delta });
549559 }
550560 }
551− let enough = cf_cost.max(own_cost) >= min_cost_micros as f64;
552561 // Models: what AI Gateway priced g1t's own provider traffic at (its
553562 // lines, as "Cloudflare's" side) against the ledger's model cost. Only
554563 // once the gateway has been read; then the ledger having none of it is
555− // drift too (traffic no run was charged for).
556− let models = NOT_CLOUDFLARE.contains(&bucket) && cf_cost > 0.0;
557− if enough && !overhead && cf_cost > 0.0 && (own_cost > 0.0 || models) {
558− let delta = delta_percent(own_cost, cf_cost);
564+ // drift too (traffic no run was charged for). Every other bucket: its
565+ // usage at Cloudflare's list prices, before the included amounts.
566+ let not_cloudflare = NOT_CLOUDFLARE.contains(&bucket);
567+ let theirs = if not_cloudflare { cf_cost } else { list_micros.unwrap_or(0.0) };
568+ let enough = theirs.max(own_cost) >= min_cost_micros as f64;
569+ let models = not_cloudflare && cf_cost > 0.0;
570+ if enough && !overhead && theirs > 0.0 && (own_cost > 0.0 || models) {
571+ let delta = delta_percent(own_cost, theirs);
559572 if delta.is_some_and(|d| d.abs() > threshold) {
560− out.push(Drift { bucket: bucket.into(), kind: DriftKind::Cost, ours: own_cost, cloudflare: cf_cost, delta_percent: delta });
573+ out.push(Drift { bucket: bucket.into(), kind: DriftKind::Cost, ours: own_cost, cloudflare: theirs, delta_percent: delta });
561574 }
562575 }
563576 // The ledger has model cost and the gateway priced none of it: a token
572585 out
573586 }
574587
588+/// What each bucket's billable usage over a window comes to at
589+/// Cloudflare's list prices, before the included amounts (`cycle::list_cost`
590+/// line by line, in micros): what the price book's cost of the same usage
591+/// is checked against. None for a bucket with usage on a meter that has no
592+/// list price: its usage cannot be priced like for like, so it is not
593+/// checked. Other sources (Artifacts events, AI Gateway) are left out.
594+pub(crate) fn list_costs(rules: &[Rule], lines: &[LineRow]) -> BTreeMap<String, Option<f64>> {
595+ let mut out: BTreeMap<String, Option<f64>> = BTreeMap::new();
596+ for line in lines.iter().filter(|l| l.source == SOURCE_BILLABLE) {
597+ let bucket = costs::classify(rules, &line.product, &line.meter).map_or(UNMAPPED, |r| r.bucket.as_str());
598+ let entry = out.entry(bucket.to_owned()).or_insert(Some(0.0));
599+ if line.quantity <= 0.0 {
600+ continue;
601+ }
602+ *entry = match (*entry, crate::cycle::list_cost(&line.product, &line.meter, line.quantity)) {
603+ (Some(sum), Some(cost)) => Some(sum + cost * 1_000_000.0),
604+ _ => None,
605+ };
606+ }
607+ out
608+}
609+
575610 /// What can make AI Gateway's cost differ from what the providers bill,
576611 /// said for staff: cache tokens (priced by the gateway at its own rates for
577612 /// them, which may lag the provider's), requests Cloudflare billed itself,
893928 out
894929 }
895930
896−/// Cloudflare's marginal rate for one of its units: the median over the
897−/// charged days of cost over quantity, in dollars. None while the included
898−/// amounts still cover it. Each item is a day's (quantity, cost).
931+/// Cloudflare's rate for one of its units: the median over the costed
932+/// days of cost over quantity, in dollars. None while nothing is costed.
933+/// Each item is a day's (quantity, cost), from `rate_line`.
899934 pub(crate) fn billed_rate(days: &[(f64, f64)]) -> Option<f64> {
900935 let mut rates: Vec<f64> = days.iter().filter(|(q, c)| *q > 0.0 && *c > 0.0).map(|(q, c)| c / q).collect();
901936 if rates.is_empty() {
905940 Some(rates[rates.len() / 2])
906941 }
907942
943+/// A line's (quantity, cost) for `billed_rate`: at its list price for all
944+/// of the quantity where the meter has one, never what the cycle billed,
945+/// which is net of the included amounts (nothing until the cycle passes
946+/// them, part of a day's usage on the day it does, then whole millions) and
947+/// so says nothing about the price of each unit. A meter with no list
948+/// price has only what Cloudflare billed; the median over the charged days
949+/// leaves out the day its included amount ran out.
950+pub(crate) fn rate_line(product: &str, meter: &str, quantity: f64, cost_usd: f64) -> (f64, f64) {
951+ (quantity, crate::cycle::list_cost(product, meter, quantity).unwrap_or(cost_usd))
952+}
953+
908954 /// What one of g1t's units costs, from Cloudflare's rate per its own unit
909955 /// and how many of Cloudflare's units each of g1t's took: if Cloudflare
910956 /// counts three operations for every git operation g1t counts, a git
15681614 }
15691615 let caveats = self.gateway_caveats(&since, until).await?;
15701616 let resets = self.resets_since(&since, until).await?;
1617+ // The same days' usage at list prices, before the included amounts:
1618+ // what the price book is checked against (never the bill, which is
1619+ // net of them).
1620+ let lines = self
1621+ .db
1622+ .prepare("SELECT day, source, product, meter, quantity, cost_usd FROM cost_lines WHERE source = ?1 AND day >= ?2 AND day <= ?3")
1623+ .bind(&[SOURCE_BILLABLE.into(), since.as_str().into(), until.into()])?
1624+ .all()
1625+ .await?
1626+ .results::<LineRow>()?;
1627+ let list = list_costs(&rules, &lines);
15711628 let mut found = Vec::new();
15721629 if let Some(drift) = unpriced_drift(&caveats) {
15731630 found.push(drift);
15771634 let threshold = bucket_rules.iter().map(|r| r.drift_percent).fold(f64::INFINITY, f64::min);
15781635 let threshold = if threshold.is_finite() { threshold } else { 10.0 };
15791636 let counted = bucket_rules.iter().any(|r| r.own_meter.is_some());
1580− for drift in drifts(bucket, days, threshold, counted, settings.min_daily_cost_micros) {
1637+ let list_micros = list.get(bucket).copied().flatten();
1638+ for drift in drifts(bucket, days, threshold, counted, settings.min_daily_cost_micros, list_micros) {
15811639 if wiped_not_leaked(&drift, &resets) {
15821640 continue;
15831641 }
15911649 drift.delta_percent.unwrap_or(0.0)
15921650 ),
15931651 DriftKind::Cost => format!(
1594− "{title}: Cloudflare charged {} over the last {DRIFT_DAYS} days; the price book's cost of the same usage is {} ({:+.1}%). A price may be stale: see the proposals.",
1652+ "{title}: the last {DRIFT_DAYS} days' usage comes to {} at Cloudflare's list prices, before the included amounts; the price book's cost of the same usage is {} ({:+.1}%). A price may be stale: see the proposals.",
15951653 dollars(drift.cloudflare as i64),
15961654 dollars(drift.ours as i64),
15971655 drift.delta_percent.unwrap_or(0.0)
16761734 let mine: Vec<(f64, f64)> = lines
16771735 .iter()
16781736 .filter(|l| costs::classify(&rules, &l.product, &l.meter).is_some_and(|r| r.product == s.product && r.meter == s.meter))
1679− .map(|l| (l.quantity, l.cost_usd))
1737+ .map(|l| rate_line(&l.product, &l.meter, l.quantity, l.cost_usd))
16801738 .collect();
16811739 let Some(rate) = billed_rate(&mine) else { continue };
16821740 let cf_units: f64 = mine.iter().map(|(q, _)| q).sum();
25842642 fn counts_more_than_the_threshold_apart_are_drift() {
25852643 // Cloudflare counted 30,000 operations where g1t counted 10,000:
25862644 // binding reads, perhaps. -66.7%.
2587− let drift = drifts("git", &[day("git", 3_000_000, 1_500_000, 1_800_000, 30_000.0, 10_000.0)], 10.0, true, 100_000);
2645+ let drift = drifts("git", &[day("git", 3_000_000, 1_500_000, 1_800_000, 30_000.0, 10_000.0)], 10.0, true, 100_000, Some(3_000_000.0));
25882646 assert_eq!(drift.iter().map(|d| d.kind).collect::<Vec<_>>(), vec![DriftKind::Count, DriftKind::Cost]);
25892647 assert!((drift[0].delta_percent.unwrap() + 66.666).abs() < 0.01);
25902648 // 9% apart: within 10%.
2591− assert!(drifts("git", &[day("git", 1_000_000, 1_000_000, 1_200_000, 10_000.0, 10_900.0)], 10.0, true, 100_000).is_empty());
2649+ assert!(drifts("git", &[day("git", 1_000_000, 1_000_000, 1_200_000, 10_000.0, 10_900.0)], 10.0, true, 100_000, Some(1_000_000.0)).is_empty());
25922650 // Uncounted products have no count drift.
2593− assert!(drifts("sandboxes", &[day("sandboxes", 1_000_000, 1_050_000, 1_200_000, 5.0, 0.0)], 10.0, false, 100_000).is_empty());
2651+ assert!(drifts("sandboxes", &[day("sandboxes", 1_000_000, 1_050_000, 1_200_000, 5.0, 0.0)], 10.0, false, 100_000, Some(1_000_000.0)).is_empty());
25942652 }
25952653
25962654 #[test]
25972655 fn cost_with_no_revenue_is_a_leak_but_not_for_running_g1t() {
2598− let leak = drifts("actions_cache", &[day("actions_cache", 400_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000);
2656+ let leak = drifts("actions_cache", &[day("actions_cache", 400_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None);
25992657 assert_eq!(leak.len(), 1);
26002658 assert_eq!(leak[0].kind, DriftKind::Leak);
2601− assert!(drifts("platform", &[day("platform", 5_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
2659+ assert!(drifts("platform", &[day("platform", 5_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None).is_empty());
26022660 // Pennies say nothing.
2603− assert!(drifts("actions_cache", &[day("actions_cache", 50_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
2604− assert!(drifts(UNMAPPED, &[day(UNMAPPED, 250_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000)[0].kind == DriftKind::Leak);
2661+ assert!(drifts("actions_cache", &[day("actions_cache", 50_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None).is_empty());
2662+ assert!(drifts(UNMAPPED, &[day(UNMAPPED, 250_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None)[0].kind == DriftKind::Leak);
2663+ }
2664+
2665+ /// A week of sandboxes mostly inside the cycle's included amounts:
2666+ /// Cloudflare billed $0.0922 (the memory past 25 GiB-hours), and the
2667+ /// same usage is $1.70 at list prices.
2668+ fn sandbox_week() -> (Vec<Rule>, Vec<LineRow>) {
2669+ let mut rules = rules();
2670+ rules.push(rule("durable_objects", "durable_objects_compute_duration", "sandboxes", None));
2671+ let lines = vec![
2672+ line("2026-10-07", SOURCE_BILLABLE, "containers", "container_memory_per_gib_second", 126_870.0, 0.092_175),
2673+ line("2026-10-07", SOURCE_BILLABLE, "containers", "container_vcpu", 12_000.0, 0.0),
2674+ line("2026-10-07", SOURCE_BILLABLE, "containers", "container_disk_per_gb_second", 253_740.0, 0.0),
2675+ line("2026-10-07", SOURCE_BILLABLE, "durable_objects", "durable_objects_compute_duration", 90_000.0, 0.0),
2676+ // Not billable usage: no part of the list cost.
2677+ line("2026-10-07", SOURCE_ARTIFACTS, "artifacts", "events_push", 40.0, 0.0),
2678+ ];
2679+ (rules, lines)
2680+ }
2681+
2682+ #[test]
2683+ fn usage_inside_the_included_amounts_is_not_a_stale_price() {
2684+ let (rules, lines) = sandbox_week();
2685+ let list = list_costs(&rules, &lines);
2686+ let sandboxes = list["sandboxes"].unwrap();
2687+ assert!((sandboxes - 1_699_936.8).abs() < 1.0, "{sandboxes}");
2688+ // The price book's cost of the same usage is $1.69: within 1% of
2689+ // the list, though Cloudflare billed $0.0922 after the included
2690+ // amounts. Before, that read as +1733.6%.
2691+ let week = day("sandboxes", 92_175, 1_690_000, 2_028_000, 0.0, 0.0);
2692+ assert!(drifts("sandboxes", std::slice::from_ref(&week), 10.0, false, 100_000, Some(sandboxes)).is_empty());
2693+ let net = drifts("sandboxes", &[week], 10.0, false, 100_000, Some(92_175.0));
2694+ assert_eq!(net[0].kind, DriftKind::Cost, "billed against the price book was the false alarm");
2695+ }
2696+
2697+ #[test]
2698+ fn a_stale_price_is_still_drift() {
2699+ let (rules, lines) = sandbox_week();
2700+ let sandboxes = list_costs(&rules, &lines)["sandboxes"].unwrap();
2701+ // The price book still costs the same usage at $1.20: a list price
2702+ // rose and the book did not follow.
2703+ let found = drifts("sandboxes", &[day("sandboxes", 92_175, 1_200_000, 1_440_000, 0.0, 0.0)], 10.0, false, 100_000, Some(sandboxes));
2704+ assert_eq!(found.len(), 1);
2705+ assert_eq!((found[0].kind, found[0].ours, found[0].cloudflare.round()), (DriftKind::Cost, 1_200_000.0, 1_699_937.0));
2706+ assert!((found[0].delta_percent.unwrap() + 29.4).abs() < 0.1);
2707+ // Nothing billed at all (the whole week inside the included
2708+ // amounts) is checked all the same.
2709+ assert_eq!(drifts("sandboxes", &[day("sandboxes", 0, 1_200_000, 1_440_000, 0.0, 0.0)], 10.0, false, 100_000, Some(sandboxes)).len(), 1);
2710+ }
2711+
2712+ #[test]
2713+ fn a_bucket_with_a_meter_with_no_list_price_is_not_checked() {
2714+ let (rules, mut lines) = sandbox_week();
2715+ lines.push(line("2026-10-07", SOURCE_BILLABLE, "artifacts", "storage", 3.0, 0.4));
2716+ lines.push(line("2026-10-07", SOURCE_BILLABLE, "artifacts", "operations", 0.0, 0.0));
2717+ let list = list_costs(&rules, &lines);
2718+ assert_eq!(list["git"], None);
2719+ assert!(list["sandboxes"].is_some());
2720+ // No list cost: no cost drift, but a leak is still what was billed.
2721+ assert!(drifts("git", &[day("git", 3_000_000, 1_000_000, 1_200_000, 0.0, 0.0)], 10.0, false, 100_000, None).is_empty());
2722+ assert_eq!(drifts("git", &[day("git", 3_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None)[0].kind, DriftKind::Leak);
2723+ }
2724+
2725+ #[test]
2726+ fn a_unit_rate_is_the_list_price_where_there_is_one() {
2727+ // Billed $0.20 for 11.16M CPU ms (9.16M past the included 30M, in
2728+ // whole millions): $0.02 a million at list, whatever was billed.
2729+ let (q, c) = rate_line("workers", "workers_cpu_ms", 11_160_000.0, 0.20);
2730+ assert!((billed_rate(&[(q, c)]).unwrap() * 1e6 - 0.02).abs() < 1e-12);
2731+ // No list price: what Cloudflare billed.
2732+ assert_eq!(rate_line("artifacts", "operations", 1_000.0, 0.5), (1_000.0, 0.5));
26052733 }
26062734
26072735 #[test]
26362764 let on = |day: &str, cf: f64, own: f64| ProductDay { day: day.into(), bucket: "git".into(), cf_quantity: cf, own_quantity: own, ..ProductDay::default() };
26372765 // Five days of Cloudflare's count before g1t's meter, then two that match.
26382766 let days = vec![on("2026-10-01", 500.0, 0.0), on("2026-10-05", 300.0, 0.0), on("2026-10-06", 210.0, 231.0), on("2026-10-07", 450.0, 458.0)];
2639− assert!(drifts("git", &days, 10.0, true, 0).iter().all(|d| d.kind != DriftKind::Count));
2767+ assert!(drifts("git", &days, 10.0, true, 0, None).iter().all(|d| d.kind != DriftKind::Count));
26402768 // A real gap on the days both counted still shows.
26412769 let days = vec![on("2026-10-01", 500.0, 0.0), on("2026-10-06", 400.0, 231.0), on("2026-10-07", 600.0, 300.0)];
2642− let found = drifts("git", &days, 10.0, true, 0);
2770+ let found = drifts("git", &days, 10.0, true, 0, None);
26432771 let count = found.iter().find(|d| d.kind == DriftKind::Count).unwrap();
26442772 assert_eq!((count.ours, count.cloudflare), (531.0, 1000.0));
26452773 // A meter that never counted is compared over every day.
26462774 let days = vec![on("2026-10-06", 400.0, 0.0)];
2647− assert!(drifts("git", &days, 10.0, true, 0).iter().any(|d| d.kind == DriftKind::Count));
2775+ assert!(drifts("git", &days, 10.0, true, 0, None).iter().any(|d| d.kind == DriftKind::Count));
26482776 }
26492777
26502778 #[test]
27522880 #[test]
27532881 fn the_gateways_total_against_the_ledgers_model_cost_is_drift() {
27542882 // The gateway priced $5 of g1t's own traffic; the ledger has $3.
2755− let short = drifts("models", &[day("models", 5_000_000, 3_000_000, 3_600_000, 0.0, 0.0)], 10.0, false, 100_000);
2883+ let short = drifts("models", &[day("models", 5_000_000, 3_000_000, 3_600_000, 0.0, 0.0)], 10.0, false, 100_000, None);
27562884 assert_eq!(short.iter().map(|d| d.kind).collect::<Vec<_>>(), vec![DriftKind::Cost]);
27572885 assert!((short[0].delta_percent.unwrap() + 40.0).abs() < 1e-9);
27582886 // Gateway traffic with nothing on the ledger at all: cost drift and a leak.
2759− let none = drifts("models", &[day("models", 2_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000);
2887+ let none = drifts("models", &[day("models", 2_000_000, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None);
27602888 assert_eq!(none.iter().map(|d| d.kind).collect::<Vec<_>>(), vec![DriftKind::Cost, DriftKind::Leak]);
27612889 // Within the threshold: nothing.
2762− assert!(drifts("models", &[day("models", 1_050_000, 1_000_000, 1_200_000, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
2890+ assert!(drifts("models", &[day("models", 1_050_000, 1_000_000, 1_200_000, 0.0, 0.0)], 10.0, false, 100_000, None).is_empty());
27632891 // The gateway priced nothing against a ledger that has model cost:
27642892 // not agreement (a token that cannot see AI Gateway reads as no
27652893 // rows), so it is said. Under the minimum, or no model cost: nothing.
2766− let silent = drifts("models", &[day("models", 0, 1_000_000, 1_200_000, 0.0, 0.0)], 10.0, false, 100_000);
2894+ let silent = drifts("models", &[day("models", 0, 1_000_000, 1_200_000, 0.0, 0.0)], 10.0, false, 100_000, None);
27672895 assert_eq!(silent, vec![Drift { bucket: "models".into(), kind: DriftKind::Cost, ours: 1_000_000.0, cloudflare: 0.0, delta_percent: None }]);
27682896 // Why it is empty, as far as the run could tell.
27692897 let why = |caveats: &costs::GatewayCaveats, read: costs::GatewayRead| models_detail(&silent[0], caveats, &[], &read);
27802908 let unpriced = costs::GatewayCaveats { requests: 42.0, unpriced: vec!["anthropic_claude_new_1".into()], ..Default::default() };
27812909 let said = why(&unpriced, costs::GatewayRead::Rows);
27822910 assert!(said.contains("logged 42 requests") && said.contains("no price for the models used (anthropic_claude_new_1)"), "{said}");
2783− assert!(drifts("models", &[day("models", 0, 50_000, 60_000, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
2784− assert!(drifts("models", &[day("models", 0, 0, 0, 0.0, 0.0)], 10.0, false, 100_000).is_empty());
2911+ assert!(drifts("models", &[day("models", 0, 50_000, 60_000, 0.0, 0.0)], 10.0, false, 100_000, None).is_empty());
2912+ assert!(drifts("models", &[day("models", 0, 0, 0, 0.0, 0.0)], 10.0, false, 100_000, None).is_empty());
27852913 // The detail says which way and why it may be off.
27862914 let caveats = costs::GatewayCaveats { cache_read_tokens: 3_000_000.0, unpriced: vec!["anthropic_claude_new_1".into()], ..Default::default() };
27872915 let detail = models_detail(&short[0], &caveats, &[], &costs::GatewayRead::default());
28512979 let models = |days: &[ProductDay]| days.iter().find(|d| d.bucket == "models").cloned().unwrap();
28522980 // Without it: AI Gateway's $11.11 against the ledger's $2.49.
28532981 let (days, _) = gateway_and_ledger(false);
2854− let drift = drifts("models", &[models(&days)], 10.0, false, 100_000);
2982+ let drift = drifts("models", &[models(&days)], 10.0, false, 100_000, None);
28552983 assert_eq!(drift.iter().map(|d| d.kind).collect::<Vec<_>>(), vec![DriftKind::Cost]);
28562984 assert_eq!((drift[0].ours, drift[0].cloudflare), (2_490_000.0, 11_110_000.0));
28572985 // With it: the ledger's model cost and the reset's add up to the gateway's.
28582986 let (days, _) = gateway_and_ledger(true);
28592987 let m = models(&days);
28602988 assert_eq!(m.own_cost_micros, 11_110_000);
2861− assert!(drifts("models", &[m], 10.0, false, 100_000).is_empty());
2989+ assert!(drifts("models", &[m], 10.0, false, 100_000, None).is_empty());
28622990 // The reset's own row makes no bucket of its own.
28632991 assert!(!days.iter().any(|d| d.bucket.is_empty()));
28642992 }