Home / Replicate / The reader API: a map, and a review

Phase 0 of the move: map

The reader API: a map, and a review

Everything a reader does on this site, before it moves: the files and their hashes, what a reader can click, type or pay, where their data goes, the contracts other sites and agents program against, and what a line-by-line review found.

“API” here means what the brief means by it: client-side. Components, formats and contracts the browser runs, with no server of ours and nothing to deploy but files. It is what other sites and agents program against: the meter’s configuration, the reading-list link, the article numbers, the persona links, the share’s envelope. This page maps all of it as it is at v0.12.72, before anything moves, and reviews it. It is regenerated on every build from the tree, and from the tables kept beside the code in admin/build/api_map.py; the same map is map.json, and the review is also a note.

What the hashes are for. The brief’s rule for the move is that the live site serves the same bytes after it as before, proved by hash and not by reading the diff. The hashes below are that “before”: scripts, styles and JSON raw; pages with the site chrome and version stamps normalised, as the gate already does for imported pages. They change when the files change, and they are meant to.

In numbers

Reader code that runs in a browser10,263 lines in 47 files; 5,697 more lines in 20 frozen files of earlier meter versions, served and never changed
Pages that carry it30 pages written for it; the balance chip and a charged-page element on every page of the site
Views, controls, inputs12 views of one element, 56 kinds of control, 11 ways data comes in, 9 ways it goes out
Contracts14, listed below; the two that other sites and agents already program against are the reading-list link and articles/ids.json
Tests511 check calls in three files (loops make more checks at run time), now run in CI before a tag (R11)
Pages with a Content-Security-Policy0 (R10)
Upstreamstwo: sgit.ai for the account pages, subscribe, the contact file and the meter to 1.3.1, rewritten on import to this site's 1.4.0 (R12)

The files

The code a reader runs, where it comes from, and what each file reaches for. 5 of the 47 code files come from sgit.ai byte for byte and cannot be changed here by a rule of the import; that is R4 and R12 below.

FileWhat it isFromLinesReachesSHA-256 (normalised for pages)
assets/components/sg-meter/v1/v1.4/v1.4.1/sg-meter.jsSG Meter, the element: twelve views, the reader card, the share form, the graphthis site, from sgit.ai's v1.3.18172 fetch, sg:meter.changed, confirm(), WebCryptoba46eeca15d5d9b2
assets/components/sg-meter/v1/v1.4/v1.4.1/sg-meter-core.jsSG Meter, the meter: prices, the charge, ratings, declining, credit, personas, picks, the share textthis site, from sgit.ai's v1.3.15131 fetch, localStorage, sessionStorage, sg:meter.changed, sg:meter.charged, sg:meter.declined, sg:meter.persona, sg:meter.reset, sg:meter.restored, sg:meter.shared, sg:meter.toppedup9986294b0fbc5a62
assets/components/sg-meter/v1/v1.4/v1.4.1/sg-meter.cssSG Meter styles, adopted into the shadow rootthis site, from sgit.ai's v1.3.1192—3f464b7d1df4e15f
assets/components/sg-meter/v1/v1.4/v1.4.1/sg-meter.tplSG Meter markup (one div)this site, from sgit.ai's v1.3.11—104c7c1ded976408
assets/components/base/v1/v1.0/v1.0.0/sg-component.jsThe component base: fetches the .tpl and .css beside a component and calls onReady()sgit.ai, verbatim1181 fetch, sg-meter:error, innerHTMLe566a78ad98fbd18
for/reading-list.jsA reading list in a link: parse, build, show, open as a personathis site3431 fetch5f8fa1378b99c24c
for/make.jsThe page that makes a reading list and previews itthis site2541 fetchec9abc1f40b5f564
for/for.cssStyles for the two reading-list pagesthis site109—6ad9b2f9c39e9974
assets/subscribe.jsThe subscribe form: encrypts the address and posts it to the lanesgit.ai, verbatim1172 fetch, WebCryptofe2eec58c28f15e5
wire/vault-reader.jsReads an sgit vault in the browser with a published read key: ref, commit, tree, blobs, decrypted with Web Crypto; the markdown rendererthis site, vendored from sgit.ai's vault-docs.js1821 fetch, WebCrypto332509b6be44538d
wire/graphs.jsThe wires' graphs: a provenance strip per story, stories per day by beat, the threads on a date axis, the entities stories share, a sources tally, a story-by-persona matrix; SVG from the bundle, text nodes only, colours as theme tokensthis site356—22f48fcb1185389f
admin/build/infographics.pyOne card per story and one hero per edition through dev.tools.sgraph.ai, with a cost ledger per edition and per wire; the key from the environment only, a dry run without itthis site704hosts: dev.tools.sgraph.ai, openrouter.aic5dee92d109b48eb
wire/wire.jsThe wires: the edition with its lens, release notes and corrections, a map or a stack when the vault has one, the sources, the documents, drawn from the vault as text; one code for every wirethis site6042 fetch, localStorage, sessionStorage, innerHTMLc7d5e1ecbed37d7b
wire/wire.cssStyles for the wire pagesthis site279—f134589e1aac457e
wire/subscribe.jsSubscribe to one wire alone: the newsletter's envelope and lane (assets/subscribe.js, unedited), with X-SGit-List naming the wirethis site1032 fetch, WebCryptoe08913d389ede55f
admin/build/wire-reach.jsonThe organisation routes this site found for each wire's reach page, under the published contact rules: page, date, hash, role addressesthis site109—380f4d81d3f129e2
admin/build/wire-vault.jsonThe Industry Wire's vault: endpoint, vault id, the public read key (the only kind of key on this site)this site14—c8c84bea5a65df09
admin/build/wire-vault.owasp.jsonThe OWASP Wire's vault (wire/owasp/): the same contract, its own public read keythis site17—d584c029352338f2
admin/build/wire-vault.aws.jsonThe AWS Wire's vault (wire/aws/): the same contract, its own public read keythis site17—3c0448d6e39bfeea
admin/build/wire-vault.portugal.jsonThe Portugal Wire's vault (wire/portugal/): the same contract, its own public read key; the bundle is bilingualthis site17—752b9fbb38c9015d
wire/snapshot.jsonThe Industry Wire as it was in the vault at the last release: headlines, primary links, corrections, the latest release note; the fallback and the agents' indexthis site, generated1,337—62085ccd612b1abf
wire/owasp/snapshot.jsonThe OWASP Wire at the last release, the same shapethis site, generated292—42aa828998de833a
wire/aws/snapshot.jsonThe AWS Wire at the last release, the same shape, with the stack's deadlinesthis site, generated944—073f0270783a41d2
wire/portugal/snapshot.jsonThe Portugal Wire at the last release, the same shape, with each headline in Portuguese toothis site, generated515—e188f02148e47a55
assets/vault-ui-embed.jsThe study page: embeds the vault browser from dev.vault.sgraph.ai and hands it a read keysgit.ai, verbatim126hosts: dev.vault.sgraph.ai, innerHTML308af99563423f5e
assets/shots.jsFigures: sets each image's address at runtimesgit.ai, verbatim2781 fetch6c03d07971ec6808
assets/site.jsPage behaviour: nav, copy buttons, reveal-on-scrollsgit.ai, verbatim81—0069c69190c8afc1
assets/nav.jsNav behaviour on the pages this site writesthis site42—bf18958cd99e10b5
.well-known/sgit-subscribe.jsonThe lane and the public key the share and the subscription are encrypted to and written intosgit.ai, verbatim25hosts: dev.send.sgraph.ai1414b8bb22c50b86
articles/ids.jsonA number for every article: the contract reading lists are built onthis site, generated2,114—b6ec65cb73926839
persona/links.jsonThe persona links, for whoever writes to someonethis site, generated268—1db0c1f2e342a8c1
articles/graphs.jsonThe feed the picks and the graph read: every article with topics, tags and citationssgit.ai, imported35,933—3a8b7ff07506f886
assets/newsroom-vault.jsThe newsroom's vault module: create, open, read, write and publish through the vendored library; the door, master and reader vaults; unlock; add a doorthis site258localStorage, WebCryptoaaa49a06cb76f82c
assets/door.jsFrom a passkey's PRF bytes (or a recovery code) to a door vault's key: the article's derivation, shared by the lab and the panethis site139WebCrypto740779876d19aceb
assets/vault-pane/vault-pane.jsThe vault pane on every page with the meter, in four tabs: this page (the three steps, what the browser knows about the page, the notes, write to the newsroom, send to the newsroom, the lane's read key), reading (every page read here, with its decision), the vault (enable with the passkey, create or join, update the vault, the table across devices, bring their activity here), the keys (every vault of the account); above the tabs, what is only in this browserthis site904hosts: dev.send.sgraph.ai, dev.vault.sgraph.ai, localStorage, sessionStorage, innerHTML40f1981a99cdcc18
assets/vault-pane/notes.jsThe reader's notes as records: one tap at a piece (done, read later, follow up, not for me, more like this, liked, wrong, fix this, idea) with words and the selected phrase, or a message alone, kept in localStorage sgit.notes.v1; keep, add words, remove, mark sent; the decision at a pagethis site139localStorage283e816e1fe72456
assets/search/search.jsSearch in the browser: the index from search/docs.json and search/text.json, BM25 over fields and paragraphs, phrases, numbers, prefixes, snippets; the search pagethis site3182 fetcha1602529e616f0be
assets/search/search.cssStyles for the search pagethis site25—1fcdffcfa53fc8d3
assets/search/ask.jsAsk: the top passages, numbered, given to Chrome's built-in model or to openrouter.ai with the reader's own key, answered with citations; the key in the tab unless keptthis site; openrouter.ai, only with the reader's key184hosts: openrouter.ai, 1 fetch, localStorage, sessionStorageec50bb11017fe7fb
assets/search/ask.cssStyles for Ask on the search pagethis site23—9f8cd3cb95d659e8
assets/vault-pane/inbox.jsThe inbox (049): the reader's RSA-OAEP key pair, the link (sgi1.<vault>.<token>.<key>), sgit's hybrid envelope, the append routes (configure, write, list, fetch, mark-processed), the drain, the QR codedev.send.sgraph.ai /api/vault/append/*227hosts: dev.send.sgraph.ai, 1 fetch, WebCrypto0d31b92a8827d26a
assets/vault-pane/send.jsThe sending side of the inbox, for a browser with the link and no passkey: its own records as one device file, sealed and appendeddev.send.sgraph.ai /api/vault/append/write104localStorage, WebCrypto06b1b151b35f927b
assets/vendor/qrcode-generator/2.0.4/qrcode.mjsQR code generator (Kazuhiko Arase, MIT), vendored unchanged, for the inbox linkthis site2,237hosts: www.d-project.com, www.denso-wave.com, www.opensource.orgea91d7118a539528
assets/search/search-nav.cssThe Search link in the nav, on every pagethis site5—c750cab906e8d159
assets/vault-pane/wire-state.jsWhat this browser knows about the wire: stories seen and when, the visit, the lens chosen; and which of the vault's releases to showthis site78localStorage, sessionStorage36e1d5bbbd300c62
assets/vault-pane/pending.jsWhat this browser holds that the reader vault does not yet: reads, top-ups, notes and stories seen since the last updatethis site47localStorage4d4c30ec10e0ebe1
assets/vault-pane/reading.jsWhat this browser knows about what it has read, from the meter's records: one page summed up (times, when, how far, cost, rating) and the reading historythis site41localStoragee8f2b778f2b2eec0
assets/vault-pane/sync.jsThe activity vault's logic as pure functions: a device's file (reads, top-ups, personas, notes), the consolidated view, totals recomputed, merging into local statethis site237—f00763d6b4f17156
assets/vault-pane/vault-pane.cssStyles for the pane, its tabs, the chips and the notes, and the page shiftthis site119—8a6aa362c7ee9b86
assets/vendor/sg-vault/local/sg-vault--create-with-id.jsSGVault.createWithId: a vault at a derived id, a local patch pending upstreamthis site, pending upstream99—b668c4ab0062a3b3
assets/vendor/sg-vault/v0.2.3/sg-send/sg-send-crypto.jsAES-256-GCM and PBKDF2 over Web CryptoSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)151WebCrypto2c4af16bbaf4f43e
assets/vendor/sg-vault/v0.2.3/sg-send/sg-send.jsThe HTTP client: read, write, batch, presigned; x-sgraph-access-tokenSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)2754 fetch9bc98e580f5fc1ad
assets/vendor/sg-vault/v0.2.3/sg-vault/sg-vault-crypto.jsKey derivation: read key, write key, ref and index ids; key prefixesSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)302WebCryptof8d31359204a9e33
assets/vendor/sg-vault/v0.2.3/sg-vault/sg-vault-object-store.jsContent-addressed objects, the write batch, the block cachesSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)290WebCryptoa45d7ad0f9dbd916
assets/vendor/sg-vault/v0.2.3/sg-vault/sg-vault-ref-manager.jsRefs and the branch index; write-if-matchSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)158—5b8ea87ace83d59e
assets/vendor/sg-vault/v0.2.3/sg-vault/sg-vault-commit.jsCommits and trees, every field encryptedSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)175WebCryptoe686de3756eaeed1
assets/vendor/sg-vault/v0.2.3/sg-vault/sg-vault.jsSGVault: create, open, openReadOnly, the tree, the commitSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)566WebCrypto0108884f9b8d539e
assets/vendor/sg-vault/v0.2.3/sg-vault/sg-vault--file-ops.jsaddFile, updateFile, getFile, removeFile, renameFile, moveFileSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)157—f114db89754638b5
assets/vendor/sg-vault/v0.2.3/sg-vault/sg-vault--folder-ops.jsFolders, lazy sub-treesSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)125—10fea1377422c536
assets/vendor/sg-vault/v0.2.3/sg-vault/sg-vault--sync.jspush, pushIfMatch, pull, merge, ahead and behindSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)456—b9f521e7b70ca66f
assets/vendor/sg-vault/v0.2.3/sg-vault/sg-vault--history.jsHistory: commits, trees and files at a commitSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)89—265b215e27216ad2
assets/vendor/sg-vault/v0.2.3/sg-vault/sg-vault--branches.jsBranches from the indexSGraph-AI__App__Send v0.2.3, vendored and unedited (VENDOR.md)57—bc1eec8bbc2326a7

The page loader: an inline script on every imported page (17 lines, SHA-256 f114004ba2928d82) that loads the stylesheet and assets/site.js, by the vault bridge when the page is inside a vault and otherwise by fetching the text and evaluating it. 506 pages carry a script that evaluates fetched text (R10). The 20 files of earlier meter versions (1.0.0 to 1.3.1, 5,697 lines) are published artefacts on immutable paths; they move frozen, and their only test is that they are still served byte for byte.

Labs, beside the reader code and not part of it

Code that runs in a browser on this site, is not on a reader’s path, and will move with the rest: today, the passkey lab. It makes no change to the reading account and is counted apart.

FileWhat it isLinesSHA-256
passkey/lab.jsThe passkey lab: WebAuthn with PRF, the door derivation, sgit's key derivation, and real vaults through assets/newsroom-vault.js34508c47eae0b95943e
passkey/lab.cssits styles29ca352ad9cdc8631d
passkey/vectors.jsonits test vectors, from sgit's Python19d50a3642c131bbaa
passkey/index.htmlthe page3286620cffd5731fd4b

The pages

PageWhatLinesSHA-256, chrome and stamps normalised
account/index.htmlthe reading account262bc2f591e3a452542
account/newsroom.htmlthe reader's newsroom257d51094542674dd01
account/personas.htmlthe personas25721ad4f65ea6c5ad0
account/share.htmlsend us your reading259a21883ac53716f44
account/top-up.htmlthe top-up2576a94fbab4eace27c
account/topped-up.htmlthe page a payment returns to25674dc5138c9559741
subscribe/index.htmlsubscribe2771251580b865830dd
persona/index.htmlthe persona picker2407c0c30a9ae0de576
persona/founder/index.htmla persona link247043526965d23edb0
persona/journalist/index.htmla persona link2478c9acb523b68c36f
persona/security/index.htmla persona link2472db575acd574852c
persona/builder/index.htmla persona link247fce8ebd416a924a8
persona/board/index.htmla persona link247384885a41bf6abad
for/index.htmla reading list254cbd0376dd53283fe
for/make.htmlmake a reading list2442e61fd52d2b8fc98
wire/index.htmlthe Industry Wire's edition264a89dfc55f0e31f7e
wire/sources.htmlthe wire's sources245ff37914b86bdcd7c
wire/how.htmlhow the wire is read, and its documents25074e7248838ce7d1b
wire/owasp/index.htmlthe OWASP Wire's edition, with the projects graph265fc2c31f75ffeb12e
wire/aws/index.htmlthe AWS Wire's edition, with the stack and its deadlines2653ea762f3c45529c1
wire/portugal/index.htmlthe Portugal Wire's edition, in English and Portuguese, with the ecosystem map265eea9703a0319827a
wire/days.htmlevery wire day by day24621de5ee688ca6010
wire/costs.htmlwhat the wires' pictures cost: every image, edition, wire and month, and the key's budget; wire/costs.json is the data254bc2f78a98c4fdb78
wire/charter.htmlthe charter of the wires2581f4cc9dc555555d8
wire/desk-brief.htmlthe brief for the desks2649ab290dea086eb67
wire/reach.htmlwho should know, by organisation route2455d4214dad6ff3f65
vault/index.htmlyour vault: the reading account carried between devices, with the pane open2697db79ab551958dfa
meter/index.htmlSG Meter, documented39855656dcab6bf2887
meter/security.htmlthe meter's security model2839fc8faaf1fc72627
demos/vaults/newsroom-synthetic-users/index.htmlthe synthetic-readers study, with the embedded vault344e8d83c803a3e0e72

Which pages carry which view: account on 1, landing on 6, newsroom on 1, page on 691, personas on 1, share on 1, topped-up on 1, topup on 1. The balance chip is in the nav of every page; every page is a charged or free page.

Capabilities and web controls

Everything a reader can click, type or pay, by view, with the call it makes. Where a control changes money or a persona, the call is named so a test can target it.

balance · every page (the nav chip)

ControlLabelWhat it does
linkthe balanceopens the account page

page · every charged page, at the foot

ControlLabelWhat it does
linkHow paying for reading works hereopens links.how
linkBalanceopens the account page
button+ Add to your reading list / ✓ In your reading listmeter.curate(slug, 'on' | '')
5 buttons × 3How useful was this? · More like this? · Level of detailmeter.rate(field, 1–5); usefulness re-prices the page
linkYour newsroomopens links2.newsroom
buttonSomething wrong with this page? Don’t charge meshows the four reasons
4 buttonsthe reasonsmeter.decline(reason): refunds the page

account · account/index.html

ControlLabelWhat it does
linkTop up £5.00navigates to the Stripe payment link (topup.link)
buttonExport as JSONdownloads reading-account.json (the whole state)
linksYour newsroom · Your personas · Send us your readingown pages
linksone per history rowthe page read, from the log's own path
file inputRestore from an export…confirm(), then meter.restore(text)

topup · account/top-up.html

ControlLabelWhat it does
linkPay £5.00navigates to the Stripe payment link; with no link, the simulated cart

topped-up · account/topped-up.html

ControlLabelWhat it does
on load?session_id=…meter.credit(amount, 'link', id), once per id, unverified
linkYour account

picks · the front page

ControlLabelWhat it does
linksone per pickopens the article

newsroom · account/newsroom.html, and under a persona banner

ControlLabelWhat it does
buttonsthe persona chipsmeter.setActive(id)
link+ Add or manage personasthe personas page
linkA newsroom designed for youthe share page (not when compact)
2 buttons per pickAdd to reading list · Not for this personameter.curate(slug, 'on' | 'off', active)
button per kept / put-out articleremove · bring backmeter.curate(slug, '')
buttonsAdd this persona (per preset) · Add a blank personameter.addPersona(preset | '')

personas · account/personas.html

ControlLabelWhat it does
input + button per personaname · Renamemeter.renamePersona(id, name)
button per personaMake activemeter.setActive(id)
link per personaOpen its newsroomsetActive, then the newsroom page
button per personaRemoveconfirm(), then meter.removePersona(id)
buttonsthe preset cardsmeter.addPersona

graph · account and newsroom pages

ControlLabelWhat it does
linksone per article dotopens the article

link · pages that carry it

ControlLabelWhat it does
linkYour newsroom

landing · persona/index.html and persona/<id>/

ControlLabelWhat it does
on loadthe persona named by the attribute or the #fragmentmeter.visit(id): added as a visitor, made active, nothing charged
buttonsKeep this persona · Remove itmeter.keepPersona(id) · meter.removePersona(id)
buttonRead as <name> againmeter.visit(id) after a removal
linksYour personas · Other personasown pages
per preset (the picker)Read as this persona · Copy linkopens persona/<id>/ · clipboard

share · account/share.html

ControlLabelWhat it does
buttonCopy to clipboard · Copy it insteadclipboard; meter.noteShared('copied')
inputsname · email (required) · note · consent · newsletter · a hidden honeypotthe form; nothing is sent without consent
submitSend it, encryptedfetch the contact file, verify the key's fingerprint, encrypt an .eml, POST to the lane

reading list (for/) · for/index.html

ControlLabelWhat it does
on loadthe fields after the #parse(); visitList(): a visitor persona named for the two people, its articles as the reading list
linkStart readingthe first article
linkReply to <sender>mailto:, only when the link carries e=
buttonsKeep this list as a persona · Remove it · Add it againmeter.keepPersona · meter.removePersona · visitList
linksevery article, every persona or collection to followown pages

make a reading list · for/make.html

ControlLabelWhat it does
inputsFrom · To · a note · your emailvalidated as the recipient's page validates them
search and two selectstitle, topic or number · topic · personafilters the article list
2 buttons per articleRead · Lookputs the article in a group
per chosen article↑ · ↓ · To look / To read · Remove, and dragorders the list
checkboxespersonas and collections to follow
buttonCopy linkclipboard
linkOpen as the recipientfor/#… in a new tab
input + buttonEdit an existing link · Loadparse() into the form
buttonClear the list

subscribe form · subscribe/index.html

ControlLabelWhat it does
inputsemail · name · consent · a hidden honeypot
submitSubscribefetch the contact file, verify the fingerprint, encrypt an .eml, POST to the lane; on failure, a mailto: link with the same text

the study page · demos/vaults/newsroom-synthetic-users/

ControlLabelWhat it does
on loadtwo iframes from dev.vault.sgraph.aithe published read key is handed over postMessage; the vault is decrypted in the frame

article foot · every article

ControlLabelWhat it does
linkstart a reading list with itfor/make.html#r=<number>

Inputs

Where data comes inWhatWho controls it, and how it is checkedIn the code
Element attributesview · kind · date · topics · title · quiet · count · persona · compactthe page's own markup (trusted: it is this site's build)sg-meter.js
The page's config<script type="application/json" id="sg-meter-config">this site's build, written once per page; every key has a defaultsg-meter-core.js _config()
The address: query?session_id= on account/topped-up.htmlwhoever made the link; any value is credited once (accepted, documented)_toppedUp()
The address: fragment#<persona id> on persona/; #from,to,r,s,f,n,e on for/ and for/make.htmlwhoever made the link; validated field by field, the rest dropped and listed_named(), reading-list.js parse()
The address: pathlocation.pathnamethe charge is keyed on it; the slug is taken from it; the share records itopen(), _page(), _send()
Local storagesgit.meter.v1 (the state), sgit.meter.v1.seen (session: the charge per path)this browser, written by the meter; also by the reader, who may edit it (accepted)_read(), _seen()
A fileRestore from an export…whoever made the file; checked for balance:number and log:array onlyrestore()
Formsthe share form, the subscribe form, the make pagethe reader; email checked by a loose pattern, names by a strict one_send(), subscribe.js, make.js
The wires' vaultsdata/bundle.json (with wire, map or stack when the vault has them), data/deliveries.json, data/releases.json, docs/*.md, decrypted in the browser; one vault per wirewhoever holds the vault's write key (the wire's agents); drawn as text, links only from http(s) values, markdown rendered by an escaping renderer; the vault's own HTML is not usedwire/wire.js
Own-origin dataarticles/graphs.json · articles/ids.json · .well-known/sgit-subscribe.json · sg-meter.tpl · sg-meter.cssthis site's build; the contact file's key is checked against its fingerprintfeed(), openList(), _send()
postMessagereplies from the dev.vault.sgraph.ai framesorigin-checked; drives a status line onlyvault-ui-embed.js

Where the reader’s data is kept

One key in this origin’s local storage, sgit.meter.v1, holds all of it; a session key holds the charge per path so a reload does not charge twice. Keys for the share and the subscription exist only in memory while the page encrypts. There is no cookie and no server-side store.

Field in sgit.meter.v1Holds
v3
createdISO time
balance · spent · declinedpence, to 0.1p
readspages opened
pausedalways 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)
activethe active persona's id
personas[]id, preset, name, created, on[], off[], visitor?, list? (a reading list's key), theme?
shares[]≤ 20: at, how, pages

What leaves the browser, and external calls

No page loads a script from another origin. These are the ways anything leaves, each tied to something the reader does, except the first.

What leaves the browserCarryingWhenWho sees it
GET, own originevery page, script, style, image and JSONwhenever a page opensGitHub Pages, which sees the request as any host does
POST https://dev.send.sgraph.ai/api/vault/append/write/y9j3nc60{append_token, payload}: an .eml encrypted in the browser to the lane's public keywhen the reader presses Send on the share page or Subscribethe vault host; it sees ciphertext, a size and a time
Navigation to https://buy.stripe.com/…the reader leaves for Stripe's page and returns to /account/topped-up.html?session_id=…when the reader presses Top up / PayStripe (card, email, name, postcode)
GET https://dev.send.sgraph.ai/api/vault/read/u9i9jp0l/…the ciphertext of the wire's vault: its head pointer, commit, tree and the files the page needs, decrypted in the browser with the published read keywhen a wire page opens, and when the reader asks for a new commit; the pointer at most every two minutesthe vault server; it sees the request and nothing of the reader
iframe https://dev.vault.sgraph.ai/?embed=1two frames; the published read key is posted to themon the study page onlythe vault UI host
mailto:Reply to <sender> on a reading list; the subscribe fallbackwhen pressed; the page sends nothingthe reader's mail client
A downloadreading-account.json: the whole state, payment references includedExport as JSONthe reader's disk
The clipboarda link, or the share textCopythe reader's clipboard
postMessage to the parent window{type:'sg-app-ready'} with targetOrigin *when a page is framedwhoever framed it

The contracts

ContractShapeOwner
The meter configstorageKey, 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{}}, rootsg-meter-core.js DEFAULTS; written into every page by sgit.ai's build and re-pointed by the import
A charged page<sg-meter view="page" kind="article|issue|note|collection|page|free" date="…" topics="a b" title="…" [quiet]>: the price class comes from the pagesgit.ai's build for imported pages; page.py (kind=free, quiet) for this site's
A persona link/persona/<id>/ and /persona/#<id>, 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:<collection>, ≤ 10; n ≤ 240 chars; e an addressfor/reading-list.js LIMITS, parse(), build()
Article numbersarticles/ids.json: n and slug are the contract, assigned once in publication order, never changed or reused; every other field is display textadmin/build/ids.py; validate.js check 12
The return page?session_id=<ref>: credited once per distinct value, never verified_toppedUp(), credit()
The exportthe state as JSON; restore accepts any file with balance:number and log:arrayrestore()
The sharea 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-PageshareData(), _send()
The envelopesgit 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 payloadenvelope(), subscribe.js encrypt()
The contact filesgit-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' vaultsu9i9jp0l (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 everadmin/build/wire-vault*.json; wire/how.html; each wire's agents own its content
Eventssg:meter.changed · charged · rated · declined · toppedup · reset · restored · persona · shared; sg-meter:errorsg-meter-core.js _emit(), sg-component.js failed()
A componentstatic jsUrl, get resourceName(), onReady(); a .tpl and a .css beside the .js; published at an immutable versioned pathsg-component.js
The import's rulesthe 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 movessync_newsroom.py items 9–15

The build, the gate and the tests

Build and gateWhat it doesLines
admin/build/ids.pyassigns article numbers, writes articles/ids.json156
admin/build/lists.pywrites the two reading-list pages103
admin/build/personas.pywrites the persona pages and persona/links.json160
admin/build/page.pythe page template, with the meter block every page this site writes carries117
admin/build/sync_newsroom.pythe import: re-points the meter to v1.4.1, applies the text rules, prints the article number709
admin/build/newsroom-meter.jsonthe top-up link, applied on import5
admin/build/newsroom-text.jsonthe ten text rules, applied on import167
admin/build/wire.pywrites the three pages of every wire (wire/, wire/owasp/, wire/aws/) and, from a clone, each wire's snapshot.json380
admin/build/vault_page.pywrites the vault page113
admin/build/lane.pythe newsroom's end of the feedback loop: clones or pulls a reader's lane with its read key (from the environment, never a file) and prints the notes by page147
admin/build/search_index.pywrites search/docs.json, search/text.json and the search page from the published pages, the article graphs and versions, the wires' snapshots and the notes356
admin/build/engineering.pyrenders /engineering/ from admin/engineering/: the board, one page per issue card, one per release, issues.json203
admin/build/md.pythe small markdown renderer the engineering pages use; escapes everything first144
admin/build/chrome.pythe nav and footer on every page, with the meter's balance and the Vault button and pane on pages that carry the meter361
admin/build/validate.jsthe gate: checks 2d (live link), 5 (key tripwire), 11 (imports unedited), 12 (numbers hold)493
Test fileWhat it drivesCheck callsLinesIn CI
admin/build/test_meter.mjsthe meter in Chromium40222yes
admin/build/test_lists.mjsthe parser in Node and the list pages in Chromium58214yes
admin/build/test_review.mjsthe review's findings, pinned as the behaviour is today14145yes
admin/build/test_passkey.mjsthe passkey lab in Chromium, with a virtual authenticator that has PRF, against the mock of the vault API33159yes
admin/build/test_wire.mjsevery wire's pages in Chromium, each reading its real vault65236yes
admin/build/test_vault.mjsreal vaults in Node: the vendored library against the mock of the vault API, and the sgit CLI round trip28180yes
admin/build/test_sync.mjsthe activity vault's logic in Node, with two devices' states48127yes
admin/build/test_pane.mjsthe vault pane in Chromium: the reading account carried between two devices through the mock vault, and a third opened with the recovery code31164yes
admin/build/test_search.mjssearch: the index and the ranking in Node, the page in Chromium37148yes
admin/build/test_ask.mjsask: the passages and the messages in Node; no model, the reader's key (openrouter.ai answered by a route) and the built-in model (faked) in Chromium32175yes
admin/build/test_inbox.mjsthe inbox: the link, the envelope, the refusals in Node; a browser with a passkey and one without, the QR code decoded, the drain and a new link, in Chromium against the mock with the append routes48247yes
admin/build/test_notes.mjsthe feedback loop in Chromium: notes on an article and at a wire story, carried by the vault, sent to the lane, read by the sgit CLI and lane.py with the read key alone, brought into a second device77329yes
admin/build/vault-mock.mjsthe mock of the vault API the two vault tests run against0207no

The review

Read line by line on 11 October 2026, as villager code is read: the meter element and core, the component base, the subscribe script, the reading-list code, the loader, the build scripts and the gate. Each finding has its evidence and, where it is a bug, a test in admin/build/test_review.mjs that passes while the behaviour stays as it is, so that the fix is a visible change to the test. Severity is for a reader’s money or data first, the move second. Nothing here was changed in the code, by the brief’s rule: no behaviour changes until phase five. The one exception is R11, which is the pipeline and not the product.

#SeverityAreaFindingEvidencePinned byProposed change
R1mediumthe meter's logicDeclining a page as “not relevant” leaves it in the persona's reading list. open() auto-keeps an article on first open; decline() refunds the charge but does not undo the auto-keep, so the article stays under “Its reading list” and profile() adds +5 for it while the decline subtracts 2: a page the reader rejected pulls its topics up.sg-meter-core.js open() [autoKeep], decline(); seen in a browser on 11 Octobertest_review.mjs R1 (passes while the article stays in the list)decline() removes an autoKept entry from the active persona's on[]; a reader who added it by hand keeps it
R2lowrestore from a filerestore() checks that the file has a numeric balance and a log array and nothing else. A persona without on[] and off[] makes the personas and newsroom views throw and show nothing; a log entry's path is rendered as the history link's href as it is; a persona's theme is written into a style attribute as it is. The file is the reader's own, so the exposure is a file someone else sent them to restore.sg-meter-core.js restore(), _read(); sg-meter.js _account() history rows, _chip() styletest_review.mjs R2 (the blank personas page); the href and theme cases are described, not pinnednormalise on read: personas get arrays, paths must be same-origin paths, themes must be a hex colour; unknown fields dropped
R3lowthe return pageA malformed percent sequence in ?session_id= (for example %E0%A4%A) makes decodeURIComponent throw inside onReady, so the element fails with “did not load: URI malformed” instead of saying the reference is missing.sg-meter.js _toppedUp()test_review.mjs R3try/catch around the decode; treat an undecodable id as no id
R4mediumdata correctnessA reading share or a subscription sent from this site is labelled sgit.ai: the imported config has share.site = "sgit.ai" (so the text begins “My reading on sgit.ai”), subscribe.js writes From: site@sgit.ai, and the contact file says site: sgit.ai. The agent that drains the lane can tell the two sites apart only by the Sent from line.index.html sg-meter-config share.site; assets/subscribe.js eml(); .well-known/sgit-subscribe.jsontest_review.mjs R4a recorded meter rule for share.site (admin/build/newsroom-meter.json); subscribe.js and the contact file are verbatim imports and need this site's own copies, or a parameter, after the move
R5noteoperationsOn the live site the lane for shares and subscriptions is on dev.send.sgraph.ai and the study page's vault frames come from dev.vault.sgraph.ai. Both hosts are named “dev”. Production reader data goes to them..well-known/sgit-subscribe.json inbox.endpoint; assets/vault-ui-embed.js ORIGINtest_review.mjs R5 (records the hosts)a decision, not a code change: either these are the production hosts and say so, or the files point at production ones
R6lowdisplayMoney is kept to a tenth of a penny and shown to a penny. Balance and spent are each formatted on their own, so a half-penny state shows, from £5.00, a balance of £4.95 and £0.06 spent, which do not add up; the history shows 5.5p. A reader who checks the arithmetic finds a penny missing.sg-meter-core.js money(), r1(); seen with balance 494.5test_review.mjs R6one rounding rule for the pair (format spent as start − balance, or keep both to 0.1p on the account page)
R7questionpickspicks() excludes every page in the log, whichever persona read it, while profile() is per persona. An article read as Morgan is never picked for Rowan.sg-meter-core.js picks()test_review.mjs R7 (records the behaviour)decide: per-persona exclusion, or keep and say so on the newsroom page
R8lowrobustnessfeed() caches a failed fetch as null for the life of the page, so one transient failure leaves “The list of articles could not be loaded” on every view until a reload.sg-meter-core.js feed()test_review.mjs R8do not cache a failure; retry on the next call
R9mediumthe number contractIf sgit.ai renames an article (a new slug), ids.py marks the old number withdrawn and gives the article a new number. A reading list that named the old number then says the article is no longer published, although it is.admin/build/ids.py assign()described; a rename has not happened yeta renamed_to field on the old entry, followed by the list page; the gate requires it when a slug disappears and a new one appears in the same import
R10high for the moveContent-Security-PolicyEvery imported page loads assets/site.js, subscribe.js and shots.js by fetching the text and running it with (0, eval)(text) (the loader's bridge path), and the loader and the meter config are inline. A script-src policy without 'unsafe-eval' and without hashes breaks every page. frame-ancestors cannot be set in a meta tag at all, and GitHub Pages sets no headers, so framing (the Keep, Remove and Top up buttons under another site) is not prevented today.the loader IIFE on every imported page; subscribe/index.htmltest_review.mjs R10 (counts the pages that eval)load scripts with <script src> or import(); then a meta CSP for script-src and connect-src; frame-ancestors needs a host that sets headers
R11high per the briefthe pipelineThe one workflow ran validate.js only; the browser tests (46 + 65 checks) never ran in CI..github/workflows/deploy-pages.ymlfixed in this release: a reader-tests job runs all three test files before the tagdone; the move's pipeline adds WebKit and Firefox and a pre-production copy
R12note for the movetwo upstreamsThe account pages, subscribe, the contact file and the component base are imported from sgit.ai on every sync and cannot be changed here except by a recorded rule, and verbatim files (scripts, JSON) cannot be changed by rule at all. R4 is the first consequence. The newsroom's meter is v1.4.0 while sgit.ai's is v1.3.1.sync_newsroom.py VERBATIM, MOVED_DIRSthe brief's phase 1 removes itthe move: both sites pin released files
R13notewhat leavesThe export includes the payment references (sessions, topups[].ref) that the share deliberately leaves out. A reader who sends their export instead of their share sends them.sg-meter.js _export(); shareData()describedsay so beside the button, or export without sessions
R14lowboundssessions[] and topups[] grow without bound, and a payment reference is not checked for shape before it is stored.sg-meter-core.js credit()describedcap both; keep references that look like cs_… only
R15lowpersonaspersona(id) falls back to personas[0], which is “me” only when “me” is first; after a restore that puts another persona first, an unknown id resolves to that persona.sg-meter-core.js persona(), _read()describedfall back to the persona with id me
R16processcachingImported pages load the meter as sg-meter.js?v=<sgit.ai's release>, so the cache-buster never changes for a release of this site. That is fine while a published version is never edited in place, which is the rule; it is written here because v1.4.0 was edited in place while it was being built.the import's own_meter() keeps sgit.ai's ?v=describeda change is a new version path, always
R17notereading listsA list's persona is keyed by a 32-bit hash of the names and numbers, so two different lists can share one persona with probability about one in four billion per pair.for/reading-list.js listKey()describeda longer hash, or the fragment itself as the key
R18notelistenersThe landing view adds a window hashchange listener per element and never removes it, and re-renders on every meter change (the picker's “Copied” label resets).sg-meter.js _landing()describedone listener, removed on disconnect

Accepted, and said so

Accepted gapWhyWhere it is said
The return page credits any session ida static page cannot verify a payment; building the address by hand credits the reader's own screenmeter/security.html
The reader can edit their own balancethe state is in their storage; nothing is behind a wallmeter/security.html, No server by design
The names in a reading list are whatever the link saysthere is no signature yet; a signed list is a later step on RFC 0001's keysfor/index.html
A page is charged by the deepest point reached, and the reader card is at the footreaching the card to rate a page is reading to the endgoing-live-with-the-reading-meter.html

What the move takes from here

For an agent

This page maps the client-side API of newsroom.sgit.ai at v0.12.72: every reader file with its hash, every view and control, every input, what leaves the browser, the contracts, and a review with 18 findings. The same data is at https://newsroom.sgit.ai/newsroom-api/map.json. It is regenerated by admin/build/api_map.py on each build; the findings are pinned by admin/build/test_review.mjs. The brief it answers is https://newsroom.sgit.ai/articles/explorer-to-villager-newsroom.html.