Developers
For systems that read this archive rather than people. What the identifiers guarantee, the cadence the data moves at, the form a citation takes, the path by which a value is checked, and which entries carry text that may travel.
The record publishes what official sources published, when, how it relates, and what is forming — and never what should be done about it.
Identifiers
Every tracked entry has a slug — eu-gmp-annex-11 — and that slug
is its identifier at /r/<id>/ and in every API
response. A published URL is treated as a citation here: it keeps resolving,
directly or by redirect, and scripts/test_url_stability.py
fails the build if one stops. When an entry changes kind — two moved from
instruments to bodies on 2026-08-02 — the old URL redirects rather than
disappearing.
The v1 contract
Additive only. A field is never removed and never re-typed;
a new field or a new endpoint may appear. A consumer that reads fields it
knows and ignores the rest will not break. Where a value's MEANING changes,
a correction is logged and the feed carries it —
that has happened twice, and both entries are public on
corrections. The third surface this sentence
used to name, /changelog, was retired on 2026-09-06 and 301s to
changes; nothing it carried about a document
left the site with it.
/api/v1/instruments.json ·
observations.json ·
citations.json ·
crawlability.json
The observation spec — GXP-OBS v1
GXP-OBS v1 states what an observation IS here: every
field, what it records, what a reader may not conclude from it, and what its
absence is. The machine-readable document is one JSON file at a versioned
address — /api/v1/schema/gxp-obs-v1.json
— carrying x-gxplex-does-not-claim and
x-gxplex-absent-means on every property. A validator ignores
both; a consumer reads them.
It is not the same document as
/api/v1/schema/series.json,
which is the JSON Schema for the same records and states shape alone. A
record can validate perfectly and still be read as a claim it does not make:
that a digest covers a document rather than a normalisation of one, that a
null digest is an empty document rather than an absent measurement, that a
run of unchanged digests is a document's history rather than one address's.
The spec is where those readings are refused.
A later spec version takes a later address and this one keeps resolving. The
spec version, the API contract version and a record's own
schema_version are three numbers that move separately.
/api/v1/series.jsonl —
the observation series: what a URL returned on a date. ·
probe.jsonl —
the probe matrix: how a named client was answered, which is transport
measurement rather than document observation.
The controlled topic identifiers, and the rule that forms them, are on conventions.
Model Context Protocol
A Model Context Protocol server is kept in this repository at
mcp/. It serves the same data as the JSON routes above and
exposes nothing they do not: search_sources,
get_source, get_history,
get_changes, get_change_evidence,
get_calendar, get_relations,
get_integrity, get_neighborhood and
get_signals. Every one is read-only.
It is documented here and is listed in no registry and submitted nowhere. Running it is a reader’s own act against a local checkout; this site serves no endpoint for it, and a rights field on an object reached through it means what it means on the same object reached through the JSON.
Three worked reads
Read-only, no key, no account. A consumer identifying itself honestly and staying under a request a second is asking of this record what this record asks of the sources it observes.
Changes since a date
# curl
curl -s 'https://gxplex.com/api/v1/events.json' | jq '[.items[] | select(.occurred_on >= "2026-09-01")]'
# python3 (standard library only)
import json, urllib.request
u = "https://gxplex.com/api/v1/events.json"
req = urllib.request.Request(u, headers={"User-Agent": "your-tool/1.0"})
doc = json.load(urllib.request.urlopen(req))
since = [e for e in doc["items"] if e["occurred_on"] >= "2026-09-01"]
// typescript
const r = await fetch("https://gxplex.com/api/v1/events.json");
const doc = await r.json();
const since = doc.items.filter((e: any) => e.occurred_on >= "2026-09-01");Sources by lens
A lens is the same object the board encodes in its own URL, so a view a person shares and a query a machine sends are one grammar.
# curl
curl -s 'https://gxplex.com/api/v1/instruments.json?lens=jurisdiction:EU'
# python3
import json, urllib.parse, urllib.request
q = urllib.parse.urlencode({"lens": "jurisdiction:EU"})
u = f"https://gxplex.com/api/v1/instruments.json?{q}"
doc = json.load(urllib.request.urlopen(
urllib.request.Request(u, headers={"User-Agent": "your-tool/1.0"})))
// typescript
const u = new URL("https://gxplex.com/api/v1/instruments.json");
u.searchParams.set("lens", "jurisdiction:EU");
const doc = await (await fetch(u)).json();Compare two dates
Rights-gated: where this record does not retain a document’s text, the response is a refusal naming the condition that refused it, never an empty difference. An empty difference and a comparison that could not be made are opposite facts.
# curl
curl -s 'https://gxplex.com/api/v1/compare.json' | jq '.items[] | select(.id == "eu-gmp-annex-11")'
# python3
import json, urllib.request
u = "https://gxplex.com/api/v1/compare.json"
doc = json.load(urllib.request.urlopen(
urllib.request.Request(u, headers={"User-Agent": "your-tool/1.0"})))
row = next(i for i in doc["items"] if i["id"] == "eu-gmp-annex-11")
// typescript
const doc = await (await fetch("https://gxplex.com/api/v1/compare.json")).json();
const row = doc.items.find((i: any) => i.id === "eu-gmp-annex-11");Cadence
Sources are fetched and hashed daily, and the hash is recorded whether or not anything changed — "unchanged for N days" is only a fact if every one of those days was observed. Statuses change only when a person confirms one against the primary source. There is no undertaking about when a run happens, and a day with no run is visible as a gap in the series rather than smoothed over.
This record is pull, not push. There are no webhooks, no alerts and no subscriptions that trigger on a change, and that is a boundary rather than an unbuilt feature: a subscription is a consumer record, and this record collects no consumer data of any kind — no accounts, no cookies, no client-side storage, no analytics. A surface that notified anyone would first have to hold who they are.
What is served instead is the whole series, on demand and without identification: the changes feed, the calendar, and the observation and instrument endpoints. Polling any of them returns the same bytes to everyone, and the record does not learn that it happened.
Citing a value
A value here is a reading of a source on a date, so a citation needs both:
GxPlex, "<short title>" (<instrument id>), status <status>
as recorded on <YYYY-MM-DD>. https://gxplex.com/r/<id>/Citing the status without the date cites a fact that has no owner: this record's claim is always about a day.
Checking a retrieved claim
- Each observation carries a SHA-256 of the normalised text of the source
on that day — in
observations.jsonand on the instrument page. - Fetch the source yourself and compare, or compare against the retained snapshot where one exists.
- Recompute the day's integrity root from the published leaves and check it against the published root: the command is on /verify, and it is run against the shipping bytes on every build.
These are hashes, not signatures. They prove the published data is the data the root was built from. They do not prove who published it, because no signing key exists yet — a fact /verify states in its own words. When one does, the manifest carries a detached signature and this step becomes a signed hash. A consumer describing values from here as cryptographically attributable would, today, be claiming something this record does not yet support.
Licence — which text may travel
Every row carries four rights fields, so a pipeline inherits the boundary
instead of discovering it: licence_class,
text_retained, quote_allowed and
snapshot_available. Where a row names no tracked entry all four
are null and the row says why — four nulls record the absence of
a reading, never a reading that came back negative.
The two retention fields are not the permission field.
Retention is not publication: this record retains normalised prose for
documents whose text it never publishes, and on
32 of
132 tracked entries
text_retained is true while quote_allowed is
false. A consumer reading the first alone and inferring a quotation was
available would be wrong on exactly those.
quote_allowed records whether a quotation from the document
appears in this record's own published output. It is a fact about this
record's handling, not a reading of what a publisher permits, and it is not
a grant: treating it as permission to quote elsewhere adds a claim this
record did not make.
| Field | Count | Of | What the count is |
|---|---|---|---|
quote_allowed true | 99 | 132 | Entries whose text this record quotes, each quotation citing the surface it was read from. |
quote_allowed false | 33 | 132 | Entries whose text appears nowhere here. Metadata, dates, a digest and a link to the publisher. |
text_retained true | 126 | 132 | Entries holding normalised prose a quotation can be checked against here. Held, never served. |
snapshot_available true | 126 | 132 | Entries holding a retained snapshot of a retrieval, so a digest can be re-checked here. No snapshot is served. |
| snapshot without prose | 0 | 132 | Entries where a snapshot is held and no prose came out of it. The two retention fields agree today and are two measurements; this cell is where they would part. |
| no reading recorded | 0 | 132 | Entries carrying four nulls. An absent reading, never a reading that came back negative. |
Every row also carries robots_policy: how this archive's fetch of
that row was permitted. Absent means the ordinary case — a robots file was
read and allows the path. A non-default value names the state and its
receipt, including absent-by-confirmation, where a host
publishes no robots file at all and the page's own meta-robots was read
before its content was believed.
Every row also carries record_tier. It records what this
archive undertakes for a class of entries and asserts nothing about what has
happened to any one of them: for core, this archive undertakes
to put an entry's status to a person; for register, no
person-verification is promised. Neither value is a ranking, and neither is
derived from an entry's content: register is written on the
row, and core is what the schema records where a row writes no
value. What happened to a row is in that row's own fields and its
observation records. This endpoint emits last_verified,
first_observed, checked_by and
check_date.
checked_by is the ACT, not a rank: human where a
person confirmed the status, machine where a tier method read
it. check_date is the day that act was recorded. Neither is a
status and neither is a verification — a row whose status is
unknown still carries them, and machine must not
be read as a person's confirmation.
first_observed is the earliest observation date in THAT ROW's
own series — a fact about this archive's window on the row, not about the
document. It is null where a row holds no observation yet,
which is a real state between admission and first fetch, and it is never
filled from the corpus start date: a row is not observed by its
neighbours.
| Value | Count | What it means here |
|---|---|---|
open, text retained | 81 | A snapshot and a hash exist. Quotations can be checked against retained bytes. |
restricted | 33 | The document's text is not reproduced or retained here, and none is available through this record. Metadata and a link only — a consumer that generates text for these is not quoting this archive. |
text_retained: false | 48 | Nothing was hashed, so there is nothing here to verify a quotation against. The entry states why on its own page. |
What this record warrants, and what it does not
Integrity is provable. That the published data is the data the root was built from, and that a document's text on a given day hashed to a given value, are things a consumer can check without trusting this site — the arithmetic is published and so are the inputs.
Accuracy is not a cryptographic property. A status read wrongly hashes exactly as well as one read correctly. Accuracy lives in the reading, in the human check, and in the corrections log, which is additive and never silently edits. "Verified" carries exactly one sense here — checked against the primary source by a person, on a stated date — and it is the only sense this record can support for a value taken from it.
This record makes no assessment of what any document requires of anyone. A consumer that presents its contents as a compliance obligation has added a claim that is not here.
Found something wrong? corrections@gxplex.com. Method: how this record is maintained.
The full record
These are audit surfaces. They are live and permanent, and they are not indexed or linked from the reader's navigation — they exist so a claim on this site can be checked, not to be browsed.
- Full internal log — the 120 entries this record no
longer publishes to readers (counting rules, template repairs, figures,
quoted spans, gate work) are retained in the repository history, in the
commit that wrote each one. There is no machine endpoint for them: no
API response has ever carried the changelog, so removing the page breaks
no contract.
git log --follow src/content/changelog/reads the whole of it. - Corrections — every correction to a published claim about a document, dated, with what it corrected
- Integrity record — the Merkle root, the timestamps, and the commands that check them without anything of ours
- Archive index — every recorded document version, with its digest
- Sources — every address this record reads
Verifying the record yourself
Carried here from the retired /verify page on 2026-09-05. These
are the three commands that check this record without trusting it: recompute
the Merkle root from the published leaves, check the timestamp authority's
signature over that root, and check the OpenTimestamps attestation. Each
needs the published files and nothing from this site's code.
The integrity record
- Leaves in the tree
- 5393
- Fixed at
- 00:00:00Z — the day is this record's unit, so the time is a constant, not a clock reading
The root is a SHA-256 Merkle tree over sorted
id|date|content_hash lines. An odd node at the end of a level
is promoted, never paired with itself.
Read from state/integrity.json, written by
scripts/integrity.py at build time.
fd0980cf7ca42518ff7457b92b075df16163772d48d6f75e546b4869fe7bedab
The lines are distinct: 11953 records in
the series carry both a hash and a date, and they reduce to 5393 lines,
because 6560 of them repeat an
id|date|content_hash a line already covers. A source
re-fetched on a day it has already been fetched, returning the same
bytes, adds no line — there is no further state to fix in time.
What is in the file, and what is not. The first field is the id an observation was recorded under. Of the 132 instrument rows this record publishes, 126 contribute at least one line and 6 contribute none: a line stands for retained bytes. All 6 have been observed, and no observation of them has retained content.
A further 5 ids in the file account for 282 lines: sources this record observes without publishing an instrument row. The file is a record of observations that retained content, so it is neither a list of this site’s pages nor a census of the rows it publishes.
Recompute the root from the leaves
python3 - <<'EOF'
import hashlib
L = [l.strip() for l in open('leaves.txt') if l.strip()]
h = [hashlib.sha256(x.encode()).hexdigest() for x in L]
while len(h) > 1:
n = [hashlib.sha256((h[i] + h[i+1]).encode()).hexdigest()
for i in range(0, len(h) - 1, 2)]
if len(h) % 2:
n.append(h[-1])
h = n
print(h[0])
EOFIt prints the published root, or one of us is wrong — and the published data is the evidence for which, not this sentence.
Check the timestamp authority's signature
Authority time: . The token records the authority's exact instant, to the second. This page shows the date; the instant is in the token, and anyone verifying the proof reads it there. Nothing is withheld — the artifact is published unchanged, and the command below prints the time it carries.
openssl ts -verify -data root.txt -in root.tsr \
-CAfile tsa-cacert.pem -untrusted tsa.crtGranularity. One stamp per daily cycle, taken by the scheduled daily job and by nothing else — never per commit, never per working session. A completed proof names a Bitcoin block, and block times are public, so a proof discloses roughly when its stamp was taken. What that discloses here is the schedule, which is published on this site anyway. Everything this record states about its own activity is day-granular by rule.
Anchoring proves WHEN, not WHO. Both anchors here fix digests at times. Neither says who assembled them, whether the documents behind them were read correctly, or whether any person checked anything. The named-person verification this record describes elsewhere is a separate act, and it is still owed — no quantity of cryptography here discharges it.
Check the OpenTimestamps attestation
pip install opentimestamps-client==0.7.2
curl -O https://gxplex.com/integrity/root.txt
curl -O https://gxplex.com/integrity/root.txt.ots
ots verify root.txt.otsThis build does not run these lines. They install a client from PyPI, fetch this site over the network, and ask a Bitcoin block explorer for a block header — none of which a build of this site does.
Where each published reason is read from
Every reason the site gives for not tracking a document's text is read from a named field on that document's own record. The reader-facing pages give the reason in plain words; this is the field behind each, so a count can be checked against the store rather than taken on trust.
- text behind a licence
- Read from:
frozen_reason: paywalled_stage_tracked - published only within a listing
- Read from:
frozen_reason: listing_only - no public document
- Read from:
frozen_reason: no_public_document - the site refuses automated requests
- Read from:
frozen_reason: access_blocked - a register, which carries no status
- Read from:
frozen_reason: register_no_status
Last updated: