receipts.youSeal a screenshot
Methodology · Folio M
full technical walk-through
§ How it works

The whole machine, end to end.

The shortest possible summary: when you drop a screenshot, your browser computes its SHA-256 hash and sends the 32-byte hash (never the image) to a Cloudflare Worker. The Worker signs the hash with our ECDSA P-256 key, mints a short receipt ID, and asynchronously anchors the hash into the timestamp network via OpenTimestamps. Your browser then composites a QR code onto the image, hashes the composite, and tells the Worker that stamped hash too — so neither the clean original nor the stamped version can be substituted without detection.

The data flow, step by step

  1. You drop a screenshot at /seal. The file goes into a Blob in JavaScript memory.
  2. Hash in browser. We call crypto.subtle.digest("SHA-256", blob) — the standard Web Crypto API, built into every browser. The result is 32 bytes / 64 hex characters.
  3. POST /api/seal with { original_hash, note?, source_url? }. The Worker receives the hash, mints an 8-char base62 receipt ID, signs a canonical-JSON payload { format, id, original_hash, signed_at, signer_kid } with ECDSA P-256 / SHA-256, persists a row to D1, and fires off a non-blocking OpenTimestamps submission to three external anchoring calendars in parallel.
  4. Worker responds with { id, url, signed_at, signature_b64, signer_kid, original_hash }.
  5. Generate QR + composite. Browser renders a branded QR (rounded modules, editorial palette, center r-mark) via qr-code-styling at error-correction level H, composites it onto the original image at the bottom-right corner with a § SEALED · RECEIPTS.YOU kicker and r/<id> label, and exports the result as PNG via canvas.toBlob().
  6. Hash the composite. Same SHA-256 routine, applied to the stamped PNG bytes.
  7. POST /api/seal/finalize with { id, stamped_hash }. The Worker performs UPDATE receipts SET stamped_hash = ? WHERE id = ? AND stamped_hash IS NULL — anti-tamper: once recorded, the stamped hash is immutable.
  8. You download the stamped image and share the receipt URL. Anyone scanning the QR lands at receipts.you/r/<id>, which is rendered server-side by the same Worker reading the same D1 row.
  9. Verification: the verification page lets anyone drop the image (either clean original or stamped composite) to compute its SHA-256 locally and POST { id, hash } to /api/verify. The Worker returns one of: match_original, match_stamped, or mismatch.

The dual-hash anti-tamper design

We store two hashes per receipt — original and stamped — and the receipt verifies against either. This blocks the obvious attack:

  • Attack: copy a real QR from someone else's sealed screenshot, paste it onto a fake image, claim the QR is your receipt.
  • Defense: the verification page computes the SHA-256 of the image you uploaded. It matches neither the original_hash (the clean original someone else sealed) nor the stamped_hash (the stamped composite they shared) → mismatch verdict. Page tells the verifier: “this QR is real, but the image attached to it is not.”

The QR is just a discoverability mechanism — it tells the verifier where to look up the canonical hashes. The hashes are the source of truth.

The cryptography

  • Hash: SHA-256. 256-bit collision resistance. The Web Crypto API is hardware-accelerated and identical in browsers, Workers, and openssl.
  • Signature: ECDSA P-256 (a.k.a. prime256v1, secp256r1) with SHA-256. Industry standard, NIST-approved, supported natively by every modern client.
  • Signing key storage: an encrypted Wrangler Secret in Cloudflare's infrastructure. The private key never appears in source code, the marketing site, browser memory, or D1.
  • Public verification key: published at /.well-known/receipts-pubkey.pem. Anyone can re-verify a receipt offline with openssl dgst -verify.

The external timestamp anchor (OpenTimestamps)

Every hash is also submitted to the OpenTimestamps calendar network as a parallel, independent proof of existence. The calendars aggregate many digests into a Merkle tree, mine the root into an anchor transaction, and publish an upgraded proof that walks the Merkle path from your hash all the way to a confirmed anchor block header.

Why it matters: if receipts.you disappears tomorrow, the upgraded OTS proof still verifies independently. The trust chain collapses to the timestamp network's proof-of-work, which is uncensorable and has fifteen years of history backing it. We use this as a defense-in-depth, not the primary trust mechanism, but it's there for the paranoid.

What we store

For every receipt, a single row in Cloudflare D1:

id              TEXT (8 chars, base62)
original_hash   TEXT (64 hex chars — SHA-256)
stamped_hash    TEXT (64 hex chars — set on finalize)
signed_at       TEXT (ISO-8601 UTC)
signature_b64   TEXT (base64 ECDSA signature)
signer_kid      TEXT (key identifier, lets us rotate later)
note            TEXT (optional, user-supplied, max 280 chars)
source_url      TEXT (optional, user-supplied, not verified)
ip_country      TEXT (2-letter country code, for abuse rate-limit only)
ua_hash         TEXT (SHA-256(UA + day) — no UA stored)
ots_pending     BLOB (OpenTimestamps pending proof, ~200 bytes)
ots_upgraded    BLOB (post-anchor upgraded proof, set hours later)

Each row is ~400 bytes. One million receipts = ~400 MB, well inside Cloudflare D1's 5 GB free tier. We never store the image itself. Even if a court compelled us to produce “the image for receipt X”, we have nothing — only the hash. The receipt holder is the custodian.

What this proves

  • An image with this exact SHA-256 hash existed at receipts.you at the timestamp above (signed by our key, anchored externally).
  • The image has not been altered since (any modification produces a different hash → mismatch).
  • The QR code on the stamped image is genuine (cryptographically tied to the same receipt ID).

What this does NOT prove

  • That what's depicted in the image is real (we can't verify content; anyone can seal anything).
  • That the original screenshot was authentic at capture time (we sealed what you uploaded; we don't see your screen).
  • That you took the screenshot (we have no way to verify authorship).
  • That a court must accept the receipt as evidence (we are a notary-grade timestamping service, not an admissibility certifier).

The product is byte-level provenance since sealing, not objective truth. If you screenshot a real tweet, seal it, and the tweet later gets deleted, your receipt is dated cryptographic proof the image existed in your possession in this exact form before the dispute. The earlier you seal, the stronger your position. That's the value.

Why Cloudflare-only?

  • Free tier covers MVP scale. Workers (100k req/day), D1 (5GB + 100k writes/day), Pages (unlimited). Zero recurring cost until ~10× our current traffic.
  • Single dashboard, single bill. If we ever pay, it's one place.
  • Edge runtime, low latency worldwide. The sealing endpoint is ~50ms from any populated continent.
  • No AI services anywhere — everything is symmetric crypto + standard algorithms. Per-receipt marginal cost is essentially zero.

Verify offline with openssl

# 1. Get our public verification key
curl -sO https://receipts.you/.well-known/receipts-pubkey.pem

# 2. Reconstruct the canonical-JSON payload from the receipt's JSON
#    (drop signature_b64, sort keys alphabetically, no whitespace)

# 3. Decode the base64 signature
echo "<signature_b64>" | base64 -d > sig.bin

# 4. Verify
openssl dgst -sha256 -verify <(openssl ec -in receipts-pubkey.pem -pubin) \
             -signature sig.bin canonical_payload.json
# Expected: Verified OK

Or just use the verifier in your browser — drop the image at any /r/<id> page, get the verdict in <100ms.