What we shipped
The running list of changes to UnTXT. Newest first.
[4.92.0] - 2026-09-07 - English, and no pretending otherwise
Removed
- Fifty other interface languages. This app claimed fifty-one for a while, all machine-translated, and enough of that translation was wrong that the claim did more harm than the feature ever did good. Our German tutorial said "Showing Noch lange nicht vorbeik" where it meant "Showing somebody". Roughly a third of the strings were damaged in some version of that way. We quietly switched everybody to English in September and left the files sitting in place, which was the worst of both worlds: nobody could read their own language, and the repo still insisted they could.
- The language menu in Settings. Since the switcher went off it had been showing a heading marked LANGUAGE with nothing whatsoever underneath it.
- Right-to-left layout for Arabic, Urdu and Persian. It worked. What it mirrored was an interface that had already been English for a month, so it was mirroring English.
Changed
- Our languages page now answers the question people actually ask, which is whether this thing can read an essay that is not in English. That answer already sat on the page underneath a list of fifty-one language names, and it is the whole page now. We calibrated on English. A mark on anything else is somewhere to go and look rather than a finding, and careful writing in a second language reads as regular to every heuristic detector going, ours very much included — which is the reason two models argue over your text instead of leaving the heuristics to decide by themselves.
Note
- What you can check has not changed. Text in any language goes in exactly as before, and passages come back marked in the language you wrote them in, because showing somebody a translation of their own sentence and asking whether it sounds like them was never going to work.
[4.79.0] - 2026-09-07 - One analysis, not two
Changed
- Pressing Analyse now runs the Academic Weapon and nothing else. The eleven-module pass that used to run first is gone from the web app. It had already stopped being a result — we covered it with the loading screen in June, because people were reading it as the answer and stopping there — but it was still quietly marking up a whole document underneath, which you could see through the blur if you looked. Computing an answer we deliberately hide is not a loading state, it is a second product left switched off. The eleven modules still run everywhere they are genuinely used: the API, the browser extension, and the check we run on our own writing.
- A document is saved once, when the verdict lands. Nothing is written to Documents and nothing is charged for a run that never finished.
- The binoculars sit in the middle of the screen now, where you are looking, rather than up near the top with the progress bar holding the centre.
Fixed
- A failed run leaves you where you started, with your text still in the box and a note saying what went wrong. It used to uncover the first pass instead and offer you that; there is no first pass to offer any more, and a half-answer was never much of a consolation.
- The Analyse button could get stuck reading "Analyzing…" after a run was refused, with no way back except reloading the page.
- The tour's sample paragraph had two sentences our own reviewers marked as human writing, on a paragraph we wrote to be obviously machine-made. They have been rewritten, because the first result a new person sees should not teach them the wrong thing.
[4.75.0] - 2026-09-07 - A finding, not a rewrite
Removed
- The box that offered to rewrite your sentence. Clicking a marked passage used to bring up replacement suggestions, a text box holding the whole sentence, a Replace sentence button, and an accept-or-skip state for every mark. It has all gone. We tell you which passages read as machine writing and why; writing them again is the part that has to be yours, and a tool that hands you a replacement is quietly doing your work for you.
- Hover tooltips. The reason for a mark used to appear the moment your pointer touched it, follow the cursor across the page, and say exactly what you would have read one click later anyway.
Fixed
- Marks stopped changing colour after you clicked them. Opening one washed the sentences around it in a tint of that mark's old module colour and lit the mark itself, which is where the eleven colours kept coming back from in a product that shows two. Replacing a sentence then turned its row purple in the sidebar and flashed the next mark blue. A finding should not light up because you looked at it.
- Every recalled AUTHENTIC passage was labelled SUSPECT. Reopening a document read its verdict back off the wrong field, so the gold passages came back grey in the sidebar and the popup, and only the highlight itself was right.
- The double-click editor could not be opened at all, by anyone, and had not been openable for some time: the first click raised a panel over the whole window and the second click landed on that instead. It went out with the rest.
Changed
- Two colours, and they stay put. Gold means a passage carries the marks of a person writing. Grey means it reads the way machine writing reads. Clicking one tells you what was noticed about it, and closing it puts everything back exactly as it was.
[4.60.0] - 2026-09-05 - The documentation caught up with the product
Fixed
- The documentation was describing an app we stopped shipping in July. Six of the nine public pages still explained eleven colour-coded signals, several still walked you through Standard Mode and its risk bands, one offered UnTXT in 21 languages when it has been in 51 for some time, and one quoted an Academic Weapon price that was out by a factor of eight. Every one of them has been rewritten against what the app actually does.
- The Help & Guide inside the app had the same problem, in every language including English. Newer wording had been written in the right spirit and the wrong place, so none of it ever reached a screen, and the guide was still explaining a mode retired two months ago.
- The press page had been saying "Last updated: May 2026" since May. Fifty-five releases went past it.
- A handful of pages on the public site still promised colour-coded results, including the badge on the sign-in page, which had been advertising eleven modules to everybody arriving.
Changed
- No page of ours quotes an accuracy figure any more. Every number of that kind in this market comes from a benchmark chosen by whoever is quoting it, and that includes the ones we have quoted before. We would rather hand you the reasoning behind a mark and let you weigh it yourself.
- Every public page now reads in one voice, and every one of them passes our own detector comfortably. We sell a tool that finds machine-written prose, so pages of ours that it flags are a fair thing to be judged on.
[4.59.0] - 2026-09-05 - Somewhere to put the story
Added
- A box at /rant for the story of handing something in. No account, no name, and it does not have to mention us at all. We store the message, the time, and nothing else — no address, no account, no trail back to you — because a page that asks for the honest version and quietly keeps a record of who said it is not worth having. There is nobody to reply to, which the admin side says on the panel so that nobody here forgets it either.
[4.58.0] - 2026-09-04 - Documents rows you can tell apart
Added
- Every row in Documents now carries an icon saying what it is, so an Academic Weapon report and an ordinary document stop looking like the same thing.
- Right-click a row for Download and Delete. Deleting still takes two deliberate clicks, because the alternative is losing a document to a twitchy finger.
[4.57.0] - 2026-09-04 - Buttons that stayed readable when you hovered them
Fixed
- Eight buttons went dark-on-purple when you hovered over them, which is to say invisible. It was one CSS rule quietly winning an argument it should have lost, in eight places at once.
[4.56.0] - 2026-09-04 - Documents and folders on one surface
Changed
- Documents has a sidebar now. Where your documents live — yours, your cohort's, the ones shared with you — sits down the left alongside your folders, which used to be somewhere else entirely. Browsing folders and browsing documents were two separate places doing one job.
- Nothing was taken away. The same search, sorting, dates, downloads and version history are all where they were.
[4.55.0] - 2026-09-03 - The page behind a dialogue stops taking clicks
Fixed
- The blurred page behind an open dialogue was still clickable. It looked switched off and was not, which you only find out by clicking something you were only looking past.
Changed
- The feedback box is two questions: what is working, and what could be better. It used to ask you to pick a category first, which is a small tax on somebody who already has something to say.
[4.54.0] - 2026-09-03 - Fifty credits for bringing a friend
Added
- 50 CVT when somebody you invited finishes setting up, with no subscription needed at their end. It runs until 3 December 2026 and stacks with what was already there.
Fixed
- The old referral panel promised a reward after three invites, and there was no such threshold. It said so for longer than we would like.
[4.51.0] - 2026-09-03 - Eleven colours become two
Changed
- There are two marks now instead of eleven colours. Gold for a passage that reads as yours, grey for one that reads as machine-written, and the reasoning behind either one a click away. Learning a taxonomy of eleven colours before you can read your own essay was never a fair thing to ask, and the colours were only ever answering one question anyway: which bits should I go and look at. That question has two answers.
- All eleven signals still run underneath, exactly as they did. Developers calling the API still get every one of them back. What changed is what we put in front of you.
- Free credit says when it expires, which is ninety days, in the place where it matters rather than in the small print. Documents somebody shared with you stay readable either way.
- Buying credit moved out of the toolbar and onto your account page, where the rest of your billing already lived.
[4.50.0] - 2026-09-03 - Documents, a toolbar, and sharing that respects a school's walls
Changed
- The Filing System is called Documents, in all three of the places that were calling it something different.
- A row opens the document. It used to raise a preview, which meant the list needed a separate button just to get in properly. Eight buttons per row are now two.
- Copy, Download, Version history and Share sit in one bar above the editor, rather than being scattered across three parts of the page.
Added
- A share modal with two halves, because sending a document to one named person and publishing a link anyone can open are genuinely different acts and should not share a button.
- Connect. Somebody you have no connection with gets a request rather than a document, and nothing reaches their account unless they accept. Accepting is also what forms the connection, which the card says outright, since a standing permission is a larger thing to agree to than one file. You can see everyone holding that permission and remove any of them.
- Organisations get a wall that holds from both sides. Members share with colleagues and nobody else, nobody outside reaches in, and the public-link half is not rendered for them at all — absent rather than disabled, because a control you can see but not use is an invitation to go looking for a way round it.
Fixed
- "Failed to render PDF" was never the renderer. Four separate defects, the main one a missing fallback on the sign-in token that sent the literal word
nulland came back as "Unauthorized" with nothing in the log to say so.
[4.49.0] - 2026-09-02 - No scores anywhere
Changed
- Every percentage is gone. Not in the app, not on a shared link, not in the wording attached to a mark. A number invites you to work it downwards, and working a number downwards is not the same activity as writing better, though it feels close enough that people spend evenings on it. The passages are what we report, because the passages are what you can act on.
Added
- A shared link can hand over the actual document, into the reader's own account, editable and with its own version history. You opt in, and it is off by default.
- Version history is a tree rather than a list. Each version names itself from the longest run of text it changed, and starting from an older version forks the history there rather than quietly overwriting.
[4.48.0] - 2026-09-02 - One result instead of two
Changed
- Standard Mode is retired. It collapsed the modules into risk bands, and once the first detection pass became the loading screen rather than the answer, a second way of presenting that pass had nothing left to present.
[4.47.0] - 2026-09-02 - Plainer legal pages, and a comparison that tells the truth
Changed
- The privacy policy and the terms of service now read more plainly. Legal has been through both and cleared the new wording as a restatement, which means that nothing you agreed to has actually moved. The company, the law governing it, the liability cap, the age minimum, how long credit lasts and who processes your text all sit exactly where they were before, and only the sentences around them changed. Both pages are dated 2 September 2026 so that you can see when it happened.
- Seven public pages were rewritten so that they pass our own detector. We sell a tool that finds machine written prose, and pages of ours that the tool flags are a fair thing to be judged on, so they no longer flag.
Fixed
- Our GPTZero comparison was selling a price plan that we no longer offer. It said UnTXT needs no subscription, that it runs on buy once bundles, and that credit lasts 365 days. None of that has been true since early 2026, and it was sitting on the one page written to answer that question. It now describes the monthly plan, what happens when you go past your allowance, and where unused credit ends up.
- The use cases page priced an Academic Weapon run at roughly ten times a normal check, when the real figure is about 89 credits against 1. It also claimed that Academic Weapon waits for you to click it, described three models where there are in fact two and a tiebreaker, and managed to spell our own company name wrong.
- How it works still said that there was no file upload, though you have been able to drop a PDF or a Word file straight onto the canvas since August.
[4.46.0] - 2026-09-01 - Replies reach us, and links survive signing in
Fixed
- Replies to our email now actually arrive. A feedback request went out at the end of August with no reply address on it, so everyone who answered got a bounce rather than reaching anybody. Every automated message now carries a live reply address, set in the sending code itself rather than remembered case by case, and a message cannot go out without one.
- A link we email you survives signing in. If you clicked a link that pointed at a particular part of the app while signed out, you used to land on the ordinary app once you signed in, with the thing you clicked for gone. The address you asked for is now kept through the sign-in step and you arrive where you meant to.
Added
- A shorter way to tell us what you think. Rather than one empty box, the feedback form now asks three questions: one thing you liked, one thing you want changed, and the writing frustration that is currently worst. All three stay on screen while you type, so answering the first does not hide the other two.
[4.45.0] - 2026-08-31 - Universities outside the usual domains
Fixed
- A real university address was being refused for the wrong reason. Verification only recognised domains ending in .edu, .ac.uk and a handful of similar patterns. That quietly excluded most German, Dutch, Nordic, Czech, Romanian and Estonian universities, where the address carries no academic marker at all, along with places like the University of Toronto. Roughly 10,600 university domains worldwide are now recognised. Nothing went onto that list on trust: every one had to answer to a DNS check and read as an educational institution when its site was actually fetched.
Added
- Testimonials on the sign-in page, from named people at named institutions.
- Creators can now join the partner programme, paid a one-off bonus for an approved piece of content on top of the ongoing commission on anyone who signs up through their link.
[4.44.0] - 2026-08-31 - The Filing System, and documents you can edit
Added
- The Filing System replaces the old list of past analyses. Documents carry a title taken from their opening words rather than just a date and a score, and any of them reopens into the editor in one click. The three most recent sit above the paste box when you arrive.
- A reopened document is editable, and every version is kept. Edit it, and the earlier version does not disappear — both stay, and a colour-coded diff shows exactly what changed between any two of them. Editing costs no credit. Only re-running the analysis does.
- What's New, a panel in the profile menu listing what shipped in each release, with anything you have not read marked. It is there when you want it rather than interrupting you.
Changed
- The sign-in page was rebuilt as a page you can scroll: what the tool does, who uses it, and what they said about it.
Removed
- The mode toggle, the manual Academic Weapon button and the signal legend have gone from the results view. Academic Weapon already runs on every analysis, so the button had nothing to do, and clicking a highlight explains it better than a legend sitting off to one side.
[4.42.0] - 2026-08-31 - Free tier trimmed, My Documents, logo links home
Changed
- Organic free tier grant cut to 1,000 words (14 CVT), down from 9,000 words for UK signups and 6,000 for the rest of the world. A signup that arrives through a partner or sales tracked link keeps 3,000 words (40 CVT) instead, three times the organic amount, because those links only stay worth using if they pay off better than showing up on your own. The grant is now computed and checked on the server in
api/auth-routes.js, so it can no longer be inflated by whatever the signing in browser happens to send. - The UnTXT logo now links home. Click it from anywhere in the app and you land back on the main canvas. If there is unsaved pasted text sitting in the input box first, you will be asked to confirm before it leaves, since that text has nowhere else to go.
- Past Analyses is now called My Documents. Same feature, new name, plus a document icon instead of the old clock, since a saved check reads more like something you own than a log entry you glance at once. Actually, the bigger change is what shows up next to it: a returning signed in user now sees their last three saved documents right above the paste box, under a Continue where you left off heading, so finding earlier work does not depend on remembering the header button is even there. Each of those entries, and the ones inside My Documents itself, now shows a short title pulled from the first few words of the text. Before, all you got was a date and a score.
- The sign in page adds one sourced comparison instead of an unsupported claim. F1 0.73 on a 935 text peer reviewed benchmark, ahead of GPTZero at 0.68 and Turnitin AI at 0.65, linking through to the full methodology, so nobody has to take our word for it.
[4.29.0] - 2026-08-18 - CVT priced from cost
Changed
- CVT is now derived from what an operation costs to serve, not from how many tokens it moved.
calculateCvtFromTokens'sceil((input + output) / 100)billed a $0.05/MTokgpt-5-nanotoken identically to a $30/MTokgpt-5.6-soloutput token — a 600× spread flattened to one rate — and treated cheap input the same as expensive output. Every path had drifted far above the 40% gross-margin ceiling UnTXT prices to (chosen so a later 20% VAT still leaves ~20% net):
| Path | API cost | Was charged | Margin | Now |
|---|---|---|---|---|
| Preliminary (1,000w) | £0.0002 | 14 CVT | 99.9% | 1 CVT |
| Tharoorizer (1,000w) | £0.0199 | 140 CVT | 98.6% | 4 CVT |
| Academic Weapon (1,100w) | £0.530 | 766 CVT | 93.1% | 89 CVT |
utils/cvt-calculator.js now carries a per-model rate table and cvtForCalls([{model, usage}]), which charges cost / (1 - 0.40) / £0.01. Every charging path routes through it; calculateCvtFromTokens survives only for pre-flight estimates, where the model mix is not yet known. Adding or repricing a model means editing MODEL_RATES and nothing else.
Measured against three real Academic Weapon runs from onboarding-m13-cost-log (63k input / 13.6k output for 1,100 words), not estimated.
- The 766 CVT charge is why mandatory Academic Weapon looked unaffordable. A Student plan is 800 CVT/month, so a subscriber got 0.95 analyses. At 89 CVT they get nine — with no price rise anywhere.
- Overage was over the ceiling on every plan (46–54%), because the rates were set against a cost assumption 2.5× too low. All four now bill £0.0099/CVT — the ceiling rate, and effectively the same as simply buying credit, so going over is no longer a penalty dressed as pricing.
- Professional raised to 1,400 CVT (was 1,300, at 40.4% — marginally over the ceiling). The B2B seat follows to 1,400 to keep the "a seat is never a downgrade from the individual plan" promise made in
legal/org-agreement.md; at £11.70 net that is 28.7% margin and ~15.7 AW runs per seat.
- The B2B seat cost basis is now measured, not estimated.
BLENDED_COST_PER_CVT_GBPtook the Sonnet-derived floor and applied an invented 6× uplift to a guessed 15% M13 share, landing at £0.00235 — 2.5× optimistic against the real £0.00596. It is now derived live from the rate table, so a model reprice flows through instead of going stale.
- Free signup grant raised to 9,000 words (120 CVT) UK / 6,000 words (80 CVT) international, so a new account has preliminary headroom after its complimentary Academic Weapon run.
Fixed
GPT5_NANO_COSTinutils/cvt-calculator.jswas wrong by 1000× —0.05 / 1000is $50/MTok against a real $0.05/MTok. Exported but never consumed, so nothing was mispriced, but it would have misled anyone costing a path from it. Removed along with the rest of the reference-only constants.- The AW pre-flight estimate (server and its client mirror) still quoted the old ~842 CVT. Both now price the same way the charge does, so the modal and the bill agree.
Added
scripts/test-pricing-invariants.js— 23 checks asserting no operation and no plan exceeds the ceiling. Because integer CVT rounding necessarily overshoots on sub-penny operations, it asserts the pre-rounding price respects the ceiling and that rounding adds at most one CVT;marginOfreturns a rounding-aware bound that converges to 40% as charges grow.- Charged AW runs are now written to
onboarding-m13-cost-logalongside the free ones, with real GBP cost, so the £0.530 reference can be re-derived if model rates move. - Per-model usage accounting in
utils/adversarial-debate.js(recordUsage), including cache read/write fields, so the prompt-caching reorder needs no further plumbing.
Notes
- Fixed costs are deliberately not amortised into unit price. At current volume (26 chargeable analyses/month) that would add ~£1.35 per analysis — 2.5× the entire marginal cost of an AW run — and falls ~100× as volume grows. Break-even instead: ~100 AW runs/month covers UnTXT's ~£35/month share of infrastructure.
[4.28.1] - 2026-08-18 - Onboarding ladder hand-off
Fixed
- New users completed tier 1 and were never offered tiers 2 or 3.
stage3-onboarding.jsrefused to open while#stage2ReportModalwas on screen, butstage2-onboarding.jshanded off 800ms after opening that modal — and the modal has no auto-dismiss, it closes only on a user click. The timer lost to every human who actually read their report, soStage3Onboarding.maybeShow()returned silently and nothing re-fired. Deterministic, not intermittent. Introduced inf661ad8.
The hand-off now chains from the report modal's Close click rather than a timer. Stage 3 carried the identical latent bug and only appeared to work because stage4-onboarding.js's guard happened to omit #stage3ReportModal; all three now share one isOverlayBusy() helper that deliberately excludes report modals, since chaining happens after they close.
Firestore signature of an affected account: stage2CompletedAt present, stage3OfferedAt absent.
- Declining a tier left people with nowhere to go. The "No thanks" handlers were empty no-ops — the comment above them notes the refactor removed "the old hardcoded stage2-decline-chains-to-stage3 logic", and nothing replaced it. Declining now advances the ladder, via a new
skipoption onresolveNextStep. The option is load-bearing: without it the resolver re-returns the tier just declined and the offer modal reopens immediately, so a naive fix would have replaced a dead end with a loop.
Added
scripts/test-onboarding-ladder.js— 12 checks covering tier routing, the decline-skip path (including an explicit assertion of the loop hazard) and the overlay guard. Runs the shipped browser IIFE in a vm sandbox against a stub DOM, so it tests real code rather than a copy.
Notes
- Tier 3 (coursework) remains student-only by design (
stage4-onboarding.js), so teachers and professionals see a two-rung ladder. Worth knowing when reading "3/3" in support reports.
[4.28.0] - 2026-08-18 - B2B organisation layer
Security
- Anyone who knew an org slug could buy credit into that org at its discount.
createCheckout's org branch (api/stripe-routes-enhanced.js) checked only that the named organisation existed and had 3+ members — never that the buyer belonged to it. Its sibling branch had always checked. WithdiscountPctcapped at 86%, this was a live pricing hole. Now both branches assert membership. - Stripe webhook replays double-credited CVT.
checkout.session.completedrancreateBatch+addCVTunconditionally; onlyinvoice.paidguarded against redelivery. Astripe-events/{event.id}claim document, created transactionally before any branch runs, now covers every event type at once. - Self-provisioned developer-portal orgs are now terminal (
status: 'api_only'). Loading/api/external/v1/portalstill mints one, but such an org can never acquire seats, members, a white-label portal or branding — those need an application an admin has approved.
Added
- Organisation accounts — a per-seat B2B layer for schools, tutoring agencies, student societies and teacher training programmes. All four run identical code;
orgTypedrives copy and defaults only, never permissions. org-memberships/{uid}— membership moves out of theusers/{uid}.untxt.orgSlugscalar into a collection keyed by uid, written witht.create()inside a transaction. A second concurrent membership now fails inside Firestore rather than depending on an application check against a fieldcreateOrUpdateUserrewrites on every login.orgs.members[]anduntxt.orgSlugremain as read caches and no longer authorise anything.- Per-member CVT allocation (
utils/org-allocation.js) — a manager moves pooled credit into member-scoped batches under a neworgmem_<slug>_<uid>owner prefix, reusing the existing batch machinery (FIFO by expiry,cvt-movementsaudit trail) rather than a parallel ledger. Allocations inherit the pool's expiry, so moving credit never extends its life. Offboarding expires a member's allocation in the same transaction that deletes their membership, so nobody leaves holding spendable org credit. - Org-context consumption —
consumeWithOveragetakes an optional org context; work in the portal spends the member's allocation and stops there (402allocation_exhausted) rather than reaching for the org's metered overage, which is a manager-level decision. Without org context it touches nothing but personal balance. TheX-UnTXT-Orgheader signals intent only; the org used is resolved from the caller's own membership document. - Split history — analyses carry
orgSlugandcontext; personal history filters tocontext === 'personal', the portal to its own org. - Self-serve signup at
/org-signupbehind manual admin approval. Card captured with a SetupIntent and left uncharged, invitations written but held unsent, portal 404ing — until an admin approves, which starts billing and flushes invites in one idempotent step. - Manager console at
/org-console— allocate credit, activate/deactivate, invite, seats, branding, billing. Deliberately shows counts, credit and last-active and nothing else: no document titles, no scores, no text. - Co-branded portal at
/org/<slug>— organisation logo and colour applied client-side (followingapp-settings.js'ssetPropertyprecedent; there is no server-side templating). The UnTXT mark is always present and there is no field in the branding payload capable of hiding it. - Logo upload as JSON base64, so
parseBodystays string-only and no multipart parser enters the HTTP layer. Magic bytes are sniffed rather than the declared content type trusted; SVG is refused outright as a script container. Accent colours are checked for WCAG AA contrast in both themes and a rejection suggests the nearest passing shade. org-audit— append-only trail with no exposed delete path (accurate wording:firebase-adminbypasses security rules, so "immutable" would be a lie). Covers every manager and admin action, and surfaces on the organisation's own console — including the times UnTXT looked.- Read-only admin inspection (
GET /api/admin/orgs/:slug/inspect) for malpractice and whistleblower reports. Areasonis mandatory and recorded. Shows exactly what the org's own manager sees; analysis text stays encrypted and is not decrypted here. There is deliberately no impersonation, which is what keeps a manager's action provably theirs. - Invitations bound to a single email address and consumable once — the check that stops one person collecting allocations through several Google accounts.
- Organisation agreement (
legal/org-agreement.md→/org-agreement, generated byscripts/generate-org-agreement.js), click-wrap gated byORG_AGREEMENT_VERSION. scripts/backfill-org-memberships.js(dry-run by default) andscripts/test-org-invariants.js(36 offline checks)./docs/organisationsdocumentation page.
Changed
- Org billing is now per-seat; the
starter/growth/scaletiers are gone. They priced a CVT bucket with no relationship to headcount. Seats are graduated, not volume-banded: under volume pricing a 50-seat plan (50 × £10.50) cost less in total than a 49-seat one (49 × £11.70), a cliff no threshold choice removes while the discount is meaningful. The Stripe price isbilling_scheme: 'tiered',tiers_mode: 'graduated', one per currency. Rates: £11.70/seat for the first 49, £10.50 for 50–249, £9.30 from 250 — the entry rate being exactly 10% below the individual Professional plan. 1,300 CVT per seat, matching the Professional allowance, so a seat is never a downgrade. Margin at 100% pool use stays above 66% at every band against a blended serving cost of £0.00235/CVT (the existing £0.001343 floor, uplifted 6× for the 15% of volume assumed to run through M13). - Billing, API keys and cancellation now require
role: 'manager'. Previously any member could do all three —api/org-subscription-routes.jssaid so in its own doc comment. The backfill assigns the role fromownerUidand lists domain-provisioned orgs (deliberately ownerless) for an admin to resolve. - Payment failure now walks a lazily-evaluated ladder (
utils/org-state.js): grace → frozen → suspended → deactivated, derived purely from stored timestamps. No scheduler is involved, because this repo has none.paymentFailedAtis stamped once so Stripe's retries cannot restart the clock. Members' personal accounts are untouched at every rung. getAnalysisHistory(uid, scope)takes an optional scope; existing documents, which have neither field, stay in the personal list.
Notes
- Requires
FIREBASE_STORAGE_BUCKETand Storage enabled in the Firebase console before logo upload works.firebase-adminalready ships the Storage client, so there is no new dependency and nopackage-lock.jsoncoordination. - Run
node scripts/backfill-org-memberships.js(review the conflict report, then--commit) before this ships to any account with existing orgs.
The complete history, including everything older than this, lives in the changelog.