~ $ cat ./guides/integrity

Your pipeline can rebuild an output.
Can it prove it was not changed?

The audit question is never “did the model run?”. It is “how do you know this is what it produced?”. Here is how to answer it with a hash, a stamp and a verification step, and how to tell which of the three you are actually missing.

The symptoms

Diagnose first: identity, integrity, or time?

These are three different problems and three different tools. Stamping fixes the third one. Buying a timestamp for a problem that needs a signature is a common and expensive mistake.

The question you cannot answerWhat you are missingThe tool
“Who produced or approved this?”Identity and assentA signature (ideally qualified), an audit log with authenticated actors
“Has this changed since we stored it?”IntegrityA SHA-256 hash recorded at write time, checked on read
“When did this exist, and can a third party confirm it?”Time, independentlyA timestamp anchored in a public blockchain
“Is this legally the date, with a presumption?”Legal statusA qualified timestamp from a QTSP — not a blockchain stamp

Most pipelines have the first two partly solved and the third not at all. A hash in the same mutable database as the artifact is a checksum, not evidence; the moment it is committed to a public chain, it becomes something a stranger can check. That is the leg a stamp adds, and it is the only leg it adds.

The fix, in four steps

  1. Hash at write time, in the same transaction that stores the artifact. Not in a nightly job: by then the bytes may already have been rewritten by something else.
  2. Build a manifest for the run: every artifact hash, plus the run id, the model or code version and the input hash. One hash of the manifest covers the set.
  3. Stamp the manifest hash (or a batch of artifact hashes when each one needs its own proof). $0.05 per hash; up to 100 hashes per request.
  4. Verify on read and on a schedule: recompute the hash, send it with the stored proof, and treat invalid as an incident rather than a warning.

Code: hash, stamp, store

import { createHash } from 'node:crypto'
import { writeFile } from 'node:fs/promises'

const sha256 = (b: Uint8Array | string) => createHash('sha256').update(b).digest('hex')

// 1. Hash exactly what you are about to store, and store the bytes you hashed.
const artifacts = await Promise.all(outputs.map(async (o) => {
  const bytes = new TextEncoder().encode(o.body)
  return { id: o.id, bytes, hash: sha256(bytes) }
}))

// 2. One manifest per run: what was produced, by what, from what.
const manifest = {
  run_id: run.id,
  produced_at: new Date().toISOString(),
  model: run.model,
  input_hash: run.inputHash,
  artifacts: artifacts.map(({ id, hash }) => ({ id, hash })),
}
const manifestHash = sha256(JSON.stringify(manifest))

// 3. Stamp it. (Pay via your x402 client; see /guides/agents for the wrapper.)
const res = await pay(`${BASE}/v1/stamps`, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ hashes: [manifestHash], webhook: { url: `${HOOK}` } }),
})
const { stamps, receipt } = await res.json()

// 4. Store proof, manifest and receipt together, next to the artifacts.
await writeFile(`runs/${run.id}/manifest.json`, JSON.stringify(manifest, null, 2))
await writeFile(`runs/${run.id}/manifest.ots`, Buffer.from(stamps[0].ots, 'base64'))
await writeFile(`runs/${run.id}/receipt.json`, JSON.stringify(receipt, null, 2))

The manifest is the artifact that matters. Changing any entry changes its hash, which breaks the proof: that is the property you want, and why the manifest should be written once and never edited.

Code: verify on read

async function assertUnchanged(artifact: { hash: string }, proofPath: string) {
  const hash = sha256(await readFile(artifactPath))
  const out = await fetch(`${BASE}/v1/verify`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ hash, ots: (await readFile(proofPath)).toString('base64') }),
  }).then((r) => r.json())

  if (out.status === 'invalid') throw new IntegrityError(out.reason)
  if (out.status === 'pending') return { verified: true, pending: true }
  return { verified: true, pending: false, block: out.bitcoin.height, time: out.bitcoin.block_time }
}

Run this before an artifact is served, exported or used as an input to another step. It is free, it stores nothing, and it returns the block height and time when the proof is complete, which is exactly the sentence an auditor wants: these bytes existed by block N.

Why a manifest, and not a proof per file

A proof per artifact is the strictest option and costs $0.05 each. A manifest is one proof for a set: cheaper, atomic (a run either produced this set or it did not), and easier to hand to a third party, who can verify one .ots and then compare the manifest against the files they were given. Stamp per artifact when each file travels alone and needs its own proof; stamp the manifest when the run is the unit of meaning. Many pipelines do both: a manifest per run, and a proof for the handful of artifacts that leave the building.

Common failure modes

SymptomCauseFix
The hash no longer matches the fileA re-save, metadata change or re-encoding altered the bytesStore artifacts read-only; hash the bytes as stored, not a parsed structure
The proof verifies, but against the wrong hashYou stamped a normalised form and lost the recipeRecord the canonicalisation with the proof; hash the shipped bytes
Status stays pending for hoursThe calendars have not had a Bitcoin confirmation yetWait; poll the status URL, or call /v1/verify to upgrade it
The proof file is goneIt lived only in the pipeline’s own databaseWrite the .ots next to the artifact in immutable storage, with the manifest
A 100-hash batch is rejectedA duplicate hash, or a value that is not 64 hex charactersValidate and deduplicate in the manifest step; a repeated hash is a 400
The same output was paid for twiceA new payment was sent on retryPersist the payment payload; retries with the same body and payment are idempotent
Verification passes on a changed fileYou verified the manifest, not the artifactVerify each artifact against the manifest hashes, and the manifest against the proof

What this does not solve

Checklist

Questions

Is a hash enough, without a timestamp?

A hash proves the bytes match a value. It says nothing about when that value existed, and if the hash is stored next to the artifact by the same system, it proves very little to a sceptic. A timestamp moves the hash somewhere the pipeline cannot rewrite: a public blockchain.

How often should a pipeline stamp?

Once per unit you would have to defend: a run, a report, a published dataset. Continuous logs are usually better stamped as a daily manifest, which is one hash a day rather than one per line.

Can I stamp a database row or an API response?

Yes, if you hash a deterministic serialisation of it and record the recipe. Sort keys, fix the encoding, exclude volatile fields, and hash exactly that. The proof covers the hash; reproducing the hash is your job.

Does stamping leak the data?

No. Only the hash is sent, and each hash is mixed with a random 16-byte nonce before it joins a Merkle tree, so other customers’ proofs do not reveal yours. Your payment is public on chain and linked to the time, not to the hash.

What does a complete proof actually say?

That this hash existed no later than the time of a specific Bitcoin block, accurate to about two hours, and that anyone can check it without trusting us. It does not say who made the file or that its contents are true.

Checked October 2026. Something out of date? Tell us and we will fix it. This page is also served as markdown at /guides/integrity.md.