create-mantle-facilitator 0.3.4 → 0.4.0

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/README.md ADDED
@@ -0,0 +1,402 @@
1
+ # create-mantle-facilitator
2
+
3
+ [![npm version](https://img.shields.io/npm/v/create-mantle-facilitator.svg)](https://www.npmjs.com/package/create-mantle-facilitator)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ CLI tool to scaffold a self-hosted x402 payment facilitator for Mantle blockchain.
7
+
8
+ ## Quick Start
9
+
10
+ ```bash
11
+ npx create-mantle-facilitator my-facilitator
12
+ cd my-facilitator
13
+ npm install
14
+ npm run dev
15
+ ```
16
+
17
+ The CLI will guide you through the setup:
18
+
19
+ ```
20
+ Welcome to Mantle Facilitator Setup!
21
+
22
+ ? Where should we create the facilitator? my-facilitator
23
+ ? RPC URL for Mantle network: https://rpc.mantle.xyz
24
+ ? Facilitator wallet private key: (optional, set later)
25
+ ? USDC contract address: 0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9
26
+ ? Enable telemetry? Yes
27
+ ? Enter PROJECT_KEY from dashboard: (optional)
28
+
29
+ ✓ Done! Your facilitator is ready.
30
+ ```
31
+
32
+ ## Configuration
33
+
34
+ After creation, edit the `.env` file:
35
+
36
+ ```env
37
+ # Required
38
+ FACILITATOR_PRIVATE_KEY=0x... # Wallet that pays gas fees
39
+ RPC_URL=https://rpc.mantle.xyz # Mantle RPC endpoint
40
+ FACILITATOR_SECRET=fac_xxx... # Auto-generated security secret
41
+
42
+ # Optional
43
+ USDC_ADDRESS=0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9
44
+ PORT=8080
45
+ NETWORK_ID=mantle-mainnet
46
+ CHAIN_ID=5000
47
+
48
+ # Telemetry (optional)
49
+ TELEMETRY_PROJECT_KEY=pk_xxx # Get from dashboard
50
+ ```
51
+
52
+ ### Important Notes
53
+
54
+ - **FACILITATOR_PRIVATE_KEY**: This wallet will pay gas fees for all settlements. Make sure it has MNT for gas.
55
+ - **FACILITATOR_SECRET**: Auto-generated during setup. **Copy this to your backend's environment variables** to enable secure authentication.
56
+ - **Security**: Never commit `.env` to version control. The generated `.gitignore` already excludes it.
57
+
58
+ ### Connecting to Your Backend
59
+
60
+ Add `FACILITATOR_SECRET` to your backend and configure the SDK:
61
+
62
+ ```typescript
63
+ import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/express';
64
+
65
+ const pay = mantlePaywall({
66
+ priceUsd: 0.01,
67
+ payTo: '0xYourWallet',
68
+ facilitatorUrl: 'https://your-facilitator.com',
69
+ facilitatorSecret: process.env.FACILITATOR_SECRET, // Same secret as in facilitator
70
+ projectKey: process.env.PROJECT_KEY, // Optional: from dashboard for analytics
71
+ });
72
+ ```
73
+
74
+ **Note:** The `facilitatorUrl` is automatically passed to clients via 402 responses, so frontend code doesn't need to configure it.
75
+
76
+ ## Running Locally
77
+
78
+ ```bash
79
+ # Development mode (hot reload)
80
+ npm run dev
81
+
82
+ # Build for production
83
+ npm run build
84
+
85
+ # Run production build
86
+ npm start
87
+ ```
88
+
89
+ Facilitator runs on `http://localhost:8080` by default.
90
+
91
+ ---
92
+
93
+ ## Deployment
94
+
95
+ ### Railway (Recommended)
96
+
97
+ ```bash
98
+ # Install Railway CLI
99
+ npm install -g @railway/cli
100
+
101
+ # Login and deploy
102
+ railway login
103
+ railway init
104
+ railway up
105
+ ```
106
+
107
+ Then add environment variables in Railway dashboard.
108
+
109
+ ### Render
110
+
111
+ 1. Push your facilitator to GitHub
112
+ 2. Create new Web Service in Render
113
+ 3. Connect your repository
114
+ 4. Set build command: `npm install && npm run build`
115
+ 5. Set start command: `npm start`
116
+ 6. Add environment variables
117
+
118
+ ### Fly.io
119
+
120
+ ```bash
121
+ # Install Fly CLI and login
122
+ flyctl auth login
123
+
124
+ # Launch (creates fly.toml)
125
+ flyctl launch
126
+
127
+ # Set secrets
128
+ flyctl secrets set FACILITATOR_PRIVATE_KEY=0x...
129
+ flyctl secrets set RPC_URL=https://rpc.mantle.xyz
130
+ flyctl secrets set USDC_ADDRESS=0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9
131
+
132
+ # Deploy
133
+ flyctl deploy
134
+ ```
135
+
136
+ ### Docker
137
+
138
+ ```dockerfile
139
+ FROM node:20-alpine
140
+ WORKDIR /app
141
+ COPY package*.json ./
142
+ RUN npm install
143
+ COPY . .
144
+ RUN npm run build
145
+ EXPOSE 8080
146
+ CMD ["npm", "start"]
147
+ ```
148
+
149
+ ```bash
150
+ docker build -t my-facilitator .
151
+ docker run -p 8080:8080 --env-file .env my-facilitator
152
+ ```
153
+
154
+ ### VPS (DigitalOcean, Hetzner, etc.)
155
+
156
+ ```bash
157
+ # On your server
158
+ git clone <your-repo>
159
+ cd my-facilitator
160
+ npm install
161
+ npm run build
162
+
163
+ # Install PM2 for process management
164
+ npm install -g pm2
165
+
166
+ # Start with PM2
167
+ pm2 start dist/index.js --name facilitator
168
+
169
+ # Save process list and setup startup script
170
+ pm2 save
171
+ pm2 startup
172
+ ```
173
+
174
+ Optional: Add Nginx reverse proxy for HTTPS.
175
+
176
+ ---
177
+
178
+ ## API Endpoints
179
+
180
+ ### GET /health
181
+
182
+ Returns facilitator status and wallet info.
183
+
184
+ ```bash
185
+ curl http://localhost:8080/health
186
+ ```
187
+
188
+ ```json
189
+ {
190
+ "status": "ok",
191
+ "network": "mantle-mainnet",
192
+ "chainId": 5000,
193
+ "facilitatorAddress": "0x...",
194
+ "blockNumber": 12345678,
195
+ "mntBalance": "1.5"
196
+ }
197
+ ```
198
+
199
+ ### GET /supported
200
+
201
+ Returns supported networks and assets.
202
+
203
+ ```bash
204
+ curl http://localhost:8080/supported
205
+ ```
206
+
207
+ ```json
208
+ {
209
+ "x402Version": 1,
210
+ "schemes": ["exact"],
211
+ "networks": ["mantle-mainnet"],
212
+ "assets": {
213
+ "mantle-mainnet": [{
214
+ "address": "0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9",
215
+ "symbol": "USDC",
216
+ "decimals": 6
217
+ }]
218
+ }
219
+ }
220
+ ```
221
+
222
+ ### POST /verify
223
+
224
+ Verifies a payment header without executing.
225
+
226
+ ```bash
227
+ curl -X POST http://localhost:8080/verify \
228
+ -H "Content-Type: application/json" \
229
+ -d '{
230
+ "x402Version": 1,
231
+ "paymentHeader": "base64...",
232
+ "paymentRequirements": {...}
233
+ }'
234
+ ```
235
+
236
+ ```json
237
+ {
238
+ "isValid": true
239
+ }
240
+ ```
241
+
242
+ ### POST /settle
243
+
244
+ Executes the USDC transfer on-chain. Requires a valid `settleToken` for authentication.
245
+
246
+ ```bash
247
+ curl -X POST http://localhost:8080/settle \
248
+ -H "Content-Type: application/json" \
249
+ -d '{
250
+ "x402Version": 1,
251
+ "paymentHeader": "base64...",
252
+ "paymentRequirements": {...},
253
+ "settleToken": "base64..."
254
+ }'
255
+ ```
256
+
257
+ ```json
258
+ {
259
+ "success": true,
260
+ "txHash": "0x..."
261
+ }
262
+ ```
263
+
264
+ **Note:** The `settleToken` is automatically generated by the SDK when `facilitatorSecret` is configured, and passed through the client to authenticate with your facilitator.
265
+
266
+ ---
267
+
268
+ ## How It Works
269
+
270
+ ```
271
+ ┌─────────────────────────────────────────────────────────────────┐
272
+ │ Settlement Flow │
273
+ ├─────────────────────────────────────────────────────────────────┤
274
+ │ │
275
+ │ 1. Client signs EIP-712 authorization (gasless for user) │
276
+ │ │ │
277
+ │ ▼ │
278
+ │ 2. Client sends to facilitator POST /settle │
279
+ │ │ │
280
+ │ ▼ │
281
+ │ 3. Facilitator verifies signature │
282
+ │ │ │
283
+ │ ▼ │
284
+ │ 4. Facilitator calls USDC.transferWithAuthorization() │
285
+ │ - Pays gas in MNT │
286
+ │ - USDC transfers from user → merchant │
287
+ │ │ │
288
+ │ ▼ │
289
+ │ 5. Returns txHash to client │
290
+ │ │
291
+ └─────────────────────────────────────────────────────────────────┘
292
+ ```
293
+
294
+ The facilitator uses EIP-3009 `transferWithAuthorization` which allows:
295
+ - **Gasless for users**: Users only sign, never pay gas
296
+ - **Atomic transfers**: Signature + transfer in one transaction
297
+ - **Replay protection**: Nonces prevent double-spending
298
+
299
+ ---
300
+
301
+ ## Telemetry
302
+
303
+ When `TELEMETRY_PROJECT_KEY` is set, the facilitator sends anonymous analytics:
304
+
305
+ **What is sent:**
306
+ - Payment metadata (buyer address, amount, asset, network)
307
+ - Transaction hash
308
+ - Timestamp
309
+ - Facilitator address
310
+
311
+ **What is NOT sent:**
312
+ - Private keys
313
+ - Personal information
314
+ - Request/response payloads
315
+
316
+ Get your project key from [Dashboard](https://x402mantlesdk.xyz/dashboard).
317
+
318
+ Telemetry errors never affect payment processing — if analytics backend is down, payments continue normally.
319
+
320
+ ---
321
+
322
+ ## Security Considerations
323
+
324
+ 1. **Facilitator Secret (REQUIRED)**
325
+ - `FACILITATOR_SECRET` prevents unauthorized usage of your facilitator
326
+ - Without it, anyone could use your facilitator URL to settle their payments and drain your gas wallet
327
+ - The secret creates a secure token flow: Backend → Client → Facilitator
328
+ - Always set `facilitatorSecret` in your backend SDK configuration
329
+
330
+ 2. **Private Key Security**
331
+ - Never commit `.env` to git
332
+ - Use secret management in production (Railway secrets, Fly secrets, etc.)
333
+ - Consider using hardware wallets for high-value facilitators
334
+
335
+ 3. **Gas Management**
336
+ - Monitor MNT balance regularly
337
+ - Set up alerts for low balance
338
+ - Facilitator needs ~0.001 MNT per settlement
339
+
340
+ 4. **Network Security**
341
+ - Use HTTPS in production
342
+ - Consider rate limiting
343
+ - Monitor for unusual patterns
344
+
345
+ ---
346
+
347
+ ## Monitoring
348
+
349
+ Check facilitator health:
350
+
351
+ ```bash
352
+ # Check if running
353
+ curl http://your-facilitator.com/health
354
+
355
+ # Check wallet balance
356
+ curl http://your-facilitator.com/health | jq '.mntBalance'
357
+ ```
358
+
359
+ Set up monitoring alerts for:
360
+ - Health endpoint availability
361
+ - Low MNT balance (< 0.1 MNT)
362
+ - High error rates
363
+
364
+ ---
365
+
366
+ ## Troubleshooting
367
+
368
+ ### "Missing env var: FACILITATOR_PRIVATE_KEY"
369
+
370
+ Set the private key in `.env`:
371
+ ```env
372
+ FACILITATOR_PRIVATE_KEY=0x...
373
+ ```
374
+
375
+ ### "Insufficient funds for gas"
376
+
377
+ The facilitator wallet needs MNT for gas fees. Send some MNT to the facilitator address shown in `/health`.
378
+
379
+ ### "Invalid signature"
380
+
381
+ - Check that the client is signing for the correct network (Mantle mainnet, chainId 5000)
382
+ - Verify USDC address matches: `0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9`
383
+
384
+ ### Port already in use
385
+
386
+ Change the port in `.env`:
387
+ ```env
388
+ PORT=8081
389
+ ```
390
+
391
+ ---
392
+
393
+ ## Links
394
+
395
+ - [SDK Documentation](../x402-mantle-sdk)
396
+ - [Full Documentation](https://x402mantlesdk.xyz/docs)
397
+ - [Dashboard](https://x402mantlesdk.xyz/dashboard)
398
+ - [GitHub](https://github.com/puga-labs/x402-mantle-sdk)
399
+
400
+ ## License
401
+
402
+ MIT
package/dist/index.mjs CHANGED
@@ -3,9 +3,13 @@
3
3
  // src/index.ts
4
4
  import path from "path";
5
5
  import fs from "fs";
6
+ import crypto from "crypto";
6
7
  import { fileURLToPath } from "url";
7
8
  import fse from "fs-extra";
8
9
  import prompts from "prompts";
10
+ function generateFacilitatorSecret() {
11
+ return "fac_" + crypto.randomBytes(32).toString("hex");
12
+ }
9
13
  function getAsciiArt() {
10
14
  const asciiArtPath = path.join(
11
15
  path.dirname(fileURLToPath(import.meta.url)),
@@ -94,6 +98,7 @@ async function promptSetup() {
94
98
  });
95
99
  }
96
100
  function generateEnvFile(answers) {
101
+ const facilitatorSecret = generateFacilitatorSecret();
97
102
  const env = [
98
103
  `# Server Configuration`,
99
104
  `PORT=8080`,
@@ -110,6 +115,10 @@ function generateEnvFile(answers) {
110
115
  `# Facilitator Wallet (keeps private key safe)`,
111
116
  answers.privateKey ? `FACILITATOR_PRIVATE_KEY=${answers.privateKey}` : `# FACILITATOR_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE`,
112
117
  ``,
118
+ `# Security: Shared secret between your backend and this facilitator`,
119
+ `# IMPORTANT: Copy this value to your backend's facilitatorSecret config`,
120
+ `FACILITATOR_SECRET=${facilitatorSecret}`,
121
+ ``,
113
122
  `# Logging`,
114
123
  `LOG_LEVEL=info`,
115
124
  ``
@@ -167,6 +176,7 @@ Creating facilitator in ${targetDir}...`);
167
176
  if (!answers.privateKey) {
168
177
  reminders.push("IMPORTANT: Set FACILITATOR_PRIVATE_KEY in .env before starting");
169
178
  }
179
+ reminders.push("IMPORTANT: Copy FACILITATOR_SECRET from .env to your backend configuration");
170
180
  if (answers.enableTelemetry && !answers.telemetryKey) {
171
181
  reminders.push("Telemetry enabled: Get PROJECT_KEY from https://x402mantlesdk.xyz/dashboard");
172
182
  }
@@ -210,6 +220,7 @@ async function createProjectNonInteractive(projectName) {
210
220
  await fse.writeFile(path.join(targetDir, ".env"), envContent);
211
221
  console.log("\n\u2713 Done!\n");
212
222
  console.log("IMPORTANT: Edit .env file and set FACILITATOR_PRIVATE_KEY");
223
+ console.log("IMPORTANT: Copy FACILITATOR_SECRET from .env to your backend configuration");
213
224
  console.log(`Then run: cd ${projectName} && npm install && npm run dev`);
214
225
  }
215
226
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mantle-facilitator",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,53 +1,127 @@
1
- # Mantle x402 Facilitator (Self-hosted Template)
1
+ # Mantle x402 Facilitator
2
2
 
3
- A minimal, self-hosted x402 facilitator for **Mantle mainnet** using **USDC (EIP-3009)**.
3
+ A self-hosted x402 payment facilitator for **Mantle mainnet** using **USDC (EIP-3009)**.
4
4
 
5
5
  This facilitator:
6
- - verifies x402 payment headers
7
- - settles payments on-chain via `transferWithAuthorization`
8
- - pays gas from the facilitator wallet
6
+ - Verifies x402 payment headers
7
+ - Settles payments on-chain via `transferWithAuthorization`
8
+ - Pays gas from the facilitator wallet
9
+
10
+ ## Quick Start
11
+
12
+ ```bash
13
+ npm install
14
+ npm run dev
15
+ ```
16
+
17
+ Facilitator runs on `http://localhost:8080`.
18
+
19
+ ## Configuration
20
+
21
+ Edit `.env` file:
22
+
23
+ ```env
24
+ # Required
25
+ FACILITATOR_PRIVATE_KEY=0x... # Wallet that pays gas
26
+ RPC_URL=https://rpc.mantle.xyz
27
+ FACILITATOR_SECRET=fac_xxx... # Auto-generated, share with your backend
28
+
29
+ # Optional
30
+ PORT=8080
31
+ USDC_ADDRESS=0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9
32
+
33
+ # Telemetry (optional)
34
+ TELEMETRY_PROJECT_KEY=pk_xxx # Get from https://x402mantlesdk.xyz/dashboard
35
+ ```
36
+
37
+ **Important:**
38
+ - The facilitator wallet needs MNT for gas fees. Check balance at `/health`.
39
+ - Copy `FACILITATOR_SECRET` to your backend's environment variables.
40
+
41
+ ## Security
42
+
43
+ The `FACILITATOR_SECRET` protects your facilitator from unauthorized usage:
44
+
45
+ 1. Your backend generates a `settleToken` using this secret when returning 402 responses
46
+ 2. The client passes this token to your facilitator's `/settle` endpoint
47
+ 3. The facilitator verifies the token matches before executing the transaction
48
+
49
+ This prevents third parties from using your facilitator to settle their payments and drain your gas wallet.
50
+
51
+ **Backend configuration:**
52
+
53
+ ```typescript
54
+ import { mantlePaywall } from '@puga-labs/x402-mantle-sdk/server/express';
55
+
56
+ const pay = mantlePaywall({
57
+ priceUsd: 0.01,
58
+ payTo: '0xYourWallet',
59
+ facilitatorUrl: 'https://your-facilitator.com',
60
+ facilitatorSecret: process.env.FACILITATOR_SECRET, // Same as in facilitator .env
61
+ projectKey: process.env.PROJECT_KEY, // Optional: for analytics
62
+ });
63
+ ```
64
+
65
+ **Note:** The `facilitatorUrl` is automatically passed to clients via 402 responses.
9
66
 
10
67
  ## Endpoints
11
68
 
12
- - `GET /health`
13
- - `GET /supported`
14
- - `POST /verify`
15
- - `POST /settle`
69
+ | Endpoint | Method | Description |
70
+ |----------|--------|-------------|
71
+ | `/health` | GET | Facilitator status and wallet balance |
72
+ | `/supported` | GET | Supported networks and assets |
73
+ | `/verify` | POST | Verify payment header |
74
+ | `/settle` | POST | Execute USDC transfer on-chain |
16
75
 
17
- ## Quickstart
76
+ ## Scripts
18
77
 
19
- This template is generated by `create-mantle-facilitator`. The CLI automatically creates a `.env` file with all configuration.
78
+ ```bash
79
+ npm run dev # Development with hot reload
80
+ npm run build # Build for production
81
+ npm start # Run production build
82
+ ```
83
+
84
+ ## Deployment
20
85
 
21
- If you need to modify configuration:
22
- 1. Edit `.env` file
23
- 2. Set `FACILITATOR_PRIVATE_KEY` if not already set
24
- 3. Adjust `RPC_URL` if using a custom RPC endpoint
86
+ ### Railway
25
87
 
26
88
  ```bash
27
- npm install
28
- npm start
89
+ railway login
90
+ railway init
91
+ railway up
92
+ ```
93
+
94
+ Add environment variables in Railway dashboard.
95
+
96
+ ### Docker
97
+
98
+ ```bash
99
+ docker build -t facilitator .
100
+ docker run -p 8080:8080 --env-file .env facilitator
101
+ ```
102
+
103
+ ### PM2 (VPS)
104
+
105
+ ```bash
106
+ npm run build
107
+ pm2 start dist/index.js --name facilitator
29
108
  ```
30
109
 
31
- ## Optional: Analytics & Telemetry
110
+ ## Telemetry
32
111
 
33
- To enable opt-in telemetry for payment tracking and analytics:
112
+ When `TELEMETRY_PROJECT_KEY` is set:
113
+ - Settlement events are sent to analytics
114
+ - Track payments in [Dashboard](https://x402mantlesdk.xyz/dashboard)
115
+ - No sensitive data is transmitted
34
116
 
35
- 1. Get your project key from https://nosubs.ai/dashboard (or your analytics platform)
36
- 2. Add to `.env`:
37
- ```bash
38
- TELEMETRY_PROJECT_KEY=proj_abc123xyz
39
- ```
117
+ **What is sent:** buyer address, amount, asset, txHash, timestamp
40
118
 
41
- **Note:** Telemetry is sent automatically to the official analytics backend. If you don't want to send telemetry, simply don't set `TELEMETRY_PROJECT_KEY`.
119
+ **What is NOT sent:** private keys, personal info, request payloads
42
120
 
43
- **What data is sent:**
44
- - Payment metadata (buyer address, amount, asset, network)
45
- - Transaction hash (after settlement)
46
- - Timestamp
121
+ Telemetry errors never break payment processing.
47
122
 
48
- **What is NOT sent:**
49
- - Private keys
50
- - User personal information
51
- - Request/response payloads from your protected endpoints
123
+ ## Links
52
124
 
53
- Telemetry errors never break payment processing - if the analytics backend is down, payments continue to work normally.
125
+ - [SDK Documentation](https://www.npmjs.com/package/@puga-labs/x402-mantle-sdk)
126
+ - [Full Documentation](https://x402mantlesdk.xyz/docs)
127
+ - [Dashboard](https://x402mantlesdk.xyz/dashboard)
@@ -20,6 +20,9 @@ export const CONFIG = {
20
20
 
21
21
  facilitatorPrivateKey: required("FACILITATOR_PRIVATE_KEY"),
22
22
 
23
+ // Secret for settle token verification (required for security)
24
+ facilitatorSecret: required("FACILITATOR_SECRET"),
25
+
23
26
  logLevel: process.env.LOG_LEVEL ?? "info",
24
27
 
25
28
  // Telemetry config (opt-in via projectKey)
@@ -4,6 +4,7 @@ import { ethers } from "ethers";
4
4
  import { decodePaymentHeader, verifyPayment, type PaymentRequirements } from "../x402";
5
5
  import { getFacilitatorSigner, getUsdcContract } from "../blockchain";
6
6
  import { CONFIG } from "../config";
7
+ import { verifySettleToken } from "../security";
7
8
 
8
9
  function signatureToVRS(signature: string) {
9
10
  const sig = ethers.Signature.from(signature);
@@ -15,10 +16,11 @@ export async function settleRoute(req: Request, res: Response) {
15
16
  const raw = req.body ?? {};
16
17
 
17
18
  try {
18
- const { x402Version, paymentHeader, paymentRequirements } = raw as {
19
+ const { x402Version, paymentHeader, paymentRequirements, settleToken } = raw as {
19
20
  x402Version?: number;
20
21
  paymentHeader?: string;
21
22
  paymentRequirements?: PaymentRequirements;
23
+ settleToken?: string;
22
24
  };
23
25
  const projectKey = req.header("X-Project-Key"); // Optional: for hosted facilitator billing
24
26
 
@@ -47,6 +49,18 @@ export async function settleRoute(req: Request, res: Response) {
47
49
  return;
48
50
  }
49
51
 
52
+ // Verify settle token (required for self-hosted facilitator security)
53
+ const tokenVerification = verifySettleToken(settleToken, paymentRequirements, CONFIG.facilitatorSecret);
54
+ if (!tokenVerification.valid) {
55
+ res.status(401).json({
56
+ success: false,
57
+ error: tokenVerification.error || "Invalid settle token",
58
+ txHash: null,
59
+ networkId: paymentRequirements.network,
60
+ });
61
+ return;
62
+ }
63
+
50
64
  const headerObj = decodePaymentHeader(paymentHeader);
51
65
  const verify = verifyPayment(headerObj, paymentRequirements);
52
66
 
@@ -0,0 +1,126 @@
1
+ // src/security.ts
2
+ import crypto from "crypto";
3
+
4
+ export interface PaymentRequirementsForToken {
5
+ payTo: string;
6
+ maxAmountRequired: string;
7
+ asset: string;
8
+ network: string;
9
+ }
10
+
11
+ interface SettleTokenPayload {
12
+ payTo: string;
13
+ amount: string;
14
+ asset: string;
15
+ network: string;
16
+ timestamp: number;
17
+ expires: number;
18
+ }
19
+
20
+ interface DecodedSettleToken {
21
+ payload: SettleTokenPayload;
22
+ signature: string;
23
+ }
24
+
25
+ /**
26
+ * Creates a settle token that authorizes a specific payment.
27
+ * The token is signed with HMAC-SHA256 and includes expiration.
28
+ *
29
+ * @param paymentRequirements - The payment requirements to authorize
30
+ * @param secret - The FACILITATOR_SECRET
31
+ * @param expiresInSeconds - Token validity period (default: 600 seconds = 10 minutes)
32
+ * @returns Base64-encoded settle token
33
+ */
34
+ export function createSettleToken(
35
+ paymentRequirements: PaymentRequirementsForToken,
36
+ secret: string,
37
+ expiresInSeconds: number = 600
38
+ ): string {
39
+ const timestamp = Math.floor(Date.now() / 1000);
40
+
41
+ const payload: SettleTokenPayload = {
42
+ payTo: paymentRequirements.payTo.toLowerCase(),
43
+ amount: paymentRequirements.maxAmountRequired,
44
+ asset: paymentRequirements.asset.toLowerCase(),
45
+ network: paymentRequirements.network,
46
+ timestamp,
47
+ expires: timestamp + expiresInSeconds,
48
+ };
49
+
50
+ const data = JSON.stringify(payload);
51
+ const signature = crypto.createHmac("sha256", secret).update(data).digest("hex");
52
+
53
+ return Buffer.from(JSON.stringify({ payload, signature })).toString("base64");
54
+ }
55
+
56
+ /**
57
+ * Verifies a settle token against payment requirements.
58
+ * Checks signature, expiration, and that the token matches the requirements.
59
+ *
60
+ * @param token - Base64-encoded settle token
61
+ * @param paymentRequirements - The payment requirements to verify against
62
+ * @param secret - The FACILITATOR_SECRET
63
+ * @returns true if token is valid, false otherwise
64
+ */
65
+ export function verifySettleToken(
66
+ token: string | undefined,
67
+ paymentRequirements: PaymentRequirementsForToken,
68
+ secret: string
69
+ ): { valid: boolean; error?: string } {
70
+ if (!token) {
71
+ return { valid: false, error: "Missing settle token" };
72
+ }
73
+
74
+ try {
75
+ const decoded: DecodedSettleToken = JSON.parse(
76
+ Buffer.from(token, "base64").toString("utf-8")
77
+ );
78
+ const { payload, signature } = decoded;
79
+
80
+ // Check expiration
81
+ const now = Math.floor(Date.now() / 1000);
82
+ if (payload.expires < now) {
83
+ return { valid: false, error: "Settle token expired" };
84
+ }
85
+
86
+ // Check that token matches payment requirements (case-insensitive for addresses)
87
+ if (payload.payTo.toLowerCase() !== paymentRequirements.payTo.toLowerCase()) {
88
+ return { valid: false, error: "PayTo address mismatch" };
89
+ }
90
+
91
+ if (payload.amount !== paymentRequirements.maxAmountRequired) {
92
+ return { valid: false, error: "Amount mismatch" };
93
+ }
94
+
95
+ if (payload.asset.toLowerCase() !== paymentRequirements.asset.toLowerCase()) {
96
+ return { valid: false, error: "Asset mismatch" };
97
+ }
98
+
99
+ if (payload.network !== paymentRequirements.network) {
100
+ return { valid: false, error: "Network mismatch" };
101
+ }
102
+
103
+ // Verify signature
104
+ const data = JSON.stringify(payload);
105
+ const expectedSignature = crypto
106
+ .createHmac("sha256", secret)
107
+ .update(data)
108
+ .digest("hex");
109
+
110
+ if (signature !== expectedSignature) {
111
+ return { valid: false, error: "Invalid signature" };
112
+ }
113
+
114
+ return { valid: true };
115
+ } catch (err) {
116
+ return { valid: false, error: "Invalid token format" };
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Generates a random secret for FACILITATOR_SECRET.
122
+ * Format: fac_<64 hex characters>
123
+ */
124
+ export function generateFacilitatorSecret(): string {
125
+ return "fac_" + crypto.randomBytes(32).toString("hex");
126
+ }