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

192 lines9,146 bytesCodeBlame
1import { ArrowUpRight } from "lucide-react";
2
3import { DEPLOYMENTS_ALLOWANCE, 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: "g1t passes its costs through: what Cloudflare and model providers charge g1t, plus 20%. No seats.",
14 });
15}
16
17export async function loader() {
18 const [book, status] = await Promise.all([billing.prices().catch(() => null), billing.status().catch(() => null)]);
19 return { book, free: status?.free ?? false };
20}
21
22/** A price in dollars, with as many digits as it needs to say anything. */
23function money(micros: number): string {
24 const dollars = micros / MICROS_PER_DOLLAR;
25 if (dollars >= 1) return `$${dollars.toFixed(2)}`;
26 if (dollars >= 0.01) return `$${dollars.toFixed(3)}`;
27 return `$${dollars.toPrecision(2)}`;
28}
29
30/** Per second is easier to read per minute. */
31function perUnit(price: Price, micros: number): string {
32 return price.unit === "second" ? `${money(micros * 60)} a minute` : `${money(micros)} per ${price.unit}`;
33}
34
35const HOW = [
36 {
37 title: "Our cost, passed through",
38 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.",
39 },
40 {
41 title: "Prices follow costs, by themselves",
42 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.",
43 },
44 {
45 title: "The 20% is the overhead",
46 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.",
47 },
48 {
49 title: "No seats, ever",
50 body: "Add as many people and agents as you like. A workspace pays for what it uses, and for the features it turns on.",
51 },
52 {
53 title: "Limits that protect both of us",
54 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.",
55 },
56 {
57 title: "Enterprise billing",
58 body: "One bill, one limit and one set of terms for several workspaces. Write to us to set one up.",
59 },
60];
61
62export default function Pricing({ loaderData }: Route.ComponentProps) {
63 const { book, free } = loaderData;
64 const checked = book?.prices.map((p) => p.checkedAt).filter((at): at is string => !!at).sort().at(-1);
65 return (
66 <main className="mx-auto max-w-4xl px-4 py-12">
67 <p className="text-sm font-medium text-accent">Pricing</p>
68 <h1 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">What it costs us, plus 20%</h1>
69 <p className="mt-3 max-w-2xl text-muted">
70 g1t runs on Cloudflare and model providers, and passes those costs through. The numbers on this page are the
71 live price book g1t charges from.
72 </p>
73 {free && (
74 <div className="mt-5 rounded-xl border border-accent/30 bg-accent/5 px-4 py-3 text-sm">
75 <span className="font-medium">Free while g1t is being built out.</span>{" "}
76 <span className="text-muted">
77 Usage is recorded at these prices but not charged for now. Paid features, such as Deployments, are charged.
78 </span>
79 </div>
80 )}
81
82 <div className="mt-10 grid gap-4 sm:grid-cols-2">
83 {HOW.map((item) => (
84 <section key={item.title} className="rounded-xl border border-line bg-surface p-5">
85 <h2 className="font-medium">{item.title}</h2>
86 <p className="mt-1.5 text-sm text-muted">{item.body}</p>
87 </section>
88 ))}
89 </div>
90
91 <h2 className="mt-14 text-xl font-semibold tracking-tight">Usage</h2>
92 <p className="mt-1 text-sm text-muted">
93 {checked ? (
94 <>
95 Last checked against Cloudflare's bill <TimeAgo at={checked} />.
96 </>
97 ) : (
98 "Starting from Cloudflare's published prices; checked against its bill daily."
99 )}
100 </p>
101 <div className="mt-4 overflow-x-auto rounded-xl border border-line">
102 <table className="w-full min-w-[36rem] text-left text-sm">
103 <thead className="border-b border-line text-xs text-muted">
104 <tr>
105 <th className="px-4 py-2.5 font-medium">What</th>
106 <th className="px-4 py-2.5 font-medium">Costs g1t</th>
107 <th className="px-4 py-2.5 font-medium">Markup</th>
108 <th className="px-4 py-2.5 font-medium">You pay</th>
109 </tr>
110 </thead>
111 <tbody className="divide-y divide-line">
112 <tr>
113 <td className="px-4 py-3">
114 <p className="font-medium">Models</p>
115 <p className="text-xs text-faint">g1t's hosted models, through AI Gateway</p>
116 </td>
117 <td className="px-4 py-3 text-muted">What the provider charges, per request</td>
118 <td className="px-4 py-3 tabular-nums">{book?.modelMarginPercent ?? 20}%</td>
119 <td className="px-4 py-3 text-muted">Cost + {book?.modelMarginPercent ?? 20}%</td>
120 </tr>
121 {(book?.prices ?? []).map((price) => (
122 <tr key={price.meter}>
123 <td className="px-4 py-3">
124 <p className="font-medium">{price.title}</p>
125 <p className="text-xs text-faint">
126 {price.source === "cloudflare" ? "Measured from Cloudflare's bill" : "Cloudflare's published price"}
127 </p>
128 </td>
129 <td className="px-4 py-3 font-mono text-xs tabular-nums text-muted">{perUnit(price, price.costMicros)}</td>
130 <td className="px-4 py-3 tabular-nums">{price.markupPercent}%</td>
131 <td className="px-4 py-3 font-mono text-xs tabular-nums">{perUnit(price, price.priceMicros)}</td>
132 </tr>
133 ))}
134 </tbody>
135 </table>
136 </div>
137
138 <h2 className="mt-14 text-xl font-semibold tracking-tight">Features</h2>
139 <p className="mt-1 text-sm text-muted">Turned on per workspace with a monthly plan. Never free.</p>
140 <section className="mt-4 rounded-xl border border-line bg-surface p-5">
141 <div className="flex flex-wrap items-baseline justify-between gap-2">
142 <h3 className="font-medium">Deployments</h3>
143 <p>
144 <span className="text-2xl font-semibold">$5</span> <span className="text-sm text-muted">/ month</span>
145 </p>
146 </div>
147 <p className="mt-1 text-sm text-muted">
148 Includes {DEPLOYMENTS_ALLOWANCE.apps} apps up at once, {(DEPLOYMENTS_ALLOWANCE.requests / 1e6).toLocaleString()}{" "}
149 million requests, {(DEPLOYMENTS_ALLOWANCE.cpuMs / 1e6).toLocaleString()} million CPU milliseconds and{" "}
150 {DEPLOYMENTS_ALLOWANCE.customDomains} custom domains a month;
151 builds, and usage past that, at the prices above.
152 </p>
153 </section>
154
155 <h2 className="mt-14 text-xl font-semibold tracking-tight">Price changes</h2>
156 {book && book.changes.length > 0 ? (
157 <ul className="mt-4 divide-y divide-line rounded-xl border border-line">
158 {book.changes.map((change) => {
159 const title = book.prices.find((p) => p.meter === change.meter)?.title ?? change.meter;
160 // What you pay moved: the cost, the markup, or both.
161 const before = change.oldCostMicros * (100 + (change.oldMarkupPercent ?? change.markupPercent));
162 const after = change.newCostMicros * (100 + change.markupPercent);
163 const up = after > before;
164 return (
165 <li key={`${change.meter}-${change.createdAt}`} className="px-4 py-3 text-sm">
166 <p>
167 <span className="font-medium">{title}</span>{" "}
168 <span className={up ? "text-warn" : "text-accent"}>
169 {up ? "up" : "down"} {Math.abs((after / before - 1) * 100).toFixed(1)}%
170 </span>
171 </p>
172 <p className="mt-0.5 text-xs text-faint">
173 {change.reason} · <TimeAgo at={change.createdAt} />
174 </p>
175 </li>
176 );
177 })}
178 </ul>
179 ) : (
180 <p className="mt-2 text-sm text-muted">None yet. When a price moves, it is listed here with why.</p>
181 )}
182
183 <a
184 href="https://docs.g1t.sh/guides/usage-and-billing/"
185 className="mt-12 inline-flex items-center gap-1.5 text-sm text-accent hover:underline"
186 >
187 How usage and billing work
188 <ArrowUpRight size={14} />
189 </a>
190 </main>
191 );
192}