pr_01m47d15m3e54sn21z27rpy5n9/packages/contracts/src/billing.ts

508 lines19,497 bytesCodeBlame
1import type { User, Viewer } from "./identity";
2import type { RepoPath } from "./repos";
3import type { Result } from "./result";
4
5/** Millionths of a US dollar in one dollar: the unit money is held in. */
6export const MICROS_PER_DOLLAR = 1_000_000;
7
8/** Whether workspaces are charged for agents at all, and with real money. */
9export type BillingStatus = {
10 /**
11 * False when no card processor is configured: nothing is charged, and who
12 * may run agents is decided some other way.
13 */
14 enabled: boolean;
15 /** False while the card processor is in its test mode, where cards are not real. */
16 live: boolean;
17 /**
18 * True while g1t is being built out: runs are recorded with what they
19 * cost, but nothing is charged and no credit is needed. Not forever.
20 */
21 free?: boolean;
22};
23
24/** A workspace's standing. */
25export type BillingAccount = {
26 workspace: string;
27 /**
28 * Credit left, in millionths of a dollar. Can dip below zero by the cost
29 * of the runs that were under way when it ran out.
30 */
31 balanceMicros: number;
32 status: BillingStatus;
33 /** What is added to a run's cost, in percent. */
34 marginPercent: number;
35 /** What a run on the workspace's own model provider is charged instead. */
36 orchestrationFeeMicros: number;
37 /** The card charged near the limit and when a month closes, if one is saved. */
38 card?: { brand: string; last4: string; expMonth: number; expYear: number } | null;
39};
40
41/** One line of a workspace's statement. */
42export type LedgerEntry = {
43 id: string;
44 /** Credit bought with a card, or an agent's run. */
45 kind: "top_up" | "usage";
46 /** Positive for credit added, negative for usage. */
47 amountMicros: number;
48 description: string;
49 /** For usage: the repository and pull request the agent worked on. */
50 repo: string | null;
51 number: number | null;
52 /** For usage: `implement`, `review` or `update`. */
53 task: string | null;
54 /** For usage: the model, by its public name. */
55 model: string | null;
56 /** For usage: who paid the model provider. */
57 billedTo: "g1t" | "workspace";
58 /** For a top-up: the username of whoever paid. */
59 createdBy: string | null;
60 /** RFC 3339. */
61 createdAt: string;
62 /** The workspace the line belongs to, which tells an enterprise's lines apart. */
63 workspace?: string | null;
64};
65
66/** What lets a sandbox, and nothing else, report what its run cost. */
67export type RunTicket = { runId: string; token: string };
68
69/**
70 * What agents cost, charged to the workspace they worked for. A workspace
71 * buys credit; each run deducts its cost plus g1t's margin; with no credit,
72 * no agent starts.
73 */
74/**
75 * The free allowance on g1t's hosted models for a workspace not otherwise
76 * open to them: a few dollars of model cost each, out of one pool, until a
77 * date. Mirrors `Trial` in `crates/contracts/src/billing.rs`.
78 */
79/** How an account is charged. Standard unless g1t set otherwise in sudo. */
80export type Terms = {
81 kind: "standard" | "comped" | "custom";
82 discountPercent: number;
83 ceilingMicros: number | null;
84 note: string;
85 until: string | null;
86 setBy: string | null;
87 setAt: string | null;
88};
89
90/**
91 * Who pays: a workspace's own account, or an enterprise's, which pays for
92 * several workspaces with one bill and one limit.
93 */
94export type PayingAccount = {
95 id: string;
96 kind: "workspace" | "enterprise";
97 name: string;
98 terms: Terms;
99 workspaces: string[];
100 /** Where an enterprise's invoices go. */
101 billingEmail?: string | null;
102 /** An enterprise's invoices, newest first. */
103 invoices?: EnterpriseInvoice[];
104 createdAt: string;
105};
106
107export type AccountSummary = {
108 account: PayingAccount;
109 limit: Limit;
110 chargedMicros: number;
111 costMicros: number;
112 paidMicros: number;
113 /** The same figures for each of the account's workspaces that has any. */
114 byWorkspace: WorkspaceFigures[];
115 /** The last six months, oldest first. */
116 months?: MonthFigures[];
117};
118
119/** One workspace's share of an `AccountSummary`. */
120export type WorkspaceFigures = { workspace: string; chargedMicros: number; costMicros: number; paidMicros: number };
121
122export type StripeStatus = {
123 /** `test` or `live`, from the key; `off` without one. */
124 mode: "test" | "live" | "off" | string;
125 webhook: { url: string; endpointId: string; events: string[]; createdBy: string; createdAt: string } | null;
126 recentEvents: { id: string; kind: string; outcome: string; receivedAt: string }[];
127 error: string | null;
128};
129
130/** An enterprise's invoice: one line per workspace, paid on Stripe's page. */
131export type EnterpriseInvoice = {
132 invoiceId: string;
133 hostedUrl: string | null;
134 amountMicros: number;
135 status: "open" | "paid" | "overdue" | "void" | string;
136 period: string;
137 lines: { workspace: string; amountMicros: number }[];
138 createdAt: string;
139};
140
141/** A workspace's invoice: monthly, or when charged near its limit. Itemised, in Stripe's billing page. */
142export type WorkspaceInvoice = {
143 invoiceId: string;
144 workspace: string;
145 reason: "month" | "threshold" | string;
146 period: string;
147 amountMicros: number;
148 status: "paid" | "open" | "failed" | "void" | string;
149 hostedUrl: string | null;
150 pdfUrl: string | null;
151 lines: { description: string; amountMicros: number }[];
152 createdAt: string;
153};
154
155export type MonthFigures = { month: string; chargedMicros: number; costMicros: number; paidMicros: number };
156
157export type SignalKind = "at_limit" | "near_ceiling" | "declined" | "growing" | "established" | "first_payment" | "high_spend";
158
159/** Why a workspace is worth reaching out to. */
160export type Signal = {
161 workspace: string;
162 kind: SignalKind;
163 detail: string;
164 valueMicros: number;
165 stage: string | null;
166 owner: string | null;
167 nextStep?: string | null;
168 /** When the next step is due, YYYY-MM-DD. */
169 nextAt?: string | null;
170};
171
172/** One invoice g1t has sent, a workspace's or an enterprise's. */
173export type InvoiceSummary = {
174 invoiceId: string;
175 kind: "workspace" | "enterprise";
176 account: string;
177 name: string;
178 reason: string;
179 period: string;
180 amountMicros: number;
181 status: string;
182 hostedUrl: string | null;
183 createdAt: string;
184 paidAt: string | null;
185};
186
187export type SalesStage = "none" | "lead" | "contacted" | "negotiating" | "won" | "lost" | "churn_risk";
188
189export type SalesRecord = {
190 workspace: string;
191 stage: SalesStage | string;
192 owner: string | null;
193 nextStep: string | null;
194 nextAt: string | null;
195 notes: { id: string; text: string; by: string; createdAt: string }[];
196 updatedAt: string | null;
197};
198
199export type Overview = {
200 month: string;
201 months: MonthFigures[];
202 byKind: { kind: string; chargedMicros: number; costMicros: number }[];
203 payingWorkspaces: number;
204 stopped: number;
205 nearCeiling: number;
206 declined: number;
207 openInvoicesMicros: number;
208 followUpsDue: number;
209};
210
211/** A customer's Stripe billing page, for staff to send them. */
212export type BillingLink = {
213 /** One-time and short-lived, signed in already. */
214 portalUrl: string;
215 /** The page's sign-in, which does not expire: the customer signs in by email. */
216 loginUrl: string | null;
217 customerEmail: string | null;
218 expiresNote: string;
219};
220
221export type AdminAction = { id: string; account: string; action: string; detail: string; by: string; createdAt: string };
222
223export type AccountDetail = {
224 summary: AccountSummary;
225 workspaces: Limit[];
226 ledger: LedgerEntry[];
227 audit: AdminAction[];
228};
229
230/** Staff-only billing, for sudo.g1t.sh. Every change names who made it. */
231export interface BillingAdminApi {
232 accounts(query?: string): Promise<AccountSummary[]>;
233 account(id: string): Promise<Result<AccountDetail>>;
234 setTerms(id: string, terms: Terms, by: string): Promise<Result<PayingAccount>>;
235 createEnterprise(name: string, workspaces: string[], by: string): Promise<Result<PayingAccount>>;
236 attach(workspace: string, account: string | null, by: string): Promise<Result<PayingAccount>>;
237 credit(workspace: string, amountMicros: number, note: string, by: string): Promise<Result<LedgerEntry>>;
238 /** The workspace's Stripe billing page, to send to the customer. Logged. */
239 billingLink(workspace: string, by: string): Promise<Result<BillingLink>>;
240 /** Where billing stands with Stripe; with `setup`, registers the webhook first. */
241 stripe(setup?: boolean, by?: string): Promise<StripeStatus>;
242 /** Where an enterprise's invoices go; makes its Stripe customer. */
243 enterpriseBilling(id: string, email: string, by: string): Promise<Result<PayingAccount>>;
244 /** Sends an enterprise its invoice now, for what its workspaces owe. */
245 invoiceEnterprise(id: string, by: string): Promise<Result<EnterpriseInvoice>>;
246 /** Exactly these workspaces' accounts, such as one page of the list. */
247 accountsFor(workspaces: string[]): Promise<AccountSummary[]>;
248 /** Every workspace worth reaching out to, most urgent first. */
249 signals(): Promise<Signal[]>;
250 /** The business at a glance. */
251 overview(): Promise<Overview>;
252 /** A workspace's sales record. */
253 sales(workspace: string): Promise<SalesRecord>;
254 setSales(workspace: string, record: { stage: string; owner?: string | null; nextStep?: string | null; nextAt?: string | null }, by: string): Promise<Result<SalesRecord>>;
255 addNote(workspace: string, text: string, by: string): Promise<Result<SalesRecord>>;
256 /** A workspace's invoices from g1t, for staff. */
257 workspaceInvoices(workspace: string): Promise<WorkspaceInvoice[]>;
258 /** Every invoice g1t has sent, newest first. */
259 allInvoices(filter?: { status?: string; month?: string }): Promise<InvoiceSummary[]>;
260 /** Every change made in sudo and by Stripe, newest first, 100 at a time. */
261 audit(filter?: { by?: string; action?: string; before?: string }): Promise<AdminAction[]>;
262}
263
264/** How much a workspace has earned g1t's trust with money. */
265export type Trust = "new" | "paid" | "established" | "reviewed" | "internal";
266
267/**
268 * How far a workspace's unpaid usage has gone this month, and where its
269 * work stops: past `ceilingMicros`, no new sandboxes, builds or app
270 * requests. Usage counts at its cost to g1t or its charge, whichever is
271 * more, so it counts while g1t is free too.
272 */
273export type Limit = {
274 workspace: string;
275 /** The account that pays: the workspace's own (`ws_<slug>`), or its enterprise's. */
276 account: string;
277 accountName: string;
278 trust: Trust;
279 exposureMicros: number;
280 /** The lower of g1t's ceiling and the owner's spend limit; null for g1t's own. */
281 ceilingMicros: number | null;
282 trustCeilingMicros: number | null;
283 spendLimitMicros: number | null;
284 state: "ok" | "warning" | "stopped";
285 message: string | null;
286 /** Charged this month: what the spend limit is measured against. */
287 spentMicros?: number;
288 /** True while the owners have not chosen a limit, so the automatic one applies: $200, or twice last month's spend. */
289 defaultSpendLimit?: boolean;
290 /** The most owners may set their own limit to; past it, they contact g1t. */
291 availableMicros?: number | null;
292 /** How the ceiling grows from here, in a sentence. */
293 growth?: string | null;
294};
295
296/** One metered unit: what it costs g1t and what it is sold at; the price follows the cost. */
297export type Price = {
298 meter: "sandbox_second" | "build_second" | "app_requests" | "app_cpu" | "app_month" | string;
299 title: string;
300 unit: string;
301 costMicros: number;
302 markupPercent: number;
303 priceMicros: number;
304 /** `list`: Cloudflare's published price. `cloudflare`: measured from Cloudflare's bill. */
305 source: "list" | "cloudflare" | string;
306 checkedAt: string | null;
307 updatedAt: string;
308};
309
310export type PriceChange = {
311 meter: string;
312 oldCostMicros: number;
313 newCostMicros: number;
314 markupPercent: number;
315 reason: string;
316 createdAt: string;
317};
318
319export type PriceBook = { prices: Price[]; changes: PriceChange[]; modelMarginPercent: number };
320
321export type Trial = {
322 open: boolean;
323 usedMicros: number;
324 limitMicros: number;
325 endsAt: string | null;
326 /** Why it is closed: `off`, `ended`, `used` (this workspace's) or `pool` (everyone's). */
327 reason: "off" | "ended" | "used" | "pool" | null;
328};
329
330/**
331 * A paid feature a workspace turns on with a monthly plan, as Cloudflare's
332 * Workers for Platforms or Vercel's Pro are bought. Never free: neither
333 * `free` nor the model allowance covers it. Mirrors `Feature` in
334 * `crates/contracts/src/billing.rs`.
335 */
336export type Feature = "deployments";
337
338/** What the Deployments plan includes each month. Mirrors `deployments_allowance`. */
339export const DEPLOYMENTS_ALLOWANCE = {
340 apps: 10,
341 requests: 1_000_000,
342 cpuMs: 3_000_000,
343 /** What Cloudflare charges g1t past that, in millionths of a dollar. */
344 microsPerAppMonth: 20_000,
345 microsPerMillionRequests: 300_000,
346 microsPerMillionCpuMs: 20_000,
347 /** One second of a build's sandbox; builds are charged, not included. */
348 microsPerBuildSecond: 21,
349} as const;
350
351export type FeaturePlan = {
352 feature: Feature;
353 title: string;
354 /** Charged every month while the plan is on, in cents. */
355 monthlyCents: number;
356 /** What the price includes, one line each. */
357 includes: string[];
358 /** How usage past the allowance is charged. */
359 overage: string;
360};
361
362export type SubscriptionStatus = "active" | "canceling" | "past_due" | "canceled";
363
364export type Subscription = {
365 feature: Feature;
366 status: SubscriptionStatus;
367 /** RFC 3339: when the period paid for ends. */
368 periodEnd: string | null;
369 startedBy: string;
370 startedAt: string;
371};
372
373/** A feature as a workspace sees it. */
374export type FeatureState = {
375 plan: FeaturePlan;
376 subscription: Subscription | null;
377 /** Whether the feature works for the workspace now. */
378 on: boolean;
379};
380
381export interface BillingApi {
382 status(): Promise<BillingStatus>;
383 /** Members of the workspace only. */
384 account(workspace: string, viewer: Viewer): Promise<Result<BillingAccount>>;
385 /** Newest first. Members of the workspace only. */
386 ledger(workspace: string, viewer: Viewer): Promise<Result<LedgerEntry[]>>;
387 /** What the workspace's agents cost since `since`, broken down. Members only. */
388 usage(workspace: string, viewer: Viewer, since: string): Promise<Result<Usage>>;
389 /**
390 * Starts a card payment for credit and returns the page to send the
391 * person to. Owners only. The payment's id comes back to `returnUrl` as
392 * `session`.
393 */
394 checkout(actor: User, workspace: string, amountCents: number, returnUrl: string): Promise<Result<{ url: string }>>;
395 /**
396 * Stripe's hosted billing page for the workspace: card, invoices, billing
397 * email and address. g1t never handles card numbers. Owners only.
398 */
399 billingPortal(actor: User, workspace: string, returnUrl: string): Promise<Result<{ url: string }>>;
400 /** Credits a payment once the processor says it was made. Safe to repeat. */
401 confirm(workspace: string, viewer: Viewer, session: string): Promise<Result<BillingAccount>>;
402 /**
403 * Whether a workspace may start an agent now, asked before anything is
404 * opened for it. A failure, with the reason to show, when it has no credit.
405 */
406 canStart(workspace: string): Promise<Result<boolean>>;
407 /** A workspace's free allowance on g1t's hosted models; `exempt` are open to them anyway. */
408 trial(workspace: string, exempt: string[]): Promise<Trial>;
409 /**
410 * Asks whether a workspace may start an agent and opens the run it will be
411 * charged for. Null when billing is off; a failure when there is no credit.
412 */
413 /** Every paid feature and the workspace's plan for each. Members only. */
414 features(workspace: string, viewer: Viewer): Promise<Result<FeatureState[]>>;
415 /**
416 * Starts the card page for a feature's monthly plan. Owners only. The
417 * page's id comes back to `returnUrl` as `session`.
418 */
419 subscribe(actor: User, workspace: string, feature: Feature, returnUrl: string): Promise<Result<{ url: string }>>;
420 /** Turns the feature on once the plan is paid for. Safe to repeat. */
421 confirmSubscription(workspace: string, viewer: Viewer, session: string): Promise<Result<FeatureState>>;
422 /** Ends a plan at the end of its period, or (`resume`) takes that back. Owners only. */
423 cancelSubscription(actor: User, workspace: string, feature: Feature, resume?: boolean): Promise<Result<FeatureState>>;
424 /** Whether a feature works for a workspace now; a failure with the reason when not. */
425 hasFeature(workspace: string, feature: Feature): Promise<Result<boolean>>;
426 /**
427 * Usage past a plan's allowance, charged from credit at cost plus the
428 * margin, once per `reference`. False if it was charged before.
429 */
430 chargeFeature(charge: {
431 workspace: string;
432 feature: Feature;
433 costMicros: number;
434 description: string;
435 repo?: string | null;
436 reference: string;
437 }): Promise<Result<boolean>>;
438 /**
439 * Usage this month to be charged later (app traffic past a plan), so the
440 * workspace's limit counts it now. Replaces the last report.
441 */
442 notePending(workspace: string, source: "deployments", costMicros: number): Promise<boolean>;
443 /** Every metered price and the recent changes. Public. */
444 prices(): Promise<PriceBook>;
445 /** A workspace's limit, for its members. */
446 limit(workspace: string, viewer: Viewer): Promise<Result<Limit>>;
447 /** The same, for the services that enforce it. */
448 checkLimit(workspace: string): Promise<Result<Limit>>;
449 /**
450 * The owners' own monthly limit, up to what is available; null goes back
451 * to the default, and `useFullLimit` uses everything available. Owners only.
452 */
453 setSpendLimit(actor: User, workspace: string, spendLimitMicros: number | null, useFullLimit?: boolean): Promise<Result<Limit>>;
454 /** The workspace's invoices from g1t, newest first. Members only. */
455 invoices(workspace: string, viewer: Viewer): Promise<Result<WorkspaceInvoice[]>>;
456 /**
457 * How long a sandbox ran for a workspace, reported when it stops. Its
458 * cost is always recorded; seconds past the month's free minutes are
459 * charged. False if `reference` was recorded before.
460 */
461 recordSandbox(usage: {
462 workspace: string;
463 seconds: number;
464 description: string;
465 repo?: string | null;
466 reference: string;
467 }): Promise<Result<boolean>>;
468 startRun(run: {
469 workspace: string;
470 repo: RepoPath;
471 number: number;
472 task: string;
473 model: string;
474 /** `workspace` when the run uses the workspace's own model provider. */
475 billedTo?: "g1t" | "workspace";
476 /** The model session's id, so the run can be settled at AI Gateway's price. */
477 session?: string | null;
478 }): Promise<Result<RunTicket | null>>;
479}
480
481
482/** One slice of usage: what it was for, what it cost, how many runs. */
483export type UsageSlice = { key: string; micros: number; runs: number };
484
485/** What a workspace's agents cost over a period. */
486export type Usage = {
487 since: string;
488 /** Charged, including g1t's margin. */
489 spentMicros: number;
490 /** What g1t's model provider charged, before the margin. */
491 costMicros: number;
492 /** What runs on the workspace's own provider cost there, estimated. Not charged by g1t. */
493 providerMicros: number;
494 /** What the runs used, at cost: g1t's models and the workspace's own provider together. */
495 usedMicros: number;
496 /** g1t charges nothing for now; the slices then measure usage at cost. */
497 free: boolean;
498 runs: number;
499 /** Spend per day and task, keyed `YYYY-MM-DD/task`. */
500 byDay: UsageSlice[];
501 byTask: UsageSlice[];
502 byRepo: UsageSlice[];
503 /** Keyed `namespace/name#number`. */
504 byPull: UsageSlice[];
505 byModel: UsageSlice[];
506 /** Credit bought in the period. */
507 addedMicros: number;
508};