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

508 lines24,621 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 { wholeDollars } from "../lib/billing";
7import { page } from "../lib/meta";
8import { TimeAgo } from "../components/ui";
9import { billing } from "../lib/services.server";
10
11export function meta(args: Route.MetaArgs) {
12 return page(args, {
13 title: "Pricing · g1t",
14 description:
15 "The forge is free. One plan, $20 a month per workspace with $10 of usage included; usage at what it costs g1t plus 20%. No seats, ever.",
16 });
17}
18
19export async function loader() {
20 const [book, status] = await Promise.all([billing.prices().catch(() => null), billing.status().catch(() => null)]);
21 return { book, free: status?.free ?? false };
22}
23
24/** A price in dollars, with as many digits as it needs to say anything. */
25function money(micros: number): string {
26 const dollars = micros / MICROS_PER_DOLLAR;
27 if (dollars >= 1) return `$${dollars.toFixed(2)}`;
28 if (dollars >= 0.01) return `$${dollars.toFixed(3)}`;
29 return `$${dollars.toPrecision(2)}`;
30}
31
32/** Per second is easier to read per minute. */
33function perUnit(price: Price, micros: number): string {
34 return price.unit === "second" ? `${money(micros * 60)} a minute` : `${money(micros)} per ${price.unit}`;
35}
36
37function gigabytes(bytes: number): string {
38 return `${Math.round((bytes / 1e9) * 10) / 10} GB`;
39}
40
41function count(n: number): string {
42 return n.toLocaleString("en-US");
43}
44
45/** What the page says when billing cannot be reached: the published defaults. */
46const DEFAULT_FREE: Required<FreeTier> = {
47 trialWorkspaceMicros: 5_000_000,
48 trialMonthlyPoolMicros: 100_000_000,
49 ossPoolMicros: 25_000_000,
50 ossRepoMicros: 2_000_000,
51 freePrivateStorageBytes: 1_000_000_000,
52 auditRetentionDays: 90,
53 minChargeMicros: 5_000_000,
54 gitOperationsIncluded: 10_000,
55 gitOperationsFreeCap: 50_000,
56 planPrivateStorageBytes: 10_000_000_000,
57 paidStartCeilingMicros: 100_000_000,
58 overageForgiveCostMicros: 50_000_000,
59};
60
61const A = DEPLOYMENTS_ALLOWANCE;
62const DEPLOYMENTS_LINE = `${A.apps} apps, ${A.requests / 1e6} million requests, ${A.cpuMs / 1e6} million CPU milliseconds, ${A.customDomains} custom domains and ${A.buildSeconds / 60} build minutes a month`;
63
64const DEFAULT_PLAN: FeaturePlan = {
65 feature: "plan",
66 title: "g1t",
67 monthlyCents: 2000,
68 includes: [
69 "$10 of usage each month at cost plus 20%, used first. Unused included usage does not roll over.",
70 "Agents, checks, workflows, the merge queue, deployments and semantic search",
71 `Deployments: ${DEPLOYMENTS_LINE}`,
72 "10 GB of private repository storage",
73 "Unlimited members, at one price for the workspace: never per person",
74 ],
75 overage: "Usage past what is included is charged at cost plus 20%.",
76};
77
78/** The ways to reach g1t about an enterprise account. */
79const ENTERPRISE_MAIL = "mailto:hey@flagon.io?subject=Enterprise%20billing%20for%20g1t";
80
81const HOW = [
82 {
83 title: "Our cost, passed through",
84 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.",
85 },
86 {
87 title: "Prices follow costs, by themselves",
88 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.",
89 },
90 {
91 title: "The 20% is the overhead",
92 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.",
93 },
94 {
95 title: "No seats, ever",
96 body: "Add as many people and agents as you like. A workspace pays one flat price for the plan, and for what it uses past what the plan includes.",
97 },
98];
99
100/** One row of the free-and-plan table. */
101type Row = { what: string; free: string; plan: string; note?: string };
102
103function rows(tier: Required<FreeTier>): Row[] {
104 return [
105 {
106 what: "The forge",
107 free: "Free",
108 plan: "Included",
109 note: "Public and private repositories, issues, pull requests, reviews, protected branches, code owners, search and Explore.",
110 },
111 {
112 what: "Agents, checks, workflows, merge queue",
113 free: "After a card check: the trial, and the open-source pool for checks, workflows and the merge queue on public repositories",
114 plan: "From the plan's included usage, then at cost plus 20%",
115 },
116 {
117 what: "Deployments",
118 free: "Not available",
119 plan: DEPLOYMENTS_LINE.replace(/^./, (c) => c.toUpperCase()) + ", then at cost plus 20%",
120 },
121 {
122 what: "Semantic search",
123 free: "After a card check, from the trial",
124 plan: "From the plan's included usage, then at cost plus 20%",
125 },
126 {
127 what: "Private storage",
128 free: `${gigabytes(tier.freePrivateStorageBytes)}, never charged. Past it, pushes to private repositories stop until you make room or start the plan.`,
129 plan: `${gigabytes(tier.planPrivateStorageBytes)}, then at cost plus 20%`,
130 note: "Public repositories are never charged for storage.",
131 },
132 {
133 what: "Git operations",
134 free: `${count(tier.gitOperationsIncluded)} a month, and never charged. Past ${count(tier.gitOperationsFreeCap)} in a month, slowed to 60 an hour until the month turns.`,
135 plan: `${count(tier.gitOperationsIncluded)} a month, then at cost plus 20%`,
136 note: "Clones, fetches and pushes through g1t.",
137 },
138 {
139 what: "Audit log",
140 free: `${tier.auditRetentionDays} days, with export`,
141 plan: `${tier.auditRetentionDays} days, with export`,
142 },
143 { what: "Secret push protection", free: "Included", plan: "Included" },
144 { what: "Single sign-on", free: "On every plan, once it is built", plan: "On every plan, once it is built" },
145 { what: "Members", free: "Unlimited", plan: "Unlimited" },
146 {
147 what: "Usage past what is included",
148 free: "Not possible: a free workspace never runs up a bill",
149 plan: "Opt in, up to a spend limit you set",
150 },
151 ];
152}
153
154export default function Pricing({ loaderData }: Route.ComponentProps) {
155 const { book, free } = loaderData;
156 const checked = book?.prices.map((p) => p.checkedAt).filter((at): at is string => !!at).sort().at(-1);
157 const given = Object.fromEntries(Object.entries(book?.free ?? {}).filter(([, value]) => value != null));
158 const tier: Required<FreeTier> = { ...DEFAULT_FREE, ...given };
159 const plan = book?.plans?.find((p) => p.feature === "plan") ?? DEFAULT_PLAN;
160 const price = plan.monthlyCents / 100;
161 const dollars = wholeDollars;
162 return (
163 <main className="mx-auto max-w-4xl px-4 py-12">
164 <p className="text-sm font-medium text-accent">Pricing</p>
165 <h1 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">One plan, and usage at cost plus 20%</h1>
166 <p className="mt-3 max-w-2xl text-muted">
167 The forge is free for everyone. Work that runs on g1t's machines is metered at what it costs g1t, plus 20%. The
168 numbers on this page are the live price book g1t charges from.
169 </p>
170 {free && (
171 <div className="mt-5 rounded-xl border border-accent/30 bg-accent/5 px-4 py-3 text-sm">
172 <span className="font-medium">Free while g1t is being built out.</span>{" "}
173 <span className="text-muted">Usage is recorded at these prices but not charged for now. The plan is charged.</span>
174 </div>
175 )}
176
177 <section className="mt-10 rounded-xl border border-accent/40 bg-surface p-6">
178 <div className="flex flex-wrap items-baseline justify-between gap-3">
179 <h2 className="text-xl font-semibold tracking-tight">{plan.title}</h2>
180 <p>
181 <span className="text-3xl font-semibold tabular-nums">${price}</span>{" "}
182 <span className="text-sm text-muted">a month per workspace</span>
183 </p>
184 </div>
185 <ul className="mt-4 space-y-1.5 text-sm text-muted">
186 {plan.includes.map((line) => (
187 <li key={line} className="flex gap-2">
188 <span aria-hidden className="text-accent">
189 ✓
190 </span>
191 {line}
192 </li>
193 ))}
194 </ul>
195 <p className="mt-3 text-xs text-faint">{plan.overage}</p>
196 <div className="mt-5 grid gap-5 border-t border-line pt-5 text-sm sm:grid-cols-2">
197 <div>
198 <p className="font-medium">Where the ${price} goes</p>
199 <p className="mt-1 text-muted">
200 $10 comes back to you as usage: agents, sandboxes, builds and deployments at cost plus 20%, used before
201 anything else. The other $10 pays for running g1t, the free forge for everyone, and the people building it.
202 </p>
203 </div>
204 <div>
205 <p className="font-medium">Included usage resets each month</p>
206 <p className="mt-1 text-muted">
207 Unused included usage does not roll over: it starts again at $10 when the month turns. Past it, usage is
208 charged at cost plus 20%.
209 </p>
210 </div>
211 </div>
212 <p className="mt-5 text-sm text-muted">An owner starts the plan from the workspace's Billing page.</p>
213 </section>
214
215 <h2 className="mt-14 text-xl font-semibold tracking-tight">Free and the plan, side by side</h2>
216 <p className="mt-1 text-sm text-muted">No security feature is held back for a plan.</p>
217 {/* On a phone, one row at a time with both answers under it; a table that wide would scroll. */}
218 <ul className="mt-4 divide-y divide-line rounded-xl border border-line text-sm sm:hidden">
219 {rows(tier).map((row) => (
220 <li key={row.what} className="px-4 py-3">
221 <p className="font-medium">{row.what}</p>
222 {row.note && <p className="text-xs text-faint">{row.note}</p>}
223 <dl className="mt-2 grid grid-cols-[4.5rem_1fr] gap-x-3 gap-y-1.5">
224 <dt className="text-xs leading-5 text-faint">Free</dt>
225 <dd className="text-muted">{row.free}</dd>
226 <dt className="text-xs leading-5 text-faint">{plan.title}</dt>
227 <dd className="text-muted">{row.plan}</dd>
228 </dl>
229 </li>
230 ))}
231 </ul>
232 <div className="mt-4 hidden overflow-x-auto rounded-xl border border-line sm:block">
233 <table className="w-full min-w-[36rem] text-left text-sm">
234 <thead className="border-b border-line text-xs text-muted">
235 <tr>
236 <th className="px-4 py-2.5 font-medium">
237 <span className="sr-only">What</span>
238 </th>
239 <th className="px-4 py-2.5 font-medium">Free</th>
240 <th className="px-4 py-2.5 font-medium">
241 {plan.title}, ${price} a month
242 </th>
243 </tr>
244 </thead>
245 <tbody className="divide-y divide-line">
246 {rows(tier).map((row) => (
247 <tr key={row.what} className="align-top">
248 <td className="px-4 py-3">
249 <p className="font-medium">{row.what}</p>
250 {row.note && <p className="text-xs text-faint">{row.note}</p>}
251 </td>
252 <td className="px-4 py-3 text-muted">{row.free}</td>
253 <td className="px-4 py-3 text-muted">{row.plan}</td>
254 </tr>
255 ))}
256 </tbody>
257 </table>
258 </div>
259 <p className="mt-3 text-sm text-muted">
260 Git operations cost g1t money too: Cloudflare charges g1t $0.15 per 1,000. {count(tier.gitOperationsIncluded)} a
261 month is about twenty times what an active workspace uses; an agent run takes two to four.
262 </p>
263
264 <h2 className="mt-14 text-xl font-semibold tracking-tight">No card, no compute</h2>
265 <div className="mt-3 max-w-3xl space-y-3 text-sm text-muted">
266 <p>
267 Agents, checks, workflows, the merge queue, deployments and semantic search run on g1t's machines. They need
268 the plan, or a card check.
269 </p>
270 <p>
271 The reason: compute costs g1t real money from the first second, and free compute draws people who use it to
272 mine cryptocurrency. A card check is the smallest gate that stops that, with one trial per card.
273 </p>
274 <p>
275 A card check is never charged. Stripe saves and verifies the card, with 3-D Secure where your bank asks for it.
276 Nothing is charged until an owner starts the plan, and then the same card is used, without asking for it again.
277 The trial needs a credit or debit card; prepaid cards can still pay for the plan.
278 </p>
279 </div>
280
281 <div className="mt-5 grid gap-4 sm:grid-cols-2">
282 <section className="rounded-xl border border-line bg-surface p-5">
283 <h3 className="font-medium">The trial</h3>
284 <p className="mt-1.5 text-sm text-muted">
285 {dollars(tier.trialWorkspaceMicros)} of usage, once per workspace and once per card, after a card check with a credit or
286 debit card. It comes from a{" "}
287 {dollars(tier.trialMonthlyPoolMicros)} pool each month for everyone; when a month's pool is given out, new
288 trials wait until the 1st. The trial does not cover deployments and never turns into a charge. If the last
289 run on a trial goes past it, g1t covers the difference.
290 </p>
291 </section>
292 <section className="rounded-xl border border-line bg-surface p-5">
293 <h3 className="font-medium">The open-source pool</h3>
294 <p className="mt-1.5 text-sm text-muted">
295 Checks, workflows and the merge queue on public repositories, after a card check:{" "}
296 {dollars(tier.ossPoolMicros)} a month in all, up to {dollars(tier.ossRepoMicros)} a month per repository.
297 Agents do not run from the pool. When the pool or a repository's share is used up, the work waits for the
298 month to turn, or the workspace pays as usual.
299 </p>
300 </section>
301 </div>
302
303 <h2 className="mt-14 text-xl font-semibold tracking-tight">Limits, and how to raise them</h2>
304 <p className="mt-1 max-w-3xl text-sm text-muted">
305 g1t lets usage run ahead of payment only so far. Limits keep a mistake small, for you and for g1t.
306 </p>
307 <ul className="mt-4 grid gap-3 text-sm sm:grid-cols-2">
308 <li className="rounded-xl border border-line bg-surface p-4">
309 <p className="font-medium">{dollars(tier.paidStartCeilingMicros)} in the first month</p>
310 <p className="mt-1 text-muted">
311 A workspace on the plan can use up to {dollars(tier.paidStartCeilingMicros)} in its first month. After that,
312 the limit grows with payments that clear.
313 </p>
314 </li>
315 <li className="rounded-xl border border-line bg-surface p-4">
316 <p className="font-medium">Your own spend limit</p>
317 <p className="mt-1 text-muted">
318 Owners set a monthly spend limit anywhere up to the highest limit the workspace has had, with no approval.
319 Once, you can raise it yourself to twice that.
320 </p>
321 </li>
322 <li className="rounded-xl border border-line bg-surface p-4">
323 <p className="font-medium">Prepay to raise it now</p>
324 <p className="mt-1 text-muted">
325 Prepaying from $25 raises what you can use by the same amount, at once. From $1,000 you can also pay by bank
326 transfer.
327 </p>
328 </li>
329 <li className="rounded-xl border border-line bg-surface p-4">
330 <p className="font-medium">Raise my limit</p>
331 <p className="mt-1 text-muted">
332 Need more? Ask from Billing with the amount, why, and what you expect to spend. You get an answer within one
333 business day, in the app and by email.
334 </p>
335 </li>
336 </ul>
337
338 <h2 className="mt-14 text-xl font-semibold tracking-tight">When usage runs high</h2>
339 <ul className="mt-4 grid gap-3 text-sm sm:grid-cols-2">
340 <li className="rounded-xl border border-line bg-surface p-4">
341 <p className="font-medium">A spike pauses new work</p>
342 <p className="mt-1 text-muted">
343 When an hour's spend is five times your usual hour, and at least $5, new compute waits for an owner to choose
344 Keep going or Stop. Runs already going finish.
345 </p>
346 </li>
347 <li className="rounded-xl border border-line bg-surface p-4">
348 <p className="font-medium">Caps per run and per issue</p>
349 <p className="mt-1 text-muted">
350 One agent run stops at $2 and the agents on one issue at $10 in all, so a run stuck in a loop stops on its own.
351 Owners change both on Billing.
352 </p>
353 </li>
354 <li className="rounded-xl border border-line bg-surface p-4">
355 <p className="font-medium">Alerts at 50, 75, 90 and 100%</p>
356 <p className="mt-1 text-muted">
357 Of the plan's included usage, your spend limit and g1t's limit: in the app and by email.
358 </p>
359 </li>
360 <li className="rounded-xl border border-line bg-surface p-4">
361 <p className="font-medium">Spent more than you meant to? Tell us</p>
362 <p className="mt-1 text-muted">
363 Tell us from Billing what happened. Once in 12 months, g1t can credit back usage you did not mean, always
364 including its 20% on it. Larger credits, or a second one within 12 months, are reviewed by a person.
365 </p>
366 </li>
367 </ul>
368 <p className="mt-3 text-sm text-muted">
369 No card is charged less than {dollars(tier.minChargeMicros)} when a month closes, so a payment's fee is never most
370 of it. Smaller amounts carry over to the next invoice. Charges at a limit always go through.
371 </p>
372
373 <div className="mt-14 grid gap-4 sm:grid-cols-2">
374 {HOW.map((item) => (
375 <section key={item.title} className="rounded-xl border border-line bg-surface p-5">
376 <h2 className="font-medium">{item.title}</h2>
377 <p className="mt-1.5 text-sm text-muted">{item.body}</p>
378 </section>
379 ))}
380 </div>
381
382 <h2 className="mt-14 text-xl font-semibold tracking-tight">Usage</h2>
383 <p className="mt-1 text-sm text-muted">
384 {checked ? (
385 <>
386 Last checked against Cloudflare's bill <TimeAgo at={checked} />.
387 </>
388 ) : (
389 "Starting from Cloudflare's published prices; checked against its bill daily."
390 )}
391 </p>
392 <div className="mt-4 overflow-x-auto rounded-xl border border-line">
393 <table className="w-full text-left text-sm sm:min-w-[36rem]">
394 <thead className="border-b border-line text-xs text-muted">
395 <tr>
396 <th className="px-4 py-2.5 font-medium">What</th>
397 <th className="px-4 py-2.5 font-medium">Costs g1t</th>
398 {/* The markup is in what you pay; on a phone the column gives way. */}
399 <th className="hidden px-4 py-2.5 font-medium sm:table-cell">Markup</th>
400 <th className="px-4 py-2.5 font-medium">You pay</th>
401 </tr>
402 </thead>
403 <tbody className="divide-y divide-line">
404 <tr>
405 <td className="px-4 py-3">
406 <p className="font-medium">Models</p>
407 <p className="text-xs text-faint">g1t's hosted models, through AI Gateway</p>
408 </td>
409 <td className="px-4 py-3 text-muted">What the provider charges, per request</td>
410 <td className="hidden px-4 py-3 tabular-nums sm:table-cell">{book?.modelMarginPercent ?? 20}%</td>
411 <td className="px-4 py-3 text-muted">Cost + {book?.modelMarginPercent ?? 20}%</td>
412 </tr>
413 {(book?.prices ?? []).map((price) => (
414 <tr key={price.meter}>
415 <td className="px-4 py-3">
416 <p className="font-medium">{price.title}</p>
417 <p className="text-xs text-faint">
418 {price.source === "cloudflare" ? "Measured from Cloudflare's bill" : "Cloudflare's published price"}
419 </p>
420 </td>
421 <td className="px-4 py-3 font-mono text-xs tabular-nums text-muted">{perUnit(price, price.costMicros)}</td>
422 <td className="hidden px-4 py-3 tabular-nums sm:table-cell">{price.markupPercent}%</td>
423 <td className="px-4 py-3 font-mono text-xs tabular-nums">{perUnit(price, price.priceMicros)}</td>
424 </tr>
425 ))}
426 </tbody>
427 </table>
428 </div>
429
430 <h2 className="mt-14 text-xl font-semibold tracking-tight">Enterprise</h2>
431 <p className="mt-1 text-sm text-muted">
432 For organizations with several workspaces: one invoice and one limit. The prices are the ones on this page.
433 </p>
434 <section className="mt-4 rounded-xl border border-line bg-surface p-5">
435 <ul className="grid gap-3 text-sm sm:grid-cols-2">
436 <li>
437 <p className="font-medium">Consolidated invoicing</p>
438 <p className="text-muted">One monthly invoice for every workspace, paid by card or bank transfer.</p>
439 </li>
440 <li>
441 <p className="font-medium">Custom terms</p>
442 <p className="text-muted">One limit across every workspace, invoice terms and a billing contact, set with you.</p>
443 </li>
444 <li>
445 <p className="font-medium">Security on every plan</p>
446 <p className="text-muted">
447 The audit log for {tier.auditRetentionDays} days with export, and secret push protection, for every workspace.
448 </p>
449 </li>
450 <li>
451 <p className="font-medium">
452 Single sign-on <span className="ml-1 rounded bg-raised px-1.5 py-0.5 text-xs text-muted">Coming</span>
453 </p>
454 <p className="text-muted">Through your identity provider, on every plan once it is built. Not available yet.</p>
455 </li>
456 </ul>
457 <a
458 href={ENTERPRISE_MAIL}
459 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"
460 >
461 Write to us about enterprise
462 <ArrowUpRight size={14} />
463 </a>
464 </section>
465
466 <h2 className="mt-14 text-xl font-semibold tracking-tight">Price changes</h2>
467 {book && book.changes.length > 0 ? (
468 <ul className="mt-4 divide-y divide-line rounded-xl border border-line">
469 {book.changes.map((change) => {
470 const title = book.prices.find((p) => p.meter === change.meter)?.title ?? change.meter;
471 // What you pay moved: the cost, the markup, or both.
472 const before = change.oldCostMicros * (100 + (change.oldMarkupPercent ?? change.markupPercent));
473 const after = change.newCostMicros * (100 + change.markupPercent);
474 const up = after > before;
475 const fresh = after === before || before === 0;
476 return (
477 <li key={`${change.meter}-${change.createdAt}`} className="px-4 py-3 text-sm">
478 <p>
479 <span className="font-medium">{title}</span>{" "}
480 {fresh ? (
481 <span className="text-muted">new</span>
482 ) : (
483 <span className={up ? "text-warn" : "text-accent"}>
484 {up ? "up" : "down"} {Math.abs((after / before - 1) * 100).toFixed(1)}%
485 </span>
486 )}
487 </p>
488 <p className="mt-0.5 text-xs text-faint">
489 {change.reason} · <TimeAgo at={change.createdAt} />
490 </p>
491 </li>
492 );
493 })}
494 </ul>
495 ) : (
496 <p className="mt-2 text-sm text-muted">None yet. When a price moves, it is listed here with why.</p>
497 )}
498
499 <a
500 href="https://docs.g1t.sh/guides/usage-and-billing/"
501 className="mt-12 inline-flex items-center gap-1.5 text-sm text-accent hover:underline"
502 >
503 How usage and billing work
504 <ArrowUpRight size={14} />
505 </a>
506 </main>
507 );
508}