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"; | |
| Social cards for every page: og.g1t.sh | 6 | import { page } from "../lib/meta"; |
| Prices keep themselves current with what g1t pays | 7 | import { TimeAgo } from "../components/ui"; |
| 8 | import { billing } from "../lib/services.server"; | |
| 9 | ||
| Social cards for every page: og.g1t.sh | 10 | export function meta(args: Route.MetaArgs) { |
| 11 | return page(args, { | |
| 12 | title: "Pricing · g1t", | |
| Prices are what g1t pays plus 20%, from the first second | 13 | description: "g1t passes its costs through: what Cloudflare and model providers charge g1t, plus 20%. No seats.", |
| Social cards for every page: og.g1t.sh | 14 | }); |
| Prices keep themselves current with what g1t pays | 15 | } |
| 16 | ||
| 17 | export async function loader() { | |
| Billing accounts, terms and enterprises; g1t is no longer free | 18 | const [book, status] = await Promise.all([billing.prices().catch(() => null), billing.status().catch(() => null)]); |
| 19 | return { book, free: status?.free ?? false }; | |
| Prices keep themselves current with what g1t pays | 20 | } |
| 21 | ||
| 22 | /** A price in dollars, with as many digits as it needs to say anything. */ | |
| 23 | function 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. */ | |
| 31 | function perUnit(price: Price, micros: number): string { | |
| 32 | return price.unit === "second" ? `${money(micros * 60)} a minute` : `${money(micros)} per ${price.unit}`; | |
| 33 | } | |
| 34 | ||
| 35 | const HOW = [ | |
| 36 | { | |
| 37 | title: "Our cost, passed through", | |
| Prices are what g1t pays plus 20%, from the first second | 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.", |
| Prices keep themselves current with what g1t pays | 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 | { | |
| Prices are what g1t pays plus 20%, from the first second | 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 | { | |
| Prices keep themselves current with what g1t pays | 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", | |
| Billing accounts, terms and enterprises; g1t is no longer free | 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.", |
| Prices keep themselves current with what g1t pays | 55 | }, |
| Billing accounts, terms and enterprises; g1t is no longer free | 56 | { |
| 57 | title: "Enterprise billing", | |
| Say what g1t does, not whom it is like | 58 | body: "One bill, one limit and one set of terms for several workspaces. Write to us to set one up.", |
| Billing accounts, terms and enterprises; g1t is no longer free | 59 | }, |
| Prices keep themselves current with what g1t pays | 60 | ]; |
| 61 | ||
| 62 | export default function Pricing({ loaderData }: Route.ComponentProps) { | |
| Billing accounts, terms and enterprises; g1t is no longer free | 63 | const { book, free } = loaderData; |
| Prices keep themselves current with what g1t pays | 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> | |
| Prices are what g1t pays plus 20%, from the first second | 68 | <h1 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">What it costs us, plus 20%</h1> |
| Prices keep themselves current with what g1t pays | 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> | |
| Billing accounts, terms and enterprises; g1t is no longer free | 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 | )} | |
| Prices keep themselves current with what g1t pays | 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()}{" "} | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 149 | million requests, {(DEPLOYMENTS_ALLOWANCE.cpuMs / 1e6).toLocaleString()} million CPU milliseconds and{" "} |
| 150 | {DEPLOYMENTS_ALLOWANCE.customDomains} custom domains a month; | |
| Prices keep themselves current with what g1t pays | 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; | |
| Prices are what g1t pays plus 20%, from the first second | 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; | |
| Prices keep themselves current with what g1t pays | 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"}> | |
| Prices are what g1t pays plus 20%, from the first second | 169 | {up ? "up" : "down"} {Math.abs((after / before - 1) * 100).toFixed(1)}% |
| Prices keep themselves current with what g1t pays | 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 | ) : ( | |
| Prices are what g1t pays plus 20%, from the first second | 180 | <p className="mt-2 text-sm text-muted">None yet. When a price moves, it is listed here with why.</p> |
| Prices keep themselves current with what g1t pays | 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 | } |