x402 icon indicating copy to clipboard operation
x402 copied to clipboard

NextJS api wrapper

Open phdargen opened this issue 1 month ago • 3 comments

Description

There is a problem with the x402-next implementation not guaranteeing that payment only happens after successful API response. I tested this using a modified fullstack/next example with api route returning 500:

export async function GET() {
const response = NextResponse.json({
error: "API failed",
}, { status: 500 });
return response;
}

Server logs:

[X402-MIDDLEWARE 2025-11-21T08:58:17.863Z] ============================================
[X402-MIDDLEWARE 2025-11-21T08:58:17.863Z] Payment verification passed
[X402-MIDDLEWARE 2025-11-21T08:58:17.863Z] CALLING NextResponse.next() at: 1763715497863
[X402-MIDDLEWARE 2025-11-21T08:58:17.863Z] ============================================
RESPONSE FROM NEXT RESPONSE.NEXT() Response {
status: 200,
statusText: '',
headers: Headers { 'x-middleware-next': '1' },
body: null,
bodyUsed: false,
ok: true,
redirected: false,
type: 'default',
url: ''
}
[X402-MIDDLEWARE 2025-11-21T08:58:17.865Z] ============================================
[X402-MIDDLEWARE 2025-11-21T08:58:17.865Z] NextResponse.next() RETURNED at: 1763715497865
[X402-MIDDLEWARE 2025-11-21T08:58:17.865Z] Response status: 200
[X402-MIDDLEWARE 2025-11-21T08:58:17.865Z] ============================================
[X402-MIDDLEWARE 2025-11-21T08:58:17.865Z] ============================================
[X402-MIDDLEWARE 2025-11-21T08:58:17.865Z] STARTING SETTLEMENT at: 1763715497865
[X402-MIDDLEWARE 2025-11-21T08:58:17.865Z] ============================================
[X402-MIDDLEWARE 2025-11-21T08:58:19.086Z] ============================================
[X402-MIDDLEWARE 2025-11-21T08:58:19.086Z] SETTLEMENT COMPLETED at: 1763715499086
[X402-MIDDLEWARE 2025-11-21T08:58:19.086Z] Result: SUCCESS
[X402-MIDDLEWARE 2025-11-21T08:58:19.086Z] ============================================
[X402-MIDDLEWARE 2025-11-21T08:58:19.086Z] ============================================
[X402-MIDDLEWARE 2025-11-21T08:58:19.086Z] RETURNING RESPONSE TO CLIENT at: 1763715499086
[X402-MIDDLEWARE 2025-11-21T08:58:19.086Z] Total middleware duration: 1223ms
[X402-MIDDLEWARE 2025-11-21T08:58:19.086Z] ============================================
[ROUTE 2025-11-21T08:58:19.273Z] /api/weather handler executing
[ROUTE 2025-11-21T08:58:19.273Z] /api/weather handler completed (NextResponse.json called)
GET /api/weather 500 in 186ms

Client logs:

[CLIENT 2025-11-21T08:58:17.349Z] Sending request to http://localhost:3000/api/weather
[CLIENT 2025-11-21T08:58:19.274Z] Response received! Status: 500
[CLIENT 2025-11-21T08:58:19.275Z] body: { error: 'API failed' }

Problem: This shows that settlement happens BEFORE route execution. If API fails client receives error message but is still charged. Error response is missing payment header, so client isn't even aware of the payment in that case

Core issue: These lines of code. In the middleware NextResponse.next() returns immediately with a "dummy" response and status 200 (see log above). The following check for status < 400 is thus meaningless and always passes

Fix

  • Implement new withX402 reusable wrapper that runs verify, api, settle on a per-route basis instead of using middleware
  • This works for API routes but not protected pages. Thus keep old paymentMiddleware approach for pages and backwards compatibility of API routes
  • Added wrapped api route to fullstack/next example to show usage

Test with failed API response:

[X402-WITHX402 2025-11-21T19:02:13.286Z] ============================================
[X402-WITHX402 2025-11-21T19:02:13.286Z] Payment verification passed
[X402-WITHX402 2025-11-21T19:02:13.286Z] CALLING route handler at: 1763751733286
[X402-WITHX402 2025-11-21T19:02:13.287Z] ============================================
[ROUTE 2025-11-21T19:02:13.287Z] /api/weather handler executing
[ROUTE 2025-11-21T19:02:13.287Z] /api/weather handler completed (NextResponse.json called)
RESPONSE FROM ROUTE HANDLER Response {
  status: 500,
  statusText: '',
  headers: Headers { 'content-type': 'application/json' },
  body: ReadableStream { locked: false, state: 'readable', supportsBYOB: true },
  bodyUsed: false,
  ok: false,
  redirected: false,
  type: 'default',
  url: ''
}
[X402-WITHX402 2025-11-21T19:02:13.291Z] ============================================
[X402-WITHX402 2025-11-21T19:02:13.291Z] Route handler RETURNED at: 1763751733291
[X402-WITHX402 2025-11-21T19:02:13.291Z] Response status: 500
[X402-WITHX402 2025-11-21T19:02:13.291Z] ============================================
 GET /api/weather 500 in 443ms

CLIENT: { error: 'API failed' }

Test with successful API response:

[X402-WITHX402 2025-11-21T19:03:33.022Z] ============================================
[X402-WITHX402 2025-11-21T19:03:33.022Z] Payment verification passed
[X402-WITHX402 2025-11-21T19:03:33.022Z] CALLING route handler at: 1763751813022
[X402-WITHX402 2025-11-21T19:03:33.022Z] ============================================
[ROUTE 2025-11-21T19:03:33.022Z] /api/weather handler executing
[ROUTE 2025-11-21T19:03:33.022Z] /api/weather handler completed (NextResponse.json called)
RESPONSE FROM ROUTE HANDLER Response {
  status: 200,
  statusText: '',
  headers: Headers { 'content-type': 'application/json' },
  body: ReadableStream { locked: false, state: 'readable', supportsBYOB: true },
  bodyUsed: false,
  ok: true,
  redirected: false,
  type: 'default',
  url: ''
}
[X402-WITHX402 2025-11-21T19:03:33.023Z] ============================================
[X402-WITHX402 2025-11-21T19:03:33.023Z] Route handler RETURNED at: 1763751813023
[X402-WITHX402 2025-11-21T19:03:33.023Z] Response status: 200
[X402-WITHX402 2025-11-21T19:03:33.023Z] ============================================
[X402-WITHX402 2025-11-21T19:03:33.023Z] ============================================
[X402-WITHX402 2025-11-21T19:03:33.023Z] STARTING SETTLEMENT at: 1763751813023
[X402-WITHX402 2025-11-21T19:03:33.023Z] ============================================
[X402-SETTLEMENT 2025-11-21T19:03:33.023Z] Calling facilitator settle endpoint...
[X402-SETTLEMENT 2025-11-21T19:03:34.674Z] Facilitator settle call completed in 1651ms
[X402-SETTLEMENT 2025-11-21T19:03:34.674Z] Settlement result: {
  success: true,
  transaction: '0x6c3bf4621c7ceb6e46079f487cd360c1d0dc7d2036993fa2a65bb95ed18134ad',
  network: 'base-sepolia',
  payer: '0xE33A295AF5C90A0649DFBECfDf9D604789B892e2'
}
[X402-WITHX402 2025-11-21T19:03:34.675Z] ============================================
[X402-WITHX402 2025-11-21T19:03:34.675Z] SETTLEMENT COMPLETED at: 1763751814675
[X402-WITHX402 2025-11-21T19:03:34.675Z] Result: SUCCESS
[X402-WITHX402 2025-11-21T19:03:34.675Z] ============================================
[X402-WITHX402 2025-11-21T19:03:34.675Z] ============================================
[X402-WITHX402 2025-11-21T19:03:34.675Z] RETURNING RESPONSE TO CLIENT at: 1763751814675
[X402-WITHX402 2025-11-21T19:03:34.675Z] Total handler duration: 2068ms
[X402-WITHX402 2025-11-21T19:03:34.675Z] ============================================
 GET /api/weather 200 in 2073ms

CLIENT
{ report: { weather: 'sunny', temperature: 70 } }
{
  success: true,
  transaction: '0x6c3bf4621c7ceb6e46079f487cd360c1d0dc7d2036993fa2a65bb95ed18134ad',
  network: 'base-sepolia',
  payer: '0xE33A295AF5C90A0649DFBECfDf9D604789B892e2'
}

Tests

Added new unit tests for withX402 wrapper

Checklist

  • [x] I have formatted and linted my code
  • [x] All new and existing tests pass
  • [x] My commits are signed (required for merge) -- you may need to rebase if you initially pushed unsigned commits

phdargen avatar Nov 21 '25 19:11 phdargen

✅ Heimdall Review Status

Requirement Status More Info
Reviews 1/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

cb-heimdall avatar Nov 21 '25 19:11 cb-heimdall

@phdargen is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

vercel[bot] avatar Nov 21 '25 19:11 vercel[bot]

Review Error for carsonroscoe-cb @ 2025-11-22 01:42:16 UTC User failed mfa authentication, either user does not exist or public email is not set on your github profile. \ see go/mfa-help

cb-heimdall avatar Nov 22 '25 01:11 cb-heimdall