diff --git a/docs/ack-pay/receipt-verification.mdx b/docs/ack-pay/receipt-verification.mdx index 4437db8..faf6090 100644 --- a/docs/ack-pay/receipt-verification.mdx +++ b/docs/ack-pay/receipt-verification.mdx @@ -62,6 +62,37 @@ _Example ACK Receipt Verifiable Credential:_ the general structure. +## Receipt Evidence Metadata + +`credentialSubject.metadata` is optional and should be treated as an extension +point for verifier-specific payment evidence. Keep the core receipt fields +stable, and put references or hashes to external policy, mandate, execution, +and settlement records in metadata when a deployment needs a richer audit trail. + +Metadata fields are non-normative: ACK-Pay verifies the receipt signature and +the bound payment request token, but applications decide which metadata fields +are required for their own policy checks. Do not place secrets, private keys, +raw payment credentials, or sensitive customer data in receipt metadata. + +For agent-commerce flows that compose ACK-Pay with MPP, x402, AP2, or a policy +engine, a receipt can carry references like: + +```json +{ + "credentialSubject": { + "metadata": { + "policyRef": "policy://merchant-spend-v3", + "policySnapshotHash": "sha256:8a0f...", + "mandateRef": "ap2:mandate:checkout-123", + "executionRef": "urn:ack:execution:checkout-123", + "executionReceiptHash": "sha256:5b1d...", + "settlementNetwork": "eip155:8453", + "settlementReference": "0xabc123" + } + } +} +``` + ## Receipt Verification Process When an ACK Receipt is presented by a Client Agent to a Server Agent (e.g., when retrying a request after payment), the Server performs these verification steps: diff --git a/packages/ack-pay/src/verify-payment-receipt.test.ts b/packages/ack-pay/src/verify-payment-receipt.test.ts index 4705b55..100e45d 100644 --- a/packages/ack-pay/src/verify-payment-receipt.test.ts +++ b/packages/ack-pay/src/verify-payment-receipt.test.ts @@ -32,10 +32,12 @@ describe("verifyPaymentReceipt()", () => { let signedReceipt: Verifiable let signedReceiptJwt: JwtString let receiptIssuerDid: DidUri + let receiptIssuerKeypair: Awaited> let paymentRequestIssuerDid: DidUri + let paymentRequestToken: JwtString beforeEach(async () => { - const receiptIssuerKeypair = await generateKeypair("secp256k1") + receiptIssuerKeypair = await generateKeypair("secp256k1") receiptIssuerDid = createDidKeyUri(receiptIssuerKeypair) const paymentRequestIssuerKeypair = await generateKeypair("secp256k1") paymentRequestIssuerDid = createDidKeyUri(paymentRequestIssuerKeypair) @@ -56,16 +58,19 @@ describe("verifyPaymentReceipt()", () => { ], } - const { paymentRequestToken, paymentRequest } = - await createSignedPaymentRequest(paymentRequestInit, { + const paymentRequiredBody = await createSignedPaymentRequest( + paymentRequestInit, + { issuer: paymentRequestIssuerDid, signer: createJwtSigner(paymentRequestIssuerKeypair), algorithm: curveToJwtAlgorithm(paymentRequestIssuerKeypair.curve), - }) + }, + ) + paymentRequestToken = paymentRequiredBody.paymentRequestToken unsignedReceipt = createPaymentReceipt({ paymentRequestToken, - paymentOptionId: paymentRequest.paymentOptions[0].id, + paymentOptionId: paymentRequiredBody.paymentRequest.paymentOptions[0].id, issuer: receiptIssuerDid, payerDid: createDidPkhUri( "eip155:84532", @@ -97,6 +102,47 @@ describe("verifyPaymentReceipt()", () => { expect(result.paymentRequest).toBeDefined() }) + it("preserves receipt metadata through JWT verification", async () => { + const evidenceMetadata = { + policyRef: "policy://merchant-spend-v3", + policySnapshotHash: + "sha256:8a0f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f4f", + mandateRef: "ap2:mandate:checkout-123", + executionRef: "urn:ack:execution:checkout-123", + executionReceiptHash: + "sha256:5b1d5b1d5b1d5b1d5b1d5b1d5b1d5b1d5b1d5b1d5b1d5b1d5b1d5b1d5b1d5b1d", + settlementNetwork: "eip155:8453", + settlementReference: "0xabc123", + } + const receiptWithMetadata = createPaymentReceipt({ + paymentRequestToken, + paymentOptionId: "test-payment-option-id", + issuer: receiptIssuerDid, + payerDid: createDidPkhUri( + "eip155:84532", + "0x7B3D8F2E1C9A4B5D6E7F8A9B0C1D2E3F4A5B6C", + ), + metadata: evidenceMetadata, + }) + const signedReceiptWithMetadata = await signCredential( + receiptWithMetadata, + { + did: receiptIssuerDid, + signer: createJwtSigner(receiptIssuerKeypair), + }, + ) + + const result = await verifyPaymentReceipt(signedReceiptWithMetadata, { + resolver, + }) + + expect(result.receipt).toMatchObject({ + credentialSubject: { + metadata: evidenceMetadata, + }, + }) + }) + it("throws for an invalid JWT receipt", async () => { await expect( verifyPaymentReceipt("invalid-jwt", { resolver }),