flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

Commit

Prices are what g1t pays plus 20%, from the first second

Sandbox time is charged at cost plus 20% from its first second, like everything else in the price book; the 500 free minutes per workspace and the 138% markup that paid for them are gone. Runs on a workspace's own model provider no longer carry a $0.10 fee: they pay only their sandbox time. The price change is recorded with its reason, and the pricing page works out a markup-only change from the price. Docs, pricing page, billing page and sudo say it plainly.

syntaqxcommitted Parent9d45b83Browse files
19 files+166−1610/19 viewed
+1−1
317317
318318 A workspace pays for the g1t agents that work on its repositories, after
319319 they run: each run is charged what AI Gateway priced its model requests
320−at, plus 20%, and its sandbox by the second past the free minutes. See
320+at, plus 20%, and its sandbox by the second, at cost plus 20%. See
321321 [Usage and billing](/guides/usage-and-billing/) for how prices are set and
322322 the limits on usage not yet paid for.
323323 The workspace's **Usage** page shows what its agents have cost, by day,
+7−9
8585
8686 ## What it costs
8787
88−Your providers bill you for the models. g1t charges a flat **$0.10 per
89−run** for its orchestration, plus the run's
90−[sandbox time](/guides/usage-and-billing/#sandbox-time). A
91−change, a review, a revision, a catch-up and a plan are each a run. The
92−statement marks these runs "on your own model provider" and names the
93−model and provider; the Usage page shows what they cost at the provider,
94−as the harness estimated it, beside what g1t charged. See
95−[Usage and billing](/guides/usage-and-billing/).
88+Your providers bill you for the models. g1t charges only each run's
89+[sandbox time](/guides/usage-and-billing/#sandbox-time), at what it costs
90+g1t plus 20%, by the second. A change, a review, a revision, a catch-up
91+and a plan each run in a sandbox, and each sandbox is a line on the
92+statement. See [Usage and billing](/guides/usage-and-billing/).
9693
97−Workspaces still need credit to start agents, for the fee.
94+That sandbox time counts toward the workspace's usage limit like any
95+other.
9896
9997 ## Your keys never reach a sandbox
10098
+18−18
4343 gets **$1 of model cost on g1t's own models**, free, until **October 22,
4444 2026** (11:59 PM Pacific). That is roughly 10 to 25 agent runs: changes,
4545 reviews and revisions. The allowance pays for the model part of those
46−runs; everything else, such as sandbox time past the free minutes, is
47−charged as usual.
46+runs; everything else, such as sandbox time, is charged as usual.
4847
4948 - Mission control and **Settings → Integrations** show what is left.
5049 - When it is used up, agents and workflow runs stop starting, and the
7372 it, plus 20%. A small change costs a few cents.
7473
7574 Work a workspace routes to [its own model providers](/guides/models/) is
76−paid for at those providers instead. Each such run here is a flat $0.10
77−for g1t's orchestration, plus its [sandbox time](#sandbox-time).
75+paid for at those providers instead. Such a run is charged here only for
76+its [sandbox time](#sandbox-time), like any other sandbox.
7877
7978 The charge goes to the workspace that owns the repository, whoever
8079 assigned the issue. That is why only members of a workspace can put g1t
8483
8584 g1t passes its own costs through. Everything a workspace uses costs g1t
8685 money first, at Cloudflare or a model provider, and is charged at that
87−cost plus a set markup. There is no seat price, and nothing is bundled to
88−hide what it costs. The live prices are on
86+cost plus 20%. The 20% pays for running g1t and for building and keeping
87+up its features; g1t is not trying to make money on top of that. There is
88+no seat price, and nothing is bundled to hide what it costs. The live prices are on
8989 [g1t.sh/pricing](https://g1t.sh/pricing), straight from the price book
9090 billing charges from.
9191
105105 against what was used: Containers against the seconds containers ran,
106106 Workers for Platforms per request and per CPU millisecond. When a cost
107107 moves by 2% or more, the price book moves with it, since each price is
108− its cost times its markup, and the change is listed on the pricing page
108+ its cost plus its markup, and the change is listed on the pricing page
109109 with the reason. A measurement far from the current cost (more than 4×
110110 either way) is not adopted, only logged, so one odd day cannot reprice
111111 anything.
113113 | | Markup |
114114 | --- | --- |
115115 | Models | 20% |
116−| Deploy builds, app requests, CPU and apps | 20% |
117−| Sandbox time | 138%, which also pays for the orchestration around each sandbox and everyone's free minutes |
116+| Sandbox time | 20% |
117+| Deploy builds | 20% |
118+| App requests, CPU and apps | 20% |
119+| Custom domains past the plan | 20% |
118120
119121 ## Sandbox time
120122
126128
127129 | | |
128130 | --- | --- |
129−| Free each month | 500 minutes (calendar month, UTC) |
130−| Past that | about $0.002 a minute, by the second |
131131 | What it costs g1t | about $0.0009 a minute (Containers, standard-1, at the CPU sandboxes really use) |
132+| What you pay | that plus 20%, about $0.0011 a minute, by the second from the first |
132133
133134 Both follow what Cloudflare bills, so they move; today's exact figures are
134135 on [g1t.sh/pricing](https://g1t.sh/pricing). See
138139 charges them by the second on its own plan.
139140
140141 Each sandbox is one line on the [statement](#the-statement), such as
141−*Checks on acme/api#12: 3m 12s of sandbox time*, with whether it fell
142−within the free minutes.
142+*Checks on acme/api#12: 3m 12s of sandbox time*.
143143
144144 ## Usage limits
145145
203203 with its PDF and emailed as a receipt:
204204
205205 - **When each month closes**, an invoice for what the workspace owes,
206− itemised: agents on g1t's models, runs on your own model provider,
207− sandbox time, and deployments past the plan. Credit you paid in advance
206+ itemised: agents on g1t's models, sandbox time, and deployments past
207+ the plan. Credit you paid in advance
208208 is taken off as *Paid in advance*; anything left unpaid from before is
209209 added.
210210 - **When the workspace nears its ceiling** mid-month, the same, sooner.
341341 | Line | What it holds |
342342 | --- | --- |
343343 | Agent runs | Runs on g1t's model provider: the model's cost plus the margin. |
344− | Runs on your own model provider | The run fee for runs billed by your own provider. |
344+ | Runs on your own model provider | Older months only: the flat fee runs on your own provider used to carry. |
345345 | Sandbox time | Each sandbox's time, memory and disk. |
346346 | Deployments | Builds and apps beyond the allowance. |
347347 | Payments | Card payments and invoices paid. |
368368 - **g1t's agents, for any workspace with its own model provider:** connect
369369 an Anthropic key or endpoint under [Integrations](/guides/models/) and the
370370 workspace's agents, acceptance checks and merge queue work at once. Your
371− provider bills you for the models; g1t charges nothing for now, and $0.10
372− a run once pricing starts.
371+ provider bills you for the models; g1t charges nothing for now, and only
372+ each run's sandbox time once pricing starts.
373373 - **g1t's hosted models:** while payments are in test mode, every
374374 workspace can use g1t's own models on
375375 [the free allowance](#the-free-allowance) ($1 each, until October 22),
+0−1
163163 deployments: "Deployments",
164164 builds: "Builds",
165165 plans: "Plans",
166− orchestration: "Orchestration",
167166 };
168167
169168 export function kindLabel(kind: string): string {
+1−1
201201 plans: [
202202 "Runs, failures and cost per model, per day",
203203 "Who may use g1t's hosted models, and the free allowance's pool",
204− "Workspaces on their own provider, and what g1t charges them to orchestrate",
204+ "Workspaces on their own provider, and the sandbox time their runs use",
205205 "Stuck or long-running agents, with a way to stop one",
206206 ],
207207 },
+7−0
1515 export const CHANGELOG: ChangelogEntry[] = [
1616 {
1717 date: "2026-10-05",
18+ title: "Cost plus 20%, on everything",
19+ about:
20+ "Sandbox time is charged at cost plus 20% from the first second, with no free minutes. Runs on your own model provider no longer carry a $0.10 fee: they pay only their sandbox time.",
21+ href: "/pricing",
22+ },
23+ {
24+ date: "2026-10-05",
1825 title: "Catch up in seconds",
1926 about:
2027 "Catch up with main merges it in at once when the two changed different files. When they overlap, the merge box shows g1t-agent at work, step by step.",
+13−11
1010 export function meta(args: Route.MetaArgs) {
1111 return page(args, {
1212 title: "Pricing · g1t",
13− description: "g1t passes its costs through: what Cloudflare and model providers charge g1t, plus a set markup. No seats.",
13+ description: "g1t passes its costs through: what Cloudflare and model providers charge g1t, plus 20%. No seats.",
1414 });
1515 }
1616
3535 const HOW = [
3636 {
3737 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 a set markup. Use a little, pay a little.",
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.",
3939 },
4040 {
4141 title: "Prices follow costs, by themselves",
4242 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.",
4343 },
4444 {
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+ {
4549 title: "No seats, ever",
4650 body: "Add as many people and agents as you like. A workspace pays for what it uses, and for the features it turns on.",
4751 },
6165 return (
6266 <main className="mx-auto max-w-4xl px-4 py-12">
6367 <p className="text-sm font-medium text-accent">Pricing</p>
64− <h1 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">What it costs us, plus a markup</h1>
68+ <h1 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">What it costs us, plus 20%</h1>
6569 <p className="mt-3 max-w-2xl text-muted">
6670 g1t runs on Cloudflare and model providers, and passes those costs through. The numbers on this page are the
6771 live price book g1t charges from.
120124 <p className="font-medium">{price.title}</p>
121125 <p className="text-xs text-faint">
122126 {price.source === "cloudflare" ? "Measured from Cloudflare's bill" : "Cloudflare's published price"}
123− {price.meter === "sandbox_second" && " · 500 minutes free each month"}
124127 </p>
125128 </td>
126129 <td className="px-4 py-3 font-mono text-xs tabular-nums text-muted">{perUnit(price, price.costMicros)}</td>
131134 </tbody>
132135 </table>
133136 </div>
134− <p className="mt-3 text-xs text-faint">
135− Sandbox time carries more markup than the rest: it pays for the orchestration around each sandbox, and the
136− free minutes everyone gets.
137− </p>
138137
139138 <h2 className="mt-14 text-xl font-semibold tracking-tight">Features</h2>
140139 <p className="mt-1 text-sm text-muted">Turned on per workspace with a monthly plan. Never free.</p>
158157 <ul className="mt-4 divide-y divide-line rounded-xl border border-line">
159158 {book.changes.map((change) => {
160159 const title = book.prices.find((p) => p.meter === change.meter)?.title ?? change.meter;
161− const up = change.newCostMicros > change.oldCostMicros;
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;
162164 return (
163165 <li key={`${change.meter}-${change.createdAt}`} className="px-4 py-3 text-sm">
164166 <p>
165167 <span className="font-medium">{title}</span>{" "}
166168 <span className={up ? "text-warn" : "text-accent"}>
167− {up ? "up" : "down"} {Math.abs((change.newCostMicros / change.oldCostMicros - 1) * 100).toFixed(1)}%
169+ {up ? "up" : "down"} {Math.abs((after / before - 1) * 100).toFixed(1)}%
168170 </span>
169171 </p>
170172 <p className="mt-0.5 text-xs text-faint">
175177 })}
176178 </ul>
177179 ) : (
178− <p className="mt-2 text-sm text-muted">None yet. When a cost moves, it is listed here with why.</p>
180+ <p className="mt-2 text-sm text-muted">None yet. When a price moves, it is listed here with why.</p>
179181 )}
180182
181183 <a
+5−5
267267 <p className="mt-1 max-w-2xl text-sm text-muted">
268268 g1t agents that work on this workspace's repositories are paid for from
269269 its account: what the model cost, plus {account.marginPercent}%. Every sandbox, for agents, checks, the merge
270− queue and workflows, is metered by the second past 500 free minutes a month. Credit added here pays usage in
270+ queue and workflows, is metered by the second, from the first. Credit added here pays usage in
271271 advance; the usage limit above decides whether work starts.
272272 </p>
273273
356356 <h3 className="font-medium">How it is charged</h3>
357357 <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted">
358358 <li>
359− Usage is charged after it runs, to the workspace that owns the repository: agents at what the model cost
360− plus {account.marginPercent}%, and sandbox time, builds and apps at what they cost g1t plus a set markup.
359+ Usage is charged after it runs, to the workspace that owns the repository: models, sandbox time, builds
360+ and apps at what they cost g1t plus {account.marginPercent}%, which pays for running and building g1t.
361+ Nothing is bundled in.
361362 </li>
362363 <li>
363364 Each month closes with an itemised invoice, charged to the card on file. Near your limit, g1t sends one
368369 <Link to={`/${slug}/-/integrations`} className="text-fg hover:underline">
369370 Integrations
370371 </Link>
371− , the provider bills you for the model and each run here is {dollars(account.orchestrationFeeMicros)}, plus
372− its sandbox time.
372+ , the provider bills you for the model, and a run here is charged only its sandbox time.
373373 </li>
374374 <li>No seats: add as many people and agents as you like.</li>
375375 </ul>
+3−4
7171 hostedOpen: access.hosted,
7272 trial: access.trial,
7373 routes: routes.ok ? routes.value : [],
74− feeMicros: account.ok ? account.value.orchestrationFeeMicros : 100_000,
7574 free: account.ok ? Boolean(account.value.status.free) : false,
7675 marginPercent: account.ok ? account.value.marginPercent : 20,
7776 };
179178 }
180179
181180 export default function WorkspaceIntegrations({ loaderData, actionData }: Route.ComponentProps) {
182− const { slug, role, connections, deliveries, repos: repoNames, adding, feeMicros, free, marginPercent, hostedOpen, trial, routes } =
181+ const { slug, role, connections, deliveries, repos: repoNames, adding, free, marginPercent, hostedOpen, trial, routes } =
183182 loaderData;
184183 const owner = role === "owner";
185184 const busy = useNavigation().state === "submitting";
238237 <p className="mt-3 text-xs text-faint">
239238 On your own providers, their bills are yours.{" "}
240239 {free
241− ? `g1t charges nothing while it is being built out; once pricing starts, ${dollars(feeMicros)} a run for the sandbox and orchestration.`
242− : `g1t charges ${dollars(feeMicros)} a run for the sandbox and orchestration.`}{" "}
240+ ? `g1t charges nothing while it is being built out; once pricing starts, only each run's sandbox time, at cost plus ${marginPercent}%.`
241+ : `g1t charges only each run's sandbox time, at cost plus ${marginPercent}%.`}{" "}
243242 Keys go only from g1t's model proxy to the provider: the agent's
244243 sandbox holds a token that dies with the run.
245244 </p>
+2−2
215215 as it uses, with each
216216 kind of work routed to one of them or to g1t's hosted models
217217 (`PUT /workspaces/{workspace}/model-routes`). Those providers bill the
218− workspace; g1t charges nothing while it is being built out (later, $0.10
219− a run). Sandboxes never hold a key.
218+ workspace; g1t charges nothing while it is being built out (later, only
219+ each run's sandbox time, at cost plus 20%). Sandboxes never hold a key.
220220 - **Alerts** (`sentry`, `datadog`, `webhook`): each problem opens one issue
221221 in a chosen repository, optionally with an agent put on it at once.
222222 Senders sign requests to `https://api.g1t.sh/hooks/{integration}`.
+45−21
7474 pub status: Status,
7575 /// What is added to a run's cost, in percent.
7676 pub margin_percent: u32,
77− /// What a run on the workspace's own model provider is charged: g1t's
78− /// sandbox and orchestration, with the model paid for elsewhere.
79− pub orchestration_fee_micros: i64,
8077 /// The card g1t charges as the workspace nears its limit and when a
8178 /// month closes, if one is on file.
8279 #[serde(default)]
152149 /// For usage: the model, by its public name.
153150 pub model: Option<String>,
154151 /// For usage: `g1t` when g1t paid the model provider, `workspace` when
155− /// the workspace's own account did and only orchestration is charged.
152+ /// the workspace's own account did. Runs on the workspace's own
153+ /// provider pay only their sandbox time now, so only older entries
154+ /// are `workspace`.
156155 #[serde(default = "g1t")]
157156 pub billed_to: String,
158157 /// For a top-up: the username of whoever paid.
367366 pub const MICROS_PER_DOMAIN_MONTH: i64 = 100_000;
368367 }
369368
370−/// Sandbox time: every sandbox g1t starts for a workspace (agents,
371−/// reviews, checks, the merge queue, workflow jobs) is metered by the
372−/// second. Deploy builds are charged by the Deployments plan instead.
373−pub mod sandbox_allowance {
374− /// Free each calendar month (UTC): 500 minutes.
375− pub const FREE_SECONDS: i64 = 30_000;
376− /// What one second costs g1t (Cloudflare Containers, standard-1),
377− /// rounded up. Recorded with every entry.
378− pub const COST_MICROS_PER_SECOND: i64 = super::deployments_allowance::MICROS_PER_BUILD_SECOND;
379− /// What one second past the free minutes is charged: $0.003 a minute.
380− pub const MICROS_PER_SECOND: i64 = 50;
381−}
382−
383369 /// `record_sandbox`: how long one sandbox ran for a workspace, reported by
384−/// the runner when it stops. Recorded once per `reference`, with what it
385−/// cost g1t; seconds past the month's free minutes are charged at
386−/// `sandbox_allowance::MICROS_PER_SECOND`, unless `FREE_WHILE_BUILDING`.
370+/// the runner when it stops. Every sandbox g1t starts for a workspace
371+/// (agents, reviews, checks, the merge queue, workflow jobs) is metered by
372+/// the second, from the first: recorded once per `reference`, with what it
373+/// cost g1t, and charged at the price book's `sandbox_second` price unless
374+/// `FREE_WHILE_BUILDING`. Deploy builds are charged by the Deployments plan
375+/// instead.
387376 /// Returns `Outcome<bool>`: false if that reference was recorded before.
388377 #[derive(Debug, Serialize, Deserialize)]
389378 #[serde(rename_all = "camelCase")]
548537 pub old_cost_micros: f64,
549538 pub new_cost_micros: f64,
550539 pub markup_percent: u32,
540+ /// The markup before, when the change was to the markup rather than
541+ /// to the cost. Absent when the markup stayed `markup_percent`.
542+ #[serde(default, skip_serializing_if = "Option::is_none")]
543+ pub old_markup_percent: Option<u32>,
551544 pub reason: String,
552545 pub created_at: String,
553546 }
826819 #[serde(rename_all = "camelCase")]
827820 pub struct StatementLine {
828821 /// Agent runs, Sandbox time, Deployments, Payments, Credits from g1t,
829− /// Refunds, Runs on your own model provider.
822+ /// Refunds, and, for older entries, Runs on your own model provider.
830823 pub kind: String,
831824 pub count: u32,
832825 /// Charges positive; money in (payments, credits) negative.
12961289 use super::*;
12971290
12981291 #[test]
1292+ fn an_account_carries_no_run_fee() {
1293+ let account = Account {
1294+ workspace: "acme".into(),
1295+ balance_micros: 0,
1296+ status: Status { enabled: true, live: false, free: false },
1297+ margin_percent: 20,
1298+ card: None,
1299+ };
1300+ let json = serde_json::to_value(account).unwrap();
1301+ let mut keys: Vec<&str> = json.as_object().unwrap().keys().map(String::as_str).collect();
1302+ keys.sort_unstable();
1303+ assert_eq!(keys, ["balanceMicros", "card", "marginPercent", "status", "workspace"]);
1304+ }
1305+
1306+ #[test]
1307+ fn a_price_change_says_when_the_markup_moved() {
1308+ let change = PriceChange {
1309+ meter: "sandbox_second".into(),
1310+ old_cost_micros: 21.0,
1311+ new_cost_micros: 21.0,
1312+ markup_percent: 20,
1313+ old_markup_percent: Some(138),
1314+ reason: "Sandbox time is now charged at cost plus 20% from the first second".into(),
1315+ created_at: "2026-10-05T00:00:00Z".into(),
1316+ };
1317+ assert_eq!(serde_json::to_value(&change).unwrap()["oldMarkupPercent"], 138);
1318+ let cost_only = PriceChange { old_markup_percent: None, ..change };
1319+ assert!(serde_json::to_value(&cost_only).unwrap().get("oldMarkupPercent").is_none());
1320+ }
1321+
1322+ #[test]
12991323 fn features_are_named_as_the_site_sends_them() {
13001324 assert_eq!(
13011325 serde_json::to_value(Feature::Deployments).unwrap(),
+5−5
759759 | Kind | What the workspace does | Example |
760760 | --- | --- | --- |
761761 | **Free** | Nothing | Hosting code, issues, pull requests, review, the merge queue, bringing your own agent over MCP |
762−| **Card on file** | Adds a card; pays only for what it uses | Previews, g1t's agents, workflow minutes past the free ones |
762+| **Card on file** | Adds a card; pays only for what it uses | Previews, g1t's agents, workflow minutes |
763763 | **Activation** | Turns a feature on for a monthly fee that includes an allowance; usage past it is metered | Production deployments, Security and quality |
764764 | **Usage only** | Nothing up front; every unit is metered | Model tokens, build minutes, storage past the free amount |
765765
825825
826826 | Meter | Free each month | Then | Margin | For comparison |
827827 | --- | --- | --- | --- | --- |
828−| Sandbox minutes | 500 | $0.003 / minute | 2.5× | GitHub Actions $0.008 / minute (Linux 2-core); Vercel builds $0.0035 / CPU-minute |
828+| Sandbox minutes | None (2026-10-05: charged from the first second) | Cost + 20%, by the second | 1.2× | GitHub Actions $0.008 / minute (Linux 2-core); Vercel builds $0.0035 / CPU-minute |
829829 | App requests | 1 million with Deployments | $0.50 / million | 1.7× | Vercel $0.60 / million invocations |
830830 | App CPU | 3 million ms with Deployments | $0.04 / million ms | 2× | Vercel active CPU about $0.036 / million ms |
831831 | Apps | 10 with Deployments | $0.05 / app-month | 2.5× | |
832832 | Storage | 1 GB | $1.00 / GB-month | 2× | GitHub LFS $0.07 / GB, but repositories are free there |
833833 | Git operations | 10,000 | $0.30 / 1,000 | 2× | |
834834 | g1t's models | | Cost + 20% | 1.2× | The provider's own price |
835−| Your own model provider | | $0.10 / run, plus its sandbox minutes | | |
835+| Your own model provider | | Only its sandbox minutes (the $0.10 run fee was dropped 2026-10-05) | | |
836836 | **Deployments** activation | | $5 / month, with the allowances above | covers Workers for Platforms' $25 / month across workspaces | Vercel Pro $20 per seat |
837837 | **Security and quality** activation (later) | | $10 / month; fixes as agent usage | | GitHub Advanced Security $49 per committer |
838838
12131213 between steps, and at the end), and recording sessions from anyone's own
12141214 Claude Code (`curl -fsSL https://g1t.sh/install/claude.sh | sh`). Integrations are
12151215 in: a workspace's own model provider (Anthropic or any Anthropic-compatible
1216−endpoint, reached through a model proxy so no sandbox holds a key, for a
1217−flat orchestration fee), alerts from Sentry, Datadog and signed webhooks
1216+endpoint, reached through a model proxy so no sandbox holds a key; its
1217+runs pay only their sandbox time), alerts from Sentry, Datadog and signed webhooks
12181218 that open one issue per problem and can start an agent, and Jira and Linear
12191219 tickets that agents read, people import, and that hear back. Racing a
12201220 set number of agents on one issue is dropped: choosing how many agents to
+4−4
3232 status: BillingStatus;
3333 /** What is added to a run's cost, in percent. */
3434 marginPercent: number;
35− /** What a run on the workspace's own model provider is charged instead. */
36− orchestrationFeeMicros: number;
3735 /** The card charged near the limit and when a month closes, if one is saved. */
3836 card?: { brand: string; last4: string; expMonth: number; expYear: number } | null;
3937 };
325323 oldCostMicros: number;
326324 newCostMicros: number;
327325 markupPercent: number;
326+ /** The markup before, when the change was to the markup rather than the cost. */
327+ oldMarkupPercent?: number;
328328 reason: string;
329329 createdAt: string;
330330 };
479479 invoices(workspace: string, viewer: Viewer): Promise<Result<WorkspaceInvoice[]>>;
480480 /**
481481 * How long a sandbox ran for a workspace, reported when it stops. Its
482− * cost is always recorded; seconds past the month's free minutes are
483− * charged. False if `reference` was recorded before.
482+ * cost is recorded and every second is charged, from the first. False if
483+ * `reference` was recorded before.
484484 */
485485 recordSandbox(usage: {
486486 workspace: string;
+17−0
1+-- Sandbox time is charged at cost plus 20% from the first second, like
2+-- everything else in the price book. There are no free minutes, so the
3+-- monthly count that measured them goes.
4+
5+-- A change can be to the markup rather than to the cost: what it was.
6+ALTER TABLE price_changes ADD COLUMN old_markup_percent INTEGER;
7+
8+INSERT INTO price_changes (id, meter, old_cost_micros, new_cost_micros, markup_percent, old_markup_percent, reason, created_at)
9+SELECT 'prc_sandbox_at_cost', meter, cost_micros, cost_micros, 20, markup_percent,
10+ 'Sandbox time is now charged at cost plus 20% from the first second',
11+ strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
12+FROM prices WHERE meter = 'sandbox_second' AND markup_percent <> 20;
13+
14+UPDATE prices SET markup_percent = 20, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
15+WHERE meter = 'sandbox_second';
16+
17+DROP TABLE IF EXISTS sandbox_months;
+2−0
268268 old_cost_micros: f64,
269269 new_cost_micros: f64,
270270 markup_percent: u32,
271+ old_markup_percent: Option<u32>,
271272 reason: String,
272273 created_at: String,
273274 }
334335 old_cost_micros: row.old_cost_micros,
335336 new_cost_micros: row.new_cost_micros,
336337 markup_percent: row.markup_percent,
338+ old_markup_percent: row.old_markup_percent,
337339 reason: row.reason,
338340 created_at: row.created_at,
339341 })
+31−67
138138 /// Absent when no card processor is configured.
139139 stripe: Option<Stripe>,
140140 margin_percent: u32,
141− /// Charged for a run on the workspace's own model provider.
142− orchestration_fee_micros: i64,
143141 /// While g1t is being built out, nothing is charged (`FREE_WHILE_BUILDING`).
144142 free: bool,
145143 /// The free allowance on g1t's hosted models, when there is one.
198196 balance_micros: row.map_or(0, |row| row.balance_micros),
199197 status: self.status(),
200198 margin_percent: self.margin_percent,
201− orchestration_fee_micros: self.orchestration_fee_micros,
202199 card,
203200 })
204201 }
730727 if claimed.is_none() {
731728 return Ok(Outcome::Ok(false));
732729 }
733− // On the workspace's own provider, the model was paid for there:
734− // g1t charges its fee, and keeps the provider's cost to show.
735− let base = if run.own_provider() {
736− self.orchestration_fee_micros
737− } else {
738− // The free allowance on g1t's models covers what it can.
739− let cost = charge_micros(a.cost_usd, 0);
740− let covered = self.trial_covers(&run.workspace, cost).await?;
741− charge_micros((cost - covered) as f64 / MICROS_PER_DOLLAR as f64, self.margin_percent)
742− };
730+ // On the workspace's own provider, the model was paid for there,
731+ // and the run's sandbox time is recorded on its own: nothing more
732+ // to charge.
733+ if run.own_provider() {
734+ return Ok(Outcome::Ok(true));
735+ }
736+ // The free allowance on g1t's models covers what it can.
737+ let cost = charge_micros(a.cost_usd, 0);
738+ let covered = self.trial_covers(&run.workspace, cost).await?;
739+ let base = charge_micros((cost - covered) as f64 / MICROS_PER_DOLLAR as f64, self.margin_percent);
743740 let (charge, terms_note) = self.charged(&run.workspace, base).await?;
744741 let mut description = match run.task.as_str() {
745742 "plan" => format!("Planning for {}", run.repo),
747744 "update" => format!("Catching up {}#{}", run.repo, run.number),
748745 _ => format!("Work on {}#{}", run.repo, run.number),
749746 };
750− if run.own_provider() {
751− description.push_str(", on your own model provider");
752− }
753747 description.push_str(&terms_note);
754748 self.enter(
755749 &run.workspace,
768762 }
769763
770764 impl Billing {
771− /// Records how long a sandbox ran: its cost always, and a charge for
772− /// the seconds past the month's free minutes.
765+ /// Records how long a sandbox ran, with its cost and its charge: every
766+ /// second, from the first, at the price book's price.
773767 async fn record_sandbox(&self, a: RecordSandboxArgs) -> Result<Outcome<bool>> {
774768 if self.stripe.is_none() || a.seconds == 0 {
775769 return Ok(Outcome::Ok(false));
786780 }
787781 let now = now_ms();
788782 let timestamp = rfc3339(now);
789− let month = &timestamp[..7];
790− #[derive(Deserialize)]
791− struct Used {
792− seconds: i64,
793− }
794783 let seconds = i64::from(a.seconds);
795− let after = self
796− .db
797− .prepare(
798− "INSERT INTO sandbox_months (workspace, month, seconds) VALUES (?1, ?2, ?3)
799− ON CONFLICT (workspace, month) DO UPDATE SET seconds = seconds + ?3
800− RETURNING seconds",
801− )
802− .bind(&[workspace.as_str().into(), month.into(), (seconds as f64).into()])?
803− .first::<Used>(None)
804− .await?
805− .map_or(seconds, |used| used.seconds);
806− let billable = sandbox_billable(after - seconds, seconds);
807− // From the price book, which follows what Cloudflare bills g1t.
808− let (cost_per_second, price_per_second) = self.price("sandbox_second").await?.unwrap_or((
809− sandbox_allowance::COST_MICROS_PER_SECOND as f64,
810− sandbox_allowance::MICROS_PER_SECOND as f64,
811− ));
812− let (charge, terms_note) = self.charged(&workspace, (billable as f64 * price_per_second).ceil() as i64).await?;
813− let mut description = format!("{}: {} of sandbox time", a.description, duration(seconds));
814− if billable < seconds {
815− description.push_str(if billable == 0 {
816− ", within the month's free minutes"
817− } else {
818− ", partly within the month's free minutes"
819− });
820− }
821− if billable > 0 {
822− description.push_str(&terms_note);
823− }
784+ // From the price book, which follows what Cloudflare bills g1t. A
785+ // sandbox is the same container as a build, so without a row it is
786+ // a build second's cost plus the margin.
787+ let (cost_per_second, price_per_second) = self.price("sandbox_second").await?.unwrap_or_else(|| {
788+ let cost = deployments_allowance::MICROS_PER_BUILD_SECOND as f64;
789+ (cost, Price::price_for(cost, self.margin_percent))
790+ });
791+ let (charge, terms_note) = self.charged(&workspace, sandbox_charge(seconds, price_per_second)).await?;
792+ let description = format!("{}: {} of sandbox time{terms_note}", a.description, duration(seconds));
824793 self.db
825794 .batch(vec![
826795 self.db
857826 }
858827 }
859828
860−/// Of `seconds` used after `before` this month, how many are past the
861−/// free minutes.
862−fn sandbox_billable(before: i64, seconds: i64) -> i64 {
863− let free_left = (sandbox_allowance::FREE_SECONDS - before).max(0);
864− (seconds - free_left).max(0)
829+/// What `seconds` of sandbox time are charged at `price_per_second`, in
830+/// millionths of a dollar: every second, rounded up to the next millionth.
831+fn sandbox_charge(seconds: i64, price_per_second: f64) -> i64 {
832+ (seconds as f64 * price_per_second).ceil() as i64
865833 }
866834
867835 /// `1h 2m`, `3m 12s` or `40s`.
917885 .ok()
918886 .and_then(|percent| percent.to_string().parse().ok())
919887 .unwrap_or(20),
920− orchestration_fee_micros: env
921− .var("ORCHESTRATION_FEE_MICROS")
922− .ok()
923− .and_then(|fee| fee.to_string().parse().ok())
924− .unwrap_or(100_000),
925888 free: env.var("FREE_WHILE_BUILDING").is_ok_and(|v| v.to_string() == "true"),
926889 ceilings: limits::Ceilings::from_env(&env),
927890 prepaid_only: env.var("PREPAID_ONLY").is_ok_and(|v| v.to_string() == "true"),
10781041 }
10791042
10801043 #[test]
1081− fn sandbox_seconds_are_charged_only_past_the_free_minutes() {
1082− let free = sandbox_allowance::FREE_SECONDS;
1083− assert_eq!(sandbox_billable(0, 600), 0);
1084− assert_eq!(sandbox_billable(free - 100, 600), 500);
1085− assert_eq!(sandbox_billable(free + 5, 600), 600);
1044+ fn sandbox_time_is_charged_from_the_first_second() {
1045+ // 21 millionths a second at cost, plus 20%.
1046+ let price = Price::price_for(21.0, 20);
1047+ assert_eq!(sandbox_charge(1, price), 26);
1048+ assert_eq!(sandbox_charge(60, price), 1_512);
1049+ assert_eq!(sandbox_charge(0, price), 0);
10861050 }
10871051
10881052 #[test]
+1−6
5151 customer_id = COALESCE(excluded.customer_id, accounts.customer_id),
5252 created_at = MIN(accounts.created_at, excluded.created_at)",
5353 "DELETE FROM accounts WHERE workspace = ?2",
54− // Sandbox time counts toward the month's free minutes: the seconds add.
55− "INSERT INTO sandbox_months (workspace, month, seconds)
56− SELECT ?1, month, seconds FROM sandbox_months WHERE workspace = ?2
57− ON CONFLICT (workspace, month) DO UPDATE SET seconds = sandbox_months.seconds + excluded.seconds",
58− "DELETE FROM sandbox_months WHERE workspace = ?2",
5954 // Replaced on each report with the month's whole figure: the newer
6055 // report wins.
6156 "INSERT INTO pending_usage (workspace, source, month, charge_micros, updated_at)
243238 fn every_table_keyed_by_a_slug_is_moved() {
244239 let all = STATEMENTS.join("\n");
245240 for table in [
246− "ledger", "runs", "checkouts", "workspace_invoices", "sales_notes", "accounts", "sandbox_months",
241+ "ledger", "runs", "checkouts", "workspace_invoices", "sales_notes", "accounts",
247242 "pending_usage", "limits", "subscriptions", "month_closes", "account_members", "sales_records",
248243 "enterprise_invoice_lines", "billing_accounts", "admin_actions", "enterprise_invoices",
249244 ] {
+2−0
1919
2020 /// The kind of charge a ledger row is, as the statement names it. One SQL
2121 /// expression, so grouping and filtering agree.
22+/// `Runs on your own model provider` is only on older months: those runs
23+/// carried a flat fee then, and pay only their sandbox time now.
2224 pub(crate) const KIND_SQL: &str = "CASE
2325 WHEN kind = 'top_up' AND reference LIKE 'crd%' THEN 'Credits from g1t'
2426 WHEN kind = 'top_up' AND amount_micros < 0 THEN 'Refunds'
+2−6
2626 "consumers": [{ "queue": "g1t-events-billing", "max_batch_size": 20, "max_batch_timeout": 1 }]
2727 },
2828 "vars": {
29− // What is added to a run's cost, in percent. It pays the card
30− // processor's fee and the sandbox the agent ran in.
29+ // What is added to a model run's cost, in percent: g1t's overhead for
30+ // running and building it. The price book's markups are the same.
3131 "MARGIN_PERCENT": "20",
32− // What a run on a workspace's own model provider is charged, in
33− // millionths of a dollar: the sandbox and the orchestration around
34− // it, with the model paid for at the provider. $0.10.
35− "ORCHESTRATION_FEE_MICROS": "100000",
3632 // While g1t is being built out, workspaces pay nothing: runs are
3733 // recorded with what they cost, and nothing is charged. Set to
3834 // "false" when pricing starts.