# 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`.