Skip to content

feat(receipt): show what the signature actually binds, and stop dropping the amount - #11

Merged
dkitchell merged 1 commit into
mainfrom
feat/receipt-bindings
Sep 15, 2026
Merged

dkitchell merged 1 commit into
mainfrom
feat/receipt-bindings

Conversation

@dkitchell

Copy link
Copy Markdown
Contributor

The demo artifact carries the whole story and the tool was showing none of it.

Before

✓ VALID  receipt 2492a060-8fbc-40ae-beab-7258aefb0608
  signed by    (published Agent Commerce key) (CertifiedData.io)
  signature    pass
  payload hash pass
  public key   /.well-known/certifieddata-public-key.pem
  settlement   succeeded_live

After

✓ VALID  receipt 2492a060-8fbc-40ae-beab-7258aefb0608
  signed by    (published Agent Commerce key) (CertifiedData.io)
  signature    pass
  payload hash pass
  public key   /.well-known/certifieddata-public-key.pem

  ── bound by this signature ──
  amount         0.99 USD
  settlement     succeeded_live
  settled at     2026-08-20T22:39:38.008Z
  rail           stripe
  purpose        Dataset purchase: fraud-detection-verification-sample (certificate-linked, 500 rows x 7 columns)
  agent          38ed5be8-fb59-438e-b99d-73549b242b09
  policy         0acaeed1-7dbc-4c17-b405-c31517a0be78
  policy hash    sha256:f4e59785c1f3107b3f6afac362051fe42bc25d3605ee641e8936f0e8c3a1eb8d
  authorization  df033885-5b76-4137-8aa4-a8e8a98a95b2
  decision       709519fd-40bb-445e-a6c4-b9b2b1cba9fb
  artifact hash  sha256:bd48985485c9a3e19838e29795bb89ddedd7f7e5c706b57c190cc6c46119a660
  certificate    fb914a90-b1b3-4355-8147-cc0194160e23
  transaction    b00d1924-6187-49f3-aee7-3bbffff1c78c
  payment ref    pi_3U6ecXDRYQxqHzlc2IoF5mAB

  The verdict above was computed locally — not taken from the server.

That is real output from the live production receipt, not a mockup.

Why this is the right failure to fix

The pitch is "don't trust our dashboard, verify it yourself." The verifier was answering with an unsupported VALID and leaving the substance — which policy, which artifact, which decision, which settlement — to be taken on trust anyway. The signature was only as interesting as what it bound, and the tool never said.

Safety: everything printed is read from the payload that has already passed both the signature and payload-hash checks, so nothing is displayed as bound by a signature that does not cover it. A post-signing tamper fails before any of it renders. Absent fields are omitted rather than invented.

A silent data bug found while wiring this up

Production emits amount as a string ("99"). The code tested typeof p.amount === "number", so every real receipt verified with no amount displayed at all. Both forms now work; a non-numeric value is dropped rather than coerced into something wrong.

Verified against production

Each binding was confirmed to dereference before being surfaced:

link state
certificate fb914a90… ISSUED — retained, not one of the #383 orphans
…in transparency log log_index 689
decision record 709519fd… exists, written 21s before the receipt
authorization df033885… executed

Tests

98, up from 93. New coverage: the binding surface, both amount forms, non-numeric amounts, absent bindings reported as null rather than invented, and that a post-signing tamper fails before anything is reported as bound.

Version bumped to 0.1.1.

🤖 Generated with Claude Code

…ing the amount

A verified receipt printed six lines, none of which said what had been verified.
The signed payload of the production receipt used for the sales demo carries the
whole chain — policy id and policy hash, authorization id, decision record id,
artifact hash, certificate id, transaction id, Stripe PaymentIntent, settlement
state and time, amount, currency, rail, agent, purpose — and the tool showed
none of it.

That is the wrong failure for this tool in particular. The pitch is "do not
trust our dashboard, verify it yourself", and the verifier answered with an
unsupported VALID, leaving the substance to be taken on trust anyway.

Everything now printed is read from the payload that has already passed BOTH
the signature and the payload-hash check, so nothing is displayed as bound by a
signature that does not cover it. A tampered payload still fails before any of
it is shown. Absent fields are omitted rather than invented.

Also fixes a silent data bug found while wiring this up: production emits
`amount` as a STRING ("99"), and the code tested `typeof p.amount === "number"`.
Every real receipt therefore verified with no amount at all. Both forms are now
accepted; a non-numeric value is dropped rather than coerced.

`bindings` is exposed on ReceiptVerifyResult so library consumers get the same
fields without re-parsing the payload.

98 tests (was 93). The new ones cover the binding surface, both amount forms,
non-numeric amounts, absent bindings, and that a post-signing tamper fails
before anything is reported as bound.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dkitchell
dkitchell merged commit c94be6c into main Sep 15, 2026
6 checks passed
@dkitchell
dkitchell deleted the feat/receipt-bindings branch September 15, 2026 02:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant