diff --git a/.changeset/eight-pants-drum.md b/.changeset/eight-pants-drum.md new file mode 100644 index 00000000000..3744dd06674 --- /dev/null +++ b/.changeset/eight-pants-drum.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Support x402 v2 diff --git a/apps/playground-web/src/app/api/paywall/route.ts b/apps/playground-web/src/app/api/paywall/route.ts index aae663b63ef..f3dd2b6ee43 100644 --- a/apps/playground-web/src/app/api/paywall/route.ts +++ b/apps/playground-web/src/app/api/paywall/route.ts @@ -27,7 +27,9 @@ export async function GET(request: NextRequest) { vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN, }); - const paymentData = request.headers.get("X-PAYMENT"); + const paymentData = + request.headers.get("PAYMENT-SIGNATURE") || + request.headers.get("X-PAYMENT"); const queryParams = request.nextUrl.searchParams; const chainId = queryParams.get("chainId"); diff --git a/apps/portal/src/app/x402/facilitator/page.mdx b/apps/portal/src/app/x402/facilitator/page.mdx index c8d53e540b4..69fd272eb9d 100644 --- a/apps/portal/src/app/x402/facilitator/page.mdx +++ b/apps/portal/src/app/x402/facilitator/page.mdx @@ -103,7 +103,7 @@ The payment token must support either: }); export async function GET(request: Request) { - const paymentData = request.headers.get("x-payment"); + const paymentData = request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT"); const result = await settlePayment({ resourceUrl: "https://api.example.com/premium-content", diff --git a/apps/portal/src/app/x402/page.mdx b/apps/portal/src/app/x402/page.mdx index fc73911cf7c..b6223cf2d9a 100644 --- a/apps/portal/src/app/x402/page.mdx +++ b/apps/portal/src/app/x402/page.mdx @@ -113,7 +113,7 @@ const thirdwebX402Facilitator = facilitator({ }); export async function GET(request: Request) { - const paymentData = request.headers.get("x-payment"); + const paymentData = request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT"); const result = await settlePayment({ resourceUrl: "https://api.example.com/premium-content", diff --git a/apps/portal/src/app/x402/server/page.mdx b/apps/portal/src/app/x402/server/page.mdx index e19c2b6597f..8bd38516156 100644 --- a/apps/portal/src/app/x402/server/page.mdx +++ b/apps/portal/src/app/x402/server/page.mdx @@ -211,7 +211,7 @@ Protect individual API endpoints with x402 payments: }); export async function GET(request: Request) { - const paymentData = request.headers.get("x-payment"); + const paymentData = request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT"); // Verify and process the payment const result = await settlePayment({ @@ -383,7 +383,7 @@ Protect multiple endpoints with a shared middleware: export async function middleware(request: NextRequest) { const method = request.method.toUpperCase(); const resourceUrl = request.nextUrl.toString(); - const paymentData = request.headers.get("x-payment"); + const paymentData = request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT"); const result = await settlePayment({ resourceUrl, diff --git a/packages/nexus/src/settle-payment.ts b/packages/nexus/src/settle-payment.ts index d64c748a7cd..a8d881882f0 100644 --- a/packages/nexus/src/settle-payment.ts +++ b/packages/nexus/src/settle-payment.ts @@ -32,7 +32,7 @@ import { stringify } from "./utils.js"; * }); * * export async function GET(request: Request) { - * const paymentData = request.headers.get("x-payment"); + * const paymentData = request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT"); * * // verify and process the payment * const result = await settlePayment({ diff --git a/packages/nexus/src/verify-payment.ts b/packages/nexus/src/verify-payment.ts index 30ca88cd975..41315905d40 100644 --- a/packages/nexus/src/verify-payment.ts +++ b/packages/nexus/src/verify-payment.ts @@ -24,7 +24,7 @@ import { * }); * * export async function GET(request: Request) { - * const paymentData = request.headers.get("x-payment"); + * const paymentData = request.headers.get("PAYMENT-SIGNATURE") || request.headers.get("X-PAYMENT"); * * const paymentArgs = { * resourceUrl: "https://api.example.com/premium-content", diff --git a/packages/thirdweb/src/x402/common.ts b/packages/thirdweb/src/x402/common.ts index c15ba4873ee..86e23f7cabb 100644 --- a/packages/thirdweb/src/x402/common.ts +++ b/packages/thirdweb/src/x402/common.ts @@ -6,7 +6,7 @@ import { resolveContractAbi } from "../contract/actions/resolve-abi.js"; import { getContract } from "../contract/contract.js"; import { isPermitSupported } from "../extensions/erc20/__generated__/IERC20Permit/write/permit.js"; import { isTransferWithAuthorizationSupported } from "../extensions/erc20/__generated__/USDC/write/transferWithAuthorization.js"; -import { decodePayment } from "./encode.js"; +import { decodePayment, encodePaymentRequired } from "./encode.js"; import { networkToCaip2ChainId, type RequestedPaymentPayload, @@ -16,6 +16,8 @@ import { type ERC20TokenAmount, type PaymentArgs, type PaymentRequiredResult, + type PaymentRequiredResultV1, + type PaymentRequiredResultV2, type SupportedSignatureType, x402Version, } from "./types.js"; @@ -27,6 +29,50 @@ type GetPaymentRequirementsResult = { decodedPayment: RequestedPaymentPayload; }; +/** + * Formats a payment required response in x402 v2 format (header-based) + */ +function formatPaymentRequiredResponseV2( + paymentRequirements: RequestedPaymentRequirements[], + error: string, + resourceUrl: string, +): PaymentRequiredResultV2 { + const paymentRequired = { + x402Version: 2, + error, + accepts: paymentRequirements, + resource: { url: resourceUrl }, + }; + + return { + status: 402, + responseHeaders: { + "PAYMENT-REQUIRED": encodePaymentRequired(paymentRequired), + }, + responseBody: {} as Record, + }; +} + +/** + * Formats a payment required response in x402 v1 format (body-based) + */ +function formatPaymentRequiredResponseV1( + paymentRequirements: RequestedPaymentRequirements[], + error: string, +): PaymentRequiredResultV1 { + return { + status: 402, + responseHeaders: { + "Content-Type": "application/json", + }, + responseBody: { + x402Version: 1, + error, + accepts: paymentRequirements, + }, + }; +} + /** * Decodes a payment request and returns the payment requirements, selected payment requirements, and decoded payment * @param args @@ -35,37 +81,36 @@ type GetPaymentRequirementsResult = { export async function decodePaymentRequest( args: PaymentArgs, ): Promise { - const { facilitator, routeConfig = {}, paymentData } = args; + const { facilitator, routeConfig = {}, paymentData, resourceUrl } = args; const { errorMessages } = routeConfig; + // facilitator.accepts() returns v1 format from API - extract payment requirements const paymentRequirementsResult = await facilitator.accepts(args); + const paymentRequirements = paymentRequirementsResult.responseBody.accepts; - // Check for payment header, if none, return the payment requirements + // Check for payment header, if none, return the payment requirements in v2 format (default) if (!paymentData) { - return paymentRequirementsResult; + return formatPaymentRequiredResponseV2( + paymentRequirements, + "Payment required", + resourceUrl, + ); } - const paymentRequirements = paymentRequirementsResult.responseBody.accepts; - // decode b64 payment let decodedPayment: RequestedPaymentPayload; try { decodedPayment = decodePayment(paymentData); - decodedPayment.x402Version = x402Version; + // Preserve version provided by the client, default to the current protocol version if missing + decodedPayment.x402Version ??= x402Version; } catch (error) { - return { - status: 402, - responseHeaders: { - "Content-Type": "application/json", - }, - responseBody: { - x402Version, - error: - errorMessages?.invalidPayment || - (error instanceof Error ? error.message : "Invalid payment"), - accepts: paymentRequirements, - }, - }; + // Decode error - default to v2 format since we can't determine client version + return formatPaymentRequiredResponseV2( + paymentRequirements, + errorMessages?.invalidPayment || + (error instanceof Error ? error.message : "Invalid payment"), + resourceUrl, + ); } const selectedPaymentRequirements = paymentRequirements.find( @@ -75,19 +120,19 @@ export async function decodePaymentRequest( networkToCaip2ChainId(decodedPayment.network), ); if (!selectedPaymentRequirements) { - return { - status: 402, - responseHeaders: { - "Content-Type": "application/json", - }, - responseBody: { - x402Version, - error: - errorMessages?.noMatchingRequirements || - "Unable to find matching payment requirements", - accepts: paymentRequirements, - }, - }; + // Use the client's version for the response format + const errorMessage = + errorMessages?.noMatchingRequirements || + "Unable to find matching payment requirements"; + + if (decodedPayment.x402Version === 1) { + return formatPaymentRequiredResponseV1(paymentRequirements, errorMessage); + } + return formatPaymentRequiredResponseV2( + paymentRequirements, + errorMessage, + resourceUrl, + ); } return { diff --git a/packages/thirdweb/src/x402/encode.ts b/packages/thirdweb/src/x402/encode.ts index 4c95fb446c2..b9bb5d8c0bd 100644 --- a/packages/thirdweb/src/x402/encode.ts +++ b/packages/thirdweb/src/x402/encode.ts @@ -1,5 +1,8 @@ import type { ExactEvmPayload } from "x402/types"; -import type { RequestedPaymentPayload } from "./schemas.js"; +import type { + RequestedPaymentPayload, + RequestedPaymentRequirements, +} from "./schemas.js"; /** * Encodes a payment payload into a base64 string, ensuring bigint values are properly stringified @@ -44,6 +47,21 @@ export function decodePayment(payment: string): RequestedPaymentPayload { return obj; } +/** + * Encodes a payment required object into a base64 string for the PAYMENT-REQUIRED header (x402 v2) + * + * @param paymentRequired - The payment required object to encode + * @returns A base64 encoded string representation of the payment required object + */ +export function encodePaymentRequired(paymentRequired: { + x402Version: number; + error?: string; + accepts: RequestedPaymentRequirements[]; + resource?: { url: string; description?: string; mimeType?: string }; +}): string { + return safeBase64Encode(JSON.stringify(paymentRequired)); +} + /** * Encodes a string to base64 format * diff --git a/packages/thirdweb/src/x402/facilitator.ts b/packages/thirdweb/src/x402/facilitator.ts index fec56574399..444d5acd63d 100644 --- a/packages/thirdweb/src/x402/facilitator.ts +++ b/packages/thirdweb/src/x402/facilitator.ts @@ -10,7 +10,11 @@ import { type RequestedPaymentPayload, type RequestedPaymentRequirements, } from "./schemas.js"; -import type { PaymentArgs, PaymentRequiredResult } from "./types.js"; +import { + type PaymentArgs, + type PaymentRequiredResultV1, + x402Version, +} from "./types.js"; export type WaitUntil = "simulated" | "submitted" | "confirmed"; @@ -50,7 +54,7 @@ export type ThirdwebX402Facilitator = { }) => Promise; accepts: ( args: Omit, - ) => Promise; + ) => Promise; }; const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402"; @@ -264,7 +268,7 @@ export function facilitator( async accepts( args: Omit, - ): Promise { + ): Promise { const url = config.baseUrl ?? DEFAULT_BASE_URL; let headers = { "Content-Type": "application/json" }; const authHeaders = await facilitator.createAuthHeaders(); @@ -284,6 +288,7 @@ export function facilitator( serverWalletAddress: facilitator.address, recipientAddress: args.payTo, extraMetadata: args.extraMetadata, + x402Version: args.x402Version ?? x402Version, }), }); if (res.status !== 402) { @@ -294,7 +299,7 @@ export function facilitator( return { status: res.status as 402, responseBody: - (await res.json()) as PaymentRequiredResult["responseBody"], + (await res.json()) as PaymentRequiredResultV1["responseBody"], responseHeaders: { "Content-Type": "application/json", }, diff --git a/packages/thirdweb/src/x402/fetchWithPayment.test.ts b/packages/thirdweb/src/x402/fetchWithPayment.test.ts index 9207e6f264f..ff925da2ef1 100644 --- a/packages/thirdweb/src/x402/fetchWithPayment.test.ts +++ b/packages/thirdweb/src/x402/fetchWithPayment.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { safeBase64Decode, safeBase64Encode } from "./encode.js"; import { wrapFetchWithPayment } from "./fetchWithPayment.js"; +import { getPaymentRequestHeader } from "./headers.js"; // Mock the createPaymentHeader function vi.mock("./sign.js", () => ({ @@ -34,7 +35,7 @@ describe("wrapFetchWithPayment", () => { }; const mock402ResponseData = { - x402Version: 1, + x402Version: 2, accepts: [mockPaymentRequirements], error: undefined, }; @@ -109,9 +110,11 @@ describe("wrapFetchWithPayment", () => { expect(response.status).toBe(200); expect(mockFetch).toHaveBeenCalledTimes(2); - // Verify the second call includes the X-PAYMENT header + // Verify the second call includes the payment header for the version const secondCallInit = mockFetch.mock.calls[1]?.[1] as RequestInit; - expect(secondCallInit.headers).toHaveProperty("X-PAYMENT"); + expect(secondCallInit.headers).toHaveProperty( + getPaymentRequestHeader(mock402ResponseData.x402Version), + ); }); it("should parse payment requirements from JSON body when payment-required header is absent", async () => { @@ -141,9 +144,11 @@ describe("wrapFetchWithPayment", () => { expect(response.status).toBe(200); expect(mockFetch).toHaveBeenCalledTimes(2); - // Verify the second call includes the X-PAYMENT header + // Verify the second call includes the payment header for the version const secondCallInit = mockFetch.mock.calls[1]?.[1] as RequestInit; - expect(secondCallInit.headers).toHaveProperty("X-PAYMENT"); + expect(secondCallInit.headers).toHaveProperty( + getPaymentRequestHeader(mock402ResponseData.x402Version), + ); }); it("should prefer payment-required header over JSON body when both are present", async () => { @@ -152,12 +157,12 @@ describe("wrapFetchWithPayment", () => { maxAmountRequired: "500000", // Different amount to verify header is used }; const headerResponseData = { - x402Version: 1, + x402Version: 2, accepts: [headerPaymentRequirements], }; const bodyResponseData = { - x402Version: 1, + x402Version: 2, accepts: [{ ...mockPaymentRequirements, maxAmountRequired: "2000000" }], }; @@ -237,9 +242,11 @@ describe("wrapFetchWithPayment", () => { expect(response.status).toBe(200); expect(mockFetch).toHaveBeenCalledTimes(2); - // Verify the second call includes the X-PAYMENT header + // Verify the second call includes the payment header for the version const secondCallInit = mockFetch.mock.calls[1]?.[1] as RequestInit; - expect(secondCallInit.headers).toHaveProperty("X-PAYMENT"); + expect(secondCallInit.headers).toHaveProperty( + getPaymentRequestHeader(mock402ResponseData.x402Version), + ); }); it("should correctly decode a raw base64 encoded payment-required header", async () => { @@ -297,8 +304,10 @@ describe("wrapFetchWithPayment", () => { expect(response.status).toBe(200); expect(mockFetch).toHaveBeenCalledTimes(2); - // Verify the retry request was made with X-PAYMENT header + // Verify the retry request was made with the v1 payment header const secondCallInit = mockFetch.mock.calls[1]?.[1] as RequestInit; - expect(secondCallInit.headers).toHaveProperty("X-PAYMENT"); + expect(secondCallInit.headers).toHaveProperty( + getPaymentRequestHeader(parsed.x402Version), + ); }); }); diff --git a/packages/thirdweb/src/x402/fetchWithPayment.ts b/packages/thirdweb/src/x402/fetchWithPayment.ts index 9100a12a61b..2a529c4fe91 100644 --- a/packages/thirdweb/src/x402/fetchWithPayment.ts +++ b/packages/thirdweb/src/x402/fetchWithPayment.ts @@ -5,6 +5,10 @@ import type { AsyncStorage } from "../utils/storage/AsyncStorage.js"; import { webLocalStorage } from "../utils/storage/webStorage.js"; import type { Wallet } from "../wallets/interfaces/wallet.js"; import { safeBase64Decode } from "./encode.js"; +import { + getPaymentRequestHeader, + getPaymentResponseHeader, +} from "./headers.js"; import { clearPermitSignatureFromCache } from "./permitSignatureStorage.js"; import { extractEvmChainId, @@ -13,6 +17,7 @@ import { RequestedPaymentRequirementsSchema, } from "./schemas.js"; import { createPaymentHeader } from "./sign.js"; +import { x402Version as defaultX402Version } from "./types.js"; /** * Enables the payment of APIs using the x402 payment protocol. @@ -89,14 +94,13 @@ export function wrapFetchWithPayment( accepts: unknown[]; error?: string; }; - if (!Array.isArray(parsed.accepts)) { throw new Error( `402 response has no usable x402 payment requirements. ${parsed.error ?? ""}`, ); } - x402Version = parsed.x402Version; + x402Version = parsed.x402Version ?? defaultX402Version; parsedPaymentRequirements = parsed.accepts.map((x) => RequestedPaymentRequirementsSchema.parse(x), ); @@ -114,7 +118,7 @@ export function wrapFetchWithPayment( ); } - x402Version = body.x402Version; + x402Version = body.x402Version ?? defaultX402Version; parsedPaymentRequirements = body.accepts.map((x) => RequestedPaymentRequirementsSchema.parse(x), ); @@ -180,6 +184,9 @@ export function wrapFetchWithPayment( options?.storage ?? webLocalStorage, ); + const paymentRequestHeaderName = getPaymentRequestHeader(x402Version); + const paymentResponseHeaderName = getPaymentResponseHeader(x402Version); + const initParams = init || {}; if ((initParams as { __is402Retry?: boolean }).__is402Retry) { @@ -190,8 +197,8 @@ export function wrapFetchWithPayment( ...initParams, headers: { ...(initParams.headers || {}), - "X-PAYMENT": paymentHeader, - "Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE", + [paymentRequestHeaderName]: paymentHeader, + "Access-Control-Expose-Headers": paymentResponseHeaderName, }, __is402Retry: true, }; diff --git a/packages/thirdweb/src/x402/headers.ts b/packages/thirdweb/src/x402/headers.ts new file mode 100644 index 00000000000..47d29670a60 --- /dev/null +++ b/packages/thirdweb/src/x402/headers.ts @@ -0,0 +1,26 @@ +import { type X402Version, x402Version } from "./types.js"; + +const PAYMENT_HEADER_V1 = "X-PAYMENT"; +const PAYMENT_HEADER_V2 = "PAYMENT-SIGNATURE"; +const PAYMENT_RESPONSE_HEADER_V1 = "X-PAYMENT-RESPONSE"; +const PAYMENT_RESPONSE_HEADER_V2 = "PAYMENT-RESPONSE"; + +function resolveVersion(version?: number | X402Version): X402Version { + return version === 1 ? 1 : 2; +} + +export function getPaymentRequestHeader( + version?: number | X402Version, +): string { + const resolvedVersion = resolveVersion(version ?? x402Version); + return resolvedVersion === 1 ? PAYMENT_HEADER_V1 : PAYMENT_HEADER_V2; +} + +export function getPaymentResponseHeader( + version?: number | X402Version, +): string { + const resolvedVersion = resolveVersion(version ?? x402Version); + return resolvedVersion === 1 + ? PAYMENT_RESPONSE_HEADER_V1 + : PAYMENT_RESPONSE_HEADER_V2; +} diff --git a/packages/thirdweb/src/x402/schemas.ts b/packages/thirdweb/src/x402/schemas.ts index 6531f02b5d6..15e3fd404ed 100644 --- a/packages/thirdweb/src/x402/schemas.ts +++ b/packages/thirdweb/src/x402/schemas.ts @@ -82,7 +82,7 @@ const FacilitatorSupportedResponseSchema = SupportedPaymentKindsResponseSchema.extend({ kinds: z.array( z.object({ - x402Version: z.literal(1), + x402Version: z.union([z.literal(1), z.literal(2)]), scheme: PaymentSchemeSchema, network: FacilitatorNetworkSchema, extra: z diff --git a/packages/thirdweb/src/x402/settle-payment.ts b/packages/thirdweb/src/x402/settle-payment.ts index 50cb2b61fa2..e0db722f7fd 100644 --- a/packages/thirdweb/src/x402/settle-payment.ts +++ b/packages/thirdweb/src/x402/settle-payment.ts @@ -1,6 +1,7 @@ import { stringify } from "../utils/json.js"; import { decodePaymentRequest } from "./common.js"; import { safeBase64Encode } from "./encode.js"; +import { getPaymentResponseHeader } from "./headers.js"; import { type SettlePaymentArgs, type SettlePaymentResult, @@ -37,7 +38,9 @@ import { * }); * * export async function GET(request: Request) { - * const paymentData = request.headers.get("x-payment"); + * const paymentData = + * request.headers.get("PAYMENT-SIGNATURE") || + * request.headers.get("X-PAYMENT"); * * // verify and process the payment * const result = await settlePayment({ @@ -104,7 +107,8 @@ import { * const result = await settlePayment({ * resourceUrl: `${req.protocol}://${req.get('host')}${req.originalUrl}`, * method: req.method, - * paymentData: req.headers["x-payment"], + * paymentData: + * req.headers["payment-signature"] ?? req.headers["x-payment"], * payTo: "0x1234567890123456789012345678901234567890", * network: arbitrumSepolia, // or any other chain * price: "$0.05", @@ -151,6 +155,9 @@ export async function settlePayment( decodePaymentResult; try { + const paymentResponseHeaderName = getPaymentResponseHeader( + decodedPayment.x402Version, + ); const settlement = await facilitator.settle( decodedPayment, selectedPaymentRequirements, @@ -162,8 +169,8 @@ export async function settlePayment( status: 200, paymentReceipt: settlement, responseHeaders: { - "Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE", - "X-PAYMENT-RESPONSE": safeBase64Encode(stringify(settlement)), + "Access-Control-Expose-Headers": paymentResponseHeaderName, + [paymentResponseHeaderName]: safeBase64Encode(stringify(settlement)), }, }; } else { @@ -174,7 +181,7 @@ export async function settlePayment( "Content-Type": "application/json", }, responseBody: { - x402Version, + x402Version: decodedPayment.x402Version ?? x402Version, error, errorMessage: errorMessages?.settlementFailed || settlement.errorMessage, @@ -190,7 +197,7 @@ export async function settlePayment( "Content-Type": "application/json", }, responseBody: { - x402Version, + x402Version: decodedPayment.x402Version ?? x402Version, error: "Settlement error", errorMessage: errorMessages?.settlementFailed || diff --git a/packages/thirdweb/src/x402/types.ts b/packages/thirdweb/src/x402/types.ts index 54c3887f36d..8afd31f6aae 100644 --- a/packages/thirdweb/src/x402/types.ts +++ b/packages/thirdweb/src/x402/types.ts @@ -11,7 +11,9 @@ import type { SupportedSignatureTypeSchema, } from "./schemas.js"; -export const x402Version = 1; +const supportedX402Versions = [1, 2] as const; +export type X402Version = (typeof supportedX402Versions)[number]; +export const x402Version: X402Version = 2; /** * Configuration object for verifying or processing X402 payments. @@ -23,7 +25,9 @@ export type PaymentArgs = { resourceUrl: string; /** The HTTP method used to access the resource */ method: "GET" | "POST" | ({} & string); - /** The payment data/proof provided by the client, typically from the X-PAYMENT header */ + /** + * The payment data/proof provided by the client, typically from the PAYMENT-SIGNATURE (v2) or X-PAYMENT (v1) header + */ paymentData?: string | null; /** The blockchain network where the payment should be processed */ network: FacilitatorNetwork | Chain; @@ -41,15 +45,22 @@ export type PaymentArgs = { payTo?: string; /** Optional extra data to be included in the payment request */ extraMetadata?: Record; + /** The x402 protocol version to use, defaults to v2 */ + x402Version?: X402Version; }; export type SettlePaymentArgs = PaymentArgs & { waitUntil?: WaitUntil; }; -export type PaymentRequiredResult = { +/** + * Payment required result for x402 v1 (body-based format) + */ +export type PaymentRequiredResultV1 = { /** HTTP 402 - Payment Required, verification or processing failed or payment missing */ status: 402; + /** Response headers for the error response */ + responseHeaders: Record; /** The error response body containing payment requirements */ responseBody: { /** The X402 protocol version */ @@ -65,10 +76,27 @@ export type PaymentRequiredResult = { /** Optional link to a wallet to fund the wallet of the payer */ fundWalletLink?: string; }; - /** Response headers for the error response */ +}; + +/** + * Payment required result for x402 v2 (header-based format) + */ +export type PaymentRequiredResultV2 = { + /** HTTP 402 - Payment Required, verification or processing failed or payment missing */ + status: 402; + /** Response headers containing base64 encoded payment requirements */ responseHeaders: Record; + /** Empty response body for v2 */ + responseBody: Record; }; +/** + * Payment required result supporting both v1 and v2 formats + */ +export type PaymentRequiredResult = + | PaymentRequiredResultV1 + | PaymentRequiredResultV2; + /** * The result of a payment settlement operation. * diff --git a/packages/thirdweb/src/x402/verify-payment.ts b/packages/thirdweb/src/x402/verify-payment.ts index 237e51bbd79..0ebf7436ab6 100644 --- a/packages/thirdweb/src/x402/verify-payment.ts +++ b/packages/thirdweb/src/x402/verify-payment.ts @@ -29,7 +29,9 @@ import { * }); * * export async function GET(request: Request) { - * const paymentData = request.headers.get("x-payment"); + * const paymentData = + * request.headers.get("payment-signature") ?? + * request.headers.get("x-payment"); * * const paymentArgs = { * resourceUrl: "https://api.example.com/premium-content", @@ -121,7 +123,7 @@ export async function verifyPayment( "Content-Type": "application/json", }, responseBody: { - x402Version, + x402Version: decodedPayment.x402Version ?? x402Version, error: error, errorMessage: errorMessages?.verificationFailed || verification.errorMessage, @@ -137,7 +139,7 @@ export async function verifyPayment( "Content-Type": "application/json", }, responseBody: { - x402Version, + x402Version: decodedPayment.x402Version ?? x402Version, error: "Verification error", errorMessage: errorMessages?.verificationFailed ||