flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/crates/contracts/src/billing.rs

500 lines17,546 bytesCodeBlame
1//! The billing service: what agents cost, charged to the workspace they
2//! worked for.
3//!
4//! A workspace buys credit and each agent run deducts what it cost, plus
5//! g1t's margin. With no credit, no agent starts. Money is held in
6//! millionths of a US dollar, so that a run costing a fraction of a cent is
7//! recorded exactly.
8//!
9//! Each `*Args` struct is the argument of the method of the same name,
10//! served at `POST /rpc/<method>`.
11
12use serde::{Deserialize, Serialize};
13
14use crate::repos::RepoPath;
15use crate::{User, Viewer};
16
17/// Millionths of a US dollar in one dollar.
18pub const MICROS_PER_DOLLAR: i64 = 1_000_000;
19
20/// Whether workspaces are charged for agents at all, and with real money.
21/// `status` takes nothing and returns this.
22#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
23pub struct Status {
24 /// False when no payment provider is configured: nothing is charged,
25 /// and who may run agents is decided some other way.
26 pub enabled: bool,
27 /// False while the payment provider is in its test mode, where cards
28 /// are not real.
29 pub live: bool,
30 /// True while g1t is being built out: runs are recorded, with what
31 /// they cost, but nothing is charged and no credit is needed. Not a
32 /// promise that it stays free.
33 #[serde(default)]
34 pub free: bool,
35}
36
37/// `trial`: the free allowance on g1t's hosted models for a workspace that
38/// is not otherwise open to them, so people can try g1t's agents without a
39/// key of their own. Each workspace gets a few dollars of model cost, out
40/// of one pool, until an end date. Returns `Trial`.
41#[derive(Debug, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct TrialArgs {
44 pub workspace: String,
45 /// Workspaces open to hosted models anyway, whose use is not counted
46 /// against the pool.
47 #[serde(default)]
48 pub exempt: Vec<String>,
49}
50
51#[derive(Clone, Debug, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct Trial {
54 /// Whether its agents may use g1t's hosted models on the allowance now.
55 pub open: bool,
56 /// What its runs on g1t's models have cost, in millionths of a dollar.
57 pub used_micros: i64,
58 pub limit_micros: i64,
59 /// RFC 3339; when the allowance ends for everyone.
60 pub ends_at: Option<String>,
61 /// Why it is closed: `off` (no allowance), `ended`, `used` (this
62 /// workspace's is spent) or `pool` (everyone's is).
63 pub reason: Option<String>,
64}
65
66/// A workspace's standing.
67#[derive(Clone, Debug, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct Account {
70 pub workspace: String,
71 /// Credit left, in millionths of a dollar. Can dip below zero by the
72 /// cost of the runs that were under way when it ran out.
73 pub balance_micros: i64,
74 pub status: Status,
75 /// What is added to a run's cost, in percent.
76 pub margin_percent: u32,
77 /// What a run on the workspace's own model provider is charged: g1t's
78 /// sandbox and orchestration, with the model paid for elsewhere.
79 pub orchestration_fee_micros: i64,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum EntryKind {
85 /// Credit bought with a card.
86 TopUp,
87 /// An agent's run, or a paid feature's usage past its allowance.
88 Usage,
89}
90
91/// One line of a workspace's statement.
92#[derive(Clone, Debug, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct LedgerEntry {
95 pub id: String,
96 pub kind: EntryKind,
97 /// Positive for credit added, negative for usage.
98 pub amount_micros: i64,
99 pub description: String,
100 /// For usage: the repository and pull request the agent worked on.
101 pub repo: Option<String>,
102 pub number: Option<u32>,
103 /// For usage: `implement`, `review` or `update`.
104 pub task: Option<String>,
105 /// For usage: the model, by its public name.
106 pub model: Option<String>,
107 /// For usage: `g1t` when g1t paid the model provider, `workspace` when
108 /// the workspace's own account did and only orchestration is charged.
109 #[serde(default = "g1t")]
110 pub billed_to: String,
111 /// For a top-up: the username of whoever paid.
112 pub created_by: Option<String>,
113 /// RFC 3339.
114 pub created_at: String,
115}
116
117fn g1t() -> String {
118 "g1t".to_owned()
119}
120
121/// `account` (`Outcome<Account>`) and `ledger` (`Outcome<Vec<LedgerEntry>>`,
122/// newest first). Members of the workspace only.
123#[derive(Debug, Serialize, Deserialize)]
124pub struct AccountArgs {
125 pub workspace: String,
126 pub viewer: Viewer,
127}
128
129/// `checkout`: starts a card payment for credit. Owners of the workspace
130/// only. Returns `Outcome<Checkout>`.
131#[derive(Debug, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct CheckoutArgs {
134 pub actor: User,
135 pub workspace: String,
136 /// How much credit to buy, in cents.
137 pub amount_cents: u32,
138 /// Where the payment page sends the person afterwards. The payment's
139 /// id is appended as `session`.
140 pub return_url: String,
141}
142
143#[derive(Debug, Serialize, Deserialize)]
144pub struct Checkout {
145 /// The payment page to send the person to.
146 pub url: String,
147}
148
149/// `confirm`: credits a payment once the provider says it was made. Safe
150/// to call any number of times. Returns `Outcome<Account>`.
151#[derive(Debug, Serialize, Deserialize)]
152pub struct ConfirmArgs {
153 pub workspace: String,
154 pub viewer: Viewer,
155 /// The payment's id, as returned to `return_url`.
156 pub session: String,
157}
158
159/// `can_start`: whether a workspace may start an agent now, asked before
160/// anything is opened for it. Returns `Outcome<bool>`: a failure, with the
161/// reason to show, when it has no credit.
162#[derive(Debug, Serialize, Deserialize)]
163pub struct CanStartArgs {
164 pub workspace: String,
165}
166
167/// `start_run`: asks whether a workspace may start an agent, and opens the
168/// run it will be charged for. Called by the runner service. Returns
169/// `Outcome<Option<RunTicket>>`: no ticket when billing is off, a failure
170/// when the workspace has no credit.
171#[derive(Debug, Serialize, Deserialize)]
172pub struct StartRunArgs {
173 pub workspace: String,
174 pub repo: RepoPath,
175 pub number: u32,
176 /// `implement`, `review` or `update`.
177 pub task: String,
178 /// The model, by its public name.
179 pub model: String,
180 /// `workspace` when the run uses the workspace's own model provider.
181 /// The runner, which is TypeScript, sends it as `billedTo`.
182 #[serde(default = "g1t", alias = "billedTo")]
183 pub billed_to: String,
184}
185
186#[derive(Clone, Debug, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct RunTicket {
189 pub run_id: String,
190 /// Lets the sandbox, and nothing else, report what this run cost.
191 pub token: String,
192}
193
194/// `finish_run`: what a run cost, as its sandbox reports it. Charged once.
195/// Returns `Outcome<bool>`.
196#[derive(Debug, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct FinishRunArgs {
199 pub run_id: String,
200 pub token: String,
201 /// What the model provider charged, in US dollars.
202 pub cost_usd: f64,
203 #[serde(default)]
204 pub turns: u32,
205}
206
207
208/// `usage`: what a workspace's agents cost over a period, broken down.
209/// Members only. Returns `Outcome<Usage>`.
210#[derive(Debug, Serialize, Deserialize)]
211pub struct UsageArgs {
212 pub workspace: String,
213 pub viewer: Viewer,
214 /// RFC 3339: the start of the period. The period runs to now.
215 pub since: String,
216}
217
218/// One slice of usage: what it was for, what it cost, how many runs.
219#[derive(Clone, Debug, Serialize, Deserialize)]
220#[serde(rename_all = "camelCase")]
221pub struct UsageSlice {
222 pub key: String,
223 pub micros: i64,
224 pub runs: u32,
225}
226
227/// What a workspace's agents cost over a period.
228#[derive(Clone, Debug, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct Usage {
231 pub since: String,
232 /// Charged, including g1t's margin.
233 pub spent_micros: i64,
234 /// What g1t's model provider charged, before the margin.
235 pub cost_micros: i64,
236 /// What runs on the workspace's own provider cost there, as the harness
237 /// estimated it. Not charged by g1t.
238 pub provider_micros: i64,
239 /// What the runs used, at cost: g1t's models and the workspace's own
240 /// provider together, whatever was charged for them.
241 pub used_micros: i64,
242 /// g1t charges nothing for now. The slices then measure usage at cost,
243 /// since every charge is zero.
244 pub free: bool,
245 pub runs: u32,
246 /// Spend per day (`YYYY-MM-DD`) and task, as `day/task` keys.
247 pub by_day: Vec<UsageSlice>,
248 /// Per task: implement, review, revise, update, plan.
249 pub by_task: Vec<UsageSlice>,
250 /// Per repository, `namespace/name`.
251 pub by_repo: Vec<UsageSlice>,
252 /// The pull requests that cost most, as `namespace/name#number`.
253 pub by_pull: Vec<UsageSlice>,
254 /// Per model, by its public name.
255 pub by_model: Vec<UsageSlice>,
256 /// Credit bought in the period.
257 pub added_micros: i64,
258}
259
260/// A paid feature a workspace turns on with a monthly plan, the way
261/// Cloudflare's Workers for Platforms or Vercel's Pro are bought. Never
262/// free: `FREE_WHILE_BUILDING` and the free model allowance do not cover
263/// it.
264#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "snake_case")]
266pub enum Feature {
267 /// Previews per pull request and production on g1t.page.
268 Deployments,
269}
270
271impl Feature {
272 pub const ALL: [Feature; 1] = [Feature::Deployments];
273
274 pub fn as_str(self) -> &'static str {
275 match self {
276 Feature::Deployments => "deployments",
277 }
278 }
279
280 pub fn parse(name: &str) -> Option<Feature> {
281 Feature::ALL.into_iter().find(|feature| feature.as_str() == name)
282 }
283
284 pub fn title(self) -> &'static str {
285 match self {
286 Feature::Deployments => "Deployments",
287 }
288 }
289}
290
291/// What the Deployments plan includes each month; usage past it is charged
292/// at cost plus the margin. The billing service describes the plan with
293/// these and the deployments service meters against them.
294pub mod deployments_allowance {
295 /// Apps deployed at once: production and previews together.
296 pub const APPS: u32 = 10;
297 pub const REQUESTS: u64 = 1_000_000;
298 pub const CPU_MS: u64 = 3_000_000;
299 /// What Cloudflare charges g1t past that, in millionths of a dollar.
300 pub const MICROS_PER_APP_MONTH: i64 = 20_000;
301 pub const MICROS_PER_MILLION_REQUESTS: i64 = 300_000;
302 pub const MICROS_PER_MILLION_CPU_MS: i64 = 20_000;
303 /// What one second of a build's sandbox costs g1t (Cloudflare
304 /// Containers, standard-1: half a vCPU, 4 GiB, 8 GB disk), rounded up.
305 /// Builds are not in the allowance: each is charged at this plus the
306 /// margin.
307 pub const MICROS_PER_BUILD_SECOND: i64 = 21;
308}
309
310/// Sandbox time: every sandbox g1t starts for a workspace (agents,
311/// reviews, checks, the merge queue, workflow jobs) is metered by the
312/// second. Deploy builds are charged by the Deployments plan instead.
313pub mod sandbox_allowance {
314 /// Free each calendar month (UTC): 500 minutes.
315 pub const FREE_SECONDS: i64 = 30_000;
316 /// What one second costs g1t (Cloudflare Containers, standard-1),
317 /// rounded up. Recorded with every entry.
318 pub const COST_MICROS_PER_SECOND: i64 = super::deployments_allowance::MICROS_PER_BUILD_SECOND;
319 /// What one second past the free minutes is charged: $0.003 a minute.
320 pub const MICROS_PER_SECOND: i64 = 50;
321}
322
323/// `record_sandbox`: how long one sandbox ran for a workspace, reported by
324/// the runner when it stops. Recorded once per `reference`, with what it
325/// cost g1t; seconds past the month's free minutes are charged at
326/// `sandbox_allowance::MICROS_PER_SECOND`, unless `FREE_WHILE_BUILDING`.
327/// Returns `Outcome<bool>`: false if that reference was recorded before.
328#[derive(Debug, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330pub struct RecordSandboxArgs {
331 pub workspace: String,
332 pub seconds: u32,
333 /// What ran, e.g. `Checks on acme/api#12`.
334 pub description: String,
335 /// `namespace/name`.
336 pub repo: Option<String>,
337 /// Unique to the run.
338 pub reference: String,
339}
340
341/// What a feature's plan costs and includes.
342#[derive(Clone, Debug, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct Plan {
345 pub feature: Feature,
346 pub title: String,
347 /// Charged every month while the plan is on, in cents.
348 pub monthly_cents: u32,
349 /// What the monthly price includes, one line each, for people to read.
350 pub includes: Vec<String>,
351 /// How usage past the allowance is charged, for people to read.
352 pub overage: String,
353}
354
355#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
356#[serde(rename_all = "snake_case")]
357pub enum SubscriptionStatus {
358 /// Paid up; the feature works.
359 Active,
360 /// Paid up to the end of the period, and ends then.
361 Canceling,
362 /// The last payment failed; the feature is off until it is paid.
363 PastDue,
364 /// Ended.
365 Canceled,
366}
367
368impl SubscriptionStatus {
369 /// Whether the feature works in this state.
370 pub fn on(self) -> bool {
371 matches!(self, SubscriptionStatus::Active | SubscriptionStatus::Canceling)
372 }
373}
374
375/// A workspace's plan for one feature.
376#[derive(Clone, Debug, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct Subscription {
379 pub feature: Feature,
380 pub status: SubscriptionStatus,
381 /// RFC 3339: when the period paid for ends, and the plan renews or
382 /// ends.
383 pub period_end: Option<String>,
384 /// Username of whoever turned it on.
385 pub started_by: String,
386 /// RFC 3339.
387 pub started_at: String,
388}
389
390/// A feature as a workspace sees it: what it costs, and its plan if it has
391/// one.
392#[derive(Clone, Debug, Serialize, Deserialize)]
393#[serde(rename_all = "camelCase")]
394pub struct FeatureState {
395 pub plan: Plan,
396 pub subscription: Option<Subscription>,
397 /// Whether the feature works for the workspace now.
398 pub on: bool,
399}
400
401/// `features`: every paid feature and the workspace's plan for each.
402/// Members only. Returns `Outcome<Vec<FeatureState>>`.
403#[derive(Debug, Serialize, Deserialize)]
404pub struct FeaturesArgs {
405 pub workspace: String,
406 pub viewer: Viewer,
407}
408
409/// `subscribe`: starts the card page for a feature's monthly plan. Owners
410/// only. Returns `Outcome<Checkout>`; the page's id comes back to
411/// `return_url` as `session`, for `confirm_subscription`.
412#[derive(Debug, Serialize, Deserialize)]
413#[serde(rename_all = "camelCase")]
414pub struct SubscribeArgs {
415 pub actor: User,
416 pub workspace: String,
417 pub feature: Feature,
418 pub return_url: String,
419}
420
421/// `confirm_subscription`: turns the feature on once the processor says
422/// the plan was paid for. Safe to call any number of times. Returns
423/// `Outcome<FeatureState>`.
424#[derive(Debug, Serialize, Deserialize)]
425pub struct ConfirmSubscriptionArgs {
426 pub workspace: String,
427 pub viewer: Viewer,
428 pub session: String,
429}
430
431/// `cancel_subscription` (`resume` false) ends a plan at the end of the
432/// period paid for; with `resume` true, takes that back. Owners only.
433/// Returns `Outcome<FeatureState>`.
434#[derive(Debug, Serialize, Deserialize)]
435pub struct CancelSubscriptionArgs {
436 pub actor: User,
437 pub workspace: String,
438 pub feature: Feature,
439 #[serde(default)]
440 pub resume: bool,
441}
442
443/// `has_feature`: whether a feature works for a workspace now, asked by the
444/// service that provides it before doing paid work. Returns
445/// `Outcome<bool>`: a failure, with the reason to show, when it does not.
446/// True everywhere when no card processor is configured.
447#[derive(Debug, Serialize, Deserialize)]
448pub struct HasFeatureArgs {
449 pub workspace: String,
450 pub feature: Feature,
451}
452
453/// `charge_feature`: usage of a feature past its plan's allowance, charged
454/// from the workspace's credit at cost plus the margin, whatever
455/// `FREE_WHILE_BUILDING` says. Called by the service that provides it.
456/// Charged once per `reference`. Returns `Outcome<bool>`: false if that
457/// reference was charged before.
458#[derive(Debug, Serialize, Deserialize)]
459#[serde(rename_all = "camelCase")]
460pub struct ChargeFeatureArgs {
461 pub workspace: String,
462 pub feature: Feature,
463 /// What it cost g1t, in millionths of a dollar, before the margin.
464 pub cost_micros: i64,
465 pub description: String,
466 /// `namespace/name`, when the usage was one repository's.
467 pub repo: Option<String>,
468 /// Unique to this charge, e.g. `deployments/acme/2026-10`.
469 pub reference: String,
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn features_are_named_as_the_site_sends_them() {
478 assert_eq!(
479 serde_json::to_value(Feature::Deployments).unwrap(),
480 serde_json::json!("deployments")
481 );
482 assert_eq!(Feature::parse("deployments"), Some(Feature::Deployments));
483 assert!(SubscriptionStatus::Canceling.on());
484 assert!(!SubscriptionStatus::PastDue.on());
485 }
486
487 #[test]
488 fn who_pays_is_read_as_the_runner_sends_it() {
489 let run: StartRunArgs = serde_json::from_value(serde_json::json!({
490 "workspace": "acme",
491 "repo": { "namespace": "acme", "name": "web" },
492 "number": 7,
493 "task": "implement",
494 "model": "Claude Sonnet 5.5",
495 "billedTo": "workspace",
496 }))
497 .unwrap();
498 assert_eq!(run.billed_to, "workspace");
499 }
500}