# SGit Newsroom API: the whole site in one fetch
Generated with the site at v0.2.14 (reader-code release v0.2.5). Each section is a page; its address is given first.
---
# About, licence, contact
> What this site and repository are, who is responsible, the licence, how to reach the people behind it, and the disclosure about how it is written.
# About, licence, contact
**What this is.** The code a reader runs on [newsroom.sgit.ai](https://newsroom.sgit.ai/), the reading meter, the reader's personas and lists, subscribe and the encrypted share, the vault layer, search, the wire reader, the passkey lab, released from one repository and served from this host at versioned paths that are never edited after they are published. "API" means client-side: components, formats and contracts the browser runs, with no server and nothing to deploy but files. On this network "API" can also mean the SG/Send server; this is not that.
**Who.** It is made by the SGit Newsroom's agents under the editorial and engineering responsibility of Dinis Cruz, as every page on newsroom.sgit.ai discloses. The reviews are done by agents and are open to people, which is what the public issues are for. The brief that governs the work is [Go slow to go fast](https://newsroom.sgit.ai/articles/explorer-to-villager-newsroom.html), refreshed in [`briefs/`]({{github}}/tree/dev/briefs).
**Licence.** Apache 2.0, for the code, the tests and this site. The articles on newsroom.sgit.ai are the newsroom's and carry their own terms.
**Contact.** [agent@riskmandate.ai](mailto:agent@riskmandate.ai), which an agent reads first and a person answers, for pilots and partnerships; [issues]({{github}}/issues) in the repository for anything about the code, which is where the work happens in any case.
**This site.** Generated by `site/build.py` in the repository from markdown pages, the map of what a reader runs, the release manifests and the proof of the move; nothing on it is typed twice. Every page has a `.md` twin, an agent block, and a place in [llms.txt](../llms.txt) and [llms-full.txt](../llms-full.txt). The source of a page is a file a person or an agent can edit; a pull request changes the site.
**Names.** SGit is the family of tools this comes from: the `sgit` command line, the SG/Send encrypted vaults, the sites under sgit.ai. The newsroom is one of them.
---
# Adopt it
> Three ways in: put the meter on your own pages today; run a newsroom like newsroom.sgit.ai; pilot it with the people who built it. What you need, what you keep, what you give, and why the formats matter more than any one site.
# Adopt it
> Nothing here needs permission. The code is Apache 2.0, the releases are directories of files with a manifest of hashes, and the live site that runs it is the reference, not the product. The aim is other newsrooms running the same reader, so that the formats, the article number, the list link, the share, the envelope, mean the same thing everywhere.
## Three ways in
**1. Put the meter on your pages.** A static site, a JSON config, a custom element, a pinned import from this host. Readers get a credit and a balance, pay by depth and after reading, and rate. Your site publishes two small data files. [Ten lines, explained](../how-to/add-the-meter/). This is the route for a section of an existing site.
**2. Run a newsroom like this one.** The newsroom's repository is public: the pages, the build, the sync, the gate, the engineering board. Fork it, point it at this host, change the words. The newsroom's own [replicate](https://newsroom.sgit.ai/replicate/) page says what is packaged and what is not yet (the article generator; payments beyond a Payment Link).
**3. Pilot it with us.** For a publisher who wants to try the whole thing, meter, personas, lists, the vault, on a part of their site with their own payment link and lane, and to learn with us from what readers do. Write to [agent@riskmandate.ai](mailto:agent@riskmandate.ai) (an agent reads first; a person answers) or [open an issue]({{github}}/issues).
## What you need
- A static host for your pages (any; the newsroom uses GitHub Pages).
- The article feed (`articles/graphs.json`) and the numbers file (`articles/ids.json`), written by your build. [The contracts](../docs/contracts/).
- A Stripe Payment Link for the top-up, returning to your account page. Until it is set the site says so and nothing is charged.
- Optionally a vault on an SG/Send host for the lane: shares, subscriptions and notes arrive there, encrypted to your key. [Setting up the lane](../how-to/subscribe-lane/).
- One line that pins the release you load, and a copy of its manifest. [Taking a release](../how-to/take-a-release/).
## What you keep, what you give
You keep your readers' trust in a concrete form: their data never reaches you unless they send it, and when they do it arrives encrypted to a key only you hold. You keep the money, through your own payment link. You keep the site you have.
You give nothing to us. There is no account, no key, no telemetry, no call home: the only requests to this host are for the code, and a release never changes. If you prefer, copy a release to your own host; the manifest tells you whether you have the same bytes.
## Why the formats matter more than the site
An article number that never changes. A reading list that is a link. A persona that is a name, a theme and a list of what was kept. A share that is an encrypted envelope to a published key. These are the parts that are worth agreeing on across newsrooms, because then a reader's habits are theirs and not a site's. The [contracts](../docs/contracts/) are small on purpose, and changing one is a release with a note, never an edit in place.
## Contributing
- Issues are the work: every finding of the review is one, and every change starts as one.
- A pull request is reviewed by a person and an agent before it merges; the agent's review is a required check.
- The served files are frozen until phase 4 of [the brief](https://newsroom.sgit.ai/articles/explorer-to-villager-newsroom.html) is finished: a change is a new release with a test that fails without it and a reason in the pull request.
- The tests run in three engines on every commit, against the newsroom's real pages; a change that breaks a page there is caught before it ships.
The brief that governs the work, and what changed when the move was done: [`briefs/`]({{github}}/tree/dev/briefs) in the repository. The way the agents that keep this work are briefed is [the agents page](../agents/), which is written for people too.
---
# For agents
> The brief for the agents that keep this repository and the newsroom: what the API is, the rules, where everything is, how a change is made and reviewed, how the newsroom consumes a release, and what never to do. Written for people too.
# For agents
> You are reading the brief for the agents that keep this repository, and for the ones that keep newsroom.sgit.ai and consume it. It is public because the work is. A person changing the code should read it too.
## What this is, in three lines
The code a reader runs on newsroom.sgit.ai, released from this repository and served from `https://api.newsroom.sgit.ai//`, never edited after it is published. "API" means client-side: components, formats and contracts the browser runs; no server. The newsroom (and, next, sgit.ai) pins one release.
## Read these first
1. [`llms.txt`](../llms.txt): every page of this site with one line each; [`llms-full.txt`](../llms-full.txt) is the whole site in one fetch.
2. [The current manifest](../{{release}}/manifest.json): every served path, bytes, SHA-256, and which files differ from the newsroom's bytes and why.
3. [`briefs/`]({{github}}/tree/dev/briefs): the plan, refreshed at newsroom v0.12.10, with the addendum written when the move was done. The original is [Go slow to go fast](https://newsroom.sgit.ai/articles/explorer-to-villager-newsroom.html).
4. [`map/before/`]({{github}}/tree/dev/map/before): the map of everything a reader runs, as it was before the move; [the review](../review/) is read from it.
5. [`tests/`]({{github}}/tree/dev/tests): what is proved, and how the harness works.
## The rules
- **The served files are frozen** until phase 4 of the brief is finished. A change is a new release with a test that fails without it and a reason in the pull request. A fix to a review finding is a deliberate change to the test that pinned it.
- **A release is a directory that is never changed.** `releases//` and its `manifest.json`, cut by `tools/release.py cut`; `check` recomputes every hash and fails when the tree is not the release `tools/version.json` names. Nothing is loaded unversioned.
- **Identical paths, for now.** A served path is the path the file had in the newsroom; reshaping is phase 4.
- **Three files locate the site root from the page** (`data-root`), not from their own URL: `vault-pane.js`, `search.js`, `ask.js`. Any new file that links to pages or fetches site data does the same.
- **Tests in three engines on every commit**, against the newsroom at the commit in `tests/newsroom.json`; a `SKIP` fails CI. Clipboard reads and CDP (the virtual authenticator, the faked built-in model) are Chromium's.
- **A pull request needs a person and an agent reviewer.** The agent's review is a required check.
- **Bugs and vulnerabilities are tests that pass**, recording the behaviour as it is, with an issue; a vulnerability test and its issue stay in a private vault until the fix lands.
- **No third-party script on a reader page.** One vendored library, pinned at a version, never edited here.
## Where things are
| | |
|---|---|
| the served files | `assets/`, `for/`, `wire/`, `passkey/` at the root; `tools/served.json` is the list |
| releases | `releases//`, `tools/version.json`, `tools/release.py` (cut, check, site), `tools/live_check.py` |
| the proof of the move | `tools/prove_move.py`, `map/proof/.json` |
| the tests | `tests/harness.mjs` (two trees: API and SITE; routes the host to the tree), `tests/test_*.mjs`, `tests/vault-mock.mjs`, `tests/newsroom.json` |
| this site | `site/build.py` → `www/`; pages in `site/pages/` (markdown or HTML fragments with front matter); data in `site/data/`; release notes in `site/releases/`; `--check` in CI |
| the map | `map/before//map.json` and `hashes.json`; `tools/api_map.py` (moved from the newsroom; to be re-pointed at this tree and a newsroom checkout, nightly) |
| the review's findings | issues 1 to 18, labelled by phase, area and severity |
| CI and deploy | `.github/workflows/ci.yml`, `deploy-pages.yml` (main: check, tag, Pages, live check) |
## How the newsroom consumes a release
`admin/build/api.json` pins the release; `admin/build/api-manifest.json` is its manifest, copied; `admin/build/api.py` writes every reference (`--repoint`), recomputes the import gate's hashes (`--rerecord`) and checks every page; `validate.js` check 14 refuses a relative, unpinned or unlisted reader-file reference, check 13 holds the frozen meter copies to the manifest; the sync's items 11 and 14 point imported pages at the host and stop importing the reader code. The normaliser's one added rule (a reader-file reference, relative or on the host, reads as `API/`) is in `api.py`, mirrored in `validate.js`; keep the two in step. The newsroom's release note v0.13.0 and card 036 describe the move from its side.
## Changing this site
Edit or add a page under `site/pages/` (markdown, or an `.html` fragment when the layout needs cards, doors or a diagram), with front matter: `title`, `description`, `section`, `eyebrow`, `crumbs`, `agent` (the one-paragraph brief for agents), `wide`. Then `python3 site/build.py` and commit `www/` with the source; CI fails if `www/` is not what the build writes. The data pages (contracts, state, review, map, proof, releases, articles) are built from the records, not written: change the record. Refresh the article feed from a newsroom checkout with `--refresh-articles`.
## What never to do
Edit anything under `releases/`. Move a tag. Load a reader file unversioned or by a relative path. Change a served file without a new release and a failing test. Add a third-party script to a reader page. Put a key, a read key or an access key in a file. Merge without a person's review.
---
# The components
> The served files by group, what each is for, where it came from, and which are frozen: SG Meter and its versions, the component base, the reading lists, subscribe, the vault layer and the pane, search and ask, the wire reader, the vendored vault library, the passkey lab.
# The components
> {{files}} files in the current release, {{bytes}} bytes, listed path by path in [the manifest](../../{{release}}/manifest.json). What follows is the same set by group: what each is for and where it came from.
## SG Meter: `assets/components/sg-meter/v1/`
| Version | Files | State |
|---|---|---|
| `v1.4/v1.4.1/` (`sg-meter.js`, `sg-meter-core.js`, `sg-meter.css`, `sg-meter.tpl`) | 4 | **the line that continues**: the newsroom's build of v1.3.1 without "Pause the meter" and "Start again", with the landing view for persona links, and the v1.4.1 changes the newsroom made in October |
| `v1.4/v1.4.0/`, `v1.3/v1.3.1/`, `v1.3/v1.3.0/`, `v1.2/v1.2.0/`, `v1.1/v1.1.0/`, `v1.0/v1.0.0/` | 24 | frozen: published artefacts, served byte for byte, never edited; a page that pinned one keeps loading the same files |
`sg-meter.js` is the element (twelve views, the reader card, the share form, the persona graph); `sg-meter-core.js` is the meter (prices, the charge, ratings, declining, credit, personas, picks, the share text); the `.tpl` and `.css` are fetched beside it.
## The component base: `assets/components/base/v1/v1.0/v1.0.0/sg-component.js`
The contract a component follows: `static jsUrl = import.meta.url`, `get resourceName()`, `onReady()`, a `.tpl` and a `.css` beside the `.js`, published at an immutable versioned path. It fetches and caches the siblings and reports a failure as `:error`.
## Reading lists: `for/`
`reading-list.js` parses and builds the link (`LIMITS`, `parse()`, `build()`), shows a list and opens it as a visitor persona through the meter's public calls; `make.js` is the page that makes and previews one; `for.css` styles both. [The grammar](../reading-list-link/).
## Subscribe and the share: `assets/subscribe.js`
The subscribe form: reads the contact file, checks the key's fingerprint, encrypts an `.eml` to the published key (the sgit hybrid envelope v2) and appends it to the lane. The meter's share view uses the same envelope.
## The vault layer: `assets/newsroom-vault.js`, `assets/door.js`, `assets/vault-pane/`
`door.js` derives the door from a passkey's PRF bytes or a recovery code; `newsroom-vault.js` is the newsroom's one module over the vault library: the door, the master, the reader vault, the lane. `vault-pane/` is the pane on every page that carries the meter: `vault-pane.js` the UI, `notes.js` the notes as records, `reading.js` what was read, `wire-state.js` the wire's lens and stories seen, `pending.js` what only this browser holds, `sync.js` the sync as pure functions, `inbox.js` and `send.js` (with `send.css`) the reader's inbox: sending this browser's records to a vault from any browser, with a QR code from the vendored `qrcode-generator`.
## Search and ask: `assets/search/`
`search.js` ranks the site's pages in the browser (BM25 over fields and paragraphs) from `search/docs.json` and `search/text.json` that the site publishes; `ask.js` gives the top passages to Chrome's built-in model or to openrouter.ai with the reader's own key; `search-nav.css` is the Search link in every nav, `search.css` and `ask.css` the page.
## The wire reader: `wire/`
`vault-reader.js` opens a public-read vault in the browser with its published read key (vendored from sgit.ai's vault); `wire.js` draws the edition, the sources and the documents as text, never as markup, for every wire (the Industry Wire, the OWASP Wire, the AWS Wire, the Portugal Wire); `graphs.js` draws the edition as graphs; `subscribe.js` is a wire's subscribe form; `wire.css`.
## The vendored libraries: `assets/vendor/`
`qrcode-generator/2.0.4/qrcode.mjs`: the QR code the inbox shows, with its README.
### `assets/vendor/sg-vault/`
`v0.2.3/`: twelve files of the library that runs the SG/Send host, from `SGraph-AI__App__Send` v0.2.3, never edited here; `VENDOR.md` records the upstream commit and every hash, and the vault test checks them. `local/sg-vault--create-with-id.js`: one extension pending upstream (create a vault at a chosen id).
## The passkey lab: `passkey/lab.js`, `lab.css`, `vectors.json`
The proofs of concept for [the passkey opens the vault](https://newsroom.sgit.ai/articles/the-passkey-opens-the-vault.html): a passkey with PRF, the door derivation written out, sgit's keys, a recovery code, test vectors against sgit's Python (`tools/passkey_vectors.py` writes them), and the door created on a vault server. A lab, not a feature; it moved because it is reader-run code with a test.
## What is not here, on purpose
The pages (the newsroom's), the content data (`articles/graphs.json`, `articles/ids.json`, `persona/links.json`, `search/*.json`, `wire/snapshot.json`), the contact file at `/.well-known/`, and the site's chrome (`site.js`, `shots.js`, `nav.js`). They belong to the site that serves the page; this host serves what the browser runs.
---
# The hosting model
> How the reader code is served and consumed: a versioned prefix per release, never edited in place; the CORS header a cross-origin import needs; what stays on the page's origin (state, data, addresses); data-root; pinning, the manifest, rollback; CSP.
# The hosting model
> The code lives on one host, in one directory per release, and a release is never changed after it is published. The pages that run it live wherever the newsroom lives. Everything about a reader stays with the page.
## The address of a file
```
https://api.newsroom.sgit.ai//
https://api.newsroom.sgit.ai/{{release}}/assets/components/sg-meter/v1/v1.4/v1.4.0/sg-meter.js
https://api.newsroom.sgit.ai/{{release}}/manifest.json
```
`` is the path the file had in the newsroom's repository, so the move was a prefix rewrite and nothing else; reshaping the paths is a later phase. The meter's own component versions nest inside the release unchanged. `manifest.json` lists every path with its bytes and SHA-256, and which files differ from the newsroom's bytes and why. [`releases.json`](../../releases.json) lists the releases.
## What a cross-origin host needs
GitHub Pages serves every file with `access-control-allow-origin: *`. That is what an ES module import from another origin needs, and what the component base needs when it fetches a component's `.tpl` and `.css` beside its `.js` through `import.meta.url`. Classic `
Prices are in pence (or the smallest unit of symbol). start is the opening credit. The paths in links, picks and share.contact are from your site's root, and the meter resolves them against the page, never against this host. Every key has a default, so a config can be short; the full shape is [the meter config contract](../docs/contracts/).
Only sg-meter.js is imported; it brings its core, its template and its stylesheet from the same versioned directory on this host. A published path is never changed: a fix is a new version path, so a page that works today keeps loading the same bytes.
## 2. The element's views
One element, twelve views, each a page or a part of one. view="page" charges and shows the reader card; the rest are the account.
ViewWhere it goesWhat it does
balancethe navigation, on every pagethe balance chip; marks a negative balance; opens the account
pagethe foot of any content pagecharges by depth for the page's kind; the reader card: rating, decline, keep for a persona. quiet for a free page
accountyour account/index.htmlbalance, spent, history, top up, export and restore
topup, topped-upthe top-up page; the page the payment returns tothe packs and the payment link; crediting the return once per reference
newsroomthe reader's newsroom pagepicks for the active persona from your feed; the persona graph
personasthe personas pagethe presets, the reader's own, rename, activate, keep and reject
landinga persona page (/persona//)a link opens the site as a persona, added as a visitor the reader can keep or remove
sharethe share pagea summary and the data as JSON, encrypted to your key and sent to your lane, after consent
The newsroom's own account pages are the reference implementation: [account]({{newsroom}}/account/), [newsroom]({{newsroom}}/account/newsroom.html), [personas]({{newsroom}}/account/personas.html), [share]({{newsroom}}/account/share.html), [top-up]({{newsroom}}/account/top-up.html). Copy their bodies; the element does the work.
## 3. The data your site publishes
FileForShape
articles/graphs.jsonpicks, the persona graph, the reading list's titles{"articles": [{"slug", "title", "date", "url", "teaser", "topics": [], "tags": []}, …]}; the newsroom's is [here]({{newsroom}}/articles/graphs.json)
articles/ids.jsonreading lists in a link{"next": N, "articles": [{"n", "slug", "title", "date", "url", …}]}: n and slug are the contract, assigned once, never changed or reused
.well-known/sgit-subscribe.jsonthe share and subscribe lanesgit-subscribe/v1: your public key and its fingerprint, the lane's vault, endpoint and append token. [Setting one up](../how-to/subscribe-lane/)
persona/links.jsonwhoever writes the persona linksone entry per preset, with its page; generated from the config
## 4. The reader's own newsroom: lists, personas, the vault, search
These are pages of your own that import the code from this host. Each needs (the path from the page to your site's root), which is how the pane and search find your pages and data from another origin.
The make page (for/make.js), the persona pages, the search page (assets/search/search.js, ask.js) and the wire (wire/wire.js) follow the same pattern; the newsroom's pages are the worked examples, and [the how-to guides](../how-to/) walk through each.
## 5. The hosting model, and what a different host changes
GitHub Pages serves every file here with access-control-allow-origin: *, which is what a cross-origin module import and the component's sibling fetches need. Storage belongs to the page's origin, so a reader's state stays on your site whichever host serves the script. The meter fetches your feed and your contact file from the page's origin; nothing is fetched from this host but code. If you add a Content-Security-Policy, script-src and connect-src name https://api.newsroom.sgit.ai. [The hosting model](../docs/hosting/).
## 6. Pin a release, verify it, roll it back
# the current release and its manifest
curl -s https://api.newsroom.sgit.ai/releases.json
curl -s https://api.newsroom.sgit.ai/{{release}}/manifest.json | head -40
# check one file against the manifest
curl -s https://api.newsroom.sgit.ai/{{release}}/assets/components/sg-meter/v1/v1.4/v1.4.0/sg-meter.js | sha256sum
Write the release into one place in your build and generate every URL from it; the newsroom does this with admin/build/api.json and checks every page against the manifest in its gate. [Taking a release and rolling back](../how-to/take-a-release/).
## 7. Run the tests against your pages
The tests that cover the meter, the lists, the pane, search and the wire are in the repository, and they run against any site that imports this code: they serve your pages locally and route this host to the working tree, in Chromium, WebKit or Firefox.
git clone {{github}}
cd SGit-AI__Website__Newsroom__API && npm install --no-save playwright@1.56.1 && npx playwright install chromium
NEWSROOM=/path/to/your/site node tests/test_meter.mjs
BROWSER=firefox NEWSROOM=/path/to/your/site node tests/test_lists.mjs
[Running the tests](../how-to/run-the-tests/), including what each file covers and which need Chromium.
## Where everything is
- [Reference](../docs/): contracts, state, inputs and outputs, components, hosting.
- [How-to guides](../how-to/), one task each.
- [Releases](../releases/) and [the current manifest](../{{release}}/manifest.json).
- [Source]({{github}}), [issues]({{github}}/issues), and [the brief for agents](../agents/), which is also the brief for a person changing the code.
- [SG Meter, documented]({{newsroom}}/meter/) on the newsroom, with [its security model]({{newsroom}}/meter/security.html).
---
# For evaluators
> The architecture, the trust boundaries, what leaves the browser, the review's findings as public issues, the release manifest, the proof of the move, the pipeline, and how to verify every claim yourself.
# For evaluators
This page is for the person who has to say yes or no. It is written so that nothing on it needs to be taken on trust: every number is read from a record in the repository, every finding is a public issue, every file has a hash on the live host, and the move that put the code here was proved byte for byte.
## The architecture in four sentences
There is no server of ours: the reader code is static files on a host that cannot run code, and the pages that use it are static too. The reader's state (balance, history, personas, lists, notes) lives in the browser's storage on the newsroom's origin, and optionally in an encrypted vault whose keys derive from the reader's passkey and which the newsroom cannot open. Money moves through a Stripe Payment Link the newsroom owns; a return page credits the balance once per payment reference. Anything a reader sends (a share, a subscription, a note) is encrypted in the browser to a published key and appended to a write-only lane in a vault.
## Trust boundaries
PartySeesCannot
The reader's browsereverything: the state is theirs, readable, exportable, editablebe stopped from editing their own balance (accepted; see the security model)
The newsroom (static host)page requests, as any host doessee a balance, a persona, a list or a note; verify a payment
This host (static files)requests for the coderun code, set headers, see any reader data
Stripecard, email, name, postcode, at paymentsee what was read
The vault host (SG/Send)ciphertext, sizes, timesread a share, a note or an account; the keys are the reader's and the newsroom's
The newsroom's agent, with the lane's read keyshares, subscriptions and notes, decryptedwrite to the lane or to a reader's vault
## What leaves the browser
Nine channels, each listed with what, when and who sees it, on [state, inputs and outputs](../docs/state/), read from the map. In short: the page's own origin for files; POST of an encrypted envelope to the lane when the reader presses Send; navigation to Stripe when they press Top up; GET of ciphertext from the wire's vault; a postMessage ready signal if a page is framed; the clipboard and a download at the reader's command. The tests record the hosts a page contacted and fail on any other.
## The accepted gaps, written down
The security model on the newsroom ([meter/security.html]({{newsroom}}/meter/security.html)) accepts four things a server would prevent, because there is no server by design: the return page credits any session id; the reader can edit their own balance; the names in a reading list are whatever the link says; a page is charged by the deepest point reached. The review's job was to prove those are the only gaps.
## The review: {{findings}} findings, each a public issue
Before the code moved here it was mapped and reviewed line by line, the way [a fractal semantic graph]({{newsroom}}/articles/code-review-as-a-fractal-semantic-graph.html) is read: every file, every control a reader can click, type or pay, every input and output, every contract, then every function the findings name. The findings are [issues 1 to 18]({{github}}/issues) in the repository, labelled by phase, area and severity. Where a finding can be pinned, a test records the behaviour as it is today, so that the fix is a visible change to a test. The three a security evaluator will want first:
- No Content-Security-Policy on any page ([R10]({{github}}/issues/10), high). The pages' loader runs fetched scripts through eval, and GitHub Pages sets no headers, so frame-ancestors cannot be set. The fix is a phase-5 change: load scripts as modules, then a meta CSP; framing protection needs a host that sets headers.
- Payment references unbounded and unchecked ([R14]({{github}}/issues/14), low) and restore() trusts a file's fields ([R2]({{github}}/issues/2), low): the exposure is a file someone sent the reader to restore.
- Production reader data goes to hosts named dev ([R5]({{github}}/issues/5)): a naming decision for the operator, listed so that nobody is surprised.
[The full table](../review/), with the test that pins each and the proposed change.
## Supply chain
- Third-party scripts on the reader pages: none. The build fails if one appears (the brief's rule).
- One vendored library, the vault library that runs the SG/Send host, pinned at a version and never edited here; its twelve files are hashed in VENDOR.md and the test checks them.
- Every release is a manifest of paths, bytes and SHA-256; the live host is checked against it after every deploy (tools/live_check.py), because [green does not mean live]({{newsroom}}/articles/green-does-not-mean-live.html).
- Nothing is edited in place. A release directory, once published, is frozen; CI recomputes every hash in every manifest on every commit.
## The proof of the move
This code lived in the newsroom's repository until 13 October 2026. Moving it to a host of its own was done as a pure refactoring, and proved: of {{files}} files, 60 hash exactly as they did before, and the three that differ do so by the lines that located the site root from the script's own address, each recorded in the manifest with its before-hash and the reason; of 597 pages on the newsroom, 593 normalise to what they were. [The proof](../review/proof/), with the report it was written from.
## The pipeline
Every commit: syntax, the manifest, a build that is the same bytes twice, and the tests in Chromium, WebKit and Firefox against the newsroom's real pages, in about four minutes. Every push to main: a tag, the Pages deploy, and the live check. A pull request needs a person and an agent reviewer. [The runs]({{github}}/actions).
## Verify it yourself
# every file of the current release, with its hash, from the live host
python3 - <<'END'
import json, hashlib, urllib.request
host = "https://api.newsroom.sgit.ai/{{release}}/"
m = json.load(urllib.request.urlopen(host + "manifest.json"))
for f in m["files"]:
b = urllib.request.urlopen(host + f["path"]).read()
print("ok " if hashlib.sha256(b).hexdigest() == f["sha256"] else "BAD", f["path"])
END
# the same, from the repository's own tool
git clone {{github}} && cd SGit-AI__Website__Newsroom__API && python3 tools/live_check.py
The newsroom's gate does the equivalent on its side: every page must load the reader code from the pinned release, by a URL that is in the manifest, and never by a relative path. Its source is [public]({{newsroom_github}}) too.
## Licence and provenance
Apache 2.0. The code was written by the newsroom's agents under Dinis Cruz's editorial and engineering responsibility, as every page on newsroom.sgit.ai discloses; the review was done by agents and is open to people, which is what the issues are for. [About, licence, contact](../about/).
---
# For publishers, editors and executives
> What a reader gets, what the newsroom keeps, what it costs to adopt, what is live and taking money, and what is honestly not there yet. The case for a reading meter and a reader's own newsroom, from a site that runs one.
# For publishers, editors and executives
You know the problem. A paywall turns a reader away at the door; a free site pays nothing back; a subscription asks for a commitment before the first paragraph. This is a third thing, running on a live site since October 2026: a reader walks in with a small credit, a meter charges by how far they read, they pay after, and what they pay is set by what they thought of it. Around it, a newsroom that is theirs: personas, picks, lists they can send in a link, an account that travels with them, and none of it on a server of ours.
{{live: Everything on this page is running at [newsroom.sgit.ai]({{newsroom}}/). Open any article and watch the balance in the top bar; [the reading account]({{newsroom}}/account/) shows what you spent and why. It is a working instance, not a demo.}}
## What a reader gets
[Pay after you read
### A meter, not a wall
£5 of credit to begin. An article costs 5p, a new one 10p, charged by the deepest point reached. Rate it at the foot and the price moves: excellent is double, not useful is free. Declining refunds the page.
]({{newsroom}}/articles/pay-after-you-read.html)
[Personas
### Read as who you are today
A founder, a journalist, a security lead, a builder, a board member. Each has a starting reading list and picks that follow what they keep and reject. A link can open the site as one.
]({{newsroom}}/articles/a-link-to-a-persona.html)
[Reading lists
### Send five articles, not five links
Every article has a number that never changes. A reading list is a link with the numbers in it, from you to a named reader, with a note. Nothing is stored anywhere but the link.
]({{newsroom}}/articles/a-reading-list-in-a-link.html)
[The vault
### An account that travels, with no sign-in
A passkey opens an encrypted vault that carries the reading account between a phone and a laptop. The newsroom never holds a key and cannot read it.
]({{newsroom}}/articles/the-vault-is-live.html)
[Notes
### Write back, one tap
A reader marks a page done, wrong, more like this, or writes to the newsroom; the notes travel in their vault and arrive encrypted in a lane the newsroom reads.
]({{newsroom}}/articles/the-site-is-an-inbox.html)
[The wire
### Industry news as an inbox
Stories agents gather into a vault, read live in the browser, filtered by the reader's lens, with what is new since their last visit.
]({{newsroom}}/wire/)
## What the newsroom keeps, and what it never holds
### Keeps
- The money. A reader tops up through a Stripe Payment Link you own; the return page credits their balance. No card data touches your site.
- The formats. Article numbers, persona links, the reading-list grammar, the share envelope: published contracts your readers and their agents can rely on, and that other newsrooms can share.
- The relationship. Shares and subscriptions arrive encrypted to a key only you hold, in a write-only lane; notes from the pane arrive the same way, with the page and the reader's words.
- The site you have. Static pages, any host. The reader code is a handful of files pinned from this host.
### Never holds
- Reader data. The balance, the history, the personas, the lists and the notes live in the reader's browser, and in their own vault if they choose one. There is no reader database because there is no server.
- Keys. The vault's keys are derived from the reader's passkey; the newsroom reads a lane with a read key the reader hands over, and nothing else.
- A dependency on us. The code is Apache 2.0, every release is a directory of files with a manifest of hashes, and a rollback is one line.
## What it costs to adopt
-
### A static site
Your pages, wherever they are hosted. The meter is a custom element and a JSON config on each page; the balance chip sits in your navigation. [Ten lines](../how-to/add-the-meter/).
-
### Two small data files
An article feed (titles, dates, topics, teasers) for the picks, and a numbers file that gives every article a stable number. The newsroom's build writes both; yours can too. [The contracts](../docs/contracts/).
-
### A payment link
A Stripe Payment Link for the top-up, returning to your account page. Until it is set, the site says so and no money moves.
-
### A vault for the lane, optional
Shares, subscriptions and notes arrive in an encrypted vault on an SG/Send host, drained by a person or an agent with your key. Without it, the meter still runs; the reader just cannot write back.
-
### A pinned release
One line names the release you load from this host; a manifest lists every file's hash; a rollback is the previous line. [How](../how-to/take-a-release/).
## The numbers from a live site
credit to start£5.00in the reader's browser, no sign-in
an article5p10p when new; by depth; nothing if not useful
top-up£5by card, through a Stripe Payment Link
starting personas5each a reading list and a link
reader code{{reader_lines}} linesopen source, reviewed, tested in three engines
servers of ours0by design; the security model says what that accepts
## What is honestly not there yet
The site that runs this says so on its own [what runs, what is designed]({{newsroom}}/shipped/) page, and the review here lists it as [findings](../review/). In short: the balance lives in the reader's browser, so a reader can edit their own credit, and the return page credits a payment reference without verifying it; both are accepted and written down in [the security model]({{newsroom}}/meter/security.html). No page sets a Content-Security-Policy yet. The vault hosts the live site uses are named dev. Shares from the newsroom are still labelled with the site the code came from. Each is an open issue with a proposed change, and each will be a visible change to a test when it is fixed.
## The thinking behind it
None of this was built first and explained after. Each piece is an article on newsroom.sgit.ai, written before it shipped and again when it went live. For an editor or an executive, start with these four:
[The case
### The future of news is a story vault, not a paywall
Why the unit of news should be a story a reader can hold, not a wall they must cross.
]({{newsroom}}/articles/future-of-news-story-vault-not-paywall.html)
[The thesis
### Sell the graph
The newsroom's founding idea: the value is in the connections between what is published, and that is what a reader pays for.
]({{newsroom}}/thesis/)
[Economics
### Paying the fact creator
What a meter makes possible that a subscription does not: money that follows the reading, down to the person who found the fact.
]({{newsroom}}/economics/)
[Going live
### Going live with the reading meter
The day real money started moving, what was switched on, and the line between what runs and what is designed.
]({{newsroom}}/articles/going-live-with-the-reading-meter.html)
More: [the articles behind it](../articles/), in reading order. The design pages on [corrections that propagate, provenance as the product, and content rights]({{newsroom}}/design/) are the newsroom's own.
## Starting a pilot
If you run a newsroom and want to try this on a section of your site, with your own payment link and your own lane, the people behind newsroom.sgit.ai will help you set it up and will learn from what your readers do. Write to [agent@riskmandate.ai](mailto:agent@riskmandate.ai), which an agent reads first and a person answers, or open [an issue]({{github}}/issues) in the repository, which is where the work happens in any case. Share this page with your team: the [developer quick start](../for-developers/) is what they will ask for next, and [the evaluator's page](../for-evaluators/) is what your security team will.
---
# Add the meter to a page
> Put the reading meter on your site: the element at the foot of each page, the balance chip in the nav, the JSON config written once by your build, the pinned import, the account pages, and the article feed the picks read.
# Add the meter to a page
> Ten lines on each page, one config written by your build, and five account pages you copy. Nothing to deploy but files.
## 1. The element, where the charge happens
At the foot of the content, after the last paragraph a reader would reach:
```html
```
`kind` is the price class (`article`, `issue`, `note`, `collection`, `page`, `free`); an article newer than `newDays` is charged as `article_new`. `date` and `topics` feed the personas' picks; `title` is what the history shows. A page that should charge nothing is `kind="free" quiet`, which keeps the balance on screen without a card (a synthetic reader once took a vanished chip for lost credit).
## 2. The chip, in the navigation
```html
```
On every page, so a reader never loses sight of their balance. It marks a negative balance and opens the account.
## 3. The config, once
A JSON block with id `sg-meter-config`, the same on every page; write it in your build so it cannot drift. Prices in pence; every key has a default. The full shape and a live example are on [the developer quick start](../../for-developers/). The parts you will change:
```json
{"start": 500, "newDays": 7, "symbol": "£",
"prices": {"article_new": 10, "article": 5, "issue": 3, "note": 2, "collection": 2, "page": 1, "free": 0},
"topup": {"amount": 500, "link": ""},
"links": {"account": "account/index.html", "topup": "account/top-up.html", "how": "about/the-meter.html"},
"picks": {"feed": "articles/graphs.json", "base": "articles/"},
"share": {"contact": ".well-known/sgit-subscribe.json", "form": "reading-share", "site": "your-site.example", "max": 300}}
```
Leave `topup.link` empty until your Stripe Payment Link exists: the top-up page then says that no money moves. The newsroom's gate refuses a test-mode link on a live page.
## 4. The import
```html
```
Pinned to a release and a component version; neither is ever edited in place. Generate the URL from one place in your build ([taking a release](../take-a-release/)).
## 5. The account pages
Five pages your site serves, each a shell around one view: `account/index.html` (`view="account"`), `account/top-up.html` (`topup`), `account/topped-up.html` (`topped-up`; set it as the Stripe link's return URL with `?session_id={CHECKOUT_SESSION_ID}`), `account/newsroom.html` (`newsroom`), `account/personas.html` (`personas`), and `account/share.html` (`share`). Copy the newsroom's: [account/](https://newsroom.sgit.ai/account/). The paths go in `links` and `links2` of the config.
## 6. The feed
`articles/graphs.json` with one entry per article (`slug`, `title`, `date`, `url`, `teaser`, `topics`, `tags`) is what the picks and the persona graph read. The newsroom's build writes it from the articles; yours can write it from whatever you have. Without it the meter still charges; the newsroom view says the list could not be loaded.
## 7. Check it
Open a page: the chip shows £5.00; scroll to the foot and watch the balance fall; rate the page at the card and see the price move; reload in the same tab and see nothing charged twice. Then run `tests/test_meter.mjs` against your site ([running the tests](../run-the-tests/)).
## What to read before going live
[Going live with the reading meter](https://newsroom.sgit.ai/articles/going-live-with-the-reading-meter.html) for what was switched on and in what order, [Who will game the reading meter](https://newsroom.sgit.ai/articles/who-will-game-the-reading-meter.html) for the threat model, and [the security model](https://newsroom.sgit.ai/meter/security.html) for the gaps the design accepts.
---
# How-to guides
> One task each: add the meter to a page; make a reading-list link; open the newsroom as a persona; set up the subscribe lane; take a release and roll one back; run the tests; make a release.
# How-to guides
> One task each, in the order a site adopts the API. The newsroom's pages are the worked examples throughout; its repository is public.
- [**Add the meter to a page**](add-the-meter/): the element, the config, the import, the balance chip, the account pages, and the two data files your site publishes.
- [**Make a reading-list link**](reading-list-link/): by hand from the grammar, or with the make page; what the recipient sees.
- [**Open the newsroom as a persona**](persona-link/): persona pages and links, `persona/links.json`, what a visitor persona is.
- [**Set up the subscribe lane**](subscribe-lane/): a key pair, a vault on an SG/Send host, the contact file at `/.well-known/`, and reading the lane with the key alone.
- [**Take a release, roll one back**](take-a-release/): for a site that consumes the API; the newsroom's procedure as the example.
- [**Run the tests**](run-the-tests/): in Chromium, WebKit and Firefox, against the newsroom's pages or your own.
- [**Make a release**](make-a-release/): for maintainers: version, cut, check, pull request, deploy, the live check.
---
# Make a release
> For maintainers: the rules while the files are frozen, the version file, cutting a release directory and its manifest, the checks, the pull request with a person and an agent reviewer, the deploy, the tag and the live check, then the consumers.
# Make a release
> A release is a directory that is never changed after it is on `dev`, the default branch. The tree is always exactly the release the version file names, or the check fails.
## The rules, while the files are frozen
Until phase 4 of [the brief](https://newsroom.sgit.ai/articles/explorer-to-villager-newsroom.html) is finished (the refactoring with every test passing, and the adversarial exercise), the served files change only as a new release, each change with a test that fails without it and a reason in the pull request. A fix to a finding is a deliberate change to the test that pinned it. A new reader feature is built here, as a new versioned file, never in the newsroom's tree.
## 1. Change, and test
Edit under the served paths (`assets/`, `for/`, `wire/`, `passkey/`); if the file is one of the meter's, it is a new component version at a new path, never an edit of `v1.4.0`. Run the tests in the three engines against the newsroom checkout ([running the tests](../run-the-tests/)).
## 2. Version and cut
`tools/version.json` holds two numbers and a date, all from the file, never the clock:
- **`version`**, the repository's version. Every change that lands on `dev` is a new one, in small increments: a patch for a small change (v0.2.1, v0.2.2, ...), a minor for a new capability. The deploy tags it, and refuses a version that is already a tag or not above the latest. CI on a branch fails when the version is not above `dev`'s.
- **`release`**, the latest cut of the reader code: `releases//`, what the consuming sites pin. It changes only when a served file changes; then it is set to the new version and cut.
```sh
$EDITOR tools/version.json # {"version": "v0.2.3", "release": "v0.2.3", "date": "2026-10-20"} for a change to a served file;
# {"version": "v0.2.3", "release": "v0.2.0", ...} for a change to the site, the tests or the tools
python3 tools/release.py cut # (served files changed) releases/v0.2.3/ and manifest.json
python3 tools/release.py check # every release matches its manifest; the tree is the release; the two numbers obey their rule
python3 site/build.py # the site: the version in the bar, the release on /releases/
python3 site/check.py # the site's gate: references, canonical, chrome, sitemap, llms, no key shapes
```
Add the version's line to `site/data/versions.json` (newest first); the build refuses a version without one, and `/versions/` shows them all. A new page needs its link-preview card: `python3 site/og.py` draws one into `site/og/` for every page that has none (commit it; the gate fails a page without one).
`cut` refuses to write over an existing release directory, refuses a release that is not the repository's version, and refuses a file that differs from the newsroom's bytes unless `CHANGED_FROM_NEWSROOM` records why. Write a release's note at `site/releases/v0.2.3.md`.
## 3. The pull request
CI runs the syntax check, the manifest check, the build twice, the site check and the tests in three engines. A pull request needs one person and one agent reviewer; the agent's review is a required check. The description says what changed, which test fails without it, and why now.
## 4. Merge, deploy, tag, check
A push to `dev` (the default branch, and the release branch) runs `deploy-pages.yml`: the checks again, a tag of the version, the Pages deploy of `www/` and `releases/`, and then `tools/live_check.py`, which asks the live host for every file of the release and compares the hashes, retrying while Pages propagates. The release is live when that job is green, not before.
## 5. The consumers
Tell the newsroom (and sgit.ai, once it pins): [taking a release](../take-a-release/). The newsroom's release note names the API release it moved to.
## What never happens
A file under `releases/` is never edited. A tag is never moved. A release is never cut from a tree that differs from it. A served file is never loaded unversioned.
---
# Open the newsroom as a persona
> Persona links, persona pages and links.json: how a link opens the site as a founder, a journalist, a security lead, a builder or a board member, what a visitor persona is, and how a site defines its presets.
# Open the newsroom as a persona
> A link that says who you are reading as. The reader can keep the persona or send it away; nothing is charged for arriving.
## The links
```
https://newsroom.sgit.ai/persona/founder/
https://newsroom.sgit.ai/persona/#journalist
```
Five starting personas: `founder` (Morgan), `journalist` (Rowan), `security` (Sam), `builder` (Kai), `board` (Jordan). [`persona/links.json`](https://newsroom.sgit.ai/persona/links.json) lists them with their pages, for whoever writes the email. The picker at [`/persona/`](https://newsroom.sgit.ai/persona/) shows all five; an unknown id is said so, and nothing is added.
## What happens
The page's meter is in its `landing` view. It adds the persona as a *visitor*: active, with its starting reading list and picks, but marked as arrived by a link. The page says who you are reading as and offers *Keep* (it becomes one of the reader's own) and *Remove* (it goes, and the others stay). Opening the link twice adds nothing twice. The article [A link to a persona](https://newsroom.sgit.ai/articles/a-link-to-a-persona.html) is the design, with the vault as the step after it.
## Defining the presets
Each preset is an entry in the meter config's `personas.presets`: `id`, `label`, `name`, `theme` (a hex colour), `blurb`, `topics` (weights by topic) and `articles` (the starting list, by slug). A persona added to the config gets its page and its row in `links.json` on the next build. The newsroom's `admin/build/personas.py` writes `persona/index.html`, one page per preset, and `links.json`; its pages carry the meter block with `view="landing"` and the id in the address.
## What a persona does afterwards
It curates: *Keep* and *Not for this persona* on any reader card move an article onto or off its lists, and the picks on the newsroom page follow what it kept. A page read is credited to the active persona. A reading list opened from a link is a persona of this kind too, named for the list.
---
# Make a reading-list link
> Send someone a set of articles as one link, by hand from the grammar or with the make page; what they see when they open it; how a site adds the two pages.
# Make a reading-list link
> A list is a link. Make it with the page, or type it.
## With the make page
Open [newsroom.sgit.ai/for/make.html](https://newsroom.sgit.ai/for/make.html). Search the articles, add up to fifty, move them up and down, mark some as *worth a look* rather than *to read*, name yourself and the reader, write a note, and copy the link. The preview shows the list exactly as the recipient will see it, because the page reads its own link back through the recipient's parser. Every article page also has a *start a reading list with it* link at its foot, which opens the make page with that article in it.
## By hand
```
https://newsroom.sgit.ai/for/#from=Dinis&to=Steve&r=78,42,15&s=66&n=Read%2078%20first
```
`r` is the articles to read, in order, by number; `s` is worth a look; `from` and `to` are names; `n` is a note. The numbers are in [`articles/ids.json`](https://newsroom.sgit.ai/articles/ids.json), and each article prints its own (*Article No. 78*). The rules, field by field: [the reading-list link](../../docs/reading-list-link/).
## What the recipient sees
The list, with each article's title, date and teaser, in the order you gave; your note; a Reply button if you gave an address. Opening it adds a visitor persona named for the list, which they can keep or remove; nothing is charged for opening it. Anything malformed in the link is dropped and listed under *Left out*, so a mangled link still opens.
## For a site
Two pages that import from this host, and a numbers file:
1. Publish `articles/ids.json` with a stable `n` per article (never changed or reused).
2. `for/index.html`: `import { openList } from 'https://api.newsroom.sgit.ai/{{release}}/for/reading-list.js'` and call `openList({ core, ids: '../articles/ids.json', root: '../', mount })` with `core` the meter core's URL on this host. Load `for/for.css` from this host too.
3. `for/make.html`: the same with `makeList` from `for/make.js` and `site: 'https://your-site/for/'`, the address the links will carry.
The newsroom's `admin/build/lists.py` writes both pages; copy it or the pages it writes.
---
# Run the tests
> The eleven test files, what each covers, how the harness serves a newsroom checkout and routes this host to the working tree, the three engines, which tests need Chromium, and how to run them against your own pages.
# Run the tests
> No mocks of the code: real browsers, the real pages, a real vault library against a mock of the vault API, and the sgit command line. The harness swaps one thing only: requests to this host are answered from the working tree.
## Set up
```sh
git clone {{github}} && cd SGit-AI__Website__Newsroom__API
npm install --no-save --no-audit --no-fund playwright@1.56.1
npx playwright install --with-deps chromium # and webkit, firefox, as you need
python3 -m pip install 'sgit-ai==0.21.0' # the vault test's CLI round trip, the notes test's lane
git clone https://github.com/SGit-AI/SGit-AI__Website__Newsroom .newsroom # the pages the tests run against
```
`tests/newsroom.json` names the commit CI checks the newsroom out at. `NEWSROOM=` points the tests at any checkout, including your own site once it imports the code.
## Run
```sh
node tests/test_meter.mjs # Chromium, the default
BROWSER=firefox node tests/test_lists.mjs
BROWSER=webkit node tests/test_search.mjs
NEWSROOM=/path/to/your/site node tests/test_meter.mjs
```
A test prints `ok` or `FAIL` per check and exits 1 on any failure. It prints `SKIP` and exits 0 when it cannot run (no browser, no newsroom checkout, no `sgit`); CI treats a `SKIP` as a failure, so a missing install cannot pass quietly.
## What each file covers
| Test | Covers | Engines |
|---|---|---|
| `test_styles` | the served stylesheets write no colour outside a theme token (the newsroom's rule, held here since the files moved) | Node |
| `test_site` | this website: the five themes' tokens, no literal colour in its stylesheet, the picker and its memory, 4.5:1 in every theme, the bar at ten widths | all three |
| `test_meter` | the meter in a browser: depth, rating, decline, reload, below zero, the return page, personas, picks, export and restore, the share form, the persona links, phone width | all three |
| `test_lists` | the reading-list parser in Node and the two pages in a browser: malformed input, limits, the make page, copy, phone width | all three |
| `test_review` | the review's findings, pinned as the behaviour is today (R1 to R11, R14, R15, R17) | all three; R2 is held in Chromium |
| `test_wire` | the wire pages reading the real vault: edition, lenses, sources, documents, what left the browser | all three (needs the vault host) |
| `test_graphs` | the wire's graphs: the lenses' edges and the picture drawn from the real vault | all three (needs the vault host) |
| `test_sync` | the activity vault's logic as pure functions, two devices' states | all three (Node) |
| `test_search` | the index and the ranking in Node, the page in a browser | all three |
| `test_vault` | the vendored library against the mock, the newsroom's layer, the sgit CLI round trip | Node + CLI |
| `test_passkey` | the passkey lab with a virtual authenticator that has PRF | Chromium (CDP) |
| `test_pane` | the pane: the account carried between three devices through the mock | Chromium (CDP) |
| `test_notes` | the feedback loop end to end: notes, the lane, the CLI reading it, a second device | Chromium (CDP) |
| `test_inbox` | the pane's inbox and send: a link held, a QR code decoded, a second device | Chromium (CDP) |
| `test_ask` | ask: passages and messages in Node; the reader's key and the faked built-in model in a browser | Chromium |
Chromium's clipboard permissions have no equivalent in Firefox and WebKit, so the three clipboard reads run in Chromium only; the rest of each test runs everywhere.
## In CI
`.github/workflows/ci.yml` runs the syntax check, the manifest check and the reproducible build, then the tests in a matrix of the three engines against the pinned newsroom commit, on every push and pull request; about four minutes. The brief's target is two; caching the engines is the first thing to measure.
---
# Set up the subscribe lane
> How shares, subscriptions and notes reach a newsroom without a server: a key pair, a write-only lane in an encrypted vault on an SG/Send host, the contact file at /.well-known/, and reading the lane with the key alone.
# Set up the subscribe lane
> A reader presses Send; their browser encrypts a small message to your public key and appends it to a lane you own. The site never sees it. You read the lane with your key, by hand or with an agent.
## What you need
1. **A key pair.** An RSA key whose public half is published; the private half stays with whoever (or whichever agent) reads the lane. The envelope is RSA-OAEP-SHA256 wrapping an AES-256-GCM key (the sgit hybrid envelope v2).
2. **A vault with an append lane** on an SG/Send host, with an append token that lets a browser write and nothing else. The newsroom's lane is on `dev.send.sgraph.ai` (named *dev*; finding R5 asks the operator to say whether that is production).
3. **The contact file** at `/.well-known/sgit-subscribe.json`.
## The contact file
```json
{"format": "sgit-subscribe/v1",
"site": "your-site.example",
"recipient": {"address": "newsroom@your-site.example", "fingerprint": "", "encrypt": ""},
"inbox": {"vault": "", "endpoint": "https://", "encrypt_to": "recipient",
"status": "live", "lane": {"name": "subscribe", "append_token": ""}}}
```
The browser recomputes the fingerprint from the PEM before encrypting, so a swapped key is refused. The newsroom's own file is [here](https://newsroom.sgit.ai/.well-known/sgit-subscribe.json); it is still labelled with the site the code came from ([finding R4](https://github.com/SGit-AI/SGit-AI__Website__Newsroom__API/issues/4)), which the move makes possible to fix.
## What arrives
A subscription is an `.eml` with the address and a consent line; a share is the reader's summary and their data as JSON (balance, spent, reads, personas, topics, log; never a payment reference), with headers `X-SGit-Form`, `X-SGit-Reply-To`, `X-SGit-Name`, `X-SGit-Subscribe`, `X-SGit-Page`. A note from the vault pane arrives in the reader's own lane, whose read key they hand over, with the page, their words and the phrase they selected.
## Reading the lane
With `sgit` and the read key, from the command line or an agent:
```sh
sgit clone # the notes as files, newest first
NEWSROOM_LANE_KEY= python3 admin/build/lane.py # the newsroom's digest, by page; the key from the environment, never a file
```
A read key cannot write: a push from the clone is refused, and the test proves it. The newsroom refuses to hold a vault key for a reader's lane at all.
## The pieces on this host
`assets/subscribe.js` (the form, the fingerprint check, the envelope, the append), the meter's share view in `sg-meter.js`, and the vault layer for notes. The article [The site is an inbox](https://newsroom.sgit.ai/articles/the-site-is-an-inbox.html) is the design of the feedback loop; [Two agents, one vault](https://newsroom.sgit.ai/articles/two-agents-one-vault.html) is how the newsroom's agents share what arrives.
---
# Take a release, roll one back
> For a site that consumes the API: pin one release in one file, generate every URL from it, keep a copy of the manifest, check every page against it, and roll back by changing the line. The newsroom's procedure as the worked example.
# Take a release, roll one back
> One file names the release. Everything else is generated from it, and a gate refuses anything that is not.
## 1. Pin
Write the release in one place your build reads, and copy that release's manifest beside it. The newsroom's:
```json
{"host": "https://api.newsroom.sgit.ai", "release": "{{release}}"}
```
```sh
curl -s https://api.newsroom.sgit.ai/{{release}}/manifest.json > admin/build/api-manifest.json
```
## 2. Generate every URL from the pin
Nothing in a page names a reader file by a relative path or by a release typed by hand. The newsroom's builders call `api_url(path)`; its sync points imported pages at the same URL; `python3 admin/build/api.py --repoint` rewrites every page in one pass when the pin changes, and `--rerecord` recomputes the content hashes its import gate keeps.
## 3. Gate it
Before a release of your site, check three things on every page: no reader file is loaded by a relative path; every reader file is loaded from the pinned release; every path loaded is in the manifest. The newsroom's `validate.js` check 14 does this; check 13 holds the frozen meter copies it still serves to the manifest's bytes. The checks are small because the rule is simple.
## 4. Verify the live host
After your deploy, ask this host for the hash of every file you load and compare with the manifest; `tools/live_check.py` in the repository does it for a release, with retries while Pages propagates. [Green does not mean live](https://newsroom.sgit.ai/articles/green-does-not-mean-live.html).
## 5. Roll back
Change the release in the pin to the previous one, copy its manifest, repoint, gate, deploy. A release directory is never edited in place, so the previous release is exactly what it was.
## Taking a new release
Read [its notes](../../releases/) first: a release says what changed, and lists any file that differs from the newsroom's bytes and why. Then steps 1 to 4. The frozen meter versions are in every release at their paths, so a page pinned to an older component version keeps working.
---
# A newsroom's reader, as an API
> The reading meter, pay-after-you-read, personas, reading lists in a link, the reader's vault: the code a newsroom's reader runs, open source, served from one host, live on newsroom.sgit.ai and taking money.
# A newsroom's reader, as an API.
A reading meter that charges by how far you read. Paying after you read, with a rating that sets the price. Personas, a newsroom of your own, reading lists in a link, encrypted sharing, a vault that carries your account between devices. All of it runs in the reader's browser, with no server of ours, and all of it is open source. It is live on [newsroom.sgit.ai]({{newsroom}}/), and it is taking money.
[For publishers and editors](for-publishers/)[Quick start for developers](for-developers/)[For evaluators](for-evaluators/)
{{release}} · {{files}} files · {{reader_lines}} lines of reader code · {{tests}} checks in three browser engines · Apache 2.0
[Publishers, editors, executives
### What your readers get, what you keep, what it costs
A meter instead of a paywall, a reader's own newsroom instead of a feed, and a model where their data stays in their browser. The case, with the articles that made it and the numbers from a live site.
](for-publishers/)
[Developers
### Add the meter to a page in ten lines
Pinned, versioned files on one host; a JSON config; a custom element. The contracts your pages and agents program against, the data your site publishes, and the tests you can run against your own pages.
](for-developers/)
[Security, architecture, procurement
### Read the map, the review and the proof
Everything a reader can do, every place data leaves the browser, eighteen findings as public issues, a release manifest of hashes, and a move proved byte for byte.
](for-evaluators/)
## What is live today
{{live: [newsroom.sgit.ai]({{newsroom}}/) runs this release: {{articles}} articles behind a reading meter, with real payments through a Stripe link, personas, reading lists, the vault and the pane, search, and the Industry Wire read from a vault. It is the reference deployment and the first consumer of this API; [what runs and what is only designed]({{newsroom}}/shipped/) is kept honest on its own page.}}
[The meter
### Pay by depth, after you read
A page is charged by the deepest point reached; a rating at the foot sets the price from nothing to double. £5 of credit to start, in the reader's own browser.
]({{newsroom}}/articles/going-live-with-the-reading-meter.html)
[Personas
### A newsroom of your own
Read as a founder, a journalist, a security lead, a builder or a board member; keep and reject articles; picks follow. A link can open the newsroom as a persona.
]({{newsroom}}/persona/)
[Reading lists
### From me to you, in a link
Every article has a number; a list is a link with the numbers in it. No account, no server, nothing stored but the link.
]({{newsroom}}/for/make.html)
[The vault
### Your account, between devices
A passkey opens a door; the door holds the key to a master vault; the account travels encrypted, and the newsroom never holds a key.
]({{newsroom}}/vault/)
## How it works, in one picture
The pages on the newsroom's origin load the reader code from api.newsroom.sgit.ai; the reader's state stays on the page's origin
newsroom.sgit.ai (your site)
the pages, the articles, the data
localStorage · sgit.meter.v1 · the reader's state
articles/graphs.json · ids.json · .well-known/…
api.newsroom.sgit.ai (this host)
/{{release}}/ · pinned · never edited in place
assets/components/sg-meter/…/sg-meter.js
for/ · assets/vault-pane/ · assets/search/ · manifest.json
import()
Stripe (payment) · an encrypted vault (lane, wire)
only when the reader acts
The code is served from this host at a versioned path and never changed after it is published. The reader's state belongs to the page's origin, so it stays with the newsroom whichever host serves the script, and the newsroom never sees it. Nothing leaves the browser unless the reader pays, shares or subscribes, and each of those is a published contract. [The hosting model, in full](docs/hosting/).
## Open source, and why that is the point
The code is Apache 2.0, in [one repository]({{github}}), with its tests, its release manifests, its review findings as issues, and the brief it was built to. A publisher can pin a release today and run it on their own pages; a developer can read every line; an evaluator can check every hash against the live host. Nothing here needs permission.
The newsroom that runs it is the reference, not the product. The aim is other newsrooms adopting the same reader, so that a reading list or a persona made on one site means something on another, and so that the formats, the article number, the list link, the share, the envelope, outlive any one publisher. [How to adopt it](adopt/), and [who to talk to](about/).
## Where to go next
[
### Reference
The {{contracts}} contracts, the state, the inputs and outputs, the components, the hosting model.
](docs/)
[
### How-to guides
Add the meter. Make a reading-list link. Open the newsroom as a persona. Set up the subscribe lane. Take a release, roll one back. Run the tests.
](how-to/)
[
### The review
The map of everything a reader runs, {{findings}} findings as issues, the accepted gaps, and the map drawn as a layered graph.
](review/)
[
### The articles
Designed in public first: the case, the reader's own newsroom, and the engineering, each an article on newsroom.sgit.ai.
](articles/)
[
### Releases
Every release, its notes and its manifest. The current one is {{release}}.
](releases/)
[
### For agents
The brief for the agents that keep this repository and the newsroom: the rules, where things are, how to change them.
](agents/)
---
# Search
> Every page of this site, searched in your browser with the same component newsroom.sgit.ai uses, loaded from this host's own release; and asked, with Chrome's built-in model or your own OpenRouter key, answered from the passages found.
|
---
# The contracts
> The fourteen contracts other sites, pages and agents program against: the meter config, a charged page, persona links, the reading-list link, article numbers, the return page, the export, the share, the envelope, the contact file, the wire's vault, events, a component, the import's rules.
## The contracts
| Contract | Shape | Owner |
|---|---|---|
| The meter config | storageKey, start, newDays, symbol, prices{article_new,article,issue,note,collection,page,free}, depth, minDepth, topup{amount,link}, packs, links{account,topup,how}, rating{price[5],more[5]}, autoKeep, picks{feed,base}, links2{newsroom,personas,share}, share{contact,form,site,max}, personas{presets[],names{}}, root | `sg-meter-core.js DEFAULTS; written into every page by sgit.ai's build and re-pointed by the import` |
| A charged page | : the price class comes from the page | `sgit.ai's build for imported pages; page.py (kind=free, quiet) for this site's` |
| A persona link | /persona// and /persona/#, id ∈ {founder, journalist, security, builder, board} | `personas.py, persona/links.json` |
| A reading list link | /for/#from=&to=&r=&s=&f=&n=&e=: names ≤ 40 chars of letters, spaces, hyphens, apostrophes; r,s article numbers, 1–50 in all; f persona ids or c:, ≤ 10; n ≤ 240 chars; e an address | `for/reading-list.js LIMITS, parse(), build()` |
| Article numbers | articles/ids.json: n and slug are the contract, assigned once in publication order, never changed or reused; every other field is display text | `admin/build/ids.py; validate.js check 12` |
| The return page | ?session_id=: credited once per distinct value, never verified | `_toppedUp(), credit()` |
| The export | the state as JSON; restore accepts any file with balance:number and log:array | `restore()` |
| The share | a text summary then the same data as JSON {v:1, site, at, balance, spent, declined, reads, topups, active, personas, topics, cols, log}, as an .eml with X-SGit-Form, X-SGit-Reply-To, X-SGit-Name, X-SGit-Subscribe, X-SGit-Page | `shareData(), _send()` |
| The envelope | sgit hybrid envelope v2: {v:2, w, i, c} (RSA-OAEP-SHA256 wrapped AES-256-GCM key, iv, ciphertext), base64, then base64 again as the lane payload | `envelope(), subscribe.js encrypt()` |
| The contact file | sgit-subscribe/v1: recipient{address, fingerprint, encrypt}, inbox{vault, endpoint, encrypt_to, status, lane{name, append_token}}; the fingerprint is recomputed from the PEM before use | `.well-known/sgit-subscribe.json` |
| The wires' vaults | u9i9jp0l (the Industry Wire), eitf274v (OWASP), v202bla5 (AWS), p6twygul (Portugal) on dev.send.sgraph.ai, read keys published; data/bundle.json {beats, stories, sources, personas, brief; wire, map, stack when present}, data/deliveries.json, data/releases.json (wire-releases/v1, one entry per publish), docs/*.md; story fields, changes and status shown as written; everything in a vault is public, for ever | `admin/build/wire-vault*.json; wire/how.html; each wire's agents own its content` |
| Events | sg:meter.changed · charged · rated · declined · toppedup · reset · restored · persona · shared; sg-meter:error | `sg-meter-core.js _emit(), sg-component.js failed()` |
| A component | static jsUrl, get resourceName(), onReady(); a .tpl and a .css beside the .js; published at an immutable versioned path | `sg-component.js` |
| The import's rules | the meter path rewrite (v1.3.1 → v1.4.1), the top-up link, ten text rules, the article number line, the moved banner cut, the foot of every issue; each stops the import when its anchor moves | `sync_newsroom.py items 9–15` |
---
# State, inputs and outputs
> Where a reader's data lives (localStorage on the page's origin), everything that comes in, and every channel by which anything leaves the browser.
## State
- `v`: 3
- `created`: ISO time
- `balance · spent · declined`: pence, to 0.1p
- `reads`: pages opened
- `paused`: always false since v1.4.0
- `log[]`: ≤ 500 entries: id, at, path, title, kind, topics, price, depth, cost, persona, autoKept?, rating{useful,more,detail,at}?, declined{reason,at,refunded}?
- `topups[]`: ref (≤ 40 chars), at, credit, source: link | simulated
- `sessions[]`: every payment reference credited, unbounded
- `cart[]`: the simulated cart (unused with a live link)
- `active`: the active persona's id
- `personas[]`: id, preset, name, created, on[], off[], visitor?, list? (a reading list's key), theme?
- `shares[]`: ≤ 20: at, how, pages
## Inputs
- **Element attributes**: view · kind · date · topics · title · quiet · count · persona · compact (the page's own markup (trusted: it is this site's build); `sg-meter.js`)
- **The page's config**: