create-x402-app 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/index.js +2 -2
  2. package/package.json +2 -2
  3. package/templates/backend-express/README.md +56 -0
  4. package/templates/backend-express/env.example +8 -0
  5. package/templates/backend-express/gitignore +4 -0
  6. package/templates/backend-express/package.json +23 -0
  7. package/templates/backend-express/src/index.ts +79 -0
  8. package/templates/backend-express/tsconfig.json +18 -0
  9. package/templates/backend-hono/README.md +57 -0
  10. package/templates/backend-hono/env.example +8 -0
  11. package/templates/backend-hono/gitignore +4 -0
  12. package/templates/backend-hono/package.json +21 -0
  13. package/templates/backend-hono/src/index.ts +75 -0
  14. package/templates/backend-hono/tsconfig.json +18 -0
  15. package/templates/fullstack-express/README.md +308 -0
  16. package/templates/fullstack-express/_env.example +11 -0
  17. package/templates/fullstack-express/_gitignore +46 -0
  18. package/templates/fullstack-express/eslint.config.mjs +18 -0
  19. package/templates/fullstack-express/next-env.d.ts +6 -0
  20. package/templates/fullstack-express/next.config.ts +7 -0
  21. package/templates/fullstack-express/package.json +33 -0
  22. package/templates/fullstack-express/postcss.config.mjs +7 -0
  23. package/templates/fullstack-express/public/X402.png +0 -0
  24. package/templates/fullstack-express/src/app/api/info/route.ts +14 -0
  25. package/templates/fullstack-express/src/app/api/premium/route.ts +52 -0
  26. package/templates/fullstack-express/src/app/api/weather/route.ts +103 -0
  27. package/templates/fullstack-express/src/app/favicon.ico +0 -0
  28. package/templates/fullstack-express/src/app/globals.css +82 -0
  29. package/templates/fullstack-express/src/app/layout.tsx +42 -0
  30. package/templates/fullstack-express/src/app/page.tsx +511 -0
  31. package/templates/fullstack-express/src/components/grain-overlay.tsx +11 -0
  32. package/templates/fullstack-express/src/components/magnetic-button.tsx +90 -0
  33. package/templates/fullstack-express/src/components/ui/blur-fade.tsx +81 -0
  34. package/templates/fullstack-express/src/components/ui/magic-card.tsx +103 -0
  35. package/templates/fullstack-express/src/lib/utils.ts +6 -0
  36. package/templates/fullstack-express/src/types/ethereum.d.ts +11 -0
  37. package/templates/fullstack-express/tsconfig.json +34 -0
  38. package/templates/fullstack-hono/README.md +308 -0
  39. package/templates/fullstack-hono/_env.example +11 -0
  40. package/templates/fullstack-hono/_gitignore +46 -0
  41. package/templates/fullstack-hono/eslint.config.mjs +18 -0
  42. package/templates/fullstack-hono/next-env.d.ts +6 -0
  43. package/templates/fullstack-hono/next.config.ts +7 -0
  44. package/templates/fullstack-hono/package.json +33 -0
  45. package/templates/fullstack-hono/postcss.config.mjs +7 -0
  46. package/templates/fullstack-hono/public/X402.png +0 -0
  47. package/templates/fullstack-hono/src/app/api/info/route.ts +14 -0
  48. package/templates/fullstack-hono/src/app/api/premium/route.ts +52 -0
  49. package/templates/fullstack-hono/src/app/api/weather/route.ts +103 -0
  50. package/templates/fullstack-hono/src/app/favicon.ico +0 -0
  51. package/templates/fullstack-hono/src/app/globals.css +82 -0
  52. package/templates/fullstack-hono/src/app/layout.tsx +42 -0
  53. package/templates/fullstack-hono/src/app/page.tsx +511 -0
  54. package/templates/fullstack-hono/src/components/grain-overlay.tsx +11 -0
  55. package/templates/fullstack-hono/src/components/magnetic-button.tsx +90 -0
  56. package/templates/fullstack-hono/src/components/ui/blur-fade.tsx +81 -0
  57. package/templates/fullstack-hono/src/components/ui/magic-card.tsx +103 -0
  58. package/templates/fullstack-hono/src/lib/utils.ts +6 -0
  59. package/templates/fullstack-hono/src/types/ethereum.d.ts +11 -0
  60. package/templates/fullstack-hono/tsconfig.json +34 -0
@@ -0,0 +1,42 @@
1
+ import type React from "react"
2
+ import type { Metadata } from "next"
3
+ import { Geist, Geist_Mono } from "next/font/google"
4
+ import "./globals.css"
5
+
6
+ const geist = Geist({ subsets: ["latin"], variable: "--font-geist" })
7
+ const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-geist-mono" })
8
+
9
+ export const metadata: Metadata = {
10
+ title: "x402 Demo - HTTP 402 Payments on Mantle",
11
+ description:
12
+ "Experience HTTP 402 payments in action. A demo app built with x402-mantle-sdk for pay-per-request APIs on Mantle Network.",
13
+ keywords: [
14
+ "x402",
15
+ "HTTP 402",
16
+ "Mantle",
17
+ "Mantle Network",
18
+ "API monetization",
19
+ "blockchain payments",
20
+ "web3 payments",
21
+ "MNT",
22
+ ],
23
+ icons: {
24
+ icon: "/X402.png",
25
+ shortcut: "/X402.png",
26
+ apple: "/X402.png",
27
+ },
28
+ }
29
+
30
+ export default function RootLayout({
31
+ children,
32
+ }: Readonly<{
33
+ children: React.ReactNode
34
+ }>) {
35
+ return (
36
+ <html lang="en">
37
+ <body className={`${geist.variable} ${geistMono.variable} font-sans antialiased`}>
38
+ {children}
39
+ </body>
40
+ </html>
41
+ )
42
+ }
@@ -0,0 +1,511 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import { BrowserProvider } from "ethers"
5
+ import { PaymentModal } from "x402-mantle-sdk/client/react"
6
+ import type { PaymentRequest, PaymentResponse } from "x402-mantle-sdk/client"
7
+ import { GrainOverlay } from "@/components/grain-overlay"
8
+ import { MagneticButton } from "@/components/magnetic-button"
9
+ import { MagicCard } from "@/components/ui/magic-card"
10
+ import { BlurFade } from "@/components/ui/blur-fade"
11
+
12
+ const API_BASE = ""
13
+
14
+ interface ApiResponse {
15
+ success?: boolean
16
+ data?: Record<string, unknown>
17
+ message?: string
18
+ endpoints?: Record<string, string>
19
+ error?: string
20
+ }
21
+
22
+ export default function Home() {
23
+ const [loading, setLoading] = useState<string | null>(null)
24
+ const [response, setResponse] = useState<ApiResponse | null>(null)
25
+ const [error, setError] = useState<string | null>(null)
26
+ const [walletAddress, setWalletAddress] = useState<string | null>(null)
27
+ const [isConnecting, setIsConnecting] = useState(false)
28
+ const [provider, setProvider] = useState<BrowserProvider | null>(null)
29
+
30
+ // Payment modal state
31
+ const [showPaymentModal, setShowPaymentModal] = useState(false)
32
+ const [paymentRequest, setPaymentRequest] = useState<PaymentRequest | null>(null)
33
+ const [pendingEndpoint, setPendingEndpoint] = useState<string | null>(null)
34
+
35
+ // Check if wallet is already connected and listen for account changes
36
+ useEffect(() => {
37
+ const checkWallet = async () => {
38
+ if (typeof window !== "undefined" && window.ethereum) {
39
+ try {
40
+ const provider = new BrowserProvider(window.ethereum)
41
+ const accounts = await provider.listAccounts()
42
+ if (accounts.length > 0) {
43
+ setWalletAddress(accounts[0].address)
44
+ setProvider(provider)
45
+ }
46
+ } catch (err) {
47
+ console.error("Error checking wallet:", err)
48
+ }
49
+ }
50
+ }
51
+ checkWallet()
52
+
53
+ // Listen for account changes
54
+ if (typeof window !== "undefined" && window.ethereum) {
55
+ const handleAccountsChanged = (...args: unknown[]) => {
56
+ const accounts = args[0] as string[]
57
+ if (accounts && accounts.length > 0) {
58
+ setWalletAddress(accounts[0])
59
+ const provider = new BrowserProvider(window.ethereum!)
60
+ setProvider(provider)
61
+ } else {
62
+ setWalletAddress(null)
63
+ setProvider(null)
64
+ }
65
+ }
66
+
67
+ window.ethereum.on?.("accountsChanged", handleAccountsChanged)
68
+
69
+ return () => {
70
+ window.ethereum?.removeListener?.("accountsChanged", handleAccountsChanged)
71
+ }
72
+ }
73
+ }, [])
74
+
75
+ // Connect wallet
76
+ const connectWallet = async () => {
77
+ if (typeof window === "undefined" || !window.ethereum) {
78
+ setError("MetaMask is not installed. Please install MetaMask to continue.")
79
+ return
80
+ }
81
+
82
+ setIsConnecting(true)
83
+ setError(null)
84
+
85
+ try {
86
+ const provider = new BrowserProvider(window.ethereum)
87
+ await provider.send("eth_requestAccounts", [])
88
+ const signer = await provider.getSigner()
89
+ const address = await signer.getAddress()
90
+
91
+ setWalletAddress(address)
92
+ setProvider(provider)
93
+
94
+ // Switch to Mantle Sepolia if needed
95
+ const network = await provider.getNetwork()
96
+ const mantleSepoliaChainId = BigInt(5003)
97
+
98
+ if (network.chainId !== mantleSepoliaChainId) {
99
+ try {
100
+ await window.ethereum.request({
101
+ method: "wallet_switchEthereumChain",
102
+ params: [{ chainId: `0x${mantleSepoliaChainId.toString(16)}` }],
103
+ })
104
+ } catch (switchError: unknown) {
105
+ const err = switchError as { code?: number }
106
+ if (err.code === 4902) {
107
+ await window.ethereum.request({
108
+ method: "wallet_addEthereumChain",
109
+ params: [
110
+ {
111
+ chainId: `0x${mantleSepoliaChainId.toString(16)}`,
112
+ chainName: "Mantle Sepolia Testnet",
113
+ nativeCurrency: {
114
+ name: "MNT",
115
+ symbol: "MNT",
116
+ decimals: 18,
117
+ },
118
+ rpcUrls: ["https://rpc.sepolia.mantle.xyz"],
119
+ blockExplorerUrls: ["https://explorer.sepolia.mantle.xyz"],
120
+ },
121
+ ],
122
+ })
123
+ }
124
+ }
125
+ }
126
+ } catch (err) {
127
+ setError(err instanceof Error ? err.message : "Failed to connect wallet")
128
+ } finally {
129
+ setIsConnecting(false)
130
+ }
131
+ }
132
+
133
+ const fetchEndpoint = async (endpoint: string, name: string) => {
134
+ if (!walletAddress || !window.ethereum) {
135
+ setError("Please connect your wallet first to pay for this endpoint")
136
+ return
137
+ }
138
+
139
+ setLoading(name)
140
+ setError(null)
141
+ setResponse(null)
142
+
143
+ try {
144
+ const res = await fetch(`${API_BASE}${endpoint}`)
145
+
146
+ if (res.status === 402) {
147
+ const body = await res.json().catch(() => ({}))
148
+
149
+ const amount = res.headers.get("x-402-amount") || body.amount || "0"
150
+ const token = res.headers.get("x-402-token") || body.token || "MNT"
151
+ const network = res.headers.get("x-402-network") || body.network || "mantle-sepolia"
152
+ const recipient = res.headers.get("x-402-recipient") || body.recipient || ""
153
+ const chainId = parseInt(res.headers.get("x-402-chain-id") || String(body.chainId) || "5003")
154
+
155
+ setPaymentRequest({
156
+ amount,
157
+ token,
158
+ network,
159
+ recipient,
160
+ chainId,
161
+ })
162
+ setPendingEndpoint(endpoint)
163
+ setShowPaymentModal(true)
164
+ setLoading(null)
165
+ return
166
+ }
167
+
168
+ const data = await res.json()
169
+ setResponse(data)
170
+ } catch (err) {
171
+ setError(err instanceof Error ? err.message : "Request failed")
172
+ } finally {
173
+ setLoading(null)
174
+ }
175
+ }
176
+
177
+ const handlePaymentComplete = async (payment: PaymentResponse) => {
178
+ setShowPaymentModal(false)
179
+ setPaymentRequest(null)
180
+
181
+ if (!pendingEndpoint) return
182
+
183
+ setLoading("confirming")
184
+ await new Promise((resolve) => setTimeout(resolve, 3000))
185
+
186
+ setLoading("retrying")
187
+ let lastError = ""
188
+
189
+ for (let attempt = 1; attempt <= 3; attempt++) {
190
+ try {
191
+ const res = await fetch(`${API_BASE}${pendingEndpoint}`, {
192
+ headers: {
193
+ "X-402-Transaction-Hash": payment.transactionHash,
194
+ },
195
+ })
196
+ const data = await res.json()
197
+
198
+ if (res.ok) {
199
+ setResponse(data)
200
+ setLoading(null)
201
+ setPendingEndpoint(null)
202
+ return
203
+ }
204
+
205
+ lastError = data.error || "Verification failed"
206
+
207
+ if (attempt < 3) {
208
+ await new Promise((resolve) => setTimeout(resolve, 2000))
209
+ }
210
+ } catch (err) {
211
+ lastError = err instanceof Error ? err.message : "Request failed"
212
+ }
213
+ }
214
+
215
+ setError(`Payment verification failed after 3 attempts: ${lastError}. Transaction: ${payment.transactionHash}`)
216
+ setLoading(null)
217
+ setPendingEndpoint(null)
218
+ }
219
+
220
+ const handlePaymentCancel = () => {
221
+ setShowPaymentModal(false)
222
+ setPaymentRequest(null)
223
+ setPendingEndpoint(null)
224
+ setError("Payment cancelled")
225
+ }
226
+
227
+ const fetchFree = async (endpoint: string, name: string) => {
228
+ setLoading(name)
229
+ setError(null)
230
+ setResponse(null)
231
+
232
+ try {
233
+ const res = await fetch(`${API_BASE}${endpoint}`)
234
+ const data = await res.json()
235
+ setResponse(data)
236
+ } catch (err) {
237
+ setError(err instanceof Error ? err.message : "Request failed")
238
+ } finally {
239
+ setLoading(null)
240
+ }
241
+ }
242
+
243
+ return (
244
+ <main className="relative min-h-screen w-full overflow-hidden">
245
+ <GrainOverlay />
246
+
247
+ {/* Payment Modal from SDK */}
248
+ {paymentRequest && (
249
+ <PaymentModal
250
+ request={paymentRequest}
251
+ isOpen={showPaymentModal}
252
+ onComplete={handlePaymentComplete}
253
+ onCancel={handlePaymentCancel}
254
+ />
255
+ )}
256
+
257
+ {/* Navigation */}
258
+ <nav className="fixed left-0 right-0 top-0 z-40 flex items-center justify-between px-6 py-6 md:px-12">
259
+ <div className="flex items-center gap-2">
260
+ <div className="flex h-10 w-10 items-center justify-center overflow-hidden">
261
+ <img src="/X402.png" alt="x402" className="h-full w-full object-contain" />
262
+ </div>
263
+ <span className="font-sans text-sm font-semibold tracking-tight text-foreground">Demo</span>
264
+ </div>
265
+
266
+ <div className="flex items-center gap-3">
267
+ {walletAddress ? (
268
+ <div className="flex items-center gap-2 rounded-full border border-foreground/10 bg-foreground/5 px-4 py-2 backdrop-blur-xl">
269
+ <div className="h-2 w-2 rounded-full bg-green-500" />
270
+ <span className="font-mono text-sm text-foreground">
271
+ {walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
272
+ </span>
273
+ </div>
274
+ ) : (
275
+ <MagneticButton variant="secondary" onClick={connectWallet} disabled={isConnecting}>
276
+ {isConnecting ? "Connecting..." : "Connect Wallet"}
277
+ </MagneticButton>
278
+ )}
279
+ </div>
280
+ </nav>
281
+
282
+ {/* Hero Section */}
283
+ <section className="flex min-h-screen flex-col justify-center px-6 pt-24 md:px-12">
284
+ <div className="mx-auto max-w-6xl w-full">
285
+ <div className="grid gap-12 lg:grid-cols-2 lg:items-center">
286
+ {/* Left - Text Content */}
287
+ <div>
288
+ <BlurFade delay={0} direction="up" offset={10} blur="4px">
289
+ <div className="mb-4 inline-block w-fit rounded-full border border-foreground/20 bg-foreground/5 px-4 py-1.5 backdrop-blur-md">
290
+ <p className="font-mono text-xs text-foreground/90">HTTP 402 Demo on Mantle Sepolia</p>
291
+ </div>
292
+ </BlurFade>
293
+
294
+ <BlurFade delay={0.2} direction="up" offset={20} blur="6px">
295
+ <h1 className="mb-6 font-sans text-5xl font-light leading-[1.1] tracking-tight text-foreground md:text-6xl lg:text-7xl">
296
+ <span className="text-balance">
297
+ Pay-Per-Request
298
+ <br />
299
+ API Demo
300
+ </span>
301
+ </h1>
302
+ </BlurFade>
303
+
304
+ <BlurFade delay={0.3} direction="up" offset={15} blur="4px">
305
+ <p className="mb-8 max-w-xl text-lg leading-relaxed text-foreground/80 md:text-xl">
306
+ <span className="text-pretty">
307
+ Experience HTTP 402 payments in action. Connect your wallet, click a paid endpoint, and watch the x402 PaymentModal handle everything automatically.
308
+ </span>
309
+ </p>
310
+ </BlurFade>
311
+
312
+ <BlurFade delay={0.4} direction="up" offset={10} blur="4px">
313
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
314
+ {!walletAddress ? (
315
+ <MagneticButton size="lg" variant="primary" onClick={connectWallet} disabled={isConnecting}>
316
+ {isConnecting ? "Connecting..." : "Connect Wallet"}
317
+ </MagneticButton>
318
+ ) : (
319
+ <MagneticButton size="lg" variant="primary" onClick={() => fetchEndpoint("/api/premium", "premium")} disabled={loading !== null}>
320
+ {loading === "premium" ? "Processing..." : "Try Premium API"}
321
+ </MagneticButton>
322
+ )}
323
+ <MagneticButton size="lg" variant="secondary" onClick={() => window.open("https://mantle-x402.vercel.app/dashboard?tab=docs", "_blank")} disabled={loading !== null}>
324
+ docs
325
+ </MagneticButton>
326
+ </div>
327
+ </BlurFade>
328
+ </div>
329
+
330
+ {/* Right - Endpoint Cards */}
331
+ <div className="space-y-4">
332
+ <BlurFade delay={0.4} direction="right" offset={20} blur="8px">
333
+ {/* Free Endpoint Card */}
334
+ <MagicCard
335
+ gradientSize={300}
336
+ gradientFrom="oklch(0.5 0.15 150)"
337
+ gradientTo="oklch(0.4 0.13 150)"
338
+ gradientColor="oklch(0.5 0.15 150)"
339
+ gradientOpacity={0.15}
340
+ className="rounded-2xl mb-4"
341
+ >
342
+ <div className="rounded-2xl border border-foreground/10 bg-foreground/5 p-6 backdrop-blur-xl">
343
+ <div className="flex items-center justify-between mb-4">
344
+ <div>
345
+ <span className="inline-block rounded-full bg-green-500/10 px-3 py-1 text-xs font-medium text-green-600 mb-2">
346
+ FREE
347
+ </span>
348
+ <h3 className="font-mono text-sm text-foreground">GET /api/info</h3>
349
+ <p className="text-sm text-foreground/60">API information endpoint</p>
350
+ </div>
351
+ <MagneticButton
352
+ variant="secondary"
353
+ onClick={() => fetchFree("/api/info", "info")}
354
+ disabled={loading !== null}
355
+ >
356
+ {loading === "info" ? "..." : "Fetch"}
357
+ </MagneticButton>
358
+ </div>
359
+ </div>
360
+ </MagicCard>
361
+ </BlurFade>
362
+
363
+ <BlurFade delay={0.5} direction="right" offset={20} blur="8px">
364
+ {/* Premium Endpoint Card */}
365
+ <MagicCard
366
+ gradientSize={300}
367
+ gradientFrom="oklch(0.55 0.2 45)"
368
+ gradientTo="oklch(0.45 0.18 45)"
369
+ gradientColor="oklch(0.55 0.2 45)"
370
+ gradientOpacity={0.15}
371
+ className="rounded-2xl mb-4"
372
+ >
373
+ <div className="rounded-2xl border border-foreground/10 bg-foreground/5 p-6 backdrop-blur-xl">
374
+ <div className="flex items-center justify-between mb-4">
375
+ <div>
376
+ <span className="inline-block rounded-full bg-amber-500/10 px-3 py-1 text-xs font-medium text-amber-600 mb-2">
377
+ 0.001 MNT
378
+ </span>
379
+ <h3 className="font-mono text-sm text-foreground">GET /api/premium</h3>
380
+ <p className="text-sm text-foreground/60">Premium secret data</p>
381
+ </div>
382
+ <MagneticButton
383
+ variant="primary"
384
+ onClick={() => fetchEndpoint("/api/premium", "premium")}
385
+ disabled={loading !== null || !walletAddress}
386
+ >
387
+ {loading === "premium" ? "..." : "Pay"}
388
+ </MagneticButton>
389
+ </div>
390
+ </div>
391
+ </MagicCard>
392
+ </BlurFade>
393
+
394
+ <BlurFade delay={0.6} direction="right" offset={20} blur="8px">
395
+ {/* Weather Endpoint Card */}
396
+ <MagicCard
397
+ gradientSize={300}
398
+ gradientFrom="oklch(0.55 0.15 240)"
399
+ gradientTo="oklch(0.45 0.13 240)"
400
+ gradientColor="oklch(0.55 0.15 240)"
401
+ gradientOpacity={0.15}
402
+ className="rounded-2xl"
403
+ >
404
+ <div className="rounded-2xl border border-foreground/10 bg-foreground/5 p-6 backdrop-blur-xl">
405
+ <div className="flex items-center justify-between mb-4">
406
+ <div>
407
+ <span className="inline-block rounded-full bg-blue-500/10 px-3 py-1 text-xs font-medium text-blue-600 mb-2">
408
+ 0.0005 MNT
409
+ </span>
410
+ <h3 className="font-mono text-sm text-foreground">GET /api/weather</h3>
411
+ <p className="text-sm text-foreground/60">Weather forecast data</p>
412
+ </div>
413
+ <MagneticButton
414
+ variant="secondary"
415
+ onClick={() => fetchEndpoint("/api/weather", "weather")}
416
+ disabled={loading !== null || !walletAddress}
417
+ >
418
+ {loading === "weather" ? "..." : "Pay"}
419
+ </MagneticButton>
420
+ </div>
421
+ </div>
422
+ </MagicCard>
423
+ </BlurFade>
424
+ </div>
425
+ </div>
426
+
427
+ {/* Response Display */}
428
+ {(response || error || loading === "confirming" || loading === "retrying") && (
429
+ <BlurFade delay={0} direction="up" offset={10} blur="4px">
430
+ <div className="mt-12">
431
+ <MagicCard
432
+ gradientSize={400}
433
+ gradientFrom="oklch(0.35 0.15 240)"
434
+ gradientTo="oklch(0.3 0.13 240)"
435
+ gradientColor="oklch(0.35 0.15 240)"
436
+ gradientOpacity={0.1}
437
+ className="rounded-2xl"
438
+ >
439
+ <div className="rounded-2xl border border-foreground/10 bg-foreground/5 p-6 backdrop-blur-xl">
440
+ <h3 className="mb-4 text-sm font-medium text-foreground/60">Response</h3>
441
+ {loading === "confirming" && (
442
+ <div className="flex items-center gap-3 text-foreground/80">
443
+ <div className="h-4 w-4 animate-spin rounded-full border-2 border-foreground/20 border-t-foreground/80" />
444
+ <span>Waiting for transaction confirmation...</span>
445
+ </div>
446
+ )}
447
+ {loading === "retrying" && (
448
+ <div className="flex items-center gap-3 text-foreground/80">
449
+ <div className="h-4 w-4 animate-spin rounded-full border-2 border-foreground/20 border-t-foreground/80" />
450
+ <span>Verifying payment...</span>
451
+ </div>
452
+ )}
453
+ {error && !loading && (
454
+ <div className="rounded-lg bg-red-500/10 p-4 text-sm text-red-600">
455
+ {error}
456
+ </div>
457
+ )}
458
+ {response && !loading && (
459
+ <pre className="overflow-x-auto rounded-lg bg-foreground/5 p-4 font-mono text-sm text-foreground/90">
460
+ {JSON.stringify(response, null, 2)}
461
+ </pre>
462
+ )}
463
+ </div>
464
+ </MagicCard>
465
+ </div>
466
+ </BlurFade>
467
+ )}
468
+
469
+ {/* Instructions */}
470
+ <BlurFade delay={0.7} direction="up" offset={10} blur="4px">
471
+ <div className="mt-12 mb-12">
472
+ <MagicCard
473
+ gradientSize={300}
474
+ gradientFrom="oklch(0.4 0.1 240)"
475
+ gradientTo="oklch(0.35 0.08 240)"
476
+ gradientColor="oklch(0.4 0.1 240)"
477
+ gradientOpacity={0.1}
478
+ className="rounded-2xl"
479
+ >
480
+ <div className="rounded-2xl border border-foreground/10 bg-foreground/5 p-6 backdrop-blur-xl">
481
+ <h3 className="mb-4 text-sm font-medium text-foreground">How It Works</h3>
482
+ <div className="grid gap-4 md:grid-cols-3">
483
+ <div className="flex gap-3">
484
+ <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-foreground/10 text-xs font-medium text-foreground">1</span>
485
+ <p className="text-sm text-foreground/70">Connect MetaMask on Mantle Sepolia testnet</p>
486
+ </div>
487
+ <div className="flex gap-3">
488
+ <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-foreground/10 text-xs font-medium text-foreground">2</span>
489
+ <p className="text-sm text-foreground/70">Click a paid endpoint and approve the MNT payment</p>
490
+ </div>
491
+ <div className="flex gap-3">
492
+ <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-foreground/10 text-xs font-medium text-foreground">3</span>
493
+ <p className="text-sm text-foreground/70">After verification, the data is returned automatically</p>
494
+ </div>
495
+ </div>
496
+ </div>
497
+ </MagicCard>
498
+ </div>
499
+ </BlurFade>
500
+ </div>
501
+ </section>
502
+
503
+ {/* Footer */}
504
+ <footer className="border-t border-foreground/10 py-6">
505
+ <div className="mx-auto max-w-6xl px-6 text-center text-sm text-foreground/50">
506
+ Built with x402-mantle-sdk
507
+ </div>
508
+ </footer>
509
+ </main>
510
+ )
511
+ }
@@ -0,0 +1,11 @@
1
+ export function GrainOverlay() {
2
+ return (
3
+ <div
4
+ className="pointer-events-none fixed inset-0 z-50 opacity-[0.08]"
5
+ style={{
6
+ backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")`,
7
+ mixBlendMode: "overlay",
8
+ }}
9
+ />
10
+ )
11
+ }
@@ -0,0 +1,90 @@
1
+ "use client"
2
+
3
+ import type React from "react"
4
+ import { useRef } from "react"
5
+
6
+ interface MagneticButtonProps {
7
+ children: React.ReactNode
8
+ className?: string
9
+ variant?: "primary" | "secondary" | "ghost"
10
+ size?: "default" | "lg"
11
+ onClick?: () => void
12
+ disabled?: boolean
13
+ }
14
+
15
+ export function MagneticButton({
16
+ children,
17
+ className = "",
18
+ variant = "primary",
19
+ size = "default",
20
+ onClick,
21
+ disabled = false,
22
+ }: MagneticButtonProps) {
23
+ const ref = useRef<HTMLButtonElement>(null)
24
+ const positionRef = useRef({ x: 0, y: 0 })
25
+ const rafRef = useRef<number>()
26
+
27
+ const handleMouseMove = (e: React.MouseEvent<HTMLButtonElement>) => {
28
+ if (!ref.current || disabled) return
29
+
30
+ const rect = ref.current.getBoundingClientRect()
31
+ const x = e.clientX - rect.left - rect.width / 2
32
+ const y = e.clientY - rect.top - rect.height / 2
33
+
34
+ positionRef.current = { x: x * 0.15, y: y * 0.15 }
35
+
36
+ if (rafRef.current) cancelAnimationFrame(rafRef.current)
37
+ rafRef.current = requestAnimationFrame(() => {
38
+ if (ref.current) {
39
+ ref.current.style.transform = `translate3d(${positionRef.current.x}px, ${positionRef.current.y}px, 0)`
40
+ }
41
+ })
42
+ }
43
+
44
+ const handleMouseLeave = () => {
45
+ positionRef.current = { x: 0, y: 0 }
46
+ if (rafRef.current) cancelAnimationFrame(rafRef.current)
47
+ rafRef.current = requestAnimationFrame(() => {
48
+ if (ref.current) {
49
+ ref.current.style.transform = "translate3d(0px, 0px, 0)"
50
+ }
51
+ })
52
+ }
53
+
54
+ const variants = {
55
+ primary:
56
+ "bg-foreground/95 text-background hover:bg-foreground backdrop-blur-md",
57
+ secondary:
58
+ "bg-foreground/5 text-foreground hover:bg-foreground/10 backdrop-blur-xl border border-foreground/10 hover:border-foreground/20",
59
+ ghost: "bg-transparent text-foreground hover:bg-foreground/5 backdrop-blur-sm",
60
+ }
61
+
62
+ const sizes = {
63
+ default: "px-6 py-2.5 text-sm",
64
+ lg: "px-8 py-3.5 text-base",
65
+ }
66
+
67
+ return (
68
+ <button
69
+ ref={ref}
70
+ onClick={onClick}
71
+ disabled={disabled}
72
+ onMouseMove={handleMouseMove}
73
+ onMouseLeave={handleMouseLeave}
74
+ className={`
75
+ relative overflow-hidden rounded-full font-medium
76
+ transition-all duration-300 ease-out will-change-transform
77
+ disabled:opacity-50 disabled:cursor-not-allowed
78
+ ${variants[variant]}
79
+ ${sizes[size]}
80
+ ${className}
81
+ `}
82
+ style={{
83
+ transform: "translate3d(0px, 0px, 0)",
84
+ contain: "layout style paint",
85
+ }}
86
+ >
87
+ <span className="relative z-10">{children}</span>
88
+ </button>
89
+ )
90
+ }