Skip to content
406 linesCodeBlameRaw

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

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1import { ArrowUpRight, CreditCard, FileText } from "lucide-react";
Merge branch 'worktree-agent-a985c01f43538af01'2import { Form, Link, data, redirect } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell3
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look4import { MICROS_PER_DOLLAR, type WorkspaceInvoice } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell5
6import type { Route } from "./+types/billing";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look7import {
8 Alerts,
9 CapsCard,
10 OverageCard,
11 PlanCard,
12 PrepayCard,
13 RaiseCard,
14 type SectionError,
15 SpendLimitCard,
16 SpikeBanner,
17 TrialCard,
18} from "../../components/billing";
The statement is a month at a time, a line per kind of charge19import { StatementView } from "../../components/statement";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar20import { SecurityActivationCard } from "../../components/security-activation";
Merge branch 'worktree-agent-a985c01f43538af01'21import { SubmitButton } from "../../components/ui";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look22import {
23 cardCheckResult,
24 dollars,
25 parseCaps,
26 parseLimitRequest,
27 parsePrepay,
28 parseSpendLimit,
29 planStatus,
30 wholeDollars,
31} from "../../lib/billing";
32import { page } from "../../lib/meta";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas33import { billing } from "../../lib/services.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look34import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
Agents as a team: lifecycle, merge queue, billing and a new shell35
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look36/** The trial and pools as published, when the price book cannot be read. */
37const DEFAULT_TRIAL_MICROS = 5_000_000;
Agents as a team: lifecycle, merge queue, billing and a new shell38
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look39/** What a change says once it is done (`?done=`). */
40const DONE: Record<string, string> = {
41 subscribed: "The g1t plan is on.",
42 prepaid: "Payment received. Your prepaid balance and what you can use went up by as much.",
43 limit: "Spend limit saved.",
44 caps: "Caps saved. They apply to runs that start from now on.",
45 requested: "Sent. g1t answers within one business day, here and by email.",
46 told: "Thanks. g1t looks at it and answers within one business day, here and by email.",
47 keep: "Compute is running again for 24 hours, unless spending doubles again first.",
48 stop: "New compute stays paused until an owner chooses Keep going.",
49 canceled: "The plan ends at the end of the period. Nothing more is charged for it.",
50 resumed: "The plan continues.",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar51 security_on: "Security and quality is on for every private repository in the workspace.",
52 security_canceled: "Security and quality ends at the end of the period. Nothing more is charged for it.",
53 security_resumed: "Security and quality continues.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look54};
55
Social cards for every page: og.g1t.sh56export function meta({ params, ...args }: Route.MetaArgs) {
57 return page(args, { title: `Billing · ${params.owner} · g1t` });
Agents as a team: lifecycle, merge queue, billing and a new shell58}
59
60export async function loader({ params, context, request }: Route.LoaderArgs) {
61 const viewer = getViewer(context);
62 const role = roleIn(viewer, params.owner);
63 // Members only; to anyone else the page does not exist.
64 if (!role) throw data(null, { status: 404 });
65 const slug = params.owner.toLowerCase();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look66 const url = new URL(request.url);
67 const here = `/${slug}/-/billing`;
Agents as a team: lifecycle, merge queue, billing and a new shell68
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69 // Back from Stripe: record what happened, then drop the ids from the address.
70 const cardCheck = url.searchParams.get("card_check");
71 if (cardCheck) {
72 const checked = await billing.confirmCardCheck(slug, viewer, cardCheck);
73 throw redirect(checked.ok ? `${here}?checked=1#trial` : `${here}?problem=${encodeURIComponent(checked.error.message)}#trial`);
74 }
Agents as a team: lifecycle, merge queue, billing and a new shell75 const session = url.searchParams.get("session");
76 if (session) {
Paid features: a workspace turns on Deployments with a monthly plan77 if (url.searchParams.get("plan")) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look78 const started = await billing.confirmSubscription(slug, viewer, session);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar79 const doneKey = url.searchParams.get("plan") === "security" ? "security_on" : "subscribed";
80 throw redirect(started.ok ? `${here}?done=${doneKey}` : `${here}?problem=${encodeURIComponent(started.error.message)}`);
Paid features: a workspace turns on Deployments with a monthly plan81 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look82 const paid = await billing.confirm(slug, viewer, session);
83 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 shell84 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85 // A plan started on the card already checked comes straight back.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar86 const plans = url.searchParams.getAll("plan");
87 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 look88
The statement is a month at a time, a line per kind of charge89 const group: "day" | "project" = url.searchParams.get("group") === "project" ? "project" : "day";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas90 const [account, statement, features, meters, limit, invoices, entitlements, requests, book] = await Promise.all([
Agents as a team: lifecycle, merge queue, billing and a new shell91 billing.account(slug, viewer),
The statement is a month at a time, a line per kind of charge92 billing.statement(slug, viewer, url.searchParams.get("month"), group),
Paid features: a workspace turns on Deployments with a monthly plan93 billing.features(slug, viewer),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas94 billing.usageMeters(slug, viewer).catch(() => null),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look95 billing.limit(slug, viewer).catch(() => null),
Two limits, real invoices, trust that grows by itself, sales signals96 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 put97 billing.entitlements(slug).catch(() => null),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look98 billing.limitRequests(slug, viewer).catch(() => null),
99 billing.prices().catch(() => null),
Agents as a team: lifecycle, merge queue, billing and a new shell100 ]);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look101 const featureStates = unwrap(features);
102 const trialMicros = book?.free?.trialWorkspaceMicros ?? DEFAULT_TRIAL_MICROS;
103 const done = url.searchParams.get("done");
Agents as a team: lifecycle, merge queue, billing and a new shell104 return {
105 slug,
106 role,
107 account: unwrap(account),
The statement is a month at a time, a line per kind of charge108 statement: unwrap(statement),
109 group,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look110 plan: featureStates.find((state) => state.plan.feature === "plan") ?? null,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar111 securityPlan: featureStates.find((state) => state.plan.feature === "security") ?? null,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas112 meters: meters?.ok ? meters.value : null,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look113 limit: limit?.ok ? limit.value : null,
Two limits, real invoices, trust that grows by itself, sales signals114 invoices: invoices?.ok ? invoices.value : [],
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put115 entitlements,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look116 requests: requests?.ok ? requests.value : [],
117 trialMicros,
118 notice:
119 url.searchParams.has("checked") && entitlements
120 ? cardCheckResult(entitlements, trialMicros)
121 : done && DONE[done]
122 ? DONE[done]
123 : null,
124 problem: url.searchParams.get("problem"),
Agents as a team: lifecycle, merge queue, billing and a new shell125 };
126}
127
128export async function action({ request, params, context }: Route.ActionArgs) {
129 assertSameOrigin(request);
130 const user = requireUser(context, request);
131 const form = await request.formData();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look132 const slug = params.owner.toLowerCase();
133 // Spelled out: a form post arrives at a data address, not the page's.
134 const here = `${new URL(request.url).origin}/${slug}/-/billing`;
135 const intent = String(form.get("intent") ?? "");
136 const fail = (section: string, error: string) => data<SectionError>({ section, error }, { status: 422 });
137 const done = (key: string, anchor = "") => redirect(`/${slug}/-/billing?done=${key}${anchor}`);
138
139 switch (intent) {
140 case "portal": {
141 // Card, invoices and billing details live on Stripe's own page.
142 const started = await billing.billingPortal(user, slug, here);
143 if (!started.ok) return fail("plan", started.error.message);
144 throw redirect(started.value.url);
145 }
146 case "subscribe": {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar147 // The plan, or the Security and quality activation.
148 const feature = form.get("feature") === "security" ? "security" : "plan";
149 const started = await billing.subscribe(user, slug, feature, `${here}?plan=${feature}`);
150 if (!started.ok) return fail(feature, started.error.message);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look151 throw redirect(started.value.url);
152 }
153 case "cancel":
154 case "resume": {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar155 const feature = form.get("feature") === "security" ? "security" : "plan";
156 const changed = await billing.cancelSubscription(user, slug, feature, intent === "resume");
157 if (!changed.ok) return fail(feature, changed.error.message);
158 const prefix = feature === "security" ? "security_" : "";
159 throw done(`${prefix}${intent === "resume" ? "resumed" : "canceled"}`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look160 }
161 case "card-check": {
162 const started = await billing.cardCheck(user, slug, here);
163 if (!started.ok) return fail("trial", started.error.message);
164 throw redirect(started.value.url);
165 }
166 case "spend-limit": {
167 const parsed = parseSpendLimit({ mode: form.get("mode"), limit: form.get("limit") });
168 if (!parsed.ok) return fail("limit", parsed.error);
169 let micros = parsed.value.micros;
170 if (parsed.value.raiseOnce && micros == null) {
171 // The one-time raise with no amount: as high as it goes.
172 const limit = await billing.limit(slug, user);
173 micros = limit.ok ? (limit.value.raiseOnceMicros ?? null) : null;
174 if (micros == null) return fail("limit", "The one-time raise is not available. Prepay, or use Raise my limit.");
175 }
176 const set = await billing.setSpendLimit(user, slug, micros, parsed.value.useFull, parsed.value.raiseOnce);
177 if (!set.ok) return fail("limit", set.error.message);
178 throw done("limit", "#limit");
179 }
180 case "request": {
181 const parsed = parseLimitRequest({
182 kind: form.get("kind"),
183 amount: form.get("amount"),
184 reason: form.get("reason"),
185 expected: form.get("expected"),
186 });
187 const section = String(form.get("kind")) === "overage" ? "overage" : "raise";
188 if (!parsed.ok) return fail(section, parsed.error);
189 const sent = await billing.requestLimit(user, slug, parsed.value);
190 if (!sent.ok) return fail(section, sent.error.message);
191 throw done(section === "overage" ? "told" : "requested", `#${section}`);
192 }
193 case "caps": {
194 const parsed = parseCaps({ run: form.get("run"), issue: form.get("issue") });
195 if (!parsed.ok) return fail("caps", parsed.error);
196 const set = await billing.setCaps(user, slug, parsed.value);
197 if (!set.ok) return fail("caps", set.error.message);
198 throw done("caps", "#caps");
199 }
200 case "spike": {
201 const keep = String(form.get("decision")) === "keep";
202 const answered = await billing.confirmSpike(user, slug, keep);
203 if (!answered.ok) return fail("spike", answered.error.message);
204 throw done(keep ? "keep" : "stop");
Usage limits: unpaid usage can only go so far205 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look206 case "prepay": {
207 const parsed = parsePrepay({ amount: form.get("amount"), custom: form.get("custom"), method: form.get("method") });
208 if (!parsed.ok) return fail("prepay", parsed.error);
209 const started = await billing.checkout(user, slug, parsed.value.amountCents, here, parsed.value.method);
210 if (!started.ok) return fail("prepay", started.error.message);
211 throw redirect(started.value.url);
Paid features: a workspace turns on Deployments with a monthly plan212 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look213 default:
214 return fail("plan", "Unknown action.");
Paid features: a workspace turns on Deployments with a monthly plan215 }
Agents as a team: lifecycle, merge queue, billing and a new shell216}
217
218export default function WorkspaceBilling({ loaderData, actionData }: Route.ComponentProps) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar219 const { slug, role, account, statement, group, plan, securityPlan, meters, limit, invoices, entitlements, requests, trialMicros, notice, problem } =
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put220 loaderData;
Agents as a team: lifecycle, merge queue, billing and a new shell221 const { status } = account;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look222 const owner = role === "owner";
223 const error = actionData as SectionError;
224 const err = (section: string) => (error && error.section === section ? error.error : undefined);
225 const standing = planStatus(plan, entitlements);
226 const paying = standing.kind === "paid" || standing.kind === "canceling" || standing.kind === "past_due";
227 const free = standing.kind === "free" || standing.kind === "trial";
228 const prepaid = limit?.prepaidMicros ?? entitlements?.prepaidMicros ?? 0;
229
Agents as a team: lifecycle, merge queue, billing and a new shell230 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look231 <div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_20rem]">
Agents as a team: lifecycle, merge queue, billing and a new shell232 <div className="min-w-0">
Free while g1t is being built out; agents can check out their own forks233 {status.free && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look234 <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 forks235 <h2 className="font-medium">Free while g1t is being built out</h2>
236 <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 look237 Runs are recorded with what they cost, so Usage shows what you use, but nothing is charged for now. We will say so well
238 before anything is.
Free while g1t is being built out; agents can check out their own forks239 </p>
240 </div>
241 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242 {notice && <p className="mb-6 rounded-lg border border-accent/30 bg-accent/5 px-4 py-2.5 text-sm">{notice}</p>}
243 {problem && <p className="mb-6 rounded-lg border border-danger/40 bg-danger/5 px-4 py-2.5 text-sm">{problem}</p>}
244
245 {entitlements && <SpikeBanner slug={slug} entitlements={entitlements} owner={owner} />}
246 {err("spike") && <p className="mb-6 text-sm text-danger">{err("spike")}</p>}
247 <Alerts alerts={entitlements?.alerts ?? []} />
248
249 <PlanCard
250 state={plan}
251 status={standing}
252 entitlements={entitlements}
253 owner={owner}
254 enabled={status.enabled}
255 live={status.live}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas256 meters={meters}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look257 error={err("plan")}
258 />
259
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar260 <SecurityActivationCard state={securityPlan} owner={owner} enabled={status.enabled} error={err("security")} />
261
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look262 {free && status.enabled && (
Merge branch 'worktree-agent-a985c01f43538af01'263 <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 owner264 )}
265
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look266 {paying && limit && limit.trust !== "internal" && (
267 <>
Merge branch 'worktree-agent-a985c01f43538af01'268 <SpendLimitCard limit={limit} owner={owner} error={err("limit")} />
269 <RaiseCard requests={requests} owner={owner} error={err("raise")} />
270 <PrepayCard prepaidMicros={prepaid} owner={owner} live={status.live} error={err("prepay")} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look271 </>
272 )}
273
274 {entitlements && (paying || standing.kind === "trial" || standing.kind === "comped" || standing.kind === "enterprise") && (
Merge branch 'worktree-agent-a985c01f43538af01'275 <CapsCard entitlements={entitlements} owner={owner} error={err("caps")} />
Usage limits: unpaid usage can only go so far276 )}
277
Merge branch 'worktree-agent-a985c01f43538af01'278 {paying && <OverageCard requests={requests} owner={owner} error={err("overage")} />}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look279
280 {status.enabled && (
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace281 <section className="mb-10 rounded-xl border border-line bg-surface p-5">
282 <div className="flex flex-wrap items-center justify-between gap-3">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look283 <div className="min-w-0">
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace284 <h2 className="font-medium">Card and invoices</h2>
285 {account.card ? (
286 <p className="mt-1 text-sm">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look287 <span className="capitalize">{account.card.brand}</span> ending <span className="font-mono">{account.card.last4}</span>
288 <span className="text-muted"> · expires {String(account.card.expMonth).padStart(2, "0")}/{account.card.expYear}</span>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace289 </p>
290 ) : (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look291 <p className="mt-1 max-w-xl text-sm text-muted">No card yet. The plan, the trial and prepaying each ask for one on Stripe.</p>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace292 )}
293 <p className="mt-2 max-w-xl text-xs text-faint">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look294 Cards, invoices, receipts and the billing email, address and tax ID are on Stripe's billing page. g1t never sees card
295 numbers.
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace296 </p>
297 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look298 {owner && (
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace299 <Form method="post">
Merge branch 'worktree-agent-a985c01f43538af01'300 <SubmitButton variant="quiet" name="intent" value="portal" pending="Opening Stripe…">
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace301 <CreditCard size={14} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look302 Open Stripe billing
Merge branch 'worktree-agent-a985c01f43538af01'303 </SubmitButton>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace304 </Form>
305 )}
306 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look307 {!status.live && (
308 <p className="mt-3 text-xs text-faint">Test mode: use card 4242 4242 4242 4242, any future date and code.</p>
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace309 )}
310 </section>
311 )}
312
Two limits, real invoices, trust that grows by itself, sales signals313 {invoices.length > 0 && <InvoiceList invoices={invoices} />}
Agents as a team: lifecycle, merge queue, billing and a new shell314
The statement is a month at a time, a line per kind of charge315 <StatementView slug={slug} statement={statement} group={group} />
Agents as a team: lifecycle, merge queue, billing and a new shell316 </div>
317
318 <aside className="space-y-5 text-sm">
319 <section className="rounded-xl border border-line bg-surface p-5">
320 <h3 className="font-medium">How it is charged</h3>
321 <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted">
322 <li>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look323 The plan is {wholeDollars((plan?.plan.monthlyCents ?? 2000) * 10_000)} a month for the workspace. Usage is charged at what it
324 costs g1t plus {account.marginPercent}%, from the first second, after what is included.
Agents as a team: lifecycle, merge queue, billing and a new shell325 </li>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look326 <li>What paid first is on each statement line: the plan's included usage, the trial, the open-source pool, or g1t.</li>
Agents as a team: lifecycle, merge queue, billing and a new shell327 <li>
Integrations: your own model provider, alerts that open issues, tickets agents read328 With your own model provider, connected under{" "}
329 <Link to={`/${slug}/-/integrations`} className="text-fg hover:underline">
330 Integrations
331 </Link>
Prices are what g1t pays plus 20%, from the first second332 , 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 shell333 </li>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put334 <li>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look335 Each month closes with an itemised invoice. No card is charged less than{" "}
336 {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 put337 </li>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look338 <li>Alerts at 50, 75, 90 and 100% of the included usage and your limits, here and by email.</li>
A flat project menu with Code first, views as tabs, and Stripe for every owner339 <li>No seats: add as many people and agents as you like.</li>
Agents as a team: lifecycle, merge queue, billing and a new shell340 </ul>
A flat project menu with Code first, views as tabs, and Stripe for every owner341 <div className="mt-4 space-y-1.5 border-t border-line pt-4 text-sm">
342 <Link to={`/${slug}/-/usage`} className="flex items-center gap-2 text-muted hover:text-fg">
343 <FileText size={14} /> Usage, run by run
344 </Link>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look345 <Link to="/pricing" className="flex items-center gap-2 text-muted hover:text-fg">
346 <CreditCard size={14} /> Pricing and today's prices
347 </Link>
348 <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 owner349 <ArrowUpRight size={14} /> How usage and billing work
350 </a>
351 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell352 </section>
353 </aside>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put354 </div>
355 );
356}
357
Two limits, real invoices, trust that grows by itself, sales signals358/** The workspace's invoices from g1t, each kept on Stripe with its PDF. */
359function InvoiceList({ invoices }: { invoices: WorkspaceInvoice[] }) {
360 return (
361 <section className="mb-10">
362 <h2 className="font-medium">Invoices</h2>
363 <p className="mt-1 text-sm text-muted">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look364 One when each month closes, and one each time g1t charges the card near your limit. Receipts and PDFs are also on Stripe's
365 billing page.
Two limits, real invoices, trust that grows by itself, sales signals366 </p>
367 <ul className="mt-4 divide-y divide-line overflow-hidden rounded-xl border border-line">
368 {invoices.map((invoice) => (
369 <li key={invoice.invoiceId} className="px-4 py-3 text-sm">
370 <div className="flex flex-wrap items-center gap-3">
371 <span className="font-medium">
372 {invoice.reason === "month" ? `Usage for ${invoice.period}` : `Charged near the limit, ${invoice.period}`}
373 </span>
374 <span
375 className={`rounded-full border px-2 py-0.5 text-xs ${
376 invoice.status === "paid" ? "border-accent/40 text-accent" : "border-danger/40 text-danger"
377 }`}
378 >
379 {invoice.status === "paid" ? "Paid" : invoice.status === "failed" ? "Payment failed" : invoice.status}
380 </span>
381 <span className="ml-auto font-mono tabular-nums">{dollars(invoice.amountMicros)}</span>
382 {invoice.hostedUrl && (
383 <a href={invoice.hostedUrl} className="text-xs text-muted hover:text-fg">
384 View
385 </a>
386 )}
387 {invoice.pdfUrl && (
388 <a href={invoice.pdfUrl} className="text-xs text-muted hover:text-fg">
389 PDF
390 </a>
391 )}
392 </div>
393 <ul className="mt-1.5 space-y-0.5 text-xs text-faint">
394 {invoice.lines.map((line) => (
395 <li key={line.description} className="flex justify-between gap-4">
396 <span>{line.description}</span>
397 <span className="font-mono tabular-nums">{dollars(line.amountMicros)}</span>
398 </li>
399 ))}
400 </ul>
401 </li>
402 ))}
403 </ul>
404 </section>
405 );
406}

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