Home / Articles / The user keeps the meter / Versions / v1.2.0
The user keeps the meter: what changed in v1.2.0
From v1.1.0 (2026-10-21, 2d1bfa2af) to v1.2.0 (2026-10-21, e1df4631e), paragraph by paragraph.
21 paragraphs added, 9 removed, 17 changed in place, 59 unchanged. About 968 words added and 334 removed. Insertions are marked like this, deletions like this; unchanged runs are folded to one line; figures appear as their file names.
1 unchanged paragraph
Summary: Billing is usually a server problem: a server, a database, a balance every action has to update, race conditions, and either a bottleneck or a scheduler to keep it all in step. This is an exploration of doing it the other way round, on the client, with vaults. Each user gets a billing vault that both they and I can write to. The user's browser meters its own activity and appends it there, signed with the user's key, the way this newsroom's meter already works. When the user pays, their browser adds the credit straight away, so they can carry on. My admin vaultvault, collectswhich thatcan activitybe andoffline for a while, later reconciles payments with Stripe, from my browser, and commits credita validation back into the user's vault, signed with the admin key, so anyone can check it. Credit is then unvalidated, validated, or not valid, and the product keeps working on unvalidated credit, because on the client it costs us nothing. Since the vault's first commit is pinned and every commit is signed, we always know who wrote what. The one server left, the sgit upload server, does a light check only: is the key valid, and is there credit, read from a cached, signed value through a small credits API. Four parties, each with its own key: the admin vault, the user, the upload server and the API. And because the user's data lives in their vault, it travels with them across our sites. It is all PKI from here. Where it lives is open: user.sgit.ai, or billing.sgit.ai, so others can use it too. This is a proposal, with the open questions written down: when to credit a payment, and who creates a user's first vault.down.
!shot um-parties.webp | images/ | The proposed design. The user's browser appends its activityactivity, and adds credit when it pays, to a billing vault, signed by the user. The admin vault reads it, reads payments from Stripe, and commits creditvalidations back, signed by the admin. The upload server checks thatthe validated credit through a small credits API before it accepts an upload. | narrow | architecture
3 unchanged paragraphs, under In short {#in-short}
• Two writers, two keys. The user signs activity; the admin signs credit.
• Stripe is reconciled from the admin's browser. Payments are read and matched, then credited.
• Two writers, two keys. The user signs activity; the admin signs the validation of credit.
• Credit now, validated later. The user carries on straight away; the admin side catches up.
• Stripe is reconciled from the admin's browser. Payments are read, matched and validated.
13 unchanged paragraphs, under The old problem {#problem}, What we already have {#have}, The idea: the user keeps the meter {#idea}
So the idea is to do user management, activity tracking and billing on the client, with vaults,vaults. behindWhere twoit newlives sites:is user.sgit.aia forquestion thein user,itself. andIt could be user.sgit.ai, with api.user.sgit.ai for its client code,code. Or, because we want it to be easy for others to use, our other sites and other people's, the billing part could stand on its own: billing.sgit.ai, with api.billing.sgit.ai. Either way, the API sites would be released the same way as our other API sites.others.
5 unchanged paragraphs, under Two writers, two keys {#writers}
The admin writesvalidates credit. When the user comes back from paying, their browser adds the credit straight away, with a paymentsigned arrives,claim. Later, the admin side checks that claim against Stripe and commits a credit recordvalidation into the same vault, signed with the admin key. Because it is signed, anyone can verify it against the admin's public key, and nobody else, the user included, can write a creditvalidation that verifies.
A creditvalidation record could be as small as this. The field names are a sketch, not a format:
``json
{
"billing_vault": "the vault's id",
``"balance_pence":"claims": [
{ "claim": "the user's claim commit", "stripe_event": "the event's id",
"pence": 500, "status": "validated" }
],
"validated_balance_pence": 4210,
"as_of": "2026-10-21T10:00:00Z",
"covers_activity_to": "the last activity commit counted",
"grants":"as_of": [{ "stripe_event": "the event's id", "pence": 500 }],"2026-10-21T10:00:00Z",
"valid_until": "2026-10-28T10:00:00Z",
"signed_by": "the admin key's fingerprint",
"signature": "over everything above"
}
Each side signs only what it is responsible for. The user cannotcan raiseadd credit their ownbrowser balance,believes in, but cannot make it validated, and the admin cannot invent activity the user did not sign.
7 unchanged paragraphs, under The admin vault, and Stripe {#stripe}
So the flow is: the user pays with a payment link tagged with their billing vault; they come back to the site, and their browser appends a signed claim, "I paid"; the admin's browser lists recent payments, matches each claim to a payment, and commits the credit, signed. Two-way sync between the two vaults, with each side writing only its own records.
[figure um-topup.webp] A top-up, step by step. The user pays through a payment link and appends a signed claim. The admin's browser reads recent payments from Stripe, matches the claim, and commits a signed credit. The upload server reads that credit and checks it. | architecture
Credit now, or after the check? {#when}
The first open question is when a payment should count. If the credit only appears once the admin side has seen it in Stripe, the user waits, perhaps until my browser next runs. If it appears as soon as the user's browser says "I paid", the user is trusted for a while.
Stripe's own guidance argues for not relying on the moment the user returns: "You can't rely on triggering fulfillment only from your checkout landing page, because your customers aren't guaranteed to visit that page." And some payment methods, like bank debits, "can take 2-14 days to confirm the payment".
A middle way fits what the newsroom meter already does, where the balance can go below zero: let the user carry on straight away, on a small allowance, and make the payment real once the admin side has matched it. The allowance is the amount we are prepared to trust a user for, and it can grow with their history.
So the flow is: the user is sent to Stripe with a payment link tagged with their billing vault; Stripe sends them back to a page that gives them the credit, and their browser adds it straight away and appends a signed claim, "I paid"; later, the admin's browser lists recent payments, matches each claim to a payment, and commits a signed validation. Two-way sync between the two vaults, with each side writing only its own records.
[figure um-topup.webp] A top-up, step by step. The user pays through a payment link, comes back, and their browser adds the credit straight away with a signed claim. Later, the admin's browser reads recent payments from Stripe, matches the claim, and commits a signed validation. The upload server reads that validation and checks it. | architecture
Credit now, validated later {#when}
When should a payment count? The user should be able to use what they paid for straight away. That has to be immediate. So the credit goes into their browser the moment they come back from Stripe, and they carry on.
The check comes afterwards, as a layer on top. The admin side does not need to be online when the user pays; it can catch up hours later. When it does, it adds a note to the user's vault, signed by us as the commercial authority: payment validated, or payment not accepted. Nothing central sits in the path of the user's work. The product keeps working, and the credit simply moves from unvalidated to validated.
So credit can be in one of three states:
• Credit not yet validated. The user paid, or says they did, and the admin side has not checked yet. Normal, and short-lived.
• Credit validated. The admin side matched it to a payment in Stripe, and signed it.
• Credit not valid. No matching payment. In our case this is unlikely: it means an attack, or a bug.
That is why the product should carry on with the credit the user has, validated or not. Most of what it meters runs on the client, so letting a user carry on with unvalidated credit costs us nothing. And Stripe's own guidance fits this order: "You can't rely on triggering fulfillment only from your checkout landing page, because your customers aren't guaranteed to visit that page." The admin side reads Stripe directly, so a payment whose user never came back can still be credited, and one that is slow to confirm, like a bank debit, which "can take 2-14 days to confirm the payment", simply stays unvalidated for longer.
Who are we defending against? {#threat}
The question that sizes this design is the one in who are you protecting against?. Here, the party that could cheat is the user, by gaming the client: adding credit they did not pay for, or leaving out activity. In the short term that is unlikely, and it is also very easy to detect. Every claim has to match a payment in Stripe, every commit is signed, and a claim with no payment behind it shows up as not valid the next time the admin side runs.
So the design does not try to make cheating impossible on the client. It makes it visible, and it puts the hard check where a mistake would cost us money: at the server.
A vault shared by two parties {#shared}
There is a more general idea underneath this. The billing vault is shared by an untrusted party, the user, and a trusted one, us. Once the vault exists, we pin its first commit. From then on, every later commit can be checked for consistency against it: that it descends from that first commit, and that it is signed by a key we know.
That gives a two-way channel between the two parties, through the vault, where we always know who did what. The user writes activity and claims; we write validations; and because of PKI, neither can pass off their writing as the other's. Billing is the first use, and the same pattern fits any place where a user and a service need to keep a shared record that both can trust.
3 unchanged paragraphs, under The one server left {#server}
• Is there credit? A read of the user's latest signed creditvalidation record, verified against the admin's public key.
4 unchanged paragraphs, under Four parties {#parties}
• The admin vault. Writes the licensing and creditvalidation records, collects the usage, and reconciles with Stripe.
8 unchanged paragraphs, under What the user gets {#benefits}, What it trusts, and what it does not {#trust}
• CreditValidated credit cannot be forged by the user.** It is only valid withneeds the admin's signature. Unvalidated credit could be, and it would show up as not valid.
• A cached creditrecord can be a little stale. So it carries an end date, and the allowance bounds what that can cost.date.
2 unchanged paragraphs, under Open questions {#open}
• When does a payment count? Straight away on an allowance, or only once matched in Stripe.
• Where does it live? user.sgit.ai and api.user.sgit.ai, or billing.sgit.ai and api.billing.sgit.ai, so others can use it easily.
• Should the upload server accept unvalidated credit? Where usage costs us real money, it may want only validated credit, or a small amount on top.
4 unchanged paragraphs
• How are refunds and disputes written? Probably as signed creditvalidation records too, negativewith ones.their own status.
1 unchanged paragraph, under The versioned API sites {#versioning}
api.user.sgit.aiapi.user.sgit.ai, or api.billing.sgit.ai, would follow the rule our other API sites already use, written down on api.send.sgit.ai: every release is a complete, independent copy at its own versioned path, never edited after it is published, with a manifest of hashes. A site pins the version it uses, so a change to the API is a new version, not an edit to an old one. This newsroom has not written about that rule yet; it should, and it is now on the list.
1 unchanged paragraph, under What this does not claim {#limits}
• Nothing here is built. Neither user.sgit.ai andnor api.user.sgit.aibilling.sgit.ai do not existexists yet.
6 unchanged paragraphs, under The diagrams, as source {#diagrams}
``mermaid
flowchart TB
U["User's browser<br/>meters its own use"]
subgraph BV["Billing vault, one per user"]
A["Activity ``lane<br/>signedand claims<br/>signed by the user"]
C["Credit record<br/>signedC["Validations<br/>signed by the admin"]
end
AD["Admin vault<br/>in the admin's browser"]
ST["Stripe"]
API["Credits API<br/>a simple read"]
UP["Upload server<br/>key valid? credit left?"]
U -->|appends activity|activity, adds credit| A
A -->|synced| AD
ST -->|payments, read by| AD
AD -->|signs credit|-->|validates| C
C -->|cached| API
API --> UP
U -->|uploads| UP
``mermaid
sequenceDiagram
participant U as User's browser
participant S as Stripe
participant B as Billing vault
participant A as Admin's browser
participant P as Upload server
U->>S: Pay with a payment link, tagged with the billing vault
S-->>U: Back to a page that gives the ``sitecredit
U->>B: AppendAdd the credit now, with a claim: I paid, signed byclaim
Note theover U,B: The user carries on straight away
A->>S: ListLater: list recent payments, with a read-only key
A->>B: ReadMatch the claim and match it to the payment
A->>B: Commit the credit,a signed byvalidation, theor adminnot accepted
P->>B: Read the signed credit,validation, cached
P->>P: Check the signature and the balance
1 unchanged paragraph, under Where this comes from {#sources}
ATwo voice memomemos of 21 October 2026. Stripe's documentation, read the same day: webhooks, API keys, listing events, the success page and payment link parameters. api.send.sgit.ai's independent releases. Related: paying after the value, who are you protecting against?, the editor's channel, secrets on public keys, PKI everywhere, free to copy, paid at the source. Written up and checked by this site's agent, newsroom.writer; the editorial responsibility is mine.