aws-samples / aws-samples/sample-agentcore-cloudfront-x402-payments
Potential atomicity issue: payment is settled before paid content retrieval succeeds
- Dominant language
- Python
- Stars
- 25
- Forks
- 11
- PR merge metrics
- No merged PRs in 30d
Description
# Potential atomicity issue: payment is settled before paid content retrieval succeeds
Hi, I noticed a possible payment-flow issue in the current repository state. This is a conservative report based on the current code path, and I may be missing deployment-specific guards outside this repository.
Repository: `https://github.com/aws-samples/sample-agentcore-cloudfront-x402-payments`
Reviewed HEAD: `36fc3ef`
## What I observed
The Lambda@Edge verifier settles the payment first and only then retrieves/generates the paid content. If content retrieval fails after settlement, the outer catch path returns a 500 payment-processing error after the payment may already have been settled.
Relevant code excerpts:
`seller-infrastructure/lib/lambda-edge/payment-verifier.ts:792-815`
```text
792 // Settle payment with facilitator
793 const settlementStartTime = Date.now();
794 const settlement = await settlePaymentWithFacilitator(
795 paymentPayload,
796 paymentRequirement,
797 logger
798 );
...
801 if (!settlement.success) {
...
809 return createErrorResponse(
810 '402',
811 'Payment Required',
812 'Settlement Failed',
813 `Payment settlement failed: ${settlement.errorReason}`
814 );
815 }
```
`seller-infrastructure/lib/lambda-edge/payment-verifier.ts:817-836`
```text
817 // Payment verified and settled - return dynamic content
818 logger.incrementCounter(MetricName.PAYMENT_SETTLED);
...
828 logger.info('Payment settled successfully', {
829 payer,
830 transactionHash: settlement.transaction,
831 amount: paymentRequirement.amount,
832 network: paymentRequirement.network,
833 });
834
835 // Get dynamic content from content manager
836 const content = await contentManager.getContent(uri);
```
`seller-infrastructure/lib/lambda-edge/payment-verifier.ts:854-887`
```text
854 return {
855 status: '200',
856 statusDescription: 'OK',
...
874 body: JSON.stringify(content),
875 };
876 } catch (error) {
877 logger.error('Unexpected error processing payment', error);
878 logger.incrementCounter(MetricName.PAYMENT_FAILED);
...
881 return createErrorResponse(
882 '500',
883 'Internal Server Error',
884 'Payment Processing Error',
885 'Failed to process payment'
886 );
887 }
```
## Why this may matter
This ordering can create a charged-without-content state: settlement succeeds, but content lookup/generation fails afterward. The request then receives a 500 error rather than the paid resource or an explicit recoverable entitlement.
## Suggested check
Consider checking content availability before settlement, or persisting a durable entitlement/receipt before returning the error path so a user can retry content delivery without paying again. If content generation can fail, settlement and content delivery may need a two-phase or recoverable state.
## Conservative caveat
If `contentManager.getContent(uri)` is guaranteed not to fail for all paid resources, impact may be lower. I am reporting this because the current code settles before the paid content sink has succeeded.
Contributor guide
Research direction
Start in seller-infrastructure/lib/lambda-edge/payment-verifier.ts around lines 792-887 and trace the settlement, contentManager.getContent(uri), and catch paths. Reproduce or inspect the failure sequence where retrieval fails after settlement, then define a recoverable behavior that prevents a charged-without-content result; done means the payment and delivery outcomes are handled consistently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, typescript
- Domain
- backend, cloud, payments
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100