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/sad-hairs-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": minor
---

x402 utilities
1 change: 1 addition & 0 deletions apps/playground-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"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
10 changes: 10 additions & 0 deletions apps/playground-web/src/app/api/paywall/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NextResponse } from "next/server";
// Allow streaming responses up to 5 minutes
export const maxDuration = 300;

export async function GET(_req: Request) {
return NextResponse.json({
success: true,
message: "Congratulations! You have accessed the protected route.",
});
}
4 changes: 4 additions & 0 deletions apps/playground-web/src/app/navLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ const payments: ShadcnSidebarLink = {
href: "/payments/transactions",
label: "Onchain Transaction",
},
{
href: "/payments/x402",
label: "x402",
},
],
};

Expand Down
19 changes: 0 additions & 19 deletions apps/playground-web/src/app/payments/page.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";

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 {
ConnectButton,
getDefaultToken,
useActiveAccount,
useActiveWallet,
} from "thirdweb/react";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { THIRDWEB_CLIENT } from "../../../../lib/client";

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

export function X402ClientPreview() {
const activeWallet = useActiveWallet();
const activeAccount = useActiveAccount();
const paidApiCall = useMutation({
mutationFn: async () => {
if (!activeWallet) {
throw new Error("No active wallet");
}
const fetchWithPay = wrapFetchWithPayment(
fetch,
THIRDWEB_CLIENT,
activeWallet,
);
const response = await fetchWithPay("/api/paywall");
return response.json();
},
});

const handlePayClick = async () => {
paidApiCall.mutate();
};

return (
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
<ConnectButton
client={THIRDWEB_CLIENT}
chain={chain}
detailsButton={{
displayBalanceToken: {
[chain.id]: token!.address,
},
}}
supportedTokens={{
[chain.id]: [token!],
}}
/>
<Card className="p-6">
<div className="flex items-center gap-3 mb-4">
<LockIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">Paid API Call</span>
<span className="text-xl font-bold text-red-600">$0.01</span>
</div>

<Button
onClick={handlePayClick}
className="w-full mb-4"
size="lg"
disabled={paidApiCall.isPending || !activeAccount}
>
Pay Now
</Button>
<p className="text-sm text-muted-foreground">
{" "}
<a
className="underline"
href={"https://faucet.circle.com/"}
target="_blank"
rel="noopener noreferrer"
>
Click here to get USDC on {chain.name}
</a>
</p>
</Card>
<Card className="p-6">
<div className="flex items-center gap-3 mb-2">
<CodeIcon className="w-5 h-5 text-muted-foreground" />
<span className="text-lg font-medium">API Call Response</span>
</div>
{paidApiCall.isPending && <div className="text-center">Loading...</div>}
{paidApiCall.isError && (
<div className="text-center">Error: {paidApiCall.error.message}</div>
)}
{paidApiCall.data && (
<CodeClient
code={JSON.stringify(paidApiCall.data, null, 2)}
lang="json"
/>
)}
</Card>
</div>
);
}
127 changes: 127 additions & 0 deletions apps/playground-web/src/app/payments/x402/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { CodeServer } from "@workspace/ui/components/code/code.server";
import { CircleDollarSignIcon, Code2Icon } from "lucide-react";
import { CodeExample, TabName } from "@/components/code/code-example";
import ThirdwebProvider from "@/components/thirdweb-provider";
import { PageLayout } from "../../../components/blocks/APIHeader";
import { createMetadata } from "../../../lib/metadata";
import { X402ClientPreview } from "./components/x402-client-preview";

const title = "x402 Payments";
const description =
"Use the x402 payment protocol to pay for API calls using any web3 wallet.";
const ogDescription =
"Use the x402 payment protocol to pay for API calls using any web3 wallet.";

export const metadata = createMetadata({
title,
description: ogDescription,
image: {
icon: "payments",
title,
},
});

export default function Page() {
return (
<ThirdwebProvider>
<PageLayout
icon={CircleDollarSignIcon}
title={title}
description={description}
docsLink="https://portal.thirdweb.com/payments/x402?utm_source=playground"
>
<X402Example />
<div className="h-8" />
<ServerCodeExample />
</PageLayout>
</ThirdwebProvider>
);
}

function ServerCodeExample() {
return (
<>
<div className="mb-4">
<h2 className="font-semibold text-xl tracking-tight">
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.
</p>
</div>
<div className="overflow-hidden rounded-lg border bg-card">
<div className="flex grow flex-col border-b md:border-r md:border-b-0">
<TabName icon={Code2Icon} name="Server Code" />
<CodeServer
className="h-full rounded-none border-none"
code={`// src/middleware.ts

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

const client = createThirdwebClient({ secretKey: "your-secret-key" });

export const middleware = paymentMiddleware(
"0xYourWalletAddress",
{
"/api/paid-endpoint": {
price: "$0.01",
network: "base-sepolia",
config: {
description: "Access to paid content",
},
},
},
facilitator({
client,
serverWalletAddress: "0xYourServerWalletAddress",
}),
);

// Configure which paths the middleware should run on
export const config = {
matcher: ["/api/paid-endpoint"],
};

`}
lang="tsx"
/>
</div>
Comment on lines +41 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Align server example route with the actual demo endpoint.

Preview calls “/api/paywall”, but the server snippet uses “/api/paid-endpoint”. Align to avoid copy-paste errors.

Apply this diff inside the example string:

-    "/api/paid-endpoint": {
+    "/api/paywall": {
@@
-export const config = {
-  matcher: ["/api/paid-endpoint"],
-};
+export const config = {
+  matcher: ["/api/paywall"],
+};

Also confirm the paymentMiddleware signature (recipient vs server wallet) matches x402-next docs.


🏁 Script executed:

#!/bin/bash
rg -nP 'export\s+const\s+middleware\s*=\s*paymentMiddleware\(' apps/playground-web/src/middleware.ts -n -C3

Length of output: 264


Align server example route with the actual demo endpoint.

Update the snippet in apps/playground-web/src/app/payments/x402/page.tsx to use /api/paywall:

-    "/api/paid-endpoint": {
+    "/api/paywall": {
@@
-export const config = {
-  matcher: ["/api/paid-endpoint"],
-};
+export const config = {
+  matcher: ["/api/paywall"],
+};

The paymentMiddleware(payTo, routes, facilitator) signature already matches the x402-next docs.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function ServerCodeExample() {
return (
<>
<div className="mb-4">
<h2 className="font-semibold text-xl tracking-tight">
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.
</p>
</div>
<div className="overflow-hidden rounded-lg border bg-card">
<div className="flex grow flex-col border-b md:border-r md:border-b-0">
<TabName icon={Code2Icon} name="Server Code" />
<CodeServer
className="h-full rounded-none border-none"
code={`// src/middleware.ts
import { facilitator } from "thirdweb/x402";
import { createThirdwebClient } from "thirdweb";
import { paymentMiddleware } from "x402-next";
const client = createThirdwebClient({ secretKey: "your-secret-key" });
export const middleware = paymentMiddleware(
"0xYourWalletAddress",
{
"/api/paid-endpoint": {
price: "$0.01",
network: "base-sepolia",
config: {
description: "Access to paid content",
},
},
},
facilitator({
client,
serverWalletAddress: "0xYourServerWalletAddress",
}),
);
// Configure which paths the middleware should run on
export const config = {
matcher: ["/api/paid-endpoint"],
};
`}
lang="tsx"
/>
</div>
function ServerCodeExample() {
return (
<>
<div className="mb-4">
<h2 className="font-semibold text-xl tracking-tight">
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.
</p>
</div>
<div className="overflow-hidden rounded-lg border bg-card">
<div className="flex grow flex-col border-b md:border-r md:border-b-0">
<TabName icon={Code2Icon} name="Server Code" />
<CodeServer
className="h-full rounded-none border-none"
code={`// src/middleware.ts
import { facilitator } from "thirdweb/x402";
import { createThirdwebClient } from "thirdweb";
import { paymentMiddleware } from "x402-next";
const client = createThirdwebClient({ secretKey: "your-secret-key" });
export const middleware = paymentMiddleware(
"0xYourWalletAddress",
{
"/api/paywall": {
price: "$0.01",
network: "base-sepolia",
config: {
description: "Access to paid content",
},
},
},
facilitator({
client,
serverWalletAddress: "0xYourServerWalletAddress",
}),
);
// Configure which paths the middleware should run on
export const config = {
matcher: ["/api/paywall"],
};
`}
lang="tsx"
/>
</div>
🤖 Prompt for AI Agents
In apps/playground-web/src/app/payments/x402/page.tsx around lines 41 to 91, the
embedded server example uses the route "/api/paid-endpoint" but the demo expects
"/api/paywall"; update the code snippet so the routes object key and the matcher
both use "/api/paywall" (replace "/api/paid-endpoint" occurrences with
"/api/paywall") and ensure the comment or surrounding text remains consistent
with the change.

</div>
</>
);
}

function X402Example() {
return (
<CodeExample
header={{
title: "Client Code Example",
description:
"Wrap your fetch requests with the `wrapFetchWithPayment` function to enable x402 payments.",
}}
code={`import { createThirdwebClient } from "thirdweb";
import { wrapFetchWithPayment } from "thirdweb/x402";
import { useActiveWallet } from "thirdweb/react";

const client = createThirdwebClient({ clientId: "your-client-id" });

export default function Page() {
const wallet = useActiveWallet();

const onClick = async () => {
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
const response = await fetchWithPay('/api/paid-endpoint');
}

return (
<Button onClick={onClick}>Pay Now</Button>
);
}`}
lang="tsx"
preview={<X402ClientPreview />}
/>
);
}
36 changes: 36 additions & 0 deletions apps/playground-web/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createThirdwebClient } from "thirdweb";
import { facilitator } from "thirdweb/x402";
import { paymentMiddleware } from "x402-next";

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

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",
},
},
},
facilitator({
baseUrl: `${API_URL}/v1/payments/x402`,
client,
serverWalletAddress: BACKEND_WALLET_ADDRESS,
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
}),
);

// Configure which paths the middleware should run on
export const config = {
matcher: ["/api/paywall"],
};
4 changes: 4 additions & 0 deletions apps/portal/src/app/payments/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export const sidebar: SideBar = {
href: `${paymentsSlug}/custom-data`,
name: "Custom Data",
},
{
href: `${paymentsSlug}/x402`,
name: "x402",
},
],
name: "Guides",
},
Expand Down
Loading
Loading