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