# ISCC Monitor
## Evidence for every ISCC-ID that you can check yourself
ISCC hubs record every ISCC-ID they issue in a public transparency log. The ISCC Monitor follows every hub's log
from the outside. It checks the signatures, checks that each log only grows, and keeps a complete copy. For any
ISCC-ID, it publishes evidence that you can check without trusting the hub or the monitor.
{ .iscc-lead }
[Check an ISCC-ID](guides/certificate.md){ .iscc-btn .iscc-btn--primary }
[How it works](concepts/how-it-works.md){ .iscc-btn }
*Open source · Apache-2.0 · maintained by the [ISCC Foundation](https://iscc.io)*
## Find your path
### Anyone with an ISCC-ID
Open the Certificate of Inclusion for an ISCC-ID. It shows which hub recorded the ID, where in the log, and since
when Bitcoin attests that it existed.
[Read a certificate →](guides/certificate.md)
### Developers and auditors
Download a proof bundle and verify it on your own machine. Check Bitcoin timestamps. Compare your view of a hub's
log against the monitor's.
[Verify a proof bundle →](guides/verify-bundle.md)
### Operators
Run your own monitor instance for mainnet or testnet from the container image, under your own name.
[Deploy an instance →](operate/deploy.md)
## Public instances
The ISCC Foundation runs one monitor instance per network. Anyone can run another one, and every instance checks
the hubs on its own.
| Network | Instance | API reference |
| ------- | ------------------------------- | ------------------------------------ |
| Mainnet | | |
| Testnet | | |
To look up an ISCC-ID, enter it on the instance's start page or open `/inclusion/`, for example
[`https://monitor.iscc.io/inclusion/ISCC:MEIGKV5N6NTVF4AB`](https://monitor.iscc.io/inclusion/ISCC:MEIGKV5N6NTVF4AB).
## What the monitor does
- Follows the transparency log of every hub in the network's
[Hub-List](https://github.com/iscc/iscc-hub/tree/main/hubs), every five minutes by default.
- Verifies each new checkpoint. It checks the hub's Ed25519 signature under the key in its did:web document, and
an RFC 6962 consistency proof that the log only grew. It freezes a hub that rewrites its history and keeps the
evidence.
- Mirrors every hub's log, so the records stay available and verifiable if the hub goes offline.
- Anchors checkpoint roots in Bitcoin with OpenTimestamps.
- Publishes a Certificate of Inclusion and a proof bundle for any ISCC-ID in the network. Its
[HTTP API](reference/http-api.md) serves the raw log data and proofs.
The monitor is a *verifiable cache*, not a trusted oracle. Everything it serves comes with the evidence to check
it. [What the monitor guarantees](concepts/trust-model.md) shows where that holds and where it ends.
# Read a Certificate of Inclusion
A Certificate of Inclusion shows, clause by clause, the evidence that an ISCC-ID is in the transparency log of the
hub that issued it.
{ .iscc-lead }
## Open a certificate
Enter the ISCC-ID on the start page of a [monitor instance](../index.md#public-instances), or open
`/inclusion/`:
```text
https://monitor.iscc.io/inclusion/ISCC:MEIGKV5N6NTVF4AB
```
Any letter case works, with or without the `ISCC:` or `did:iscc:` prefix. You do not need to know the hub. The
ISCC-ID names its network and hub, and the monitor finds the hub in the network's Hub-List.
Use the instance for the ISCC-ID's network: `monitor.iscc.io` for mainnet, `monitor-test.iscc.io` for testnet.
## The header
The header shows the ISCC-ID, the hub whose log has it, and its position in that log. If the certificate proves
inclusion, it has the **ISCC Trust Seal**. The seal repeats the ISCC-ID and the issue date, with the time beneath
it.
The seal and the certificate are the monitor's account of the evidence. They vouch only that the ISCC-ID is in the
log, not for the content it identifies or for who declared it.
## The clauses
§1 Subject
: The ISCC-ID and the position (*seq*) of the log record that commits it.
§2 Checkpoint
: The size and Merkle root of the hub's log that the certificate refers to. This is the latest checkpoint the
monitor accepted for the hub. The hub signed it and the monitor checked it.
§3 Inclusion Proof
: The sibling hashes that rebuild the §2 root from the record. The monitor rebuilt the root before it rendered
the page. With JavaScript on, your browser checks the proof again.
§4 Signing Key
: The hub's Ed25519 key that signed the §2 checkpoint, from the hub's did:web document at
`https:///.well-known/did.json`.
§5 Bitcoin Anchor
: The earliest OpenTimestamps proof, confirmed in a Bitcoin block, of a log state that includes the record. It
shows that the record existed by the time of that block. The clause links the proof file and the `ots verify`
command; see [Check a Bitcoin anchor](bitcoin-anchor.md). Until a confirmed anchor covers the record, the
clause shows the timestamp state of the hub's newest root, usually pending. Bitcoin confirmation takes a few
hours.
Comparison Anchor
: The log state the monitor observed from the hub, and since when it has watched the hub. If the hub showed you
a different history, a comparison with this log state finds it. See
[Compare your view of a log](split-view.md).
§6 Record History
: Every record in the log that names this ISCC-ID: the declaration and any later deletion. A deletion is a new
record. The original declaration stays in the log.
## When the monitor cannot certify
If the ISCC-ID is malformed, belongs to an unknown hub, or is not (yet) in the log the monitor accepted, the page
says so and shows no clauses. A newly declared ISCC-ID appears after the monitor's next poll of its hub, by
default within five minutes.
## Keep the evidence
The certificate is a page from one monitor. The "Download proof bundle" button gives you the same evidence as one
JSON file, the **proof bundle**. It holds the hub-signed checkpoint, the inclusion proof, the record and the hub's
key. If there is a Bitcoin anchor, it holds that evidence too. You can store the bundle, pass it on and check it
without the monitor. See [Verify a proof bundle](verify-bundle.md).
# Verify a proof bundle
A proof bundle has all you need to check that an ISCC-ID is in a hub's log. If you verify it yourself, the
result rests on the hub's signature and on Merkle hashing, not on the monitor's word.
{ .iscc-lead }
## 1. Download the bundle
Click "Download proof bundle" on the [certificate](certificate.md), or fetch it:
```bash
curl -OJ https://monitor.iscc.io/inclusion/ISCC:MEIGKV5N6NTVF4AB.bundle
```
This saves `MEIGKV5N6NTVF4AB.bundle.json`. Its members are in [Proof bundle format](../reference/proof-bundle.md).
The monitor gives a bundle only for an ISCC-ID whose inclusion it verified itself. For any other ID it answers
`{"error": "no proof bundle available for this id"}`.
## 2. Run the verifier
[`verify_bundle.py`](verify_bundle.py) is a short, self-contained Python script. Read it before you rely on it;
that is the point. It declares its one dependency (`cryptography`, for Ed25519) inline, so
[uv](https://docs.astral.sh/uv/) runs it in one step:
```bash
uv run verify_bundle.py --resolve MEIGKV5N6NTVF4AB.bundle.json
```
Without uv, install `cryptography` and use Python 3.10 or newer. A bundle that passes prints:
```text
ok record names ISCC:MEIGKV5N6NTVF4AB
ok checkpoint: signed by iscc.id/log key 68c3b89a, record at 0 of 220
ok anchor: signed by iscc.id/log key 68c3b89a, record at 0 of 1
check the Bitcoin anchor: ots verify -d 951420d087ae7ff1ac814091012ac68a89736f0f137f74c62dce7503ddaebb80 MEIGKV5N6NTVF4AB.anchor.ots
ok did:web:iscc.id lists the signing key
```
A failed check prints `FAIL` with the reason and exits with status 1.
## What the script checks
1. The record names the ISCC-ID. The record is the exact bytes the hub logged, JSON with an `iscc_id` field.
1. The hub signed the checkpoint. The checkpoint is a [signed note](https://c2sp.org/signed-note) with the log's
origin, size and Merkle root. The script checks its Ed25519 signature with the bundle's key.
1. The record is in that log state. The script hashes the record into an RFC 6962 leaf, applies the inclusion
proof and compares the result with the checkpoint's root.
1. The Bitcoin anchor covers the record, if the bundle has an `anchor` member. The script checks its checkpoint
and inclusion proof the same way. It writes the OpenTimestamps proof to a `.anchor.ots` file and prints the
command to [check it](bitcoin-anchor.md).
1. The key is the hub's own, with `--resolve`. The bundle's key comes from the monitor. `--resolve` fetches the
hub's did:web document, `https:///.well-known/did.json`, and checks that it lists the key. Without it,
step 2 shows only that the checkpoint matches the key the monitor gave.
## What a passing bundle proves
A passing bundle proves that the hub signed a log state with this record at this position. With a confirmed
anchor, `ots verify` adds that the record existed by the time of a specific Bitcoin block.
It does not prove that the hub showed everyone the same log. That needs a comparison between independent
observers; see [Compare your view of a log](split-view.md). It says nothing about the content the ISCC-ID
identifies beyond what the record declares.
## The script
```python title="verify_bundle.py"
--8<-- "docs/guides/verify_bundle.py"
```
# Check a Bitcoin anchor
The monitor timestamps the Merkle root of each log state it accepts with [OpenTimestamps](https://opentimestamps.org).
Once a Bitcoin block confirms the timestamp, anyone can check that the log state, and every record in it, existed
by the time of that block.
{ .iscc-lead }
## What you need
- The **OpenTimestamps proof** (`.ots`) for the log state that covers the record.
- The **root** it timestamps, in hex.
- The `ots` command from [opentimestamps-client](https://github.com/opentimestamps/opentimestamps-client):
`pip install opentimestamps-client`, or run it without installing as
`uvx --from opentimestamps-client ots ...`. On Windows the client fails to load its OpenSSL library. Use WSL
or a Linux container.
- For the full check, a **Bitcoin node** that `ots` can reach: a local Bitcoin Core, or one you give with
`--bitcoin-node `. If you have none, see [Without a Bitcoin node](#without-a-bitcoin-node).
## From a certificate
§5 of the [certificate](certificate.md) shows the block, the log size the anchor covers, a link to the
`checkpoint.ots` file and the exact command. Download the file and run the command, for example:
```bash
ots verify -d 951420d087ae7ff1ac814091012ac68a89736f0f137f74c62dce7503ddaebb80 checkpoint.ots
```
`-d` gives the root as the digest to check. If the proof is for a different root, `ots` stops with
"Digest provided does not match digest in timestamp". If not, it gets the attesting block from the Bitcoin node
and reports that the root existed by that block's time.
## Without a Bitcoin node
Without a node, `ots verify` checks the digest, then stops with "Could not connect to Bitcoin node". Finish the
check by hand:
1. Run `ots info`. It prints the block the proof commits to and the merkle root that block must have:
```bash
ots info checkpoint.ots
```
```text
...
verify BitcoinBlockHeaderAttestation(956107)
# Bitcoin block merkle root a697ce8abe505838208a26b6454eb7af9f5f3c06af05ffac1083335b4c238ca4
```
1. Get the block hash from a block explorer, for example `https://mempool.space/api/block-height/956107`.
1. Read the `merkle_root` from `https://mempool.space/api/block/`.
1. Compare the two merkle roots. If they match, the root existed by the block's time.
This check trusts the explorer to report the real block header. A Bitcoin node of your own removes that trust.
## From a proof bundle
A bundle with an `anchor` member has the same proof in base64. It also has the hub-signed checkpoint at the
anchored size and the record's inclusion proof against it. The [verification script](verify-bundle.md) checks
this evidence, writes the proof to `.anchor.ots` and prints the `ots verify` command with the root.
## From the API
- `GET //log/checkpoint.ots` returns the proof for the hub's latest accepted root.
- `GET //log/checkpoint.ots?size=N` returns the proof for the root the monitor accepted at size `N`.
For `ots verify`, decode the checkpoint's root from base64 to hex.
A *pending* proof is in an OpenTimestamps calendar but not yet in Bitcoin. The monitor asks the calendar for an
upgrade, by default every hour, and less often for a root that stays pending. It serves the confirmed proof once
it has it.
## Why the anchor matters
The hub's signature shows what the hub claims. The Bitcoin anchor adds a time that depends on neither the hub nor
the monitor. A record in a log state that block *N* confirmed cannot have been added later, even if the hub
rewrote its log afterwards.
# Compare your view of a log
A hub can show different, internally consistent histories to different audiences: a *split view*. No one can see
it from their own view alone. You find it by comparing the log state a hub showed you with the one it showed an
independent monitor.
{ .iscc-lead }
## What you compare
A hub's log state is a **checkpoint**: the log's size and Merkle root, signed by the hub. Get one from the hub:
```bash
curl https://iscc.id/log/checkpoint
```
Get the monitor's from its mirror of the same log:
```bash
curl https://monitor.iscc.io/iscc.id/log/checkpoint
```
The hub signs both. Two checkpoints that cannot belong to one log prove a split view.
- Same size: the roots must be equal. Different roots, both with a valid hub signature, are a split view.
- Different sizes: check consistency, as below.
## Check consistency
A log only grows, so a smaller checkpoint must be a prefix of a larger one. An RFC 6962 **consistency proof**
shows this with the two roots and a few hashes.
The monitor computes a consistency proof from your earlier size to the size it accepted:
```bash
curl "https://monitor.iscc.io/iscc.id/log/consistency?from=1"
```
```json
{
"type": "IsccLogConsistencyProof",
"firstSize": 1,
"secondSize": 220,
"consistencyProof": ["6EXyB1geohg7S5JLntu1k2itax1HegKkRjZQ1gxUsAg=", "..."]
}
```
Verify it with the root of *your* checkpoint at `firstSize` and the root of the monitor's at `secondSize`. Use an
RFC 6962 library such as Go's [`transparency-dev/merkle`](https://github.com/transparency-dev/merkle)
(`proof.VerifyConsistency`). If the proof fails, the two checkpoints cannot both be states of one log.
`from` can be any size up to the monitor's accepted size, even one the monitor never saw a checkpoint for. If your
checkpoint is larger than the monitor's, the endpoint returns `400`: the monitor has not seen that state yet. Wait
for its next poll, by default within five minutes.
Any tlog-tiles client can also compute the consistency proof from the hub's own log tiles, without the monitor.
## What the monitor checks on its own
The monitor compares each checkpoint it fetches with the earlier ones. If a hub's log shrinks, changes its root at
the same size, or fails a consistency proof, the monitor freezes the hub and keeps both checkpoints as evidence.
See [How it works](../concepts/how-it-works.md#hub-status).
The monitor cannot see on its own that a hub shows a different history to someone else. That is why your
comparison matters, and why more than one independent monitor helps.
## Coverage
The monitor vouches only for what it observed from its coverage start onward. Each hub's page and the
certificate's Comparison Anchor show the coverage start. A split view that ended before then is out of the
monitor's reach.
# How it works
An ISCC hub issues ISCC-IDs and records each declaration in a public, append-only transparency log. The monitor
watches those logs from the outside. It runs one loop per network: follow, verify, mirror, anchor, publish.
{ .iscc-lead }
## Roles
| Role | What it does |
| ---------------- | -------------------------------------------------------------------------------------------------- |
| Hub | Issues ISCC-IDs and logs every declaration in a [tlog-tiles](https://c2sp.org/tlog-tiles) transparency log, a Merkle tree served as static files. It signs its log state with an Ed25519 key from its did:web document. |
| Hub-List | The authoritative list of a network's hubs, in the [iscc-hub repository](https://github.com/iscc/iscc-hub/tree/main/hubs). It gives each hub's domain and 12-bit `hub_id`. Every ISCC-ID the hub issues carries this number. |
| Monitor instance | Follows every hub on the Hub-List, checks their logs and publishes what it saw. Anyone can run one, and each checks the hubs on its own. |
| Client | A person or program that looks up ISCC-IDs on a monitor and verifies the evidence. |
## The loop
### 1. Follow
Every five minutes by default, the monitor fetches each hub's latest checkpoint: the log's size and Merkle root,
signed by the hub. The monitor re-reads the Hub-List every hour, so a changed hub entry takes
effect without a restart. The monitor follows a newly listed hub after the next restart.
### 2. Verify
The monitor accepts a new checkpoint only if:
1. Its signature verifies under the hub's key from `https:///.well-known/did.json`.
2. An RFC 6962 consistency proof shows that it extends the last accepted checkpoint. No record was changed or
removed.
3. The monitor can rebuild its Merkle root from the fetched log tiles.
### 3. Mirror
The monitor stores every hub's tiles and entry bundles in one SQLite file per network and serves them at the hub's
own paths. The mirror keeps a log available and verifiable if the hub goes offline. Full tiles never change, so
the monitor fetches each one once. Its traffic grows with the log's growth, not with its size.
### 4. Anchor
Every hour, and at startup, the monitor submits newly accepted roots to an
[OpenTimestamps](https://opentimestamps.org) calendar and collects the Bitcoin confirmations of earlier ones. A
confirmed timestamp shows that a log state existed by a given Bitcoin block.
### 5. Publish
For any ISCC-ID in the network, the monitor shows a Certificate of Inclusion and offers a proof bundle. Its
[HTTP API](../reference/http-api.md) serves the mirrored log, inclusion and consistency proofs, and a summary
verdict.
## Hub status
Each hub has one of five statuses, shown on the instance's pages and in its metrics:
| Status | Meaning |
| ---------------- | ---------------------------------------------------------------------------------------------------- |
| **verified** | The latest checkpoint is signed with the hub's did:web key, consistent with earlier ones and rebuilt |
| **unresolvable** | The monitor cannot fetch or parse the hub's did:web document |
| **unverified** | The checkpoint's signature does not match the hub's own did:web key |
| **frozen** | The hub's log contradicted itself. The monitor stopped accepting its checkpoints and kept the evidence |
| **inactive** | The hub is removed or paused in the Hub-List |
The monitor follows and mirrors every hub, whatever its status. Only a *verified* hub's checkpoints advance the
accepted state.
The monitor freezes a hub when its log shrinks, shows a different root at a size already seen, or fails a
consistency proof. Each is a self-consistency violation: a rewritten history. The monitor keeps both
contradicting hub-signed checkpoints as permanent evidence. It logs a warning and keeps polling the hub at a slower
pace. It does not lift a freeze automatically.
## Records and ISCC-IDs
A log record is a **declaration**, which commits an ISCC-ID, or a **deletion**, a later record that marks a
declaration as deleted. A deletion never removes the declaration from the log, so the monitor always keeps it. A
hub can also *redact* a declaration from its own responses. A redaction writes no record, and the monitor never
sees it.
Verification does not interpret record contents. The monitor reads them only to index ISCC-IDs and to label records
on its pages.
# What the monitor guarantees
Nobody has to take the monitor's word for anything. This page shows what that means and where the evidence stops.
{ .iscc-lead }
## A verifiable cache, not a trusted oracle
You can check everything the monitor serves against the hub's signature and Merkle hashing. Fetching data from the
monitor is not the same as trusting it. You can check an ISCC-ID in three ways:
- **Proof bundle**: the hub-signed checkpoint, the record and the inclusion proof. If you verify it and resolve
the hub's key yourself, the monitor is out of the trust path. This is the authoritative way to check an
ISCC-ID. See [Verify a proof bundle](../guides/verify-bundle.md).
- **Certificate of Inclusion**: the monitor's readable account of the same evidence. Your browser re-checks its
inclusion proof, but the monitor renders the rest of the page.
- **verify-for-me** (`GET //log/verify`): a verdict you trust the monitor for. It is convenient, but it is
the weaker path.
## What the monitor detects on its own
The monitor detects a hub that rewrites its history *against the monitor*. This is a self-consistency violation.
It takes one of three forms:
- The log shrinks.
- A different root appears at a size the monitor already saw.
- A checkpoint fails a consistency proof.
The monitor freezes the hub. It keeps the contradicting hub-signed checkpoints as evidence that can never be
regenerated.
## What needs a comparison
In a **split view**, a hub shows one history to the monitor and another to you. Neither observer can see it alone.
To make it detectable, the monitor publishes the log states it observed, so you can compare yours against them.
See [Compare your view of a log](../guides/split-view.md). Monitors do not yet compare notes with each other
(gossip) or cosign checkpoints as witnesses.
## Coverage
Guarantees hold only from the **coverage start**, the moment a monitor started watching a hub. A monitor that
starts late copies the full log and can verify it from then on. It cannot tell whether the hub showed different
histories before anyone watched. Each hub's page and each certificate show the coverage start.
## What the evidence does not say
| Not shown | Why |
| ------------------------------- | --------------------------------------------------------------------------------------- |
| Who owns the content | A certificate shows that an ISCC-ID was declared in a hub's log, at a position and by a time. It does not vouch for the content the ISCC-ID identifies, or for the declarer's claims. |
| That the hub's domain is honest | A hub's identity is its domain, and its key comes from the domain's did:web document. The monitor cannot detect a compromised domain. |
| Anything about redactions | A hub can stop serving a declaration without writing a record. The monitor keeps the declaration, but the redaction leaves no trace in the log. |
## Why the Bitcoin anchor helps
A hub signature shows what the hub claims, at a time the hub chooses to state. An OpenTimestamps anchor confirmed
in Bitcoin fixes when a log state existed, independent of the hub and the monitor. A record in an anchored log
state was in the log by that block, whatever the hub does later.
# Glossary
Terms used on the monitor's pages, in its API and in this documentation.
{ .iscc-lead }
Bitcoin anchoring
: Timestamping a checkpoint root in the Bitcoin chain with OpenTimestamps. "Anchoring" means only this, never the
comparison anchor.
Checkpoint
: A hub's signed statement of its log state: the log's origin, its size and its Merkle root, in the
[signed-note](https://c2sp.org/signed-note) format.
Comparison anchor
: The monitor's role as an independent record of what each hub showed it. A client compares its own log state
against it to detect a split view.
Consistency proof
: The hashes that show a smaller log state is a prefix of a larger one, so nothing was changed or removed
(RFC 6962).
Coverage
: The window from `monitored_since` to now. `monitored_since` is the coverage start: the size and time at which
the monitor started watching a hub. All guarantees hold only within this window.
Declaration
: A log record that commits an ISCC-ID. The default subject of an inclusion proof.
Deletion
: A later log record that carries an existing ISCC-ID and marks its declaration as deleted. It never removes the
declaration from the log.
did:web
: How a hub publishes its signing key. `did:web:` resolves to `https:///.well-known/did.json`.
The domain's owner manages and rotates the key there.
Hub
: A service that issues ISCC-IDs and records them in its transparency log.
Hub-List
: The authoritative list of a network's hubs, with each hub's domain and 12-bit `hub_id`, published in the
[iscc-hub repository](https://github.com/iscc/iscc-hub/tree/main/hubs).
Hub status
: The monitor's verdict on a hub: verified, unresolvable, unverified, frozen or inactive. See
[Hub status](how-it-works.md#hub-status).
Inclusion proof
: The sibling hashes that rebuild a log's Merkle root from one record, showing that the record is in that log
state (RFC 6962).
Irreplaceable evidence
: What the monitor can never regenerate: the checkpoints it observed, contradicting checkpoint pairs and
OpenTimestamps proofs. Backups exist to protect it. The mirror, in contrast, can be fetched again.
Mirror
: The monitor's complete copy of a hub's log tiles and entry bundles, served at the hub's own paths.
Monitor instance
: One running deployment of the monitor at its own domain, such as `monitor.iscc.io`. Anyone can run one.
Proof bundle
: A JSON file that a client verifies on its own. It holds the hub-signed checkpoint, the inclusion proof, the
record bytes, the hub's key and, when there is one, the Bitcoin anchor evidence. See
[Proof bundle format](../reference/proof-bundle.md).
Redaction
: A hub's local choice to stop serving a declaration. It writes no log record, so the monitor never sees it and
keeps the declaration.
Self-consistency violation
: Successive checkpoints from one hub that cannot belong to one log. They show that the hub rewrote its history
against this monitor. The monitor detects this on its own and freezes the hub.
Split view
: A hub showing different, internally consistent histories to different audiences. You find it by comparing
views, for example yours against a monitor's.
Verifiable cache
: What the monitor is, as opposed to a trusted oracle. Clients re-verify whatever it serves against the hub's
signature and Merkle hashing.
verify-for-me
: The API endpoint whose verdict you trust the monitor for. It is weaker than verifying a proof bundle yourself.
# Deploy an instance
The monitor is one static binary in a small container image, configured with environment variables. One instance
follows one network, mainnet or testnet, and keeps all its state in one SQLite file.
{ .iscc-lead }
## Requirements
- A container runtime, such as Docker with Compose.
- A database volume that can grow with the logs of the busiest hub in the network.
- A reverse proxy that terminates TLS, such as Caddy.
- A subdomain for the instance alone. The monitor serves its pages, assets and hub routes from the root of its
host, so it cannot run under a sub-path.
- Outbound HTTPS. See [Egress](#egress).
## Quick start
1. Create `compose.yaml`:
```yaml title="compose.yaml"
services:
iscc-monitor:
image: ghcr.io/iscc/iscc-monitor:v0.1.0
environment:
ISCC_MONITOR_DB: /data/monitor.db
ISCC_MONITOR_REALM: https://raw.githubusercontent.com/iscc/iscc-hub/refs/heads/main/hubs/mainnet.yaml
ISCC_MONITOR_INSTANCE: monitor.example.com
volumes:
- monitor-data:/data
stop_grace_period: 60s
# No ports: the reverse proxy reaches the monitor on port 9464 over the internal network.
volumes:
monitor-data:
name: monitor-data
```
For testnet, use `.../hubs/testnet.yaml`. [Configuration](configuration.md) lists every setting.
1. Before the first start, give the volume to uid 65532. Then start the instance:
```bash
docker run --rm -v monitor-data:/data alpine chown -R 65532:65532 /data
docker compose up -d
```
The image runs as the unprivileged uid 65532, but a new Docker volume belongs to root. Without this step, the
monitor cannot create its database. `name: monitor-data` makes Compose mount this volume, not one prefixed
with the project name. A bind-mounted host directory owned by uid 65532 also works.
1. Check that the instance is up. The first poll finishes within seconds.
```bash
curl https://monitor.example.com/healthz
curl https://monitor.example.com/version
```
1. Open the start page. It lists every hub of the network with its status.
## Image and tags
The image is `ghcr.io/iscc/iscc-monitor`.
| Tag | Use |
| -------------- | ------------------------------------------------------------------------------------------------- |
| `:vX.Y.Z` | A release, described in the [changelog](../reference/changelog.md). Pin production instances to it |
| `:latest` | The newest release. It moves, so do not pin to it |
| `:develop` | The latest build of the `develop` branch, for continuous testnet deployments |
| `:sha-` | One exact build of a release or of `develop`. It never moves. Use it or `:vX.Y.Z` to roll back |
A released version is never re-tagged. `GET /version` shows the running build. A release image shows the release
and its commit, such as `{"version":"v0.1.0+ac65974"}`. A `develop` image shows the full commit.
The image is based on distroless `static-debian12:nonroot`. It has the CA certificates for outbound HTTPS and
loads nothing from a CDN at runtime. Its license terms are in `/usr/share/doc/iscc-monitor/`.
## Reverse proxy
The monitor serves plain HTTP on `:9464` and sets no connection timeouts, so it must not face the internet
directly. Do not publish the port on the host, because Docker's published ports bypass the host firewall. Let the
reverse proxy reach the container over the internal network and terminate TLS.
All routes share one listener, so the proxy must keep them apart. Everything is public by design except
`/metrics`, which is your decision; see [Monitoring](monitoring.md#metrics). No route carries a secret, because
the monitor holds no signing key.
With Caddy:
```text title="Caddyfile"
monitor.example.com {
respond /metrics 403
reverse_proxy iscc-monitor:9464
}
```
## Egress
The monitor sends outbound HTTPS requests to:
- Every hub in the Hub-List: its log at `https:///log/...` and its did:web document at
`https:///.well-known/did.json`.
- The Hub-List, once an hour, when `ISCC_MONITOR_REALM` is a URL.
- The OpenTimestamps calendar `https://alice.btc.calendar.opentimestamps.org`, at startup and then every
`ISCC_MONITOR_OTS_INTERVAL` (default one hour).
Hubs join and leave the network, so there is no fixed address list. An egress allow-list must permit HTTPS to the
hub domains and the calendar.
## Resources
Estimates, not benchmarks:
- Memory: tens of megabytes. The peak is the tiles in flight during a poll.
- CPU: close to idle between polls, with short bursts for signature checks and Merkle root rebuilds.
- Disk: the value to watch. The database holds the full log of every hub and grows with their activity. Set a
disk-usage alert on the volume and size it with headroom.
## Shutdown
On `SIGTERM`, which `docker stop` sends, the monitor finishes the current poll, closes the database and exits
cleanly. Set `stop_grace_period` long enough for this, as in the quick start. Otherwise the runtime kills the
monitor mid-poll.
## Next steps
- [Put your name on the instance](branding.md)
- [Back up the database and upgrade safely](data.md)
- [Watch it with Prometheus](monitoring.md)
# Configuration
The monitor reads its configuration from environment variables at startup and checks them first. If a value is
invalid, the monitor does not start and names the variable at fault.
{ .iscc-lead }
## Required
`ISCC_MONITOR_DB`
: Path to the SQLite database file of the network, for example `/data/monitor.db`. The monitor creates the file
on the first start. Use one file per network. Never point two running instances at the same file.
`ISCC_MONITOR_REALM`
: The source of the network's hubs: the URL or file path of an
[iscc-hub Hub-List](https://github.com/iscc/iscc-hub/tree/main/hubs).
| Network | Hub-List URL |
| ------- | ---------------------------------------------------------------------------------- |
| Mainnet | `https://raw.githubusercontent.com/iscc/iscc-hub/refs/heads/main/hubs/mainnet.yaml` |
| Testnet | `https://raw.githubusercontent.com/iscc/iscc-hub/refs/heads/main/hubs/testnet.yaml` |
The monitor fetches a URL again every hour, so changed hub entries apply without a restart. A newly added hub
is followed after a restart. The image default is a built-in testnet list whose hosts no longer serve, so
always set this variable.
## Timing
Intervals use Go's duration syntax, such as `30s`, `5m` or `1h`, and must be positive.
| Variable | Default | Meaning |
| --------------------------- | ------- | ------------------------------------------------------------------------------------ |
| `ISCC_MONITOR_NORMAL` | `5m` | Poll interval for each hub |
| `ISCC_MONITOR_FROZEN` | `1h` | Poll interval for a frozen hub, for evidence. Must be at least `ISCC_MONITOR_NORMAL` |
| `ISCC_MONITOR_OTS_INTERVAL` | `1h` | Interval for sending new roots to the OpenTimestamps calendar and checking pending roots for Bitcoin confirmation |
The OpenTimestamps pass also runs once at startup. However short the interval, the monitor asks the calendar
about one root at most once an hour. While the root stays unconfirmed, the wait doubles, up to once a day.
## Network
`ISCC_MONITOR_ADDR`
: The HTTP listen address. Default `:9464`. Keep it on the internal network; see
[Reverse proxy](deploy.md#reverse-proxy).
## Masthead
All optional. Without them, the masthead shows neutral placeholder text.
`ISCC_MONITOR_INSTANCE`
: The domain of the instance, for example `monitor.example.com`.
`ISCC_MONITOR_OPERATOR`
: The line below the domain. Default: "independent Trust & Transparency service · ISCC-Hub network".
`ISCC_MONITOR_REALM_NAME`
: The display name of the network, for example `ISCC mainnet`. It appears in the title of the hub list on the
start page.
## Operator co-branding
All optional. [Branding](branding.md) shows them in use.
`ISCC_MONITOR_OPERATOR_NAME`
: The organization that runs the instance. It appears as "Operated by ..." in the footer and as the text
alternative of the logo. Required when `ISCC_MONITOR_OPERATOR_URL` or `ISCC_MONITOR_OPERATOR_LOGO` is set.
`ISCC_MONITOR_OPERATOR_URL`
: The operator's website, an absolute `http` or `https` URL. The name and the logo link to it.
`ISCC_MONITOR_OPERATOR_LOGO`
: Path to the operator's logo: `.svg`, `.png`, `.webp` or `.jpg`, at most 256 KiB, of the type its extension
names. The monitor reads it once at startup and serves it at `/_ds/operator-logo`.
`ISCC_MONITOR_FOOTER_LINKS`
: Footer links, such as an imprint or a privacy policy, as `Label=URL` pairs separated by `|`. Each URL is
`http`, `https` or `mailto:`.
```text
Imprint=https://example.com/imprint|Privacy=https://example.com/privacy
```
## Example
A local instance for testnet with a short poll interval:
```bash
ISCC_MONITOR_DB=./monitor-test.db \
ISCC_MONITOR_REALM=https://raw.githubusercontent.com/iscc/iscc-hub/refs/heads/main/hubs/testnet.yaml \
ISCC_MONITOR_NORMAL=30s \
ISCC_MONITOR_ADDR=127.0.0.1:41464 \
iscc-monitor
```
# Branding
Every page has the Deep Navy ISCC masthead and footer. The footer shows the running version and the ISCC
Foundation's open-source credit. You can add your own identity beside them without a rebuild.
{ .iscc-lead }
## What you can add
| Item | Where it shows |
| ----------------- | ------------------------------------------------------------------------------- |
| Organization name | Footer, as "Operated by ..." |
| Website | Link on the name and the logo |
| Logo | Masthead, after an "Operated by" label, on a small white plate |
| Footer links | Footer. Many jurisdictions require an imprint or a privacy policy on a public site |
| Instance domain | Masthead, with an operator line beneath it |
The ISCC mark, the colors and the open-source credit always stay. Co-branding does not re-theme the monitor.
## Example
Mount the logo read-only and point the settings at it:
```yaml title="compose.yaml (excerpt)"
services:
iscc-monitor:
environment:
ISCC_MONITOR_INSTANCE: monitor.example.com
ISCC_MONITOR_OPERATOR_NAME: Example GmbH
ISCC_MONITOR_OPERATOR_URL: https://example.com/
ISCC_MONITOR_OPERATOR_LOGO: /etc/iscc-monitor/operator-logo.svg
ISCC_MONITOR_FOOTER_LINKS: "Imprint=https://example.com/imprint|Privacy=https://example.com/privacy"
volumes:
- ./operator-logo.svg:/etc/iscc-monitor/operator-logo.svg:ro
```
[Configuration](configuration.md#operator-co-branding) describes each setting.
## Logo guidelines
- Use a transparent background.
- Draw the logo for a light background. The masthead is dark, but the logo sits on a white plate.
- The masthead shows the logo at most 28 pixels tall and 160 pixels wide. Pick proportions that read at that size.
- Keep the file under 256 KiB. SVG stays sharp at every size.
The monitor reads the logo once at startup and serves it itself. Pages load nothing from another origin.
## When the instance refuses to start
The monitor checks the settings at startup. It exits with a message if:
- a URL is malformed,
- a logo or URL is set without a name,
- the logo is missing, larger than 256 KiB, or not the image type its extension names.
# Data, backups and upgrades
One SQLite file holds all the state of an instance for its network. Part of it is evidence that nothing can
regenerate, so handle the file with care.
{ .iscc-lead }
## What is in the database
- **The mirror** of every hub's log: tiles and entry bundles. It is the bulk of the file. The monitor could fetch
it again from the hubs.
- **Irreplaceable evidence**: the checkpoints the monitor observed, the contradicting checkpoints of any frozen
hub, and the OpenTimestamps proofs. If a hub goes offline or rewrites its log, nothing can recreate them.
Keep the file on a volume that outlives the container. A new container on the same file continues where the old
one stopped.
## Backups
The database runs in SQLite's WAL mode, so recent changes can sit in two files next to it. Back up all three files
as one set: `monitor.db`, `monitor.db-wal` and `monitor.db-shm`. A copy of `monitor.db` alone, taken while the
instance runs, can miss the latest changes.
The simplest consistent backup is a copy taken while the instance is stopped. For a running instance, use
SQLite's own backup, for example:
```bash
sqlite3 /data/monitor.db ".backup '/backup/monitor-$(date -u +%Y%m%dT%H%M%SZ).db'"
```
The image has no shell and no `sqlite3`, so run the command from another container that mounts the same volume.
## Upgrades
1. Snapshot the database (all three files). Keep the snapshot until the new version has proven itself.
1. Change the image tag to the new `:vX.Y.Z` and restart.
1. Check `GET /version` and the start page.
On startup, the new version migrates the database automatically, one transaction per step. If a step fails, the
database stays as it was and the monitor does not start.
!!! warning
Never recreate the volume to get a clean schema. That destroys the evidence.
## Rollbacks
Between versions with the same database schema, a rollback is a tag change. A version refuses a database that a
newer version migrated, so a rollback across a migration means restoring the snapshot from before the upgrade.
You lose everything the monitor observed since then.
## Rules
- One running instance per database file. The monitor is the only writer of the file.
- uid 65532, the user of the image, must be able to write to the volume. See [Deploy](deploy.md#quick-start).
# Monitoring
An instance reports its health, its build, the state of each hub and events that need a person. It uses two small
JSON endpoints, Prometheus metrics and its log.
{ .iscc-lead }
## Health and version
`GET /healthz`
: `200` when the process is live and the database is ready, `503` when the database is not. Use it for
container health checks and uptime monitoring.
`GET /version`
: The running build, for example `{"version":"v0.1.0+ac65974"}`. Check it instead of trusting a floating image
tag.
## Metrics
`GET /metrics` serves Prometheus text format:
| Metric | Type | Labels | Meaning |
| ---------------------------------- | ------- | ------------------ | ------------------------------------------------------------------------- |
| `iscc_monitor_hub_status` | gauge | `hub_id`, `status` | 1 for the hub's current status, 0 for the others |
| `iscc_monitor_last_observed_at` | gauge | `hub_id` | Unix time of the hub's latest poll that reached a verdict |
| `iscc_monitor_poll_failures_total` | counter | `hub_id` | Failed polls: the hub was unreachable or sent a broken response |
| `iscc_monitor_violations_total` | counter | `hub_id`, `kind` | Self-consistency violations, by `kind`: `shrink`, `fork` or `equivocation` |
`status` is one of the five [hub statuses](../concepts/how-it-works.md#hub-status). `hub_id` is the hub's number
in this instance's database, given in the order the instance first followed the hubs. It is not necessarily the
`hub_id` in the Hub-List.
Alerts to set:
- Any increase of `iscc_monitor_violations_total`, or `iscc_monitor_hub_status{status="frozen"} == 1`. A hub
rewrote its history, and a person must look at the evidence.
- `iscc_monitor_hub_status{status=~"unresolvable|unverified"} == 1` for longer than a few polls.
- `time() - iscc_monitor_last_observed_at` well above the poll interval.
- A rising rate of `iscc_monitor_poll_failures_total`.
### Public or private
Metrics carry no secrets, but they are operator telemetry, not evidence. The monitor has one listener for all
routes, so you decide at the reverse proxy whether `/metrics` is public. The
[Caddy example](deploy.md#reverse-proxy) refuses it.
## Log
The monitor writes one line per event to standard error, in logfmt (`key=value`). The lines to know:
| Message | Level | When |
| -------------------------------------------------- | ----- | --------------------------------------------------------------------------- |
| `hub frozen` | WARN | A hub's log contradicted itself. `kind` names how. Investigate |
| `poll hub failed` | ERROR | One poll of one hub failed. Occasional failures are normal |
| `ots pass` | INFO | Each OpenTimestamps pass: roots `stamped`, `confirmed`, still `pending` and `failed`, and the time of the `next` pass |
| `ots pass had failures` | ERROR | At least one root failed. `err` has the first error. The next pass tries again |
| `realm Hub-List refreshed` | INFO | The hourly Hub-List refresh applied |
| `realm refresh failed; keeping last good Hub-List` | ERROR | The monitor could not fetch the Hub-List. The previous one stays in use |
# HTTP API
Every instance serves its web pages and a machine API from the same host. All routes are `GET`. They need no
authentication and allow cross-origin requests (`Access-Control-Allow-Origin: *`).
{ .iscc-lead }
## Interactive reference
Each instance has an interactive reference of its machine API at `/docs`, with a console to try requests. It is
generated from the OpenAPI 3.1 contract at `/openapi.json` and `/openapi.yaml`.
- Mainnet:
- Testnet:
## Machine API
`` is the domain of a hub that the instance follows, such as `iscc.id`. ISCC-ID parameters accept any letter
case, with or without the `ISCC:` or `did:iscc:` prefix.
### Evidence
| Route | Returns |
| -------------------------------------- | ---------------------------------------------------------------------------------------- |
| `/inclusion/.bundle` | The [proof bundle](proof-bundle.md) for an ISCC-ID, as a JSON download |
| `//log/inclusion?iscc_id=` | Inclusion proof of the ISCC-ID's record against the accepted checkpoint. If several records name the ID, `&index=` picks one |
| `//log/consistency?from=` | Consistency proof from an earlier size to the accepted size |
| `//log/verify?iscc_id=` | verify-for-me: the monitor's own verdict, the weaker path |
For the consistency route, `from` can be any size from 0 to the accepted size, with or without a recorded checkpoint
at that size. A larger `from` returns `400`. `404` means no accepted checkpoint yet, or a tile is not mirrored.
### Mirror
These routes serve the monitor's mirror of the hub's log at [tlog-tiles](https://c2sp.org/tlog-tiles) paths. Standard
tlog-tiles clients can read the log from the monitor instead of the hub.
| Route | Returns |
| -------------------------------------- | --------------------------------------------------------------------------- |
| `//log/checkpoint` | The last hub-signed checkpoint that the monitor accepted |
| `//log/tile/` | A hash tile or entry bundle, such as `tile/0/000` or `tile/entries/000` |
| `//log/entries?index=` | The bytes of the single record at position `n` |
| `//log/checkpoint.ots[?size=]` | The OpenTimestamps proof of the accepted root, or of the root accepted at size `n` |
### Operations
| Route | Returns |
| ----------- | ------------------------------------------------------------------ |
| `/healthz` | Liveness and database readiness |
| `/version` | The running build |
| `/metrics` | Prometheus metrics. Often not public, see [Monitoring](../operate/monitoring.md#metrics) |
## Web pages
The pages work without JavaScript and load nothing from other sites. They are not part of the OpenAPI contract.
| Route | Page |
| ------------------------- | ------------------------------------------------------------- |
| `/` | Start page: ISCC-ID lookup and every hub with its status |
| `/inclusion/` | [Certificate of Inclusion](../guides/certificate.md) |
| `/` | A hub's page: status, coverage, checkpoints and Bitcoin anchors |
| `//log/` | The hub's mirrored log |
| `//log/records` | The hub's records, newest first |
| `//log/record?index=` | One record |
| `/docs` | Interactive API reference |
## Examples
Get the monitor's verdict:
```bash
curl "https://monitor.iscc.io/iscc.id/log/verify?iscc_id=ISCC:MEIGKV5N6NTVF4AB"
```
```json
{
"iscc_id": "ISCC:MEIGKV5N6NTVF4AB",
"hub_status": "verified",
"tree_size": 220,
"root": "ILlsIpVPzNH4BGP8hM2I+roOSakYq+qyx67H+21yIq0=",
"leaf_index": 0,
"included": true,
"verified": true,
"reason": ""
}
```
If the monitor cannot verify an ISCC-ID, it still returns `200`, with `"verified": false` and a `reason`.
Get the bytes of that record:
```bash
curl "https://monitor.iscc.io/iscc.id/log/entries?index=0"
```
# Proof bundle format
A proof bundle is one JSON file with the evidence that an ISCC-ID is in its hub's log. It is the authoritative way
to check an ISCC-ID. A client verifies it on its own, without trusting the monitor.
{ .iscc-lead }
Get it from `GET /inclusion/.bundle`, or with the "Download proof bundle" button on a certificate.
[Verify a proof bundle](../guides/verify-bundle.md) shows how to check one. Example:
[`MEIGKV5N6NTVF4AB.bundle.json`](MEIGKV5N6NTVF4AB.bundle.json).
## Members
`iscc_id`
: The ISCC-ID, in its canonical `ISCC:` form.
`hub`
: The hub whose log has the record: its `domain` and its did:web identifier `did`. The hub publishes its key at
`https:///.well-known/did.json`.
`checkpoint`
: The latest checkpoint the monitor accepted for the hub, as the hub signed it. It is a
[signed note](https://c2sp.org/signed-note): the log's origin, size and base64 Merkle root, then a blank line
and the signature line.
`inclusion`
: The inclusion proof of the record against `checkpoint`, in the hub's `IsccLogInclusionProof` shape:
| Member | Content |
| ---------------- | -------------------------------------------------------- |
| `type` | `"IsccLogInclusionProof"` |
| `checkpoint` | The same signed checkpoint |
| `treeSize` | The checkpoint's size |
| `leafIndex` | The record's position in the log, counted from 0 |
| `inclusionProof` | The RFC 6962 sibling hashes, base64, from the leaf upward |
`record`
: The record exactly as the hub logged it, base64-encoded. Its RFC 6962 leaf hash is the SHA-256 of a zero byte
followed by these bytes.
`key`
: The key that signed `checkpoint`. `id` is its 4-byte signed-note key hash in hex. `multibase` is the Ed25519
public key as the hub's did:web document lists it (`publicKeyMultibase`).
`anchor` (optional)
: Present when a confirmed Bitcoin anchor covers the record. It has the evidence for the earliest such anchor.
| Member | Content |
| ------------ | ------------------------------------------------------------------- |
| `checkpoint` | The hub-signed checkpoint at the anchored size |
| `inclusion` | The record's inclusion proof against that checkpoint |
| `key` | The key that signed that checkpoint |
| `ots` | The OpenTimestamps proof of that checkpoint's root, base64 |
## How the parts fit
1. Hash `record` to a leaf. `inclusion.inclusionProof` rebuilds the root of a tree of `treeSize` from that leaf at
`leafIndex`.
1. The rebuilt root must equal the root in `checkpoint`.
1. `checkpoint` must have a valid signature by `key`.
1. `key` must be in the hub's did:web document. Check this yourself, because the bundle's copy comes from the
monitor.
1. For `anchor`, repeat steps 1 to 4 with its `checkpoint`, `inclusion` and `key`. Then
`ots verify -d ` checks its `ots` proof against Bitcoin.
## No bundle
If the monitor cannot certify an ISCC-ID, the route returns `200` with:
```json
{"error": "no proof bundle available for this id", "iscc_id": "..."}
```
The monitor offers a bundle only after it has verified the inclusion proof against its accepted root. It never
returns a partial bundle.
--8<-- "CHANGELOG.md"
# Development
The monitor is a Go program with no C dependencies. The source is at
[github.com/iscc/iscc-monitor](https://github.com/iscc/iscc-monitor).
{ .iscc-lead }
## Build and run
You need Go 1.26. The task shortcuts also need [mise](https://mise.jdx.dev/).
```bash
git clone https://github.com/iscc/iscc-monitor
cd iscc-monitor
go build -o iscc-monitor ./cmd/iscc-monitor
ISCC_MONITOR_DB=./monitor-test.db \
ISCC_MONITOR_REALM=https://raw.githubusercontent.com/iscc/iscc-hub/refs/heads/main/hubs/testnet.yaml \
ISCC_MONITOR_NORMAL=30s \
ISCC_MONITOR_ADDR=127.0.0.1:41464 \
./iscc-monitor
```
Then open . [Configuration](operate/configuration.md) lists all settings.
## Quality gate
```bash
mise run check # go build, go vet and go test for every package
mise run fmt # gofmt
```
`mise run check` must pass before a change is merged. CI runs it with `govulncheck` on every push and pull request.
## Project layout
| Path | Content |
| ---------------------- | -------------------------------------------------------------------- |
| `cmd/iscc-monitor` | The monitor binary |
| `cmd/wasm` | The in-browser inclusion verifier, built to `internal/web/verify.wasm` |
| `internal/` | Follower, store, proof serving, certificate and web pages |
| `internal/openapi` | The OpenAPI 3.1 contract. A test fails when it and the routes drift apart |
| `deploy/` | The fallback realm document built into the image |
| `docs/` | This documentation site |
| `.claude/` | Product requirements, architecture decision records and design specs |
## Documentation
This site uses [Zensical](https://zensical.org) and the ISCC theme
[zensical-iscc](https://github.com/iscc/zensical-iscc). The tooling needs [uv](https://docs.astral.sh/uv/).
```bash
mise run docs:build # build into site/
mise run docs:check # check the built site against the theme's rules
mise run docs:serve # preview on http://127.0.0.1:41465
```
Pages are Markdown files under `docs/`, listed in `nav` in `zensical.toml`. Each push to `main` that changes the
documentation publishes it to monitor.iscc.codes.
## Releases
Releases follow [Semantic Versioning](https://semver.org). They stay below 1.0.0 until the scope of the first major
version is reached. A release is a tag on `main`. It publishes the `:vX.Y.Z` container image and a GitHub Release.
The [changelog](reference/changelog.md) describes each release.
## License
The monitor is licensed under the [Apache License 2.0](https://github.com/iscc/iscc-monitor/blob/main/LICENSE),
copyright ISCC Foundation. Third-party components keep their own licenses, collected in
[`THIRD_PARTY_NOTICES.txt`](https://github.com/iscc/iscc-monitor/blob/main/THIRD_PARTY_NOTICES.txt). The license
grants no rights to the ISCC name or logo.
# For Coding Agents
Prescriptive reference for agents. Two audiences: agents that **use** a monitor instance (verify evidence, call the
API; sections marked *use*) and agents that **change** the monitor in `iscc/iscc-monitor` (marked *change*).
Full Markdown of this site: [`llms-full.txt`](https://monitor.iscc.codes/llms-full.txt).
## Terms
Use these words exactly. The codebase, the UI and these docs use them the same way.
| Term | Meaning | Do not say |
| ------------------------ | ----------------------------------------------------------------------------------------- | -------------------------- |
| Monitor instance | One running deployment, such as `monitor.iscc.io` | "the monitor" for one host |
| Hub status | `verified`, `unresolvable`, `unverified`, `frozen` or `inactive` | |
| Self-consistency violation | Two checkpoints from one hub that fail RFC 6962 consistency; freezes the hub | inconsistency, fork |
| Split view | A hub showing different histories to different audiences; found by comparison only | fork, equivocation |
| Comparison anchor | The monitor's role as a signed reference to compare a client's `(size, root)` against | witness |
| Bitcoin anchoring | OpenTimestamps proof of a checkpoint root in Bitcoin; the only meaning of "anchor" | |
| Coverage | `monitored_since` (size and time) to now; no guarantee before it | |
| Proof bundle | Self-contained JSON evidence a client verifies without the monitor | receipt |
| verify-for-me | `//log/verify`: the monitor's own verdict, the weaker path | |
| Declaration / Deletion | Log records with `note.$schema` `iscc-note-0.8.0` / `iscc-note-delete-0.8.0` | |
| Redaction | A hub's local suppression; writes no record, the monitor never sees it | deletion |
## Decision dispatch *(use)*
| Goal | Use |
| -------------------------------------------------------- | --------------------------------------------------------------------------- |
| Prove an ISCC-ID is in its hub's log, trusting nobody | `GET /inclusion/.bundle`, then verify it yourself (recipe below) |
| Show a person the evidence | `GET /inclusion/`, the HTML certificate |
| Quick yes/no, trusting the instance | `GET //log/verify?iscc_id=` |
| Check your own `(size, root)` for a split view | Compare with `GET //log/checkpoint`; prove growth with `consistency` |
| Read a hub's log when the hub is offline | The tlog-tiles mirror: `//log/checkpoint`, `//log/tile/...` |
| Get one record's bytes | `GET //log/entries?index=` |
| Bitcoin timestamp of a root | `GET //log/checkpoint.ots[?size=]`, then `ots verify -d ` |
| Machine-readable API description | `GET /openapi.json` or `/openapi.yaml` (OpenAPI 3.1) |
| Health, build, metrics | `/healthz`, `/version`, `/metrics` |
All routes are `GET`, need no authentication and send `Access-Control-Allow-Origin: *`. Every page:
[HTTP API](reference/http-api.md).
## Constraints *(use)*
| Rule |
| ------------------------------------------------------------------------------------------------------------- |
| ISCC-ID inputs accept any case, surrounding whitespace and an `ISCC:` or `did:iscc:` prefix. |
| The ISCC-ID itself names its hub (12-bit `hub_id`); the instance resolves it through the realm's Hub-List. |
| "Cannot certify" answers are `200`, not 4xx/5xx: the bundle route returns `{"error": ..., "iscc_id": ...}`, verify-for-me returns `"verified": false` with a `reason`. Check the body. |
| A bundle exists only after the instance verified the inclusion itself; it is never partial. |
| `anchor` in a bundle is optional; it is present only when a confirmed Bitcoin anchor covers the record. |
| `key.revoked` (RFC 3339) is present only when the hub's did:web document marks the key revoked. |
| `//log/consistency?from=` accepts any `n` up to the accepted size; a larger `n` is `400` (the instance has not seen that state yet). |
| `checkpoint.ots` answers `404` while the root is not yet anchored. |
| A log's origin is `/log`, for example `iscc.id/log`. |
| Guarantees start at coverage start; an instance cannot vouch for what happened before it watched. |
## Recipe: verify a proof bundle *(use)*
The documented script does every step: [Verify a proof bundle](guides/verify-bundle.md)
(`uv run verify_bundle.py --resolve `). To implement it yourself, with the members from
[Proof bundle format](reference/proof-bundle.md):
| Step | Check |
| ---- | ------------------------------------------------------------------------------------------------------------------------------- |
| 1 | `key.multibase` is base58btc (`z…`); decoded, it is `0xed 0x01` followed by the 32-byte Ed25519 public key. |
| 2 | `checkpoint` is a signed note: body lines origin, size, base64 root; a blank line; signature lines `— `. |
| 3 | The signature base64 decodes to a 4-byte key hash and a 64-byte signature. Key hash = `SHA-256(name + "\n" + 0x01 + pubkey)[:4]`; it equals `key.id` (hex). |
| 4 | The Ed25519 signature verifies over the note body, up to and including its last newline before the blank line. |
| 5 | Leaf hash = `SHA-256(0x00 + base64decode(record))`. |
| 6 | Rebuild the root from the leaf, `inclusion.leafIndex`, `inclusion.treeSize` and `inclusion.inclusionProof` (RFC 9162 §2.1.3.2); it equals the checkpoint root and `treeSize` equals the checkpoint size. |
| 7 | Fetch `https:///.well-known/did.json` yourself; `key.multibase` must be a `publicKeyMultibase` in it. |
| 8 | If `anchor` is present: steps 2–6 for its `checkpoint`, `inclusion` and `key`; then `ots verify -d ` on its base64-decoded `ots` (needs a Bitcoin node). |
Skipping step 7 leaves the monitor in the trust path.
## Architecture map *(change)*
Go 1.26, `CGO_ENABLED=0`, one binary, configured only by `ISCC_MONITOR_*` environment variables, one SQLite file per
network.
| Path | Contains |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| `cmd/iscc-monitor` | `main.go`: config, realm loading, hub registration, HTTP mux, OTS loop, realm refresh |
| `cmd/wasm` | In-browser inclusion verifier, built to `internal/web/verify.wasm` |
| `cmd/notices` | Generates `THIRD_PARTY_NOTICES.txt` |
| `internal/config` | Typed, validated startup configuration |
| `internal/registry` | Hub-List YAML (or legacy domains file) parsing; `AtomicHubList` hub_id resolver |
| `internal/logclient` | Pure verification chain: checkpoint, did:web key, consistency, tiles, fsck, inclusion proofs |
| `internal/didweb` | did:web key resolution, revocation windows |
| `internal/follower` | `Loop` and `PollHub`: the per-hub poll pipeline; freeze; OTS stamping (`OTSTick`) |
| `internal/store` | SQLite: `hubs`, `checkpoints`, `tiles`, `entry_bundles`, `follow_state`, `hub_keys`, `iscc_index`, `ots`, `violations`; `schema.sql` plus `user_version` migrations |
| `internal/proof/verify` | Pure `VerifyInclusion`, shared by server and WASM |
| `internal/proofserve` | `//log/inclusion`, `consistency`, `entries`, `verify` |
| `internal/tilesserve` | `//log/checkpoint`, `tile/...`, `checkpoint.ots` |
| `internal/certificate` | `/inclusion/` and its `.bundle`; Trust Seal |
| `internal/dashboard`, `dossier` | `/` and `/` pages |
| `internal/chrome`, `badge`, `web` | Shared masthead and footer, status badge, `/_ds/` static assets |
| `internal/openapi`, `docs` | `openapi.yaml` + `openapi.json`, `/docs` API reference page |
| `internal/index` | ISCC-IDv1 decoding and `Canonical` |
| `internal/ots`, `otsclient` | OpenTimestamps proofs and calendar transport |
| `internal/metrics`, `metricshttp`, `healthz`, `version`, `corsmw` | Operations routes and the CORS middleware |
| `docs/`, `zensical.toml` | This site |
| `.claude/` | PRD, ADRs and design specs; not public docs |
Runtime: `run()` starts the HTTP server, the OTS loop and the hourly realm refresh as goroutines; `follower.Loop` runs
in the foreground and polls each hub (`ISCC_MONITOR_NORMAL`, or `ISCC_MONITOR_FROZEN` for frozen hubs).
Poll pipeline (`follower.PollHub`): fetch checkpoint → `AcceptCheckpoint` verdict (signature under the did:web key)
→ only if `verified`: mirror the tiles → RFC 6962 consistency against the prior accepted checkpoint → on a
violation, freeze the hub, keep both signed checkpoints and alert once; otherwise record the checkpoint and coverage,
advance the follow cursor, rebuild the root from the mirror (fsck) and record the root for OpenTimestamps. A frozen
hub is re-polled for evidence only and never advances.
## Change playbook *(change)*
| If you change | Also |
| -------------------------------------- | ---------------------------------------------------------------------------------------------- |
| An `ISCC_MONITOR_*` key | `docs/operate/configuration.md`; `internal/config` has a test that fails when the two differ |
| A machine route | `internal/openapi/openapi.yaml` and `openapi.json` (tests check they agree and match the mux); `docs/reference/http-api.md` |
| An HTML route | `docs/reference/http-api.md`; keep it out of the OpenAPI document |
| The proof bundle | `docs/reference/proof-bundle.md`, `docs/guides/verify_bundle.py`, the example `docs/reference/MEIGKV5N6NTVF4AB.bundle.json` (the docs workflow runs the script on it) |
| `internal/proof/verify` or `cmd/wasm` | `mise run build:wasm`, then `web.WasmVerifyHash`; a test pins the hash of the committed file |
| A metric or a log line | `docs/operate/monitoring.md` |
| The database schema | Append a migration step (`user_version`); never edit an applied one; `docs/operate/data.md` |
| A dependency or vendored asset | `mise run notices`; a new vendored asset also goes into `vendoredAssets` in `cmd/notices` |
| Operator branding | `docs/operate/branding.md` |
| Anything user-visible | `CHANGELOG.md`, written by hand for readers |
| The public API, CLI or configuration | Update this page |
Gates: `mise run check` (build, vet, test) and `gofmt -l .` must be clean; for docs, `mise run docs:build` and
`mise run docs:check` must end with `0 errors, 0 warnings`.
## Common mistakes
**NEVER** treat a `200` from the bundle or verify route as success. **ALWAYS** read `error`, `verified` and `reason`.
---
**NEVER** trust the `key` in a bundle on its own. **ALWAYS** check it against the hub's did:web document.
---
**NEVER** call the Comparison anchor an "anchor" alone or a "witness". **ALWAYS** reserve "anchor" for Bitcoin
anchoring.
---
**NEVER** claim the monitor detects split views on its own. **ALWAYS** say it detects self-consistency violations;
split views need a comparison against it.
---
**NEVER** advance accepted state for a hub that is not `verified`, or for a frozen hub. **ALWAYS** keep frozen hubs
evidence-only.
---
**NEVER** add a request to another origin to a page (CDN, fonts, analytics). **ALWAYS** serve assets from `/_ds/`.
---
**NEVER** commit to `main` or create tags from an automated loop. **ALWAYS** release through a PR from `develop`
with a merge commit, as in `CLAUDE.md`.