Client loses paymentRequirements when creating PaymentPayload
Bug Description
When a client creates a PaymentPayload, the original paymentRequirements information is lost because PaymentPayloadSchema does not include a field to carry it. This causes resource servers/facilitators to lose access to critical payment requirement information when processing payments.
Current Behavior
The PaymentPayloadSchema currently only includes:
export const PaymentPayloadSchema = z.object({
x402Version: z.number().refine(val => x402Versions.includes(val as 1)),
scheme: z.enum(schemes),
network: NetworkSchema,
payload: z.union([ExactEvmPayloadSchema, ExactSvmPayloadSchema]),
// No paymentRequirements field
});
When a client creates a payment:
- Client receives
PaymentRequirementsfrom the 402 response - Client creates and signs a
PaymentPayloadusing these requirements - The original
PaymentRequirementsis not included in the payload - Resource servers/facilitators receive the
PaymentPayloadbut cannot access the original requirements
Problem
This causes the resource servers/facilitators to lose access to important metadata from the original requirements:
extra: Custom fields for extended payment flowsasset: Token/currency addresspayTo: Original payee addressdescription,mimeType,outputSchema: Resource metadatamaxTimeoutSeconds,maxAmountRequired: Payment constraints
Expected Behavior
The PaymentPayload should optionally include the paymentRequirements that were used to create it, making it self-contained and allowing facilitators to process payments without maintaining additional state.
Proposed Solution
Add an optional paymentRequirements field to PaymentPayloadSchema:
export const PaymentPayloadSchema = z.object({
x402Version: z.number().refine(val => x402Versions.includes(val as 1)),
scheme: z.enum(schemes),
network: NetworkSchema,
payload: z.union([ExactEvmPayloadSchema, ExactSvmPayloadSchema]),
paymentRequirements: PaymentRequirementsSchema.optional(), // Allow client to send back original requirements
});
The problem here is privacy - if you add the requirements then the facilitator will know exactly what the user purchased ...
@jolestar = why do you think this is critical for the facilitator to get payment requirements. The facilitator is essentially just payment transfers API .. May be you can give examples?
@kladkogex Thanks for the privacy callout — fully agree. We do NOT want to expose any content metadata (resource URL / description / mimeType).
Current flow (and why this hurts stateless correctness):
- Client first fetches
accepts(payment requirements) from the resource server. - When sending the final request, the client does not echo the chosen requirements, so the server must “regenerate/match” them.
- That makes it hard to verify the exact option the client selected (price/asset/timeout) and prevents exchanging minimal dynamic, non-content parameters (e.g., an opaque
contextId). - Even if the server stores the first response, it still needs a correlator (id/hash/handle) to bind the final payment to that specific option without session state.
We’re asking for guidance on a minimal, privacy-preserving way to carry only non-content technical parameters:
-
Preferred channel to echo the exact choice:
- OPTIONAL narrow echo (e.g.,
selectedAcceptIdor arequirementsHashof the chosen option), or - An opaque
executionHandlereturned by the server that can be resolved later.
- OPTIONAL narrow echo (e.g.,
-
If you prefer a client ↔ resource-server ↔ facilitator side-channel instead of putting anything in the payment header, is there a recommended pattern (e.g., additional HTTP headers, or a resolve endpoint/JWE) we should follow?
-
If we need to extend the resource-server ↔ facilitator protocol with a few extra technical parameters in the future, what’s the recommended transport? We outlined motivations and examples in our RFC #584 and repo
https://github.com/nuwa-protocol/x402-exec.
Hey @jolestar
I see what you mean... It prevents the resource price from being dynamic, supporting multiple assets, or using different networks.
I mentioned some of this here: https://github.com/coinbase/x402/issues/525
An opaque executionHandle returned by the server that can be resolved later.
This is probably the best approach, since it could be used for different purposes. It’s probably best to add this as an opaque field to PaymentRequirements and require the client to return the field.