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 look | 1 | import { ArrowUpRight, CreditCard, FileText } from "lucide-react"; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 2 | import { Form, Link, data, redirect, useNavigation } from "react-router"; |
| 3 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 4 | import { MICROS_PER_DOLLAR, type WorkspaceInvoice } from "@g1t/contracts"; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 5 | |
| 6 | import type { Route } from "./+types/billing"; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 7 | import { |
| 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 charge | 19 | import { StatementView } from "../../components/statement"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 20 | import { Button } from "../../components/ui"; |
| 21 | import { | |
| 22 | cardCheckResult, | |
| 23 | dollars, | |
| 24 | parseCaps, | |
| 25 | parseLimitRequest, | |
| 26 | parsePrepay, | |
| 27 | parseSpendLimit, | |
| 28 | planStatus, | |
| 29 | wholeDollars, | |
| 30 | } from "../../lib/billing"; | |
| 31 | import { page } from "../../lib/meta"; | |
| Deployments: a preview for every pull request, production on g1t.page | 32 | import { billing, deployments } from "../../lib/services.server"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 33 | import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server"; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 34 | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 35 | /** The trial and pools as published, when the price book cannot be read. */ |
| 36 | const DEFAULT_TRIAL_MICROS = 5_000_000; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 37 | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 38 | /** What a change says once it is done (`?done=`). */ |
| 39 | const DONE: Record<string, string> = { | |
| 40 | subscribed: "The g1t plan is on.", | |
| 41 | prepaid: "Payment received. Your prepaid balance and what you can use went up by as much.", | |
| 42 | limit: "Spend limit saved.", | |
| 43 | caps: "Caps saved. They apply to runs that start from now on.", | |
| 44 | requested: "Sent. g1t answers within one business day, here and by email.", | |
| 45 | told: "Thanks. g1t looks at it and answers within one business day, here and by email.", | |
| 46 | keep: "Compute is running again for 24 hours, unless spending doubles again first.", | |
| 47 | stop: "New compute stays paused until an owner chooses Keep going.", | |
| 48 | canceled: "The plan ends at the end of the period. Nothing more is charged for it.", | |
| 49 | resumed: "The plan continues.", | |
| 50 | }; | |
| 51 | ||
| Social cards for every page: og.g1t.sh | 52 | export function meta({ params, ...args }: Route.MetaArgs) { |
| 53 | return page(args, { title: `Billing · ${params.owner} · g1t` }); | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 54 | } |
| 55 | ||
| 56 | export async function loader({ params, context, request }: Route.LoaderArgs) { | |
| 57 | const viewer = getViewer(context); | |
| 58 | const role = roleIn(viewer, params.owner); | |
| 59 | // Members only; to anyone else the page does not exist. | |
| 60 | if (!role) throw data(null, { status: 404 }); | |
| 61 | const slug = params.owner.toLowerCase(); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 62 | const url = new URL(request.url); |
| 63 | const here = `/${slug}/-/billing`; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 64 | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 65 | // Back from Stripe: record what happened, then drop the ids from the address. |
| 66 | const cardCheck = url.searchParams.get("card_check"); | |
| 67 | if (cardCheck) { | |
| 68 | const checked = await billing.confirmCardCheck(slug, viewer, cardCheck); | |
| 69 | throw redirect(checked.ok ? `${here}?checked=1#trial` : `${here}?problem=${encodeURIComponent(checked.error.message)}#trial`); | |
| 70 | } | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 71 | const session = url.searchParams.get("session"); |
| 72 | if (session) { | |
| Paid features: a workspace turns on Deployments with a monthly plan | 73 | if (url.searchParams.get("plan")) { |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 74 | const started = await billing.confirmSubscription(slug, viewer, session); |
| 75 | throw redirect(started.ok ? `${here}?done=subscribed` : `${here}?problem=${encodeURIComponent(started.error.message)}`); | |
| Paid features: a workspace turns on Deployments with a monthly plan | 76 | } |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 77 | const paid = await billing.confirm(slug, viewer, session); |
| 78 | 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 shell | 79 | } |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 80 | // A plan started on the card already checked comes straight back. |
| 81 | if (url.searchParams.get("plan") === "started") throw redirect(`${here}?done=subscribed`); | |
| 82 | ||
| The statement is a month at a time, a line per kind of charge | 83 | const group: "day" | "project" = url.searchParams.get("group") === "project" ? "project" : "day"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 84 | const [account, statement, features, deployUsage, limit, invoices, entitlements, requests, book] = await Promise.all([ |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 85 | billing.account(slug, viewer), |
| The statement is a month at a time, a line per kind of charge | 86 | billing.statement(slug, viewer, url.searchParams.get("month"), group), |
| Paid features: a workspace turns on Deployments with a monthly plan | 87 | billing.features(slug, viewer), |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 88 | deployments.usage(slug, viewer).catch(() => null), |
| 89 | billing.limit(slug, viewer).catch(() => null), | |
| Two limits, real invoices, trust that grows by itself, sales signals | 90 | 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 put | 91 | billing.entitlements(slug).catch(() => null), |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 92 | billing.limitRequests(slug, viewer).catch(() => null), |
| 93 | billing.prices().catch(() => null), | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 94 | ]); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 95 | const featureStates = unwrap(features); |
| 96 | const trialMicros = book?.free?.trialWorkspaceMicros ?? DEFAULT_TRIAL_MICROS; | |
| 97 | const done = url.searchParams.get("done"); | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 98 | return { |
| 99 | slug, | |
| 100 | role, | |
| 101 | account: unwrap(account), | |
| The statement is a month at a time, a line per kind of charge | 102 | statement: unwrap(statement), |
| 103 | group, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 104 | plan: featureStates.find((state) => state.plan.feature === "plan") ?? null, |
| 105 | deployUsage: deployUsage?.ok ? deployUsage.value : null, | |
| 106 | limit: limit?.ok ? limit.value : null, | |
| Two limits, real invoices, trust that grows by itself, sales signals | 107 | invoices: invoices?.ok ? invoices.value : [], |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 108 | entitlements, |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 109 | requests: requests?.ok ? requests.value : [], |
| 110 | trialMicros, | |
| 111 | notice: | |
| 112 | url.searchParams.has("checked") && entitlements | |
| 113 | ? cardCheckResult(entitlements, trialMicros) | |
| 114 | : done && DONE[done] | |
| 115 | ? DONE[done] | |
| 116 | : null, | |
| 117 | problem: url.searchParams.get("problem"), | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 118 | }; |
| 119 | } | |
| 120 | ||
| 121 | export async function action({ request, params, context }: Route.ActionArgs) { | |
| 122 | assertSameOrigin(request); | |
| 123 | const user = requireUser(context, request); | |
| 124 | const form = await request.formData(); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 125 | const slug = params.owner.toLowerCase(); |
| 126 | // Spelled out: a form post arrives at a data address, not the page's. | |
| 127 | const here = `${new URL(request.url).origin}/${slug}/-/billing`; | |
| 128 | const intent = String(form.get("intent") ?? ""); | |
| 129 | const fail = (section: string, error: string) => data<SectionError>({ section, error }, { status: 422 }); | |
| 130 | const done = (key: string, anchor = "") => redirect(`/${slug}/-/billing?done=${key}${anchor}`); | |
| 131 | ||
| 132 | switch (intent) { | |
| 133 | case "portal": { | |
| 134 | // Card, invoices and billing details live on Stripe's own page. | |
| 135 | const started = await billing.billingPortal(user, slug, here); | |
| 136 | if (!started.ok) return fail("plan", started.error.message); | |
| 137 | throw redirect(started.value.url); | |
| 138 | } | |
| 139 | case "subscribe": { | |
| 140 | const started = await billing.subscribe(user, slug, "plan", `${here}?plan=plan`); | |
| 141 | if (!started.ok) return fail("plan", started.error.message); | |
| 142 | throw redirect(started.value.url); | |
| 143 | } | |
| 144 | case "cancel": | |
| 145 | case "resume": { | |
| 146 | const changed = await billing.cancelSubscription(user, slug, "plan", intent === "resume"); | |
| 147 | if (!changed.ok) return fail("plan", changed.error.message); | |
| 148 | throw done(intent === "resume" ? "resumed" : "canceled"); | |
| 149 | } | |
| 150 | case "card-check": { | |
| 151 | const started = await billing.cardCheck(user, slug, here); | |
| 152 | if (!started.ok) return fail("trial", started.error.message); | |
| 153 | throw redirect(started.value.url); | |
| 154 | } | |
| 155 | case "spend-limit": { | |
| 156 | const parsed = parseSpendLimit({ mode: form.get("mode"), limit: form.get("limit") }); | |
| 157 | if (!parsed.ok) return fail("limit", parsed.error); | |
| 158 | let micros = parsed.value.micros; | |
| 159 | if (parsed.value.raiseOnce && micros == null) { | |
| 160 | // The one-time raise with no amount: as high as it goes. | |
| 161 | const limit = await billing.limit(slug, user); | |
| 162 | micros = limit.ok ? (limit.value.raiseOnceMicros ?? null) : null; | |
| 163 | if (micros == null) return fail("limit", "The one-time raise is not available. Prepay, or use Raise my limit."); | |
| 164 | } | |
| 165 | const set = await billing.setSpendLimit(user, slug, micros, parsed.value.useFull, parsed.value.raiseOnce); | |
| 166 | if (!set.ok) return fail("limit", set.error.message); | |
| 167 | throw done("limit", "#limit"); | |
| 168 | } | |
| 169 | case "request": { | |
| 170 | const parsed = parseLimitRequest({ | |
| 171 | kind: form.get("kind"), | |
| 172 | amount: form.get("amount"), | |
| 173 | reason: form.get("reason"), | |
| 174 | expected: form.get("expected"), | |
| 175 | }); | |
| 176 | const section = String(form.get("kind")) === "overage" ? "overage" : "raise"; | |
| 177 | if (!parsed.ok) return fail(section, parsed.error); | |
| 178 | const sent = await billing.requestLimit(user, slug, parsed.value); | |
| 179 | if (!sent.ok) return fail(section, sent.error.message); | |
| 180 | throw done(section === "overage" ? "told" : "requested", `#${section}`); | |
| 181 | } | |
| 182 | case "caps": { | |
| 183 | const parsed = parseCaps({ run: form.get("run"), issue: form.get("issue") }); | |
| 184 | if (!parsed.ok) return fail("caps", parsed.error); | |
| 185 | const set = await billing.setCaps(user, slug, parsed.value); | |
| 186 | if (!set.ok) return fail("caps", set.error.message); | |
| 187 | throw done("caps", "#caps"); | |
| 188 | } | |
| 189 | case "spike": { | |
| 190 | const keep = String(form.get("decision")) === "keep"; | |
| 191 | const answered = await billing.confirmSpike(user, slug, keep); | |
| 192 | if (!answered.ok) return fail("spike", answered.error.message); | |
| 193 | throw done(keep ? "keep" : "stop"); | |
| Usage limits: unpaid usage can only go so far | 194 | } |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 195 | case "prepay": { |
| 196 | const parsed = parsePrepay({ amount: form.get("amount"), custom: form.get("custom"), method: form.get("method") }); | |
| 197 | if (!parsed.ok) return fail("prepay", parsed.error); | |
| 198 | const started = await billing.checkout(user, slug, parsed.value.amountCents, here, parsed.value.method); | |
| 199 | if (!started.ok) return fail("prepay", started.error.message); | |
| 200 | throw redirect(started.value.url); | |
| Paid features: a workspace turns on Deployments with a monthly plan | 201 | } |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 202 | default: |
| 203 | return fail("plan", "Unknown action."); | |
| Paid features: a workspace turns on Deployments with a monthly plan | 204 | } |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 205 | } |
| 206 | ||
| 207 | export default function WorkspaceBilling({ loaderData, actionData }: Route.ComponentProps) { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 208 | const { slug, role, account, statement, group, plan, deployUsage, 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 put | 209 | loaderData; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 210 | const { status } = account; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 211 | const owner = role === "owner"; |
| 212 | const busy = useNavigation().state === "submitting"; | |
| 213 | const error = actionData as SectionError; | |
| 214 | const err = (section: string) => (error && error.section === section ? error.error : undefined); | |
| 215 | const standing = planStatus(plan, entitlements); | |
| 216 | const paying = standing.kind === "paid" || standing.kind === "canceling" || standing.kind === "past_due"; | |
| 217 | const free = standing.kind === "free" || standing.kind === "trial"; | |
| 218 | const prepaid = limit?.prepaidMicros ?? entitlements?.prepaidMicros ?? 0; | |
| 219 | ||
| Agents as a team: lifecycle, merge queue, billing and a new shell | 220 | return ( |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 221 | <div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_20rem]"> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 222 | <div className="min-w-0"> |
| Free while g1t is being built out; agents can check out their own forks | 223 | {status.free && ( |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 224 | <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 forks | 225 | <h2 className="font-medium">Free while g1t is being built out</h2> |
| 226 | <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 look | 227 | Runs are recorded with what they cost, so Usage shows what you use, but nothing is charged for now. We will say so well |
| 228 | before anything is. | |
| Free while g1t is being built out; agents can check out their own forks | 229 | </p> |
| 230 | </div> | |
| 231 | )} | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 232 | {notice && <p className="mb-6 rounded-lg border border-accent/30 bg-accent/5 px-4 py-2.5 text-sm">{notice}</p>} |
| 233 | {problem && <p className="mb-6 rounded-lg border border-danger/40 bg-danger/5 px-4 py-2.5 text-sm">{problem}</p>} | |
| 234 | ||
| 235 | {entitlements && <SpikeBanner slug={slug} entitlements={entitlements} owner={owner} />} | |
| 236 | {err("spike") && <p className="mb-6 text-sm text-danger">{err("spike")}</p>} | |
| 237 | <Alerts alerts={entitlements?.alerts ?? []} /> | |
| 238 | ||
| 239 | <PlanCard | |
| 240 | state={plan} | |
| 241 | status={standing} | |
| 242 | entitlements={entitlements} | |
| 243 | owner={owner} | |
| 244 | enabled={status.enabled} | |
| 245 | live={status.live} | |
| 246 | busy={busy} | |
| 247 | deployUsage={deployUsage} | |
| 248 | error={err("plan")} | |
| 249 | /> | |
| 250 | ||
| 251 | {free && status.enabled && ( | |
| 252 | <TrialCard entitlements={entitlements} trialMicros={trialMicros} owner={owner} enabled={status.enabled} busy={busy} error={err("trial")} /> | |
| A flat project menu with Code first, views as tabs, and Stripe for every owner | 253 | )} |
| 254 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 255 | {paying && limit && limit.trust !== "internal" && ( |
| 256 | <> | |
| 257 | <SpendLimitCard limit={limit} owner={owner} busy={busy} error={err("limit")} /> | |
| 258 | <RaiseCard requests={requests} owner={owner} busy={busy} error={err("raise")} /> | |
| 259 | <PrepayCard prepaidMicros={prepaid} owner={owner} live={status.live} busy={busy} error={err("prepay")} /> | |
| 260 | </> | |
| 261 | )} | |
| 262 | ||
| 263 | {entitlements && (paying || standing.kind === "trial" || standing.kind === "comped" || standing.kind === "enterprise") && ( | |
| 264 | <CapsCard entitlements={entitlements} owner={owner} busy={busy} error={err("caps")} /> | |
| Usage limits: unpaid usage can only go so far | 265 | )} |
| 266 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 267 | {paying && <OverageCard requests={requests} owner={owner} busy={busy} error={err("overage")} />} |
| 268 | ||
| 269 | {status.enabled && ( | |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 270 | <section className="mb-10 rounded-xl border border-line bg-surface p-5"> |
| 271 | <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 look | 272 | <div className="min-w-0"> |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 273 | <h2 className="font-medium">Card and invoices</h2> |
| 274 | {account.card ? ( | |
| 275 | <p className="mt-1 text-sm"> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 276 | <span className="capitalize">{account.card.brand}</span> ending <span className="font-mono">{account.card.last4}</span> |
| 277 | <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 workspace | 278 | </p> |
| 279 | ) : ( | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 280 | <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 workspace | 281 | )} |
| 282 | <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 look | 283 | Cards, invoices, receipts and the billing email, address and tax ID are on Stripe's billing page. g1t never sees card |
| 284 | numbers. | |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 285 | </p> |
| 286 | </div> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 287 | {owner && ( |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 288 | <Form method="post"> |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 289 | <Button variant="quiet" type="submit" name="intent" value="portal" disabled={busy}> |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 290 | <CreditCard size={14} /> |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 291 | Open Stripe billing |
| Billing on Stripe's pages, month-end charges, warnings; sudo by workspace | 292 | </Button> |
| 293 | </Form> | |
| 294 | )} | |
| 295 | </div> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 296 | {!status.live && ( |
| 297 | <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 workspace | 298 | )} |
| 299 | </section> | |
| 300 | )} | |
| 301 | ||
| Two limits, real invoices, trust that grows by itself, sales signals | 302 | {invoices.length > 0 && <InvoiceList invoices={invoices} />} |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 303 | |
| The statement is a month at a time, a line per kind of charge | 304 | <StatementView slug={slug} statement={statement} group={group} /> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 305 | </div> |
| 306 | ||
| 307 | <aside className="space-y-5 text-sm"> | |
| 308 | <section className="rounded-xl border border-line bg-surface p-5"> | |
| 309 | <h3 className="font-medium">How it is charged</h3> | |
| 310 | <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted"> | |
| 311 | <li> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 312 | The plan is {wholeDollars((plan?.plan.monthlyCents ?? 2000) * 10_000)} a month for the workspace. Usage is charged at what it |
| 313 | costs g1t plus {account.marginPercent}%, from the first second, after what is included. | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 314 | </li> |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 315 | <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 shell | 316 | <li> |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 317 | With your own model provider, connected under{" "} |
| 318 | <Link to={`/${slug}/-/integrations`} className="text-fg hover:underline"> | |
| 319 | Integrations | |
| 320 | </Link> | |
| Prices are what g1t pays plus 20%, from the first second | 321 | , 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 shell | 322 | </li> |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 323 | <li> |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 324 | Each month closes with an itemised invoice. No card is charged less than{" "} |
| 325 | {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 put | 326 | </li> |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 327 | <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 owner | 328 | <li>No seats: add as many people and agents as you like.</li> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 329 | </ul> |
| A flat project menu with Code first, views as tabs, and Stripe for every owner | 330 | <div className="mt-4 space-y-1.5 border-t border-line pt-4 text-sm"> |
| 331 | <Link to={`/${slug}/-/usage`} className="flex items-center gap-2 text-muted hover:text-fg"> | |
| 332 | <FileText size={14} /> Usage, run by run | |
| 333 | </Link> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 334 | <Link to="/pricing" className="flex items-center gap-2 text-muted hover:text-fg"> |
| 335 | <CreditCard size={14} /> Pricing and today's prices | |
| 336 | </Link> | |
| 337 | <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 owner | 338 | <ArrowUpRight size={14} /> How usage and billing work |
| 339 | </a> | |
| 340 | </div> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 341 | </section> |
| 342 | </aside> | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 343 | </div> |
| 344 | ); | |
| 345 | } | |
| 346 | ||
| Two limits, real invoices, trust that grows by itself, sales signals | 347 | /** The workspace's invoices from g1t, each kept on Stripe with its PDF. */ |
| 348 | function InvoiceList({ invoices }: { invoices: WorkspaceInvoice[] }) { | |
| 349 | return ( | |
| 350 | <section className="mb-10"> | |
| 351 | <h2 className="font-medium">Invoices</h2> | |
| 352 | <p className="mt-1 text-sm text-muted"> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 353 | One when each month closes, and one each time g1t charges the card near your limit. Receipts and PDFs are also on Stripe's |
| 354 | billing page. | |
| Two limits, real invoices, trust that grows by itself, sales signals | 355 | </p> |
| 356 | <ul className="mt-4 divide-y divide-line overflow-hidden rounded-xl border border-line"> | |
| 357 | {invoices.map((invoice) => ( | |
| 358 | <li key={invoice.invoiceId} className="px-4 py-3 text-sm"> | |
| 359 | <div className="flex flex-wrap items-center gap-3"> | |
| 360 | <span className="font-medium"> | |
| 361 | {invoice.reason === "month" ? `Usage for ${invoice.period}` : `Charged near the limit, ${invoice.period}`} | |
| 362 | </span> | |
| 363 | <span | |
| 364 | className={`rounded-full border px-2 py-0.5 text-xs ${ | |
| 365 | invoice.status === "paid" ? "border-accent/40 text-accent" : "border-danger/40 text-danger" | |
| 366 | }`} | |
| 367 | > | |
| 368 | {invoice.status === "paid" ? "Paid" : invoice.status === "failed" ? "Payment failed" : invoice.status} | |
| 369 | </span> | |
| 370 | <span className="ml-auto font-mono tabular-nums">{dollars(invoice.amountMicros)}</span> | |
| 371 | {invoice.hostedUrl && ( | |
| 372 | <a href={invoice.hostedUrl} className="text-xs text-muted hover:text-fg"> | |
| 373 | View | |
| 374 | </a> | |
| 375 | )} | |
| 376 | {invoice.pdfUrl && ( | |
| 377 | <a href={invoice.pdfUrl} className="text-xs text-muted hover:text-fg"> | |
| 378 | ||
| 379 | </a> | |
| 380 | )} | |
| 381 | </div> | |
| 382 | <ul className="mt-1.5 space-y-0.5 text-xs text-faint"> | |
| 383 | {invoice.lines.map((line) => ( | |
| 384 | <li key={line.description} className="flex justify-between gap-4"> | |
| 385 | <span>{line.description}</span> | |
| 386 | <span className="font-mono tabular-nums">{dollars(line.amountMicros)}</span> | |
| 387 | </li> | |
| 388 | ))} | |
| 389 | </ul> | |
| 390 | </li> | |
| 391 | ))} | |
| 392 | </ul> | |
| 393 | </section> | |
| 394 | ); | |
| 395 | } |