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

185 lines8,604 bytesCodeBlame

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.

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