# Verify

```bash
signafaide verify                       # the local .signafaide state: your own agent keys and head
signafaide verify pack.json             # a pack from `signafaide export` (every agent; --agent for one)

# an auditor, with the pack and two small files from the customer (the one command, everywhere):
npx signafaide verify pack.json --keys https://<service>/.well-known/signafaide-keys.json --agent-keys agent-keys.json --head head.json
```

The three files have the names the command uses:

- `pack.json`: the verification pack (from the console, an auditor link, or `signafaide export -o pack.json`).
- `agent-keys.json`: from the customer, who runs `signafaide keys export -o agent-keys.json` in the project that runs the agents.
- `head.json`: the customer's `.signafaide/head.json`, the newest receipt their SDK got for each agent. With it, the check fails if any record was cut from the end of the pack. When the customer has not sent one, leave out `--head head.json`: the pack's own signed heads are still checked (see [The head](#the-head)).

An auditor link carries all three when the customer attaches the keys and the head file, and shows this command with the right address.

The Python and TypeScript CLIs give the same result (`npx signafaide verify` for TypeScript). Before anything was sent (no API key yet), `signafaide verify` makes no request: it checks the signatures, chain links and content hashes on your machine, shows the witness's checks as `NOT CHECKED` ("nothing co-signed yet"), and ends with what is still missing:

```
co-signature, public timestamp: not yet. Run `signafaide init` to send hashes, or self-host (SIGNAFAIDE_URL)
PASS: 4 of 12 checks passed, 8 not checked (nothing co-signed yet)
```

Once records are co-signed, the local state holds their receipts but not the hourly anchors: a batch less than 2 hours old shows anchors and public timestamp as `PENDING`, as a fresh pack does. For an older batch, verify an export (`signafaide export -o pack.json`, then `signafaide verify pack.json`) to check its public timestamp.

## The checks

| # | check | passes when |
|---|---|---|
| 1 | Envelope schema | the pack holds at least one record, and every envelope has exactly the required fields with valid values and names the pack's project and env |
| 2 | Record hashes | each record hash matches its envelope |
| 3 | Client signatures | each record is signed by a key from the customer's agent keys that was active at the record's time and not revoked before the service received it |
| 4 | Agent chains | sequence numbers and previous-record links are unbroken per agent, times never go back, and no record is dated more than 300 s after its batch |
| 5 | Merkle proofs | each receipt's proof leads to its batch root |
| 6 | Batch hashes and signatures | the service signed each batch |
| 7 | Batch chain | the project's batches link up and their times never go back |
| 8 | Signed heads | the service signed the export statement (what the pack covers, when it was made, and exactly which records it holds) and each head; every record is inside the export's filter; each agent's records reach its newest head (in the pack, and in `--head`) |
| 9 | drand rounds | each batch was written after its round and at most 120 s later, and the round's BLS signature verifies (every verifier checks it; without a BLS library a verifier says `NOT CHECKED`) |
| 10 | Anchors | each batch's path into its anchor, the anchor signature, the service key list it commits to, and its time; a batch older than two hours that no anchor covers shows `not timestamped` |
| 11 | OpenTimestamps | each public timestamp covers its anchor and matches the public record it names: `confirmed` passes; `pending` (co-signed, the public timestamp not confirmed yet) is counted as pending, not passed; `not checked` offline |
| 12 | Content hashes | the content you hold and its salt hash to each record's `content_hash`: `sha256(salt || JCS(content))`; content without its salt fails |

The output is one line per check, then the result line. It says how many of the 12 checks passed, which are pending, which could not run, what the pack covers and when the service made it. Whenever a check is pending, a line under the checks says what that means: co-signed, waiting for the public timestamp (an anchor is made every hour and usually confirms within a few hours); never counted as passed.

```
PASS: 12 of 12 checks passed; export of every agent, all records, generated 2026-11-13T12:00:00.000Z
PASS: 11 of 12 checks passed, 1 pending (public timestamp); export of every agent, all records, generated 2026-11-13T12:00:00.000Z
PASS: 11 of 12 checks passed, 1 not checked (public timestamp); export of every agent, all records, generated 2026-11-13T12:00:00.000Z
PASS (agent keys taken from the pack, not independent): 12 of 12 checks passed; export of agent billing-bot, records from 2026-11-01T00:00:00.000Z, generated 2026-11-13T12:00:00.000Z
FAIL: 1 of 12 checks failed (record hashes), 9 passed, 2 not checked; export of every agent, all records, generated 2026-11-13T12:00:00.000Z
```

Most checks run offline. The public timestamp check needs a network lookup of the block it names (or `--timestamp-check` pointing at an explorer you run); with `--offline` it is reported as not checked.

`PENDING` and `NOT CHECKED` never count as a pass, and nothing is called confirmed that was not checked. A pack with no records fails (`no records`): an empty pack proves nothing. The service signs each pack's export statement, so anyone who passes the pack on cannot drop, add or reorder records, or change its filter, heads or time, without check 8 failing. A record missing inside an agent's chain fails. A pack that ends before the agent's newest head fails (records cut off at the end). Only an export made with `--from` may start mid-chain (and only when `--from` is after the agent's first key), and only one made with `--to` may end before the head. Two different records with the same sequence number fail.

## In code

`signafaide.verify()` (Python) or `await verify()` (TypeScript) returns a report whose `ok` is true or false.

```python
import signafaide

report = signafaide.verify()        # or signafaide.verify("pack.json")
print(report)                       # one line per check, then the result line
assert report.ok
```

```ts
import { verify } from "signafaide";

const report = await verify();      // or await verify("pack.json")
console.log(String(report));        // one line per check, then the result line
if (!report.ok) process.exit(1);
```

## Exit codes

`0` PASS, `1` FAIL, `2` when an input can't be read (a missing file, invalid JSON, no service keys, an agent-key file whose self-signatures do not check).

## Service keys

The service keys must come from a source that is not the pack: `--keys <file|url>`, or by default the service's `/.well-known/signafaide-keys.json`. `--trust-embedded-keys` uses the keys inside the pack and prints a warning, because a forged pack could carry forged keys. Keys over plain `http://` are accepted only from this machine (for example `http://127.0.0.1`), with a warning, and do not count as independent; from anywhere else they are refused. Each anchor commits to the key list (`keys_hash`), and the public timestamp covers the anchor, so a key list rewritten later fails check 10.

## Agent keys

The agent keys must also come from a source that is not the pack: otherwise the service could add a key of its own and sign records in your agent's name. The customer writes them with:

```bash
signafaide keys export -o agent-keys.json
```

and gives the file to the auditor (or publishes it). Each entry is signed by its own key. A key replaced with `signafaide keys rotate` stays in the file with the time the service retired it, signed by that key: records it signs later fail check 3. The auditor passes `--agent-keys agent-keys.json`. Without it, check 3 says `NOT INDEPENDENT: agent keys came from the pack` and the result is `FAIL`; `--trust-embedded-agent-keys` accepts the pack's keys and the result then reads `PASS (agent keys taken from the pack, not independent)`. On your own machine, `signafaide verify pack.json` uses this project's own agent keys in `.signafaide/agents/` automatically.

## The head

`--head head.json` (the customer's `.signafaide/head.json`) is the newest signed head the customer's SDK received for each agent. The verifier fails a pack that ends before it, so records cut off at the end cannot hide, even by the service. On your own machine `signafaide verify pack.json` uses this project's `.signafaide/head.json` automatically, and `signafaide verify` with no pack always checks the local head.

## The public timestamp check

Public timestamps use OpenTimestamps, an open standard that commits the hash to the Bitcoin blockchain; anyone can check it independently. `verify` looks up the block each proof names (`--timestamp-check <url>`, default `https://mempool.space/api`; `https://blockstream.info/api` works too), checks that the block's header hashes to that block and carries its proof of work, and shows `confirmed` only when the proof matches that header. With `--offline`, over plain `http://`, or when the lookup fails, the check shows `NOT CHECKED` and the detail names the block the proof claims: it is never shown as confirmed.

## The drand check

Both SDKs check it by default: `@noble/curves` is a dependency of the TypeScript SDK, and `py_ecc` of the Python SDK (the Python install is about 40 MB in all, about 23 MB of it the BLS library and its dependencies). If the BLS library cannot be loaded, `verify` says `drand: not checked` and the drand check shows `NOT CHECKED`, never a pass. The round's time is checked either way.

## BLOCKED chains and CI

`signafaide verify` with no pack also says when a chain cannot ship. A chain is BLOCKED when the witness refused its records (a revoked agent key, a record dated before its key or more than 5 minutes ahead of the witness's clock) or when it was written so that no witness can take it (an older SDK let the time go back after a clock step). The line names the chain, the first record and the next step:

```
BLOCKED prj_01JCZQ8Z5X3V7N2M4K6H8G0F1D/live/claims-bot: seq 7 has time ..., earlier than the record before it ...; 18 records cannot ship. Next: run `signafaide repair --agent claims-bot` ...
FAIL: 1 chain cannot ship (prj_01JCZQ8Z5X3V7N2M4K6H8G0F1D/live/claims-bot); 9 of 12 checks passed, 3 not checked (...)
```

A BLOCKED chain always fails (exit 1), and so does `signafaide status`. For a CI gate, add `--fail-on-pending-send`: it also fails while any record waits to be co-signed. `signafaide repair` sets the records that can never ship aside in `.signafaide/held/` (kept, and still checked by `verify` for their signatures, links and content hashes), gives the agent a new key, and writes a `signafaide.repair` note that commits to the held records' hashes, so the new segment's first co-signed record fixes them in time. Co-signed records are never touched.

## When verify fails

A failure is evidence: do not edit `.signafaide/` to make it pass. The failing lines name the record and the reason. See [Errors](/docs/errors) for service errors and [Concepts](/docs/concepts) for what each part proves.
