GxPlex

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

  1. Each observation carries a SHA-256 of the normalised text of the source on that day — in observations.json and on the instrument page.
  2. Fetch the source yourself and compare, or compare against the retained snapshot where one exists.
  3. 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.

The four rights fields over the tracked entries
FieldCount OfWhat 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.

What each licence position permits a consumer
ValueCountWhat 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.

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])
EOF

It 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.crt

Granularity. 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.ots

This 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: