Release spec

Every report we count is in a public log. Every table we publish, and the number of reports it needs first, is written down below. This page is the short version first and the full specification after it.

What is published, and when

Only aggregate statistics. Individual reports are never displayed, shared, licensed, or given to researchers, in any form.

Thresholds. What is published about a compound depends on how many people have reported on it, so the number of published figures never gets close to the number of reports behind them (tiers):

  • Under 10 reports: nothing under the compound’s own name. It is counted with the other compounds in its class.
  • 10 or more: its report count, route of administration, and source type.
  • 50 or more: intended purpose, dose ranges, frequency, duration, purity testing, side effects, and whether people are still taking it, finished, or stopped early.
  • 100 or more: outcome by goal — of the people who took it hoping for X, how many reported no change, slight, moderate, or large improvement (T8).
  • 200 or more: when each side effect started and whether it stopped; and percentages, with 95% confidence intervals, added beside the counts for everything above. (Below 200 a percentage would mislead: on 40 reports its interval spans twenty points.)
  • 1,000 or more: outcome by goal shown side by side with other compounds used for the same goal (T9). Compounds are never combined into one number and never ranked.

Small groups are hidden. Any figure under 5 reports is hidden, and one neighbouring figure is hidden with it so the small one cannot be worked out by subtraction; both are labelled “not shown” (cell rules). Age and sex are reported only as totals across all contributors, never with any other answer.

Never. No ranking, no “most effective,” no comparison statistic between compounds, no average of any scale, no per-vendor or per-region breakdown, no individual row, no live count, no statistic on request (the full list; analyses we will never run).

Cadence. Releases are monthly for the first year, then quarterly. Each is cumulative, reviewed by a person, signed, and witnessed, and is never deleted or changed (cadence). A compound’s tables are recomputed only once at least 5 reports have been added or removed since they were last computed — 20 once it has cross-tabulated tables — and are otherwise carried forward unchanged and labelled with the release they are as of, so that subtracting one release from the next never reveals a smaller group than the tables themselves show (refresh floor). The rules themselves can change only by a versioned amendment announced a full release period in advance (amendment).

How we count reports

Every site that publishes a report count is asking you to take that number on faith. Ours is checkable.

The moment a report is stored, it is also committed to a public, append-only log: a hash of the report mixed with a secret random value, so the log reveals nothing about any report and no one — including the person who submitted it — can find a particular entry. With every release we publish the full list of log entries, the root that summarises them, and the previous release’s root. The release itself is signed, and its hash is recorded in two public transparency logs we do not control.

That lets anyone check, without trusting us: that the published count is the number of entries in the log; that nothing was removed, reordered, or backdated between releases; that the tables were computed from that log under rules fixed before the data existed; and that a release has not been quietly replaced since. Reports excluded from the tables for quality reasons are never deleted: they stay in the log and are listed, by position and reason, in every release (exclusions).

We cannot independently verify report counts published elsewhere, and we do not claim they are wrong. We can only say that ours is checkable — by you, with the steps below.

What this does not prove

  • That any report is true. Nothing can. A fabricated report is committed as faithfully as an honest one. What is guaranteed is that whatever was committed was counted, and nothing was altered afterwards.
  • That reports came from different people. Because no report is linked to a person, we cannot detect whether reports came from someone who previously submitted. This is an inherent downside to our privacy-first approach. What we can detect is a pattern: the coordinated-submission detector — a script in the public repository, tools/detect.py — flags reports that contradict themselves and unusual bursts, such as many reports about one compound arriving on one day. Flagged reports are left out of the tables and listed by log position in each release’s exclusion list, and each release page ends with an Integrity log section (table T14 in the spec) showing how many were flagged, why, and a note on any pattern found.
  • That a report was not dropped before commitment. The log proves what happened after a report entered it. The insertion policy is published: every submission that passes validation, the bot trap, and the hourly rate limit is committed, in the same transaction that stores it, with nothing held in between. A submission refused by the rate limit is shown an error and is not stored. You have to take that on the code, which is public, and on the operator.
  • That the published tables are the only computation that was run. The specification lists every table that will ever be published. Anything else computed privately is, by definition, not published — and the specification cannot be extended without a witnessed amendment one release period in advance.

Verify a release yourself

You need Python 3 and the release directory; the two external witnesses need openssl, rekor-cli and ots. One command runs every check — provided you hand it a copy of the signing key that did not ship inside the release (the repository’s releases/pubkey.pem, or a copy you saved earlier), because a key inside the release proves nothing:

python3 tools/verify_release.py releases/2026-12 --prior releases/2026-11 \
    --spec spec/RELEASE_SPEC.md --taxonomy spec/taxonomy.v1.json --pubkey releases/pubkey.pem

tools/verify_release.py is in the public repository, is short, and reads nothing but the files you give it. Each line of its output is one of the checks below.

1. The files are the files

release.json lists the SHA-256 of every other file in the release. Recompute any of them (sha256sum releases/2026-12/tables/T8.json) and compare. If they match, that table is byte-for-byte what was signed and witnessed.

2. The count is the count

merkle/leaves.txt is the full list of log entries at release time, one per line. The number of lines is the committed count in release.json and in table T0; the Merkle root recomputed from the lines equals the published root. The tree is the RFC 6962 construction — interior node = SHA-256(0x01 ‖ left ‖ right), a lone right-hand node promoted — and tools/pa_store.py has it in twenty lines.

3. Nothing was removed or backdated

The previous release’s leaves.txt must be an exact prefix of this release’s. If it is, every report committed as of the previous release is still there, in the same position, and everything since was appended after it. release.json records the previous release’s root as prior_root, so the chain can be followed back to the first release.

4. The rules were fixed first

release.json carries the SHA-256 of spec/RELEASE_SPEC.md and of spec/taxonomy.v1.json as they stood when the release was computed. spec/WITNESS.md will record the transparency-log entries for those documents, submitted before the first report is accepted. If the hashes match, the tables were computed under rules that predate the data.

5. The release was witnessed

witness/release.json.sig is a detached signature on release.json under the key in witness/pubkey.pem. The same key will be published at /releases/pubkey.pem and in the repository before the first real release; until then there is no signing key and nothing to check.

openssl dgst -sha256 -verify /path/to/your/copy/of/pubkey.pem -signature witness/release.json.sig release.json

witness/release.json.rekor.json names the entry in the Sigstore Rekor public transparency log holding that hash and signature — confirm it with rekor-cli get --log-index <log_index>. witness/release.json.ots is an OpenTimestamps proof anchoring the hash in the Bitcoin blockchain — confirm it with ots verify -f release.json witness/release.json.ots. Two independent logs, neither operated by us, each showing the hash existed no later than the recorded time. A release cannot later be quietly replaced: the replacement would not match the witnessed hash.

Checking what leaves your browser

Open your browser’s network panel before pressing Continue on any screen of the report form. Each request is a POST to /contribute whose body holds: at (which screen you are on), go (the button you pressed), s (every answer so far, as a base64url-encoded JSON object of vocabulary ids — decode it with any base64 tool), the choices you just made on that screen under their field names, and an always-empty field named website — the invisible bot trap, which your browser submits blank. Every value is a short id from the schema; none is anything you typed. There is no cookie, no request to any other host, and no request at all until you press a button. Nothing is stored on the server until the final Agree and submit.

Release spec

The full specification: every table that will ever be published and the rule for every cell. Fixed before the first report; changed only by witnessed amendment.

Version 1.0 — effective from the first release. This document enumerates every statistic that will ever be published from the submission store, and the rule that governs each one. It is fixed before the first report is accepted, its hash is submitted to a public transparency log, and it is changed only by a versioned amendment that is itself witnessed before it takes effect.

The purpose is that no one — including us — can decide after seeing the data which results to show. What is listed here is published on schedule regardless of what it says. What is not listed here is never published.

Definitions

  • Report — one row in the submission store, as defined in SCHEMA.md.
  • Release — a set of static files generated from the store at a point in time, reviewed by a person, committed with a signed tag, and witnessed.
  • Cell — one count in one published table.
  • Denominator — the number of reports a proportion is computed over.
  • Class — a mechanism class from SCHEMA.md, used to roll up compounds below threshold.
  • Group — a compound, a class, or all reports together: the unit whose tables the refresh floor (below) recomputes or holds as a whole.

Thresholds and disclosure tiers

Thresholds are fixed. They are not tuned after data exists.

The set of tables published for a compound grows with that compound’s report count, so that the number of published cells stays near or below the number of rows they are computed from. This is the control against reconstruction from aggregates: publishing roughly n or more accurate cells about n rows lets an adversary solve for the rows, and a single unlock threshold with the full table set would publish ~150 cells per compound at n=10.

Tier Compound n What is published under the compound’s name
0 under 10 Nothing. The compound contributes to its class tables only and is shown as “fewer than 10 reports.”
1 10–49 Report count (T0). Route (T1) and source channel (T2) as counts.
2 50–99 All remaining one-way tables as counts: dose bands (T3), frequency (T4), duration (T5), purity (T6), adverse effects (T10), status and stop reason (T12), primary goal (T16). (T7, handling, was removed before the freeze and its number is not reused.)
3 100–199 Outcome by goal (T8), for any goal stratum with at least 20 reports. Still counts only.
4 200–999 Percentages with 95% intervals on every table above. Adverse-effect onset and dechallenge (T11).
5 1,000+ The compound participates in the cross-compound outcome view (T9) for any goal where at least one other compound is also at tier 4 or above.

Class tables follow the same tiers using the class’s report count. Demographics (T13) are overall-only and appear once total reports reach 200.

Within every tier, these cell rules apply:

Name Value Governs
Cell suppression floor 5 Any cell with a count below 5 is hidden. Complementary suppression is applied so a suppressed cell cannot be recovered by subtraction from a published total; the complement is also hidden. Every hidden cell, floor or complement, carries the same label, “not shown” — never a number, and never a label that reveals which of the two is the small one — where “published total” means every marginal any table publishes: the table’s own n, and any subtotal or stratum size another table shows (the stopped subtotal and its complement for T12; the goal stratum sizes T8 shows, which is why T8 shows a stratum’s size only when T16 shows that goal).
Proportion display tier 4, and 100 A percentage is shown only when the compound (or class) is at tier 4 and the specific denominator is at least 100 — so a small goal stratum inside a large compound stays as counts. Every percentage carries a 95% (Wilson) interval and its denominator, rendered inside the figure.
Cross-tabulation 20 per stratum, 5 per cell A two-way table is published stratum by stratum: a stratum appears only when it has at least 20 reports, and is otherwise shown as “fewer than 20 reports.” Within a published stratum the cell floor and complementary suppression apply exactly as in a one-way table, with the stratum size as the published total.

Cells per row, by tier, for a typical compound: tier 1 publishes ~14 cells (mostly suppressed at the low end); tier 2 ~70; tier 3 ~95; tier 4 ~140. At each tier’s lower bound the ratio stays near or below 1.

A suppression floor is not a privacy guarantee — an adversary who can submit crafted reports can push a cell over any threshold. The floor is a readability convention and a first-line control. The real controls are the tiering above, that the set of tables is bounded, that no interactive querying exists, and that this specification cannot be extended on request.

Cadence and epochs

  • Releases are cumulative snapshots: each release recomputes every table from all reports received to date, minus the exclusion list — subject to the refresh floor below.
  • Cadence is monthly for the first twelve months after the first release, then quarterly. Monthly releases are identified YYYY-MM, quarterly ones YYYY-QN. The move to quarterly is stated here in advance and is not an amendment. The cadence may lengthen but not shorten without amendment.
  • A release is never deleted. A later release supersedes but does not remove an earlier one.
  • The exclusion list (see below) is applied identically to every table in a release, and the same exclusion list is used for every release computed on or after the date it was published.
  • There is no real-time feed, no live counter finer than the last release, and no interactive query interface. Published counts are as of the release date.

Refresh floor. Two releases can be differenced by anyone, and the difference between two cumulative snapshots is the distribution of whatever changed in between. A month in which one report on a compound arrives would otherwise publish that report’s answers, cell by cell, as the difference between two releases. So the tables for a group — a compound, a class, or all reports together — are recomputed only when at least 5 reports have been added to or removed from that group since the release that last recomputed them, or 20 once the group’s published tables include a cross-tabulated stratum (tier 2 and above): the smallest group those tables themselves may show. Otherwise the group’s tables and figures are carried into the new release unchanged, byte for byte, and labelled with the release they are as of. A group’s first publication is always a fresh computation. T0 counts are exact in every release regardless: a bare count identifies no one.

Groups nest — compounds in a class, classes in all reports — and a parent’s tables minus its published children’s tables is a table of the remaining rows (compounds below unlock; “other (not listed)”) that anyone can compute. So a class, or all reports, is also held whenever one of its published children is held with a change, and is recomputed only when the rows shown through it alone have changed by none or by at least 5. A compound’s first publication waits until at least 5 of its reports are newer than its class’s last recomputation, because its earlier rows were already visible, pooled, through the class.

The pipeline enforces all of this in one place (tools/refresh.py); release.json records, for every published group, the release that last recomputed it and the count it had then; tools/verify_release.py --prior checks that every held table and figure is identical to the prior release’s, and with the store re-runs the decision and requires the same answer.

This bounds what a difference between releases can reveal to the distribution of at least five, or twenty, changed reports per table. It is a stated limit, not a solved problem; the bounded table set and the tiers are what keep it bounded beyond that.

Exclusions

Reports flagged by the quality detector (malformed values, implausible combinations, coordinated submission patterns) are not deleted. They remain in the Merkle-committed submission log. They are listed, by log position, reason code and the date the exclusion was noted, in a separately published exclusion list (exclusions.json), and they are omitted from every table. The exclusion list is itself part of each release. Exclusions are proposed by tools/detect.py and applied by a person; the detector’s rules are in that file.

Reason codes: malformed · implausible · duplicate-pattern · coordinated · test.

The published submission count is always the count of all committed reports; the excluded count is shown beside it.

The tables

Every table below is computed for each compound at or above the compound unlock threshold and for each class. “Per compound” below means “per compound or class.” A compound below the unlock threshold contributes to its class’s tables and to no compound-level table.

T0 — Counts

Field Rule
Total reports committed Always published, with the Merkle root and log index
Reports excluded, by reason code Always published
Reports per class Published for every class
Reports per compound Published for compounds at or above unlock; below unlock, shown as “fewer than 10.” This is a presentation rule, not a suppression rule: class totals are exact, so a single hidden compound’s count within a class is inferable by subtraction. A bare count of reports on a compound identifies no one; complementary suppression protects cells within tables, not the T0 hierarchy.
Reports received since the prior release Published, as the difference between the two committed counts. (Committed counts are exact — they are what the Merkle root commits to — so this difference is exact by construction.)
Tables as of For each compound and class whose tables are held under the refresh floor, the release that last recomputed them.

T1 — Route

Per compound: distribution of route. Counts from the tier at which the table unlocks; percentages at tier 4.

T2 — Source channel

Per compound: distribution of source_channel. Counts from the tier at which the table unlocks; percentages at tier 4.

T3 — Dose bands

Per compound: distribution of start_dose and, separately, of current_dose. Counts from the tier at which the table unlocks; percentages at tier 4. Never published for a class — dose bands are compound-specific and a class rollup would be meaningless.

T4 — Frequency

Per compound: distribution of frequency (which includes cycling). Counts from the tier at which the table unlocks; percentages at tier 4.

T5 — Duration

Per compound: distribution of duration. Counts from the tier at which the table unlocks; percentages at tier 4.

T6 — Purity testing

Per compound: distribution of purity_tested. Counts from the tier at which the table unlocks; percentages at tier 4. Additionally, per class and overall: the same distribution.

T7 — (removed)

Handling/reconstitution was dropped from the schema before the freeze to shorten the form. The number is retained so table ids stay stable.

T8 — Outcome by goal

Per compound × goal: distribution of outcome (no change / slight / moderate / large) among reports that selected that goal. Tier 3 and above. Cross-tabulation rules apply: each compound–goal stratum needs ≥20 reports, each cell ≥5. Percentages at tier 4. “Other (not listed)” is counted as a goal but is never a stratum: there is no stated goal to condition on. A stratum’s size is shown only if T16 shows that goal’s count; a goal T16 hides (below the floor, or the complement protecting one that is) is shown here as “not shown”, whatever its size, because its size would give the hidden count away by subtraction from the compound total.

This is the efficacy instrument. It is presented as “of N people who took X hoping for Y, this many reported no change / slight / moderate / large.” It is never presented as an effectiveness score, a rating, or a rate.

T9 — Outcome by goal, across compounds

Per goal: the T8 rows of every compound at tier 4 or above that has a publishable stratum for that goal, side by side, stratified by compound and never pooled. The table for a goal is published only when at least two compounds qualify and at least one of them is at tier 5. Every number in it is already public in T8; the table adds juxtaposition, nothing else.

Tier 5. This is the only cross-compound view that will ever exist. It permits a reader to see that 40% of BPC-157 reports for tendon injury reported no change while 55% of TB-500 reports did, with the denominators beside each. It does not permit a ranking, a difference, a ratio, or a “better than” statement, and the page will not compute one.

T10 — Adverse effects

Per compound: for each adverse effect in the universal list and that compound’s list, the number of reports that selected it, over the compound’s total reports, plus one cell for reports that selected no effect at all. Cell floor 5. Percentages at tier 4. Effects below the cell floor are shown as “not shown.” Effects are multi-select, so cells do not sum to the total and no complementary suppression is needed.

T11 — Adverse effect onset and dechallenge

Per compound × adverse effect (only for effects at or above the cell floor in T10): distribution of onset; distribution of dechallenge. Tier 4 and above. Cross-tabulation rules apply.

T12 — Status and discontinuation

Per compound: distribution of status; among stopped reports, distribution of stop_reason. The stopped reports are a stratum (≥20, else “fewer than 20”), and their subtotal — and therefore its complement, the not-stopped subtotal — is a published total for complementary suppression in the status table: neither partition may end with exactly one hidden cell. Counts from tier 2; percentages at tier 4.

T13 — Demographics

Overall only, never per compound or class: distribution of age_band; distribution of sex; the proportion of reports that declined each. Published once per release.

Age and sex are never crossed with compound, goal, dose, or any other field. They exist to describe the contributor population as a whole and for no other purpose.

T14 — Integrity log

Per release: the number of reports flagged by each detector rule in the period, and a plain- language description of any coordinated-submission pattern detected, without any information that would identify the pattern’s rows. This is the running integrity log.

T16 — Primary goal

Per compound: distribution of goal — what people took it for — over the compound’s predefined goal list plus “other (not listed)”. Counts from tier 2; percentages at tier 4. This is the single source of exact goal counts: the stratum sizes T8 publishes at tier 3 are these same counts, and T8 shows a size only for goals shown here. When a complement must be hidden here, a goal with fewer than 20 reports (which T8 would not size exactly anyway) is preferred.

T15 — Uniqueness summary

Per release: the summary numbers from tools/uniqueness.py real run on the store at release time — the same five quasi-identifier sets as in SCHEMA.md, with percent unique and median cell size. Never any row-level output.

What is never published

  • Any table not listed above.
  • Any table crossing three or more fields.
  • Any table crossing age_band or sex with any other field.
  • Any table for a compound below the unlock threshold under its own name.
  • Any dose-band table for a class.
  • Any mean, average, or sum of an ordinal scale. Distributions only.
  • Any ranking, league table, “top rated,” “most effective,” or ordering of compounds by outcome.
  • Any difference, ratio, or comparison statistic between two compounds.
  • Any per-vendor, per-pharmacy, or per-brand breakdown. (No such field exists.)
  • Any per-region breakdown. (No such field exists.)
  • Any row, any partial row, any synthetic row, any row-shaped artifact.
  • Any count finer than the last release, on any page, including a live counter.
  • Any statistic in response to a request. Requests are answered by pointing at this document.

Presentation rules

These apply to every rendered figure and table:

  1. The denominator is rendered inside every figure, in the same image, not in surrounding text.
  2. Every percentage carries a 95% interval, rendered inside the figure.
  3. Every figure carries, inside the image, the sentence: “Self-reported. Not evidence of effectiveness or safety. See methodology.”
  4. Suppressed cells are shown as “not shown,” never blank, never zero, and never with a label that distinguishes a floor suppression from its complement.
  5. Compounds below unlock are shown as “fewer than 10 reports — not enough to show,” never hidden.
  6. No figure orders compounds by any outcome. Compounds appear in the fixed order of SCHEMA.md.
  7. Robust statistics only where a summary is unavoidable: medians and interquartile ranges, never means.

Release procedure

  1. The pipeline runs against the store and the current exclusion list. It is run by a person, by hand, on the host (the exact commands are in deploy/README.md), never on a schedule.
  2. It writes static files: one JSON per table, one rendered figure per table, the exclusion list, the integrity log, the full list of Merkle leaves in log order (merkle/leaves.txt, so that anyone can recompute the root and the count), and release.json carrying the release date, the schema version, this document’s version and hash, the Merkle root and leaf count, the prior release’s root, and the SHA-256 of every file. The pipeline first verifies the store against its own log and refuses to run if any row fails; it then audits its own output and refuses to write any count below the cell floor.
  3. A person reviews the output against this specification before anything is published.
  4. The files are committed under a signed tag release/<id> (YYYY-MM while monthly, YYYY-QN once quarterly).
  5. release.json is signed; its hash is submitted to the Sigstore Rekor public-good instance and timestamped with OpenTimestamps; the signature, public key, Rekor log index and entry UUID, and the upgraded .ots file are published alongside in witness/. release.json itself is never modified after signing.
  6. The release goes live. Nothing about a prior release changes. Anyone can check every step with tools/verify_release.py; see How we count reports.

Amendment

This specification may be amended only by:

  1. Publishing the proposed amendment, with a version increment, at least one full release period before it takes effect.
  2. Submitting the amended document’s hash to the transparency log on publication.
  3. Applying the amendment only to releases computed after the effective date.

An amendment may tighten a threshold or remove a table without notice. An amendment that loosens a threshold or adds a table requires the full period.

Witness

This document’s SHA-256 and its transparency-log entry are recorded in spec/WITNESS.md when it is witnessed, before the first release; that file lists every witnessed version.

Analyses we will never run

Rankings, comparisons, rates, causal language, individualized output — the list of refusals.

The release specification says what we publish. This page says what we will not compute, no matter who asks, because the data cannot support it and publishing it would mislead.

We will never rank compounds. No “most effective,” no “top rated,” no league table, no ordering by any outcome. Every compound appears in the same fixed order on every page.

We will never compare two compounds. No difference, no ratio, no “X worked better than Y.” The one cross-compound view we publish shows each compound’s outcome distribution side by side, stratified, with denominators — and computes nothing across them. The reader can look; the site will not conclude.

We will never publish an effectiveness score. There is no rating, no star, no percentage labelled “effectiveness.” There is a distribution of what people reported for a stated goal.

We will never use incidence or rate language. Not “the rate of nausea,” not “X% of users experience.” The denominator is people who chose to report, not people who used the compound. We say “of N reports, this many mentioned.”

We will never use causal language. Not “causes,” not “leads to,” not “results in.” A report that someone took a compound and something happened is a report of a sequence, not a mechanism.

We will never compare our numbers to a clinical trial, a regulatory database, or another site. The populations, the collection methods, and the biases are different in ways no adjustment at our scale can correct.

We will never publish a per-vendor, per-pharmacy, or per-brand breakdown. We do not collect the field. We will not add it.

We will never publish a per-region breakdown. We do not collect the field. We will not add it.

We will never cross age or sex with anything else. They describe the contributor population as a whole. They are not stratifiers.

We will never compute a mean of an ordinal scale. The outcome scale and the effect-onset buckets are ordered categories, not numbers.

We will never publish an individualized output. No “based on your profile,” no “people like you,” no calculator, no dose suggestion, no comparison against a reader’s own numbers.

We will never answer a data request with a new table. The set of tables is fixed and published. A request for something not on the list is answered by pointing at the list.

We will never run a statistic on a compound with fewer than ten reports under its own name. It appears inside its mechanism class until it has enough reports to stand alone.


Every one of these is a statistic someone will eventually ask for, and several are the ones that would get the most attention. That is why they are written down now, before there is data to be tempted by.