# 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

- An auditor or customer asks for evidence that a stored report, dataset or model output is the one that was produced, and the answer is a screenshot of a log.
- You can tell whether an artifact matches its hash, but not **when** that hash existed, so a hash kept in the same database as the artifact proves little.
- The service that writes the logs is also the service that could rewrite them.
- A dispute starts weeks after the fact, and the only record of the output is in a system the other side does not trust.
- Someone edited a file in place, the hash changed, and nobody noticed until a verification failed in production.

## 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 answer | What you are missing | The tool |
| --- | --- | --- |
| “Who produced or approved this?” | Identity and assent | A signature (ideally qualified), an audit log with authenticated actors |
| “Has this changed since we stored it?” | Integrity | A SHA-256 hash recorded at write time, checked on read |
| “When did this exist, and can a third party confirm it?” | Time, independently | A timestamp anchored in a public blockchain |
| “Is this legally the date, with a presumption?” | Legal status | A 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

| Symptom | Cause | Fix |
| --- | --- | --- |
| The hash no longer matches the file | A re-save, metadata change or re-encoding altered the bytes | Store artifacts read-only; hash the bytes as stored, not a parsed structure |
| The proof verifies, but against the wrong hash | You stamped a normalised form and lost the recipe | Record the canonicalisation with the proof; hash the shipped bytes |
| Status stays pending for hours | The calendars have not had a Bitcoin confirmation yet | Wait; poll the status URL, or call `/v1/verify` to upgrade it |
| The proof file is gone | It lived only in the pipeline’s own database | Write the `.ots` next to the artifact in immutable storage, with the manifest |
| A 100-hash batch is rejected | A duplicate hash, or a value that is not 64 hex characters | Validate and deduplicate in the manifest step; a repeated hash is a 400 |
| The same output was paid for twice | A new payment was sent on retry | Persist the payment payload; retries with the same body and payment are idempotent |
| Verification passes on a changed file | You verified the manifest, not the artifact | Verify each artifact against the manifest hashes, and the manifest against the proof |

## What this does not solve

- **Authorship.** A stamp does not say who produced the file. Sign it with a key you control, then stamp it.
- **Tampering before the stamp.** If the bytes were wrong when you hashed them, the stamp proves the wrong thing existed. Hash at the point of production, not after a queue.
- **Content truth.** The stamp says nothing about whether the report is accurate, only that these bytes existed by a date.
- **Legal presumption of the date.** That needs a qualified timestamp from a QTSP; a blockchain stamp is admissible in many places but carries no presumption. See [Evidence](https://stampthisfile.com/evidence).

## Checklist

- Every artifact has a SHA-256 recorded at write time, in the same transaction that stored the bytes.
- Each run has a manifest with the artifact hashes, the model or code version and the input hash.
- The manifest is stamped, and the `.ots`, the manifest and the receipt are stored together.
- Verification runs on read, and an `invalid` answer stops the pipeline rather than logging a warning.
- Pending proofs are completed: a webhook, a poll, or `/v1/verify`, and the finished `.ots` replaces the pending one.
- The wallet key is in a secret store, and the x402 client has a maximum price it will pay.
- Somebody has written down what the stamp proves, and what it does not, for the auditor who asks.

## 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. HTML: https://stampthisfile.com/guides/integrity · Agent docs: https://stampthisfile.com/llms.txt_
