Skip to content

Potential settlement-ordering issue: handlers run before settlement in middleware flow #44

@chenshj73

Description

@chenshj73

Potential settlement-ordering issue: handlers run before settlement in middleware flow

Hi, I noticed a possible settlement-ordering issue in the current middleware flow.

In packages/core/src/middleware/core.ts, successful verification causes the middleware to continue to the protected handler:

286: async processPreHandle(request: PaymentContext): Promise<PaymentMiddlewareResult> {
305:   const verification = await this.httpServer.processVerification(
306:     this.config.facilitator.url,
307:     payment.payload,
308:     requirements,
309:   )
319:   if (verification.isValid) {
320:     this.recordPayment(payment, request, verification)
322:     return {
323:       action: "continue",
324:       payment,
325:       verification,
326:     }

Settlement is handled later in processAfterHandle:

375: async processAfterHandle(
376:   payment: X402DecodedPayment,
377:   request: PaymentContext,
378:   response?: any,
379: ): Promise<PaymentMiddlewareResult> {
388:   const settlement = await this.httpServer.processSettlement(
389:     this.config.facilitator.url,
390:     payment.payload,
391:     requirements,
392:   )

If settlement succeeds, response headers are attached. If settlement fails, the code records failure, but this occurs after the handler phase:

418: } catch (error) {
419:   this.recordFailedSettlement(payment, request, error as Error)
421:   return {
422:     action: "error",
423:     response: this.createPaymentRequiredResponse(
424:       request,
425:       "Settlement failed",
426:     ),
427:     error: error as Error,
428:   }
429: }

The Express adapter shows the same high-level ordering. It calls handlePaymentMiddleware, and when that result is continue, it overrides res.send so settlement can run after the route handler produces a response:

152: if (result.action === "continue" && result.payment) {
153:   // Store payment info on request for later settlement
154:   req.payment = {
155:     payload: result.payment,
156:     verification: result.verification,
157:   }
160:   const originalSend = res.send
161:   res.send = function (body: any) {
162:     // Process settlement before sending response
163:     core.processAfterHandle(result.payment!, context, body)

The protected handler is then invoked through next():

194: next()

The concern is the current source-to-sink chain:

source: incoming X-PAYMENT / X-PAYMENT-SIGNATURE header
transform: verification returns action: continue
sink: framework handler runs before processAfterHandle settlement completes

For pure response generation this may be an acceptable design because Express response sending is intercepted. The risk is paid handler side effects: database writes, external API calls, tool execution, compute work, or irreversible state changes can happen before settlement is attempted. If settlement later fails, the middleware can alter the response path, but it cannot necessarily undo side effects that already occurred inside the handler.

Possible hardening directions:

  • Offer a blocking-settlement mode that settles before invoking the protected handler.
  • Document that handlers protected by the current mode should avoid irreversible side effects before settlement.
  • Expose a pre-settlement "verified only" state distinct from a finalized paid state.
  • Add adapter tests where the handler mutates state before res.send and settlement fails, to make the ordering explicit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions