Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/ack-pay/receipt-verification.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,37 @@ _Example ACK Receipt Verifiable Credential:_
the general structure.
</Tip>

## 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:
Expand Down
56 changes: 51 additions & 5 deletions packages/ack-pay/src/verify-payment-receipt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ describe("verifyPaymentReceipt()", () => {
let signedReceipt: Verifiable<W3CCredential>
let signedReceiptJwt: JwtString
let receiptIssuerDid: DidUri
let receiptIssuerKeypair: Awaited<ReturnType<typeof generateKeypair>>
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)
Expand All @@ -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",
Expand Down Expand Up @@ -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 }),
Expand Down