x402 icon indicating copy to clipboard operation
x402 copied to clipboard

Client loses paymentRequirements when creating PaymentPayload

Open jolestar opened this issue 2 months ago • 3 comments

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:

  1. Client receives PaymentRequirements from the 402 response
  2. Client creates and signs a PaymentPayload using these requirements
  3. The original PaymentRequirements is not included in the payload
  4. Resource servers/facilitators receive the PaymentPayload but 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 flows
  • asset: Token/currency address
  • payTo: Original payee address
  • description, mimeType, outputSchema: Resource metadata
  • maxTimeoutSeconds, 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
});

jolestar avatar Nov 01 '25 06:11 jolestar

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 avatar Nov 01 '25 14:11 kladkogex

@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:

  1. Preferred channel to echo the exact choice:

    • OPTIONAL narrow echo (e.g., selectedAcceptId or a requirementsHash of the chosen option), or
    • An opaque executionHandle returned by the server that can be resolved later.
  2. 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?

  3. 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.

jolestar avatar Nov 02 '25 03:11 jolestar

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.

kladkogex avatar Nov 02 '25 14:11 kladkogex