diff --git a/apps/loopover-ui/content/docs/what-you-can-verify.mdx b/apps/loopover-ui/content/docs/what-you-can-verify.mdx
index 63785eb4c..939d99d78 100644
--- a/apps/loopover-ui/content/docs/what-you-can-verify.mdx
+++ b/apps/loopover-ui/content/docs/what-you-can-verify.mdx
@@ -18,7 +18,9 @@ the assumptions and the gaps are what let you decide whether the guarantee is wo
### 1. Gate-decision integrity
-**Claim:** the sequence of decisions was not silently reordered, deleted, or rewritten.
+**Claim:** the sequence of decisions was not silently reordered, deleted, or rewritten — and, since
+external anchoring shipped, that a *wholesale* chain rewrite is independently catchable too, not
+only tampering within it.
Every persisted verdict appends to a hash-chained ledger — each row's hash covers the previous row's
hash, so any edit to history breaks the chain at a point you can locate.
@@ -31,11 +33,89 @@ Returns `{ ok, checked, nextAfterSeq, tipSeq, tipHash, totalCount }`, and a `bre
`409` status if the chain is inconsistent. No API key — **anyone** can run it.
- **Trust assumption: tamper-evident, not tamper-proof.** This detects sequence gaps, predecessor
- and row-hash mismatches, truncated tails, and records whose content no longer matches their
- chained digest. It does **not** detect an operator deleting the chain wholesale and re-chaining
- from genesis — catching that requires an external anchor the operator does not control, which is
- tracked but **not built today**.
+ **Trust assumption: tamper-evident, externally anchored.** The check above still catches sequence
+ gaps, predecessor and row-hash mismatches, truncated tails, and content drift exactly as it always
+ has. A scheduled job additionally publishes a *signed*, self-describing checkpoint of the chain's
+ tip — hourly, or every 256 new rows, whichever comes first — to two places the operator does not
+ control: a Sigstore Rekor transparency log, and a git commit cross-mirrored by GH Archive and
+ Software Heritage the moment it's pushed. Rewriting history *back past* an anchor already
+ published before the tamper now means forging that signature or fabricating matching evidence at
+ an external mirror too — not just editing this repo's own tables. **What this does not close:** a
+ rewrite made *since* the last anchor, followed by ordinary appends afterward, gets silently
+ absorbed into every anchor published from then on — see "What you cannot verify" below for the
+ precise boundary. Anchoring bounds how far back an undetected rewrite could reach; it does not make
+ every row individually external-checkable in real time.
+
+
+**Verify an anchor end to end**
+
+a. Fetch the most recent anchor and the currently-published signing key:
+
+```bash
+curl -s "https://api.loopover.ai/v1/public/decision-ledger/anchors?limit=1" | jq '.anchors[0]' > anchor.json
+curl -s "https://api.loopover.ai/v1/public/decision-ledger/anchor-key" | jq -c '.keys' > anchor-keys.json
+```
+
+b. Fetch the actual signed payload the anchor committed to. For the git-commit backend,
+`anchor.json`'s `backendRef` names the exact commit — the JSONL line it appended *is* the signed
+artifact:
+
+```bash
+OWNER=$(jq -r '.backendRef.owner' anchor.json)
+REPO=$(jq -r '.backendRef.repo' anchor.json)
+SHA=$(jq -r '.backendRef.sha' anchor.json)
+FILE_PATH=$(jq -r '.backendRef.path' anchor.json)
+curl -s "https://raw.githubusercontent.com/$OWNER/$REPO/$SHA/$FILE_PATH" | tail -n 1 > anchor-signed.json
+```
+
+For the Rekor backend, `backendRef` instead names a transparency-log entry (`shardBaseUrl`, `uuid`)
+— verify with `rekor-cli verify --rekor_server "$SHARD" --uuid "$UUID"`, or fetch the entry directly
+and decode its `hashedRekordRequestV002` body.
+
+c. Verify the signature offline, against the published key — zero contact with LoopOver required:
+
+```bash
+npx tsx -e '
+import { readFileSync } from "node:fs";
+import { verifyLedgerAnchorSignature, anchorKeyById, parseAnchorPublicKeys } from "./src/review/ledger-anchor.ts";
+(async () => {
+ const signed = JSON.parse(readFileSync("anchor-signed.json", "utf8"));
+ const keys = parseAnchorPublicKeys(readFileSync("anchor-keys.json", "utf8"));
+ const key = anchorKeyById(keys, signed.keyId);
+ console.log(key && (await verifyLedgerAnchorSignature(signed, key.publicKeySpki)) ? "signature OK" : "SIGNATURE INVALID");
+})();
+'
+```
+
+d. Bind the anchor back to the *live* chain: fetch the row at the anchored `seq` and recompute its
+hash yourself:
+
+```bash
+SEQ=$(jq -r '.payload.seq' anchor-signed.json)
+curl -s "https://api.loopover.ai/v1/public/decision-ledger/row/$SEQ" | jq > row.json
+npx tsx -e '
+import { readFileSync } from "node:fs";
+import { ledgerRowHash } from "./src/review/decision-record.ts";
+(async () => {
+ const row = JSON.parse(readFileSync("row.json", "utf8"));
+ const signed = JSON.parse(readFileSync("anchor-signed.json", "utf8"));
+ const recomputed = await ledgerRowHash(row.prevHash, { seq: row.seq, recordId: row.recordId, recordDigest: row.recordDigest, createdAt: row.createdAt });
+ console.log(recomputed === row.rowHash && row.rowHash === signed.payload.rowHash ? "row hash OK -- anchor matches the live chain" : "MISMATCH");
+})();
+'
+```
+
+A mismatch here — a live row whose hash no longer matches what was anchored — is exactly what a
+wholesale rewrite before this checkpoint would produce. It's public, and anyone can check it, without
+asking LoopOver anything.
+
+
+ **Bittensor on-chain anchoring is optional, separate corroboration — not part of this default
+ check.** A third, Gittensor/SN74-audience-specific backend (tracked, not yet
+ shipped — [#9277](https://github.com/JSONbored/loopover/issues/9277)) publishes the same checkpoint
+ as an on-chain commitment, signed by a dedicated hotkey run on the operator's own infrastructure.
+ It's additive corroboration for that specific audience, never folded into the default two-backend
+ claim every verifier above is told to check.
### 2. Decision-record authenticity
@@ -114,7 +194,7 @@ Stated plainly, because a boundary you discover later is worse than one publishe
| --- | --- | --- |
| **Live gate execution** | The merge/close calls acting on your PR are not attested. Putting an attestation service in the live request path trades real availability for a proof that replay already provides more cheaply. | Standing design, not a pending gap. Revisited only if a tenant contractually requires attested live decisions. |
| **Ground-truth honesty** | Accuracy numbers are scored against recorded human-override events on maintainer infrastructure. Attestation proves *computation*, not *data provenance* — a perfectly attested run over cherry-picked labels is still cherry-picked. | Provenance at capture time (receipts verifiable by the humans whose overrides they record). Different mechanism entirely. |
-| **Wholesale ledger replacement** | Row 1's limit — self-operated chains are tamper-evident against everyone except the operator. | External anchoring: signed checkpoints, a transparency log, or an on-chain commitment. |
+| **A rewrite made since the last anchor, then re-anchored consistently** | An anchor proves *a* checkpoint existed at *a* time — it has no independent way to know what the tip *would have been* absent tampering. A rewrite to rows since the last published anchor, followed by ordinary appends afterward, gets silently absorbed into every anchor published from then on. Row 1's walkthrough only catches a rewrite that reaches *back past* an anchor already published *before* the tamper happened. | Tighter cadence (currently hourly or every 256 rows, whichever first) shrinks the window of opportunity; it cannot close it fully — that would mean anchoring every single write, at which point it stops being periodic checkpointing at all. |
| **Model behavior** | Row 5's limit — no artifact makes a non-deterministic model reproducible. | Nothing planned; this is a property of the models, not of our record-keeping. |
## Self-hosting versus hosted
diff --git a/migrations/0180_decision_ledger.sql b/migrations/0180_decision_ledger.sql
index a2651cd5e..f1f57d283 100644
--- a/migrations/0180_decision_ledger.sql
+++ b/migrations/0180_decision_ledger.sql
@@ -6,10 +6,16 @@
-- Every persistDecisionRecord write appends (including latest-finalize-wins rewrites of the same record id
-- -- supersessions are deliberately VISIBLE history, not silent replacement).
--
--- HONEST LIMIT (module header repeats this): a self-operated chain is tamper-EVIDENT, not tamper-PROOF --
--- the operator can still rewrite wholesale. External anchoring (signed checkpoints / witness cosigning) is
--- the tracked follow-up once tenants exist, per the epic's sequencing. That gap does not reduce the value
--- against every OTHER actor, or against accidental corruption.
+-- HONEST LIMIT (module header repeats this; see migrations/0195_decision_ledger_anchors.sql, #9267): a
+-- self-operated chain is tamper-EVIDENT against every actor except the operator, on its own. As of #9267, a
+-- scheduled job (src/review/ledger-anchor-scheduler.ts) additionally publishes a SIGNED, self-describing
+-- checkpoint of this chain's tip -- hourly, or every 256 new rows, whichever comes first -- to two places the
+-- operator does not control: a Sigstore Rekor transparency log and a git commit (cross-mirrored by GH Archive
+-- / Software Heritage the moment it's pushed). Rewriting history before the oldest still-referenced anchor
+-- now requires forging that signature or fabricating matching evidence at an external mirror too, not just
+-- editing this table. What remains open: the UNANCHORED TAIL since the last checkpoint is exactly as
+-- tamper-evident-only as before anchoring existed -- anchoring bounds how far back an undetected rewrite
+-- could reach, it does not make every row individually external-checkable in real time.
CREATE TABLE IF NOT EXISTS decision_ledger (
seq INTEGER PRIMARY KEY, -- explicit, contiguous (verified); NOT autoincrement -- gaps are breaks
record_id TEXT NOT NULL, -- decision_records.id at append time
diff --git a/src/review/decision-record.ts b/src/review/decision-record.ts
index 9db79b535..858731486 100644
--- a/src/review/decision-record.ts
+++ b/src/review/decision-record.ts
@@ -13,12 +13,16 @@
// replay" is structurally impossible.
//
// HONEST LIMIT (#9122, mirrored from migrations/0180_decision_ledger.sql's own header): the hash-chained
-// ledger below makes this instance's history tamper-EVIDENT, not tamper-PROOF — an operator with direct DB
-// access can still rewrite the chain wholesale (delete every row, recompute a fresh one from genesis) and
-// nothing here can detect that from first principles. External anchoring (a signed checkpoint published
-// somewhere the operator does not control — a git commit, a transparency log, an on-chain commitment) is the
-// tracked follow-up once tenants exist, not yet built. That gap does not reduce the value against every OTHER
-// actor (a maintainer quietly deleting one disputed decision, or an unprivileged bug), or against accidental
+// ledger below makes this instance's history tamper-EVIDENT against every actor except an operator with
+// direct DB access, on its own — such an operator could still rewrite the chain wholesale (delete every row,
+// recompute a fresh one from genesis) and nothing INTERNAL to this table can detect that from first
+// principles. As of #9267, external anchoring closes most of that gap: a scheduled job (ledger-anchor-
+// scheduler.ts) publishes a signed checkpoint of the tip to a Rekor transparency log and a git commit (cross-
+// mirrored by GH Archive / Software Heritage) that the operator does not control — rewriting history before
+// the oldest still-referenced anchor now means forging that signature or fabricating matching external
+// evidence too. The gap that remains: the unanchored tail since the last checkpoint is exactly as tamper-
+// evident-only as before anchoring existed. None of this reduces the value against every OTHER actor (a
+// maintainer quietly deleting one disputed decision, or an unprivileged bug), or against accidental
// corruption — both of which the chain below still catches deterministically.
//
// #9124 (v4): three of the four commitments this record makes did not commit to what actually decided the
@@ -395,7 +399,8 @@ export type LedgerBreak =
* record (see below) — and the cursor for the next window. Always returns the CURRENT global tip
* (`tipSeq`/`tipHash`) and total row count, regardless of where this window's pagination stopped, so a
* third-party checkpoint-keeper can compare it against whatever tip it last observed (#9122 — the exact shape
- * a future external-anchoring job would need). #9078: also reconciles each row against `decision_records` —
+ * the scheduled anchoring job, #9274, now consumes via {@link loadDecisionLedgerTip}). #9078: also reconciles
+ * each row against `decision_records` —
* recomputing `contentDigest(JSON.parse(record_json))` and comparing it to the digest the chain itself
* committed to, so a rewrite of `record_json` that left `decision_records.record_digest` untouched (or vice
* versa) is caught here instead of only being provable by an external challenger who happens to still have the