Skip to content

Commit e0e2b4e

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

File tree

17 files changed

+1894
-166
lines changed

17 files changed

+1894
-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: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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+
The server code is responsible for handling the chat requests and
50+
streaming the responses to the client.
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/app/api/chat/route.ts
59+
60+
import { convertToModelMessages, streamText } from "ai";
61+
import { createThirdwebAI } from "@thirdweb-dev/ai-sdk-provider";
62+
63+
// Allow streaming responses up to 5 minutes
64+
export const maxDuration = 300;
65+
66+
const thirdwebAI = createThirdwebAI({
67+
secretKey: process.env.THIRDWEB_SECRET_KEY,
68+
});
69+
70+
export async function POST(req: Request) {
71+
const { messages, id } = await req.json();
72+
const result = streamText({
73+
model: thirdwebAI.chat(id, {
74+
context: {
75+
chain_ids: [8453], // optional chain ids
76+
from: "0x...", // optional from address
77+
auto_execute_transactions: true, // optional, defaults to false
78+
},
79+
}),
80+
messages: convertToModelMessages(messages),
81+
tools: thirdwebAI.tools(), // optional, to use handle transactions and swaps
82+
});
83+
84+
return result.toUIMessageStreamResponse({
85+
sendReasoning: true, // optional, to send reasoning steps to the client
86+
});
87+
}
88+
89+
`}
90+
lang="tsx"
91+
/>
92+
</div>
93+
</div>
94+
</>
95+
);
96+
}
97+
98+
function X402Example() {
99+
return (
100+
<CodeExample
101+
header={{
102+
title: "Client Code Example",
103+
description:
104+
"Wrap your fetch requests with the `wrapFetchWithPayment` function to enable x402 payments.",
105+
}}
106+
code={`'use client';
107+
108+
import { useChat } from '@ai-sdk/react';
109+
import { DefaultChatTransport } from 'ai';
110+
import { useState } from 'react';
111+
import { ThirdwebAiMessage } from '@thirdweb-dev/ai-sdk-provider';
112+
113+
export default function Page() {
114+
const { messages, sendMessage } = useChat<ThirdwebAiMessage>({
115+
transport: new DefaultChatTransport({
116+
// see server implementation below
117+
api: '/api/chat',
118+
}),
119+
});
120+
121+
return (
122+
<>
123+
{messages.map(message => (
124+
<RenderMessage message={message} />
125+
))}
126+
<ChatInputBox send={sendMessage} />
127+
</>
128+
);
129+
}`}
130+
lang="tsx"
131+
preview={<X402ClientPreview />}
132+
/>
133+
);
134+
}
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+
};

packages/thirdweb/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"toml": "3.0.0",
4141
"uqr": "0.1.2",
4242
"viem": "2.33.2",
43+
"x402": "0.6.1",
4344
"zod": "3.25.75"
4445
},
4546
"devDependencies": {
@@ -226,6 +227,11 @@
226227
"react-native": "./dist/esm/exports/wallets/in-app.native.js",
227228
"import": "./dist/esm/exports/wallets/in-app.js",
228229
"default": "./dist/cjs/exports/wallets/in-app.js"
230+
},
231+
"./x402": {
232+
"types": "./dist/types/exports/x402.d.ts",
233+
"import": "./dist/esm/exports/x402.js",
234+
"default": "./dist/cjs/exports/x402.js"
229235
}
230236
},
231237
"files": [
@@ -414,6 +420,9 @@
414420
],
415421
"insight": [
416422
"./dist/types/exports/insight.d.ts"
423+
],
424+
"x402": [
425+
"./dist/types/exports/x402.d.ts"
417426
]
418427
}
419428
},
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export {
2+
facilitator,
3+
type ThirdwebX402FacilitatorConfig,
4+
} from "../x402/facilitator.js";
5+
export { wrapFetchWithPayment } from "../x402/fetchWithPayment.js";

0 commit comments

Comments
 (0)