Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .changeset/afraid-clocks-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"agentcommercekit": minor
"@agentcommercekit/ack-pay": minor
---

- Deprecate `createPaymentRequestBody` in favor of `createSignedPaymentRequest`
- Rename `paymentToken` to `paymentRequestToken` in payment requests and receipts
- Remove `createPaymentRequestResponse`, which only built a `Response` object in a demo
61 changes: 31 additions & 30 deletions demos/e2e/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
createDidPkhDocument,
createDidWebDocumentFromKeypair,
createJwtSigner,
createPaymentRequestBody,
createSignedPaymentRequest,
curveToJwtAlgorithm,
generateKeypair
} from "agentcommercekit"
Expand Down Expand Up @@ -205,45 +205,46 @@ ${colors.bold(otherAgent.did)}

// If no receipt provided, generate payment request
if (!receipt) {
const { paymentRequest, paymentToken } = await createPaymentRequestBody(
{
id: crypto.randomUUID(),
expiresAt: new Date(Date.now() + 3600000), // 1 hour from now
paymentOptions: [
{
id: crypto.randomUUID(),
amount: BigInt(500).toString(),
decimals: 2,
currency: "USD",
recipient: this.walletDid
}
]
},
{
issuer: this.did,
signer: this.signer,
algorithm: curveToJwtAlgorithm(this.keypair.curve)
}
)

// Optional: Store the pending payment tokens.
this.pendingRequests[paymentToken] = paymentRequest

throw new PaymentRequiredError(paymentRequest, paymentToken)
const { paymentRequest, paymentRequestToken } =
await createSignedPaymentRequest(
{
id: crypto.randomUUID(),
expiresAt: new Date(Date.now() + 3600000), // 1 hour from now
paymentOptions: [
{
id: crypto.randomUUID(),
amount: BigInt(500).toString(),
decimals: 2,
currency: "USD",
recipient: this.walletDid
}
]
},
{
issuer: this.did,
signer: this.signer,
algorithm: curveToJwtAlgorithm(this.keypair.curve)
}
)

// Optional: Store the pending payment request tokens.
this.pendingRequests[paymentRequestToken] = paymentRequest

throw new PaymentRequiredError(paymentRequest, paymentRequestToken)
}

// Verify the receipt
const { paymentToken } = await this.receiptVerifier.verifyReceipt(
const { paymentRequestToken } = await this.receiptVerifier.verifyReceipt(
receipt,
this.did
)

// Optional: Ensure the payment token was for this same type of request.
// Optional: Ensure the payment request token was for this same type of request.
// Payment requests and receipts are designed to allow for stateless
// operation, but it can be useful to map a given request to a specific
// action on the server.
if (!this.pendingRequests[paymentToken]) {
throw new Error("Payment token not found")
if (!this.pendingRequests[paymentRequestToken]) {
throw new Error("Payment Request token not found")
}

// Provide the service
Expand Down
12 changes: 8 additions & 4 deletions demos/e2e/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,11 @@ If identity is verified, Agent 2 will then respond with a 402 Payment Required e

const message = "What's the current price of AAPL stock?"

const { paymentRequest, paymentToken } = await chat(agent1, agent2, message)
const { paymentRequest, paymentRequestToken } = await chat(
agent1,
agent2,
message
)

log(
colors.dim(`
Expand All @@ -217,7 +221,7 @@ Next, we will perform the payment and fetch a Receipt.
log(
colors.dim(`
Payment must be made using token:
${paymentToken}
${paymentRequestToken}

`)
)
Expand All @@ -241,7 +245,7 @@ ${paymentToken}
const receipt = await receiptIssuer.issueReceipt({
payerDid: agent1.walletDid,
txHash,
paymentToken
paymentRequestToken
})
log(successMessage("Payment Receipt VC Issued!"))
logJson(await parseJwtCredential(receipt, resolver))
Expand Down Expand Up @@ -291,7 +295,7 @@ async function chat(agent1: Agent, agent2: Agent, message: string) {
if (error instanceof PaymentRequiredError) {
return {
paymentRequest: error.paymentRequest,
paymentToken: error.paymentToken
paymentRequestToken: error.paymentRequestToken
}
}

Expand Down
6 changes: 3 additions & 3 deletions demos/e2e/src/payment-required-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import type { PaymentRequest } from "agentcommercekit"
*/
export class PaymentRequiredError extends Error {
paymentRequest: PaymentRequest
paymentToken: string
paymentRequestToken: string

constructor(paymentRequest: PaymentRequest, paymentToken: string) {
constructor(paymentRequest: PaymentRequest, paymentRequestToken: string) {
super("402 Payment Required")
this.name = "PaymentRequiredError"
this.paymentRequest = paymentRequest
this.paymentToken = paymentToken
this.paymentRequestToken = paymentRequestToken
}
}
17 changes: 10 additions & 7 deletions demos/e2e/src/receipt-issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
createPaymentReceipt,
generateKeypair,
signCredential,
verifyPaymentToken
verifyPaymentRequestToken
} from "agentcommercekit"
import type {
DidDocument,
Expand Down Expand Up @@ -65,15 +65,18 @@ export class ReceiptIssuer {
async issueReceipt({
payerDid,
txHash,
paymentToken
paymentRequestToken
}: {
payerDid: DidUri
txHash: string
paymentToken: string
paymentRequestToken: string
}): Promise<JwtString> {
const { paymentRequest } = await verifyPaymentToken(paymentToken, {
resolver: this.resolver
})
const { paymentRequest } = await verifyPaymentRequestToken(
paymentRequestToken,
{
resolver: this.resolver
}
)

// Verify the payment on-chain
const paymentVerified = await this.verifyPaymentOnChain(
Expand All @@ -86,7 +89,7 @@ export class ReceiptIssuer {

// Create and sign the receipt credential
const credential = createPaymentReceipt({
paymentToken,
paymentRequestToken,
paymentOptionId: paymentRequest.paymentOptions[0].id,
issuer: this.did,
payerDid
Expand Down
2 changes: 1 addition & 1 deletion demos/e2e/src/receipt-verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class ReceiptVerifier {
resolver: this.resolver,
trustedReceiptIssuers: this.trustedIssuers,
paymentRequestIssuer: paymentRequestIssuer,
verifyPaymentTokenJwt: true
verifyPaymentRequestTokenJwt: true
})
}
}
42 changes: 24 additions & 18 deletions demos/payments/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
isJwtString,
parseJwtCredential
} from "agentcommercekit"
import { paymentRequestBodySchema } from "agentcommercekit/schemas/valibot"
import {
jwtStringSchema,
paymentRequestSchema
} from "agentcommercekit/schemas/valibot"
import * as v from "valibot"
import { isAddress } from "viem"
import {
Expand Down Expand Up @@ -118,14 +121,17 @@ The Client attempts to access a protected resource on the Server. Since no valid
throw new Error("Server did not respond with 402")
}

const { paymentToken, paymentRequest } = v.parse(
paymentRequestBodySchema,
const { paymentRequestToken, paymentRequest } = v.parse(
v.object({
paymentRequestToken: jwtStringSchema,
paymentRequest: paymentRequestSchema
}),
await response1.json()
)

// This demo uses JWT strings for the payment token, but this is not a requirement of the protocol.
if (!isJwtString(paymentToken)) {
throw new Error(errorMessage("Invalid payment token"))
// This demo uses JWT strings for the payment request token, but this is not a requirement of the protocol.
if (!isJwtString(paymentRequestToken)) {
throw new Error(errorMessage("Invalid payment request token"))
}

log(
Expand All @@ -138,7 +144,7 @@ The Client attempts to access a protected resource on the Server. Since no valid
log(
colors.magenta(
wordWrap(
"\n💡 The 'paymentToken' is a JWT signed by the Server, ensuring the integrity and authenticity of the payment request. The Client will include this token when requesting a receipt, along with the payment option id and metadata."
"\n💡 The 'paymentRequestToken' is a JWT signed by the Server, ensuring the integrity and authenticity of the payment request. The Client will include this token when requesting a receipt, along with the payment option id and metadata."
)
)
)
Expand Down Expand Up @@ -168,15 +174,15 @@ The Client attempts to access a protected resource on the Server. Since no valid
const paymentResult = await performStripePayment(
clientKeypairInfo,
selectedPaymentOption,
paymentToken
paymentRequestToken
)
receipt = paymentResult.receipt
details = paymentResult.details
} else if (selectedPaymentOption.network === chainId) {
const paymentResult = await performOnChainPayment(
clientKeypairInfo,
selectedPaymentOption,
paymentToken
paymentRequestToken
)
receipt = paymentResult.receipt
details = paymentResult.details
Expand Down Expand Up @@ -215,7 +221,7 @@ The Client Agent now retries the original request to the Server Agent, this time

${colors.bold("The Server Agent then performs its own verifications:")}
1. Validates the receipt's signature (ensuring it was issued by a trusted Receipt Service and hasn't been tampered with).
2. Checks the receipt's claims: Confirms the receipt is for the correct payment (e.g., matches the 'paymentToken' it originally issued), is not expired, and meets any other criteria for accessing the resource.
2. Checks the receipt's claims: Confirms the receipt is for the correct payment (e.g., matches the 'paymentRequestToken' it originally issued), is not expired, and meets any other criteria for accessing the resource.

If the receipt is valid, the Server grants access to the protected resource.`
)
Expand Down Expand Up @@ -262,7 +268,7 @@ If the receipt is valid, the Server grants access to the protected resource.`
async function performOnChainPayment(
client: KeypairInfo,
paymentOption: PaymentRequest["paymentOptions"][number],
paymentToken: JwtString
paymentRequestToken: JwtString
) {
const receiptServiceUrl = paymentOption.receiptService
if (!receiptServiceUrl) {
Expand Down Expand Up @@ -346,11 +352,11 @@ The Client Agent now uses the details from the Payment Request to make the payme
colors.dim(
`${colors.bold("Client Agent 👤 -> Receipt Service 🧾")}

With the payment confirmed, the Client Agent now requests a formal, cryptographically verifiable payment receipt from the Receipt Service. The Client sends the original 'paymentToken' (received from the Server in Step 1) and the transaction hash (as metadata) to the Receipt Service. The Client also signs this request with its own DID to prove it's the one who made the payment.
With the payment confirmed, the Client Agent now requests a formal, cryptographically verifiable payment receipt from the Receipt Service. The Client sends the original 'paymentRequestToken' (received from the Server in Step 1) and the transaction hash (as metadata) to the Receipt Service. The Client also signs this request with its own DID to prove it's the one who made the payment.

${colors.bold("The Receipt Service then performs several crucial verifications:")}
1. Validates the 'paymentToken' (e.g., signature, expiry, ensuring it was issued by a trusted server for the expected payment context).
2. Verifies the on-chain transaction: Confirms that the transaction hash is valid, the correct amount of the specified currency was transferred to the correct recipient address as per the 'paymentToken'.
1. Validates the 'paymentRequestToken' (e.g., signature, expiry, ensuring it was issued by a trusted server for the expected payment context).
2. Verifies the on-chain transaction: Confirms that the transaction hash is valid, the correct amount of the specified currency was transferred to the correct recipient address as per the 'paymentRequestToken'.
3. Verifies the Client's signature on the request, ensuring the payer is who they claim to be (linking the payment action to the Client's DID).

If all checks pass, the Receipt Service issues a Verifiable Credential (VC) serving as the payment receipt.`
Expand All @@ -364,7 +370,7 @@ If all checks pass, the Receipt Service issues a Verifiable Credential (VC) serv
)

const payload = {
paymentToken,
paymentRequestToken,
paymentOptionId: paymentOption.id,
metadata: {
txHash: hash,
Expand Down Expand Up @@ -397,7 +403,7 @@ If all checks pass, the Receipt Service issues a Verifiable Credential (VC) serv
async function performStripePayment(
_client: KeypairInfo,
paymentOption: PaymentRequest["paymentOptions"][number],
paymentToken: JwtString
paymentRequestToken: JwtString
) {
const paymentServiceUrl = paymentOption.paymentService
if (!paymentServiceUrl) {
Expand Down Expand Up @@ -429,7 +435,7 @@ This flow is simulated in this example.
method: "POST",
body: JSON.stringify({
paymentOptionId: paymentOption.id,
paymentToken
paymentRequestToken
})
})

Expand Down Expand Up @@ -468,7 +474,7 @@ This flow is simulated in this example.
method: "POST",
body: JSON.stringify({
paymentOptionId: paymentOption.id,
paymentToken,
paymentRequestToken,
metadata: {
eventId: "evt_" + Math.random().toString(36).substring(7) // Simulated Stripe event ID
}
Expand Down
Loading
Loading