Public: anyone can read this; the crew can change it.
Revenue: awake time, storage and crew spend with the margin, per app per month — the statement billing reads #10418
Check · Review · Land · Live done — Landed and live in production.
Each app now gets a monthly statement of what the forge would charge it: awake time and storage at list prices, plus crew spend × (1 + margin%). It can be read as JSON or CSV from the API and printed from the app's settings.
shipped production is serving it deploy befa93c
Each app now gets a monthly statement of what the forge would charge it: awake time and storage at list prices, plus crew spend × (1 + margin%). It can be read as JSON or CSV from the API and printed from the app's settings.
List prices. Two new prices, awakePriceHourUsd (customer $/awake-hour) and storagePriceGbMonthUsd (customer $/GB-month), are stored as whole cents next to the operator's costs. They are set in Settings → Economics (a "List" row under Prices), PUT /api/v1/forge/economics, the MCP economics tool and the terminal economics screen, with the same validation and change history as the cost prices.
src/core/revenue.ts. statementFor(use, month, rates) is pure. It takes the app's awake-seconds, GB-hours and crew spend, plus the list prices and margin. It returns lines { what, quantity, unit, price, amount } for:
- awake time, all environments, owned or cloud;
- storage (GB-hours / 730);
- crew spend at cost;
- the margin on crew spend.
The margin applies to crew spend only. Amounts and the total are summed in cents and rounded once at the end; the total is the unrounded sum, rounded, as cost.ts does it. statementCsv writes the same lines as RFC 4180 CSV, quoting fields where needed and defusing a leading =, +, - or @. src/actions/statement.ts gathers one repository's meter and usage-ledger rows for the half-open UTC month; the repository's admins and forge admins can read it, and anyone else gets not_found.
GET /api/v1/repos/:owner/:name/statement?month=YYYY-MM. JSON by default; format=csv or accept: text/csv gives CSV. Without month it is this month so far. A month that is not YYYY-MM (2000-01 or later) gets 422 bad_month; a token with no access, or no token, gets 404. It is described in the OpenAPI routes.
Print view. /:owner/:name/settings/app/statement shows a month picker, the lines as a table with the total, and CSV/JSON links. Print rules hide the navigation and the picker. The app's settings link to it, including before the repository runs as an app, since crew spend counts already.
Worked example. 100 awake-hours at $0.10, 3 GB-months at $0.25 and $57.10 of crew at a 20% margin: 10.00 + 0.75 + 57.10 + 11.42 = $79.27. At margin 0 the total is $67.85. This is in docs/PROFIT.md and the new docs/features/statement.md.
Tests.
test/revenue.test.ts: the hand-built month to the cent, margin 0, margin on crew spend only, rounding once (a third of a cent plus two thirds is one cent), month parsing, and the CSV matching the JSON.test/statement.test.ts: the same month built from real meter intervals, storage samples and usage rows, with another repository's use and November's spend left out. It checks JSON against CSV (both byformatand byaccept), 404 for someone who does not administer the repository, 422 for a bad month or format, the printed page, and the link from settings.
The prices object grew by two fields, so whole-object assertions in test/economics.test.ts, test/mcp-economics.test.ts and the test/cost.test.ts fixture now include them; test/economics.test.ts also gets a new case for setting list prices.
Not done:
- No payment processor and no stored invoices: the statement is computed each time it is read.
- Each request computes the month's meter use for every app and keeps this one, the same read the economics page already does.
- The board's revenue line and gap are #510.
- The task asks me to keep a short plan on the issue with a
keep_plantool, but that tool was not available in this run, so the issue has no plan.
Checks run: bun scripts/gate.ts passes, bun x tsc --noEmit is clean, and bun scripts/design-lint.ts reports nothing added outside the system. scripts/fast.ts named 1,846 of 2,415 test files (the diff touches api.ts and server.ts), so I ran the covering suites myself; 717 pass, 0 fail:
- the economics, MCP economics, cost, slider, TUI economics and app-cost-line tests;
- the new revenue and statement tests;
- app settings;
- the OpenAPI, contract and MCP-tools inventories;
- route timing;
- security-floor routes and settings routes;
- design lint and audit.
Try it: set the list prices on /settings/economics, then open /:owner/:name/settings/app/statement?month=2026-11 and print it, or GET /api/v1/repos/:owner/:name/statement?month=2026-11&format=csv with a token.
🤖 Generated with Claude Code
This pull adds a revenue statement for each app and month: what the forge would charge, as awake time + storage + crew spend × (1 + margin), shown beside what each line costs (#508, part of the profit slider #392).
- Calculation:
src/core/revenue.tsholds a purestatementFor(app, month). It returns lines of{ what, quantity, unit, price, amount }and a total in USD, rounded to the cent once, at the end. - List prices: customer prices for awake time ($/hour) and storage ($/GB-month) are set on
/settings/economics. The margin % from the slider row applies to crew spend only. - API:
GET /api/v1/repos/:owner/:name/statement?month=YYYY-MManswers in JSON, or CSV withformat=csv(src/http/api/statement.ts). Only people who can read the repository can see it. - Page: a print view at
/:owner/:name/settings/app/statement?month=YYYY-MM(src/web/pages/statement.ts), with the assumptions written on it. - Tests: a hand-built month matches to the cent, margin 0 bills crew spend at cost, and the CSV and JSON agree.
The last commit has no code in it. It carries the Why now: line the gate's "say why it goes ahead of the pipeline" rule asks of a pull that adds a new action, page or route. The reason given is that the owner released #508 from Later on 2026-11-09.
Locally, bun scripts/gate.ts and bun x tsc --noEmit pass. scripts/fast.ts picked 1846 test files, over its 5-minute budget, so I ran the five that cover this change instead: revenue, statement, statements-frozen-clock, economics and why-now. All 32 tests pass.
Not done here: no payment processor and no invoicing; this is a statement anyone can check by hand.
To try it: set list prices on /settings/economics, then open /:owner/:name/settings/app/statement?month=2026-11 and print it, or request /api/v1/repos/:owner/:name/statement?month=2026-11&format=csv.
🤖 Generated with Claude Code
Each app gets a revenue statement per month (#508). Earnings are worked out from the list prices an admin sets on /settings/economics, priced on awake time, storage and crew spend, and shown beside what the app costs. The margin is figured on crew spend only.
- Pricing core:
src/core/revenue.tsholds the pricing as plain data in and out.test/revenue.test.tschecks it against hand-worked figures. - Page:
/:owner/:name/settings/app/statement?month=YYYY-MMis rendered on the server and has a print view. - API:
GET /api/v1/repos/:owner/:name/statement?month=YYYY-MMreturns JSON, or CSV with&format=csv. Its inputs are checked at the boundary, readers are checked for access, and the security floor stays on the response. - List prices: the economics settings, its API, the MCP tool and the terminal screen all now carry the list prices.
- Docs:
docs/PROFIT.md,docs/features/statement.mdanddocs/features/economics.mddescribe the statement.
The last check failed on two tests this pull doesn't touch: the timed security-floor sweep (added on main in #10416) and the build-cart capture test. This push merges current main. After the merge, both pass here, along with the gate, tsc --noEmit, design-lint and the statement, revenue and economics tests. The cart test also passed in three repeated runs. I didn't change either test. The failing run reported 16% disk free, and the sweep's 0 ms total means its timed requests never ran, which looks like load rather than a code fault.
Not done: no chart library and no forecasting. The statement only covers months that already have recorded usage.
To try it: set list prices on /settings/economics, then open /:owner/:name/settings/app/statement?month=2026-11 and print it, or curl the API with &format=csv.
🤖 Generated with Claude Code
The core, API and tests are sound: reads are gated to repo admins and forge admins with a 404 otherwise, month and format are validated, CSV is escaped and formula-defused, and the rounding follows cost.ts. Two things block it.
- Both doors (fix first). The statement exists on the page and in the API, but there is no screen for it in src/tui/, so a person at the terminal cannot read an app's statement. Only the list-price levers were added to the economics screen. Add a statement screen in src/tui/screens/, entered where AGENTS.md says. It must call readStatement, so the same admin gate applies. Show the lines and the total, and take a month. Add a test that a guest or non-admin sees nothing. Cover it in docs/features/statement.md.
- Design principle 1 (one confident sentence, nothing explained twice). The statement page repeats itself. The footer paragraph (From..to, 'Awake time and storage at list price; crew spend at cost plus a N% margin; each amount is rounded once') restates what the table's unit, price and margin rows already say. The economics page adds a new explanatory paragraph ('What a customer pays, beside the costs...') above the list-price form. The settings block adds another ('what the forge would charge, to print or export'). Cut each to a single plain sentence, or remove it where the row label already says it (docs/WRITING.md).
Also check, and fix if so: the inline PRINT_CSS passed to settingsPageResponse must be allowed by the CSP (nonce or a shared stylesheet), not weakened for it. Prefer a rule in the existing print stylesheet over page-inline CSS.
Boxes: the head meets all four issue boxes. The remaining work is the terminal door and the copy.
This fixes both review points: the statement now has a terminal screen, and the extra explanatory copy is gone.
Terminal screen. A new screen, src/tui/screens/statement.ts, adds owner/name — Statement to a repository's : palette. It sits in the workspace's per-repository commands, next to the traffic view. It reads through readStatement, so the gate is the same as the page and the API: the repository's admins and forge admins. Anyone else isn't offered the command, and if they reach the screen anyway they see no lines. It shows each line as quantity × price = amount, then the total, in USD for one UTC month. m asks for a month (a bad one is refused in the API's words), and [ / ] go back and forward a month.
test/tui-statement.test.ts checks that an admin's October matches a hand sum: 84 h × $0.10 + $50 + 20% of $50 = $68.40. It also covers month stepping, refusing 2026-13, and that a non-admin and a guest get no command and no lines. docs/features/statement.md now explains how to use it from the terminal.
Copy. I cut three pieces of text that repeated what the page already shows:
- the statement page's footer paragraph (the date range, the pricing and the rounding). The heading now ends in "(UTC)", so that survives printing even though the month picker is hidden.
- the paragraph above the list-price form on the economics page; the field labels and the margin beside the slider already say it.
- the explanation after the settings link, which is now just "This month's statement".
Print CSS. The inline CSS did not weaken the CSP. The layout already lists each inline <style> by its sha256 hash. I moved it anyway, as review preferred: the shared @media print rules in src/web/style/base.ts now hide [data-print-hide], and hide the repository's navigation on any page marked [data-print-alone]. The statement page sets those attributes and no longer passes inline CSS. test/statement.test.ts now checks that the page has no inline print style and that the shared stylesheet has the rule.
Checks. scripts/gate.ts, tsc --noEmit and design-lint pass. scripts/fast.ts named 1,870 test files because the shared stylesheet changed, so I ran the ones that cover this change myself: statement, the terminal statement screen, revenue, economics (page, terminal, MCP), settings routes and app domains, the design audit, lint and map, palette entries, the workspace, and the app-variables screen. They all pass.
🤖 Generated with Claude Code
This pull adds a revenue statement for each repository: income set against awake time, storage and crew spend. You can read it on a web page, through GET /api/v1/repos/{owner}/{name}/statement and its MCP tool, and on a terminal screen behind readStatement's gate. It also adds the economics settings the statement reads and a print view in the shared stylesheet.
Why the check failed: the cause came from main, not from this change. Commit 54dacba00 took the "why now" rule out of the gate, which left src/kernel/why_now.ts imported only by its own test, so test/unreached.test.ts failed. That module's own header says it is retired and kept only "until the next sweep of unused modules". So this pull deletes it and test/why-now.test.ts, after merging main. Adding it to scripts/unreached-allow.json would have passed the check too, but that list is meant to only ever shrink, so I didn't.
What I ran locally, all passing:
bun scripts/gate.tsbun x tsc --noEmittest/unreached.test.ts- the statement, revenue, terminal statement and economics tests
To try it: open the repository's statement page on the preview, or curl -H "authorization: Bearer $FORGE_TOKEN" https://lab--10418--forge-trevato-apps.open-platform.sh/api/v1/repos/trevato/forge/statement.
🤖 Generated with Claude Code
Both earlier asks are met. The statement now has a terminal screen (src/tui/screens/statement.ts) behind the same readStatement gate as the page and API, with a test. The extra copy is gone, and the print rules moved into the shared stylesheet, so there is no inline CSS. Pricing is pure and rounds once; the CSV defuses formula cells; reads are admin-only and return 404 otherwise; the month is validated. I did not run the tests in this round. The preview answers 404 to my token for trevato/forge, so I could not see the page or API working there; I read this as the token having no admin access on that repo. The behaviour is covered by the pull's own tests. Non-blocking: deleting src/kernel/why_now.ts and its test is unrelated to #508. It is justified here because it was red on main, but it would have been cleaner as its own pull.
Each app now gets a monthly statement of what the forge would charge it: awake time and storage at list prices, plus crew spend × (1 + margin%). It can be read as JSON or CSV from the API and printed from the app's settings.
List prices. Two new prices,
awakePriceHourUsd(customer $/awake-hour) andstoragePriceGbMonthUsd(customer $/GB-month), are stored as whole cents next to the operator's costs. They are set in Settings → Economics (a "List" row under Prices),PUT /api/v1/forge/economics, the MCPeconomicstool and the terminal economics screen, with the same validation and change history as the cost prices.src/core/revenue.ts.statementFor(use, month, rates)is pure. It takes the app's awake-seconds, GB-hours and crew spend, plus the list prices and margin. It returns lines{ what, quantity, unit, price, amount }for:The margin applies to crew spend only. Amounts and the total are summed in cents and rounded once at the end; the total is the unrounded sum, rounded, as
cost.tsdoes it.statementCsvwrites the same lines as RFC 4180 CSV, quoting fields where needed and defusing a leading=,+,-or@.src/actions/statement.tsgathers one repository's meter and usage-ledger rows for the half-open UTC month; the repository's admins and forge admins can read it, and anyone else getsnot_found.GET /api/v1/repos/:owner/:name/statement?month=YYYY-MM. JSON by default;format=csvoraccept: text/csvgives CSV. Withoutmonthit is this month so far. A month that is notYYYY-MM(2000-01 or later) gets 422bad_month; a token with no access, or no token, gets 404. It is described in the OpenAPI routes.Print view.
/:owner/:name/settings/app/statementshows a month picker, the lines as a table with the total, and CSV/JSON links. Print rules hide the navigation and the picker. The app's settings link to it, including before the repository runs as an app, since crew spend counts already.Worked example. 100 awake-hours at $0.10, 3 GB-months at $0.25 and $57.10 of crew at a 20% margin: 10.00 + 0.75 + 57.10 + 11.42 = $79.27. At margin 0 the total is $67.85. This is in
docs/PROFIT.mdand the newdocs/features/statement.md.Tests.
test/revenue.test.ts: the hand-built month to the cent, margin 0, margin on crew spend only, rounding once (a third of a cent plus two thirds is one cent), month parsing, and the CSV matching the JSON.test/statement.test.ts: the same month built from real meter intervals, storage samples and usage rows, with another repository's use and November's spend left out. It checks JSON against CSV (both byformatand byaccept), 404 for someone who does not administer the repository, 422 for a bad month or format, the printed page, and the link from settings.The prices object grew by two fields, so whole-object assertions in
test/economics.test.ts,test/mcp-economics.test.tsand thetest/cost.test.tsfixture now include them;test/economics.test.tsalso gets a new case for setting list prices.Not done:
keep_plantool, but that tool was not available in this run, so the issue has no plan.Checks run:
bun scripts/gate.tspasses,bun x tsc --noEmitis clean, andbun scripts/design-lint.tsreports nothing added outside the system.scripts/fast.tsnamed 1,846 of 2,415 test files (the diff touchesapi.tsandserver.ts), so I ran the covering suites myself; 717 pass, 0 fail:Try it: set the list prices on
/settings/economics, then open/:owner/:name/settings/app/statement?month=2026-11and print it, orGET /api/v1/repos/:owner/:name/statement?month=2026-11&format=csvwith a token.🤖 Generated with Claude Code
Closes #508