flagon-io/g1t

public

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

g1t/apps/web/app/routes/pricing.tsx

356 lines16,503 bytesCodeBlame
1import { ArrowUpRight } from "lucide-react";
2
3import { DEPLOYMENTS_ALLOWANCE, type FeaturePlan, type FreeTier, MICROS_PER_DOLLAR, type Price } from "@g1t/contracts";
4
5import type { Route } from "./+types/pricing";
6import { page } from "../lib/meta";
7import { TimeAgo } from "../components/ui";
8import { billing } from "../lib/services.server";
9
10export function meta(args: Route.MetaArgs) {
11 return page(args, {
12 title: "Pricing · g1t",
13 description:
14 "g1t passes its costs through: what Cloudflare and model providers charge g1t, plus 20%. Public repositories are free. No seats, ever.",
15 });
16}
17
18export async function loader() {
19 const [book, status] = await Promise.all([billing.prices().catch(() => null), billing.status().catch(() => null)]);
20 return { book, free: status?.free ?? false };
21}
22
23/** A price in dollars, with as many digits as it needs to say anything. */
24function money(micros: number): string {
25 const dollars = micros / MICROS_PER_DOLLAR;
26 if (dollars >= 1) return `$${dollars.toFixed(2)}`;
27 if (dollars >= 0.01) return `$${dollars.toFixed(3)}`;
28 return `$${dollars.toPrecision(2)}`;
29}
30
31/** Per second is easier to read per minute. */
32function perUnit(price: Price, micros: number): string {
33 return price.unit === "second" ? `${money(micros * 60)} a minute` : `${money(micros)} per ${price.unit}`;
34}
35
36/** Whole dollars when they are whole. */
37function dollars(micros: number): string {
38 const d = micros / MICROS_PER_DOLLAR;
39 return Number.isInteger(d) ? `$${d}` : `$${d.toFixed(2)}`;
40}
41
42function gigabytes(bytes: number): string {
43 return `${Math.round((bytes / 1e9) * 10) / 10} GB`;
44}
45
46/** What the page says when billing cannot be reached: the published defaults. */
47const DEFAULT_FREE: FreeTier = {
48 trialWorkspaceMicros: 1_000_000,
49 trialMonthlyPoolMicros: 40_000_000,
50 ossPoolMicros: 10_000_000,
51 ossRepoMicros: 1_000_000,
52 freePrivateStorageBytes: 1_000_000_000,
53 auditRetentionDays: 30,
54 minChargeMicros: 5_000_000,
55};
56
57const DEFAULT_PLANS: FeaturePlan[] = [
58 {
59 feature: "team",
60 title: "Team",
61 monthlyCents: 2000,
62 includes: [
63 "$5 of usage credit each month, drawn first by the month's usage at cost plus 20%. Unused credit does not roll over.",
64 "50 GB of private repository storage, rather than 1 GB",
65 "The audit log kept for 1 year, rather than 30 days",
66 "Everyone in the workspace, at one price: never per person",
67 ],
68 overage: "Usage past the credit is charged as it is without the plan: at cost plus 20%.",
69 },
70 {
71 feature: "deployments",
72 title: "Deployments",
73 monthlyCents: 500,
74 includes: [
75 `${DEPLOYMENTS_ALLOWANCE.apps} apps deployed at once, production and previews together`,
76 `${DEPLOYMENTS_ALLOWANCE.buildSeconds / 60} build minutes`,
77 `${DEPLOYMENTS_ALLOWANCE.requests / 1e6} million requests`,
78 `${DEPLOYMENTS_ALLOWANCE.cpuMs / 1e6} million CPU milliseconds`,
79 `${DEPLOYMENTS_ALLOWANCE.customDomains} custom domains, with certificates`,
80 ],
81 overage: "Usage past that is charged at Cloudflare's price plus 20%.",
82 },
83];
84
85/** The ways to reach g1t about an enterprise account. */
86const ENTERPRISE_MAIL = "mailto:billing@g1t.sh?subject=Enterprise%20billing%20for%20g1t";
87
88const HOW = [
89 {
90 title: "Our cost, passed through",
91 body: "Every sandbox second, build, app request and model token costs g1t money at Cloudflare or a model provider. Each is metered and charged at that cost plus 20%, from the first second and the first request. Use a little, pay a little.",
92 },
93 {
94 title: "Prices follow costs, by themselves",
95 body: "Model runs are charged at what Cloudflare's AI Gateway priced each request at, so a provider's price change reaches you the same day. Every day, each Cloudflare cost is checked against what Cloudflare billed g1t; when one moves, its price moves with it, and the change is listed below.",
96 },
97 {
98 title: "The 20% is the overhead",
99 body: "It pays for running g1t and for building and keeping up the features you use. The same 20% on everything, and nothing bundled in.",
100 },
101 {
102 title: "No seats, ever",
103 body: "Add as many people and agents as you like. A workspace pays for what it uses, and for the plans it turns on, each a flat price for the whole workspace.",
104 },
105 {
106 title: "Free where something pays for it",
107 body: "Public repositories are free, and their agents' compute comes from a capped open-source pool g1t pays for. New workspaces get a small trial from a monthly budget. Nothing free is an open-ended allowance.",
108 },
109 {
110 title: "Limits that protect both of us",
111 body: "Usage not yet paid for can only go so far: $3 for a new workspace, growing with what it pays. With a card on file, g1t charges it as you near the limit, so work that is paid for never stops. Owners can set a lower limit of their own.",
112 },
113];
114
115export default function Pricing({ loaderData }: Route.ComponentProps) {
116 const { book, free } = loaderData;
117 const checked = book?.prices.map((p) => p.checkedAt).filter((at): at is string => !!at).sort().at(-1);
118 const tier = book?.free ?? DEFAULT_FREE;
119 const plans = book?.plans?.length ? book.plans : DEFAULT_PLANS;
120 return (
121 <main className="mx-auto max-w-4xl px-4 py-12">
122 <p className="text-sm font-medium text-accent">Pricing</p>
123 <h1 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">What it costs us, plus 20%</h1>
124 <p className="mt-3 max-w-2xl text-muted">
125 g1t runs on Cloudflare and model providers, and passes those costs through. The numbers on this page are the
126 live price book g1t charges from.
127 </p>
128 {free && (
129 <div className="mt-5 rounded-xl border border-accent/30 bg-accent/5 px-4 py-3 text-sm">
130 <span className="font-medium">Free while g1t is being built out.</span>{" "}
131 <span className="text-muted">
132 Usage is recorded at these prices but not charged for now. Paid features, such as Deployments, are charged.
133 </span>
134 </div>
135 )}
136
137 <div className="mt-10 grid gap-4 sm:grid-cols-2">
138 {HOW.map((item) => (
139 <section key={item.title} className="rounded-xl border border-line bg-surface p-5">
140 <h2 className="font-medium">{item.title}</h2>
141 <p className="mt-1.5 text-sm text-muted">{item.body}</p>
142 </section>
143 ))}
144 </div>
145
146 <h2 className="mt-14 text-xl font-semibold tracking-tight">Usage</h2>
147 <p className="mt-1 text-sm text-muted">
148 {checked ? (
149 <>
150 Last checked against Cloudflare's bill <TimeAgo at={checked} />.
151 </>
152 ) : (
153 "Starting from Cloudflare's published prices; checked against its bill daily."
154 )}
155 </p>
156 <div className="mt-4 overflow-x-auto rounded-xl border border-line">
157 <table className="w-full min-w-[36rem] text-left text-sm">
158 <thead className="border-b border-line text-xs text-muted">
159 <tr>
160 <th className="px-4 py-2.5 font-medium">What</th>
161 <th className="px-4 py-2.5 font-medium">Costs g1t</th>
162 <th className="px-4 py-2.5 font-medium">Markup</th>
163 <th className="px-4 py-2.5 font-medium">You pay</th>
164 </tr>
165 </thead>
166 <tbody className="divide-y divide-line">
167 <tr>
168 <td className="px-4 py-3">
169 <p className="font-medium">Models</p>
170 <p className="text-xs text-faint">g1t's hosted models, through AI Gateway</p>
171 </td>
172 <td className="px-4 py-3 text-muted">What the provider charges, per request</td>
173 <td className="px-4 py-3 tabular-nums">{book?.modelMarginPercent ?? 20}%</td>
174 <td className="px-4 py-3 text-muted">Cost + {book?.modelMarginPercent ?? 20}%</td>
175 </tr>
176 {(book?.prices ?? []).map((price) => (
177 <tr key={price.meter}>
178 <td className="px-4 py-3">
179 <p className="font-medium">{price.title}</p>
180 <p className="text-xs text-faint">
181 {price.source === "cloudflare" ? "Measured from Cloudflare's bill" : "Cloudflare's published price"}
182 </p>
183 </td>
184 <td className="px-4 py-3 font-mono text-xs tabular-nums text-muted">{perUnit(price, price.costMicros)}</td>
185 <td className="px-4 py-3 tabular-nums">{price.markupPercent}%</td>
186 <td className="px-4 py-3 font-mono text-xs tabular-nums">{perUnit(price, price.priceMicros)}</td>
187 </tr>
188 ))}
189 </tbody>
190 </table>
191 </div>
192
193 <h2 className="mt-14 text-xl font-semibold tracking-tight">Free, and what pays for it</h2>
194 <p className="mt-1 text-sm text-muted">
195 Hosting, git, issues, pull requests, review, search, the API and MCP cost nothing. What runs for you is metered,
196 and these pay for some of it first, each from a fixed budget.
197 </p>
198 <div className="mt-4 overflow-x-auto rounded-xl border border-line">
199 <table className="w-full min-w-[36rem] text-left text-sm">
200 <thead className="border-b border-line text-xs text-muted">
201 <tr>
202 <th className="px-4 py-2.5 font-medium">What is free</th>
203 <th className="px-4 py-2.5 font-medium">Paid for by</th>
204 </tr>
205 </thead>
206 <tbody className="divide-y divide-line">
207 <tr>
208 <td className="px-4 py-3">
209 <p className="font-medium">Public repositories and open source</p>
210 <p className="text-xs text-faint">
211 Hosting, issues, pull requests and search are never charged. Agents' sandbox time and model cost on a
212 public repository come from the pool, up to {dollars(tier.ossRepoMicros)} a month per repository.
213 </p>
214 </td>
215 <td className="px-4 py-3 text-muted">
216 g1t's open-source pool, {dollars(tier.ossPoolMicros)} a month in all. When it or a repository's share is
217 spent, the workspace pays as usual.
218 </td>
219 </tr>
220 {tier.trialWorkspaceMicros > 0 && (
221 <tr>
222 <td className="px-4 py-3">
223 <p className="font-medium">A trial for each new workspace</p>
224 <p className="text-xs text-faint">
225 {dollars(tier.trialWorkspaceMicros)} of usage credit, once, given the first time it uses something.
226 </p>
227 </td>
228 <td className="px-4 py-3 text-muted">
229 A trial budget of {dollars(tier.trialMonthlyPoolMicros)} a month. When a month's is given out, new
230 trials start again on the 1st.
231 </td>
232 </tr>
233 )}
234 <tr>
235 <td className="px-4 py-3">
236 <p className="font-medium">Private storage</p>
237 <p className="text-xs text-faint">
238 {gigabytes(tier.freePrivateStorageBytes)} per workspace; past it, at the storage price above.
239 </p>
240 </td>
241 <td className="px-4 py-3 text-muted">g1t, as part of the free core: at most {gigabytes(tier.freePrivateStorageBytes)} of storage a workspace, about $0.50 a month at Cloudflare's price.</td>
242 </tr>
243 </tbody>
244 </table>
245 </div>
246 <p className="mt-3 text-sm text-muted">
247 No card is charged less than {dollars(tier.minChargeMicros)}, so a payment's fee is never most of it. Smaller
248 amounts carry over to the next invoice.
249 </p>
250
251 <h2 className="mt-14 text-xl font-semibold tracking-tight">Plans</h2>
252 <p className="mt-1 text-sm text-muted">
253 Turned on by an owner, per workspace, at one flat price a month. Never per person.
254 </p>
255 <div className="mt-4 grid gap-4 sm:grid-cols-2">
256 {plans.map((plan) => (
257 <section key={plan.feature} className="flex flex-col rounded-xl border border-line bg-surface p-5">
258 <div className="flex flex-wrap items-baseline justify-between gap-2">
259 <h3 className="font-medium">{plan.title}</h3>
260 <p>
261 <span className="text-2xl font-semibold">${plan.monthlyCents / 100}</span>{" "}
262 <span className="text-sm text-muted">/ month per workspace</span>
263 </p>
264 </div>
265 <ul className="mt-3 space-y-1.5 text-sm text-muted">
266 {plan.includes.map((line) => (
267 <li key={line} className="flex gap-2">
268 <span aria-hidden className="text-accent">
269 ·
270 </span>
271 {line}
272 </li>
273 ))}
274 </ul>
275 <p className="mt-3 text-xs text-faint">{plan.overage}</p>
276 </section>
277 ))}
278 </div>
279
280 <h2 className="mt-14 text-xl font-semibold tracking-tight">Enterprise</h2>
281 <p className="mt-1 text-sm text-muted">
282 For organizations that would rather we run it for several teams, on terms set with them.
283 </p>
284 <section className="mt-4 rounded-xl border border-line bg-surface p-5">
285 <ul className="grid gap-3 text-sm sm:grid-cols-2">
286 <li>
287 <p className="font-medium">Consolidated invoicing</p>
288 <p className="text-muted">One monthly invoice for every workspace, one limit, paid by card or bank transfer.</p>
289 </li>
290 <li>
291 <p className="font-medium">Custom terms</p>
292 <p className="text-muted">A limit of your own, discounts on usage, and terms until a date, set with you.</p>
293 </li>
294 <li>
295 <p className="font-medium">Audit log export</p>
296 <p className="text-muted">Every action by people and agents, as CSV or JSON, kept a year on Team.</p>
297 </li>
298 <li>
299 <p className="font-medium">
300 Single sign-on <span className="ml-1 rounded bg-raised px-1.5 py-0.5 text-xs text-muted">Coming</span>
301 </p>
302 <p className="text-muted">Sign in through your identity provider. Not available yet.</p>
303 </li>
304 </ul>
305 <a
306 href={ENTERPRISE_MAIL}
307 className="mt-5 inline-flex items-center gap-1.5 rounded-md border border-line px-3 py-1.5 text-sm hover:border-line-strong"
308 >
309 Write to us about enterprise
310 <ArrowUpRight size={14} />
311 </a>
312 </section>
313
314 <h2 className="mt-14 text-xl font-semibold tracking-tight">Price changes</h2>
315 {book && book.changes.length > 0 ? (
316 <ul className="mt-4 divide-y divide-line rounded-xl border border-line">
317 {book.changes.map((change) => {
318 const title = book.prices.find((p) => p.meter === change.meter)?.title ?? change.meter;
319 // What you pay moved: the cost, the markup, or both.
320 const before = change.oldCostMicros * (100 + (change.oldMarkupPercent ?? change.markupPercent));
321 const after = change.newCostMicros * (100 + change.markupPercent);
322 const up = after > before;
323 const fresh = after === before;
324 return (
325 <li key={`${change.meter}-${change.createdAt}`} className="px-4 py-3 text-sm">
326 <p>
327 <span className="font-medium">{title}</span>{" "}
328 {fresh ? (
329 <span className="text-muted">new</span>
330 ) : (
331 <span className={up ? "text-warn" : "text-accent"}>
332 {up ? "up" : "down"} {Math.abs((after / before - 1) * 100).toFixed(1)}%
333 </span>
334 )}
335 </p>
336 <p className="mt-0.5 text-xs text-faint">
337 {change.reason} · <TimeAgo at={change.createdAt} />
338 </p>
339 </li>
340 );
341 })}
342 </ul>
343 ) : (
344 <p className="mt-2 text-sm text-muted">None yet. When a price moves, it is listed here with why.</p>
345 )}
346
347 <a
348 href="https://docs.g1t.sh/guides/usage-and-billing/"
349 className="mt-12 inline-flex items-center gap-1.5 text-sm text-accent hover:underline"
350 >
351 How usage and billing work
352 <ArrowUpRight size={14} />
353 </a>
354 </main>
355 );
356}