Skip to content

Compare changes

Choose two branches to see what one has that the other does not, then open a pull request for it.

Open a pull request

3 commits

58 files+966−600/58 viewed
+48−0
1+# g1t brand files
2+
3+The mark is "the fleet": three 1s stepping back in depth, the agents at work
4+behind the one change in front. The front 1 is `#ededef` (`--g1t-fg`), the two
5+behind it are lavender `#b6a8ff` (`--g1t-merged`) at 65% and 35%, on the very
6+dark gray base `#0f0f11` (`--g1t-bg`). On light backgrounds the front 1 is
7+`#161618` and the lavender deepens to `#6b56e8`.
8+
9+These are the files to upload to Stripe and similar services. The site serves
10+its own copies from `apps/web/public` and `apps/docs/public`.
11+
12+| File | Size | Use |
13+| --- | --- | --- |
14+| `icon-512.png` | 512×512, dark square | Stripe branding icon (square, at least 128px), Slack, GitHub app avatars |
15+| `icon-1024.png` | 1024×1024, dark square | App stores and anywhere asking for a large square icon |
16+| `icon-transparent-1024.png` | 1024×1024, transparent | Placing the mark on a dark surface of your own |
17+| `logo-on-dark.png` | 1056×642, dark background | Stripe branding logo (wide), docs and decks on dark |
18+| `logo-on-light.png` | 1056×642, white background | Invoices, receipts and anywhere the background is light |
19+| `og-1200x630.png` | 1200×630 | Social and Open Graph card |
20+
21+## Sources
22+
23+`svg/` holds the source of every file above and of the site's favicons:
24+
25+| File | What it is |
26+| --- | --- |
27+| `svg/mark.svg` | The mark on transparent, for dark backgrounds (also `packages/theme/mark.svg`) |
28+| `svg/mark-on-light.svg` | The mark on transparent, for light backgrounds |
29+| `svg/mark-small.svg` | The 16px cut: pixel-aligned, with a notch between the front 1's flag and the bar behind it |
30+| `svg/icon-square.svg` | The mark on a full-bleed dark square (`icon-512.png`, `icon-1024.png`) |
31+| `svg/icon-rounded.svg` | The mark on a rounded dark tile (`favicon.ico` at 32 and 48, `brand/g1t-mark.*`) |
32+| `svg/icon-padded.svg` | The mark with room for a circle crop (`apple-touch-icon.png`, `icon-192.png`, `icon-512.png` in the apps) |
33+| `svg/favicon.svg` | The 16px cut with no tile, coloured for a light or dark browser (`favicon.svg` in the apps) |
34+| `svg/logo.svg`, `svg/logo-on-light.svg` | The mark and the wordmark, transparent, for dark and light backgrounds |
35+| `svg/logo-dark-bg.svg`, `svg/logo-light-bg.svg` | The same lockups with padding and a background |
36+| `svg/og.svg` | The social card; its tagline uses Inter and JetBrains Mono as live text |
37+
38+The wordmark is "g1t" in Inter ExtraBold at -0.05em tracking, outlined to paths
39+so the files need no font.
40+
41+## On the web
42+
43+`apps/web/public/brand/` is served at `https://g1t.sh/brand/` for emails and
44+anything that links to the mark:
45+
46+- `g1t-mark.svg`, `g1t-mark.png` (256px): the mark on a rounded dark tile, readable on any background
47+- `g1t-logo.svg`, `g1t-logo.png`: the lockup for light backgrounds, used in g1t's emails
48+- `g1t-logo-on-dark.svg`, `g1t-logo-on-dark.png`: the lockup for dark backgrounds
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.f{fill:#161618}.f.s{fill:none;stroke:#161618}.m{fill:#6b56e8}@media (prefers-color-scheme: dark){.f{fill:#ededef}.f.s{fill:none;stroke:#ededef}.m{fill:#b6a8ff}}</style><mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16"><rect width="16" height="16" fill="#fff"/><path d="M10.5 3.5 8.5 5.4" stroke="#000" stroke-width="3.6" stroke-linecap="round"/></mask><g mask="url(#cut)"><rect class="m" x="3" y="5.5" width="2" height="8.5" rx="1" fill-opacity="0.5"/><rect class="m" x="6" y="4" width="2" height="10" rx="1" fill-opacity="0.85"/></g><rect class="f" x="9" y="2" width="3" height="12" rx="1.5"/><path class="f s" d="M10.5 3.5 8.5 5.4" stroke-width="2.2" stroke-linecap="round"/></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="#0f0f11"/><g transform="translate(3.2 3.2) scale(0.8)"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#b6a8ff" fill-opacity="0.35"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#b6a8ff" fill-opacity="0.65"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#ededef"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#ededef" stroke-width="4.6" stroke-linecap="round"/></g></g></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#0f0f11"/><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#b6a8ff" fill-opacity="0.35"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#b6a8ff" fill-opacity="0.65"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#ededef"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#ededef" stroke-width="4.6" stroke-linecap="round"/></g></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="#0f0f11"/><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#b6a8ff" fill-opacity="0.35"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#b6a8ff" fill-opacity="0.65"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#ededef"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#ededef" stroke-width="4.6" stroke-linecap="round"/></g></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352.19 214.04"><rect width="352.19" height="214.04" rx="0" fill="#0f0f11"/><g transform="translate(37.23 43.65) scale(3.3)"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#b6a8ff" fill-opacity="0.35"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#b6a8ff" fill-opacity="0.65"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#ededef"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#ededef" stroke-width="4.6" stroke-linecap="round"/></g></g><path transform="translate(161.08 132.75)" fill="#ededef" d="M30.42 21.29Q22.8 21.29 17.5 19.26Q12.21 17.24 9.16 13.67Q6.1 10.11 5.22 5.42L20.75 4.1Q21.19 5.66 22.46 6.86Q23.73 8.06 25.71 8.74Q27.69 9.42 30.37 9.42Q35.3 9.42 37.89 6.93Q40.48 4.44 40.48 -0.59V-9.28H39.89Q38.72 -6.54 36.65 -4.57Q34.57 -2.59 31.67 -1.56Q28.76 -0.54 25.1 -0.54Q18.7 -0.54 13.7 -3.64Q8.69 -6.74 5.83 -12.6Q2.98 -18.46 2.98 -26.76Q2.98 -35.3 5.91 -41.36Q8.84 -47.41 13.82 -50.63Q18.8 -53.86 25 -53.86Q28.81 -53.86 31.71 -52.66Q34.62 -51.46 36.77 -49.41Q38.92 -47.36 40.23 -44.73H40.48V-52.93H57.23V-2.1Q57.23 5.91 53.91 11.08Q50.59 16.26 44.56 18.77Q38.53 21.29 30.42 21.29ZM30.32 -13.38Q33.54 -13.38 35.89 -15.01Q38.23 -16.65 39.53 -19.7Q40.82 -22.75 40.82 -26.95Q40.82 -31.15 39.53 -34.23Q38.23 -37.3 35.89 -38.99Q33.54 -40.67 30.32 -40.67Q27.15 -40.67 24.88 -38.99Q22.61 -37.3 21.41 -34.23Q20.21 -31.15 20.21 -26.95Q20.21 -22.71 21.41 -19.65Q22.61 -16.6 24.88 -14.99Q27.15 -13.38 30.32 -13.38ZM94.12 -72.75V0H77.08V-57.37H76.84L60.33 -46.53V-61.57L77.47 -72.75ZM130.48 -52.93V-40.09H95.18V-52.93ZM103.28 -66.5H120.22V-17.04Q120.22 -14.75 121.23 -13.75Q122.23 -12.74 124.81 -12.74Q125.89 -12.74 127.62 -12.84Q129.36 -12.94 130.14 -13.04L131.11 -0.29Q129.11 0.1 126.43 0.22Q123.74 0.34 121.1 0.34Q112.07 0.34 107.68 -3.42Q103.28 -7.18 103.28 -14.79Z"/></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352.19 214.04"><rect width="352.19" height="214.04" rx="0" fill="#ffffff"/><g transform="translate(37.23 43.65) scale(3.3)"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#6b56e8" fill-opacity="0.4"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#6b56e8" fill-opacity="0.75"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#161618"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#161618" stroke-width="4.6" stroke-linecap="round"/></g></g><path transform="translate(161.08 132.75)" fill="#161618" d="M30.42 21.29Q22.8 21.29 17.5 19.26Q12.21 17.24 9.16 13.67Q6.1 10.11 5.22 5.42L20.75 4.1Q21.19 5.66 22.46 6.86Q23.73 8.06 25.71 8.74Q27.69 9.42 30.37 9.42Q35.3 9.42 37.89 6.93Q40.48 4.44 40.48 -0.59V-9.28H39.89Q38.72 -6.54 36.65 -4.57Q34.57 -2.59 31.67 -1.56Q28.76 -0.54 25.1 -0.54Q18.7 -0.54 13.7 -3.64Q8.69 -6.74 5.83 -12.6Q2.98 -18.46 2.98 -26.76Q2.98 -35.3 5.91 -41.36Q8.84 -47.41 13.82 -50.63Q18.8 -53.86 25 -53.86Q28.81 -53.86 31.71 -52.66Q34.62 -51.46 36.77 -49.41Q38.92 -47.36 40.23 -44.73H40.48V-52.93H57.23V-2.1Q57.23 5.91 53.91 11.08Q50.59 16.26 44.56 18.77Q38.53 21.29 30.42 21.29ZM30.32 -13.38Q33.54 -13.38 35.89 -15.01Q38.23 -16.65 39.53 -19.7Q40.82 -22.75 40.82 -26.95Q40.82 -31.15 39.53 -34.23Q38.23 -37.3 35.89 -38.99Q33.54 -40.67 30.32 -40.67Q27.15 -40.67 24.88 -38.99Q22.61 -37.3 21.41 -34.23Q20.21 -31.15 20.21 -26.95Q20.21 -22.71 21.41 -19.65Q22.61 -16.6 24.88 -14.99Q27.15 -13.38 30.32 -13.38ZM94.12 -72.75V0H77.08V-57.37H76.84L60.33 -46.53V-61.57L77.47 -72.75ZM130.48 -52.93V-40.09H95.18V-52.93ZM103.28 -66.5H120.22V-17.04Q120.22 -14.75 121.23 -13.75Q122.23 -12.74 124.81 -12.74Q125.89 -12.74 127.62 -12.84Q129.36 -12.94 130.14 -13.04L131.11 -0.29Q129.11 0.1 126.43 0.22Q123.74 0.34 121.1 0.34Q112.07 0.34 107.68 -3.42Q103.28 -7.18 103.28 -14.79Z"/></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232.19 94.04"><g transform="translate(-22.77 -16.35) scale(3.3)"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#6b56e8" fill-opacity="0.4"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#6b56e8" fill-opacity="0.75"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#161618"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#161618" stroke-width="4.6" stroke-linecap="round"/></g></g><path transform="translate(101.08 72.75)" fill="#161618" d="M30.42 21.29Q22.8 21.29 17.5 19.26Q12.21 17.24 9.16 13.67Q6.1 10.11 5.22 5.42L20.75 4.1Q21.19 5.66 22.46 6.86Q23.73 8.06 25.71 8.74Q27.69 9.42 30.37 9.42Q35.3 9.42 37.89 6.93Q40.48 4.44 40.48 -0.59V-9.28H39.89Q38.72 -6.54 36.65 -4.57Q34.57 -2.59 31.67 -1.56Q28.76 -0.54 25.1 -0.54Q18.7 -0.54 13.7 -3.64Q8.69 -6.74 5.83 -12.6Q2.98 -18.46 2.98 -26.76Q2.98 -35.3 5.91 -41.36Q8.84 -47.41 13.82 -50.63Q18.8 -53.86 25 -53.86Q28.81 -53.86 31.71 -52.66Q34.62 -51.46 36.77 -49.41Q38.92 -47.36 40.23 -44.73H40.48V-52.93H57.23V-2.1Q57.23 5.91 53.91 11.08Q50.59 16.26 44.56 18.77Q38.53 21.29 30.42 21.29ZM30.32 -13.38Q33.54 -13.38 35.89 -15.01Q38.23 -16.65 39.53 -19.7Q40.82 -22.75 40.82 -26.95Q40.82 -31.15 39.53 -34.23Q38.23 -37.3 35.89 -38.99Q33.54 -40.67 30.32 -40.67Q27.15 -40.67 24.88 -38.99Q22.61 -37.3 21.41 -34.23Q20.21 -31.15 20.21 -26.95Q20.21 -22.71 21.41 -19.65Q22.61 -16.6 24.88 -14.99Q27.15 -13.38 30.32 -13.38ZM94.12 -72.75V0H77.08V-57.37H76.84L60.33 -46.53V-61.57L77.47 -72.75ZM130.48 -52.93V-40.09H95.18V-52.93ZM103.28 -66.5H120.22V-17.04Q120.22 -14.75 121.23 -13.75Q122.23 -12.74 124.81 -12.74Q125.89 -12.74 127.62 -12.84Q129.36 -12.94 130.14 -13.04L131.11 -0.29Q129.11 0.1 126.43 0.22Q123.74 0.34 121.1 0.34Q112.07 0.34 107.68 -3.42Q103.28 -7.18 103.28 -14.79Z"/></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232.19 94.04"><g transform="translate(-22.77 -16.35) scale(3.3)"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#b6a8ff" fill-opacity="0.35"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#b6a8ff" fill-opacity="0.65"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#ededef"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#ededef" stroke-width="4.6" stroke-linecap="round"/></g></g><path transform="translate(101.08 72.75)" fill="#ededef" d="M30.42 21.29Q22.8 21.29 17.5 19.26Q12.21 17.24 9.16 13.67Q6.1 10.11 5.22 5.42L20.75 4.1Q21.19 5.66 22.46 6.86Q23.73 8.06 25.71 8.74Q27.69 9.42 30.37 9.42Q35.3 9.42 37.89 6.93Q40.48 4.44 40.48 -0.59V-9.28H39.89Q38.72 -6.54 36.65 -4.57Q34.57 -2.59 31.67 -1.56Q28.76 -0.54 25.1 -0.54Q18.7 -0.54 13.7 -3.64Q8.69 -6.74 5.83 -12.6Q2.98 -18.46 2.98 -26.76Q2.98 -35.3 5.91 -41.36Q8.84 -47.41 13.82 -50.63Q18.8 -53.86 25 -53.86Q28.81 -53.86 31.71 -52.66Q34.62 -51.46 36.77 -49.41Q38.92 -47.36 40.23 -44.73H40.48V-52.93H57.23V-2.1Q57.23 5.91 53.91 11.08Q50.59 16.26 44.56 18.77Q38.53 21.29 30.42 21.29ZM30.32 -13.38Q33.54 -13.38 35.89 -15.01Q38.23 -16.65 39.53 -19.7Q40.82 -22.75 40.82 -26.95Q40.82 -31.15 39.53 -34.23Q38.23 -37.3 35.89 -38.99Q33.54 -40.67 30.32 -40.67Q27.15 -40.67 24.88 -38.99Q22.61 -37.3 21.41 -34.23Q20.21 -31.15 20.21 -26.95Q20.21 -22.71 21.41 -19.65Q22.61 -16.6 24.88 -14.99Q27.15 -13.38 30.32 -13.38ZM94.12 -72.75V0H77.08V-57.37H76.84L60.33 -46.53V-61.57L77.47 -72.75ZM130.48 -52.93V-40.09H95.18V-52.93ZM103.28 -66.5H120.22V-17.04Q120.22 -14.75 121.23 -13.75Q122.23 -12.74 124.81 -12.74Q125.89 -12.74 127.62 -12.84Q129.36 -12.94 130.14 -13.04L131.11 -0.29Q129.11 0.1 126.43 0.22Q123.74 0.34 121.1 0.34Q112.07 0.34 107.68 -3.42Q103.28 -7.18 103.28 -14.79Z"/></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#6b56e8" fill-opacity="0.4"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#6b56e8" fill-opacity="0.75"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#161618"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#161618" stroke-width="4.6" stroke-linecap="round"/></g></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16"><rect width="16" height="16" fill="#fff"/><path d="M10.5 3.5 8.5 5.4" stroke="#000" stroke-width="3.6" stroke-linecap="round"/></mask><g mask="url(#cut)"><rect x="3" y="5.5" width="2" height="8.5" rx="1" fill="#b6a8ff" fill-opacity="0.5"/><rect x="6" y="4" width="2" height="10" rx="1" fill="#b6a8ff" fill-opacity="0.85"/></g><rect x="9" y="2" width="3" height="12" rx="1.5" fill="#ededef"/><path d="M10.5 3.5 8.5 5.4" fill="none" stroke="#ededef" stroke-width="2.2" stroke-linecap="round"/></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#b6a8ff" fill-opacity="0.35"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#b6a8ff" fill-opacity="0.65"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#ededef"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#ededef" stroke-width="4.6" stroke-linecap="round"/></g></svg>
+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630"><rect width="1200" height="630" fill="#0f0f11"/><g fill="#2a2a30"><circle cx="20" cy="15" r="1.4"/><circle cx="20" cy="55" r="1.4"/><circle cx="20" cy="95" r="1.4"/><circle cx="20" cy="135" r="1.4"/><circle cx="20" cy="175" r="1.4"/><circle cx="20" cy="215" r="1.4"/><circle cx="20" cy="255" r="1.4"/><circle cx="20" cy="295" r="1.4"/><circle cx="20" cy="335" r="1.4"/><circle cx="20" cy="375" r="1.4"/><circle cx="20" cy="415" r="1.4"/><circle cx="20" cy="455" r="1.4"/><circle cx="20" cy="495" r="1.4"/><circle cx="20" cy="535" r="1.4"/><circle cx="20" cy="575" r="1.4"/><circle cx="20" cy="615" r="1.4"/><circle cx="60" cy="15" r="1.4"/><circle cx="60" cy="55" r="1.4"/><circle cx="60" cy="95" r="1.4"/><circle cx="60" cy="135" r="1.4"/><circle cx="60" cy="175" r="1.4"/><circle cx="60" cy="215" r="1.4"/><circle cx="60" cy="255" r="1.4"/><circle cx="60" cy="295" r="1.4"/><circle cx="60" cy="335" r="1.4"/><circle cx="60" cy="375" r="1.4"/><circle cx="60" cy="415" r="1.4"/><circle cx="60" cy="455" r="1.4"/><circle cx="60" cy="495" r="1.4"/><circle cx="60" cy="535" r="1.4"/><circle cx="60" cy="575" r="1.4"/><circle cx="60" cy="615" r="1.4"/><circle cx="100" cy="15" r="1.4"/><circle cx="100" cy="55" r="1.4"/><circle cx="100" cy="95" r="1.4"/><circle cx="100" cy="135" r="1.4"/><circle cx="100" cy="175" r="1.4"/><circle cx="100" cy="215" r="1.4"/><circle cx="100" cy="255" r="1.4"/><circle cx="100" cy="295" r="1.4"/><circle cx="100" cy="335" r="1.4"/><circle cx="100" cy="375" r="1.4"/><circle cx="100" cy="415" r="1.4"/><circle cx="100" cy="455" r="1.4"/><circle cx="100" cy="495" r="1.4"/><circle cx="100" cy="535" r="1.4"/><circle cx="100" cy="575" r="1.4"/><circle cx="100" cy="615" r="1.4"/><circle cx="140" cy="15" r="1.4"/><circle cx="140" cy="55" r="1.4"/><circle cx="140" cy="95" r="1.4"/><circle cx="140" cy="135" r="1.4"/><circle cx="140" cy="175" r="1.4"/><circle cx="140" cy="215" r="1.4"/><circle cx="140" cy="255" r="1.4"/><circle cx="140" cy="295" r="1.4"/><circle cx="140" cy="335" r="1.4"/><circle cx="140" cy="375" r="1.4"/><circle cx="140" cy="415" r="1.4"/><circle cx="140" cy="455" r="1.4"/><circle cx="140" cy="495" r="1.4"/><circle cx="140" cy="535" r="1.4"/><circle cx="140" cy="575" r="1.4"/><circle cx="140" cy="615" r="1.4"/><circle cx="180" cy="15" r="1.4"/><circle cx="180" cy="55" r="1.4"/><circle cx="180" cy="95" r="1.4"/><circle cx="180" cy="135" r="1.4"/><circle cx="180" cy="175" r="1.4"/><circle cx="180" cy="215" r="1.4"/><circle cx="180" cy="255" r="1.4"/><circle cx="180" cy="295" r="1.4"/><circle cx="180" cy="335" r="1.4"/><circle cx="180" cy="375" r="1.4"/><circle cx="180" cy="415" r="1.4"/><circle cx="180" cy="455" r="1.4"/><circle cx="180" cy="495" r="1.4"/><circle cx="180" cy="535" r="1.4"/><circle cx="180" cy="575" r="1.4"/><circle cx="180" cy="615" r="1.4"/><circle cx="220" cy="15" r="1.4"/><circle cx="220" cy="55" r="1.4"/><circle cx="220" cy="95" r="1.4"/><circle cx="220" cy="135" r="1.4"/><circle cx="220" cy="175" r="1.4"/><circle cx="220" cy="215" r="1.4"/><circle cx="220" cy="255" r="1.4"/><circle cx="220" cy="295" r="1.4"/><circle cx="220" cy="335" r="1.4"/><circle cx="220" cy="375" r="1.4"/><circle cx="220" cy="415" r="1.4"/><circle cx="220" cy="455" r="1.4"/><circle cx="220" cy="495" r="1.4"/><circle cx="220" cy="535" r="1.4"/><circle cx="220" cy="575" r="1.4"/><circle cx="220" cy="615" r="1.4"/><circle cx="260" cy="15" r="1.4"/><circle cx="260" cy="55" r="1.4"/><circle cx="260" cy="95" r="1.4"/><circle cx="260" cy="135" r="1.4"/><circle cx="260" cy="175" r="1.4"/><circle cx="260" cy="215" r="1.4"/><circle cx="260" cy="255" r="1.4"/><circle cx="260" cy="295" r="1.4"/><circle cx="260" cy="335" r="1.4"/><circle cx="260" cy="375" r="1.4"/><circle cx="260" cy="415" r="1.4"/><circle cx="260" cy="455" r="1.4"/><circle cx="260" cy="495" r="1.4"/><circle cx="260" cy="535" r="1.4"/><circle cx="260" cy="575" r="1.4"/><circle cx="260" cy="615" r="1.4"/><circle cx="300" cy="15" r="1.4"/><circle cx="300" cy="55" r="1.4"/><circle cx="300" cy="95" r="1.4"/><circle cx="300" cy="135" r="1.4"/><circle cx="300" cy="175" r="1.4"/><circle cx="300" cy="215" r="1.4"/><circle cx="300" cy="255" r="1.4"/><circle cx="300" cy="295" r="1.4"/><circle cx="300" cy="335" r="1.4"/><circle cx="300" cy="375" r="1.4"/><circle cx="300" cy="415" r="1.4"/><circle cx="300" cy="455" r="1.4"/><circle cx="300" cy="495" r="1.4"/><circle cx="300" cy="535" r="1.4"/><circle cx="300" cy="575" r="1.4"/><circle cx="300" cy="615" r="1.4"/><circle cx="340" cy="15" r="1.4"/><circle cx="340" cy="55" r="1.4"/><circle cx="340" cy="95" r="1.4"/><circle cx="340" cy="135" r="1.4"/><circle cx="340" cy="175" r="1.4"/><circle cx="340" cy="215" r="1.4"/><circle cx="340" cy="255" r="1.4"/><circle cx="340" cy="295" r="1.4"/><circle cx="340" cy="335" r="1.4"/><circle cx="340" cy="375" r="1.4"/><circle cx="340" cy="415" r="1.4"/><circle cx="340" cy="455" r="1.4"/><circle cx="340" cy="495" r="1.4"/><circle cx="340" cy="535" r="1.4"/><circle cx="340" cy="575" r="1.4"/><circle cx="340" cy="615" r="1.4"/><circle cx="380" cy="15" r="1.4"/><circle cx="380" cy="55" r="1.4"/><circle cx="380" cy="95" r="1.4"/><circle cx="380" cy="135" r="1.4"/><circle cx="380" cy="175" r="1.4"/><circle cx="380" cy="215" r="1.4"/><circle cx="380" cy="255" r="1.4"/><circle cx="380" cy="295" r="1.4"/><circle cx="380" cy="335" r="1.4"/><circle cx="380" cy="375" r="1.4"/><circle cx="380" cy="415" r="1.4"/><circle cx="380" cy="455" r="1.4"/><circle cx="380" cy="495" r="1.4"/><circle cx="380" cy="535" r="1.4"/><circle cx="380" cy="575" r="1.4"/><circle cx="380" cy="615" r="1.4"/><circle cx="420" cy="15" r="1.4"/><circle cx="420" cy="55" r="1.4"/><circle cx="420" cy="95" r="1.4"/><circle cx="420" cy="135" r="1.4"/><circle cx="420" cy="175" r="1.4"/><circle cx="420" cy="215" r="1.4"/><circle cx="420" cy="255" r="1.4"/><circle cx="420" cy="295" r="1.4"/><circle cx="420" cy="335" r="1.4"/><circle cx="420" cy="375" r="1.4"/><circle cx="420" cy="415" r="1.4"/><circle cx="420" cy="455" r="1.4"/><circle cx="420" cy="495" r="1.4"/><circle cx="420" cy="535" r="1.4"/><circle cx="420" cy="575" r="1.4"/><circle cx="420" cy="615" r="1.4"/><circle cx="460" cy="15" r="1.4"/><circle cx="460" cy="55" r="1.4"/><circle cx="460" cy="95" r="1.4"/><circle cx="460" cy="135" r="1.4"/><circle cx="460" cy="175" r="1.4"/><circle cx="460" cy="215" r="1.4"/><circle cx="460" cy="255" r="1.4"/><circle cx="460" cy="295" r="1.4"/><circle cx="460" cy="335" r="1.4"/><circle cx="460" cy="375" r="1.4"/><circle cx="460" cy="415" r="1.4"/><circle cx="460" cy="455" r="1.4"/><circle cx="460" cy="495" r="1.4"/><circle cx="460" cy="535" r="1.4"/><circle cx="460" cy="575" r="1.4"/><circle cx="460" cy="615" r="1.4"/><circle cx="500" cy="15" r="1.4"/><circle cx="500" cy="55" r="1.4"/><circle cx="500" cy="95" r="1.4"/><circle cx="500" cy="135" r="1.4"/><circle cx="500" cy="175" r="1.4"/><circle cx="500" cy="215" r="1.4"/><circle cx="500" cy="255" r="1.4"/><circle cx="500" cy="295" r="1.4"/><circle cx="500" cy="335" r="1.4"/><circle cx="500" cy="375" r="1.4"/><circle cx="500" cy="415" r="1.4"/><circle cx="500" cy="455" r="1.4"/><circle cx="500" cy="495" r="1.4"/><circle cx="500" cy="535" r="1.4"/><circle cx="500" cy="575" r="1.4"/><circle cx="500" cy="615" r="1.4"/><circle cx="540" cy="15" r="1.4"/><circle cx="540" cy="55" r="1.4"/><circle cx="540" cy="95" r="1.4"/><circle cx="540" cy="135" r="1.4"/><circle cx="540" cy="175" r="1.4"/><circle cx="540" cy="215" r="1.4"/><circle cx="540" cy="255" r="1.4"/><circle cx="540" cy="295" r="1.4"/><circle cx="540" cy="335" r="1.4"/><circle cx="540" cy="375" r="1.4"/><circle cx="540" cy="415" r="1.4"/><circle cx="540" cy="455" r="1.4"/><circle cx="540" cy="495" r="1.4"/><circle cx="540" cy="535" r="1.4"/><circle cx="540" cy="575" r="1.4"/><circle cx="540" cy="615" r="1.4"/><circle cx="580" cy="15" r="1.4"/><circle cx="580" cy="55" r="1.4"/><circle cx="580" cy="95" r="1.4"/><circle cx="580" cy="135" r="1.4"/><circle cx="580" cy="175" r="1.4"/><circle cx="580" cy="215" r="1.4"/><circle cx="580" cy="255" r="1.4"/><circle cx="580" cy="295" r="1.4"/><circle cx="580" cy="335" r="1.4"/><circle cx="580" cy="375" r="1.4"/><circle cx="580" cy="415" r="1.4"/><circle cx="580" cy="455" r="1.4"/><circle cx="580" cy="495" r="1.4"/><circle cx="580" cy="535" r="1.4"/><circle cx="580" cy="575" r="1.4"/><circle cx="580" cy="615" r="1.4"/><circle cx="620" cy="15" r="1.4"/><circle cx="620" cy="55" r="1.4"/><circle cx="620" cy="95" r="1.4"/><circle cx="620" cy="135" r="1.4"/><circle cx="620" cy="175" r="1.4"/><circle cx="620" cy="215" r="1.4"/><circle cx="620" cy="255" r="1.4"/><circle cx="620" cy="295" r="1.4"/><circle cx="620" cy="335" r="1.4"/><circle cx="620" cy="375" r="1.4"/><circle cx="620" cy="415" r="1.4"/><circle cx="620" cy="455" r="1.4"/><circle cx="620" cy="495" r="1.4"/><circle cx="620" cy="535" r="1.4"/><circle cx="620" cy="575" r="1.4"/><circle cx="620" cy="615" r="1.4"/><circle cx="660" cy="15" r="1.4"/><circle cx="660" cy="55" r="1.4"/><circle cx="660" cy="95" r="1.4"/><circle cx="660" cy="135" r="1.4"/><circle cx="660" cy="175" r="1.4"/><circle cx="660" cy="215" r="1.4"/><circle cx="660" cy="255" r="1.4"/><circle cx="660" cy="295" r="1.4"/><circle cx="660" cy="335" r="1.4"/><circle cx="660" cy="375" r="1.4"/><circle cx="660" cy="415" r="1.4"/><circle cx="660" cy="455" r="1.4"/><circle cx="660" cy="495" r="1.4"/><circle cx="660" cy="535" r="1.4"/><circle cx="660" cy="575" r="1.4"/><circle cx="660" cy="615" r="1.4"/><circle cx="700" cy="15" r="1.4"/><circle cx="700" cy="55" r="1.4"/><circle cx="700" cy="95" r="1.4"/><circle cx="700" cy="135" r="1.4"/><circle cx="700" cy="175" r="1.4"/><circle cx="700" cy="215" r="1.4"/><circle cx="700" cy="255" r="1.4"/><circle cx="700" cy="295" r="1.4"/><circle cx="700" cy="335" r="1.4"/><circle cx="700" cy="375" r="1.4"/><circle cx="700" cy="415" r="1.4"/><circle cx="700" cy="455" r="1.4"/><circle cx="700" cy="495" r="1.4"/><circle cx="700" cy="535" r="1.4"/><circle cx="700" cy="575" r="1.4"/><circle cx="700" cy="615" r="1.4"/><circle cx="740" cy="15" r="1.4"/><circle cx="740" cy="55" r="1.4"/><circle cx="740" cy="95" r="1.4"/><circle cx="740" cy="135" r="1.4"/><circle cx="740" cy="175" r="1.4"/><circle cx="740" cy="215" r="1.4"/><circle cx="740" cy="255" r="1.4"/><circle cx="740" cy="295" r="1.4"/><circle cx="740" cy="335" r="1.4"/><circle cx="740" cy="375" r="1.4"/><circle cx="740" cy="415" r="1.4"/><circle cx="740" cy="455" r="1.4"/><circle cx="740" cy="495" r="1.4"/><circle cx="740" cy="535" r="1.4"/><circle cx="740" cy="575" r="1.4"/><circle cx="740" cy="615" r="1.4"/><circle cx="780" cy="15" r="1.4"/><circle cx="780" cy="55" r="1.4"/><circle cx="780" cy="95" r="1.4"/><circle cx="780" cy="135" r="1.4"/><circle cx="780" cy="175" r="1.4"/><circle cx="780" cy="215" r="1.4"/><circle cx="780" cy="255" r="1.4"/><circle cx="780" cy="295" r="1.4"/><circle cx="780" cy="335" r="1.4"/><circle cx="780" cy="375" r="1.4"/><circle cx="780" cy="415" r="1.4"/><circle cx="780" cy="455" r="1.4"/><circle cx="780" cy="495" r="1.4"/><circle cx="780" cy="535" r="1.4"/><circle cx="780" cy="575" r="1.4"/><circle cx="780" cy="615" r="1.4"/><circle cx="820" cy="15" r="1.4"/><circle cx="820" cy="55" r="1.4"/><circle cx="820" cy="95" r="1.4"/><circle cx="820" cy="135" r="1.4"/><circle cx="820" cy="175" r="1.4"/><circle cx="820" cy="215" r="1.4"/><circle cx="820" cy="255" r="1.4"/><circle cx="820" cy="295" r="1.4"/><circle cx="820" cy="335" r="1.4"/><circle cx="820" cy="375" r="1.4"/><circle cx="820" cy="415" r="1.4"/><circle cx="820" cy="455" r="1.4"/><circle cx="820" cy="495" r="1.4"/><circle cx="820" cy="535" r="1.4"/><circle cx="820" cy="575" r="1.4"/><circle cx="820" cy="615" r="1.4"/><circle cx="860" cy="15" r="1.4"/><circle cx="860" cy="55" r="1.4"/><circle cx="860" cy="95" r="1.4"/><circle cx="860" cy="135" r="1.4"/><circle cx="860" cy="175" r="1.4"/><circle cx="860" cy="215" r="1.4"/><circle cx="860" cy="255" r="1.4"/><circle cx="860" cy="295" r="1.4"/><circle cx="860" cy="335" r="1.4"/><circle cx="860" cy="375" r="1.4"/><circle cx="860" cy="415" r="1.4"/><circle cx="860" cy="455" r="1.4"/><circle cx="860" cy="495" r="1.4"/><circle cx="860" cy="535" r="1.4"/><circle cx="860" cy="575" r="1.4"/><circle cx="860" cy="615" r="1.4"/><circle cx="900" cy="15" r="1.4"/><circle cx="900" cy="55" r="1.4"/><circle cx="900" cy="95" r="1.4"/><circle cx="900" cy="135" r="1.4"/><circle cx="900" cy="175" r="1.4"/><circle cx="900" cy="215" r="1.4"/><circle cx="900" cy="255" r="1.4"/><circle cx="900" cy="295" r="1.4"/><circle cx="900" cy="335" r="1.4"/><circle cx="900" cy="375" r="1.4"/><circle cx="900" cy="415" r="1.4"/><circle cx="900" cy="455" r="1.4"/><circle cx="900" cy="495" r="1.4"/><circle cx="900" cy="535" r="1.4"/><circle cx="900" cy="575" r="1.4"/><circle cx="900" cy="615" r="1.4"/><circle cx="940" cy="15" r="1.4"/><circle cx="940" cy="55" r="1.4"/><circle cx="940" cy="95" r="1.4"/><circle cx="940" cy="135" r="1.4"/><circle cx="940" cy="175" r="1.4"/><circle cx="940" cy="215" r="1.4"/><circle cx="940" cy="255" r="1.4"/><circle cx="940" cy="295" r="1.4"/><circle cx="940" cy="335" r="1.4"/><circle cx="940" cy="375" r="1.4"/><circle cx="940" cy="415" r="1.4"/><circle cx="940" cy="455" r="1.4"/><circle cx="940" cy="495" r="1.4"/><circle cx="940" cy="535" r="1.4"/><circle cx="940" cy="575" r="1.4"/><circle cx="940" cy="615" r="1.4"/><circle cx="980" cy="15" r="1.4"/><circle cx="980" cy="55" r="1.4"/><circle cx="980" cy="95" r="1.4"/><circle cx="980" cy="135" r="1.4"/><circle cx="980" cy="175" r="1.4"/><circle cx="980" cy="215" r="1.4"/><circle cx="980" cy="255" r="1.4"/><circle cx="980" cy="295" r="1.4"/><circle cx="980" cy="335" r="1.4"/><circle cx="980" cy="375" r="1.4"/><circle cx="980" cy="415" r="1.4"/><circle cx="980" cy="455" r="1.4"/><circle cx="980" cy="495" r="1.4"/><circle cx="980" cy="535" r="1.4"/><circle cx="980" cy="575" r="1.4"/><circle cx="980" cy="615" r="1.4"/><circle cx="1020" cy="15" r="1.4"/><circle cx="1020" cy="55" r="1.4"/><circle cx="1020" cy="95" r="1.4"/><circle cx="1020" cy="135" r="1.4"/><circle cx="1020" cy="175" r="1.4"/><circle cx="1020" cy="215" r="1.4"/><circle cx="1020" cy="255" r="1.4"/><circle cx="1020" cy="295" r="1.4"/><circle cx="1020" cy="335" r="1.4"/><circle cx="1020" cy="375" r="1.4"/><circle cx="1020" cy="415" r="1.4"/><circle cx="1020" cy="455" r="1.4"/><circle cx="1020" cy="495" r="1.4"/><circle cx="1020" cy="535" r="1.4"/><circle cx="1020" cy="575" r="1.4"/><circle cx="1020" cy="615" r="1.4"/><circle cx="1060" cy="15" r="1.4"/><circle cx="1060" cy="55" r="1.4"/><circle cx="1060" cy="95" r="1.4"/><circle cx="1060" cy="135" r="1.4"/><circle cx="1060" cy="175" r="1.4"/><circle cx="1060" cy="215" r="1.4"/><circle cx="1060" cy="255" r="1.4"/><circle cx="1060" cy="295" r="1.4"/><circle cx="1060" cy="335" r="1.4"/><circle cx="1060" cy="375" r="1.4"/><circle cx="1060" cy="415" r="1.4"/><circle cx="1060" cy="455" r="1.4"/><circle cx="1060" cy="495" r="1.4"/><circle cx="1060" cy="535" r="1.4"/><circle cx="1060" cy="575" r="1.4"/><circle cx="1060" cy="615" r="1.4"/><circle cx="1100" cy="15" r="1.4"/><circle cx="1100" cy="55" r="1.4"/><circle cx="1100" cy="95" r="1.4"/><circle cx="1100" cy="135" r="1.4"/><circle cx="1100" cy="175" r="1.4"/><circle cx="1100" cy="215" r="1.4"/><circle cx="1100" cy="255" r="1.4"/><circle cx="1100" cy="295" r="1.4"/><circle cx="1100" cy="335" r="1.4"/><circle cx="1100" cy="375" r="1.4"/><circle cx="1100" cy="415" r="1.4"/><circle cx="1100" cy="455" r="1.4"/><circle cx="1100" cy="495" r="1.4"/><circle cx="1100" cy="535" r="1.4"/><circle cx="1100" cy="575" r="1.4"/><circle cx="1100" cy="615" r="1.4"/><circle cx="1140" cy="15" r="1.4"/><circle cx="1140" cy="55" r="1.4"/><circle cx="1140" cy="95" r="1.4"/><circle cx="1140" cy="135" r="1.4"/><circle cx="1140" cy="175" r="1.4"/><circle cx="1140" cy="215" r="1.4"/><circle cx="1140" cy="255" r="1.4"/><circle cx="1140" cy="295" r="1.4"/><circle cx="1140" cy="335" r="1.4"/><circle cx="1140" cy="375" r="1.4"/><circle cx="1140" cy="415" r="1.4"/><circle cx="1140" cy="455" r="1.4"/><circle cx="1140" cy="495" r="1.4"/><circle cx="1140" cy="535" r="1.4"/><circle cx="1140" cy="575" r="1.4"/><circle cx="1140" cy="615" r="1.4"/><circle cx="1180" cy="15" r="1.4"/><circle cx="1180" cy="55" r="1.4"/><circle cx="1180" cy="95" r="1.4"/><circle cx="1180" cy="135" r="1.4"/><circle cx="1180" cy="175" r="1.4"/><circle cx="1180" cy="215" r="1.4"/><circle cx="1180" cy="255" r="1.4"/><circle cx="1180" cy="295" r="1.4"/><circle cx="1180" cy="335" r="1.4"/><circle cx="1180" cy="375" r="1.4"/><circle cx="1180" cy="415" r="1.4"/><circle cx="1180" cy="455" r="1.4"/><circle cx="1180" cy="495" r="1.4"/><circle cx="1180" cy="535" r="1.4"/><circle cx="1180" cy="575" r="1.4"/><circle cx="1180" cy="615" r="1.4"/></g><g transform="translate(379.41 160.66) scale(1.9)"><g transform="translate(-22.77 -16.35) scale(3.3)"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#b6a8ff" fill-opacity="0.35"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#b6a8ff" fill-opacity="0.65"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#ededef"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#ededef" stroke-width="4.6" stroke-linecap="round"/></g></g><path transform="translate(101.08 72.75)" fill="#ededef" d="M30.42 21.29Q22.8 21.29 17.5 19.26Q12.21 17.24 9.16 13.67Q6.1 10.11 5.22 5.42L20.75 4.1Q21.19 5.66 22.46 6.86Q23.73 8.06 25.71 8.74Q27.69 9.42 30.37 9.42Q35.3 9.42 37.89 6.93Q40.48 4.44 40.48 -0.59V-9.28H39.89Q38.72 -6.54 36.65 -4.57Q34.57 -2.59 31.67 -1.56Q28.76 -0.54 25.1 -0.54Q18.7 -0.54 13.7 -3.64Q8.69 -6.74 5.83 -12.6Q2.98 -18.46 2.98 -26.76Q2.98 -35.3 5.91 -41.36Q8.84 -47.41 13.82 -50.63Q18.8 -53.86 25 -53.86Q28.81 -53.86 31.71 -52.66Q34.62 -51.46 36.77 -49.41Q38.92 -47.36 40.23 -44.73H40.48V-52.93H57.23V-2.1Q57.23 5.91 53.91 11.08Q50.59 16.26 44.56 18.77Q38.53 21.29 30.42 21.29ZM30.32 -13.38Q33.54 -13.38 35.89 -15.01Q38.23 -16.65 39.53 -19.7Q40.82 -22.75 40.82 -26.95Q40.82 -31.15 39.53 -34.23Q38.23 -37.3 35.89 -38.99Q33.54 -40.67 30.32 -40.67Q27.15 -40.67 24.88 -38.99Q22.61 -37.3 21.41 -34.23Q20.21 -31.15 20.21 -26.95Q20.21 -22.71 21.41 -19.65Q22.61 -16.6 24.88 -14.99Q27.15 -13.38 30.32 -13.38ZM94.12 -72.75V0H77.08V-57.37H76.84L60.33 -46.53V-61.57L77.47 -72.75ZM130.48 -52.93V-40.09H95.18V-52.93ZM103.28 -66.5H120.22V-17.04Q120.22 -14.75 121.23 -13.75Q122.23 -12.74 124.81 -12.74Q125.89 -12.74 127.62 -12.84Q129.36 -12.94 130.14 -13.04L131.11 -0.29Q129.11 0.1 126.43 0.22Q123.74 0.34 121.1 0.34Q112.07 0.34 107.68 -3.42Q103.28 -7.18 103.28 -14.79Z"/></g><text x="600" y="470" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="40" font-weight="500" fill="#a0a0a8" letter-spacing="-0.5">A git forge for agents</text><text x="600" y="560" text-anchor="middle" font-family="JetBrains Mono, ui-monospace, Consolas, monospace" font-size="26" fill="#b6a8ff">g1t.sh</text></svg>
+3−0
5252 },
5353 lastUpdated: true,
5454 head: [
55+ { tag: 'link', attrs: { rel: 'icon', href: '/favicon.ico', sizes: '32x32' } },
56+ { tag: 'link', attrs: { rel: 'icon', type: 'image/png', sizes: '192x192', href: '/icon-192.png' } },
57+ { tag: 'link', attrs: { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' } },
5558 { tag: 'link', attrs: { rel: 'preconnect', href: 'https://fonts.googleapis.com' } },
5659 {
5760 tag: 'link',
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+1−8
1−<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2− <rect width="32" height="32" rx="7" fill="#161618"/>
3− <g transform="translate(4.5 4.6) scale(0.72)">
4− <path d="M16 2.5 28 9.4 16 16.3 4 9.4Z" fill="#ededef"/>
5− <path d="M4 10.9 15.3 17.4V30.2L4 23.7Z" fill="#ededef" fill-opacity="0.55"/>
6− <path d="M28 10.9 16.7 17.4V30.2L28 23.7Z" fill="#ededef" fill-opacity="0.25"/>
7− </g>
8−</svg>
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.f{fill:#161618}.f.s{fill:none;stroke:#161618}.m{fill:#6b56e8}@media (prefers-color-scheme: dark){.f{fill:#ededef}.f.s{fill:none;stroke:#ededef}.m{fill:#b6a8ff}}</style><mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16"><rect width="16" height="16" fill="#fff"/><path d="M10.5 3.5 8.5 5.4" stroke="#000" stroke-width="3.6" stroke-linecap="round"/></mask><g mask="url(#cut)"><rect class="m" x="3" y="5.5" width="2" height="8.5" rx="1" fill-opacity="0.5"/><rect class="m" x="6" y="4" width="2" height="10" rx="1" fill-opacity="0.85"/></g><rect class="f" x="9" y="2" width="3" height="12" rx="1.5"/><path class="f s" d="M10.5 3.5 8.5 5.4" stroke-width="2.2" stroke-linecap="round"/></svg>
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+7−4
11 ---
2−// The cube, the name and "Docs": the same mark as g1t.sh, so the two read
2+// The fleet, the name and "Docs": the same mark as g1t.sh, so the two read
33 // as one product.
44 ---
55
66 <a href="/" class="g1t-title" aria-label="g1t docs home">
77 <svg viewBox="0 0 32 32" aria-hidden="true">
8− <path d="M16 2.5 28 9.4 16 16.3 4 9.4Z" fill="currentColor"></path>
9− <path d="M4 10.9 15.3 17.4V30.2L4 23.7Z" fill="currentColor" fill-opacity="0.55"></path>
10− <path d="M28 10.9 16.7 17.4V30.2L28 23.7Z" fill="currentColor" fill-opacity="0.25"></path>
8+ <g transform="translate(0.7 0.5)">
9+ <rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="var(--g1t-merged)" fill-opacity="0.35"></rect>
10+ <rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="var(--g1t-merged)" fill-opacity="0.65"></rect>
11+ <rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="currentColor"></rect>
12+ <path d="M21.7 7.2 17.6 10.9" fill="none" stroke="currentColor" stroke-width="4.6" stroke-linecap="round"></path>
13+ </g>
1114 </svg>
1215 <span class="g1t-title-name">g1t</span>
1316 <span class="g1t-title-docs">Docs</span>
+59−2
6969 | A review by a g1t agent | Yes |
7070 | Catching up with `main` | Yes, when it needed an agent |
7171 | Planning an [outcome](/guides/outcomes/) | Yes |
72−| Acceptance checks | No |
73−| The [merge queue](/guides/merge-queue/) | No |
72+| Acceptance checks | [Sandbox time](#sandbox-time) |
73+| The [merge queue](/guides/merge-queue/) | [Sandbox time](#sandbox-time) |
74+| [Workflow](/guides/actions/) jobs | [Sandbox time](#sandbox-time) |
7475 | [Deployments](/guides/deployments/) | The plan, and builds and usage past it. Never free. |
7576 | Repositories, git, issues, pull requests, the API and MCP | No |
7677
8687 assigned the issue. That is why only members of a workspace can put g1t
8788 agents to work on its repositories.
8889
90+## Sandbox time
91+
92+Every sandbox g1t starts for a workspace runs on Cloudflare Containers, and
93+Cloudflare charges g1t for every second of it. So each one is metered by
94+the second, from start to stop, whatever it was for: agents, reviews,
95+revisions, catch-ups, planning, acceptance checks, the merge queue and
96+workflow jobs. It is charged to the workspace that owns the repository.
97+
98+| | |
99+| --- | --- |
100+| Free each month | 500 minutes (calendar month, UTC) |
101+| Past that | $0.003 a minute, by the second |
102+| What it costs g1t | about $0.0013 a minute (Containers, standard-1) |
103+
104+Deploy builds are not counted here: [Deployments](/guides/deployments/)
105+charges them by the second on its own plan.
106+
107+Each sandbox is one line on the [statement](#the-statement), such as
108+*Checks on acme/api#12: 3m 12s of sandbox time*, with whether it fell
109+within the free minutes. While g1t is being built out it is recorded but
110+not charged.
111+
112+## Usage limits
113+
114+Everything a workspace uses costs g1t money at Cloudflare or a model
115+provider before the workspace pays for it. So, as Fly and Cloudflare do
116+with new accounts, every workspace has a limit on usage not yet paid for.
117+When it is reached, the workspace's work stops until it pays, or the
118+month turns:
119+
120+- **No new sandboxes.** Assigning an agent, planning, asking for a
121+ review and workflow jobs are refused with `402 payment_required` and the
122+ reason; acceptance checks and the merge queue wait. Runs already under
123+ way finish.
124+- **No new builds**, and **deployed apps pause**: they answer with a page
125+ saying so (`402`) and run nothing. Once the workspace is under its limit
126+ again, g1t rebuilds each one from the commit it was serving, by itself.
127+
128+What counts is this month's usage (UTC), each item at what it cost g1t or
129+what it is charged, whichever is more, less what was paid this month. It
130+counts while g1t is free too: free is a price of nothing, not a way around
131+the limit.
132+
133+| Workspace | Limit |
134+| --- | --- |
135+| **New**: has not paid g1t yet | $3: the free allowances and a little more |
136+| **Paid**: has paid g1t | twice what it has paid, from $25 up to $1,000 |
137+| **Reviewed** | what g1t set for it, after talking with you |
138+
139+Payments in test mode are not money, so they do not raise the limit. To
140+go past $1,000, write to support.
141+
142+At 80% the Billing page turns amber and says how close the workspace is.
143+An owner can set a lower **spend limit** of their own under **Settings →
144+Billing → Usage limit**; work stops at whichever is lower.
145+
89146 ## Add credit
90147
91148 Only an owner of the workspace can add credit.
+2−0
2424 name="description"
2525 content="Every g1t API endpoint, with an explorer to call them from the page."
2626 />
27+ <link rel="icon" href="/favicon.ico" sizes="32x32" />
2728 <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
29+ <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
2830 <link rel="canonical" href="https://docs.g1t.sh/api/reference/" />
2931 <link rel="preconnect" href="https://fonts.googleapis.com" />
3032 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+12−9
11 /**
2− * g1t's mark: an isometric cube, the unit of work in its artwork. Three
3− * faces in three tones of the text colour, split by hairline gaps, so it
4− * holds at any size and on any background.
2+ * g1t's mark, "the fleet": three 1s stepping back in depth, the agents at
3+ * work behind the one change in front. The front 1 takes the text colour;
4+ * the two behind it fade into lavender. The same shapes as the favicon and
5+ * the brand files in .g1t/brand and public/brand.
56 */
67 export function Mark({ className }: { className?: string }) {
78 return (
89 <svg viewBox="0 0 32 32" className={className} aria-hidden="true">
9− {/* Top face */}
10− <path d="M16 2.5 28 9.4 16 16.3 4 9.4Z" fill="currentColor" />
11− {/* Left face */}
12− <path d="M4 10.9 15.3 17.4V30.2L4 23.7Z" fill="currentColor" fillOpacity="0.55" />
13− {/* Right face */}
14− <path d="M28 10.9 16.7 17.4V30.2L28 23.7Z" fill="currentColor" fillOpacity="0.25" />
10+ <g transform="translate(0.7 0.5)">
11+ {/* The agents behind */}
12+ <rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="var(--g1t-merged)" fillOpacity="0.35" />
13+ <rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="var(--g1t-merged)" fillOpacity="0.65" />
14+ {/* The 1 in front */}
15+ <rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="currentColor" />
16+ <path d="M21.7 7.2 17.6 10.9" fill="none" stroke="currentColor" strokeWidth="4.6" strokeLinecap="round" />
17+ </g>
1518 </svg>
1619 );
1720 }
+3−0
4141 import { getViewer, roleIn, viewerMiddleware } from "./lib/session.server";
4242
4343 export const links: Route.LinksFunction = () => [
44+ { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
4445 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
46+ { rel: "icon", type: "image/png", sizes: "192x192", href: "/icon-192.png" },
47+ { rel: "apple-touch-icon", href: "/apple-touch-icon.png" },
4548 { rel: "preconnect", href: "https://fonts.googleapis.com" },
4649 {
4750 rel: "preconnect",
+102−4
77 type DeployUsage,
88 type Feature,
99 type FeatureState,
10+ type Limit,
1011 } from "@g1t/contracts";
1112
1213 import type { Route } from "./+types/billing";
4647 await billing.confirm(slug, viewer, session);
4748 throw redirect(`/${slug}/-/billing?added=1`);
4849 }
49− const [account, ledger, features, deployUsage] = await Promise.all([
50+ const [account, ledger, features, deployUsage, limit] = await Promise.all([
5051 billing.account(slug, viewer),
5152 billing.ledger(slug, viewer),
5253 billing.features(slug, viewer),
5354 deployments.usage(slug, viewer),
55+ billing.limit(slug, viewer),
5456 ]);
5557 return {
5658 slug,
5961 ledger: unwrap(ledger),
6062 features: unwrap(features),
6163 deployUsage: deployUsage.ok ? deployUsage.value : null,
64+ limit: limit.ok ? limit.value : null,
6265 added: url.searchParams.has("added"),
6366 subscribed: url.searchParams.has("subscribed"),
6467 };
7073 const form = await request.formData();
7174 const page = `${new URL(request.url).origin}/${params.owner.toLowerCase()}/-/billing`;
7275 const intent = form.get("intent");
76+ if (intent === "spend-limit" || intent === "no-spend-limit") {
77+ const amount = Number(form.get("limit"));
78+ if (intent === "spend-limit" && !(Number.isFinite(amount) && amount >= 0)) {
79+ return { error: "A spend limit is a dollar amount." };
80+ }
81+ const set = await billing.setSpendLimit(
82+ user,
83+ params.owner,
84+ intent === "spend-limit" ? Math.round(amount * MICROS_PER_DOLLAR) : null,
85+ );
86+ return set.ok ? null : { error: set.error.message };
87+ }
7388 if (intent === "subscribe" || intent === "cancel" || intent === "resume") {
7489 const feature = String(form.get("feature")) as Feature;
7590 if (intent !== "subscribe") {
99114 }
100115
101116 export default function WorkspaceBilling({ loaderData, actionData }: Route.ComponentProps) {
102− const { slug, role, account, ledger, features, deployUsage, added, subscribed } = loaderData;
117+ const { slug, role, account, ledger, features, deployUsage, limit, added, subscribed } = loaderData;
103118 const { status } = account;
104119 const paying = useNavigation().state === "submitting";
105120 const empty = account.balanceMicros <= 0;
117132 </p>
118133 </div>
119134 )}
135+ {limit && account.status.enabled && (
136+ <LimitCard limit={limit} owner={role === "owner"} busy={paying} error={actionData?.error} />
137+ )}
138+
120139 <h2 className="font-medium">Plans</h2>
121140 <p className="mt-1 max-w-2xl text-sm text-muted">
122141 Paid features are turned on per workspace with a monthly plan. They are never free, including while the rest of
143162 <h2 className="mt-12 font-medium">Agent credit</h2>
144163 <p className="mt-1 max-w-2xl text-sm text-muted">
145164 g1t agents that work on this workspace's repositories are paid for from
146− its credit: what the model cost, plus {account.marginPercent}%. Checks run
147− free. With no credit, agents do not start.
165+ its credit: what the model cost, plus {account.marginPercent}%. Every sandbox, for agents, checks, the merge
166+ queue and workflows, is metered by the second past 500 free minutes a month. With no credit, agents do not
167+ start.
148168 </p>
149169
150170 <div
414434 </div>
415435 );
416436 }
437+
438+const TRUST: Record<Limit["trust"], { label: string; detail: string }> = {
439+ new: {
440+ label: "New",
441+ detail: "No payment to g1t yet, so the limit is small: the free allowances and a little more. It grows once the workspace pays.",
442+ },
443+ paid: { label: "Paid", detail: "Twice what the workspace has paid g1t, from $25 up to $1,000." },
444+ reviewed: { label: "Reviewed", detail: "Set by g1t for this workspace." },
445+ internal: { label: "g1t", detail: "One of g1t's own workspaces: no limit." },
446+};
447+
448+/**
449+ * How far this month's unpaid usage has gone, and where work stops: g1t's
450+ * ceiling for the workspace, or the owners' own spend limit if lower.
451+ */
452+function LimitCard({ limit, owner, busy, error }: { limit: Limit; owner: boolean; busy: boolean; error?: string }) {
453+ const ceiling = limit.ceilingMicros;
454+ const share = ceiling ? Math.min(1, limit.exposureMicros / Math.max(ceiling, 1)) : 0;
455+ const tone =
456+ limit.state === "stopped" ? "border-danger/40 bg-danger/5" : limit.state === "warning" ? "border-warn/40 bg-warn/5" : "border-line bg-surface";
457+ const bar = limit.state === "stopped" ? "bg-danger" : limit.state === "warning" ? "bg-warn" : "bg-accent";
458+ const trust = TRUST[limit.trust];
459+ return (
460+ <section className={`mb-10 rounded-xl border p-5 ${tone}`}>
461+ <div className="flex flex-wrap items-baseline justify-between gap-2">
462+ <h2 className="font-medium">Usage limit</h2>
463+ <span className="rounded-full border border-line px-2 py-0.5 text-xs text-muted">{trust.label}</span>
464+ </div>
465+ <p className="mt-1 max-w-2xl text-sm text-muted">
466+ What this month's usage cost g1t, or is charged, whichever is more, less what was paid this month. At the limit,
467+ new sandboxes and builds stop and apps pause until the workspace pays or the month turns. Work already running
468+ finishes.
469+ </p>
470+ <p className="mt-4 text-2xl font-semibold tabular-nums tracking-tight">
471+ {dollars(limit.exposureMicros)}
472+ <span className="text-base font-normal text-muted"> {ceiling == null ? "· no limit" : `of ${dollars(ceiling)}`}</span>
473+ </p>
474+ {ceiling != null && (
475+ <div className="mt-3 h-1.5 overflow-hidden rounded-full bg-line" role="presentation">
476+ <div className={`h-full ${bar}`} style={{ width: `${Math.max(share * 100, share > 0 ? 2 : 0)}%` }} />
477+ </div>
478+ )}
479+ {limit.message && <p className="mt-3 text-sm">{limit.message}</p>}
480+ <p className="mt-3 text-xs text-faint">
481+ {trust.detail}
482+ {limit.trustCeilingMicros != null && limit.spendLimitMicros != null && ` g1t's limit is ${dollars(limit.trustCeilingMicros)}.`}
483+ </p>
484+ {owner && limit.trust !== "internal" && (
485+ <Form method="post" className="mt-4 flex flex-wrap items-end gap-2">
486+ <label className="text-sm">
487+ <span className="block text-xs text-muted">Your own monthly spend limit</span>
488+ <span className="mt-1 flex items-center rounded-md border border-line bg-bg px-2 focus-within:border-accent">
489+ <span className="text-muted">$</span>
490+ <input
491+ name="limit"
492+ type="number"
493+ min={0}
494+ step={1}
495+ defaultValue={limit.spendLimitMicros != null ? limit.spendLimitMicros / MICROS_PER_DOLLAR : ""}
496+ placeholder="None"
497+ className="w-24 bg-transparent px-1 py-1.5 tabular-nums outline-none"
498+ />
499+ </span>
500+ </label>
501+ <Button variant="quiet" type="submit" name="intent" value="spend-limit" disabled={busy}>
502+ Set
503+ </Button>
504+ {limit.spendLimitMicros != null && (
505+ <Button variant="quiet" type="submit" name="intent" value="no-spend-limit" disabled={busy}>
506+ Remove
507+ </Button>
508+ )}
509+ <ErrorText>{error}</ErrorText>
510+ </Form>
511+ )}
512+ </section>
513+ );
514+}
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232.19 94.04"><g transform="translate(-22.77 -16.35) scale(3.3)"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#b6a8ff" fill-opacity="0.35"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#b6a8ff" fill-opacity="0.65"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#ededef"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#ededef" stroke-width="4.6" stroke-linecap="round"/></g></g><path transform="translate(101.08 72.75)" fill="#ededef" d="M30.42 21.29Q22.8 21.29 17.5 19.26Q12.21 17.24 9.16 13.67Q6.1 10.11 5.22 5.42L20.75 4.1Q21.19 5.66 22.46 6.86Q23.73 8.06 25.71 8.74Q27.69 9.42 30.37 9.42Q35.3 9.42 37.89 6.93Q40.48 4.44 40.48 -0.59V-9.28H39.89Q38.72 -6.54 36.65 -4.57Q34.57 -2.59 31.67 -1.56Q28.76 -0.54 25.1 -0.54Q18.7 -0.54 13.7 -3.64Q8.69 -6.74 5.83 -12.6Q2.98 -18.46 2.98 -26.76Q2.98 -35.3 5.91 -41.36Q8.84 -47.41 13.82 -50.63Q18.8 -53.86 25 -53.86Q28.81 -53.86 31.71 -52.66Q34.62 -51.46 36.77 -49.41Q38.92 -47.36 40.23 -44.73H40.48V-52.93H57.23V-2.1Q57.23 5.91 53.91 11.08Q50.59 16.26 44.56 18.77Q38.53 21.29 30.42 21.29ZM30.32 -13.38Q33.54 -13.38 35.89 -15.01Q38.23 -16.65 39.53 -19.7Q40.82 -22.75 40.82 -26.95Q40.82 -31.15 39.53 -34.23Q38.23 -37.3 35.89 -38.99Q33.54 -40.67 30.32 -40.67Q27.15 -40.67 24.88 -38.99Q22.61 -37.3 21.41 -34.23Q20.21 -31.15 20.21 -26.95Q20.21 -22.71 21.41 -19.65Q22.61 -16.6 24.88 -14.99Q27.15 -13.38 30.32 -13.38ZM94.12 -72.75V0H77.08V-57.37H76.84L60.33 -46.53V-61.57L77.47 -72.75ZM130.48 -52.93V-40.09H95.18V-52.93ZM103.28 -66.5H120.22V-17.04Q120.22 -14.75 121.23 -13.75Q122.23 -12.74 124.81 -12.74Q125.89 -12.74 127.62 -12.84Q129.36 -12.94 130.14 -13.04L131.11 -0.29Q129.11 0.1 126.43 0.22Q123.74 0.34 121.1 0.34Q112.07 0.34 107.68 -3.42Q103.28 -7.18 103.28 -14.79Z"/></svg>
+0−0

Binary or large file; its contents are not shown.

+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232.19 94.04"><g transform="translate(-22.77 -16.35) scale(3.3)"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#6b56e8" fill-opacity="0.4"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#6b56e8" fill-opacity="0.75"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#161618"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#161618" stroke-width="4.6" stroke-linecap="round"/></g></g><path transform="translate(101.08 72.75)" fill="#161618" d="M30.42 21.29Q22.8 21.29 17.5 19.26Q12.21 17.24 9.16 13.67Q6.1 10.11 5.22 5.42L20.75 4.1Q21.19 5.66 22.46 6.86Q23.73 8.06 25.71 8.74Q27.69 9.42 30.37 9.42Q35.3 9.42 37.89 6.93Q40.48 4.44 40.48 -0.59V-9.28H39.89Q38.72 -6.54 36.65 -4.57Q34.57 -2.59 31.67 -1.56Q28.76 -0.54 25.1 -0.54Q18.7 -0.54 13.7 -3.64Q8.69 -6.74 5.83 -12.6Q2.98 -18.46 2.98 -26.76Q2.98 -35.3 5.91 -41.36Q8.84 -47.41 13.82 -50.63Q18.8 -53.86 25 -53.86Q28.81 -53.86 31.71 -52.66Q34.62 -51.46 36.77 -49.41Q38.92 -47.36 40.23 -44.73H40.48V-52.93H57.23V-2.1Q57.23 5.91 53.91 11.08Q50.59 16.26 44.56 18.77Q38.53 21.29 30.42 21.29ZM30.32 -13.38Q33.54 -13.38 35.89 -15.01Q38.23 -16.65 39.53 -19.7Q40.82 -22.75 40.82 -26.95Q40.82 -31.15 39.53 -34.23Q38.23 -37.3 35.89 -38.99Q33.54 -40.67 30.32 -40.67Q27.15 -40.67 24.88 -38.99Q22.61 -37.3 21.41 -34.23Q20.21 -31.15 20.21 -26.95Q20.21 -22.71 21.41 -19.65Q22.61 -16.6 24.88 -14.99Q27.15 -13.38 30.32 -13.38ZM94.12 -72.75V0H77.08V-57.37H76.84L60.33 -46.53V-61.57L77.47 -72.75ZM130.48 -52.93V-40.09H95.18V-52.93ZM103.28 -66.5H120.22V-17.04Q120.22 -14.75 121.23 -13.75Q122.23 -12.74 124.81 -12.74Q125.89 -12.74 127.62 -12.84Q129.36 -12.94 130.14 -13.04L131.11 -0.29Q129.11 0.1 126.43 0.22Q123.74 0.34 121.1 0.34Q112.07 0.34 107.68 -3.42Q103.28 -7.18 103.28 -14.79Z"/></svg>
+0−0

Binary or large file; its contents are not shown.

+1−0
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#0f0f11"/><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#b6a8ff" fill-opacity="0.35"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#b6a8ff" fill-opacity="0.65"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#ededef"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#ededef" stroke-width="4.6" stroke-linecap="round"/></g></svg>
+0−0

Binary or large file; its contents are not shown.

+1−8
1−<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2− <rect width="32" height="32" rx="7" fill="#161618"/>
3− <g transform="translate(4.5 4.6) scale(0.72)">
4− <path d="M16 2.5 28 9.4 16 16.3 4 9.4Z" fill="#ededef"/>
5− <path d="M4 10.9 15.3 17.4V30.2L4 23.7Z" fill="#ededef" fill-opacity="0.55"/>
6− <path d="M28 10.9 16.7 17.4V30.2L28 23.7Z" fill="#ededef" fill-opacity="0.25"/>
7− </g>
8−</svg>
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.f{fill:#161618}.f.s{fill:none;stroke:#161618}.m{fill:#6b56e8}@media (prefers-color-scheme: dark){.f{fill:#ededef}.f.s{fill:none;stroke:#ededef}.m{fill:#b6a8ff}}</style><mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16"><rect width="16" height="16" fill="#fff"/><path d="M10.5 3.5 8.5 5.4" stroke="#000" stroke-width="3.6" stroke-linecap="round"/></mask><g mask="url(#cut)"><rect class="m" x="3" y="5.5" width="2" height="8.5" rx="1" fill-opacity="0.5"/><rect class="m" x="6" y="4" width="2" height="10" rx="1" fill-opacity="0.85"/></g><rect class="f" x="9" y="2" width="3" height="12" rx="1.5"/><path class="f s" d="M10.5 3.5 8.5 5.4" stroke-width="2.2" stroke-linecap="round"/></svg>
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+104−0
307307 pub const MICROS_PER_BUILD_SECOND: i64 = 21;
308308 }
309309
310+/// Sandbox time: every sandbox g1t starts for a workspace (agents,
311+/// reviews, checks, the merge queue, workflow jobs) is metered by the
312+/// second. Deploy builds are charged by the Deployments plan instead.
313+pub mod sandbox_allowance {
314+ /// Free each calendar month (UTC): 500 minutes.
315+ pub const FREE_SECONDS: i64 = 30_000;
316+ /// What one second costs g1t (Cloudflare Containers, standard-1),
317+ /// rounded up. Recorded with every entry.
318+ pub const COST_MICROS_PER_SECOND: i64 = super::deployments_allowance::MICROS_PER_BUILD_SECOND;
319+ /// What one second past the free minutes is charged: $0.003 a minute.
320+ pub const MICROS_PER_SECOND: i64 = 50;
321+}
322+
323+/// `record_sandbox`: how long one sandbox ran for a workspace, reported by
324+/// the runner when it stops. Recorded once per `reference`, with what it
325+/// cost g1t; seconds past the month's free minutes are charged at
326+/// `sandbox_allowance::MICROS_PER_SECOND`, unless `FREE_WHILE_BUILDING`.
327+/// Returns `Outcome<bool>`: false if that reference was recorded before.
328+#[derive(Debug, Serialize, Deserialize)]
329+#[serde(rename_all = "camelCase")]
330+pub struct RecordSandboxArgs {
331+ pub workspace: String,
332+ pub seconds: u32,
333+ /// What ran, e.g. `Checks on acme/api#12`.
334+ pub description: String,
335+ /// `namespace/name`.
336+ pub repo: Option<String>,
337+ /// Unique to the run.
338+ pub reference: String,
339+}
340+
341+/// How much a workspace has earned g1t's trust with money, which sets how
342+/// far its unpaid usage can go before its work stops.
343+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
344+#[serde(rename_all = "snake_case")]
345+pub enum Trust {
346+ /// No live payment yet: only a little past the free allowances.
347+ New,
348+ /// Has paid g1t real money: the ceiling grows with what it has paid.
349+ Paid,
350+ /// A ceiling g1t set by hand, after talking to the workspace.
351+ Reviewed,
352+ /// g1t's own workspaces: no ceiling.
353+ Internal,
354+}
355+
356+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
357+#[serde(rename_all = "snake_case")]
358+pub enum LimitState {
359+ Ok,
360+ /// Past 80% of the ceiling.
361+ Warning,
362+ /// At or past it: no new sandboxes, builds or app requests.
363+ Stopped,
364+}
365+
366+/// How far a workspace's unpaid usage has gone this month, and where its
367+/// work stops: like Fly's or Cloudflare's limits for new accounts, so no
368+/// one runs up costs g1t cannot collect. Usage counts at what it cost g1t
369+/// or what it is charged, whichever is more, so it counts while g1t is
370+/// free too.
371+#[derive(Clone, Debug, Serialize, Deserialize)]
372+#[serde(rename_all = "camelCase")]
373+pub struct Limit {
374+ pub workspace: String,
375+ pub trust: Trust,
376+ /// Usage this month (UTC) less what was paid this month.
377+ pub exposure_micros: i64,
378+ /// Where work stops: the lower of g1t's ceiling and the owner's own
379+ /// spend limit. None for g1t's own workspaces.
380+ pub ceiling_micros: Option<i64>,
381+ /// The ceiling g1t sets from `trust`.
382+ pub trust_ceiling_micros: Option<i64>,
383+ /// The owner's own monthly limit, if they set one.
384+ pub spend_limit_micros: Option<i64>,
385+ pub state: LimitState,
386+ /// What to tell people when work is stopped or close to it.
387+ pub message: Option<String>,
388+}
389+
390+/// `limit`: a workspace's limit, for its members. Returns `Outcome<Limit>`.
391+#[derive(Debug, Serialize, Deserialize)]
392+pub struct LimitArgs {
393+ pub workspace: String,
394+ pub viewer: Viewer,
395+}
396+
397+/// `check_limit`: the same, for the services that enforce it. Returns
398+/// `Outcome<Limit>`.
399+#[derive(Debug, Serialize, Deserialize)]
400+pub struct CheckLimitArgs {
401+ pub workspace: String,
402+}
403+
404+/// `set_spend_limit`: the owner's own monthly ceiling, under g1t's; None
405+/// removes it. Owners only. Returns `Outcome<Limit>`.
406+#[derive(Debug, Serialize, Deserialize)]
407+#[serde(rename_all = "camelCase")]
408+pub struct SetSpendLimitArgs {
409+ pub actor: User,
410+ pub workspace: String,
411+ pub spend_limit_micros: Option<i64>,
412+}
413+
310414 /// What a feature's plan costs and includes.
311415 #[derive(Clone, Debug, Serialize, Deserialize)]
312416 #[serde(rename_all = "camelCase")]
+39−0
7272 * open to them: a few dollars of model cost each, out of one pool, until a
7373 * date. Mirrors `Trial` in `crates/contracts/src/billing.rs`.
7474 */
75+/** How much a workspace has earned g1t's trust with money. */
76+export type Trust = "new" | "paid" | "reviewed" | "internal";
77+
78+/**
79+ * How far a workspace's unpaid usage has gone this month, and where its
80+ * work stops: past `ceilingMicros`, no new sandboxes, builds or app
81+ * requests. Usage counts at its cost to g1t or its charge, whichever is
82+ * more, so it counts while g1t is free too.
83+ */
84+export type Limit = {
85+ workspace: string;
86+ trust: Trust;
87+ exposureMicros: number;
88+ /** The lower of g1t's ceiling and the owner's spend limit; null for g1t's own. */
89+ ceilingMicros: number | null;
90+ trustCeilingMicros: number | null;
91+ spendLimitMicros: number | null;
92+ state: "ok" | "warning" | "stopped";
93+ message: string | null;
94+};
95+
7596 export type Trial = {
7697 open: boolean;
7798 usedMicros: number;
184205 repo?: string | null;
185206 reference: string;
186207 }): Promise<Result<boolean>>;
208+ /** A workspace's limit, for its members. */
209+ limit(workspace: string, viewer: Viewer): Promise<Result<Limit>>;
210+ /** The same, for the services that enforce it. */
211+ checkLimit(workspace: string): Promise<Result<Limit>>;
212+ /** The owner's own monthly ceiling, under g1t's; null removes it. Owners only. */
213+ setSpendLimit(actor: User, workspace: string, spendLimitMicros: number | null): Promise<Result<Limit>>;
214+ /**
215+ * How long a sandbox ran for a workspace, reported when it stops. Its
216+ * cost is always recorded; seconds past the month's free minutes are
217+ * charged. False if `reference` was recorded before.
218+ */
219+ recordSandbox(usage: {
220+ workspace: string;
221+ seconds: number;
222+ description: string;
223+ repo?: string | null;
224+ reference: string;
225+ }): Promise<Result<boolean>>;
187226 startRun(run: {
188227 workspace: string;
189228 repo: RepoPath;
+5−0
199199 call("cancel_subscription", { actor, workspace, feature, resume }),
200200 hasFeature: (workspace, feature) => call("has_feature", { workspace, feature }),
201201 chargeFeature: (charge) => call("charge_feature", charge),
202+ recordSandbox: (usage) => call("record_sandbox", usage),
203+ limit: (workspace, viewer) => call("limit", { workspace, viewer }),
204+ checkLimit: (workspace) => call("check_limit", { workspace }),
205+ setSpendLimit: (actor, workspace, spendLimitMicros) =>
206+ call("set_spend_limit", { actor, workspace, spendLimitMicros }),
202207 };
203208 }
204209
+1−8
1−<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2− <rect width="32" height="32" rx="7" fill="#161618"/>
3− <g transform="translate(4.5 4.6) scale(0.72)">
4− <path d="M16 2.5 28 9.4 16 16.3 4 9.4Z" fill="#ededef"/>
5− <path d="M4 10.9 15.3 17.4V30.2L4 23.7Z" fill="#ededef" fill-opacity="0.55"/>
6− <path d="M28 10.9 16.7 17.4V30.2L28 23.7Z" fill="#ededef" fill-opacity="0.25"/>
7− </g>
8−</svg>
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><style>.f{fill:#161618}.f.s{fill:none;stroke:#161618}.m{fill:#6b56e8}@media (prefers-color-scheme: dark){.f{fill:#ededef}.f.s{fill:none;stroke:#ededef}.m{fill:#b6a8ff}}</style><mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16"><rect width="16" height="16" fill="#fff"/><path d="M10.5 3.5 8.5 5.4" stroke="#000" stroke-width="3.6" stroke-linecap="round"/></mask><g mask="url(#cut)"><rect class="m" x="3" y="5.5" width="2" height="8.5" rx="1" fill-opacity="0.5"/><rect class="m" x="6" y="4" width="2" height="10" rx="1" fill-opacity="0.85"/></g><rect class="f" x="9" y="2" width="3" height="12" rx="1.5"/><path class="f s" d="M10.5 3.5 8.5 5.4" stroke-width="2.2" stroke-linecap="round"/></svg>
+1−5
1−<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2− <path d="M16 2.5 28 9.4 16 16.3 4 9.4Z" fill="#ededef"/>
3− <path d="M4 10.9 15.3 17.4V30.2L4 23.7Z" fill="#ededef" fill-opacity="0.55"/>
4− <path d="M28 10.9 16.7 17.4V30.2L28 23.7Z" fill="#ededef" fill-opacity="0.25"/>
5−</svg>
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g transform="translate(0.7 0.5)"><rect x="6.2" y="9.5" width="4.4" height="17" rx="2.2" fill="#b6a8ff" fill-opacity="0.35"/><rect x="12.4" y="7" width="4.8" height="19.5" rx="2.4" fill="#b6a8ff" fill-opacity="0.65"/><rect x="19" y="4.5" width="5.4" height="22" rx="2.7" fill="#ededef"/><path d="M21.7 7.2 17.6 10.9" fill="none" stroke="#ededef" stroke-width="4.6" stroke-linecap="round"/></g></svg>
+11−0
1+-- Sandbox time each workspace used, by calendar month (UTC), so that the
2+-- free minutes are counted before any second is charged.
3+CREATE TABLE sandbox_months (
4+ workspace TEXT NOT NULL,
5+ -- YYYY-MM.
6+ month TEXT NOT NULL,
7+ seconds INTEGER NOT NULL DEFAULT 0,
8+ PRIMARY KEY (workspace, month)
9+);
10+
11+CREATE INDEX IF NOT EXISTS ledger_by_reference ON ledger (reference);
+11−0
1+-- Per-workspace limits on usage not yet paid for. A row only exists when
2+-- g1t set a ceiling by hand or an owner set a spend limit; everything
3+-- else is worked out from the ledger. See src/limits.rs.
4+CREATE TABLE limits (
5+ workspace TEXT PRIMARY KEY,
6+ -- Set by g1t after a review; replaces the ceiling trust would give.
7+ ceiling_micros INTEGER,
8+ -- The owner's own monthly limit, under g1t's.
9+ spend_limit_micros INTEGER,
10+ updated_at TEXT NOT NULL
11+);
+1−1
6161
6262 /// Dollars to the cent, or finer for prices under a cent, so that a
6363 /// build minute's $0.0015 does not read as nothing.
64−fn dollars(micros: i64) -> String {
64+pub(crate) fn dollars(micros: i64) -> String {
6565 let text = format!("{:.4}", micros as f64 / MICROS_PER_DOLLAR as f64);
6666 let (whole, fraction) = text.split_once('.').unwrap_or((&text, ""));
6767 let fraction = fraction.trim_end_matches('0');
+133−0
1717 //! the methods and their arguments.
1818
1919 mod features;
20+mod limits;
2021 mod stripe;
2122
2223 use g1t_contracts::billing::*;
134135 trial: Option<TrialConfig>,
135136 /// The Deployments plan's monthly price (`DEPLOYMENTS_MONTHLY_CENTS`).
136137 deployments_monthly_cents: u32,
138+ /// How far unpaid usage may go; see `limits`.
139+ ceilings: limits::Ceilings,
137140 }
138141
139142 /// `TRIAL_WORKSPACE_MICROS`, `TRIAL_TOTAL_MICROS` and `TRIAL_UNTIL`.
567570 if self.stripe.is_none() {
568571 return Ok(Outcome::Ok(true));
569572 }
573+ if let Some(stopped) = self.stopped(&a.workspace).await? {
574+ return Ok(stopped);
575+ }
570576 Ok(self
571577 .out_of_credit(&a.workspace.to_lowercase())
572578 .await?
578584 return Ok(Outcome::Ok(None));
579585 }
580586 let workspace = a.workspace.to_lowercase();
587+ if let Some(stopped) = self.stopped(&workspace).await? {
588+ return Ok(stopped);
589+ }
581590 if let Some(refused) = self.out_of_credit(&workspace).await? {
582591 return Ok(refused);
583592 }
673682 }
674683 }
675684
685+impl Billing {
686+ /// Records how long a sandbox ran: its cost always, and a charge for
687+ /// the seconds past the month's free minutes.
688+ async fn record_sandbox(&self, a: RecordSandboxArgs) -> Result<Outcome<bool>> {
689+ if self.stripe.is_none() || a.seconds == 0 {
690+ return Ok(Outcome::Ok(false));
691+ }
692+ let workspace = a.workspace.to_lowercase();
693+ let seen = self
694+ .db
695+ .prepare("SELECT id FROM ledger WHERE reference = ?")
696+ .bind(&[a.reference.as_str().into()])?
697+ .first::<Touched>(None)
698+ .await?;
699+ if seen.is_some() {
700+ return Ok(Outcome::Ok(false));
701+ }
702+ let now = now_ms();
703+ let timestamp = rfc3339(now);
704+ let month = &timestamp[..7];
705+ #[derive(Deserialize)]
706+ struct Used {
707+ seconds: i64,
708+ }
709+ let seconds = i64::from(a.seconds);
710+ let after = self
711+ .db
712+ .prepare(
713+ "INSERT INTO sandbox_months (workspace, month, seconds) VALUES (?1, ?2, ?3)
714+ ON CONFLICT (workspace, month) DO UPDATE SET seconds = seconds + ?3
715+ RETURNING seconds",
716+ )
717+ .bind(&[workspace.as_str().into(), month.into(), (seconds as f64).into()])?
718+ .first::<Used>(None)
719+ .await?
720+ .map_or(seconds, |used| used.seconds);
721+ let billable = sandbox_billable(after - seconds, seconds);
722+ let charge = if self.free { 0 } else { billable * sandbox_allowance::MICROS_PER_SECOND };
723+ let mut description = format!("{}: {} of sandbox time", a.description, duration(seconds));
724+ if billable < seconds {
725+ description.push_str(if billable == 0 {
726+ ", within the month's free minutes"
727+ } else {
728+ ", partly within the month's free minutes"
729+ });
730+ }
731+ if self.free && billable > 0 {
732+ description.push_str(" (free while g1t is being built out)");
733+ }
734+ self.db
735+ .batch(vec![
736+ self.db
737+ .prepare(
738+ "INSERT INTO ledger
739+ (id, workspace, kind, amount_micros, description, repo, task,
740+ cost_micros, reference, created_at, billed_to)
741+ VALUES (?, ?, 'usage', ?, ?, ?, 'sandbox', ?, ?, ?, 'g1t')",
742+ )
743+ .bind(&[
744+ new_id("led", now).into(),
745+ workspace.as_str().into(),
746+ (-(charge as f64)).into(),
747+ description.as_str().into(),
748+ optional(a.repo.as_deref()),
749+ ((seconds * sandbox_allowance::COST_MICROS_PER_SECOND) as f64).into(),
750+ a.reference.as_str().into(),
751+ timestamp.as_str().into(),
752+ ])?,
753+ self.db
754+ .prepare(
755+ "INSERT INTO accounts (workspace, balance_micros, created_at)
756+ VALUES (?1, ?2, ?3)
757+ ON CONFLICT (workspace) DO UPDATE SET balance_micros = balance_micros + ?2",
758+ )
759+ .bind(&[
760+ workspace.as_str().into(),
761+ (-(charge as f64)).into(),
762+ timestamp.as_str().into(),
763+ ])?,
764+ ])
765+ .await?;
766+ Ok(Outcome::Ok(true))
767+ }
768+}
769+
770+/// Of `seconds` used after `before` this month, how many are past the
771+/// free minutes.
772+fn sandbox_billable(before: i64, seconds: i64) -> i64 {
773+ let free_left = (sandbox_allowance::FREE_SECONDS - before).max(0);
774+ (seconds - free_left).max(0)
775+}
776+
777+/// `1h 2m`, `3m 12s` or `40s`.
778+fn duration(seconds: i64) -> String {
779+ let (h, m, s) = (seconds / 3600, seconds % 3600 / 60, seconds % 60);
780+ if h > 0 {
781+ format!("{h}h {m}m")
782+ } else if m > 0 {
783+ format!("{m}m {s}s")
784+ } else {
785+ format!("{s}s")
786+ }
787+}
788+
676789 fn members_only<T>() -> Outcome<T> {
677790 Outcome::fail(
678791 FailureCode::Forbidden,
705818 .and_then(|fee| fee.to_string().parse().ok())
706819 .unwrap_or(100_000),
707820 free: env.var("FREE_WHILE_BUILDING").is_ok_and(|v| v.to_string() == "true"),
821+ ceilings: limits::Ceilings::from_env(&env),
708822 deployments_monthly_cents: env
709823 .var("DEPLOYMENTS_MONTHLY_CENTS")
710824 .ok()
747861 "cancel_subscription" => reply(&billing.cancel_subscription(args(body)?).await?),
748862 "has_feature" => reply(&billing.has_feature(args(body)?).await?),
749863 "charge_feature" => reply(&billing.charge_feature(args(body)?).await?),
864+ "record_sandbox" => reply(&billing.record_sandbox(args(body)?).await?),
865+ "limit" => reply(&billing.limit(args(body)?).await?),
866+ "check_limit" => reply(&billing.check_limit(args(body)?).await?),
867+ "set_spend_limit" => reply(&billing.set_spend_limit(args(body)?).await?),
750868 _ => Response::error("Unknown method", 404),
751869 }
752870 }
771889 }
772890
773891 #[test]
892+ fn sandbox_seconds_are_charged_only_past_the_free_minutes() {
893+ let free = sandbox_allowance::FREE_SECONDS;
894+ assert_eq!(sandbox_billable(0, 600), 0);
895+ assert_eq!(sandbox_billable(free - 100, 600), 500);
896+ assert_eq!(sandbox_billable(free + 5, 600), 600);
897+ }
898+
899+ #[test]
900+ fn durations_read_plainly() {
901+ assert_eq!(duration(40), "40s");
902+ assert_eq!(duration(192), "3m 12s");
903+ assert_eq!(duration(3720), "1h 2m");
904+ }
905+
906+ #[test]
774907 fn an_absurd_cost_is_capped() {
775908 assert_eq!(charge_micros(1e9, 20), 120 * MICROS_PER_DOLLAR);
776909 }
+270−0
1+//! How far a workspace can run up costs g1t has not been paid for.
2+//!
3+//! Every sandbox second, build, app request and model token costs g1t
4+//! money at Cloudflare or a model provider before the workspace pays for
5+//! it. So, like Fly or Cloudflare with new accounts, each workspace has a
6+//! ceiling on that unpaid usage, set by how much it has paid g1t before:
7+//!
8+//! - **New**: no live payment yet. A few dollars, enough for the free
9+//! allowances and a little more.
10+//! - **Paid**: twice what it has paid g1t, within bounds.
11+//! - **Reviewed**: a ceiling g1t set by hand.
12+//! - **Internal**: g1t's own workspaces, with none.
13+//!
14+//! An owner can set a lower spend limit of their own. Past 80% the
15+//! workspace is warned; at the ceiling its work stops: no new sandboxes,
16+//! builds or app requests, until it pays or the month turns. Runs already
17+//! under way finish.
18+//!
19+//! Usage counts at what it cost g1t or what it is charged, whichever is
20+//! more, so it counts while g1t is free too: free is a price, not an
21+//! exemption from the ceiling. Test-mode payments are not money, so they
22+//! do not raise trust.
23+
24+use g1t_contracts::billing::{CheckLimitArgs, Limit, LimitArgs, LimitState, SetSpendLimitArgs, Trust};
25+use g1t_contracts::time::rfc3339;
26+use g1t_contracts::{FailureCode, Outcome, Role};
27+use g1t_kit::now_ms;
28+use serde::Deserialize;
29+use worker::wasm_bindgen::JsValue;
30+use worker::{Env, Result};
31+
32+use crate::features::dollars as dollars_plain;
33+use crate::{Billing, members_only};
34+
35+/// The ceilings, from the billing service's variables.
36+pub(crate) struct Ceilings {
37+ /// `LIMIT_NEW_MICROS`.
38+ pub new: i64,
39+ /// `LIMIT_PAID_MIN_MICROS` and `LIMIT_PAID_MAX_MICROS`.
40+ pub paid_min: i64,
41+ pub paid_max: i64,
42+ /// `LIMIT_EXEMPT`: g1t's own workspaces, comma-separated.
43+ pub exempt: Vec<String>,
44+}
45+
46+impl Ceilings {
47+ pub(crate) fn from_env(env: &Env) -> Self {
48+ let number = |name: &str, default: i64| {
49+ env.var(name).ok().and_then(|v| v.to_string().parse::<i64>().ok()).unwrap_or(default)
50+ };
51+ Ceilings {
52+ new: number("LIMIT_NEW_MICROS", 3_000_000),
53+ paid_min: number("LIMIT_PAID_MIN_MICROS", 25_000_000),
54+ paid_max: number("LIMIT_PAID_MAX_MICROS", 1_000_000_000),
55+ exempt: env
56+ .var("LIMIT_EXEMPT")
57+ .map(|v| v.to_string())
58+ .unwrap_or_default()
59+ .split(',')
60+ .map(|name| name.trim().to_lowercase())
61+ .filter(|name| !name.is_empty())
62+ .collect(),
63+ }
64+ }
65+
66+ /// The ceiling for a workspace that has paid `paid` in live money.
67+ pub(crate) fn for_paid(&self, paid: i64) -> i64 {
68+ (paid * 2).clamp(self.paid_min, self.paid_max)
69+ }
70+}
71+
72+/// Where a workspace stands against its ceiling.
73+pub(crate) fn state(exposure: i64, ceiling: Option<i64>) -> LimitState {
74+ match ceiling {
75+ Some(ceiling) if exposure >= ceiling => LimitState::Stopped,
76+ Some(ceiling) if exposure * 5 >= ceiling * 4 => LimitState::Warning,
77+ _ => LimitState::Ok,
78+ }
79+}
80+
81+#[derive(Deserialize)]
82+struct LimitRow {
83+ ceiling_micros: Option<i64>,
84+ spend_limit_micros: Option<i64>,
85+}
86+
87+#[derive(Deserialize)]
88+struct Month {
89+ used: Option<i64>,
90+ paid: Option<i64>,
91+}
92+
93+#[derive(Deserialize)]
94+struct Paid {
95+ paid: Option<i64>,
96+}
97+
98+impl Billing {
99+ /// The workspace's limit, worked out from its ledger.
100+ pub(crate) async fn limit_of(&self, workspace: &str) -> Result<Limit> {
101+ let workspace = workspace.to_lowercase();
102+ let row = self
103+ .db
104+ .prepare("SELECT ceiling_micros, spend_limit_micros FROM limits WHERE workspace = ?")
105+ .bind(&[workspace.as_str().into()])?
106+ .first::<LimitRow>(None)
107+ .await?;
108+ let month_start = format!("{}-01", &rfc3339(now_ms())[..7]);
109+ // Each usage entry at its cost to g1t or its charge, whichever is
110+ // more; on the workspace's own provider, only g1t's fee is g1t's.
111+ let month = self
112+ .db
113+ .prepare(
114+ "SELECT
115+ SUM(CASE WHEN kind = 'usage' THEN
116+ CASE WHEN COALESCE(billed_to, 'g1t') = 'g1t'
117+ THEN MAX(COALESCE(cost_micros, 0), -amount_micros)
118+ ELSE -amount_micros END
119+ END) AS used,
120+ SUM(CASE WHEN kind = 'top_up' THEN amount_micros END) AS paid
121+ FROM ledger WHERE workspace = ?1 AND created_at >= ?2",
122+ )
123+ .bind(&[workspace.as_str().into(), month_start.as_str().into()])?
124+ .first::<Month>(None)
125+ .await?;
126+ let (used, paid_month) = month.map_or((0, 0), |m| (m.used.unwrap_or(0), m.paid.unwrap_or(0)));
127+ // Test-mode payments are not money: they pay nothing off.
128+ let live = self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live);
129+ let exposure = (used - if live { paid_month } else { 0 }).max(0);
130+
131+ let (trust, trust_ceiling) = if self.ceilings.exempt.iter().any(|name| *name == workspace) {
132+ (Trust::Internal, None)
133+ } else if let Some(ceiling) = row.as_ref().and_then(|row| row.ceiling_micros) {
134+ (Trust::Reviewed, Some(ceiling))
135+ } else {
136+ let paid = self.live_paid(&workspace).await?;
137+ if paid > 0 {
138+ (Trust::Paid, Some(self.ceilings.for_paid(paid)))
139+ } else {
140+ (Trust::New, Some(self.ceilings.new))
141+ }
142+ };
143+ let spend_limit = row.and_then(|row| row.spend_limit_micros);
144+ let ceiling = match (trust_ceiling, spend_limit) {
145+ (Some(ceiling), Some(own)) => Some(ceiling.min(own)),
146+ (None, Some(own)) => Some(own),
147+ (ceiling, None) => ceiling,
148+ };
149+ let state = state(exposure, ceiling);
150+ let message = match state {
151+ LimitState::Ok => None,
152+ LimitState::Warning => Some(format!(
153+ "The {workspace} workspace has used {} of its {} limit this month. At the limit, its sandboxes, builds and apps stop until it pays or the month turns.",
154+ dollars_plain(exposure),
155+ dollars_plain(ceiling.unwrap_or_default()),
156+ )),
157+ LimitState::Stopped => Some(if spend_limit.is_some() && ceiling == spend_limit {
158+ format!(
159+ "The {workspace} workspace reached the {} spend limit its owners set for this month, so its sandboxes, builds and apps are stopped. An owner can raise it under Billing.",
160+ dollars_plain(ceiling.unwrap_or_default()),
161+ )
162+ } else {
163+ format!(
164+ "The {workspace} workspace reached its {} limit for usage not yet paid for, so its sandboxes, builds and apps are stopped. The limit grows as a workspace pays g1t; an owner can pay under Billing, or write to support to have it raised.",
165+ dollars_plain(ceiling.unwrap_or_default()),
166+ )
167+ }),
168+ };
169+ Ok(Limit {
170+ workspace,
171+ trust,
172+ exposure_micros: exposure,
173+ ceiling_micros: ceiling,
174+ trust_ceiling_micros: trust_ceiling,
175+ spend_limit_micros: spend_limit,
176+ state,
177+ message,
178+ })
179+ }
180+
181+ /// Real money the workspace has paid g1t. Nothing in test mode.
182+ async fn live_paid(&self, workspace: &str) -> Result<i64> {
183+ if !self.stripe.as_ref().is_some_and(crate::stripe::Stripe::live) {
184+ return Ok(0);
185+ }
186+ Ok(self
187+ .db
188+ .prepare("SELECT SUM(amount_micros) AS paid FROM ledger WHERE workspace = ? AND kind = 'top_up'")
189+ .bind(&[workspace.into()])?
190+ .first::<Paid>(None)
191+ .await?
192+ .and_then(|row| row.paid)
193+ .unwrap_or(0))
194+ }
195+
196+ /// A refusal, with the reason, when the workspace's work is stopped.
197+ /// None while billing is off: a g1t without payments has no limits.
198+ pub(crate) async fn stopped<T>(&self, workspace: &str) -> Result<Option<Outcome<T>>> {
199+ if self.stripe.is_none() {
200+ return Ok(None);
201+ }
202+ let limit = self.limit_of(workspace).await?;
203+ Ok((limit.state == LimitState::Stopped).then(|| {
204+ Outcome::fail(
205+ FailureCode::PaymentRequired,
206+ limit.message.unwrap_or_else(|| "This workspace is over its limit.".to_owned()),
207+ )
208+ }))
209+ }
210+
211+ pub(crate) async fn limit(&self, a: LimitArgs) -> Result<Outcome<Limit>> {
212+ let workspace = a.workspace.to_lowercase();
213+ if !a.viewer.is_some_and(|viewer| viewer.is_member(&workspace)) {
214+ return Ok(members_only());
215+ }
216+ Ok(Outcome::Ok(self.limit_of(&workspace).await?))
217+ }
218+
219+ pub(crate) async fn check_limit(&self, a: CheckLimitArgs) -> Result<Outcome<Limit>> {
220+ Ok(Outcome::Ok(self.limit_of(&a.workspace).await?))
221+ }
222+
223+ pub(crate) async fn set_spend_limit(&self, a: SetSpendLimitArgs) -> Result<Outcome<Limit>> {
224+ let workspace = a.workspace.to_lowercase();
225+ if a.actor.role_in(&workspace) != Some(Role::Owner) {
226+ return Ok(Outcome::fail(
227+ FailureCode::Forbidden,
228+ "Only an owner can set the workspace's spend limit.",
229+ ));
230+ }
231+ if a.spend_limit_micros.is_some_and(|limit| limit < 0) {
232+ return Ok(Outcome::fail(FailureCode::Invalid, "A spend limit cannot be negative."));
233+ }
234+ let limit = a.spend_limit_micros.map_or(JsValue::NULL, |limit| (limit as f64).into());
235+ self.db
236+ .prepare(
237+ "INSERT INTO limits (workspace, spend_limit_micros, updated_at) VALUES (?1, ?2, ?3)
238+ ON CONFLICT (workspace) DO UPDATE SET spend_limit_micros = ?2, updated_at = ?3",
239+ )
240+ .bind(&[workspace.as_str().into(), limit, rfc3339(now_ms()).into()])?
241+ .run()
242+ .await?;
243+ Ok(Outcome::Ok(self.limit_of(&workspace).await?))
244+ }
245+}
246+
247+#[cfg(test)]
248+mod tests {
249+ use super::*;
250+
251+ fn ceilings() -> Ceilings {
252+ Ceilings { new: 3_000_000, paid_min: 25_000_000, paid_max: 1_000_000_000, exempt: vec![] }
253+ }
254+
255+ #[test]
256+ fn trust_grows_with_what_was_paid_within_bounds() {
257+ assert_eq!(ceilings().for_paid(5_000_000), 25_000_000);
258+ assert_eq!(ceilings().for_paid(100_000_000), 200_000_000);
259+ assert_eq!(ceilings().for_paid(10_000_000_000), 1_000_000_000);
260+ }
261+
262+ #[test]
263+ fn work_warns_at_eighty_percent_and_stops_at_the_ceiling() {
264+ assert_eq!(state(0, Some(100)), LimitState::Ok);
265+ assert_eq!(state(79, Some(100)), LimitState::Ok);
266+ assert_eq!(state(80, Some(100)), LimitState::Warning);
267+ assert_eq!(state(100, Some(100)), LimitState::Stopped);
268+ assert_eq!(state(1_000_000, None), LimitState::Ok);
269+ }
270+}
+7−0
3030 // recorded with what they cost, and nothing is charged. Set to
3131 // "false" when pricing starts.
3232 "FREE_WHILE_BUILDING": "true",
33+ // How far a workspace's unpaid usage may go before its work stops
34+ // (see src/limits.rs): $3 before any live payment, then twice what
35+ // it has paid, between $25 and $1,000. LIMIT_EXEMPT is g1t's own.
36+ "LIMIT_NEW_MICROS": "3000000",
37+ "LIMIT_PAID_MIN_MICROS": "25000000",
38+ "LIMIT_PAID_MAX_MICROS": "1000000000",
39+ "LIMIT_EXEMPT": "syntaqx",
3340 // A free allowance on g1t's hosted models, so people can try g1t's
3441 // agents without a key of their own: each workspace may use this
3542 // much model cost ($1), out of one pool for everyone ($40), until
+4−0
1+-- When the app was paused because its workspace reached its limit for
2+-- usage not yet paid for. Paused apps answer with a notice; they are
3+-- rebuilt from the same commit once the workspace is under it again.
4+ALTER TABLE apps ADD COLUMN paused_at TEXT;
+26−7
3434 const ASSETS_ONLY = `export default { fetch(request, env) { return env.ASSETS.fetch(request); } };\n`;
3535
3636 /** What an app answers between being taken down and being gone. */
37+const PAUSED = `export default {
38+ fetch() {
39+ return new Response("<!doctype html><meta charset=utf-8><meta name=robots content=noindex><title>Paused</title><body style='font:16px system-ui;background:#121214;color:#ececf1;display:grid;place-items:center;min-height:100vh;margin:0;padding:0 16px'><main style='max-width:32rem'><h1>This app is paused</h1><p style='color:#9a9aa6'>The workspace it belongs to reached its usage limit on g1t. It comes back by itself once the workspace is under it again.</p></main>", { status: 402, headers: { "content-type": "text/html; charset=utf-8", "x-robots-tag": "noindex", "cache-control": "no-store" } });
40+ },
41+};`;
42+
3743 const TAKEN_DOWN = `export default {
3844 fetch() {
3945 return new Response("<!doctype html><meta charset=utf-8><meta name=robots content=noindex><title>Not up</title><body style='font:16px system-ui;background:#121214;color:#ececf1;display:grid;place-items:center;min-height:100vh;margin:0'><main><h1>This app is not up</h1><p style='color:#9a9aa6'>It was taken down. Deploying it again brings it back.</p></main>", { status: 404, headers: { "content-type": "text/html; charset=utf-8", "x-robots-tag": "noindex", "cache-control": "no-store" } });
164170 * reaches the edge as fast as any deploy, and deleted after.
165171 */
166172 async deleteScript(script: string): Promise<void> {
167− const form = new FormData();
168− form.append(
169− "metadata",
170− JSON.stringify({ main_module: "index.js", compatibility_date: "2026-09-26", bindings: [], tags: ["taken-down"] }),
171− );
172− form.append("index.js", new File([TAKEN_DOWN], "index.js", { type: "application/javascript+module" }));
173− await this.call("PUT", this.scriptPath(script), form).catch(() => undefined);
173+ await this.placeholder(script, TAKEN_DOWN, "taken-down").catch(() => undefined);
174174 try {
175175 await this.call("DELETE", `${this.scriptPath(script)}?force=true`);
176176 } catch (error) {
179179 }
180180
181181 /**
182+ * Replaces an app with a notice that it is paused: its workspace reached
183+ * its limit. The notice costs next to nothing to answer with, and the app
184+ * comes back by being deployed again.
185+ */
186+ async pauseScript(script: string): Promise<void> {
187+ await this.placeholder(script, PAUSED, "paused");
188+ }
189+
190+ private async placeholder(script: string, code: string, tag: string): Promise<void> {
191+ const form = new FormData();
192+ form.append(
193+ "metadata",
194+ JSON.stringify({ main_module: "index.js", compatibility_date: "2026-09-26", bindings: [], tags: [tag] }),
195+ );
196+ form.append("index.js", new File([code], "index.js", { type: "application/javascript+module" }));
197+ await this.call("PUT", this.scriptPath(script), form);
198+ }
199+
200+ /**
182201 * Requests and CPU time per app over a period, from Workers analytics.
183202 * Apps with no traffic are absent.
184203 */
+46−1
147147 deployed_at: string;
148148 created_at: string;
149149 last_request_at: string | null;
150+ /** Set while its workspace is over its limit; see `holdToLimits`. */
151+ paused_at: string | null;
150152 };
151153
152154 function toDeployment(row: DeploymentRow): Deployment {
578580 const id = newId("dpl");
579581 const token = randomToken();
580582 const plan = await billingClient(this.env.BILLING).hasFeature(project.workspace, "deployments");
583+ const limit = await billingClient(this.env.BILLING).checkLimit(project.workspace);
581584 const cloudflare = this.cloudflare;
582585 const refused = !plan.ok
583586 ? plan.error.message
587+ : limit.ok && limit.value.state === "stopped"
588+ ? (limit.value.message ?? "The workspace reached its usage limit.")
584589 : !cloudflare
585590 ? "Deployments are not set up on this g1t: it has no Cloudflare token."
586591 : null;
784789 .prepare(
785790 `INSERT INTO apps (script, project_id, workspace, slug, kind, branch, number, commit_sha, deployed_at, created_at)
786791 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9)
787− ON CONFLICT (script) DO UPDATE SET commit_sha = ?8, number = ?7, deployed_at = ?9`,
792+ ON CONFLICT (script) DO UPDATE SET commit_sha = ?8, number = ?7, deployed_at = ?9, paused_at = NULL`,
788793 )
789794 .bind(row.script, row.project_id, row.workspace, row.slug, row.kind, row.branch, row.number, row.commit_sha, at),
790795 ]);
948953
949954 // Apps of workspaces whose plan has ended come down.
950955 const billing = billingClient(this.env.BILLING);
956+ await this.holdToLimits(apps).catch((error) => console.error("could not apply limits", error));
951957 for (const workspace of workspaces) {
952958 const plan = await billing.hasFeature(workspace, "deployments");
953959 if (!plan.ok && plan.error.code === "payment_required") {
961967 await this.chargeMonths();
962968 }
963969
970+ /**
971+ * Pauses the apps of workspaces that reached their limit for usage not
972+ * yet paid for, and rebuilds them from the same commit once they are
973+ * under it again. Paused apps answer with a notice and run nothing.
974+ */
975+ private async holdToLimits(apps: AppRow[]): Promise<void> {
976+ const cloudflare = this.cloudflare;
977+ if (!cloudflare) return;
978+ const billing = billingClient(this.env.BILLING);
979+ for (const workspace of [...new Set(apps.map((app) => app.workspace))]) {
980+ const limit = await billing.checkLimit(workspace);
981+ if (!limit.ok) continue;
982+ const theirs = apps.filter((app) => app.workspace === workspace);
983+ if (limit.value.state === "stopped") {
984+ for (const app of theirs.filter((a) => !a.paused_at)) {
985+ await cloudflare.pauseScript(app.script);
986+ await this.db.prepare("UPDATE apps SET paused_at = ? WHERE script = ?").bind(now(), app.script).run();
987+ }
988+ continue;
989+ }
990+ const paused = theirs.filter((a) => a.paused_at);
991+ if (paused.length === 0) continue;
992+ const actor = await this.workspaceActor(workspace);
993+ if (!actor) continue;
994+ for (const app of paused) {
995+ const project = await this.projects.get(workspace, app.slug, actor);
996+ if (!project.ok) continue;
997+ // A failed or refused rebuild leaves it paused, to try again next time.
998+ const rebuilt =
999+ app.kind === "production"
1000+ ? await this.deployProduction(project.value, app.commit_sha, "g1t")
1001+ : app.number != null
1002+ ? await this.deployPreview(project.value, app.number, "g1t", true)
1003+ : null;
1004+ if (rebuilt && !rebuilt.ok) console.log("could not resume", app.script, rebuilt.error.message);
1005+ }
1006+ }
1007+ }
1008+
9641009 /** Scripts in the namespace that no app holds, such as ones renamed. */
9651010 private async removeOrphans(apps: AppRow[]): Promise<void> {
9661011 const cloudflare = this.cloudflare;
+1−1
3333 text: format!("{intro}\n\n{action}: {link}\n\n{footer}\n"),
3434 html: format!(
3535 "<div style=\"font-family:system-ui,sans-serif;max-width:480px;margin:0 auto;padding:32px 16px;color:#16150f\">\
36− <p style=\"font-size:20px;font-weight:600;margin:0 0 16px\">g1t</p>\
36+ <p style=\"margin:0 0 20px\"><img src=\"{SITE}/brand/g1t-logo.png\" width=\"69\" height=\"28\" alt=\"g1t\" style=\"display:block;border:0\"></p>\
3737 <p style=\"font-size:15px;line-height:1.6\">{intro}</p>\
3838 <p style=\"margin:24px 0\"><a href=\"{link}\" style=\"background:#16150f;color:#fff;text-decoration:none;padding:10px 18px;border-radius:6px;font-size:15px\">{action}</a></p>\
3939 <p style=\"font-size:13px;line-height:1.6;color:#6e6a5e\">{footer}</p>\
+53−2
112112 | { kind: "actions"; jobId: string; token: string }
113113 /** A build of one commit, deployed to g1t.page. */
114114 | { kind: "deploy"; deployId: string; token: string };
115−type RunRequest = Run & { envVars: Record<string, string> };
115+/** Whose sandbox time it is, reported when the sandbox stops. */
116+type Meter = { workspace: string; repo: string; description: string };
117+/** Deploy builds are metered by the Deployments plan, not here. */
118+type RunRequest = Run & { envVars: Record<string, string>; meter?: Meter };
119+
120+function meter(repo: RepoPath, description: string): Meter {
121+ return { workspace: repo.namespace, repo: `${repo.namespace}/${repo.name}`, description };
122+}
116123
117124 /** What the deployments service asks a sandbox to build. */
118125 type DeployJob = {
149156 sleepAfter = "45m";
150157
151158 async run(request: RunRequest): Promise<void> {
152− const { envVars, ...run } = request;
159+ const { envVars, meter, ...run } = request;
153160 await this.ctx.storage.put("run", run);
161+ if (meter) await this.ctx.storage.put("meter", { ...meter, started: Date.now() });
154162 await this.start({ envVars, enableInternet: true });
155163 }
156164
165+ /** Reports how long the sandbox ran, once, whatever it exited with. */
166+ private async meterStop(): Promise<void> {
167+ const metered = await this.ctx.storage.get<Meter & { started: number }>("meter");
168+ if (!metered) return;
169+ await this.ctx.storage.delete("meter");
170+ const seconds = Math.max(1, Math.ceil((Date.now() - metered.started) / 1000));
171+ const recorded = await billingClient(this.env.BILLING)
172+ .recordSandbox({
173+ workspace: metered.workspace,
174+ seconds,
175+ description: metered.description,
176+ repo: metered.repo,
177+ reference: `sandbox/${this.ctx.id.toString()}/${metered.started}`,
178+ })
179+ .catch((error: unknown) => ({ ok: false as const, error: { message: String(error) } }));
180+ if (!recorded.ok) console.log("sandbox time not recorded", metered.workspace, seconds, recorded.error.message);
181+ }
182+
157183 override async onStop({ exitCode, reason }: StopParams): Promise<void> {
184+ await this.meterStop();
158185 if (exitCode === 0) return;
159186 const run = await this.ctx.storage.get<Run>("run");
160187 console.log("sandbox stopped", run?.kind, "exit", exitCode, reason);
677704 repo: RepoPath;
678705 timeoutMinutes: number;
679706 }): Promise<Result<true>> {
707+ const over = await this.overLimit(args.repo.namespace);
708+ if (over) return { ok: false, error: { code: "payment_required", message: over } };
680709 // The same workspaces that may use g1t's sandboxes for agents.
681710 if (!(await this.workspaceAllowed(args.repo.namespace))) {
682711 return {
694723 kind: "actions",
695724 jobId: args.job,
696725 token: args.token,
726+ meter: meter(args.repo, `A workflow job in ${args.repo.namespace}/${args.repo.name}`),
697727 envVars: {
698728 MODE: "actions",
699729 G1T_API: "https://api.g1t.sh",
759789
760790 /** Whether a workspace's repositories may use g1t's agents and sandboxes at all. */
761791 private async workspaceAllowed(namespace: string): Promise<boolean> {
792+ if (await this.overLimit(namespace)) return false;
762793 const access = await this.modelAccess(namespace);
763794 return access.own != null || access.hosted;
764795 }
765796
766797 /**
798+ * Why the workspace can start no sandbox: it reached its limit for usage
799+ * not yet paid for. Null when it can, or when billing cannot say.
800+ */
801+ private async overLimit(namespace: string): Promise<string | null> {
802+ const limit = await billingClient(this.env.BILLING)
803+ .checkLimit(namespace)
804+ .catch(() => null);
805+ if (!limit?.ok || limit.value.state !== "stopped") return null;
806+ return limit.value.message ?? `The ${namespace} workspace reached its usage limit.`;
807+ }
808+
809+ /**
767810 * Whether `viewer` may put agents to work: in `repo`'s workspace, which
768811 * must be allowed and theirs, or with no repo named, in any workspace of
769812 * theirs that is allowed.
9871030 kind: "queue",
9881031 entryId: job.entryId,
9891032 token: job.token,
1033+ meter: meter(job.repo, `Merge queue on ${job.repo.namespace}/${job.repo.name}`),
9901034 envVars: {
9911035 MODE: "queue",
9921036 G1T_API: "https://api.g1t.sh",
10511095 await sandbox.run({
10521096 kind: "answer",
10531097 pullId: job.pullId,
1098+ meter: meter(job.repo, `Agent answering on ${job.repo.namespace}/${job.repo.name}#${job.number}`),
10541099 envVars: {
10551100 // Answered from its change as it stands: no merging in of the
10561101 // default branch, which would push a commit for a question.
10901135 await sandbox.run({
10911136 kind: "revise",
10921137 pullId: job.pullId,
1138+ meter: meter(job.repo, `Agent revising ${job.repo.namespace}/${job.repo.name}#${job.number}`),
10931139 envVars: {
10941140 MODE: "revise",
10951141 G1T_API: "https://api.g1t.sh",
11391185 kind: "checks",
11401186 runId: job.runId,
11411187 token: job.token,
1188+ meter: meter(job.repo, `Checks on ${job.repo.namespace}/${job.repo.name}#${job.number}`),
11421189 envVars: {
11431190 MODE: "checks",
11441191 G1T_API: "https://api.g1t.sh",
12491296 await sandbox.run({
12501297 kind: "update",
12511298 pullId: update.pullId,
1299+ meter: meter(repo, `Catching up ${repo.namespace}/${repo.name}#${number}`),
12521300 envVars: {
12531301 MODE: "update",
12541302 G1T_API: "https://api.g1t.sh",
13091357 kind: "review",
13101358 runId: job.runId,
13111359 token: job.token,
1360+ meter: meter(repo, `Review of ${repo.namespace}/${repo.name}#${number}`),
13121361 envVars: {
13131362 MODE: "review",
13141363 G1T_API: "https://api.g1t.sh",
13731422 kind: "plan",
13741423 planId: job.planId,
13751424 token: job.token,
1425+ meter: meter(repo, `Planning for ${repo.namespace}/${repo.name}`),
13761426 envVars: {
13771427 MODE: "plan",
13781428 G1T_API: "https://api.g1t.sh",
14531503 actor,
14541504 repo,
14551505 number: pull.number,
1506+ meter: meter(repo, `Agent on ${repo.namespace}/${repo.name}#${pull.number}`),
14561507 envVars: {
14571508 G1T_API: "https://api.g1t.sh",
14581509 G1T_TOKEN: token,