Skip to content

g1t/apps/web/app/routes/workspace/billing.tsx

507 lines24,493 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1import { ArrowUpRight, CreditCard, FileText } from "lucide-react";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2import { Suspense } from "react";
3import { Await, Link, data, redirect } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look5import { MICROS_PER_DOLLAR, type WorkspaceInvoice } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
7import type { Route } from "./+types/billing";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look8import {
9 Alerts,
10 CapsCard,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging11 CreditsCard,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look12 OverageCard,
13 PrepayCard,
14 RaiseCard,
15 type SectionError,
16 SpendLimitCard,
17 SpikeBanner,
18 TrialCard,
19} from "../../components/billing";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit20import {
21 AddOns,
22 AiCreditCard,
23 BudgetAlerts,
24 InvoiceDetailsCard,
25 InvoicesCard,
26 PaymentMethodCard,
27 PlanSummary,
28 StripeSkeleton,
29} from "../../components/billing-settings";
The statement is a month at a time, a line per kind of charge30import { StatementView } from "../../components/statement";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look31import {
32 cardCheckResult,
33 dollars,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit34 parseAiPurchase,
35 parseAiReload,
36 parseBudgetAlerts,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look37 parseCaps,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit38 parseInvoiceDetails,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look39 parseLimitRequest,
40 parsePrepay,
41 parseSpendLimit,
42 planStatus,
43 wholeDollars,
44} from "../../lib/billing";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit45import { isStaff } from "../../lib/usage";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look46import { page } from "../../lib/meta";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas47import { billing } from "../../lib/services.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look48import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
Agents as a team: lifecycle, merge queue, billing and a new shell49
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look50/** The trial and pools as published, when the price book cannot be read. */
51const DEFAULT_TRIAL_MICROS = 5_000_000;
Agents as a team: lifecycle, merge queue, billing and a new shell52
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look53/** What a change says once it is done (`?done=`). */
54const DONE: Record<string, string> = {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit55 subscribed: "The g1t plan is on, with $5 of AI credit to start.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look56 prepaid: "Payment received. Your prepaid balance and what you can use went up by as much.",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit57 ai_credit: "AI credit bought. It is ready for Agent and AI Gateway usage.",
58 ai_reload: "Auto-reload saved.",
59 budget: "Budget alerts saved.",
60 details: "Invoice details saved on Stripe.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look61 limit: "Spend limit saved.",
62 caps: "Caps saved. They apply to runs that start from now on.",
63 requested: "Sent. g1t answers within one business day, here and by email.",
64 told: "Thanks. g1t looks at it and answers within one business day, here and by email.",
65 keep: "Compute is running again for 24 hours, unless spending doubles again first.",
66 stop: "New compute stays paused until an owner chooses Keep going.",
67 canceled: "The plan ends at the end of the period. Nothing more is charged for it.",
68 resumed: "The plan continues.",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar69 security_on: "Security and quality is on for every private repository in the workspace.",
70 security_canceled: "Security and quality ends at the end of the period. Nothing more is charged for it.",
71 security_resumed: "Security and quality continues.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look72};
73
Social cards for every page: og.g1t.sh74export function meta({ params, ...args }: Route.MetaArgs) {
75 return page(args, { title: `Billing · ${params.owner} · g1t` });
Agents as a team: lifecycle, merge queue, billing and a new shell76}
77
78export async function loader({ params, context, request }: Route.LoaderArgs) {
79 const viewer = getViewer(context);
80 const role = roleIn(viewer, params.owner);
81 // Members only; to anyone else the page does not exist.
82 if (!role) throw data(null, { status: 404 });
83 const slug = params.owner.toLowerCase();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look84 const url = new URL(request.url);
85 const here = `/${slug}/-/billing`;
Agents as a team: lifecycle, merge queue, billing and a new shell86
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look87 // Back from Stripe: record what happened, then drop the ids from the address.
88 const cardCheck = url.searchParams.get("card_check");
89 if (cardCheck) {
90 const checked = await billing.confirmCardCheck(slug, viewer, cardCheck);
91 throw redirect(checked.ok ? `${here}?checked=1#trial` : `${here}?problem=${encodeURIComponent(checked.error.message)}#trial`);
92 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit93 const aiCredit = url.searchParams.get("ai_credit");
94 if (aiCredit) {
95 const bought = await billing.confirmAiCredit(slug, viewer, aiCredit);
96 throw redirect(bought.ok ? `${here}?done=ai_credit#ai-credit` : `${here}?problem=${encodeURIComponent(bought.error.message)}#ai-credit`);
97 }
Agents as a team: lifecycle, merge queue, billing and a new shell98 const session = url.searchParams.get("session");
99 if (session) {
Paid features: a workspace turns on Deployments with a monthly plan100 if (url.searchParams.get("plan")) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look101 const started = await billing.confirmSubscription(slug, viewer, session);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar102 const doneKey = url.searchParams.get("plan") === "security" ? "security_on" : "subscribed";
103 throw redirect(started.ok ? `${here}?done=${doneKey}` : `${here}?problem=${encodeURIComponent(started.error.message)}`);
Paid features: a workspace turns on Deployments with a monthly plan104 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look105 const paid = await billing.confirm(slug, viewer, session);
106 throw redirect(paid.ok ? `${here}?done=prepaid#prepay` : `${here}?problem=${encodeURIComponent(paid.error.message)}#prepay`);
Agents as a team: lifecycle, merge queue, billing and a new shell107 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit108 // A plan started on the card already saved comes straight back.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar109 const plans = url.searchParams.getAll("plan");
110 if (plans.includes("started")) throw redirect(`${here}?done=${plans.includes("security") ? "security_on" : "subscribed"}`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look111
Usage, Billing settings and prepaid AI credit; fixes from the UX audit112 const now = new Date();
113 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString().slice(0, 10);
114 const today = now.toISOString().slice(0, 10);
The statement is a month at a time, a line per kind of charge115 const group: "day" | "project" = url.searchParams.get("group") === "project" ? "project" : "day";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit116 // Stripe's own (payment method, invoice details, invoices) is streamed:
117 // the page does not wait on Stripe, and its sections hold their place.
118 const details = billing
119 .billingDetails(slug, viewer)
120 .then((result) => (result.ok ? result.value : null))
121 .catch(() => null);
122 const [account, statement, features, limit, invoices, entitlements, requests, book, credits, ai, report] = await Promise.all([
Agents as a team: lifecycle, merge queue, billing and a new shell123 billing.account(slug, viewer),
The statement is a month at a time, a line per kind of charge124 billing.statement(slug, viewer, url.searchParams.get("month"), group),
Paid features: a workspace turns on Deployments with a monthly plan125 billing.features(slug, viewer),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look126 billing.limit(slug, viewer).catch(() => null),
Two limits, real invoices, trust that grows by itself, sales signals127 billing.invoices(slug, viewer).catch(() => null),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put128 billing.entitlements(slug).catch(() => null),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look129 billing.limitRequests(slug, viewer).catch(() => null),
130 billing.prices().catch(() => null),
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging131 billing.credits(slug, viewer).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit132 billing.aiCredit(slug, viewer).catch(() => null),
133 billing.usageReport(slug, viewer, { from: monthStart, until: today }).catch(() => null),
Agents as a team: lifecycle, merge queue, billing and a new shell134 ]);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look135 const featureStates = unwrap(features);
136 const trialMicros = book?.free?.trialWorkspaceMicros ?? DEFAULT_TRIAL_MICROS;
137 const done = url.searchParams.get("done");
Usage, Billing settings and prepaid AI credit; fixes from the UX audit138 // Credit from g1t for everything; AI credit has its own card.
139 const general = credits?.ok ? credits.value.grants.filter((grant) => grant.scope !== "models") : [];
Agents as a team: lifecycle, merge queue, billing and a new shell140 return {
141 slug,
142 role,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit143 staff: isStaff(viewer),
Agents as a team: lifecycle, merge queue, billing and a new shell144 account: unwrap(account),
The statement is a month at a time, a line per kind of charge145 statement: unwrap(statement),
146 group,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look147 plan: featureStates.find((state) => state.plan.feature === "plan") ?? null,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar148 securityPlan: featureStates.find((state) => state.plan.feature === "security") ?? null,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look149 limit: limit?.ok ? limit.value : null,
Two limits, real invoices, trust that grows by itself, sales signals150 invoices: invoices?.ok ? invoices.value : [],
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put151 entitlements,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look152 requests: requests?.ok ? requests.value : [],
Usage, Billing settings and prepaid AI credit; fixes from the UX audit153 credits: general.length > 0 && credits?.ok ? { ...credits.value, grants: general } : null,
154 ai: ai?.ok ? ai.value : null,
155 report: report?.ok ? report.value : null,
156 details,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look157 trialMicros,
158 notice:
159 url.searchParams.has("checked") && entitlements
160 ? cardCheckResult(entitlements, trialMicros)
161 : done && DONE[done]
162 ? DONE[done]
163 : null,
164 problem: url.searchParams.get("problem"),
Agents as a team: lifecycle, merge queue, billing and a new shell165 };
166}
167
168export async function action({ request, params, context }: Route.ActionArgs) {
169 assertSameOrigin(request);
170 const user = requireUser(context, request);
171 const form = await request.formData();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look172 const slug = params.owner.toLowerCase();
173 // Spelled out: a form post arrives at a data address, not the page's.
174 const here = `${new URL(request.url).origin}/${slug}/-/billing`;
175 const intent = String(form.get("intent") ?? "");
176 const fail = (section: string, error: string) => data<SectionError>({ section, error }, { status: 422 });
177 const done = (key: string, anchor = "") => redirect(`/${slug}/-/billing?done=${key}${anchor}`);
178
179 switch (intent) {
180 case "portal": {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit181 // Cards, payment methods and receipts live on Stripe's own page.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look182 const started = await billing.billingPortal(user, slug, here);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit183 if (!started.ok) return fail("payment", started.error.message);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look184 throw redirect(started.value.url);
185 }
186 case "subscribe": {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar187 // The plan, or the Security and quality activation.
188 const feature = form.get("feature") === "security" ? "security" : "plan";
189 const started = await billing.subscribe(user, slug, feature, `${here}?plan=${feature}`);
190 if (!started.ok) return fail(feature, started.error.message);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look191 throw redirect(started.value.url);
192 }
193 case "cancel":
194 case "resume": {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar195 const feature = form.get("feature") === "security" ? "security" : "plan";
196 const changed = await billing.cancelSubscription(user, slug, feature, intent === "resume");
197 if (!changed.ok) return fail(feature, changed.error.message);
198 const prefix = feature === "security" ? "security_" : "";
199 throw done(`${prefix}${intent === "resume" ? "resumed" : "canceled"}`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look200 }
201 case "card-check": {
202 const started = await billing.cardCheck(user, slug, here);
203 if (!started.ok) return fail("trial", started.error.message);
204 throw redirect(started.value.url);
205 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit206 case "buy-ai-credit": {
207 const parsed = parseAiPurchase({ amount: form.get("amount"), custom: form.get("custom") });
208 if (!parsed.ok) return fail("ai", parsed.error);
209 const started = await billing.buyAiCredit(user, slug, parsed.value.amountCents, here);
210 if (!started.ok) return fail("ai", started.error.message);
211 throw redirect(started.value.url);
212 }
213 case "ai-reload": {
214 const parsed = parseAiReload({ enabled: form.get("enabled"), threshold: form.get("threshold"), target: form.get("target"), monthly: form.get("monthly") });
215 if (!parsed.ok) return fail("ai", parsed.error);
216 const set = await billing.setAiReload(user, slug, parsed.value);
217 if (!set.ok) return fail("ai", set.error.message);
218 throw done("ai_reload", "#ai-credit");
219 }
220 case "budget": {
221 const parsed = parseBudgetAlerts({ alerts: form.getAll("alert"), pause: form.get("pause"), webhook: form.get("webhook") });
222 if (!parsed.ok) return fail("budget", parsed.error);
223 const set = await billing.setBudget(user, slug, { amountMicros: null, keepLimit: true, ...parsed.value });
224 if (!set.ok) return fail("budget", set.error.message);
225 throw done("budget", "#budget");
226 }
227 case "details": {
228 const parsed = parseInvoiceDetails(form);
229 if (!parsed.ok) return fail("details", parsed.error);
230 const saved = await billing.setBillingDetails(user, slug, parsed.value);
231 if (!saved.ok) return fail("details", saved.error.message);
232 throw done("details", "#details");
233 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look234 case "spend-limit": {
235 const parsed = parseSpendLimit({ mode: form.get("mode"), limit: form.get("limit") });
236 if (!parsed.ok) return fail("limit", parsed.error);
237 let micros = parsed.value.micros;
238 if (parsed.value.raiseOnce && micros == null) {
239 // The one-time raise with no amount: as high as it goes.
240 const limit = await billing.limit(slug, user);
241 micros = limit.ok ? (limit.value.raiseOnceMicros ?? null) : null;
242 if (micros == null) return fail("limit", "The one-time raise is not available. Prepay, or use Raise my limit.");
243 }
244 const set = await billing.setSpendLimit(user, slug, micros, parsed.value.useFull, parsed.value.raiseOnce);
245 if (!set.ok) return fail("limit", set.error.message);
246 throw done("limit", "#limit");
247 }
248 case "request": {
249 const parsed = parseLimitRequest({
250 kind: form.get("kind"),
251 amount: form.get("amount"),
252 reason: form.get("reason"),
253 expected: form.get("expected"),
254 });
255 const section = String(form.get("kind")) === "overage" ? "overage" : "raise";
256 if (!parsed.ok) return fail(section, parsed.error);
257 const sent = await billing.requestLimit(user, slug, parsed.value);
258 if (!sent.ok) return fail(section, sent.error.message);
259 throw done(section === "overage" ? "told" : "requested", `#${section}`);
260 }
261 case "caps": {
262 const parsed = parseCaps({ run: form.get("run"), issue: form.get("issue") });
263 if (!parsed.ok) return fail("caps", parsed.error);
264 const set = await billing.setCaps(user, slug, parsed.value);
265 if (!set.ok) return fail("caps", set.error.message);
266 throw done("caps", "#caps");
267 }
268 case "spike": {
269 const keep = String(form.get("decision")) === "keep";
270 const answered = await billing.confirmSpike(user, slug, keep);
271 if (!answered.ok) return fail("spike", answered.error.message);
272 throw done(keep ? "keep" : "stop");
Usage limits: unpaid usage can only go so far273 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look274 case "prepay": {
275 const parsed = parsePrepay({ amount: form.get("amount"), custom: form.get("custom"), method: form.get("method") });
276 if (!parsed.ok) return fail("prepay", parsed.error);
277 const started = await billing.checkout(user, slug, parsed.value.amountCents, here, parsed.value.method);
278 if (!started.ok) return fail("prepay", started.error.message);
279 throw redirect(started.value.url);
Paid features: a workspace turns on Deployments with a monthly plan280 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look281 default:
282 return fail("plan", "Unknown action.");
Paid features: a workspace turns on Deployments with a monthly plan283 }
Agents as a team: lifecycle, merge queue, billing and a new shell284}
285
286export default function WorkspaceBilling({ loaderData, actionData }: Route.ComponentProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit287 const { slug, role, staff, account, statement, group, plan, securityPlan, limit, invoices, entitlements, requests, credits, ai, report, details, trialMicros, notice, problem } =
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put288 loaderData;
Agents as a team: lifecycle, merge queue, billing and a new shell289 const { status } = account;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look290 const owner = role === "owner";
291 const error = actionData as SectionError;
292 const err = (section: string) => (error && error.section === section ? error.error : undefined);
293 const standing = planStatus(plan, entitlements);
294 const paying = standing.kind === "paid" || standing.kind === "canceling" || standing.kind === "past_due";
295 const free = standing.kind === "free" || standing.kind === "trial";
296 const prepaid = limit?.prepaidMicros ?? entitlements?.prepaidMicros ?? 0;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit297 // The spend limit and its budget are the owners' to set only where
298 // billing lets them: on the plan, never a free workspace's ceiling.
299 const selfServe = !!limit && (limit.trust === "paid" || limit.trust === "established");
300 const summary = (loaded: Awaited<typeof details>) => (
301 <PlanSummary slug={slug} state={plan} status={standing} details={loaded} report={report} owner={owner} enabled={status.enabled} staff={staff} error={err("plan")} />
302 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look303
Agents as a team: lifecycle, merge queue, billing and a new shell304 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look305 <div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_20rem]">
Agents as a team: lifecycle, merge queue, billing and a new shell306 <div className="min-w-0">
Free while g1t is being built out; agents can check out their own forks307 {status.free && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look308 <div className="mb-6 rounded-xl border border-accent/30 bg-accent/5 p-5">
Free while g1t is being built out; agents can check out their own forks309 <h2 className="font-medium">Free while g1t is being built out</h2>
310 <p className="mt-1.5 max-w-2xl text-sm text-muted">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look311 Runs are recorded with what they cost, so Usage shows what you use, but nothing is charged for now. We will say so well
312 before anything is.
Free while g1t is being built out; agents can check out their own forks313 </p>
314 </div>
315 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look316 {notice && <p className="mb-6 rounded-lg border border-accent/30 bg-accent/5 px-4 py-2.5 text-sm">{notice}</p>}
317 {problem && <p className="mb-6 rounded-lg border border-danger/40 bg-danger/5 px-4 py-2.5 text-sm">{problem}</p>}
318
Merge Stripe Tax, the card fee on card payments, and one free workspace per person319 <Suspense fallback={null}>
320 <Await resolve={details}>
321 {(loaded) =>
322 loaded?.taxAddressNeededAt ? (
323 <div role="alert" className="mb-6 rounded-xl border border-warn/40 bg-warn/5 p-4 text-sm">
324 <p className="font-medium">Add a billing address</p>
325 <p className="mt-1 text-muted">
326 Stripe needs it to work out tax, so g1t did not charge the card. Nothing is lost: the charge goes through once the
327 address is saved.{" "}
328 {owner ? (
329 <a href="#details" className="text-fg underline underline-offset-2">
330 Add it under Invoice details
331 </a>
332 ) : (
333 "An owner adds it under Invoice details."
334 )}
335 </p>
336 </div>
337 ) : null
338 }
339 </Await>
340 </Suspense>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look341 {entitlements && <SpikeBanner slug={slug} entitlements={entitlements} owner={owner} />}
342 {err("spike") && <p className="mb-6 text-sm text-danger">{err("spike")}</p>}
343 <Alerts alerts={entitlements?.alerts ?? []} />
344
Usage, Billing settings and prepaid AI credit; fixes from the UX audit345 <Suspense fallback={summary(null)}>
346 <Await resolve={details}>{(loaded) => summary(loaded)}</Await>
347 </Suspense>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look348
Usage, Billing settings and prepaid AI credit; fixes from the UX audit349 {ai && status.enabled && <AiCreditCard credit={ai} owner={owner} enabled={status.enabled} staff={staff} error={err("ai")} />}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar350
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look351 {free && status.enabled && (
Merge branch 'worktree-agent-a985c01f43538af01'352 <TrialCard entitlements={entitlements} trialMicros={trialMicros} owner={owner} enabled={status.enabled} error={err("trial")} />
A flat project menu with Code first, views as tabs, and Stripe for every owner353 )}
354
Usage, Billing settings and prepaid AI credit; fixes from the UX audit355 {paying && limit && selfServe && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look356 <>
Merge branch 'worktree-agent-a985c01f43538af01'357 <SpendLimitCard limit={limit} owner={owner} error={err("limit")} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit358 <BudgetAlerts limit={limit} owner={owner} error={err("budget")} />
Merge branch 'worktree-agent-a985c01f43538af01'359 <RaiseCard requests={requests} owner={owner} error={err("raise")} />
Merge Stripe Tax, the card fee on card payments, and one free workspace per person360 <PrepayCard prepaidMicros={prepaid} owner={owner} live={status.live || !staff} cardFee={ai?.cardFee ?? null} error={err("prepay")} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look361 </>
362 )}
363
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging364 {credits && <CreditsCard credits={credits} />}
365
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look366 {entitlements && (paying || standing.kind === "trial" || standing.kind === "comped" || standing.kind === "enterprise") && (
Merge branch 'worktree-agent-a985c01f43538af01'367 <CapsCard entitlements={entitlements} owner={owner} error={err("caps")} />
Usage limits: unpaid usage can only go so far368 )}
369
Merge branch 'worktree-agent-a985c01f43538af01'370 {paying && <OverageCard requests={requests} owner={owner} error={err("overage")} />}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look371
Usage, Billing settings and prepaid AI credit; fixes from the UX audit372 <AddOns plan={plan} security={securityPlan} owner={owner} enabled={status.enabled} error={err("security")} />
373
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look374 {status.enabled && (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit375 <Suspense fallback={<StripeSkeleton />}>
376 <Await resolve={details}>
377 {(loaded) => (
378 <>
379 <PaymentMethodCard details={loaded} owner={owner} enabled={status.enabled} staff={staff} error={err("payment")} />
380 <InvoiceDetailsCard details={loaded} owner={owner} enabled={status.enabled} error={err("details")} />
381 <InvoicesCard details={loaded} />
382 </>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace383 )}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit384 </Await>
385 </Suspense>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace386 )}
387
Two limits, real invoices, trust that grows by itself, sales signals388 {invoices.length > 0 && <InvoiceList invoices={invoices} />}
Agents as a team: lifecycle, merge queue, billing and a new shell389
The statement is a month at a time, a line per kind of charge390 <StatementView slug={slug} statement={statement} group={group} />
Agents as a team: lifecycle, merge queue, billing and a new shell391 </div>
392
393 <aside className="space-y-5 text-sm">
394 <section className="rounded-xl border border-line bg-surface p-5">
395 <h3 className="font-medium">How it is charged</h3>
396 <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted">
397 <li>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit398 The plan is {wholeDollars((plan?.plan.monthlyCents ?? 2000) * 10_000)} a month for the workspace, with $10 of usage
399 included. Usage is charged at what it costs g1t plus {account.marginPercent}%, from the first second, after what is
400 included.
401 </li>
402 <li>
403 Agent runs: the model at the provider's price{ai && ai.modelMarkupPercent ? ` plus ${ai.modelMarkupPercent}%` : ", with no markup"}, plus
404 g1t's agent rate per million tokens{ai && ai.agentRateMicros > 0 ? ` (${dollars(ai.agentRateMicros)})` : ""}. AI Gateway: the
405 provider's price, free of markup while in beta.
Agents as a team: lifecycle, merge queue, billing and a new shell406 </li>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit407 <li>AI usage draws on prepaid AI credit first; at $0, new runs on g1t's models wait for more credit or auto-reload.</li>
408 <li>Credit from g1t comes off what you owe, the soonest-expiring first.</li>
Agents as a team: lifecycle, merge queue, billing and a new shell409 <li>
Integrations: your own model provider, alerts that open issues, tickets agents read410 With your own model provider, connected under{" "}
411 <Link to={`/${slug}/-/integrations`} className="text-fg hover:underline">
412 Integrations
413 </Link>
Prices are what g1t pays plus 20%, from the first second414 , the provider bills you for the model, and a run here is charged only its sandbox time.
Agents as a team: lifecycle, merge queue, billing and a new shell415 </li>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put416 <li>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look417 Each month closes with an itemised invoice. No card is charged less than{" "}
418 {dollars(entitlements?.minChargeMicros ?? 5 * MICROS_PER_DOLLAR, 0)}; less carries over.
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put419 </li>
Merge Stripe Tax, the card fee on card payments, and one free workspace per person420 <li>
421 Prices exclude tax. Stripe adds tax where it applies, worked out from the billing address under Invoice details, and it
422 is its own line on every receipt and invoice.
423 </li>
424 <li>
425 Card payments carry Stripe's card processing fee as their own line, shown before you pay. Bank transfers and invoiced
426 billing have none.
427 </li>
428 <li>No seats: add as many people and agents as you like, once the workspace is on the plan.</li>
Agents as a team: lifecycle, merge queue, billing and a new shell429 </ul>
A flat project menu with Code first, views as tabs, and Stripe for every owner430 <div className="mt-4 space-y-1.5 border-t border-line pt-4 text-sm">
431 <Link to={`/${slug}/-/usage`} className="flex items-center gap-2 text-muted hover:text-fg">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit432 <FileText size={14} /> Usage, by product and project
A flat project menu with Code first, views as tabs, and Stripe for every owner433 </Link>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look434 <Link to="/pricing" className="flex items-center gap-2 text-muted hover:text-fg">
435 <CreditCard size={14} /> Pricing and today's prices
436 </Link>
437 <a href="https://docs.g1t.sh/guides/usage-and-billing/" className="flex items-center gap-2 text-muted hover:text-fg">
A flat project menu with Code first, views as tabs, and Stripe for every owner438 <ArrowUpRight size={14} /> How usage and billing work
439 </a>
440 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell441 </section>
442 </aside>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put443 </div>
444 );
445}
446
Usage, Billing settings and prepaid AI credit; fixes from the UX audit447/** The workspace's month-end invoices from g1t, each kept on Stripe with its PDF. */
Two limits, real invoices, trust that grows by itself, sales signals448function InvoiceList({ invoices }: { invoices: WorkspaceInvoice[] }) {
449 return (
450 <section className="mb-10">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit451 <h2 className="font-medium">Usage invoices</h2>
Merge Stripe Tax, the card fee on card payments, and one free workspace per person452 <p className="mt-1 text-sm text-muted">
453 One when each month closes, and one each time g1t charges the card near your limit. Amounts are for usage; the card fee
454 and tax are their own lines.
455 </p>
Two limits, real invoices, trust that grows by itself, sales signals456 <ul className="mt-4 divide-y divide-line overflow-hidden rounded-xl border border-line">
457 {invoices.map((invoice) => (
458 <li key={invoice.invoiceId} className="px-4 py-3 text-sm">
459 <div className="flex flex-wrap items-center gap-3">
460 <span className="font-medium">
461 {invoice.reason === "month" ? `Usage for ${invoice.period}` : `Charged near the limit, ${invoice.period}`}
462 </span>
463 <span
464 className={`rounded-full border px-2 py-0.5 text-xs ${
Merge branch 'worktree-agent-ad7c6d88d93adc817'465 invoice.status === "paid" ? "border-success/40 text-success" : "border-danger/40 text-danger"
Two limits, real invoices, trust that grows by itself, sales signals466 }`}
467 >
468 {invoice.status === "paid" ? "Paid" : invoice.status === "failed" ? "Payment failed" : invoice.status}
469 </span>
470 <span className="ml-auto font-mono tabular-nums">{dollars(invoice.amountMicros)}</span>
471 {invoice.hostedUrl && (
472 <a href={invoice.hostedUrl} className="text-xs text-muted hover:text-fg">
473 View
474 </a>
475 )}
476 {invoice.pdfUrl && (
477 <a href={invoice.pdfUrl} className="text-xs text-muted hover:text-fg">
478 PDF
479 </a>
480 )}
481 </div>
482 <ul className="mt-1.5 space-y-0.5 text-xs text-faint">
483 {invoice.lines.map((line) => (
484 <li key={line.description} className="flex justify-between gap-4">
485 <span>{line.description}</span>
486 <span className="font-mono tabular-nums">{dollars(line.amountMicros)}</span>
487 </li>
488 ))}
Merge Stripe Tax, the card fee on card payments, and one free workspace per person489 {(invoice.feeMicros ?? 0) > 0 && (
490 <li className="flex justify-between gap-4">
491 <span>Card processing fee</span>
492 <span className="font-mono tabular-nums">{dollars(invoice.feeMicros ?? 0)}</span>
493 </li>
494 )}
495 {(invoice.taxMicros ?? 0) > 0 && (
496 <li className="flex justify-between gap-4">
497 <span>Tax</span>
498 <span className="font-mono tabular-nums">{dollars(invoice.taxMicros ?? 0)}</span>
499 </li>
500 )}
Two limits, real invoices, trust that grows by itself, sales signals501 </ul>
502 </li>
503 ))}
504 </ul>
505 </section>
506 );
507}

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