Skip to content

Commit 8089774

Browse files
[SDK] Add x402 payment protocol utilities
1 parent 1d90437 commit 8089774

File tree

13 files changed

+1844
-163
lines changed

13 files changed

+1844
-163
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: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
useConnectModal,
13+
} from "thirdweb/react";
14+
import { wrapFetchWithPayment } from "thirdweb/x402";
15+
import { Button } from "@/components/ui/button";
16+
import { Card } from "@/components/ui/card";
17+
import { THIRDWEB_CLIENT } from "../../../../lib/client";
18+
19+
const chain = baseSepolia;
20+
const token = getDefaultToken(chain, "USDC");
21+
22+
export function X402ClientPreview() {
23+
const activeWallet = useActiveWallet();
24+
const activeAccount = useActiveAccount();
25+
const modal = useConnectModal();
26+
const paidApiCall = useMutation({
27+
mutationFn: async () => {
28+
let wallet = activeWallet;
29+
if (!wallet) {
30+
wallet = await modal.connect({
31+
client: THIRDWEB_CLIENT,
32+
chain,
33+
});
34+
}
35+
const fetchWithPay = wrapFetchWithPayment(fetch, THIRDWEB_CLIENT, wallet);
36+
const response = await fetchWithPay("/api/paywall");
37+
return response.json();
38+
},
39+
});
40+
41+
const handlePayClick = async () => {
42+
let wallet = activeWallet;
43+
if (!wallet) {
44+
wallet = await modal.connect({
45+
client: THIRDWEB_CLIENT,
46+
chain,
47+
});
48+
}
49+
paidApiCall.mutate();
50+
};
51+
52+
return (
53+
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
54+
<ConnectButton
55+
client={THIRDWEB_CLIENT}
56+
chain={chain}
57+
detailsButton={{
58+
displayBalanceToken: {
59+
[chain.id]: token!.address,
60+
},
61+
}}
62+
supportedTokens={{
63+
[chain.id]: [token!],
64+
}}
65+
/>
66+
<Card className="p-6">
67+
<div className="flex items-center gap-3 mb-4">
68+
<LockIcon className="w-5 h-5 text-muted-foreground" />
69+
<span className="text-lg font-medium">Paid API Call</span>
70+
<span className="text-xl font-bold text-red-600">$0.01</span>
71+
</div>
72+
73+
<Button
74+
onClick={handlePayClick}
75+
className="w-full"
76+
size="lg"
77+
disabled={paidApiCall.isPending || !activeAccount}
78+
>
79+
Pay Now
80+
</Button>
81+
</Card>
82+
<Card className="p-6">
83+
<div className="flex items-center gap-3 mb-2">
84+
<CodeIcon className="w-5 h-5 text-muted-foreground" />
85+
<span className="text-lg font-medium">API Call Response</span>
86+
</div>
87+
{paidApiCall.isPending && <div className="text-center">Loading...</div>}
88+
{paidApiCall.isError && (
89+
<div className="text-center">Error: {paidApiCall.error.message}</div>
90+
)}
91+
{paidApiCall.data && (
92+
<CodeClient
93+
code={JSON.stringify(paidApiCall.data, null, 2)}
94+
lang="json"
95+
/>
96+
)}
97+
</Card>
98+
</div>
99+
);
100+
}
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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
13+
export const middleware = paymentMiddleware(
14+
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
15+
{
16+
"/api/paywall": {
17+
price: "$0.01",
18+
network: "base-sepolia",
19+
config: {
20+
description: "Access to paid content",
21+
},
22+
},
23+
},
24+
facilitator({
25+
baseUrl: "https://api.thirdweb-dev.com/v1/payments/x402",
26+
client,
27+
serverWalletAddress: BACKEND_WALLET_ADDRESS,
28+
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
29+
}),
30+
);
31+
32+
// Configure which paths the middleware should run on
33+
export const config = {
34+
matcher: ["/api/paywall"],
35+
};

packages/thirdweb/package.json

Lines changed: 6 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": [
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)