create-mantle-facilitator 0.1.0 → 0.2.1
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/package.json +1 -2
- package/template/.env.example +8 -0
- package/template/README.md +25 -0
- package/template/dist/index.mjs +324 -0
- package/template/src/config.ts +9 -0
- package/template/src/constants.ts +9 -0
- package/template/src/routes/settle.ts +45 -0
- package/template/src/routes/verify.ts +6 -0
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -15,3 +15,11 @@ FACILITATOR_PRIVATE_KEY=0xYOUR_PRIVATE_KEY
|
|
|
15
15
|
|
|
16
16
|
# Optional: enable verbose logs
|
|
17
17
|
LOG_LEVEL=debug
|
|
18
|
+
|
|
19
|
+
# =============================================================================
|
|
20
|
+
# Optional: Analytics & Telemetry
|
|
21
|
+
# =============================================================================
|
|
22
|
+
# Uncomment to send usage metrics to analytics backend
|
|
23
|
+
# This helps improve the x402 ecosystem and provides you with payment analytics
|
|
24
|
+
|
|
25
|
+
# TELEMETRY_PROJECT_KEY=your_project_key_here
|
package/template/README.md
CHANGED
|
@@ -23,3 +23,28 @@ npm install
|
|
|
23
23
|
cp .env.example .env
|
|
24
24
|
# Fill RPC_URL and FACILITATOR_PRIVATE_KEY
|
|
25
25
|
npm start
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Optional: Analytics & Telemetry
|
|
29
|
+
|
|
30
|
+
To enable opt-in telemetry for payment tracking and analytics:
|
|
31
|
+
|
|
32
|
+
1. Get your project key from https://nosubs.ai/dashboard (or your analytics platform)
|
|
33
|
+
2. Add to `.env`:
|
|
34
|
+
```bash
|
|
35
|
+
TELEMETRY_PROJECT_KEY=proj_abc123xyz
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**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`.
|
|
39
|
+
|
|
40
|
+
**What data is sent:**
|
|
41
|
+
- Payment metadata (buyer address, amount, asset, network)
|
|
42
|
+
- Transaction hash (after settlement)
|
|
43
|
+
- Timestamp
|
|
44
|
+
|
|
45
|
+
**What is NOT sent:**
|
|
46
|
+
- Private keys
|
|
47
|
+
- User personal information
|
|
48
|
+
- Request/response payloads from your protected endpoints
|
|
49
|
+
|
|
50
|
+
Telemetry errors never break payment processing - if the analytics backend is down, payments continue to work normally.
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import express from "express";
|
|
3
|
+
|
|
4
|
+
// src/config.ts
|
|
5
|
+
import "dotenv/config";
|
|
6
|
+
|
|
7
|
+
// src/constants.ts
|
|
8
|
+
var DEFAULT_TELEMETRY_ENDPOINT = void 0;
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
function required(name) {
|
|
12
|
+
const v = process.env[name];
|
|
13
|
+
if (!v) throw new Error(`Missing env var: ${name}`);
|
|
14
|
+
return v;
|
|
15
|
+
}
|
|
16
|
+
var CONFIG = {
|
|
17
|
+
port: Number(process.env.PORT ?? 8080),
|
|
18
|
+
networkId: process.env.NETWORK_ID ?? "mantle-mainnet",
|
|
19
|
+
chainId: Number(process.env.CHAIN_ID ?? 5e3),
|
|
20
|
+
rpcUrl: required("RPC_URL"),
|
|
21
|
+
usdcAddress: required("USDC_ADDRESS"),
|
|
22
|
+
usdcDecimals: Number(process.env.USDC_DECIMALS ?? 6),
|
|
23
|
+
facilitatorPrivateKey: required("FACILITATOR_PRIVATE_KEY"),
|
|
24
|
+
logLevel: process.env.LOG_LEVEL ?? "info",
|
|
25
|
+
// Telemetry config (opt-in via projectKey)
|
|
26
|
+
telemetry: process.env.TELEMETRY_PROJECT_KEY ? {
|
|
27
|
+
projectKey: process.env.TELEMETRY_PROJECT_KEY,
|
|
28
|
+
endpoint: DEFAULT_TELEMETRY_ENDPOINT
|
|
29
|
+
} : void 0
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/routes/health.ts
|
|
33
|
+
import { ethers as ethers2 } from "ethers";
|
|
34
|
+
|
|
35
|
+
// src/blockchain.ts
|
|
36
|
+
import { ethers } from "ethers";
|
|
37
|
+
var USDC_EIP3009_ABI = [
|
|
38
|
+
"function transferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce,uint8 v,bytes32 r,bytes32 s) external",
|
|
39
|
+
"function balanceOf(address account) view returns (uint256)"
|
|
40
|
+
];
|
|
41
|
+
function getProvider() {
|
|
42
|
+
return new ethers.JsonRpcProvider(CONFIG.rpcUrl, CONFIG.chainId);
|
|
43
|
+
}
|
|
44
|
+
function getFacilitatorSigner() {
|
|
45
|
+
const provider = getProvider();
|
|
46
|
+
return new ethers.Wallet(CONFIG.facilitatorPrivateKey, provider);
|
|
47
|
+
}
|
|
48
|
+
function getUsdcContract(signerOrProvider) {
|
|
49
|
+
return new ethers.Contract(CONFIG.usdcAddress, USDC_EIP3009_ABI, signerOrProvider);
|
|
50
|
+
}
|
|
51
|
+
async function getMntBalance(address) {
|
|
52
|
+
const provider = getProvider();
|
|
53
|
+
return provider.getBalance(address);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/routes/health.ts
|
|
57
|
+
async function healthRoute(_req, res) {
|
|
58
|
+
try {
|
|
59
|
+
const signer = getFacilitatorSigner();
|
|
60
|
+
const provider = signer.provider;
|
|
61
|
+
if (!provider) {
|
|
62
|
+
res.status(500).json({ ok: false, error: "No provider available" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const currentBlock = await provider.getBlockNumber();
|
|
66
|
+
const mntBalanceWei = await getMntBalance(signer.address);
|
|
67
|
+
res.status(200).json({
|
|
68
|
+
ok: true,
|
|
69
|
+
network: CONFIG.networkId,
|
|
70
|
+
chainId: CONFIG.chainId,
|
|
71
|
+
facilitatorAddress: signer.address,
|
|
72
|
+
currentBlock,
|
|
73
|
+
mntBalanceWei: mntBalanceWei.toString(),
|
|
74
|
+
mntBalance: ethers2.formatEther(mntBalanceWei)
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
78
|
+
res.status(500).json({ ok: false, error: msg });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/routes/supported.ts
|
|
83
|
+
function supportedRoute(_req, res) {
|
|
84
|
+
res.status(200).json({
|
|
85
|
+
networkId: CONFIG.networkId,
|
|
86
|
+
chainId: CONFIG.chainId,
|
|
87
|
+
schemes: ["exact"],
|
|
88
|
+
assets: [
|
|
89
|
+
{
|
|
90
|
+
symbol: "USDC",
|
|
91
|
+
address: CONFIG.usdcAddress,
|
|
92
|
+
decimals: CONFIG.usdcDecimals
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/x402.ts
|
|
99
|
+
import { ethers as ethers3 } from "ethers";
|
|
100
|
+
function decodePaymentHeader(paymentHeader) {
|
|
101
|
+
try {
|
|
102
|
+
const json = Buffer.from(paymentHeader, "base64").toString("utf8");
|
|
103
|
+
return JSON.parse(json);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
106
|
+
throw new Error(`Failed to decode paymentHeader: ${msg}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function validateHeaderShape(headerObj) {
|
|
110
|
+
if (!headerObj || typeof headerObj !== "object") {
|
|
111
|
+
return { ok: false, reason: "Header is not an object" };
|
|
112
|
+
}
|
|
113
|
+
if (headerObj.x402Version !== 1) {
|
|
114
|
+
return { ok: false, reason: "Unsupported x402Version" };
|
|
115
|
+
}
|
|
116
|
+
if (headerObj.scheme !== "exact") {
|
|
117
|
+
return { ok: false, reason: "Unsupported scheme" };
|
|
118
|
+
}
|
|
119
|
+
if (!headerObj.network) {
|
|
120
|
+
return { ok: false, reason: "Missing network" };
|
|
121
|
+
}
|
|
122
|
+
if (!headerObj.payload?.authorization || !headerObj.payload?.signature) {
|
|
123
|
+
return { ok: false, reason: "Missing payload.authorization or payload.signature" };
|
|
124
|
+
}
|
|
125
|
+
return { ok: true };
|
|
126
|
+
}
|
|
127
|
+
function getUsdcTypedData(authorization) {
|
|
128
|
+
const domain = {
|
|
129
|
+
name: "USD Coin",
|
|
130
|
+
version: "2",
|
|
131
|
+
chainId: CONFIG.chainId,
|
|
132
|
+
verifyingContract: CONFIG.usdcAddress
|
|
133
|
+
};
|
|
134
|
+
const types = {
|
|
135
|
+
TransferWithAuthorization: [
|
|
136
|
+
{ name: "from", type: "address" },
|
|
137
|
+
{ name: "to", type: "address" },
|
|
138
|
+
{ name: "value", type: "uint256" },
|
|
139
|
+
{ name: "validAfter", type: "uint256" },
|
|
140
|
+
{ name: "validBefore", type: "uint256" },
|
|
141
|
+
{ name: "nonce", type: "bytes32" }
|
|
142
|
+
]
|
|
143
|
+
};
|
|
144
|
+
return { domain, types, primaryType: "TransferWithAuthorization", message: authorization };
|
|
145
|
+
}
|
|
146
|
+
function verifyAuthorizationSignature(authorization, signature) {
|
|
147
|
+
const { domain, types, message } = getUsdcTypedData(authorization);
|
|
148
|
+
return ethers3.verifyTypedData(domain, types, message, signature);
|
|
149
|
+
}
|
|
150
|
+
function verifyPayment(headerObj, paymentRequirements) {
|
|
151
|
+
const shape = validateHeaderShape(headerObj);
|
|
152
|
+
if (!shape.ok) return { isValid: false, invalidReason: shape.reason };
|
|
153
|
+
const { authorization, signature } = headerObj.payload;
|
|
154
|
+
if (headerObj.network !== paymentRequirements.network) {
|
|
155
|
+
return { isValid: false, invalidReason: "Network mismatch" };
|
|
156
|
+
}
|
|
157
|
+
if (paymentRequirements.scheme !== "exact") {
|
|
158
|
+
return { isValid: false, invalidReason: "Only exact scheme supported" };
|
|
159
|
+
}
|
|
160
|
+
if (authorization.to.toLowerCase() !== paymentRequirements.payTo.toLowerCase()) {
|
|
161
|
+
return { isValid: false, invalidReason: "Authorization.to does not match payTo" };
|
|
162
|
+
}
|
|
163
|
+
const authValue = BigInt(authorization.value);
|
|
164
|
+
const maxValue = BigInt(paymentRequirements.maxAmountRequired);
|
|
165
|
+
if (authValue !== maxValue) {
|
|
166
|
+
return { isValid: false, invalidReason: "Authorization.value does not match maxAmountRequired" };
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const recovered = verifyAuthorizationSignature(authorization, signature);
|
|
170
|
+
if (recovered.toLowerCase() !== authorization.from.toLowerCase()) {
|
|
171
|
+
return { isValid: false, invalidReason: "Signature does not match authorization.from" };
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
175
|
+
return { isValid: false, invalidReason: `Signature verification failed: ${msg}` };
|
|
176
|
+
}
|
|
177
|
+
return { isValid: true, invalidReason: null };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/routes/verify.ts
|
|
181
|
+
async function verifyRoute(req, res) {
|
|
182
|
+
try {
|
|
183
|
+
const { x402Version, paymentHeader, paymentRequirements } = req.body ?? {};
|
|
184
|
+
const projectKey = req.header("X-Project-Key");
|
|
185
|
+
if (projectKey) {
|
|
186
|
+
console.log(`[billing] Verify request from project: ${projectKey}`);
|
|
187
|
+
}
|
|
188
|
+
if (x402Version !== 1) {
|
|
189
|
+
res.status(400).json({ isValid: false, invalidReason: "Unsupported x402Version" });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (!paymentHeader || !paymentRequirements) {
|
|
193
|
+
res.status(400).json({ isValid: false, invalidReason: "Missing paymentHeader or paymentRequirements" });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const headerObj = decodePaymentHeader(paymentHeader);
|
|
197
|
+
const result = verifyPayment(headerObj, paymentRequirements);
|
|
198
|
+
res.status(200).json(result);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
201
|
+
res.status(500).json({ isValid: false, invalidReason: msg });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/routes/settle.ts
|
|
206
|
+
import { ethers as ethers4 } from "ethers";
|
|
207
|
+
function signatureToVRS(signature) {
|
|
208
|
+
const sig = ethers4.Signature.from(signature);
|
|
209
|
+
return { v: sig.v, r: sig.r, s: sig.s };
|
|
210
|
+
}
|
|
211
|
+
async function settleRoute(req, res) {
|
|
212
|
+
const raw = req.body ?? {};
|
|
213
|
+
try {
|
|
214
|
+
const { x402Version, paymentHeader, paymentRequirements } = raw;
|
|
215
|
+
const projectKey = req.header("X-Project-Key");
|
|
216
|
+
if (projectKey) {
|
|
217
|
+
console.log(`[billing] Settle request from project: ${projectKey}`);
|
|
218
|
+
}
|
|
219
|
+
if (x402Version !== 1) {
|
|
220
|
+
res.status(400).json({
|
|
221
|
+
success: false,
|
|
222
|
+
error: "Unsupported x402Version",
|
|
223
|
+
txHash: null,
|
|
224
|
+
networkId: paymentRequirements?.network
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!paymentHeader || !paymentRequirements) {
|
|
229
|
+
res.status(400).json({
|
|
230
|
+
success: false,
|
|
231
|
+
error: "Missing paymentHeader or paymentRequirements",
|
|
232
|
+
txHash: null,
|
|
233
|
+
networkId: paymentRequirements?.network
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const headerObj = decodePaymentHeader(paymentHeader);
|
|
238
|
+
const verify = verifyPayment(headerObj, paymentRequirements);
|
|
239
|
+
if (!verify.isValid) {
|
|
240
|
+
res.status(400).json({
|
|
241
|
+
success: false,
|
|
242
|
+
error: verify.invalidReason ?? "Invalid payment",
|
|
243
|
+
txHash: null,
|
|
244
|
+
networkId: paymentRequirements.network
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const { authorization, signature } = headerObj.payload;
|
|
249
|
+
const { v, r, s } = signatureToVRS(signature);
|
|
250
|
+
const signer = getFacilitatorSigner();
|
|
251
|
+
const usdc = getUsdcContract(signer);
|
|
252
|
+
const tx = await usdc.transferWithAuthorization(
|
|
253
|
+
authorization.from,
|
|
254
|
+
authorization.to,
|
|
255
|
+
authorization.value,
|
|
256
|
+
authorization.validAfter,
|
|
257
|
+
authorization.validBefore,
|
|
258
|
+
authorization.nonce,
|
|
259
|
+
v,
|
|
260
|
+
r,
|
|
261
|
+
s
|
|
262
|
+
);
|
|
263
|
+
const receipt = await tx.wait();
|
|
264
|
+
if (CONFIG.telemetry && CONFIG.telemetry.endpoint) {
|
|
265
|
+
const telemetryEvent = {
|
|
266
|
+
event: "payment_settled",
|
|
267
|
+
ts: Date.now(),
|
|
268
|
+
projectKey: CONFIG.telemetry.projectKey,
|
|
269
|
+
network: paymentRequirements.network,
|
|
270
|
+
buyer: authorization.from,
|
|
271
|
+
payTo: authorization.to,
|
|
272
|
+
amountAtomic: authorization.value,
|
|
273
|
+
asset: CONFIG.usdcAddress,
|
|
274
|
+
decimals: CONFIG.usdcDecimals,
|
|
275
|
+
nonce: authorization.nonce,
|
|
276
|
+
route: "facilitator_settle",
|
|
277
|
+
// Facilitator doesn't know original route
|
|
278
|
+
// Facilitator metadata
|
|
279
|
+
facilitatorType: "self-hosted",
|
|
280
|
+
facilitatorUrl: void 0,
|
|
281
|
+
// Facilitator doesn't have URL to itself
|
|
282
|
+
facilitatorAddress: signer.address,
|
|
283
|
+
// Get from signer (line 66)
|
|
284
|
+
// Optional metadata
|
|
285
|
+
txHash: receipt?.hash ?? tx.hash,
|
|
286
|
+
priceUsd: paymentRequirements.price
|
|
287
|
+
};
|
|
288
|
+
fetch(CONFIG.telemetry.endpoint, {
|
|
289
|
+
method: "POST",
|
|
290
|
+
headers: {
|
|
291
|
+
"Content-Type": "application/json",
|
|
292
|
+
"Authorization": `Bearer ${CONFIG.telemetry.projectKey}`
|
|
293
|
+
},
|
|
294
|
+
body: JSON.stringify(telemetryEvent)
|
|
295
|
+
}).catch((err) => console.error("[telemetry] Send failed:", err));
|
|
296
|
+
}
|
|
297
|
+
res.status(200).json({
|
|
298
|
+
success: true,
|
|
299
|
+
error: null,
|
|
300
|
+
txHash: receipt?.hash ?? tx.hash,
|
|
301
|
+
networkId: paymentRequirements.network
|
|
302
|
+
});
|
|
303
|
+
} catch (err) {
|
|
304
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
305
|
+
res.status(500).json({
|
|
306
|
+
success: false,
|
|
307
|
+
error: msg,
|
|
308
|
+
txHash: null,
|
|
309
|
+
networkId: raw?.paymentRequirements?.network
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/index.ts
|
|
315
|
+
var app = express();
|
|
316
|
+
app.use(express.json({ limit: "1mb" }));
|
|
317
|
+
app.get("/health", healthRoute);
|
|
318
|
+
app.get("/supported", supportedRoute);
|
|
319
|
+
app.post("/verify", verifyRoute);
|
|
320
|
+
app.post("/settle", settleRoute);
|
|
321
|
+
app.listen(CONFIG.port, () => {
|
|
322
|
+
console.log(`Facilitator server listening on http://localhost:${CONFIG.port}`);
|
|
323
|
+
console.log(`Network: ${CONFIG.networkId} (chainId=${CONFIG.chainId})`);
|
|
324
|
+
});
|
package/template/src/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
import "dotenv/config";
|
|
3
|
+
import { DEFAULT_TELEMETRY_ENDPOINT } from "./constants";
|
|
3
4
|
|
|
4
5
|
function required(name: string): string {
|
|
5
6
|
const v = process.env[name];
|
|
@@ -20,4 +21,12 @@ export const CONFIG = {
|
|
|
20
21
|
facilitatorPrivateKey: required("FACILITATOR_PRIVATE_KEY"),
|
|
21
22
|
|
|
22
23
|
logLevel: process.env.LOG_LEVEL ?? "info",
|
|
24
|
+
|
|
25
|
+
// Telemetry config (opt-in via projectKey)
|
|
26
|
+
telemetry: process.env.TELEMETRY_PROJECT_KEY
|
|
27
|
+
? {
|
|
28
|
+
projectKey: process.env.TELEMETRY_PROJECT_KEY,
|
|
29
|
+
endpoint: DEFAULT_TELEMETRY_ENDPOINT,
|
|
30
|
+
}
|
|
31
|
+
: undefined,
|
|
23
32
|
} as const;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default telemetry endpoint URL.
|
|
5
|
+
*
|
|
6
|
+
* Set to a valid URL when the official analytics backend is ready.
|
|
7
|
+
* If undefined, telemetry will only be sent if TELEMETRY_ENDPOINT env var is set.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_TELEMETRY_ENDPOINT: string | undefined = undefined;
|
|
@@ -3,6 +3,7 @@ import type { Request, Response } from "express";
|
|
|
3
3
|
import { ethers } from "ethers";
|
|
4
4
|
import { decodePaymentHeader, verifyPayment, type PaymentRequirements } from "../x402";
|
|
5
5
|
import { getFacilitatorSigner, getUsdcContract } from "../blockchain";
|
|
6
|
+
import { CONFIG } from "../config";
|
|
6
7
|
|
|
7
8
|
function signatureToVRS(signature: string) {
|
|
8
9
|
const sig = ethers.Signature.from(signature);
|
|
@@ -19,6 +20,12 @@ export async function settleRoute(req: Request, res: Response) {
|
|
|
19
20
|
paymentHeader?: string;
|
|
20
21
|
paymentRequirements?: PaymentRequirements;
|
|
21
22
|
};
|
|
23
|
+
const projectKey = req.header("X-Project-Key"); // Optional: for hosted facilitator billing
|
|
24
|
+
|
|
25
|
+
// Optional: Log for billing/attribution (useful for hosted facilitators)
|
|
26
|
+
if (projectKey) {
|
|
27
|
+
console.log(`[billing] Settle request from project: ${projectKey}`);
|
|
28
|
+
}
|
|
22
29
|
|
|
23
30
|
if (x402Version !== 1) {
|
|
24
31
|
res.status(400).json({
|
|
@@ -71,6 +78,44 @@ export async function settleRoute(req: Request, res: Response) {
|
|
|
71
78
|
|
|
72
79
|
const receipt = await tx.wait();
|
|
73
80
|
|
|
81
|
+
// Send telemetry if configured and endpoint is available
|
|
82
|
+
if (CONFIG.telemetry && CONFIG.telemetry.endpoint) {
|
|
83
|
+
// NOTE: This event structure matches TelemetryEvent from x402-mantle-sdk
|
|
84
|
+
// See: packages/x402-mantle-sdk/src/server/types.ts
|
|
85
|
+
const telemetryEvent = {
|
|
86
|
+
event: "payment_settled" as const,
|
|
87
|
+
ts: Date.now(),
|
|
88
|
+
projectKey: CONFIG.telemetry.projectKey,
|
|
89
|
+
network: paymentRequirements.network,
|
|
90
|
+
buyer: authorization.from,
|
|
91
|
+
payTo: authorization.to,
|
|
92
|
+
amountAtomic: authorization.value,
|
|
93
|
+
asset: CONFIG.usdcAddress,
|
|
94
|
+
decimals: CONFIG.usdcDecimals,
|
|
95
|
+
nonce: authorization.nonce,
|
|
96
|
+
route: "facilitator_settle", // Facilitator doesn't know original route
|
|
97
|
+
|
|
98
|
+
// Facilitator metadata
|
|
99
|
+
facilitatorType: "self-hosted" as const,
|
|
100
|
+
facilitatorUrl: undefined, // Facilitator doesn't have URL to itself
|
|
101
|
+
facilitatorAddress: signer.address, // Get from signer (line 66)
|
|
102
|
+
|
|
103
|
+
// Optional metadata
|
|
104
|
+
txHash: receipt?.hash ?? tx.hash,
|
|
105
|
+
priceUsd: paymentRequirements.price,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Fire-and-forget (don't await - don't delay response)
|
|
109
|
+
fetch(CONFIG.telemetry.endpoint, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: {
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
"Authorization": `Bearer ${CONFIG.telemetry.projectKey}`,
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify(telemetryEvent),
|
|
116
|
+
}).catch(err => console.error("[telemetry] Send failed:", err));
|
|
117
|
+
}
|
|
118
|
+
|
|
74
119
|
res.status(200).json({
|
|
75
120
|
success: true,
|
|
76
121
|
error: null,
|
|
@@ -5,6 +5,12 @@ import { decodePaymentHeader, verifyPayment, type PaymentRequirements } from "..
|
|
|
5
5
|
export async function verifyRoute(req: Request, res: Response) {
|
|
6
6
|
try {
|
|
7
7
|
const { x402Version, paymentHeader, paymentRequirements } = req.body ?? {};
|
|
8
|
+
const projectKey = req.header("X-Project-Key"); // Optional: for hosted facilitator billing
|
|
9
|
+
|
|
10
|
+
// Optional: Log for billing/attribution (useful for hosted facilitators)
|
|
11
|
+
if (projectKey) {
|
|
12
|
+
console.log(`[billing] Verify request from project: ${projectKey}`);
|
|
13
|
+
}
|
|
8
14
|
|
|
9
15
|
if (x402Version !== 1) {
|
|
10
16
|
res.status(400).json({ isValid: false, invalidReason: "Unsupported x402Version" });
|