Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/some-moons-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": minor
---

Accept arbitrary chain ids for x402 payments with new verifyPayment() backend utility
1 change: 0 additions & 1 deletion apps/playground-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"thirdweb": "workspace:*",
"use-debounce": "^10.0.5",
"use-stick-to-bottom": "^1.1.1",
"x402-next": "^0.6.1",
"zod": "3.25.75"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useMutation } from "@tanstack/react-query";
import { CodeClient } from "@workspace/ui/components/code/code.client";
import { CodeIcon, LockIcon } from "lucide-react";
import { baseSepolia } from "thirdweb/chains";
import { arbitrumSepolia } from "thirdweb/chains";
import {
ConnectButton,
getDefaultToken,
Expand All @@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { THIRDWEB_CLIENT } from "../../../../lib/client";

const chain = baseSepolia;
const chain = arbitrumSepolia;
const token = getDefaultToken(chain, "USDC");

export function X402ClientPreview() {
Expand Down
53 changes: 35 additions & 18 deletions apps/playground-web/src/app/payments/x402/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ function ServerCodeExample() {
Next.js Server Code Example
</h2>
<p className="max-w-4xl text-muted-foreground text-balance text-sm md:text-base">
Use any x402 middleware + the thirdweb facilitator to settle
transactions with our server wallet.
Create middleware with the thirdweb facilitator to settle transactions
with your server wallet.
</p>
</div>
<div className="overflow-hidden rounded-lg border bg-card">
Expand All @@ -57,28 +57,45 @@ function ServerCodeExample() {
className="h-full rounded-none border-none"
code={`// src/middleware.ts

import { facilitator } from "thirdweb/x402";
import { facilitator, verifyPayment } from "thirdweb/x402";
import { createThirdwebClient } from "thirdweb";
import { paymentMiddleware } from "x402-next";

const client = createThirdwebClient({ secretKey: "your-secret-key" });
const thirdwebX402Facilitator = facilitator({
client,
serverWalletAddress: "0xYourWalletAddress",
});

export async function middleware(request: NextRequest) {
const method = request.method.toUpperCase();
const resourceUrl = request.nextUrl.toString();
const paymentData = request.headers.get("X-PAYMENT");

export const middleware = paymentMiddleware(
"0xYourWalletAddress",
{
"/api/paid-endpoint": {
price: "$0.01",
network: "base-sepolia",
config: {
description: "Access to paid content",
},
const result = await verifyPayment({
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({
client,
serverWalletAddress: "0xYourServerWalletAddress",
}),
);
facilitator: thirdwebX402Facilitator,
});

if (result.status === 200) {
// payment successful, execute the request
return NextResponse.next();
}

// otherwise, request payment
return NextResponse.json(result.responseBody, {
status: result.status,
headers: result.responseHeaders,
});
}

// Configure which paths the middleware should run on
export const config = {
Expand Down
63 changes: 44 additions & 19 deletions apps/playground-web/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,59 @@
import { type NextRequest, NextResponse } from "next/server";
import { createThirdwebClient } from "thirdweb";
import { facilitator } from "thirdweb/x402";
import { paymentMiddleware } from "x402-next";
import { arbitrumSepolia } from "thirdweb/chains";
import { facilitator, verifyPayment } from "thirdweb/x402";

const client = createThirdwebClient({
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
});

const chain = arbitrumSepolia;
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
const ENGINE_VAULT_ACCESS_TOKEN = process.env
.ENGINE_VAULT_ACCESS_TOKEN as string;
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;

export const middleware = paymentMiddleware(
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
{
"/api/paywall": {
price: "$0.01",
network: "base-sepolia",
config: {
description: "Access to paid content",
},
const twFacilitator = facilitator({
baseUrl: `${API_URL}/v1/payments/x402`,
client,
serverWalletAddress: BACKEND_WALLET_ADDRESS,
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
});

export async function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const method = request.method.toUpperCase();
const resourceUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}${pathname}`;
const paymentData = request.headers.get("X-PAYMENT");

const result = await verifyPayment({
resourceUrl,
method,
paymentData,
payTo: "0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
network: `eip155:${chain.id}`,
price: "$0.01",
routeConfig: {
description: "Access to paid content",
},
},
facilitator({
baseUrl: `${API_URL}/v1/payments/x402`,
client,
serverWalletAddress: BACKEND_WALLET_ADDRESS,
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
}),
);
facilitator: twFacilitator,
});

if (result.status === 200) {
// payment successful, execute the request
const response = NextResponse.next();
for (const [key, value] of Object.entries(result.responseHeaders)) {
response.headers.set(key, value);
}
return response;
}

// otherwise, request payment
return NextResponse.json(result.responseBody, {
status: result.status,
headers: result.responseHeaders,
});
}

// Configure which paths the middleware should run on
export const config = {
Expand Down
62 changes: 41 additions & 21 deletions apps/portal/src/app/payments/x402/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,37 +41,57 @@ const response = await fetchWithPay('https://api.example.com/paid-endpoint');

## Server Side

To make your API calls payable, you can use any x402 middleware library like x402-hono, x402-next, x402-express, etc.
To make your API calls payable, you can use the `verifyPayment` function in a simple middleware or in your endpoint directly.

Then, use the `facilitator` configuratino function settle transactions with your thirdweb server wallet gaslessly and pass it to the middleware.
Use the `facilitator` configuration function settle transactions with your thirdweb server wallet gaslessly and pass it to the `verifyPayment` function.

Here's an example with Next.js:
Here's an example with a Next.js middleware:

```typescript
import { createThirdwebClient } from "thirdweb";
import { facilitator } from "thirdweb/x402";
import { facilitator, verifyPayment } from "thirdweb/x402";
import { paymentMiddleware } from "x402-next";

const client = createThirdwebClient({
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
const client = createThirdwebClient({ secretKey: "your-secret-key" });
const thirdwebX402Facilitator = facilitator({
client,
serverWalletAddress: "0xYourWalletAddress",
});

export const middleware = paymentMiddleware(
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
{
"/api/paid-endpoint": {
price: "$0.01",
network: "base-sepolia",
config: {
description: "Access to paid content",
},
export async function middleware(request: NextRequest) {
const method = request.method.toUpperCase();
const resourceUrl = request.nextUrl.toString();
const paymentData = request.headers.get("X-PAYMENT");

const result = await verifyPayment({
resourceUrl,
method,
paymentData,
payTo: "0xYourWalletAddress",
network: "eip155:1", // or any other chain id in CAIP2 format: "eip155:<chain_id>"
price: "$0.01", // can also be a ERC20 token amount
routeConfig: {
description: "Access to paid content",
},
},
facilitator({
client,
serverWalletAddress: "0x1234567890123456789012345678901234567890",
}),
);
facilitator: thirdwebX402Facilitator,
});

if (result.status === 200) {
// payment successful, execute the request
const response = NextResponse.next();
// optionally set the response headers back to the client
for (const [key, value] of Object.entries(result.responseHeaders)) {
response.headers.set(key, value);
}
return response;
}

// otherwise, request payment
return NextResponse.json(result.responseBody, {
status: result.status,
headers: result.responseHeaders,
});
}

// Configure which paths the middleware should run on
export const config = {
Expand Down
6 changes: 6 additions & 0 deletions packages/thirdweb/src/exports/x402.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export { decodePayment, encodePayment } from "../x402/encode.js";
export {
facilitator,
type ThirdwebX402FacilitatorConfig,
} from "../x402/facilitator.js";
export { wrapFetchWithPayment } from "../x402/fetchWithPayment.js";
export {
type VerifyPaymentArgs,
type VerifyPaymentResult,
verifyPayment,
} from "../x402/verify-payment.js";
10 changes: 2 additions & 8 deletions packages/thirdweb/src/react/core/utils/defaultTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,15 +289,9 @@ const DEFAULT_TOKENS = {
symbol: "USDC",
},
],
"421613": [
"421614": [
{
address: "0xe39Ab88f8A4777030A534146A9Ca3B52bd5D43A3",
icon: wrappedEthIcon,
name: "Wrapped Ether",
symbol: "WETH",
},
{
address: "0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63",
address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
icon: usdcIcon,
name: "USD Coin",
symbol: "USDC",
Expand Down
81 changes: 81 additions & 0 deletions packages/thirdweb/src/x402/encode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { ExactEvmPayload } from "x402/types";
import {
type RequestedPaymentPayload,
RequestedPaymentPayloadSchema,
} from "./schemas.js";

/**
* Encodes a payment payload into a base64 string, ensuring bigint values are properly stringified
*
* @param payment - The payment payload to encode
* @returns A base64 encoded string representation of the payment payload
*/
export function encodePayment(payment: RequestedPaymentPayload): string {
let safe: RequestedPaymentPayload;

// evm
const evmPayload = payment.payload as ExactEvmPayload;
safe = {
...payment,
payload: {
...evmPayload,
authorization: Object.fromEntries(
Object.entries(evmPayload.authorization).map(([key, value]) => [
key,
typeof value === "bigint" ? (value as bigint).toString() : value,
]),
) as ExactEvmPayload["authorization"],
},
};
return safeBase64Encode(JSON.stringify(safe));
}

/**
* Decodes a base64 encoded payment string back into a PaymentPayload object
*
* @param payment - The base64 encoded payment string to decode
* @returns The decoded and validated PaymentPayload object
*/
export function decodePayment(payment: string): RequestedPaymentPayload {
const decoded = safeBase64Decode(payment);
const parsed = JSON.parse(decoded);

const obj: RequestedPaymentPayload = {
...parsed,
payload: parsed.payload as ExactEvmPayload,
};
const validated = RequestedPaymentPayloadSchema.parse(obj);
return validated;
}

/**
* Encodes a string to base64 format
*
* @param data - The string to be encoded to base64
* @returns The base64 encoded string
*/
export function safeBase64Encode(data: string): string {
if (
typeof globalThis !== "undefined" &&
typeof globalThis.btoa === "function"
) {
return globalThis.btoa(data);
}
return Buffer.from(data).toString("base64");
}

/**
* Decodes a base64 string back to its original format
*
* @param data - The base64 encoded string to be decoded
* @returns The decoded string in UTF-8 format
*/
function safeBase64Decode(data: string): string {
if (
typeof globalThis !== "undefined" &&
typeof globalThis.atob === "function"
) {
return globalThis.atob(data);
}
return Buffer.from(data, "base64").toString("utf-8");
}
Loading
Loading