From zero to a proof you verified yourself

Record an entry, fetch its proof, and check it on your own machine with a tool that talks to nothing we run. You don't have to trust us — in about ten minutes you can confirm that for yourself.

When each layer of proof arrives

A proof is built in layers, and they don't all arrive at once. Knowing the schedule first means nothing below looks broken when it is working correctly.

Layer Available What it establishes
Ed25519 receipt Immediately We saw this exact entry at the stated time
RFC 6962 inclusion proof Within about 60 seconds The entry sits in a Merkle tree it cannot be quietly removed from
OpenTimestamps commitment With the first proof bundle A calendar has committed to the batch root
Bitcoin attestation Later, typically hours That commitment is embedded in Bitcoin's history

Steps 1 to 4 take you through the inclusion proof. The Bitcoin attestation arrives on its own schedule; step 5 covers how to read its status.

1. Get an API key

Two ways in, depending on whether a person or a program is signing up. Both produce the same kind of key, and both show it exactly once.

If you are a person

Sign up at app.trustnotch.com/signup, confirm your email, then create a key from the API keys panel on your dashboard. Hold up to five at a time; revoke any of them whenever you like.

If you are an agent

Signing up takes two calls and no human. Request a challenge, solve a small proof-of-work, submit the solution. The work is plain SHA-256 with nothing outside the standard library — it makes bulk account creation expensive, and that is all it is for.

import hashlib, json, urllib.request

BASE = "https://api.trustnotch.com"
SEP = b"|"

def post(path, body=None):
    data = json.dumps(body).encode() if body is not None else b""
    req = urllib.request.Request(
        BASE + path, data=data,
        headers={"Content-Type": "application/json"}, method="POST")
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())

def leading_zero_bits(d):
    bits = 0
    for byte in d:
        if byte == 0:
            bits += 8
            continue
        bits += 8 - byte.bit_length()
        break
    return bits

# Ask for a challenge.
ch = post("/v1/accounts/challenge")

# Find a nonce whose digest has enough leading zero bits.
n = 0
while True:
    nonce = str(n)
    msg = SEP.join((ch["random"].encode(), str(ch["issued_at"]).encode(), nonce.encode()))
    if leading_zero_bits(hashlib.sha256(msg).digest()) >= ch["difficulty_bits"]:
        break
    n += 1

# Submit it. The key is in account["api_key"], shown only here.
account = post("/v1/accounts", {
    "random": ch["random"],
    "issued_at": ch["issued_at"],
    "signature": ch["signature"],
    "nonce": nonce,
})

print(account["account_id"], account["plan"], account["included_monthly"])

At the default 20 bits that search takes roughly a million attempts — a second or two in Python, less in a compiled language. The challenge lasts 300 seconds and is bound to the IP that asked for it, so solve and submit from the same host. New accounts start free with 10,000 entries a month and no card.

2. Record an entry

Send the entry with your key as a bearer token. Two submission modes; pick one per entry.

Standard — send the payload

We canonicalize your JSON, hash it, and store both.

curl -X POST https://api.trustnotch.com/v1/logs -H "Authorization: Bearer $TN_KEY" -H "Content-Type: application/json" -d '{"payload":{"agent":"support-bot","action":"tool_call","tool":"search"}}'
{
  "id": "01a015d7-2d97-74d2-9d02-b268349a9c56",
  "content_hash": "eb3bca757a55f2d2c8640195882504a862e1e11c9f3fb399fce74e2893a825d8",
  "created_at": "2026-08-18T17:06:58.582994Z",
  "receipt_signature": "N7uipFCbADukNcoLCa848V5u6BWC9IH...",
  "receipt_key_id": 1
}

receipt_signature is an Ed25519 signature over the entry's leaf hash, made with the published key named by receipt_key_id. Step 4 checks it.

Zero-PII — send only the hash

If the content is personal data, hash it yourself and send the digest alone. We never receive the underlying bytes, so there is nothing in our database to leak, disclose, or erase. Under the EU AI Act, this is usually the mode you want.

# Hashed locally. The bytes never leave your machine.
H=$(printf '%s' '{"subject":"alice@example.com","decision":"approved"}' | sha256sum | cut -d' ' -f1)

curl -X POST https://api.trustnotch.com/v1/logs -H "Authorization: Bearer $TN_KEY" -H "Content-Type: application/json" -d "{\"content_hash\":\"$H\"}"

The response has the same shape, and the content_hash it returns matches the digest you computed. Receipt, inclusion and anchoring all work exactly as they do in standard mode. Send neither field or both and the request is rejected.

3. Fetch the proof

Ask for the bundle using the id from step 2.

curl https://api.trustnotch.com/v1/logs/$LOG_ID/proof -H "Authorization: Bearer $TN_KEY" -o bundle.json

Try it immediately and you get a 409. That is correct: entries are batched into a Merkle tree on a roughly 60-second timer, and there is no inclusion proof before the batch exists.

{"detail":"proof not available yet: entry not batched (retry within ~60s)"}

Wait a minute, ask again, and you get the leaf the proof commits to, the signed receipt, the inclusion proof, and the anchor state:

{
  "version": { "proof_format": 1, "leaf_format": 1 },
  "entry": {
    "id": "01a015d7-2d97-74d2-9d02-b268349a9c56",
    "account_id": "01a015d6-69e4-7100-9731-db878bda2336",
    "content_hash": "eb3bca757a55f2d2c86401958825...",
    "created_at": "2026-08-18T17:06:58.582994Z"
  },
  "leaf_hash": "b78311af2fc8d109113e9996d880653b...",
  "receipt": { "signature": "37bba2a4509b003ba435...", "key_id": 1 },
  "inclusion": {
    "leaf_index": 0,
    "tree_size": 2,
    "merkle_root": "f1d63cfac67f57530ff75fb10c967fb0...",
    "audit_path": ["0b45a2aeda5efacbce02b0f74b0db7cf..."]
  },
  "anchor": {
    "ots_status": "pending",
    "bitcoin_block_height": null,
    "bitcoin_block_hash": null,
    "bitcoin_tx_id": null,
    "confirmations": 0,
    "ots_proof": "004f70656e54696d657374616d7073..."
  }
}

The bundle is self-contained. Everything needed to check it is in the file, except the public keys — and those ship inside the verifier rather than being fetched from us.

4. Verify it yourself

This is the step that matters. The verifier is a separate open-source package carrying its own copy of the trusted key directory.

pip install trustnotch
trustnotch verify bundle.json
receipt:            pass
inclusion:          pass
anchor_structural:  na
reported anchor:    pending
errors:
  - ots_proof commits the root via a calendar attestation but is not yet anchored to a Bitcoin block
verified

Exit code 0 means verified, 1 means a tier failed, 2 means the file could not be read. Note the errors line on a passing run: it reports that the Bitcoin attestation hasn't landed, which for a minutes-old entry is expected, not a fault.

Prove it can say no

A verifier that approves everything is worthless. Change one character anywhere in the bundle and run it again:

receipt:            fail
inclusion:          fail
errors:
  - echoed leaf_hash does not match recomputed value (recomputed used)
  - receipt signature does not verify against the recomputed leaf
  - inclusion proof does not reconstruct the stated merkle_root
NOT verified

One altered character breaks three independent checks and the exit code becomes 1. Do this once, yourself, before trusting anything else here.

Verifying with no network

--offline skips anything that would reach the network. Be precise about what that buys: the inclusion proof is fully checkable offline, but the receipt comes back na rather than pass, because checking a signature needs the key directory. --json gives machine-readable output.

5. What we claim, and what we don't

Anchor status moves pending to confirmed to final. Every bundle is served at whatever stage it has reached and labelled honestly, so you choose the threshold your use case needs.

  • Entries are anchored in batches, not individually. Many entries across many accounts share one Merkle root and one OpenTimestamps attestation. There is no Bitcoin transaction per entry — anyone claiming otherwise is describing something nobody could afford.
  • Anchor metadata is reported, not trusted. The verifier surfaces our recorded status, height and confirmations as our observation. It does not treat them as a trust root, and neither should you.
  • Matching a commitment to the real chain is outside the offline verifier. That needs a block header source. The verifier checks structure and commitment; confirming the block is a step you can take independently.
  • A receipt proves we saw the entry, not that it is true. It establishes that this exact content existed at that time and has not changed since. Nothing more.

Next

The full endpoint reference, with request and response schemas, is published as OpenAPI at api.trustnotch.com/docs.