TimeLayer Engineering Notes Receipt-Driven Architecture
Engineering Architecture ~18 min read

Receipt-Driven Architecture

A complete engineering guide to building software that keeps no logs — where every meaningful action produces a cryptographic receipt instead of a line in a file. Lifecycle, anatomy, quorum signatures, offline verification, and patterns for real programs.

1. The core idea

Most programs prove they did something by writing a line to a log file. The log lives on the operator's server, it grows indefinitely, and proving it hasn't been edited requires trusting the operator. Receipt-driven programming inverts this model:

Core principle

Every meaningful action produces a receipt — a small (~1.5 KB) cryptographic artifact signed by independent parties — instead of a mutable log entry. The receipt travels with the user. Proof no longer lives on your server.

The action's content never leaves the client machine. You compute a hash locally — SHA-256, BLAKE3, SHA-3, or any standard hash function and send only the fingerprint. The network attests the fingerprint — it never sees what you did, only that you did something at a specific moment, and that the hash is exactly what it is.

Your program
hash(action)
SHA-256 · BLAKE3 · SHA-3 · your choice
POST /v1/notarize
only the hash travels
Receipt (cert + bundle, binary)
~1.5 KB, keep it

Once issued, the receipt is yours. It can be verified offline by anyone — without an account, without calling our servers — using the open-source verifier and the published operator keys on GitHub. Even if TimeLayer shuts down tomorrow, receipts issued today stay valid forever.

2. Receipt lifecycle

The full round-trip from program action to verifiable proof, step by step:

Step 1 — Hash the action locally

Decide what constitutes "the action" (see §6.1) and compute its hash fingerprint. Any standard hash function is accepted (SHA-256 is used in examples; BLAKE3, SHA-3, and others work equally well). The content never leaves your process.

# The content is hashed locally — it never travels to the API
CONTENT="invoice #4471 approved · amount 1200 EUR · 2026-01-15T09:00:00Z"
ACTION_HEX=$(printf '%s' "$CONTENT" | sha256sum | cut -c1-64)
# ACTION_HEX is a 64-char hex string — this is all the API will ever see

What to hash · Which algorithm

Hash a canonical, deterministic string that uniquely identifies the action: what happened + who did it + when + any relevant identifiers. Include enough fields so two different actions always produce different hashes. Timestamp resolution of 1 second is usually sufficient.

TimeLayer accepts any hex fingerprint — SHA-256, BLAKE3, SHA-3, Keccak, or any standard hash. SHA-256 is used in examples because it's universally available; the choice of hash is entirely yours.

Step 2 — POST the hash to /v1/notarize

curl -s -X POST https://api.timelayer-os.com/v1/notarize \
  -H "Authorization: Bearer $YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"action_hex\":\"$ACTION_HEX\"}"

Step 3 — Receive the receipt

The API returns a JSON object. The most important field is cert_hex — this is the receipt. Store it alongside or in place of a log entry.

{
  "cert_hex":       "a3f8…",   ← the receipt, keep this
  "bundle_hex":     "…",         ← full bundle (cert + supporting proof)
  "notarized_at":   "2026-01-15T09:00:01Z"
}

Step 4 — Store and hand off the receipt

In a log-driven program you'd append(log_file, line). In a receipt-driven program you instead:

  • Store cert_hex in your database next to the action record, or
  • Hand the receipt file directly to the user — they now hold the proof, not you, or
  • Both: keep a copy for your own audit trail, give one to the user

Step 5 — Verify at any point, offline

# Anyone can verify — no account, no network call to TimeLayer
timelayer-verifier verify cert.tlcert bundle.tlbundle
# → VALID FINAL

3. Receipt anatomy

A receipt (cert_hex decoded) is a self-contained document. It carries everything needed to verify it — no lookup required at verification time.

{
  /* what was notarized */
  "action_hex":     "…64-char hash…",      ← SHA-256 · BLAKE3 · SHA-3 · any standard hash
  "notarized_at":   "2026-01-15T09:00:01Z",

  /* who signed it — each entry is one independent operator */
  "signatures": [
    {
      "operator":  "operator-1",
      "sig_hex":   "…Ed25519 signature over the cert body…"
    },
    {
      "operator":  "operator-2",
      "sig_hex":   "…"
    }
    /* quorum of operators required */
  ],

  /* network metadata */
  "network_version": 2,
  "quorum":          "satisfied"
}

Key fields explained

  • action_hex — the hash fingerprint you sent (SHA-256 in examples; any standard hash is accepted). The receipt binds this exact value, no other.
  • notarized_at — when the network attested your action.
  • signatures — Ed25519 signatures from independent operators. Each signature covers the full cert body (action_hex + notarized_at + network metadata). A change to any field breaks all signatures.

Size

A receipt is about 1.5 KB (binary cert+bundle). This is fixed regardless of what you notarized — whether you hashed a single byte or a 10 GB file, the receipt is the same size.

TermWhat it isSize
Certificate (cert.tlcert)minimal signed proof~0.4 KB
Bundle (bundle.tlbundle)data for offline verification~1.1 KB
Full verification packagecertificate + bundle~1.5 KB

4. Quorum signatures

A receipt signed by a single key proves nothing about independence: the key holder could sign anything. TimeLayer's trust model is an independent quorum attestation.

The network has independent operators, each holding their own Ed25519 signing key. A receipt requires signatures from a quorum for it to be valid. This has several important consequences:

  • No single point of fabrication. Even TimeLayer itself — as just one operator — cannot issue a valid receipt alone. A quorum of independent operators must agree the action happened.
  • No single point of failure. If one operator is offline, the remaining operators still form a quorum and receipts continue to be issued.
  • Tamper-evidence is shared across operators. An attacker who compromises one operator cannot retroactively alter receipts — the other operators' signatures would fail verification.

Single-signer notary

One key signs. Forge the key → forge any receipt. Operator is the single trust anchor. Receipts are only as trustworthy as that one party.

Quorum (TimeLayer)

Two independent keys must agree. Compromising one party doesn't break receipts. No single operator — including us — can fabricate a receipt.

Published operator keys

Every operator's public key is published openly (GitHub). The verifier fetches the published set once and can then operate fully offline — it just checks Ed25519 signatures against the known keys. You can cross-check the published keys yourself at any time; no trust in the verifier binary is required to know what keys are active.

5. Offline verification algorithm

Verification is deliberately designed to require zero network calls and zero trust in any running service. Here is what the verifier does, step by step:

  1. Decode cert_hex — parse the JSON certificate from the hex encoding.
  2. Use the embedded operator keys — the verifier ships with the canonical set of operator keys embedded (no network needed). Each operator maps to an Ed25519 public key; the same keys are also published on GitHub for cross-check.
  3. Reconstruct the signed message — build the same canonical byte string from action_hex + notarized_at + network_version + quorum. The exact serialization is deterministic and documented in the open-source verifier.
  4. Verify each signature — for each entry in signatures, run Ed25519 verify: verify(public_key[operator], signed_message, sig_hex).
  5. Check quorum — count valid signatures. If a quorum of known operators signed, the receipt is valid.
  6. OutputVALID FINAL or UNVERIFIABLE with the reason.

What "valid" means

A VALID result means: this exact action_hex was attested at notarized_at by a quorum of independent network operators, and the certificate has not been modified since it was issued. It does not mean TimeLayer knows what the action was — it attested only the hash.

What verification doesn't cover

Verification confirms the receipt is internally consistent and genuinely signed by the network. It does not prove the hash in action_hex corresponds to any specific real-world action — that link is yours to maintain (store the original action data + its receipt together).

6. Design patterns

6.1 Defining an action boundary

The hardest design decision in receipt-driven programming is often "what exactly is the action?" The rule of thumb: one action = one commitment you want to be able to prove independently.

  • Too coarse — "today's batch of 10 000 file moves" hashed as one blob. You can prove the batch happened, but not any individual move within it.
  • Too fine — every internal function call or state transition. You'll notarize thousands of trivial events and the receipts become noise.
  • About right — a discrete, meaningful event: "invoice #4471 approved", "file x moved from A to B", "agent decided to send email to user Y".

A practical test: could a third party meaningfully dispute or verify this action on its own? If yes, it's worth a receipt.

6.2 Fail-closed integration

The most important pattern in receipt-driven programs. In log-driven code, a failed write to the log is usually silently swallowed — the action proceeds, the proof doesn't. In receipt-driven code, make notarization a gate:

# Pseudo-code — fail-closed pattern
# 1. Prepare the action (validate, build state changes)
action_data = prepare_invoice_approval(invoice_id, amount, approver)
action_hash = sha256(canonical(action_data))

# 2. Notarize BEFORE committing
resp = notarize(action_hash)   # raises if network unreachable

# 3. Commit only if we have a receipt
db.commit(action_data, receipt=resp.cert_hex)
return resp

If notarize() raises — network down, quota exceeded, timeout — the action is not committed. Your system stays provable: every committed action has a receipt; no committed action is ever unwitnessed.

Idempotency and retries

If notarization succeeds but your commit fails, you hold a receipt for an action that didn't happen. Handle this with a transaction: either both commit or neither does. The receipt for the uncommitted action is harmless — it attests a hash that isn't in your database.

6.3 Receipt store design

Where to keep receipts depends on who needs to verify them:

  • Database column — simplest. Add a receipt_cert TEXT column to your actions table. Verifiable by any party with DB access.
  • Filesystem (one file per receipt) — good for file processors. Name the file after the action hash: {action_hex[:16]}.cert. Portable, no DB required.
  • Hand to user on completion — best for user-facing actions. The receipt is the "your order is confirmed" attachment. The user holds the proof; your server could burn down and their receipt still works.
  • Embedded in artifact — for documents: append the receipt to the PDF metadata or sidecar .cert file. Proof travels with the document for its lifetime.

6.4 Idempotency

The same hash value always produces a consistent, verifiable receipt — but the network will issue a new receipt for each notarization call, with a different notarized_at timestamp. If you want exactly one receipt per action, enforce that at your application layer: check whether a receipt for this action already exists before calling the API.

7. The two-program demo

The best way to see the difference is two programs that do the same job — watch a folder, hash each incoming file, move it to an output folder — built in two different architectural styles.

log-driven/

Every file move appends a line to processor.log. After processing 250 files: ~40 KB log, growing. Proof of any one move: scroll the log and trust no one edited it.

receipt-driven/

Every file move calls notarize(sha256(move)). After processing 250 files: 250 receipt pairs (cert.tlcert + bundle.tlbundle), ~1.5 KB each. No log. Proof of any one move: timelayer-verifier verify cert.tlcert bundle.tlbundle → VALID FINAL, offline.

// receipt-driven/main.rs (simplified)
fn process_file(path: &Path) -> Result<Receipt> {
    // 1. compute the canonical action string
    let action = format!(
        "move·{}·{}·{}",
        path.file_name(), sha256_file(path), utc_now()
    );

    // 2. hash locally — content stays here
    let h = sha256(action.as_bytes());

    // 3. notarize the hash — only h travels over the network
    let resp = client.notarize(&h)?;   // fail-closed: ? propagates error

    // 4. write the receipt — two files: notarial cert + gate bundle
    let dir = output_dir.join(&h[..16]);
    write(dir.join("cert.tlcert"),     hex::decode(&resp.cert_hex)?)?;
    write(dir.join("bundle.tlbundle"), hex::decode(&resp.bundle_hex)?)?;

    // 5. only now move the file
    fs::rename(path, output_dir.join(path.file_name()))?;
    Ok(receipt)
}

Key observation: the function returns Result — if notarization fails, the ? propagates the error and the file is not moved. The receipt precedes the action at the commit point. This is fail-closed in the file system sense.

After running the demo, the receipt-driven/ output directory contains 250 receipt pairs. Pick any one:

timelayer-verifier verify output/a3f8b2c1/cert.tlcert output/a3f8b2c1/bundle.tlbundle
VALID FINAL

8. Migrating from log-driven

Migration doesn't need to be all-or-nothing. A practical path:

  1. Identify action boundaries — audit your existing log format. Each meaningful log event (not debug noise) is a candidate action. You probably have fewer than you think.
  2. Add notarization alongside logging — for each identified event, add a notarize(hash(event)) call immediately after the existing log write. (SHA-256 is fine; BLAKE3 or SHA-3 work equally well.) Store cert_hex in a new DB column. Run both in parallel for a sprint.
  3. Verify a sample — spot-check 20–30 receipts with timelayer-verifier. Confirm your canonical string is deterministic (same inputs → same hash → same provable receipt).
  4. Remove log writes for migrated events — once you trust the receipt store, drop the corresponding logger.info(…) calls. The receipt is now the record.
  5. Flip to fail-closed — move notarization before the DB commit so a notarization failure prevents the action from committing. This is the point of no return.
  6. Keep debug/trace logging — receipt-driven applies to meaningful business events, not to every function call. Debug logs are noise; they belong in ephemeral observability tools, not in your receipts. Keep them as-is.

Don't notarize everything

Notarizing HTTP health-checks, internal heartbeats, or debug traces wastes quota and creates noise. A receipt is valuable because it's deliberate — one per meaningful commitment, not one per function call.

9. Engineer FAQ

What if I need to prove the receipt corresponds to a specific real-world event?

Store the canonical action string that produced the hash alongside the receipt. Given the string, anyone can recompute hash(string) with the same algorithm and confirm it matches action_hex in the receipt. The receipt then proves: this exact string was attested at this exact time by 2 independent operators. The link between string and real-world event is your responsibility to document.

Does the action hash need to be unique across all users?

No — two different users can notarize the same hash (e.g. the same file hash). Each gets their own receipt with their own timestamp. Uniqueness matters within your own action log: two different actions should produce different hashes, so include user ID and timestamp in your canonical string.

What happens if the API is down when my program tries to notarize?

In fail-closed mode, the action doesn't commit and you retry later. In practice, you should build a small local queue: buffer pending notarizations in a local DB table, drain it asynchronously. Actions commit after the receipt is received from the queue drain. This decouples your hot path from network latency while keeping the fail-closed guarantee.

Can I batch multiple actions into one receipt?

Yes — hash a Merkle tree or a canonical concatenation of all action hashes, notarize the root. The root receipt proves the entire batch was attested at once. The trade-off: you can only prove the batch, not individual actions within it. Use batching for high-volume pipelines where per-action receipts would be wasteful.

How do I handle receipt verification in CI/CD?

The timelayer-verifier binary is a single static executable with no runtime dependencies. Drop it into your CI image. Add a verification step after your integration tests: for each output artifact, verify its sidecar receipt. A non-zero exit code fails the build. This is "receipts as a test oracle" — your CI becomes a proof checker, not just a test runner.

Is this compatible with existing audit logging frameworks?

Yes — receipts are additive. Keep your existing audit log for compliance/debug purposes; add receipt notarization for the subset of events you want to make externally verifiable. The two systems coexist; receipt-driven is not a replacement for all observability.