Verification

What "verify" actually means

When you verify a receipt, your computer is checking one question: did the Plumb operator really produce this receipt, exactly as it is, or has someone tampered with it?

The answer is always yes or no. There's no "probably" — cryptographic signatures are binary. A valid signature is mathematical proof of authenticity. An invalid signature means something's wrong: the receipt was changed, the signature was forged, or you're checking against the wrong key.

The intuition

A useful analogy: the Plumb operator has a unique wax seal. Every receipt is like a letter with that wax seal pressed into the bottom. The seal is incredibly hard to duplicate — effectively impossible given current math — but easy to check. Anyone can compare the seal on a letter against a known-good impression and say "yes, same seal" or "no, this doesn't match."

The digital version is the same idea. The "seal" is a private key only the operator has. The "impression" on each receipt is a signature unique to that exact receipt content. The "known-good impression" is a public key the operator publishes. Your computer compares them and reports match or mismatch.

You don't need to trust the operator to tell you "this receipt is fine" — you do the check yourself, with math.

The three ways to check

1. Click the button

The easiest: open the receipt in the explorer.

https://explorer.plumbtech.xyz/receipt/<id>

There's a Verify signature button. Click it and wait half a second. The explorer's code does the math right in your browser. You'll see either a green check or a red X, along with a short explanation.

The important thing is that your browser is doing the work, not a Plumb server. So even if the operator wanted to lie about whether a receipt is valid, they couldn't — you're the one checking.

2. Three lines of Python

If you're writing software:

from plumb_sdk import Client

c = Client(base_url="https://api.plumbtech.xyz")
rcpt = c.receipts.get("c7f3e0a4-...")
assert c.verify_receipt(rcpt)

That's the whole thing. verify_receipt does all the steps below, returns True if everything checks out, raises an error otherwise.

3. By hand, for the curious

What actually happens when something "verifies a receipt":

  1. Rebuild the exact bytes that got signed. Receipts are signed in a very specific format — a short piece of text with the fields in a fixed order. Your verifier reconstructs that text from the receipt you're holding.

  2. Fetch the right public key. Every receipt says which key signed it (e.g. srv-2026-04-prod). Plumb publishes the matching public key at a public URL. You grab the right one.

  3. Run the math. Feed the signature, the rebuilt text, and the public key into a standard cryptographic library. It returns yes or no. Ed25519, the signature scheme Plumb uses, is fast — this step is sub-millisecond.

That's all verification is. Every implementation (the Python SDK, the explorer, the operator console) does the same three steps. Any of them can drift from any of the others, and cross-implementation tests would catch it.

Key rotation (what happens if the operator changes keys)

Over time, the operator will rotate the key that signs receipts — standard security hygiene, just like you'd rotate a password. Old receipts don't become invalid when this happens.

When a key rotates:

  • The new key starts signing new receipts.
  • The old key is kept around (still published, just marked "no longer active") so receipts signed by it can still be verified.
  • Plumb's public key registry lists every key that's ever been used, with the dates each was active.

So if you verify a receipt from two years ago, the verifier walks back through the key history, finds the key that was active then, and runs the check against that one. Your old receipts stay verifiable forever.

Hardening for paranoid cases

What if someone intercepts your connection to the Plumb server and serves a fake public key? Then the verification would still pass, but against their fake key, not the real one. You'd be fooled.

This is a real concern — solvable but worth knowing about. The fix is called pinning: instead of trusting the server to tell you the public key, you write the correct key (or a fingerprint of it) into your code once, and any future key that doesn't match is rejected.

The console and explorer both support pinning via an environment variable. If you're running something security-sensitive, set it. If you're just kicking the tires, the default behavior (fetching keys at runtime) is fine.

What verification does NOT prove

Being straight about limits:

  • Verification doesn't prove the AI gave you a good answer. Models hallucinate. A valid receipt means the answer is genuine, not that it's correct.
  • Verification doesn't catch a dishonest operator. If the Plumb operator secretly runs a cheaper model and lies about it in the receipt, the signature will be valid for the lie. Verification proves the receipt is what the operator committed to — not that the operator is honest about what they committed to.

The second one is the interesting limit. The long-term fix is a feature called TEE attestation — running the AI inside a tamper-evident hardware enclave and including a hardware certificate with every receipt. The spec is in place; the hardware integration is planned but not shipped. Until then, verification gives you strong guarantees about what the operator said, and somewhat weaker guarantees about what they actually did.

That's still a lot better than "nothing."


For developers

The exact signature scheme is Ed25519. The canonical payload format is described in Receipts. Key rotation is implemented via the PLUMB_SIGNER_HISTORICAL_KEYS_JSON environment variable on the gateway; published keys appear under /v1/verification/keys. Pinning is via NEXT_PUBLIC_PLUMB_SIGNER_KEYS_JSON (raw hex keys) or NEXT_PUBLIC_PLUMB_SIGNER_KEY_FINGERPRINTS (SHA-256 hex of the key bytes). All three implementations (TypeScript server, browser verifiers, Python SDK) share a test fixture of known input → output vectors to catch cross-implementation drift.