create-x402-app 0.1.2 → 0.1.4
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 +54 -4
- 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,308 @@
|
|
|
1
|
+
# x402 Starter Template
|
|
2
|
+
|
|
3
|
+
A production-ready Next.js boilerplate for building **pay-per-request APIs** using the HTTP 402 protocol on Mantle Network.
|
|
4
|
+
|
|
5
|
+
## What is x402?
|
|
6
|
+
|
|
7
|
+
x402 implements the [HTTP 402 Payment Required](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402) status code to enable native payments for API access. When a client requests a paid endpoint without payment, the server returns a 402 response with payment details. After payment, the client retries with the transaction hash to access the content.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### 1. Clone and Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/your-repo/x402-starter.git
|
|
15
|
+
cd x402-starter
|
|
16
|
+
npm install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Get Your App ID
|
|
20
|
+
|
|
21
|
+
1. Visit the [x402 Dashboard](https://mantle-x402.vercel.app) or [Documentation](https://mantle-x402.vercel.app/dashboard?tab=docs)
|
|
22
|
+
2. Connect your wallet
|
|
23
|
+
3. Create a new project
|
|
24
|
+
4. Copy your **App ID**
|
|
25
|
+
|
|
26
|
+
### 3. Configure Environment
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cp .env.example .env
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Edit `.env`:
|
|
33
|
+
|
|
34
|
+
```env
|
|
35
|
+
X402_APP_ID=your_app_id_here
|
|
36
|
+
X402_PLATFORM_URL=https://mantle-x402.vercel.app
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 4. Run Development Server
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm run dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Open [http://localhost:3000](http://localhost:3000)
|
|
46
|
+
|
|
47
|
+
## Project Structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
x402-starter/
|
|
51
|
+
├── src/
|
|
52
|
+
│ ├── app/
|
|
53
|
+
│ │ ├── api/
|
|
54
|
+
│ │ │ ├── info/route.ts # Free endpoint (no payment)
|
|
55
|
+
│ │ │ ├── premium/route.ts # Paid endpoint (0.001 MNT)
|
|
56
|
+
│ │ │ └── weather/route.ts # Paid endpoint (0.0005 MNT)
|
|
57
|
+
│ │ ├── page.tsx # Demo frontend
|
|
58
|
+
│ │ └── layout.tsx # Root layout
|
|
59
|
+
│ ├── components/ # UI components
|
|
60
|
+
│ └── lib/ # Utilities
|
|
61
|
+
├── .env.example # Environment template
|
|
62
|
+
└── package.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API Endpoints
|
|
66
|
+
|
|
67
|
+
| Endpoint | Price | Description |
|
|
68
|
+
|----------|-------|-------------|
|
|
69
|
+
| `GET /api/info` | Free | API information |
|
|
70
|
+
| `GET /api/premium` | 0.001 MNT | Premium content |
|
|
71
|
+
| `GET /api/weather` | 0.0005 MNT | Weather data |
|
|
72
|
+
|
|
73
|
+
## Creating Paid Endpoints
|
|
74
|
+
|
|
75
|
+
### Basic Example
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// src/app/api/my-endpoint/route.ts
|
|
79
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
80
|
+
import { processPaymentMiddleware, initializePlatform } from 'x402-mantle-sdk/server'
|
|
81
|
+
|
|
82
|
+
let initialized = false
|
|
83
|
+
|
|
84
|
+
export async function GET(request: NextRequest) {
|
|
85
|
+
// Initialize once
|
|
86
|
+
if (!initialized) {
|
|
87
|
+
await initializePlatform()
|
|
88
|
+
initialized = true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Convert headers
|
|
92
|
+
const headers: Record<string, string> = {}
|
|
93
|
+
request.headers.forEach((value, key) => {
|
|
94
|
+
headers[key] = value
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Check payment
|
|
98
|
+
const result = await processPaymentMiddleware(
|
|
99
|
+
{ price: '0.01', token: 'MNT', testnet: true },
|
|
100
|
+
headers
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Return 402 if payment required
|
|
104
|
+
if (result.paymentRequired) {
|
|
105
|
+
const response = NextResponse.json(result.paymentRequired.body, { status: 402 })
|
|
106
|
+
Object.entries(result.paymentRequired.headers).forEach(([key, value]) => {
|
|
107
|
+
response.headers.set(key, value)
|
|
108
|
+
})
|
|
109
|
+
return response
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Return content if payment verified
|
|
113
|
+
if (result.allowed) {
|
|
114
|
+
return NextResponse.json({
|
|
115
|
+
success: true,
|
|
116
|
+
data: 'Your premium content here'
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return NextResponse.json({ error: 'Error' }, { status: 500 })
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Payment Options
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const options = {
|
|
128
|
+
price: '0.001', // Amount to charge
|
|
129
|
+
token: 'MNT', // Token (MNT, USDC, USDT)
|
|
130
|
+
testnet: true, // Use Mantle Sepolia (false for mainnet)
|
|
131
|
+
network: 'mantle-sepolia' // Or 'mantle' for mainnet
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Client-Side Integration
|
|
136
|
+
|
|
137
|
+
### Using the PaymentModal Component
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { PaymentModal } from 'x402-mantle-sdk/client/react'
|
|
141
|
+
|
|
142
|
+
function MyComponent() {
|
|
143
|
+
const [showModal, setShowModal] = useState(false)
|
|
144
|
+
const [paymentRequest, setPaymentRequest] = useState(null)
|
|
145
|
+
|
|
146
|
+
const fetchPaidEndpoint = async () => {
|
|
147
|
+
const res = await fetch('/api/premium')
|
|
148
|
+
|
|
149
|
+
if (res.status === 402) {
|
|
150
|
+
// Extract payment details from response
|
|
151
|
+
const body = await res.json()
|
|
152
|
+
setPaymentRequest({
|
|
153
|
+
amount: body.amount,
|
|
154
|
+
token: body.token,
|
|
155
|
+
network: body.network,
|
|
156
|
+
recipient: body.recipient,
|
|
157
|
+
chainId: body.chainId,
|
|
158
|
+
})
|
|
159
|
+
setShowModal(true)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const handlePaymentComplete = async (payment) => {
|
|
164
|
+
// Retry with transaction hash
|
|
165
|
+
const res = await fetch('/api/premium', {
|
|
166
|
+
headers: {
|
|
167
|
+
'X-402-Transaction-Hash': payment.transactionHash
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
const data = await res.json()
|
|
171
|
+
console.log('Premium data:', data)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<>
|
|
176
|
+
<button onClick={fetchPaidEndpoint}>Get Premium Data</button>
|
|
177
|
+
|
|
178
|
+
{paymentRequest && (
|
|
179
|
+
<PaymentModal
|
|
180
|
+
request={paymentRequest}
|
|
181
|
+
isOpen={showModal}
|
|
182
|
+
onComplete={handlePaymentComplete}
|
|
183
|
+
onCancel={() => setShowModal(false)}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
</>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Using x402Fetch (Auto-handles payments)
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { x402Fetch } from 'x402-mantle-sdk/client'
|
|
195
|
+
|
|
196
|
+
// Automatically shows payment modal when 402 is returned
|
|
197
|
+
const response = await x402Fetch('/api/premium')
|
|
198
|
+
const data = await response.json()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## HTTP 402 Protocol
|
|
202
|
+
|
|
203
|
+
### Request Flow
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
1. Client requests paid endpoint
|
|
207
|
+
GET /api/premium
|
|
208
|
+
|
|
209
|
+
2. Server returns 402 with payment details
|
|
210
|
+
HTTP/1.1 402 Payment Required
|
|
211
|
+
X-402-Amount: 0.001
|
|
212
|
+
X-402-Token: MNT
|
|
213
|
+
X-402-Network: mantle-sepolia
|
|
214
|
+
X-402-Recipient: 0x...
|
|
215
|
+
|
|
216
|
+
3. Client makes payment on-chain
|
|
217
|
+
|
|
218
|
+
4. Client retries with transaction hash
|
|
219
|
+
GET /api/premium
|
|
220
|
+
X-402-Transaction-Hash: 0x...
|
|
221
|
+
|
|
222
|
+
5. Server verifies payment and returns content
|
|
223
|
+
HTTP/1.1 200 OK
|
|
224
|
+
{ "data": "Premium content" }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Supported Networks
|
|
228
|
+
|
|
229
|
+
| Network | Chain ID | Environment |
|
|
230
|
+
|---------|----------|-------------|
|
|
231
|
+
| Mantle | 5000 | Production |
|
|
232
|
+
| Mantle Sepolia | 5003 | Testnet |
|
|
233
|
+
|
|
234
|
+
## Supported Tokens
|
|
235
|
+
|
|
236
|
+
| Token | Mainnet | Testnet |
|
|
237
|
+
|-------|---------|---------|
|
|
238
|
+
| MNT | Native | Native |
|
|
239
|
+
| USDC | 0x09Bc4E... | 0x09Bc4E... |
|
|
240
|
+
| USDT | 0x201EBa... | - |
|
|
241
|
+
|
|
242
|
+
## Testing
|
|
243
|
+
|
|
244
|
+
1. Install [MetaMask](https://metamask.io/)
|
|
245
|
+
2. Add Mantle Sepolia network:
|
|
246
|
+
- RPC: `https://rpc.sepolia.mantle.xyz`
|
|
247
|
+
- Chain ID: `5003`
|
|
248
|
+
- Symbol: `MNT`
|
|
249
|
+
3. Get test MNT from [Mantle Faucet](https://faucet.sepolia.mantle.xyz/)
|
|
250
|
+
4. Connect wallet and try paid endpoints
|
|
251
|
+
|
|
252
|
+
## Deployment
|
|
253
|
+
|
|
254
|
+
### Vercel (Recommended)
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
npm run build
|
|
258
|
+
vercel deploy
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Set environment variables in Vercel dashboard:
|
|
262
|
+
- `X402_APP_ID`
|
|
263
|
+
- `X402_PLATFORM_URL`
|
|
264
|
+
|
|
265
|
+
### Other Platforms
|
|
266
|
+
|
|
267
|
+
Works with any Node.js hosting:
|
|
268
|
+
- Railway
|
|
269
|
+
- Render
|
|
270
|
+
- AWS Lambda
|
|
271
|
+
- Google Cloud Run
|
|
272
|
+
|
|
273
|
+
## Configuration
|
|
274
|
+
|
|
275
|
+
### Environment Variables
|
|
276
|
+
|
|
277
|
+
| Variable | Required | Description |
|
|
278
|
+
|----------|----------|-------------|
|
|
279
|
+
| `X402_APP_ID` | Yes | Your project App ID from the dashboard |
|
|
280
|
+
| `X402_PLATFORM_URL` | Yes | Platform URL (default: https://mantle-x402.vercel.app) |
|
|
281
|
+
|
|
282
|
+
## Troubleshooting
|
|
283
|
+
|
|
284
|
+
### "X402_APP_ID is required"
|
|
285
|
+
|
|
286
|
+
Make sure your `.env` file exists and contains a valid App ID.
|
|
287
|
+
|
|
288
|
+
### Payment not verifying
|
|
289
|
+
|
|
290
|
+
1. Wait for transaction confirmation (~3 seconds)
|
|
291
|
+
2. Check that the transaction was successful on block explorer
|
|
292
|
+
3. Ensure the amount matches exactly
|
|
293
|
+
|
|
294
|
+
### Network mismatch
|
|
295
|
+
|
|
296
|
+
Make sure your wallet is on the same network as configured in the endpoint (testnet vs mainnet).
|
|
297
|
+
|
|
298
|
+
## Resources
|
|
299
|
+
|
|
300
|
+
- [x402 Documentation](https://mantle-x402.vercel.app/dashboard?tab=docs)
|
|
301
|
+
- [x402 Dashboard](https://mantle-x402.vercel.app)
|
|
302
|
+
- [x402-mantle-sdk on npm](https://www.npmjs.com/package/x402-mantle-sdk)
|
|
303
|
+
- [Mantle Network](https://www.mantle.xyz/)
|
|
304
|
+
- [Mantle Faucet](https://faucet.sepolia.mantle.xyz/)
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# x402 Configuration
|
|
2
|
+
# Get your App ID from: https://mantle-x402.vercel.app
|
|
3
|
+
#
|
|
4
|
+
# Steps:
|
|
5
|
+
# 1. Visit the dashboard URL above
|
|
6
|
+
# 2. Connect your wallet
|
|
7
|
+
# 3. Create a new project
|
|
8
|
+
# 4. Copy your App ID and paste it below
|
|
9
|
+
|
|
10
|
+
X402_APP_ID=your_app_id_here
|
|
11
|
+
X402_PLATFORM_URL=https://mantle-x402.vercel.app
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.*
|
|
7
|
+
.yarn/*
|
|
8
|
+
!.yarn/patches
|
|
9
|
+
!.yarn/plugins
|
|
10
|
+
!.yarn/releases
|
|
11
|
+
!.yarn/versions
|
|
12
|
+
|
|
13
|
+
# testing
|
|
14
|
+
/coverage
|
|
15
|
+
|
|
16
|
+
# next.js
|
|
17
|
+
/.next/
|
|
18
|
+
/out/
|
|
19
|
+
|
|
20
|
+
# production
|
|
21
|
+
/build
|
|
22
|
+
|
|
23
|
+
# misc
|
|
24
|
+
.DS_Store
|
|
25
|
+
*.pem
|
|
26
|
+
|
|
27
|
+
# debug
|
|
28
|
+
npm-debug.log*
|
|
29
|
+
yarn-debug.log*
|
|
30
|
+
yarn-error.log*
|
|
31
|
+
.pnpm-debug.log*
|
|
32
|
+
|
|
33
|
+
# env files
|
|
34
|
+
.env
|
|
35
|
+
.env.local
|
|
36
|
+
.env.*.local
|
|
37
|
+
|
|
38
|
+
# keep example
|
|
39
|
+
!.env.example
|
|
40
|
+
|
|
41
|
+
# vercel
|
|
42
|
+
.vercel
|
|
43
|
+
|
|
44
|
+
# typescript
|
|
45
|
+
*.tsbuildinfo
|
|
46
|
+
next-env.d.ts
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "x402-starter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Next.js starter template for building pay-per-request APIs with x402 on Mantle Network",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"lint": "eslint"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"clsx": "^2.1.1",
|
|
14
|
+
"ethers": "^6.16.0",
|
|
15
|
+
"motion": "^12.23.26",
|
|
16
|
+
"next": "16.1.0",
|
|
17
|
+
"react": "19.2.3",
|
|
18
|
+
"react-dom": "19.2.3",
|
|
19
|
+
"tailwind-merge": "^3.4.0",
|
|
20
|
+
"tw-animate-css": "^1.4.0",
|
|
21
|
+
"x402-mantle-sdk": "^0.2.3"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@tailwindcss/postcss": "^4",
|
|
25
|
+
"@types/node": "^20",
|
|
26
|
+
"@types/react": "^19",
|
|
27
|
+
"@types/react-dom": "^19",
|
|
28
|
+
"eslint": "^9",
|
|
29
|
+
"eslint-config-next": "16.1.0",
|
|
30
|
+
"tailwindcss": "^4",
|
|
31
|
+
"typescript": "^5"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
|
|
3
|
+
export async function GET() {
|
|
4
|
+
return NextResponse.json({
|
|
5
|
+
message: 'Welcome to x402 API',
|
|
6
|
+
endpoints: {
|
|
7
|
+
free: 'GET /api/info',
|
|
8
|
+
paid: 'GET /api/premium (0.001 MNT)',
|
|
9
|
+
paidWeather: 'GET /api/weather (0.0005 MNT)',
|
|
10
|
+
},
|
|
11
|
+
network: 'mantle-sepolia',
|
|
12
|
+
documentation: 'https://mantle-x402.vercel.app',
|
|
13
|
+
})
|
|
14
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { processPaymentMiddleware, initializePlatform } from 'x402-mantle-sdk/server'
|
|
3
|
+
|
|
4
|
+
let initialized = false
|
|
5
|
+
|
|
6
|
+
export async function GET(request: NextRequest) {
|
|
7
|
+
// Initialize platform once
|
|
8
|
+
if (!initialized) {
|
|
9
|
+
await initializePlatform()
|
|
10
|
+
initialized = true
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Convert headers to plain object
|
|
14
|
+
const headers: Record<string, string> = {}
|
|
15
|
+
request.headers.forEach((value, key) => {
|
|
16
|
+
headers[key] = value
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// Process payment - returns 402 if payment required
|
|
20
|
+
const result = await processPaymentMiddleware(
|
|
21
|
+
{ price: '0.001', token: 'MNT', testnet: true },
|
|
22
|
+
headers
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// Payment required - return 402 with payment details
|
|
26
|
+
if (result.paymentRequired) {
|
|
27
|
+
const response = NextResponse.json(result.paymentRequired.body, { status: 402 })
|
|
28
|
+
Object.entries(result.paymentRequired.headers).forEach(([key, value]) => {
|
|
29
|
+
response.headers.set(key, value)
|
|
30
|
+
})
|
|
31
|
+
return response
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Payment verified - return premium content
|
|
35
|
+
if (result.allowed) {
|
|
36
|
+
return NextResponse.json({
|
|
37
|
+
success: true,
|
|
38
|
+
message: 'Premium content unlocked!',
|
|
39
|
+
data: {
|
|
40
|
+
secret: 'This is premium data that required payment.',
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Error handling
|
|
47
|
+
if (result.error) {
|
|
48
|
+
return NextResponse.json({ error: result.error.message }, { status: result.error.status })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return NextResponse.json({ error: 'Unexpected error' }, { status: 500 })
|
|
52
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { processPaymentMiddleware, initializePlatform, clearCache } from 'x402-mantle-sdk/server'
|
|
3
|
+
|
|
4
|
+
// Configuration - Set these in your .env file
|
|
5
|
+
const appId = process.env.X402_APP_ID || ''
|
|
6
|
+
const platformUrl = process.env.X402_PLATFORM_URL || 'https://mantle-x402.vercel.app'
|
|
7
|
+
|
|
8
|
+
if (!process.env.X402_APP_ID) {
|
|
9
|
+
process.env.X402_APP_ID = appId
|
|
10
|
+
}
|
|
11
|
+
if (!process.env.X402_PLATFORM_URL) {
|
|
12
|
+
process.env.X402_PLATFORM_URL = platformUrl
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let initialized = false
|
|
16
|
+
let initError: Error | null = null
|
|
17
|
+
|
|
18
|
+
const initPlatform = async () => {
|
|
19
|
+
try {
|
|
20
|
+
await initializePlatform()
|
|
21
|
+
initialized = true
|
|
22
|
+
initError = null
|
|
23
|
+
} catch (error) {
|
|
24
|
+
initError = error as Error
|
|
25
|
+
console.error('Platform initialization failed:', error)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function GET(request: NextRequest) {
|
|
30
|
+
try {
|
|
31
|
+
// Initialize platform on first request
|
|
32
|
+
if (!initialized && !initError) {
|
|
33
|
+
await initPlatform()
|
|
34
|
+
} else if (initError) {
|
|
35
|
+
try { clearCache() } catch {}
|
|
36
|
+
initError = null
|
|
37
|
+
await initPlatform()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (initError) {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: 'Platform initialization failed', details: (initError as Error).message },
|
|
43
|
+
{ status: 500 }
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Payment options - cheaper for weather
|
|
48
|
+
const paymentOptions = {
|
|
49
|
+
price: '0.0005',
|
|
50
|
+
token: 'MNT',
|
|
51
|
+
testnet: true,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Convert headers
|
|
55
|
+
const headers: Record<string, string> = {}
|
|
56
|
+
request.headers.forEach((value, key) => {
|
|
57
|
+
headers[key] = value
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Process payment middleware
|
|
61
|
+
const result = await processPaymentMiddleware(paymentOptions, headers)
|
|
62
|
+
|
|
63
|
+
// Payment required - return 402
|
|
64
|
+
if (result.paymentRequired) {
|
|
65
|
+
const response = NextResponse.json(result.paymentRequired.body, { status: 402 })
|
|
66
|
+
Object.entries(result.paymentRequired.headers).forEach(([key, value]) => {
|
|
67
|
+
response.headers.set(key, value)
|
|
68
|
+
})
|
|
69
|
+
return response
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Error
|
|
73
|
+
if (result.error) {
|
|
74
|
+
return NextResponse.json(
|
|
75
|
+
{ error: result.error.message },
|
|
76
|
+
{ status: result.error.status }
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Payment verified - return weather data
|
|
81
|
+
if (result.allowed) {
|
|
82
|
+
return NextResponse.json({
|
|
83
|
+
success: true,
|
|
84
|
+
data: {
|
|
85
|
+
location: 'New York',
|
|
86
|
+
temperature: 72,
|
|
87
|
+
unit: 'F',
|
|
88
|
+
condition: 'Sunny',
|
|
89
|
+
humidity: 45,
|
|
90
|
+
forecast: ['Sunny', 'Partly Cloudy', 'Rain'],
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return NextResponse.json({ error: 'Unexpected error' }, { status: 500 })
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error in weather route:', error)
|
|
98
|
+
return NextResponse.json(
|
|
99
|
+
{ error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' },
|
|
100
|
+
{ status: 500 }
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:is(.dark *));
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
/* Light blue background with dark blue text palette */
|
|
8
|
+
--background: oklch(0.95 0.05 240); /* Light blue background */
|
|
9
|
+
--foreground: oklch(0.25 0.12 240); /* Dark blue (navy) text */
|
|
10
|
+
--card: oklch(0.92 0.04 240); /* Slightly darker light blue */
|
|
11
|
+
--card-foreground: oklch(0.25 0.12 240); /* Dark blue */
|
|
12
|
+
--popover: oklch(0.92 0.04 240);
|
|
13
|
+
--popover-foreground: oklch(0.25 0.12 240);
|
|
14
|
+
--primary: oklch(0.35 0.15 240); /* Medium dark blue */
|
|
15
|
+
--primary-foreground: oklch(0.95 0.05 240); /* Light blue */
|
|
16
|
+
--secondary: oklch(0.88 0.04 240); /* Light blue gray */
|
|
17
|
+
--secondary-foreground: oklch(0.25 0.12 240); /* Dark blue */
|
|
18
|
+
--muted: oklch(0.85 0.03 240);
|
|
19
|
+
--muted-foreground: oklch(0.4 0.1 240);
|
|
20
|
+
--accent: oklch(0.3 0.13 240); /* Dark blue accent */
|
|
21
|
+
--accent-foreground: oklch(0.95 0.05 240); /* Light blue */
|
|
22
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
23
|
+
--destructive-foreground: oklch(0.95 0.05 240);
|
|
24
|
+
--border: oklch(0.8 0.05 240); /* Light blue border */
|
|
25
|
+
--input: oklch(0.8 0.05 240);
|
|
26
|
+
--ring: oklch(0.35 0.15 240); /* Dark blue ring */
|
|
27
|
+
--radius: 0.75rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@theme inline {
|
|
31
|
+
--font-sans: "Geist", "Geist Fallback", system-ui, sans-serif;
|
|
32
|
+
--font-mono: "Geist Mono", "Geist Mono Fallback", monospace;
|
|
33
|
+
--color-background: var(--background);
|
|
34
|
+
--color-foreground: var(--foreground);
|
|
35
|
+
--color-card: var(--card);
|
|
36
|
+
--color-card-foreground: var(--card-foreground);
|
|
37
|
+
--color-popover: var(--popover);
|
|
38
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
39
|
+
--color-primary: var(--primary);
|
|
40
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
41
|
+
--color-secondary: var(--secondary);
|
|
42
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
43
|
+
--color-muted: var(--muted);
|
|
44
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
45
|
+
--color-accent: var(--accent);
|
|
46
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
47
|
+
--color-destructive: var(--destructive);
|
|
48
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
49
|
+
--color-border: var(--border);
|
|
50
|
+
--color-input: var(--input);
|
|
51
|
+
--color-ring: var(--ring);
|
|
52
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
53
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
54
|
+
--radius-lg: var(--radius);
|
|
55
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@layer base {
|
|
59
|
+
* {
|
|
60
|
+
@apply border-border outline-ring/50;
|
|
61
|
+
cursor: default;
|
|
62
|
+
}
|
|
63
|
+
body {
|
|
64
|
+
@apply bg-background text-foreground;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main {
|
|
68
|
+
/* Light blue textured background */
|
|
69
|
+
background-color: oklch(0.95 0.05 240);
|
|
70
|
+
background-image:
|
|
71
|
+
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%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.4' fill='%23e0f2fe'/%3E%3C/svg%3E");
|
|
72
|
+
background-size: 200px 200px;
|
|
73
|
+
background-repeat: repeat;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
button,
|
|
77
|
+
a {
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
transform: translateZ(0);
|
|
80
|
+
backface-visibility: hidden;
|
|
81
|
+
}
|
|
82
|
+
}
|