From e48a4122be77ad54386a032c30918defd21da0b4 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Tue, 23 Sep 2025 15:37:40 +1200 Subject: [PATCH] [SDK] Rename verifyPayment() to processPayment() for x402 payments --- .changeset/some-moons-burn.md | 2 +- .../src/app/payments/x402/page.tsx | 7 +- apps/playground-web/src/middleware.ts | 4 +- apps/portal/src/app/payments/x402/page.mdx | 10 +- packages/thirdweb/src/exports/x402.ts | 12 +- packages/thirdweb/src/x402/common.ts | 242 ++++++++++ packages/thirdweb/src/x402/facilitator.ts | 2 +- packages/thirdweb/src/x402/settle-payment.ts | 186 ++++++++ packages/thirdweb/src/x402/types.ts | 90 ++++ packages/thirdweb/src/x402/verify-payment.ts | 412 ++---------------- 10 files changed, 575 insertions(+), 392 deletions(-) create mode 100644 packages/thirdweb/src/x402/common.ts create mode 100644 packages/thirdweb/src/x402/settle-payment.ts create mode 100644 packages/thirdweb/src/x402/types.ts diff --git a/.changeset/some-moons-burn.md b/.changeset/some-moons-burn.md index cdb5a989c3a..5679ab538cd 100644 --- a/.changeset/some-moons-burn.md +++ b/.changeset/some-moons-burn.md @@ -2,4 +2,4 @@ "thirdweb": minor --- -Accept arbitrary chain ids for x402 payments with new verifyPayment() backend utility +Accept arbitrary chain ids for x402 payments with new settlePayment() and verifyPayment() backend utility functions diff --git a/apps/playground-web/src/app/payments/x402/page.tsx b/apps/playground-web/src/app/payments/x402/page.tsx index e08cb8df411..51e38809c34 100644 --- a/apps/playground-web/src/app/payments/x402/page.tsx +++ b/apps/playground-web/src/app/payments/x402/page.tsx @@ -57,7 +57,7 @@ function ServerCodeExample() { className="h-full rounded-none border-none" code={`// src/middleware.ts -import { facilitator, verifyPayment } from "thirdweb/x402"; +import { facilitator, settlePayment } from "thirdweb/x402"; import { createThirdwebClient } from "thirdweb"; const client = createThirdwebClient({ secretKey: "your-secret-key" }); @@ -71,16 +71,13 @@ export async function middleware(request: NextRequest) { const resourceUrl = request.nextUrl.toString(); const paymentData = request.headers.get("X-PAYMENT"); - const result = await verifyPayment({ + const result = await settlePayment({ resourceUrl, method, paymentData, payTo: "0xYourWalletAddress", network: "eip155:11155111", // or any other chain id price: "$0.01", // can also be a ERC20 token amount - routeConfig: { - description: "Access to paid content", - }, facilitator: thirdwebX402Facilitator, }); diff --git a/apps/playground-web/src/middleware.ts b/apps/playground-web/src/middleware.ts index c4a440d067a..eb74b1046ca 100644 --- a/apps/playground-web/src/middleware.ts +++ b/apps/playground-web/src/middleware.ts @@ -1,7 +1,7 @@ import { type NextRequest, NextResponse } from "next/server"; import { createThirdwebClient } from "thirdweb"; import { arbitrumSepolia } from "thirdweb/chains"; -import { facilitator, verifyPayment } from "thirdweb/x402"; +import { facilitator, settlePayment } from "thirdweb/x402"; const client = createThirdwebClient({ secretKey: process.env.THIRDWEB_SECRET_KEY as string, @@ -26,7 +26,7 @@ export async function middleware(request: NextRequest) { const resourceUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}${pathname}`; const paymentData = request.headers.get("X-PAYMENT"); - const result = await verifyPayment({ + const result = await settlePayment({ resourceUrl, method, paymentData, diff --git a/apps/portal/src/app/payments/x402/page.mdx b/apps/portal/src/app/payments/x402/page.mdx index 6baf022e5f9..f3b008d029f 100644 --- a/apps/portal/src/app/payments/x402/page.mdx +++ b/apps/portal/src/app/payments/x402/page.mdx @@ -41,15 +41,15 @@ const response = await fetchWithPay('https://api.example.com/paid-endpoint'); ## Server Side -To make your API calls payable, you can use the `verifyPayment` function in a simple middleware or in your endpoint directly. +To make your API calls payable, you can use the `settlePayment` function in a middleware or in your endpoint directly. -Use the `facilitator` configuration function settle transactions with your thirdweb server wallet gaslessly and pass it to the `verifyPayment` function. +Use the `facilitator` configuration function settle transactions with your thirdweb server wallet gaslessly and pass it to the `settlePayment` function. Here's an example with a Next.js middleware: ```typescript import { createThirdwebClient } from "thirdweb"; -import { facilitator, verifyPayment } from "thirdweb/x402"; +import { facilitator, settlePayment } from "thirdweb/x402"; const client = createThirdwebClient({ secretKey: "your-secret-key" }); const thirdwebX402Facilitator = facilitator({ @@ -62,7 +62,7 @@ export async function middleware(request: NextRequest) { const resourceUrl = request.nextUrl.toString(); const paymentData = request.headers.get("X-PAYMENT"); - const result = await verifyPayment({ + const result = await settlePayment({ resourceUrl, method, paymentData, @@ -97,3 +97,5 @@ export const config = { matcher: ["/api/paid-endpoint"], }; ``` + +You can also use the `verifyPayment` function to verify the payment before settling it. This lets you do the work that requires payment first and then settle the payment. diff --git a/packages/thirdweb/src/exports/x402.ts b/packages/thirdweb/src/exports/x402.ts index 0439de7cc48..7b16b43f5f1 100644 --- a/packages/thirdweb/src/exports/x402.ts +++ b/packages/thirdweb/src/exports/x402.ts @@ -4,8 +4,10 @@ export { type ThirdwebX402FacilitatorConfig, } from "../x402/facilitator.js"; export { wrapFetchWithPayment } from "../x402/fetchWithPayment.js"; -export { - type VerifyPaymentArgs, - type VerifyPaymentResult, - verifyPayment, -} from "../x402/verify-payment.js"; +export { settlePayment } from "../x402/settle-payment.js"; +export type { + PaymentArgs, + SettlePaymentResult, + VerifyPaymentResult, +} from "../x402/types.js"; +export { verifyPayment } from "../x402/verify-payment.js"; diff --git a/packages/thirdweb/src/x402/common.ts b/packages/thirdweb/src/x402/common.ts new file mode 100644 index 00000000000..4057770d711 --- /dev/null +++ b/packages/thirdweb/src/x402/common.ts @@ -0,0 +1,242 @@ +import { + type ERC20TokenAmount, + type Money, + moneySchema, + type Network, + SupportedEVMNetworks, +} from "x402/types"; +import { getAddress } from "../utils/address.js"; +import { decodePayment } from "./encode.js"; +import type { facilitator as facilitatorType } from "./facilitator.js"; +import { + type FacilitatorNetwork, + networkToChainId, + type RequestedPaymentPayload, + type RequestedPaymentRequirements, +} from "./schemas.js"; +import { + type PaymentArgs, + type PaymentRequiredResult, + x402Version, +} from "./types.js"; + +type GetPaymentRequirementsResult = { + status: 200; + paymentRequirements: RequestedPaymentRequirements[]; + selectedPaymentRequirements: RequestedPaymentRequirements; + decodedPayment: RequestedPaymentPayload; +}; + +/** + * Decodes a payment request and returns the payment requirements, selected payment requirements, and decoded payment + * @param args + * @returns The payment requirements, selected payment requirements, and decoded payment + */ +export async function decodePaymentRequest( + args: PaymentArgs, +): Promise { + const { + price, + network, + facilitator, + resourceUrl, + routeConfig = {}, + payTo, + method, + paymentData, + } = args; + const { + description, + mimeType, + maxTimeoutSeconds, + inputSchema, + outputSchema, + errorMessages, + discoverable, + } = routeConfig; + const atomicAmountForAsset = await processPriceToAtomicAmount( + price, + network, + facilitator, + ); + if ("error" in atomicAmountForAsset) { + return { + status: 402, + responseHeaders: { "Content-Type": "application/json" }, + responseBody: { + x402Version, + error: atomicAmountForAsset.error, + accepts: [], + }, + }; + } + const { maxAmountRequired, asset } = atomicAmountForAsset; + + const paymentRequirements: RequestedPaymentRequirements[] = []; + + if ( + SupportedEVMNetworks.includes(network as Network) || + network.startsWith("eip155:") + ) { + paymentRequirements.push({ + scheme: "exact", + network, + maxAmountRequired, + resource: resourceUrl, + description: description ?? "", + mimeType: mimeType ?? "application/json", + payTo: getAddress(payTo), + maxTimeoutSeconds: maxTimeoutSeconds ?? 300, + asset: getAddress(asset.address), + // TODO: Rename outputSchema to requestStructure + outputSchema: { + input: { + type: "http", + method, + discoverable: discoverable ?? true, + ...inputSchema, + }, + output: outputSchema, + }, + extra: (asset as ERC20TokenAmount["asset"]).eip712, + }); + } else { + return { + status: 402, + responseHeaders: { + "Content-Type": "application/json", + }, + responseBody: { + x402Version, + error: `Unsupported network: ${network}`, + accepts: paymentRequirements, + }, + }; + } + + // Check for payment header + if (!paymentData) { + return { + status: 402, + responseHeaders: { + "Content-Type": "application/json", + }, + responseBody: { + x402Version, + error: errorMessages?.paymentRequired || "X-PAYMENT header is required", + accepts: paymentRequirements, + }, + }; + } + + // Verify payment + let decodedPayment: RequestedPaymentPayload; + try { + decodedPayment = decodePayment(paymentData); + 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, + }, + }; + } + + const selectedPaymentRequirements = paymentRequirements.find( + (value) => + value.scheme === decodedPayment.scheme && + value.network === 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, + }, + }; + } + + return { + status: 200, + paymentRequirements, + decodedPayment, + selectedPaymentRequirements, + }; +} + +/** + * Parses the amount from the given price + * + * @param price - The price to parse + * @param network - The network to get the default asset for + * @returns The parsed amount or an error message + */ +async function processPriceToAtomicAmount( + price: Money | ERC20TokenAmount, + network: FacilitatorNetwork, + facilitator: ReturnType, +): Promise< + | { maxAmountRequired: string; asset: ERC20TokenAmount["asset"] } + | { error: string } +> { + // Handle USDC amount (string) or token amount (ERC20TokenAmount) + let maxAmountRequired: string; + let asset: ERC20TokenAmount["asset"]; + + if (typeof price === "string" || typeof price === "number") { + // USDC amount in dollars + const parsedAmount = moneySchema.safeParse(price); + if (!parsedAmount.success) { + return { + error: `Invalid price (price: ${price}). Must be in the form "$3.10", 0.10, "0.001", ${parsedAmount.error}`, + }; + } + const parsedUsdAmount = parsedAmount.data; + const defaultAsset = await getDefaultAsset(network, facilitator); + if (!defaultAsset) { + return { + error: `Unable to get default asset on ${network}. Please specify an asset in the payment requirements.`, + }; + } + asset = defaultAsset; + maxAmountRequired = (parsedUsdAmount * 10 ** asset.decimals).toString(); + } else { + // Token amount in atomic units + maxAmountRequired = price.amount; + asset = price.asset; + } + + return { + maxAmountRequired, + asset, + }; +} + +async function getDefaultAsset( + network: FacilitatorNetwork, + facilitator: ReturnType, +): Promise { + const supportedAssets = await facilitator.supported(); + const chainId = networkToChainId(network); + const matchingAsset = supportedAssets.kinds.find( + (supported) => supported.network === `eip155:${chainId}`, + ); + const assetConfig = matchingAsset?.extra + ?.defaultAsset as ERC20TokenAmount["asset"]; + return assetConfig; +} diff --git a/packages/thirdweb/src/x402/facilitator.ts b/packages/thirdweb/src/x402/facilitator.ts index 8db39bfd0e7..9588ef00b56 100644 --- a/packages/thirdweb/src/x402/facilitator.ts +++ b/packages/thirdweb/src/x402/facilitator.ts @@ -19,7 +19,7 @@ const DEFAULT_BASE_URL = "https://api.thirdweb.com/v1/payments/x402"; /** * Creates a facilitator for the x402 payment protocol. - * You can use this with `verifyPayment` or with any x402 middleware to enable settling transactions with your thirdweb server wallet. + * You can use this with `settlePayment` or with any x402 middleware to enable settling transactions with your thirdweb server wallet. * * @param config - The configuration for the facilitator * @returns a x402 compatible FacilitatorConfig diff --git a/packages/thirdweb/src/x402/settle-payment.ts b/packages/thirdweb/src/x402/settle-payment.ts new file mode 100644 index 00000000000..5ea9286e778 --- /dev/null +++ b/packages/thirdweb/src/x402/settle-payment.ts @@ -0,0 +1,186 @@ +import { stringify } from "../utils/json.js"; +import { decodePaymentRequest } from "./common.js"; +import { safeBase64Encode } from "./encode.js"; +import { + type PaymentArgs, + type SettlePaymentResult, + x402Version, +} from "./types.js"; + +/** + * Verifies and processes X402 payments for protected resources. + * + * This function implements the X402 payment protocol, verifying payment proofs + * and settling payments through a facilitator service. It handles the complete + * payment flow from validation to settlement. + * + * @param args - Configuration object containing payment verification parameters + * @returns A promise that resolves to either a successful payment result (200) or payment required error (402) + * + * @example + * + * ### Next.js API route example + * + * ```ts + * // Usage in a Next.js API route + * import { settlePayment, facilitator } from "thirdweb/x402"; + * import { createThirdwebClient } from "thirdweb"; + * + * const client = createThirdwebClient({ + * secretKey: process.env.THIRDWEB_SECRET_KEY, + * }); + * + * const thirdwebFacilitator = facilitator({ + * client, + * serverWalletAddress: "0x1234567890123456789012345678901234567890", + * }); + * + * export async function GET(request: Request) { + * const paymentData = request.headers.get("x-payment"); + * + * // verify and process the payment + * const result = await settlePayment({ + * resourceUrl: "https://api.example.com/premium-content", + * method: "GET", + * paymentData, + * payTo: "0x1234567890123456789012345678901234567890", + * network: "eip155:84532", // CAIP2 format: "eip155:" + * price: "$0.10", // or { amount: "100000", asset: { address: "0x...", decimals: 6 } } + * facilitator: thirdwebFacilitator, + * routeConfig: { + * description: "Access to premium API content", + * mimeType: "application/json", + * maxTimeoutSeconds: 300, + * }, + * }); + * + * if (result.status === 200) { + * // Payment verified and settled successfully + * return Response.json({ data: "premium content" }); + * } else { + * // Payment required + * return Response.json(result.responseBody, { + * status: result.status, + * headers: result.responseHeaders, + * }); + * } + * } + * ``` + * + * ### Express middleware example + * + * ```ts + * // Usage in Express middleware + * import express from "express"; + * import { settlePayment, facilitator } from "thirdweb/x402"; + * import { createThirdwebClient } from "thirdweb"; + * + * const client = createThirdwebClient({ + * secretKey: process.env.THIRDWEB_SECRET_KEY, + * }); + * + * const thirdwebFacilitator = facilitator({ + * client, + * serverWalletAddress: "0x1234567890123456789012345678901234567890", + * }); + * + * const app = express(); + * + * async function paymentMiddleware(req, res, next) { + * // verify and process the payment + * const result = await settlePayment({ + * resourceUrl: `${req.protocol}://${req.get('host')}${req.originalUrl}`, + * method: req.method, + * paymentData: req.headers["x-payment"], + * payTo: "0x1234567890123456789012345678901234567890", + * network: "eip155:8453", // CAIP2 format: "eip155:" + * price: "$0.05", + * facilitator: thirdwebFacilitator, + * }); + * + * if (result.status === 200) { + * // Set payment receipt headers and continue + * Object.entries(result.responseHeaders).forEach(([key, value]) => { + * res.setHeader(key, value); + * }); + * next(); + * } else { + * // Return payment required response + * res.status(result.status) + * .set(result.responseHeaders) + * .json(result.responseBody); + * } + * } + * + * app.get("/api/premium", paymentMiddleware, (req, res) => { + * res.json({ message: "This is premium content!" }); + * }); + * ``` + * + * @public + * @beta + * @bridge x402 + */ +export async function settlePayment( + args: PaymentArgs, +): Promise { + const { routeConfig = {}, facilitator } = args; + const { errorMessages } = routeConfig; + + const decodePaymentResult = await decodePaymentRequest(args); + + if (decodePaymentResult.status !== 200) { + return decodePaymentResult; + } + + const { selectedPaymentRequirements, decodedPayment, paymentRequirements } = + decodePaymentResult; + + // Settle payment + try { + const settlement = await facilitator.settle( + decodedPayment, + selectedPaymentRequirements, + ); + + if (settlement.success) { + return { + status: 200, + paymentReceipt: settlement, + responseHeaders: { + "Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE", + "X-PAYMENT-RESPONSE": safeBase64Encode(stringify(settlement)), + }, + }; + } else { + return { + status: 402, + responseHeaders: { + "Content-Type": "application/json", + }, + responseBody: { + x402Version, + error: + errorMessages?.settlementFailed || + settlement.errorReason || + "Settlement failed", + accepts: paymentRequirements, + }, + }; + } + } catch (error) { + return { + status: 402, + responseHeaders: { + "Content-Type": "application/json", + }, + responseBody: { + x402Version, + error: + errorMessages?.settlementFailed || + (error instanceof Error ? error.message : "Settlement error"), + accepts: paymentRequirements, + }, + }; + } +} diff --git a/packages/thirdweb/src/x402/types.ts b/packages/thirdweb/src/x402/types.ts new file mode 100644 index 00000000000..db28b38bce6 --- /dev/null +++ b/packages/thirdweb/src/x402/types.ts @@ -0,0 +1,90 @@ +import type { + ERC20TokenAmount, + Money, + PaymentMiddlewareConfig, +} from "x402/types"; +import type { Address } from "../utils/address.js"; +import type { Prettify } from "../utils/type-utils.js"; +import type { facilitator as facilitatorType } from "./facilitator.js"; +import type { + FacilitatorNetwork, + FacilitatorSettleResponse, + RequestedPaymentPayload, + RequestedPaymentRequirements, +} from "./schemas.js"; + +export const x402Version = 1; + +/** + * Configuration object for verifying or processing X402 payments. + * + * @public + */ +export type PaymentArgs = { + /** The URL of the resource being protected by the payment */ + 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 */ + paymentData?: string | null; + /** The wallet address that should receive the payment */ + payTo: Address; + /** The blockchain network where the payment should be processed */ + network: FacilitatorNetwork; + /** The price for accessing the resource - either a USD amount (e.g., "$0.10") or a specific token amount */ + price: Money | ERC20TokenAmount; + /** The payment facilitator instance used to verify and settle payments */ + facilitator: ReturnType; + /** Optional configuration for the payment middleware route */ + routeConfig?: PaymentMiddlewareConfig; +}; + +export type PaymentRequiredResult = { + /** HTTP 402 - Payment Required, verification or processing failed or payment missing */ + status: 402; + /** The error response body containing payment requirements */ + responseBody: { + /** The X402 protocol version */ + x402Version: number; + /** Human-readable error message */ + error: string; + /** Array of acceptable payment methods and requirements */ + accepts: RequestedPaymentRequirements[]; + /** Optional payer address if verification partially succeeded */ + payer?: string; + }; + /** Response headers for the error response */ + responseHeaders: Record; +}; + +/** + * The result of a payment settlement operation. + * + * @public + */ +export type SettlePaymentResult = Prettify< + | { + /** HTTP 200 - Payment was successfully processed */ + status: 200; + /** Response headers including payment receipt information */ + responseHeaders: Record; + /** The payment receipt from the payment facilitator */ + paymentReceipt: FacilitatorSettleResponse; + } + | PaymentRequiredResult +>; + +/** + * The result of a payment verification operation. + * + * @public + */ +export type VerifyPaymentResult = Prettify< + | { + /** HTTP 200 - Payment was successfully verified */ + status: 200; + decodedPayment: RequestedPaymentPayload; + selectedPaymentRequirements: RequestedPaymentRequirements; + } + | PaymentRequiredResult +>; diff --git a/packages/thirdweb/src/x402/verify-payment.ts b/packages/thirdweb/src/x402/verify-payment.ts index f6137548c52..067c1ce6c4c 100644 --- a/packages/thirdweb/src/x402/verify-payment.ts +++ b/packages/thirdweb/src/x402/verify-payment.ts @@ -1,90 +1,16 @@ +import { decodePaymentRequest } from "./common.js"; import { - type ERC20TokenAmount, - type Money, - moneySchema, - type Network, - type PaymentMiddlewareConfig, - SupportedEVMNetworks, -} from "x402/types"; -import { type Address, getAddress } from "../utils/address.js"; -import { stringify } from "../utils/json.js"; -import { decodePayment, safeBase64Encode } from "./encode.js"; -import type { facilitator as facilitatorType } from "./facilitator.js"; -import { - type FacilitatorNetwork, - type FacilitatorSettleResponse, - networkToChainId, - type RequestedPaymentPayload, - type RequestedPaymentRequirements, -} from "./schemas.js"; - -const x402Version = 1; - -/** - * Configuration object for verifying X402 payments. - * - * @public - */ -export type VerifyPaymentArgs = { - /** The URL of the resource being protected by the payment */ - 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 */ - paymentData?: string | null; - /** The wallet address that should receive the payment */ - payTo: Address; - /** The blockchain network where the payment should be processed */ - network: FacilitatorNetwork; - /** The price for accessing the resource - either a USD amount (e.g., "$0.10") or a specific token amount */ - price: Money | ERC20TokenAmount; - /** The payment facilitator instance used to verify and settle payments */ - facilitator: ReturnType; - /** Optional configuration for the payment middleware route */ - routeConfig?: PaymentMiddlewareConfig; -}; + type PaymentArgs, + type VerifyPaymentResult, + x402Version, +} from "./types.js"; /** - * The result of a payment verification operation. - * - * @public - */ -export type VerifyPaymentResult = - | { - /** HTTP 200 - Payment was successfully verified and settled */ - status: 200; - /** Response headers including payment receipt information */ - responseHeaders: Record; - /** The settlement receipt from the payment facilitator */ - paymentReceipt: FacilitatorSettleResponse; - } - | { - /** HTTP 402 - Payment Required, verification failed or payment missing */ - status: 402; - /** The error response body containing payment requirements */ - responseBody: { - /** The X402 protocol version */ - x402Version: number; - /** Human-readable error message */ - error: string; - /** Array of acceptable payment methods and requirements */ - accepts: RequestedPaymentRequirements[]; - /** Optional payer address if verification partially succeeded */ - payer?: string; - }; - /** Response headers for the error response */ - responseHeaders: Record; - }; - -/** - * Verifies and processes X402 payments for protected resources. - * - * This function implements the X402 payment protocol, verifying payment proofs - * and settling payments through a facilitator service. It handles the complete - * payment flow from validation to settlement. + * Verifies X402 payments for protected resources. This function only verifies the payment, + * you should use `settlePayment` to settle the payment. * * @param args - Configuration object containing payment verification parameters - * @returns A promise that resolves to either a successful payment result (200) or payment required error (402) + * @returns A promise that resolves to either a successful verification result (200) or payment required error (402) * * @example * ```ts @@ -104,7 +30,7 @@ export type VerifyPaymentResult = * export async function GET(request: Request) { * const paymentData = request.headers.get("x-payment"); * - * const result = await verifyPayment({ + * const paymentArgs = { * resourceUrl: "https://api.example.com/premium-content", * method: "GET", * paymentData, @@ -117,15 +43,22 @@ export type VerifyPaymentResult = * mimeType: "application/json", * maxTimeoutSeconds: 300, * }, - * }); + * }; + * + * // verify the payment + * const result = await verifyPayment(paymentArgs); * * if (result.status === 200) { - * // Payment verified and settled successfully - * return Response.json({ data: "premium content" }, { - * headers: result.responseHeaders, - * }); + * // Payment verified, but not settled yet + * // you can do the work that requires payment first + * const result = await doSomething(); + * // then settle the payment + * const settleResult = await settlePayment(paymentArgs); + * + * // then return the result + * return Response.json(result); * } else { - * // Payment required + * // verification failed, return payment required * return Response.json(result.responseBody, { * status: result.status, * headers: result.responseHeaders, @@ -134,244 +67,37 @@ export type VerifyPaymentResult = * } * ``` * - * @example - * ```ts - * // Usage in Express middleware - * import express from "express"; - * import { verifyPayment, facilitator } from "thirdweb/x402"; - * - * const app = express(); - * - * async function paymentMiddleware(req, res, next) { - * const result = await verifyPayment({ - * resourceUrl: `${req.protocol}://${req.get('host')}${req.originalUrl}`, - * method: req.method, - * paymentData: req.headers["x-payment"], - * payTo: "0x1234567890123456789012345678901234567890", - * network: "eip155:8453", // CAIP2 format: "eip155:" - * price: "$0.05", - * facilitator: thirdwebFacilitator, - * }); - * - * if (result.status === 200) { - * // Set payment receipt headers and continue - * Object.entries(result.responseHeaders).forEach(([key, value]) => { - * res.setHeader(key, value); - * }); - * next(); - * } else { - * // Return payment required response - * res.status(result.status) - * .set(result.responseHeaders) - * .json(result.responseBody); - * } - * } - * - * app.get("/api/premium", paymentMiddleware, (req, res) => { - * res.json({ message: "This is premium content!" }); - * }); - * ``` - * * @public * @beta * @bridge x402 */ export async function verifyPayment( - args: VerifyPaymentArgs, + args: PaymentArgs, ): Promise { - const { - price, - network, - routeConfig = {}, - resourceUrl, - method, - payTo, - paymentData: paymentProof, - facilitator, - } = args; - const { - description, - mimeType, - maxTimeoutSeconds, - inputSchema, - outputSchema, - errorMessages, - discoverable, - } = routeConfig; - - const atomicAmountForAsset = await processPriceToAtomicAmount( - price, - network, - facilitator, - ); - if ("error" in atomicAmountForAsset) { - return { - status: 402, - responseHeaders: { "Content-Type": "application/json" }, - responseBody: { - x402Version, - error: atomicAmountForAsset.error, - accepts: [], - }, - }; - } - const { maxAmountRequired, asset } = atomicAmountForAsset; + const { routeConfig = {}, facilitator } = args; + const { errorMessages } = routeConfig; - const paymentRequirements: RequestedPaymentRequirements[] = []; + const decodePaymentResult = await decodePaymentRequest(args); - if ( - SupportedEVMNetworks.includes(network as Network) || - network.startsWith("eip155:") - ) { - paymentRequirements.push({ - scheme: "exact", - network, - maxAmountRequired, - resource: resourceUrl, - description: description ?? "", - mimeType: mimeType ?? "application/json", - payTo: getAddress(payTo), - maxTimeoutSeconds: maxTimeoutSeconds ?? 300, - asset: getAddress(asset.address), - // TODO: Rename outputSchema to requestStructure - outputSchema: { - input: { - type: "http", - method, - discoverable: discoverable ?? true, - ...inputSchema, - }, - output: outputSchema, - }, - extra: (asset as ERC20TokenAmount["asset"]).eip712, - }); - } else { - return { - status: 402, - responseHeaders: { - "Content-Type": "application/json", - }, - responseBody: { - x402Version, - error: `Unsupported network: ${network}`, - accepts: paymentRequirements, - }, - }; + if (decodePaymentResult.status !== 200) { + return decodePaymentResult; } - // Check for payment header - if (!paymentProof) { - return { - status: 402, - responseHeaders: { - "Content-Type": "application/json", - }, - responseBody: { - x402Version, - error: errorMessages?.paymentRequired || "X-PAYMENT header is required", - accepts: paymentRequirements, - }, - }; - } + const { selectedPaymentRequirements, decodedPayment, paymentRequirements } = + decodePaymentResult; // Verify payment - let decodedPayment: RequestedPaymentPayload; - try { - decodedPayment = decodePayment(paymentProof); - 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, - }, - }; - } - - const selectedPaymentRequirements = paymentRequirements.find( - (value) => - value.scheme === decodedPayment.scheme && - value.network === 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, - }, - }; - } - try { const verification = await facilitator.verify( decodedPayment, selectedPaymentRequirements, ); - if (!verification.isValid) { - return { - status: 402, - responseHeaders: { - "Content-Type": "application/json", - }, - responseBody: { - x402Version, - error: - errorMessages?.verificationFailed || - verification.invalidReason || - "Payment verification failed", - accepts: paymentRequirements, - payer: verification.payer, - }, - }; - } - } catch (error) { - return { - status: 402, - responseHeaders: { - "Content-Type": "application/json", - }, - responseBody: { - x402Version, - error: - errorMessages?.verificationFailed || - (error instanceof Error - ? error.message - : "Payment Verification error"), - accepts: paymentRequirements, - }, - }; - } - - // Settle payment - try { - const settlement = await facilitator.settle( - decodedPayment, - selectedPaymentRequirements, - ); - - if (settlement.success) { + if (verification.isValid) { return { status: 200, - paymentReceipt: settlement, - responseHeaders: { - "Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE", - "X-PAYMENT-RESPONSE": safeBase64Encode(stringify(settlement)), - }, + decodedPayment, + selectedPaymentRequirements, }; } else { return { @@ -382,9 +108,9 @@ export async function verifyPayment( responseBody: { x402Version, error: - errorMessages?.settlementFailed || - settlement.errorReason || - "Settlement failed", + errorMessages?.verificationFailed || + verification.invalidReason || + "Verification failed", accepts: paymentRequirements, }, }; @@ -398,72 +124,10 @@ export async function verifyPayment( responseBody: { x402Version, error: - errorMessages?.settlementFailed || - (error instanceof Error ? error.message : "Settlement error"), + errorMessages?.verificationFailed || + (error instanceof Error ? error.message : "Verification error"), accepts: paymentRequirements, }, }; } } - -/** - * Parses the amount from the given price - * - * @param price - The price to parse - * @param network - The network to get the default asset for - * @returns The parsed amount or an error message - */ -async function processPriceToAtomicAmount( - price: Money | ERC20TokenAmount, - network: FacilitatorNetwork, - facilitator: ReturnType, -): Promise< - | { maxAmountRequired: string; asset: ERC20TokenAmount["asset"] } - | { error: string } -> { - // Handle USDC amount (string) or token amount (ERC20TokenAmount) - let maxAmountRequired: string; - let asset: ERC20TokenAmount["asset"]; - - if (typeof price === "string" || typeof price === "number") { - // USDC amount in dollars - const parsedAmount = moneySchema.safeParse(price); - if (!parsedAmount.success) { - return { - error: `Invalid price (price: ${price}). Must be in the form "$3.10", 0.10, "0.001", ${parsedAmount.error}`, - }; - } - const parsedUsdAmount = parsedAmount.data; - const defaultAsset = await getDefaultAsset(network, facilitator); - if (!defaultAsset) { - return { - error: `Unable to get default asset on ${network}. Please specify an asset in the payment requirements.`, - }; - } - asset = defaultAsset; - maxAmountRequired = (parsedUsdAmount * 10 ** asset.decimals).toString(); - } else { - // Token amount in atomic units - maxAmountRequired = price.amount; - asset = price.asset; - } - - return { - maxAmountRequired, - asset, - }; -} - -async function getDefaultAsset( - network: FacilitatorNetwork, - facilitator: ReturnType, -): Promise { - const supportedAssets = await facilitator.supported(); - const chainId = networkToChainId(network); - const matchingAsset = supportedAssets.kinds.find( - (supported) => supported.network === `eip155:${chainId}`, - ); - const assetConfig = matchingAsset?.extra - ?.defaultAsset as ERC20TokenAmount["asset"]; - return assetConfig; -}