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.
- package/dist/index.js +2 -2
- package/package.json +2 -2
- package/templates/backend-express/README.md +56 -0
- package/templates/backend-express/env.example +8 -0
- package/templates/backend-express/gitignore +4 -0
- package/templates/backend-express/package.json +23 -0
- package/templates/backend-express/src/index.ts +79 -0
- package/templates/backend-express/tsconfig.json +18 -0
- package/templates/backend-hono/README.md +57 -0
- package/templates/backend-hono/env.example +8 -0
- package/templates/backend-hono/gitignore +4 -0
- package/templates/backend-hono/package.json +21 -0
- package/templates/backend-hono/src/index.ts +75 -0
- package/templates/backend-hono/tsconfig.json +18 -0
- package/templates/fullstack-express/README.md +308 -0
- package/templates/fullstack-express/_env.example +11 -0
- package/templates/fullstack-express/_gitignore +46 -0
- package/templates/fullstack-express/eslint.config.mjs +18 -0
- package/templates/fullstack-express/next-env.d.ts +6 -0
- package/templates/fullstack-express/next.config.ts +7 -0
- package/templates/fullstack-express/package.json +33 -0
- package/templates/fullstack-express/postcss.config.mjs +7 -0
- package/templates/fullstack-express/public/X402.png +0 -0
- package/templates/fullstack-express/src/app/api/info/route.ts +14 -0
- package/templates/fullstack-express/src/app/api/premium/route.ts +52 -0
- package/templates/fullstack-express/src/app/api/weather/route.ts +103 -0
- package/templates/fullstack-express/src/app/favicon.ico +0 -0
- package/templates/fullstack-express/src/app/globals.css +82 -0
- package/templates/fullstack-express/src/app/layout.tsx +42 -0
- package/templates/fullstack-express/src/app/page.tsx +511 -0
- package/templates/fullstack-express/src/components/grain-overlay.tsx +11 -0
- package/templates/fullstack-express/src/components/magnetic-button.tsx +90 -0
- package/templates/fullstack-express/src/components/ui/blur-fade.tsx +81 -0
- package/templates/fullstack-express/src/components/ui/magic-card.tsx +103 -0
- package/templates/fullstack-express/src/lib/utils.ts +6 -0
- package/templates/fullstack-express/src/types/ethereum.d.ts +11 -0
- package/templates/fullstack-express/tsconfig.json +34 -0
- package/templates/fullstack-hono/README.md +308 -0
- package/templates/fullstack-hono/_env.example +11 -0
- package/templates/fullstack-hono/_gitignore +46 -0
- package/templates/fullstack-hono/eslint.config.mjs +18 -0
- package/templates/fullstack-hono/next-env.d.ts +6 -0
- package/templates/fullstack-hono/next.config.ts +7 -0
- package/templates/fullstack-hono/package.json +33 -0
- package/templates/fullstack-hono/postcss.config.mjs +7 -0
- package/templates/fullstack-hono/public/X402.png +0 -0
- package/templates/fullstack-hono/src/app/api/info/route.ts +14 -0
- package/templates/fullstack-hono/src/app/api/premium/route.ts +52 -0
- package/templates/fullstack-hono/src/app/api/weather/route.ts +103 -0
- package/templates/fullstack-hono/src/app/favicon.ico +0 -0
- package/templates/fullstack-hono/src/app/globals.css +82 -0
- package/templates/fullstack-hono/src/app/layout.tsx +42 -0
- package/templates/fullstack-hono/src/app/page.tsx +511 -0
- package/templates/fullstack-hono/src/components/grain-overlay.tsx +11 -0
- package/templates/fullstack-hono/src/components/magnetic-button.tsx +90 -0
- package/templates/fullstack-hono/src/components/ui/blur-fade.tsx +81 -0
- package/templates/fullstack-hono/src/components/ui/magic-card.tsx +103 -0
- package/templates/fullstack-hono/src/lib/utils.ts +6 -0
- package/templates/fullstack-hono/src/types/ethereum.d.ts +11 -0
- 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
|
+
}
|