Skip to content

Verify a receipt

The whole point of HardState is that you never have to trust the institution, the API, or us. The Verifier recomputes every link — from the raw event bytes all the way to a Bitcoin block header — in your browser, against public explorers. A HardState Proof (.hardstate.json) bundles everything needed to check one event offline.

Integrity

An anchored event cannot be altered after the fact.

Attribution

It was signed by a key the issuer’s own log authorized at that moment.

Completeness

No anchored event can be removed or reordered without breaking the chain.

Time

It provably existed before a specific Bitcoin block.

What it does not prove: the truth of the content, or that an act which was never recorded did not happen.

  1. Open app.hardstate.net/verify and drop in a .hardstate.json proof — or open a receipt directly at https://app.hardstate.net/r/<hash>.

  2. The Verifier runs entirely client-side (@hardstate/core, verifyProof). It shows each check below with its own pass / fail, and reports the earliest confirmed Bitcoin block time as “existed before”.

A proof is a self-contained JSON bundle. batch, ots, and anchor are absent until they become available — fetch the proof again later (GET /v1/proofs/<hash>) to complete it.

{
"hardstate": "proof", "hss": 1,
"subject": { "kind": "event", "signed": { "event": {…}, "sig": {…} }, "hash": "…" },
"authority": [ /* issuer.create and the key.add / session.open establishing the signer */ ],
"batch": { "id": "…", "root": "…", "size": 8, "index": 5, "path": ["…"] },
"ots": "<base64 of the detached .ots for batch.root>",
"anchor": { "network": "bitcoin", "txid": "…", "root": "…", "size": 1, "index": 0, "path": [] },
"logUrl": "https://api.hardstate.net/v1/issuers/<id>/log",
"issuedAt": "…"
}

A verifier performs — and should show — each step:

  1. Integrity. SHA-256(canonical(subject.signed.event)) == subject.hash (canonical JSON is RFC 8785).

  2. Signature & authority. Starting from issuer.create (self-signed), each key.add or session.open in authority is signed by a key established earlier and allowed to sign it; the event’s actor was established at a lower seq; its scope allows the event’s type and ts; and the signature verifies. For a session key, the verdict names the person who opened it, and the session-expiry rule applies against Bitcoin time.

  3. Batch. The leaf (0x01 ‖ hash for an event, 0x02 ‖ digest for a citizen stamp) combined with batch.path recomputes batch.root — an RFC 9162 audit path.

  4. OpenTimestamps. The .ots is a proof for batch.root; for each Bitcoin attestation at height h, the attested message equals the merkle root of block h. The block header is fetched from at least two independent sources that must agree.

  5. Anchor. 0x03 ‖ batch.root with anchor.path recomputes anchor.root; transaction anchor.txid carries the OP_RETURN script for anchor.root and is confirmed — same two-source rule.

  6. Document (optional). With a file and its salt, the recomputed commitment is one of event.docs.

  7. Completeness (optional, online). The issuer’s full log replays from seq 0 without error and holds this exact event at its seq.

Verdict Meaning
invalid Any performed step failed.
valid Checks 1–3 pass and at least one of OpenTimestamps (4) or Anchor (5) passes on Bitcoin mainnet.
pending Otherwise — e.g. the event is batched but not yet confirmed on Bitcoin.

You do not have to use HardState’s Verifier at all. The OpenTimestamps path is a standard detached .ots over the batch root, so any OpenTimestamps client works:

Terminal window
# Fetch the detached .ots for a batch and verify it against Bitcoin:
curl -s https://api.hardstate.net/v1/batches/<id>/ots -o batch.ots
ots verify -d <batch_root_hex> batch.ots

For the daily HardState Anchor, read the transaction’s OP_RETURN on any block explorer and confirm the script is:

6a 24 48 53 53 01 <32-byte anchor root> # OP_RETURN PUSH36 "HSS" ‖ 0x01 ‖ root

then recompute 0x03 ‖ batch.root up anchor.path to that root.

import { verifyProof } from "@hardstate/core";
const verdict = await verifyProof(proof);
// → { status: "valid" | "invalid" | "pending", steps: [...], existedBefore?: "…" }