Skip to content

Commit 61c5202

Browse files
[SDK] Add x402 payment protocol utilities
1 parent 7b8ceeb commit 61c5202

File tree

20 files changed

+1971
-166
lines changed

20 files changed

+1971
-166
lines changed

.changeset/sad-hairs-smash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
x402 utilities

apps/playground-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"thirdweb": "workspace:*",
4949
"use-debounce": "^10.0.5",
5050
"use-stick-to-bottom": "^1.1.1",
51+
"x402-next": "^0.6.1",
5152
"zod": "3.25.75"
5253
},
5354
"devDependencies": {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { NextResponse } from "next/server";
2+
// Allow streaming responses up to 5 minutes
3+
export const maxDuration = 300;
4+
5+
export async function GET(_req: Request) {
6+
return NextResponse.json({
7+
success: true,
8+
message: "Congratulations! You have accessed the protected route.",
9+
});
10+
}

apps/playground-web/src/app/navLinks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ const payments: ShadcnSidebarLink = {
206206
href: "/payments/transactions",
207207
label: "Onchain Transaction",
208208
},
209+
{
210+
href: "/payments/x402",
211+
label: "x402",
212+
},
209213
],
210214
};
211215

apps/playground-web/src/app/payments/page.tsx

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"use client";
2+
3+
import { useMutation } from "@tanstack/react-query";
4+
import { CodeClient } from "@workspace/ui/components/code/code.client";
5+
import { CodeIcon, LockIcon } from "lucide-react";
6+
import { baseSepolia } from "thirdweb/chains";
7+
import {
8+
ConnectButton,
9+
getDefaultToken,
10+
useActiveAccount,
11+
useActiveWallet,
12+
} from "thirdweb/react";
13+
import { wrapFetchWithPayment } from "thirdweb/x402";
14+
import { Button } from "@/components/ui/button";
15+
import { Card } from "@/components/ui/card";
16+
import { THIRDWEB_CLIENT } from "../../../../lib/client";
17+
18+
const chain = baseSepolia;
19+
const token = getDefaultToken(chain, "USDC");
20+
21+
export function X402ClientPreview() {
22+
const activeWallet = useActiveWallet();
23+
const activeAccount = useActiveAccount();
24+
const paidApiCall = useMutation({
25+
mutationFn: async () => {
26+
if (!activeWallet) {
27+
throw new Error("No active wallet");
28+
}
29+
const fetchWithPay = wrapFetchWithPayment(
30+
fetch,
31+
THIRDWEB_CLIENT,
32+
activeWallet,
33+
);
34+
const response = await fetchWithPay("/api/paywall");
35+
return response.json();
36+
},
37+
});
38+
39+
const handlePayClick = async () => {
40+
paidApiCall.mutate();
41+
};
42+
43+
return (
44+
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
45+
<ConnectButton
46+
client={THIRDWEB_CLIENT}
47+
chain={chain}
48+
detailsButton={{
49+
displayBalanceToken: {
50+
[chain.id]: token!.address,
51+
},
52+
}}
53+
supportedTokens={{
54+
[chain.id]: [token!],
55+
}}
56+
/>
57+
<Card className="p-6">
58+
<div className="flex items-center gap-3 mb-4">
59+
<LockIcon className="w-5 h-5 text-muted-foreground" />
60+
<span className="text-lg font-medium">Paid API Call</span>
61+
<span className="text-xl font-bold text-red-600">$0.01</span>
62+
</div>
63+
64+
<Button
65+
onClick={handlePayClick}
66+
className="w-full mb-4"
67+
size="lg"
68+
disabled={paidApiCall.isPending || !activeAccount}
69+
>
70+
Pay Now
71+
</Button>
72+
<p className="text-sm text-muted-foreground">
73+
{" "}
74+
<a
75+
className="underline"
76+
href={"https://faucet.circle.com/"}
77+
target="_blank"
78+
rel="noopener noreferrer"
79+
>
80+
Click here to get USDC on {chain.name}
81+
</a>
82+
</p>
83+
</Card>
84+
<Card className="p-6">
85+
<div className="flex items-center gap-3 mb-2">
86+
<CodeIcon className="w-5 h-5 text-muted-foreground" />
87+
<span className="text-lg font-medium">API Call Response</span>
88+
</div>
89+
{paidApiCall.isPending && <div className="text-center">Loading...</div>}
90+
{paidApiCall.isError && (
91+
<div className="text-center">Error: {paidApiCall.error.message}</div>
92+
)}
93+
{paidApiCall.data && (
94+
<CodeClient
95+
code={JSON.stringify(paidApiCall.data, null, 2)}
96+
lang="json"
97+
/>
98+
)}
99+
</Card>
100+
</div>
101+
);
102+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { CodeServer } from "@workspace/ui/components/code/code.server";
2+
import { CircleDollarSignIcon, Code2Icon } from "lucide-react";
3+
import { CodeExample, TabName } from "@/components/code/code-example";
4+
import ThirdwebProvider from "@/components/thirdweb-provider";
5+
import { PageLayout } from "../../../components/blocks/APIHeader";
6+
import { createMetadata } from "../../../lib/metadata";
7+
import { X402ClientPreview } from "./components/x402-client-preview";
8+
9+
const title = "x402 Payments";
10+
const description =
11+
"Use the x402 payment protocol to pay for API calls using any web3 wallet.";
12+
const ogDescription =
13+
"Use the x402 payment protocol to pay for API calls using any web3 wallet.";
14+
15+
export const metadata = createMetadata({
16+
title,
17+
description: ogDescription,
18+
image: {
19+
icon: "payments",
20+
title,
21+
},
22+
});
23+
24+
export default function Page() {
25+
return (
26+
<ThirdwebProvider>
27+
<PageLayout
28+
icon={CircleDollarSignIcon}
29+
title={title}
30+
description={description}
31+
docsLink="https://portal.thirdweb.com/payments/x402?utm_source=playground"
32+
>
33+
<X402Example />
34+
<div className="h-8" />
35+
<ServerCodeExample />
36+
</PageLayout>
37+
</ThirdwebProvider>
38+
);
39+
}
40+
41+
function ServerCodeExample() {
42+
return (
43+
<>
44+
<div className="mb-4">
45+
<h2 className="font-semibold text-xl tracking-tight">
46+
Next.js Server Code Example
47+
</h2>
48+
<p className="max-w-4xl text-muted-foreground text-balance text-sm md:text-base">
49+
Use any x402 middleware + the thirdweb facilitator to settle
50+
transactions with our server wallet.
51+
</p>
52+
</div>
53+
<div className="overflow-hidden rounded-lg border bg-card">
54+
<div className="flex grow flex-col border-b md:border-r md:border-b-0">
55+
<TabName icon={Code2Icon} name="Server Code" />
56+
<CodeServer
57+
className="h-full rounded-none border-none"
58+
code={`// src/middleware.ts
59+
60+
import { facilitator } from "thirdweb/x402";
61+
import { createThirdwebClient } from "thirdweb";
62+
import { paymentMiddleware } from "x402-next";
63+
64+
const client = createThirdwebClient({ secretKey: "your-secret-key" });
65+
66+
export const middleware = paymentMiddleware(
67+
"0xYourWalletAddress",
68+
{
69+
"/api/paid-endpoint": {
70+
price: "$0.01",
71+
network: "base-sepolia",
72+
config: {
73+
description: "Access to paid content",
74+
},
75+
},
76+
},
77+
facilitator({
78+
client,
79+
serverWalletAddress: "0xYourServerWalletAddress",
80+
}),
81+
);
82+
83+
// Configure which paths the middleware should run on
84+
export const config = {
85+
matcher: ["/api/paid-endpoint"],
86+
};
87+
88+
`}
89+
lang="tsx"
90+
/>
91+
</div>
92+
</div>
93+
</>
94+
);
95+
}
96+
97+
function X402Example() {
98+
return (
99+
<CodeExample
100+
header={{
101+
title: "Client Code Example",
102+
description:
103+
"Wrap your fetch requests with the `wrapFetchWithPayment` function to enable x402 payments.",
104+
}}
105+
code={`import { createThirdwebClient } from "thirdweb";
106+
import { wrapFetchWithPayment } from "thirdweb/x402";
107+
import { useActiveWallet } from "thirdweb/react";
108+
109+
const client = createThirdwebClient({ clientId: "your-client-id" });
110+
111+
export default function Page() {
112+
const wallet = useActiveWallet();
113+
114+
const onClick = async () => {
115+
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
116+
const response = await fetchWithPay('/api/paid-endpoint');
117+
}
118+
119+
return (
120+
<Button onClick={onClick}>Pay Now</Button>
121+
);
122+
}`}
123+
lang="tsx"
124+
preview={<X402ClientPreview />}
125+
/>
126+
);
127+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { createThirdwebClient } from "thirdweb";
2+
import { facilitator } from "thirdweb/x402";
3+
import { paymentMiddleware } from "x402-next";
4+
5+
const client = createThirdwebClient({
6+
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
7+
});
8+
9+
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
10+
const ENGINE_VAULT_ACCESS_TOKEN = process.env
11+
.ENGINE_VAULT_ACCESS_TOKEN as string;
12+
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
13+
14+
export const middleware = paymentMiddleware(
15+
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
16+
{
17+
"/api/paywall": {
18+
price: "$0.01",
19+
network: "base-sepolia",
20+
config: {
21+
description: "Access to paid content",
22+
},
23+
},
24+
},
25+
facilitator({
26+
baseUrl: `${API_URL}/v1/payments/x402`,
27+
client,
28+
serverWalletAddress: BACKEND_WALLET_ADDRESS,
29+
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
30+
}),
31+
);
32+
33+
// Configure which paths the middleware should run on
34+
export const config = {
35+
matcher: ["/api/paywall"],
36+
};

apps/portal/src/app/payments/sidebar.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export const sidebar: SideBar = {
6161
href: `${paymentsSlug}/custom-data`,
6262
name: "Custom Data",
6363
},
64+
{
65+
href: `${paymentsSlug}/x402`,
66+
name: "x402",
67+
},
6468
],
6569
name: "Guides",
6670
},

0 commit comments

Comments
 (0)