@warppay402/server 1.0.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 +93 -0
- package/dist/demo.d.ts +1 -0
- package/dist/demo.js +53 -0
- package/dist/examples/basic-monetized-server.d.ts +4 -0
- package/dist/examples/basic-monetized-server.js +25 -0
- package/dist/facilitator.d.ts +1 -0
- package/dist/facilitator.js +118 -0
- package/dist/guard.d.ts +29 -0
- package/dist/guard.js +108 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/mcpWrapper.d.ts +13 -0
- package/dist/mcpWrapper.js +169 -0
- package/dist/monetize.d.ts +38 -0
- package/dist/monetize.js +206 -0
- package/dist/pay_client.d.ts +1 -0
- package/dist/pay_client.js +94 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +25 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @warppay402/server ⚡
|
|
2
|
+
|
|
3
|
+
> Instant x402 V2 monetization SDK and self-hosted infrastructure for Model Context Protocol (MCP) AI tools, Hono HTTP APIs, Cloudflare Monetization Gateway, and Base.
|
|
4
|
+
|
|
5
|
+
`@warppay402/server` allows developers to monetize any MCP tool or HTTP API route in a few lines of code. It automatically generates standard x402 V2 HTTP payment challenges, verifies gasless EIP-712 signatures, enforces platform fee splits, protects against prompt injection threats, and prevents signature replay attacks.
|
|
6
|
+
|
|
7
|
+
Official Site: [https://www.warppay402.com](https://www.warppay402.com)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **x402 V2 Compliant:** Standardized payment challenge headers (`PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE`, `PAYMENT-RESPONSE`).
|
|
14
|
+
- **Base Network Native:** Uses USDC on Base mainnet (`eip155:8453`) by default.
|
|
15
|
+
- **Gasless Off-Chain Signing:** Clients sign `TransferWithAuthorization` payloads without paying gas fees.
|
|
16
|
+
- **Automated Platform Fee Split:** Configurable fee split in basis points (e.g. 50 BPS = 0.5%) routed directly during facilitator settlement.
|
|
17
|
+
- **x402-Guard Security:** Pre-flight middleware protecting endpoints against prompt injection attacks, payload buffer overruns, and high-velocity traffic spikes.
|
|
18
|
+
- **MCP Native Decorator:** Seamlessly wraps tools written for the Model Context Protocol.
|
|
19
|
+
- **Replay Protection:** Includes built-in `MemoryNonceStore` and distributed `RedisNonceStore` drivers to prevent double-spending or signature reuse.
|
|
20
|
+
- **Edge Compatible:** Zero Node.js-only API dependencies; runs seamlessly on Cloudflare Workers, Vercel Edge, Node.js (18+), and Deno.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @warppay402/server
|
|
28
|
+
```
|
|
29
|
+
## Usage Case 1: Monetizing Hono HTTP APIs
|
|
30
|
+
```typescript
|
|
31
|
+
import { Hono } from "hono";
|
|
32
|
+
import { monetize } from "@warppay402/server";
|
|
33
|
+
|
|
34
|
+
const app = new Hono();
|
|
35
|
+
|
|
36
|
+
app.use(
|
|
37
|
+
"/api/data",
|
|
38
|
+
monetize({
|
|
39
|
+
price: "0.01", // $0.01 USDC
|
|
40
|
+
payTo: "0xYourMerchantWalletAddress",
|
|
41
|
+
platformWallet: "0xYourPlatformTreasuryWallet",
|
|
42
|
+
platformFeeBps: 50, // 0.5% cut
|
|
43
|
+
guard: true // Enables x402-guard security scanning
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
app.get("/api/data", (c) => c.json({ status: "success", content: "Monetized Payload" }));
|
|
48
|
+
|
|
49
|
+
export default app;
|
|
50
|
+
```
|
|
51
|
+
## Usage Case 2: Monetizing MCP (Model Context Protocol) Tools
|
|
52
|
+
```typescript
|
|
53
|
+
import { createMonetizedMCPTool } from "@warppay402/server";
|
|
54
|
+
|
|
55
|
+
const originalTool = {
|
|
56
|
+
name: "web_scraper",
|
|
57
|
+
description: "Extract clean Markdown content from URLs",
|
|
58
|
+
inputSchema: { type: "object", properties: { url: { type: "string" } } },
|
|
59
|
+
handler: async ({ url }: { url: string }) => {
|
|
60
|
+
return { markdown: "# Cleaned Markdown Content" };
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const monetizedTool = createMonetizedMCPTool(originalTool, {
|
|
65
|
+
price: "0.02", // $0.02 USDC
|
|
66
|
+
payTo: "0xYourMerchantWalletAddress",
|
|
67
|
+
platformWallet: "0xYourPlatformTreasuryWallet",
|
|
68
|
+
platformFeeBps: 50
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage Case 3: Distributed Nonce Locking with Redis / Upstash
|
|
73
|
+
```typescript
|
|
74
|
+
import { Redis } from "@upstash/redis";
|
|
75
|
+
import { monetize, RedisNonceStore } from "@warppay402/server";
|
|
76
|
+
|
|
77
|
+
const redis = new Redis({
|
|
78
|
+
url: process.env.UPSTASH_REDIS_REST_URL!,
|
|
79
|
+
token: process.env.UPSTASH_REDIS_REST_TOKEN!
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
app.use(
|
|
83
|
+
"/api/v1/compute",
|
|
84
|
+
monetize({
|
|
85
|
+
price: "0.10",
|
|
86
|
+
payTo: "0xYourMerchantWalletAddress",
|
|
87
|
+
platformWallet: "0xYourPlatformTreasuryWallet",
|
|
88
|
+
nonceStore: new RedisNonceStore(redis),
|
|
89
|
+
nonceTtlSeconds: 300
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
## License: MIT © WarpPay402
|
package/dist/demo.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/demo.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { monetize, createMonetizedMCPTool } from "./index";
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// 1. Test Web API Middleware
|
|
5
|
+
// ============================================================================
|
|
6
|
+
const app = new Hono();
|
|
7
|
+
app.use("/api/weather", monetize({
|
|
8
|
+
price: "0.05",
|
|
9
|
+
payTo: "0x1111111111111111111111111111111111111111",
|
|
10
|
+
platformFeeBps: 50
|
|
11
|
+
}));
|
|
12
|
+
app.get("/api/weather", (c) => {
|
|
13
|
+
return c.json({ city: "Austin", temperature: "78F", condition: "Sunny" });
|
|
14
|
+
});
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// 2. Test MCP Tool Decorator
|
|
17
|
+
// ============================================================================
|
|
18
|
+
const monetizedSearchTool = createMonetizedMCPTool({
|
|
19
|
+
name: "web_search",
|
|
20
|
+
description: "Perform deep web searches for AI agents",
|
|
21
|
+
inputSchema: { type: "object" },
|
|
22
|
+
handler: async (args) => {
|
|
23
|
+
return { status: "success", data: `Search results for: ${args.query}` };
|
|
24
|
+
}
|
|
25
|
+
}, {
|
|
26
|
+
price: "0.10",
|
|
27
|
+
payTo: "0x1111111111111111111111111111111111111111",
|
|
28
|
+
platformFeeBps: 50
|
|
29
|
+
});
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// 3. Execution Simulation
|
|
32
|
+
// ============================================================================
|
|
33
|
+
async function runLocalVerification() {
|
|
34
|
+
console.log("==================================================");
|
|
35
|
+
console.log("1. TESTING HTTP API MIDDLEWARE WITHOUT PAYMENT");
|
|
36
|
+
console.log("==================================================");
|
|
37
|
+
const unauthenticatedRes = await app.request("http://localhost/api/weather");
|
|
38
|
+
console.log("HTTP Status Code:", unauthenticatedRes.status, "(Expected: 402)");
|
|
39
|
+
console.log("PAYMENT-REQUIRED Header:", unauthenticatedRes.headers.get("PAYMENT-REQUIRED") ? "RECEIVED ✓" : "MISSING ✗");
|
|
40
|
+
const challengeBody = await unauthenticatedRes.json();
|
|
41
|
+
console.log("Challenge Payload:", JSON.stringify(challengeBody, null, 2));
|
|
42
|
+
console.log("\n==================================================");
|
|
43
|
+
console.log("2. TESTING MCP TOOL DECORATOR WITHOUT PAYMENT");
|
|
44
|
+
console.log("==================================================");
|
|
45
|
+
try {
|
|
46
|
+
await monetizedSearchTool.handler({ query: "Base network x402" }, {});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.log("Caught Expected MCP 402 Error ✓");
|
|
50
|
+
console.log("MCP Error Payload:", JSON.parse(err.message));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
runLocalVerification();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import { monetize } from "../index.js";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
// Define the developer's wallet address (where 99.5% of funds go)
|
|
7
|
+
const developerWallet = "0x2bd4e0ea72e21155ec41f8613eafd433193c4d8b";
|
|
8
|
+
app.use("/api/weather", monetize({
|
|
9
|
+
price: "0.01",
|
|
10
|
+
payTo: developerWallet,
|
|
11
|
+
platformWallet: "0x2bd4e0ea72e21155ec41f8613eafd433193c4d8b",
|
|
12
|
+
platformFeeBps: 50,
|
|
13
|
+
facilitatorUrl: "https://warppay402.com"
|
|
14
|
+
}));
|
|
15
|
+
app.get("/api/weather", (c) => {
|
|
16
|
+
return c.json({
|
|
17
|
+
status: "success",
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
data: { city: "Austin", temperature: "78°F", condition: "Sunny" }
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
serve({ fetch: app.fetch, port: 3000 }, (info) => {
|
|
23
|
+
console.log(`[API Server] Running on http://localhost:${info.port}`);
|
|
24
|
+
});
|
|
25
|
+
export default app;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { createWalletClient, http, parseAbi, verifyTypedData } from "viem";
|
|
5
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
6
|
+
import { base } from "viem/chains";
|
|
7
|
+
const app = new Hono();
|
|
8
|
+
// Log every incoming request URL to debug the exact path hit by the SDK
|
|
9
|
+
app.use("*", async (c, next) => {
|
|
10
|
+
console.log(`[Facilitator Debug] Incoming ${c.req.method} request to path: ${c.req.path}`);
|
|
11
|
+
await next();
|
|
12
|
+
});
|
|
13
|
+
const FACILITATOR_PRIVATE_KEY = process.env.FACILITATOR_PRIVATE_KEY;
|
|
14
|
+
if (!FACILITATOR_PRIVATE_KEY) {
|
|
15
|
+
console.error("Please set FACILITATOR_PRIVATE_KEY with a Base gas-holding key.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const account = privateKeyToAccount(FACILITATOR_PRIVATE_KEY);
|
|
19
|
+
const walletClient = createWalletClient({
|
|
20
|
+
account,
|
|
21
|
+
chain: base,
|
|
22
|
+
transport: http(process.env.RPC_URL || "https://mainnet.base.org"),
|
|
23
|
+
});
|
|
24
|
+
const usdcAbi = parseAbi([
|
|
25
|
+
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external",
|
|
26
|
+
]);
|
|
27
|
+
// Shared handler logic for signature verification and on-chain broadcasting
|
|
28
|
+
const handleSettle = async (c) => {
|
|
29
|
+
try {
|
|
30
|
+
const body = await c.req.json();
|
|
31
|
+
// Extract authorization & signature regardless of payload nesting depth
|
|
32
|
+
const authorization = body.authorization ||
|
|
33
|
+
body.payload?.authorization ||
|
|
34
|
+
body.paymentPayload?.authorization ||
|
|
35
|
+
body.paymentPayload?.payload?.authorization ||
|
|
36
|
+
body.payload?.payload?.authorization;
|
|
37
|
+
const signature = body.signature ||
|
|
38
|
+
body.payload?.signature ||
|
|
39
|
+
body.paymentPayload?.signature ||
|
|
40
|
+
body.paymentPayload?.payload?.signature ||
|
|
41
|
+
body.payload?.payload?.signature;
|
|
42
|
+
if (!signature || !authorization) {
|
|
43
|
+
console.log("[Facilitator Debug] Received Body:", JSON.stringify(body, null, 2));
|
|
44
|
+
return c.json({ success: false, error: "Missing signature or authorization payload" }, 400);
|
|
45
|
+
}
|
|
46
|
+
// 1. Verify EIP-712 Signature Off-Chain
|
|
47
|
+
const domain = {
|
|
48
|
+
name: "USD Coin",
|
|
49
|
+
version: "2",
|
|
50
|
+
chainId: 8453,
|
|
51
|
+
verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
52
|
+
};
|
|
53
|
+
const types = {
|
|
54
|
+
TransferWithAuthorization: [
|
|
55
|
+
{ name: "from", type: "address" },
|
|
56
|
+
{ name: "to", type: "address" },
|
|
57
|
+
{ name: "value", type: "uint256" },
|
|
58
|
+
{ name: "validAfter", type: "uint256" },
|
|
59
|
+
{ name: "validBefore", type: "uint256" },
|
|
60
|
+
{ name: "nonce", type: "bytes32" },
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
const isValid = await verifyTypedData({
|
|
64
|
+
address: authorization.from,
|
|
65
|
+
domain,
|
|
66
|
+
types,
|
|
67
|
+
primaryType: "TransferWithAuthorization",
|
|
68
|
+
message: {
|
|
69
|
+
from: authorization.from,
|
|
70
|
+
to: authorization.to,
|
|
71
|
+
value: BigInt(authorization.value),
|
|
72
|
+
validAfter: BigInt(authorization.validAfter),
|
|
73
|
+
validBefore: BigInt(authorization.validBefore),
|
|
74
|
+
nonce: authorization.nonce,
|
|
75
|
+
},
|
|
76
|
+
signature,
|
|
77
|
+
});
|
|
78
|
+
if (!isValid) {
|
|
79
|
+
return c.json({ success: false, error: "Invalid EIP-712 signature" }, 402);
|
|
80
|
+
}
|
|
81
|
+
// 2. Extract v, r, s
|
|
82
|
+
const r = `0x${signature.slice(2, 66)}`;
|
|
83
|
+
const s = `0x${signature.slice(66, 130)}`;
|
|
84
|
+
const v = parseInt(signature.slice(130, 132), 16);
|
|
85
|
+
// 3. Broadcast to Base Mainnet using transferWithAuthorization
|
|
86
|
+
const txHash = await walletClient.writeContract({
|
|
87
|
+
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
88
|
+
abi: usdcAbi,
|
|
89
|
+
functionName: "transferWithAuthorization",
|
|
90
|
+
args: [
|
|
91
|
+
authorization.from,
|
|
92
|
+
authorization.to,
|
|
93
|
+
BigInt(authorization.value),
|
|
94
|
+
BigInt(authorization.validAfter),
|
|
95
|
+
BigInt(authorization.validBefore),
|
|
96
|
+
authorization.nonce,
|
|
97
|
+
v,
|
|
98
|
+
r,
|
|
99
|
+
s,
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
return c.json({
|
|
103
|
+
success: true,
|
|
104
|
+
txHash,
|
|
105
|
+
network: "eip155:8453",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return c.json({ success: false, error: err.message }, 500);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
// Listen on all standard routes to prevent 404 mismatch
|
|
113
|
+
app.post("/", handleSettle);
|
|
114
|
+
app.post("/settle", handleSettle);
|
|
115
|
+
app.post("/verify", handleSettle);
|
|
116
|
+
serve({ fetch: app.fetch, port: 3001 }, (info) => {
|
|
117
|
+
console.log(`[Self-Hosted Facilitator] Running on http://localhost:${info.port}`);
|
|
118
|
+
});
|
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Context, Next } from "hono";
|
|
2
|
+
export interface GuardOptions {
|
|
3
|
+
/** Maximum requests allowed within the window per IP/Wallet */
|
|
4
|
+
rateLimitMax?: number;
|
|
5
|
+
/** Rate limit window in milliseconds (default: 60000 / 1 minute) */
|
|
6
|
+
rateLimitWindowMs?: number;
|
|
7
|
+
/** Custom forbidden keywords/patterns for prompt injection scanning */
|
|
8
|
+
customBlockedPatterns?: RegExp[];
|
|
9
|
+
/** Maximum length allowed for input strings (prevents buffer overload) */
|
|
10
|
+
maxPayloadLength?: number;
|
|
11
|
+
/** Action on detected threat: 'block' (422) or 'log-only' */
|
|
12
|
+
mode?: "block" | "log-only";
|
|
13
|
+
}
|
|
14
|
+
export interface GuardAuditResult {
|
|
15
|
+
passed: boolean;
|
|
16
|
+
reason?: string;
|
|
17
|
+
threatType?: "PROMPT_INJECTION" | "VELOCITY_EXCEEDED" | "PAYLOAD_TOO_LARGE";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Scans string inputs recursively within a JSON request body
|
|
21
|
+
*/
|
|
22
|
+
export declare function scanPayloadForInjection(payload: unknown, customPatterns?: RegExp[]): {
|
|
23
|
+
safe: boolean;
|
|
24
|
+
matchedPattern?: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Pre-flight Hono Middleware for x402-guard
|
|
28
|
+
*/
|
|
29
|
+
export declare function x402Guard(options?: GuardOptions): (c: Context, next: Next) => Promise<Response | void>;
|
package/dist/guard.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in heuristic signatures for common indirect prompt injection attacks
|
|
3
|
+
*/
|
|
4
|
+
const DEFAULT_INJECTION_PATTERNS = [
|
|
5
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
6
|
+
/override\s+(system\s+)?prompt/i,
|
|
7
|
+
/system:\s*you\s+must/i,
|
|
8
|
+
/bypass\s+(payment|verification|guard|x402)/i,
|
|
9
|
+
/pay\s+(maximum|all|unlimited)\s+(usdc|funds|balance)/i,
|
|
10
|
+
/repeat\s+this\s+request\s+infinitely/i,
|
|
11
|
+
/<script[\s\S]*?>[\s\S]*?<\/script>/i,
|
|
12
|
+
/eval\(.*?\)/i,
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* In-memory sliding window store for velocity protection
|
|
16
|
+
*/
|
|
17
|
+
class VelocityStore {
|
|
18
|
+
requests = new Map();
|
|
19
|
+
isExceeded(key, limit, windowMs) {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const timestamps = this.requests.get(key) || [];
|
|
22
|
+
const validTimestamps = timestamps.filter((time) => now - time < windowMs);
|
|
23
|
+
if (validTimestamps.length >= limit) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
validTimestamps.push(now);
|
|
27
|
+
this.requests.set(key, validTimestamps);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const velocityStore = new VelocityStore();
|
|
32
|
+
/**
|
|
33
|
+
* Scans string inputs recursively within a JSON request body
|
|
34
|
+
*/
|
|
35
|
+
export function scanPayloadForInjection(payload, customPatterns = []) {
|
|
36
|
+
const patterns = [...DEFAULT_INJECTION_PATTERNS, ...customPatterns];
|
|
37
|
+
const inspectValue = (val) => {
|
|
38
|
+
if (typeof val === "string") {
|
|
39
|
+
for (const pattern of patterns) {
|
|
40
|
+
if (pattern.test(val)) {
|
|
41
|
+
return { safe: false, matchedPattern: pattern.toString() };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (typeof val === "object" && val !== null) {
|
|
46
|
+
for (const key of Object.keys(val)) {
|
|
47
|
+
const result = inspectValue(val[key]);
|
|
48
|
+
if (!result.safe)
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { safe: true };
|
|
53
|
+
};
|
|
54
|
+
return inspectValue(payload);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pre-flight Hono Middleware for x402-guard
|
|
58
|
+
*/
|
|
59
|
+
export function x402Guard(options = {}) {
|
|
60
|
+
const { rateLimitMax = 20, rateLimitWindowMs = 60000, customBlockedPatterns = [], maxPayloadLength = 50000, mode = "block", } = options;
|
|
61
|
+
return async (c, next) => {
|
|
62
|
+
const clientKey = c.req.header("x-forwarded-for") || "unknown-client";
|
|
63
|
+
let body = {};
|
|
64
|
+
try {
|
|
65
|
+
body = await c.req.json();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Body parsing optional if request has no body
|
|
69
|
+
}
|
|
70
|
+
// 1. Payload Size Check
|
|
71
|
+
const rawBody = JSON.stringify(body || {});
|
|
72
|
+
if (rawBody.length > maxPayloadLength) {
|
|
73
|
+
if (mode === "block") {
|
|
74
|
+
c.header("X-Guard-Blocked", "PAYLOAD_TOO_LARGE");
|
|
75
|
+
return c.json({
|
|
76
|
+
error: "Payload Too Large",
|
|
77
|
+
message: `Request body exceeds maximum safe size of ${maxPayloadLength} characters.`,
|
|
78
|
+
code: "PAYLOAD_TOO_LARGE",
|
|
79
|
+
}, 413);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// 2. Velocity Check
|
|
83
|
+
if (velocityStore.isExceeded(clientKey, rateLimitMax, rateLimitWindowMs)) {
|
|
84
|
+
c.header("X-Guard-Blocked", "VELOCITY_EXCEEDED");
|
|
85
|
+
if (mode === "block") {
|
|
86
|
+
return c.json({
|
|
87
|
+
error: "Rate Limit Exceeded",
|
|
88
|
+
message: "Anomalous transaction velocity detected. Request blocked before x402 payment.",
|
|
89
|
+
code: "VELOCITY_EXCEEDED",
|
|
90
|
+
}, 429);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// 3. Prompt Injection Pre-Flight Scan
|
|
94
|
+
const scanResult = scanPayloadForInjection(body, customBlockedPatterns);
|
|
95
|
+
if (!scanResult.safe) {
|
|
96
|
+
c.header("X-Guard-Blocked", "PROMPT_INJECTION");
|
|
97
|
+
if (mode === "block") {
|
|
98
|
+
return c.json({
|
|
99
|
+
error: "Security Violation",
|
|
100
|
+
message: "Potential prompt injection or payload manipulation detected in request body.",
|
|
101
|
+
code: "PROMPT_INJECTION_DETECTED",
|
|
102
|
+
}, 422);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
c.header("X-Guard-Inspected", "true");
|
|
106
|
+
await next();
|
|
107
|
+
};
|
|
108
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { monetize, MemoryNonceStore, RedisNonceStore, type MonetizeOptions, type NonceStore } from "./monetize";
|
|
2
|
+
export { createMonetizedMCPTool, type MCPToolDefinition } from "./mcpWrapper";
|
|
3
|
+
export { x402Guard, scanPayloadForInjection } from "./guard.js";
|
|
4
|
+
export type { GuardOptions } from "./guard.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MonetizeOptions } from "./monetize";
|
|
2
|
+
export interface MCPToolDefinition {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: Record<string, any>;
|
|
6
|
+
handler: (args: any, extraContext?: any) => Promise<any>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createMonetizedMCPTool(tool: MCPToolDefinition, options: MonetizeOptions): {
|
|
9
|
+
name: string;
|
|
10
|
+
inputSchema: Record<string, any>;
|
|
11
|
+
description: string;
|
|
12
|
+
handler(args: any, context: any): Promise<any>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { MemoryNonceStore } from "./monetize";
|
|
2
|
+
const DEFAULT_USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
3
|
+
const DEFAULT_NETWORK = "eip155:8453";
|
|
4
|
+
const DEFAULT_FACILITATOR = "https://warppay402.com";
|
|
5
|
+
const DEFAULT_PLATFORM_WALLET = "0x2bd4e0ea72e21155ec41f8613eafd433193c4d8b";
|
|
6
|
+
const defaultMemoryStore = new MemoryNonceStore();
|
|
7
|
+
function parseTokenUnits(priceStr) {
|
|
8
|
+
const normalized = String(priceStr).trim();
|
|
9
|
+
if (!normalized || isNaN(Number(normalized)) || Number(normalized) <= 0) {
|
|
10
|
+
throw new Error(`[Monetize] Invalid price option: "${priceStr}". Must be a positive decimal number.`);
|
|
11
|
+
}
|
|
12
|
+
const [whole, fraction = ""] = normalized.split(".");
|
|
13
|
+
const paddedFraction = fraction.padEnd(6, "0").slice(0, 6);
|
|
14
|
+
return BigInt(whole + paddedFraction);
|
|
15
|
+
}
|
|
16
|
+
async function hashSignature(signature) {
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const data = encoder.encode(signature);
|
|
19
|
+
const cryptoObj = globalThis.crypto;
|
|
20
|
+
if (!cryptoObj?.subtle) {
|
|
21
|
+
throw new Error("[Monetize] Web Crypto API (crypto.subtle) is unavailable in this environment.");
|
|
22
|
+
}
|
|
23
|
+
const hashBuffer = await cryptoObj.subtle.digest("SHA-256", data);
|
|
24
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
25
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
}
|
|
27
|
+
export function createMonetizedMCPTool(tool, options) {
|
|
28
|
+
// Validate EVM wallet addresses
|
|
29
|
+
if (!options.payTo || !options.payTo.startsWith("0x") || options.payTo.length !== 42) {
|
|
30
|
+
throw new Error("[Monetize] A valid 42-character EVM wallet address is required for 'payTo'.");
|
|
31
|
+
}
|
|
32
|
+
const asset = options.asset || DEFAULT_USDC_BASE;
|
|
33
|
+
const network = options.network || DEFAULT_NETWORK;
|
|
34
|
+
const facilitatorUrl = options.facilitatorUrl || DEFAULT_FACILITATOR;
|
|
35
|
+
const platformFeeBps = options.platformFeeBps ?? 50;
|
|
36
|
+
const platformWallet = options.platformWallet || DEFAULT_PLATFORM_WALLET;
|
|
37
|
+
const nonceStore = options.nonceStore || defaultMemoryStore;
|
|
38
|
+
const nonceTtlSeconds = options.nonceTtlSeconds ?? 300;
|
|
39
|
+
const timeoutMs = options.timeoutMs ?? 8000;
|
|
40
|
+
if (platformFeeBps > 0 && (!platformWallet || !platformWallet.startsWith("0x") || platformWallet.length !== 42)) {
|
|
41
|
+
throw new Error("[Monetize] A valid EVM 'platformWallet' address is required when platformFeeBps > 0.");
|
|
42
|
+
}
|
|
43
|
+
const baseUnits = parseTokenUnits(options.price);
|
|
44
|
+
const platformFeeUnits = (baseUnits * BigInt(platformFeeBps)) / BigInt(10_000);
|
|
45
|
+
const merchantUnits = baseUnits - platformFeeUnits;
|
|
46
|
+
return {
|
|
47
|
+
...tool,
|
|
48
|
+
description: `${tool.description} [Requires Payment: $${options.price} USDC]`,
|
|
49
|
+
async handler(args, context) {
|
|
50
|
+
// Extract payment headers passed via MCP JSON-RPC request context
|
|
51
|
+
const headers = context?.requestHeaders || context?.headers || {};
|
|
52
|
+
const paymentSignature = headers["payment-signature"] || headers["PAYMENT-SIGNATURE"];
|
|
53
|
+
const targetUrl = context?.requestUrl || `mcp://tools/${tool.name}`;
|
|
54
|
+
// STEP 1: If no signature is provided, throw standard 402 challenge
|
|
55
|
+
if (!paymentSignature) {
|
|
56
|
+
const challengePayload = {
|
|
57
|
+
code: 402,
|
|
58
|
+
message: "HTTP 402 Payment Required",
|
|
59
|
+
x402Challenge: {
|
|
60
|
+
x402Version: 2,
|
|
61
|
+
resource: {
|
|
62
|
+
url: targetUrl,
|
|
63
|
+
description: options.description || tool.description,
|
|
64
|
+
mimeType: "application/json"
|
|
65
|
+
},
|
|
66
|
+
accepts: [
|
|
67
|
+
{
|
|
68
|
+
scheme: "exact",
|
|
69
|
+
network,
|
|
70
|
+
amount: merchantUnits.toString(),
|
|
71
|
+
asset,
|
|
72
|
+
payTo: options.payTo,
|
|
73
|
+
maxTimeoutSeconds: nonceTtlSeconds,
|
|
74
|
+
extra: {
|
|
75
|
+
platformFee: platformFeeUnits.toString(),
|
|
76
|
+
platformWallet: platformWallet
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
throw new Error(JSON.stringify(challengePayload));
|
|
83
|
+
}
|
|
84
|
+
// STEP 2: Format Validation
|
|
85
|
+
if (paymentSignature.length < 10) {
|
|
86
|
+
throw new Error(JSON.stringify({ code: 400, message: "Invalid PAYMENT-SIGNATURE format" }));
|
|
87
|
+
}
|
|
88
|
+
// STEP 3: Nonce Check - Prevent Replay Attacks across MCP calls
|
|
89
|
+
const signatureHash = await hashSignature(paymentSignature);
|
|
90
|
+
const isUnique = await nonceStore.claim(signatureHash, nonceTtlSeconds);
|
|
91
|
+
if (!isUnique) {
|
|
92
|
+
throw new Error(JSON.stringify({
|
|
93
|
+
code: 409,
|
|
94
|
+
message: "Replay attack detected. PAYMENT-SIGNATURE has already been used."
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
// STEP 4: Settle via Facilitator
|
|
98
|
+
try {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
101
|
+
let parsedPayload = {};
|
|
102
|
+
try {
|
|
103
|
+
const decodedString = atob(paymentSignature);
|
|
104
|
+
parsedPayload = JSON.parse(decodedString);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
await nonceStore.release(signatureHash);
|
|
108
|
+
throw new Error(JSON.stringify({ code: 400, message: "Invalid base64 payload in PAYMENT-SIGNATURE" }));
|
|
109
|
+
}
|
|
110
|
+
const settleResponse = await fetch(`${facilitatorUrl}/settle`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
signal: controller.signal,
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
paymentPayload: parsedPayload.paymentPayload || parsedPayload.payload,
|
|
116
|
+
paymentRequirements: parsedPayload.paymentRequirements || {
|
|
117
|
+
scheme: "exact",
|
|
118
|
+
network,
|
|
119
|
+
amount: merchantUnits.toString(),
|
|
120
|
+
asset,
|
|
121
|
+
payTo: options.payTo
|
|
122
|
+
},
|
|
123
|
+
resource: { url: targetUrl }
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
clearTimeout(timeoutId);
|
|
127
|
+
const rawText = await settleResponse.text();
|
|
128
|
+
let settlement = {};
|
|
129
|
+
try {
|
|
130
|
+
settlement = JSON.parse(rawText);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
await nonceStore.release(signatureHash);
|
|
134
|
+
throw new Error(JSON.stringify({ code: 502, message: "Facilitator returned invalid non-JSON response" }));
|
|
135
|
+
}
|
|
136
|
+
if (!settleResponse.ok || !settlement.success) {
|
|
137
|
+
await nonceStore.release(signatureHash);
|
|
138
|
+
throw new Error(JSON.stringify({
|
|
139
|
+
code: 402,
|
|
140
|
+
message: "Payment verification failed",
|
|
141
|
+
details: settlement.error
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
// STEP 5: Payment verified and settled -> Execute tool handler
|
|
145
|
+
const result = await tool.handler(args, context);
|
|
146
|
+
// Append settlement metadata to response if result is an object
|
|
147
|
+
if (typeof result === "object" && result !== null) {
|
|
148
|
+
return {
|
|
149
|
+
...result,
|
|
150
|
+
_x402Settlement: settlement
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
await nonceStore.release(signatureHash);
|
|
157
|
+
if (err.message?.startsWith("{")) {
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
const isTimeout = err.name === "AbortError";
|
|
161
|
+
throw new Error(JSON.stringify({
|
|
162
|
+
code: 504,
|
|
163
|
+
message: isTimeout ? "Facilitator settlement timed out" : "Facilitator network error during settlement",
|
|
164
|
+
details: err.message
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Context, Next } from "hono";
|
|
2
|
+
import { GuardOptions } from "./guard.js";
|
|
3
|
+
export interface NonceStore {
|
|
4
|
+
claim(key: string, ttlSeconds: number): Promise<boolean>;
|
|
5
|
+
release(key: string): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare class MemoryNonceStore implements NonceStore {
|
|
8
|
+
private cache;
|
|
9
|
+
claim(key: string, ttlSeconds: number): Promise<boolean>;
|
|
10
|
+
release(key: string): Promise<void>;
|
|
11
|
+
private cleanup;
|
|
12
|
+
}
|
|
13
|
+
export declare class RedisNonceStore implements NonceStore {
|
|
14
|
+
private redisClient;
|
|
15
|
+
private keyPrefix;
|
|
16
|
+
constructor(redisClient: {
|
|
17
|
+
set: (key: string, value: string, ...args: any[]) => Promise<any>;
|
|
18
|
+
del: (key: string) => Promise<any>;
|
|
19
|
+
}, keyPrefix?: string);
|
|
20
|
+
claim(key: string, ttlSeconds: number): Promise<boolean>;
|
|
21
|
+
release(key: string): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface MonetizeOptions {
|
|
24
|
+
price: string;
|
|
25
|
+
payTo: string;
|
|
26
|
+
asset?: string;
|
|
27
|
+
network?: string;
|
|
28
|
+
platformFeeBps?: number;
|
|
29
|
+
platformWallet?: string;
|
|
30
|
+
facilitatorUrl?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
nonceStore?: NonceStore;
|
|
33
|
+
nonceTtlSeconds?: number;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
enableCorsHeaders?: boolean;
|
|
36
|
+
guard?: boolean | GuardOptions;
|
|
37
|
+
}
|
|
38
|
+
export declare function monetize(options: MonetizeOptions): (c: Context, next: Next) => Promise<void | Response>;
|
package/dist/monetize.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { x402Guard } from "./guard.js";
|
|
2
|
+
async function hashSignature(signature) {
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
const data = encoder.encode(signature);
|
|
5
|
+
const cryptoObj = globalThis.crypto;
|
|
6
|
+
if (!cryptoObj?.subtle) {
|
|
7
|
+
throw new Error("[Monetize] Web Crypto API (crypto.subtle) is unavailable in this environment.");
|
|
8
|
+
}
|
|
9
|
+
const hashBuffer = await cryptoObj.subtle.digest("SHA-256", data);
|
|
10
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
11
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
12
|
+
}
|
|
13
|
+
export class MemoryNonceStore {
|
|
14
|
+
cache = new Map();
|
|
15
|
+
async claim(key, ttlSeconds) {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const expiry = this.cache.get(key);
|
|
18
|
+
if (expiry !== undefined && expiry > now) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
this.cache.set(key, now + ttlSeconds * 1000);
|
|
22
|
+
this.cleanup(now);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
async release(key) {
|
|
26
|
+
this.cache.delete(key);
|
|
27
|
+
}
|
|
28
|
+
cleanup(now) {
|
|
29
|
+
if (this.cache.size > 2_000) {
|
|
30
|
+
for (const [k, exp] of this.cache.entries()) {
|
|
31
|
+
if (exp <= now)
|
|
32
|
+
this.cache.delete(k);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class RedisNonceStore {
|
|
38
|
+
redisClient;
|
|
39
|
+
keyPrefix;
|
|
40
|
+
constructor(redisClient, keyPrefix = "x402:nonce:") {
|
|
41
|
+
this.redisClient = redisClient;
|
|
42
|
+
this.keyPrefix = keyPrefix;
|
|
43
|
+
}
|
|
44
|
+
async claim(key, ttlSeconds) {
|
|
45
|
+
const redisKey = `${this.keyPrefix}${key}`;
|
|
46
|
+
const result = await this.redisClient.set(redisKey, "1", "EX", ttlSeconds, "NX");
|
|
47
|
+
return result === "OK" || result === 1 || result === true;
|
|
48
|
+
}
|
|
49
|
+
async release(key) {
|
|
50
|
+
const redisKey = `${this.keyPrefix}${key}`;
|
|
51
|
+
await this.redisClient.del(redisKey);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const defaultMemoryStore = new MemoryNonceStore();
|
|
55
|
+
const DEFAULT_USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
56
|
+
const DEFAULT_NETWORK = "eip155:8453";
|
|
57
|
+
const DEFAULT_FACILITATOR = "https://warppay402.com";
|
|
58
|
+
const DEFAULT_PLATFORM_WALLET = "0x2bd4e0ea72e21155ec41f8613eafd433193c4d8b";
|
|
59
|
+
function toBase64(str) {
|
|
60
|
+
const bytes = new TextEncoder().encode(str);
|
|
61
|
+
let binary = "";
|
|
62
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
63
|
+
binary += String.fromCharCode(bytes[i]);
|
|
64
|
+
}
|
|
65
|
+
return btoa(binary);
|
|
66
|
+
}
|
|
67
|
+
function parseTokenUnits(priceStr) {
|
|
68
|
+
const normalized = String(priceStr).trim();
|
|
69
|
+
if (!normalized || isNaN(Number(normalized)) || Number(normalized) <= 0) {
|
|
70
|
+
throw new Error(`[Monetize] Invalid price option: "${priceStr}". Must be a positive decimal number.`);
|
|
71
|
+
}
|
|
72
|
+
const [whole, fraction = ""] = normalized.split(".");
|
|
73
|
+
const paddedFraction = fraction.padEnd(6, "0").slice(0, 6);
|
|
74
|
+
return BigInt(whole + paddedFraction);
|
|
75
|
+
}
|
|
76
|
+
export function monetize(options) {
|
|
77
|
+
if (!options.payTo || !options.payTo.startsWith("0x") || options.payTo.length !== 42) {
|
|
78
|
+
throw new Error("[Monetize] A valid 42-character EVM wallet address is required for 'payTo'.");
|
|
79
|
+
}
|
|
80
|
+
const asset = options.asset || DEFAULT_USDC_BASE;
|
|
81
|
+
const network = options.network || DEFAULT_NETWORK;
|
|
82
|
+
const facilitatorUrl = options.facilitatorUrl || DEFAULT_FACILITATOR;
|
|
83
|
+
const platformFeeBps = options.platformFeeBps ?? 50;
|
|
84
|
+
const platformWallet = options.platformWallet || DEFAULT_PLATFORM_WALLET;
|
|
85
|
+
const nonceStore = options.nonceStore || defaultMemoryStore;
|
|
86
|
+
const nonceTtlSeconds = options.nonceTtlSeconds ?? 300;
|
|
87
|
+
const timeoutMs = options.timeoutMs ?? 8000;
|
|
88
|
+
const enableCors = options.enableCorsHeaders ?? true;
|
|
89
|
+
if (platformFeeBps > 0 && (!platformWallet || !platformWallet.startsWith("0x") || platformWallet.length !== 42)) {
|
|
90
|
+
throw new Error("[Monetize] A valid EVM 'platformWallet' address is required when platformFeeBps > 0.");
|
|
91
|
+
}
|
|
92
|
+
const baseUnits = parseTokenUnits(options.price);
|
|
93
|
+
const platformFeeUnits = (baseUnits * BigInt(platformFeeBps)) / BigInt(10_000);
|
|
94
|
+
const merchantUnits = baseUnits - platformFeeUnits;
|
|
95
|
+
const guardMiddleware = options.guard
|
|
96
|
+
? x402Guard(typeof options.guard === "object" ? options.guard : {})
|
|
97
|
+
: null;
|
|
98
|
+
return async (c, next) => {
|
|
99
|
+
if (guardMiddleware) {
|
|
100
|
+
let guardPassed = false;
|
|
101
|
+
const guardRes = await guardMiddleware(c, async () => {
|
|
102
|
+
guardPassed = true;
|
|
103
|
+
});
|
|
104
|
+
if (!guardPassed) {
|
|
105
|
+
return guardRes;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (enableCors) {
|
|
109
|
+
c.header("Access-Control-Expose-Headers", "PAYMENT-REQUIRED, PAYMENT-RESPONSE");
|
|
110
|
+
}
|
|
111
|
+
const paymentSignature = c.req.header("payment-signature") || c.req.header("PAYMENT-SIGNATURE");
|
|
112
|
+
const targetUrl = c.req.url;
|
|
113
|
+
// STEP 1: Return HTTP 402 Challenge if signature is missing
|
|
114
|
+
if (!paymentSignature) {
|
|
115
|
+
const challengePayload = {
|
|
116
|
+
x402Version: 2,
|
|
117
|
+
resource: {
|
|
118
|
+
url: targetUrl,
|
|
119
|
+
description: options.description || "Monetized API Access",
|
|
120
|
+
mimeType: "application/json"
|
|
121
|
+
},
|
|
122
|
+
accepts: [
|
|
123
|
+
{
|
|
124
|
+
scheme: "exact",
|
|
125
|
+
network,
|
|
126
|
+
amount: merchantUnits.toString(),
|
|
127
|
+
asset,
|
|
128
|
+
payTo: options.payTo,
|
|
129
|
+
maxTimeoutSeconds: nonceTtlSeconds,
|
|
130
|
+
extra: {
|
|
131
|
+
platformFee: platformFeeUnits.toString(),
|
|
132
|
+
platformWallet: platformWallet
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
c.header("PAYMENT-REQUIRED", toBase64(JSON.stringify(challengePayload)));
|
|
138
|
+
return c.json({ error: "Payment required", x402: challengePayload }, 402);
|
|
139
|
+
}
|
|
140
|
+
// STEP 2: Signature format check
|
|
141
|
+
if (paymentSignature.length < 10) {
|
|
142
|
+
return c.json({ error: "Invalid PAYMENT-SIGNATURE format" }, 400);
|
|
143
|
+
}
|
|
144
|
+
// STEP 3: Nonce Claim
|
|
145
|
+
const signatureHash = await hashSignature(paymentSignature);
|
|
146
|
+
const isUnique = await nonceStore.claim(signatureHash, nonceTtlSeconds);
|
|
147
|
+
if (!isUnique) {
|
|
148
|
+
return c.json({
|
|
149
|
+
error: "Replay attack detected",
|
|
150
|
+
details: "This PAYMENT-SIGNATURE has already been processed or submitted."
|
|
151
|
+
}, 409);
|
|
152
|
+
}
|
|
153
|
+
// STEP 4: Verify & Settle via Facilitator
|
|
154
|
+
try {
|
|
155
|
+
const controller = new AbortController();
|
|
156
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
157
|
+
let parsedPayload = {};
|
|
158
|
+
try {
|
|
159
|
+
const decodedString = atob(paymentSignature);
|
|
160
|
+
parsedPayload = JSON.parse(decodedString);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
await nonceStore.release(signatureHash);
|
|
164
|
+
return c.json({ error: "Invalid base64 payload in PAYMENT-SIGNATURE" }, 400);
|
|
165
|
+
}
|
|
166
|
+
const settleResponse = await fetch(`${facilitatorUrl}/settle`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "Content-Type": "application/json" },
|
|
169
|
+
signal: controller.signal,
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
signature: parsedPayload.signature || parsedPayload.paymentPayload?.signature,
|
|
172
|
+
authorization: parsedPayload.authorization || parsedPayload.paymentPayload?.authorization,
|
|
173
|
+
paymentPayload: parsedPayload,
|
|
174
|
+
paymentRequirements: parsedPayload.paymentRequirements,
|
|
175
|
+
resource: { url: targetUrl }
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
clearTimeout(timeoutId);
|
|
179
|
+
const rawText = await settleResponse.text();
|
|
180
|
+
let settlement = {};
|
|
181
|
+
try {
|
|
182
|
+
settlement = JSON.parse(rawText);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
await nonceStore.release(signatureHash);
|
|
186
|
+
return c.json({ error: "Facilitator returned an invalid non-JSON response", status: settleResponse.status }, 502);
|
|
187
|
+
}
|
|
188
|
+
if (!settleResponse.ok || !settlement.success) {
|
|
189
|
+
await nonceStore.release(signatureHash);
|
|
190
|
+
c.header("PAYMENT-RESPONSE", toBase64(JSON.stringify({ success: false, error: settlement.error })));
|
|
191
|
+
return c.json({ error: "Payment verification failed", details: settlement.error }, 402);
|
|
192
|
+
}
|
|
193
|
+
// STEP 5: Success
|
|
194
|
+
c.header("PAYMENT-RESPONSE", toBase64(JSON.stringify(settlement)));
|
|
195
|
+
await next();
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
await nonceStore.release(signatureHash);
|
|
199
|
+
const isTimeout = err.name === "AbortError";
|
|
200
|
+
return c.json({
|
|
201
|
+
error: isTimeout ? "Facilitator settlement timed out" : "Facilitator network error during settlement",
|
|
202
|
+
message: err.message
|
|
203
|
+
}, 504);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createWalletClient, http } from "viem";
|
|
2
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
3
|
+
import { base } from "viem/chains";
|
|
4
|
+
const PRIVATE_KEY = process.env.PRIVATE_KEY;
|
|
5
|
+
if (!PRIVATE_KEY) {
|
|
6
|
+
console.error("Please set process.env.PRIVATE_KEY");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
const account = privateKeyToAccount(PRIVATE_KEY);
|
|
10
|
+
const walletClient = createWalletClient({
|
|
11
|
+
account,
|
|
12
|
+
chain: base,
|
|
13
|
+
transport: http("https://mainnet.base.org")
|
|
14
|
+
});
|
|
15
|
+
const API_URL = process.env.API_URL || "http://localhost:3000/api/weather";
|
|
16
|
+
async function executeAgentPayment() {
|
|
17
|
+
console.log(`[Agent] Probing endpoint: ${API_URL}`);
|
|
18
|
+
// Step 1: Retrieve 402 challenge
|
|
19
|
+
const initialRes = await fetch(API_URL);
|
|
20
|
+
if (initialRes.status !== 402) {
|
|
21
|
+
console.log("[Agent] Endpoint did not request x402 payment.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const paymentRequiredHeader = initialRes.headers.get("PAYMENT-REQUIRED");
|
|
25
|
+
if (!paymentRequiredHeader) {
|
|
26
|
+
throw new Error("Missing PAYMENT-REQUIRED header from server");
|
|
27
|
+
}
|
|
28
|
+
const challenge = JSON.parse(atob(paymentRequiredHeader));
|
|
29
|
+
const requirement = challenge.accepts[0];
|
|
30
|
+
console.log(`[Agent] Payment required: $${Number(requirement.amount) / 1e6} USDC on Base`);
|
|
31
|
+
// Step 2: Sign EIP-712 payment authorization
|
|
32
|
+
const domain = {
|
|
33
|
+
name: "USD Coin",
|
|
34
|
+
version: "2",
|
|
35
|
+
chainId: 8453,
|
|
36
|
+
verifyingContract: requirement.asset
|
|
37
|
+
};
|
|
38
|
+
const types = {
|
|
39
|
+
TransferWithAuthorization: [
|
|
40
|
+
{ name: "from", type: "address" },
|
|
41
|
+
{ name: "to", type: "address" },
|
|
42
|
+
{ name: "value", type: "uint256" },
|
|
43
|
+
{ name: "validAfter", type: "uint256" },
|
|
44
|
+
{ name: "validBefore", type: "uint256" },
|
|
45
|
+
{ name: "nonce", type: "bytes32" }
|
|
46
|
+
]
|
|
47
|
+
};
|
|
48
|
+
const now = Math.floor(Date.now() / 1000);
|
|
49
|
+
const nonce = `0x${Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
|
50
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
51
|
+
.join("")}`;
|
|
52
|
+
const authorization = {
|
|
53
|
+
from: account.address,
|
|
54
|
+
to: requirement.payTo,
|
|
55
|
+
value: requirement.amount,
|
|
56
|
+
validAfter: (now - 60).toString(),
|
|
57
|
+
validBefore: (now + requirement.maxTimeoutSeconds).toString(),
|
|
58
|
+
nonce
|
|
59
|
+
};
|
|
60
|
+
const signatureHex = await walletClient.signTypedData({
|
|
61
|
+
domain,
|
|
62
|
+
types,
|
|
63
|
+
primaryType: "TransferWithAuthorization",
|
|
64
|
+
message: {
|
|
65
|
+
from: account.address,
|
|
66
|
+
to: requirement.payTo,
|
|
67
|
+
value: BigInt(requirement.amount),
|
|
68
|
+
validAfter: BigInt(now - 60),
|
|
69
|
+
validBefore: BigInt(now + requirement.maxTimeoutSeconds),
|
|
70
|
+
nonce
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// Step 3: Construct root-level payment payload compatible with XPay relayer
|
|
74
|
+
const paymentSignaturePayload = JSON.stringify({
|
|
75
|
+
x402Version: 2,
|
|
76
|
+
scheme: "exact",
|
|
77
|
+
network: "eip155:8453",
|
|
78
|
+
signature: signatureHex,
|
|
79
|
+
authorization,
|
|
80
|
+
paymentRequirements: requirement
|
|
81
|
+
});
|
|
82
|
+
const encodedSignature = btoa(paymentSignaturePayload);
|
|
83
|
+
// Step 4: Resubmit with PAYMENT-SIGNATURE header
|
|
84
|
+
console.log("[Agent] Submitting signed authorization to server...");
|
|
85
|
+
const paidRes = await fetch(API_URL, {
|
|
86
|
+
headers: {
|
|
87
|
+
"PAYMENT-SIGNATURE": encodedSignature
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
const responseData = await paidRes.json();
|
|
91
|
+
console.log(`[Server Status]: ${paidRes.status}`);
|
|
92
|
+
console.log("[Server Payload]:", responseData);
|
|
93
|
+
}
|
|
94
|
+
executeAgentPayment().catch(console.error);
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import { monetize } from "./monetize.js";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
const app = new Hono();
|
|
6
|
+
// Define the developer's wallet address (where 99.5% of funds go)
|
|
7
|
+
const developerWallet = "0x2bd4e0ea72e21155ec41f8613eafd433193c4d8b";
|
|
8
|
+
app.use("/api/weather", monetize({
|
|
9
|
+
price: "0.01",
|
|
10
|
+
payTo: developerWallet,
|
|
11
|
+
platformWallet: "0x2bd4e0ea72e21155ec41f8613eafd433193c4d8b",
|
|
12
|
+
platformFeeBps: 50,
|
|
13
|
+
facilitatorUrl: "https://warppay402.com"
|
|
14
|
+
}));
|
|
15
|
+
app.get("/api/weather", (c) => {
|
|
16
|
+
return c.json({
|
|
17
|
+
status: "success",
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
data: { city: "Austin", temperature: "78°F", condition: "Sunny" }
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
serve({ fetch: app.fetch, port: 3000 }, (info) => {
|
|
23
|
+
console.log(`[API Server] Running on http://localhost:${info.port}`);
|
|
24
|
+
});
|
|
25
|
+
export default app;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"7.0.2","root":["../demo.ts","../facilitator.ts","../guard.ts","../index.ts","../mcpWrapper.ts","../monetize.ts","../pay_client.ts","../server.ts","../examples/basic-monetized-server.ts"],"packageJsons":["../node_modules/@hono/node-server/package.json","../node_modules/@noble/curves/esm/package.json","../node_modules/@noble/curves/package.json","../node_modules/@noble/hashes/esm/package.json","../node_modules/@noble/hashes/package.json","../node_modules/@scure/bip32/lib/esm/package.json","../node_modules/@scure/bip32/package.json","../node_modules/@scure/bip39/esm/package.json","../node_modules/@scure/bip39/package.json","../node_modules/@types/node/package.json","../node_modules/abitype/package.json","../node_modules/dotenv/package.json","../node_modules/eventemitter3/package.json","../node_modules/hono/dist/types/package.json","../node_modules/hono/package.json","../node_modules/ox/AbiFunction/package.json","../node_modules/ox/BlockOverrides/package.json","../node_modules/ox/Hex/package.json","../node_modules/ox/RpcResponse/package.json","../node_modules/ox/WebAuthnP256/package.json","../node_modules/ox/package.json","../node_modules/ox/tempo/KeyAuthorization/package.json","../node_modules/ox/tempo/MultisigConfig/package.json","../node_modules/ox/tempo/SignatureEnvelope/package.json","../node_modules/ox/tempo/TempoAddress/package.json","../node_modules/ox/tempo/TokenId/package.json","../node_modules/ox/tempo/TxEnvelopeTempo/package.json","../node_modules/ox/tempo/package.json","../node_modules/typescript/package.json","../node_modules/undici-types/package.json","../node_modules/viem/_types/package.json","../node_modules/viem/accounts/package.json","../node_modules/viem/chains/package.json","../node_modules/viem/package.json","../package.json"],"missingPackageJsons":["../node_modules/@hono/node-server/dist/package.json","../node_modules/@noble/curves/_shortw_utils/package.json","../node_modules/@noble/curves/abstract/bls/package.json","../node_modules/@noble/curves/abstract/edwards/package.json","../node_modules/@noble/curves/abstract/montgomery/package.json","../node_modules/@noble/curves/esm/abstract/package.json","../node_modules/@noble/hashes/utils/package.json","../node_modules/@scure/bip39/esm/wordlists/package.json","../node_modules/@scure/bip39/wordlists/czech/package.json","../node_modules/@scure/bip39/wordlists/english/package.json","../node_modules/@scure/bip39/wordlists/french/package.json","../node_modules/@scure/bip39/wordlists/italian/package.json","../node_modules/@scure/bip39/wordlists/japanese/package.json","../node_modules/@scure/bip39/wordlists/korean/package.json","../node_modules/@scure/bip39/wordlists/portuguese/package.json","../node_modules/@scure/bip39/wordlists/simplified-chinese/package.json","../node_modules/@scure/bip39/wordlists/spanish/package.json","../node_modules/@scure/bip39/wordlists/traditional-chinese/package.json","../node_modules/@types/assert/package.json","../node_modules/@types/assert/strict/package.json","../node_modules/@types/async_hooks/package.json","../node_modules/@types/buffer/package.json","../node_modules/@types/child_process/package.json","../node_modules/@types/cluster/package.json","../node_modules/@types/console/package.json","../node_modules/@types/constants/package.json","../node_modules/@types/crypto/package.json","../node_modules/@types/dgram/package.json","../node_modules/@types/diagnostics_channel/package.json","../node_modules/@types/dns/package.json","../node_modules/@types/dns/promises/package.json","../node_modules/@types/domain/package.json","../node_modules/@types/events/package.json","../node_modules/@types/fs/package.json","../node_modules/@types/fs/promises/package.json","../node_modules/@types/http/package.json","../node_modules/@types/http2/package.json","../node_modules/@types/https/package.json","../node_modules/@types/inspector/package.json","../node_modules/@types/inspector/promises/package.json","../node_modules/@types/module/package.json","../node_modules/@types/net/package.json","../node_modules/@types/node/assert/package.json","../node_modules/@types/node/dns/package.json","../node_modules/@types/node/fs/package.json","../node_modules/@types/node/inspector/package.json","../node_modules/@types/node/path/package.json","../node_modules/@types/node/readline/package.json","../node_modules/@types/node/stream/package.json","../node_modules/@types/node/test/package.json","../node_modules/@types/node/timers/package.json","../node_modules/@types/node/util/package.json","../node_modules/@types/node/web-globals/package.json","../node_modules/@types/node/zlib/package.json","../node_modules/@types/os/package.json","../node_modules/@types/path/package.json","../node_modules/@types/path/posix/package.json","../node_modules/@types/path/win32/package.json","../node_modules/@types/perf_hooks/package.json","../node_modules/@types/process/package.json","../node_modules/@types/punycode/package.json","../node_modules/@types/querystring/package.json","../node_modules/@types/readline/package.json","../node_modules/@types/readline/promises/package.json","../node_modules/@types/repl/package.json","../node_modules/@types/stream/consumers/package.json","../node_modules/@types/stream/iter/package.json","../node_modules/@types/stream/package.json","../node_modules/@types/stream/promises/package.json","../node_modules/@types/stream/web/package.json","../node_modules/@types/string_decoder/package.json","../node_modules/@types/timers/package.json","../node_modules/@types/timers/promises/package.json","../node_modules/@types/tls/package.json","../node_modules/@types/trace_events/package.json","../node_modules/@types/tty/package.json","../node_modules/@types/url/package.json","../node_modules/@types/util/package.json","../node_modules/@types/util/types/package.json","../node_modules/@types/v8/package.json","../node_modules/@types/vm/package.json","../node_modules/@types/wasi/package.json","../node_modules/@types/worker_threads/package.json","../node_modules/@types/zlib/package.json","../node_modules/abitype/dist/package.json","../node_modules/abitype/dist/types/exports/package.json","../node_modules/abitype/dist/types/human-readable/errors/package.json","../node_modules/abitype/dist/types/human-readable/package.json","../node_modules/abitype/dist/types/human-readable/types/package.json","../node_modules/abitype/dist/types/package.json","../node_modules/assert/package.json","../node_modules/assert/strict/package.json","../node_modules/async_hooks/package.json","../node_modules/buffer/package.json","../node_modules/child_process/package.json","../node_modules/cluster/package.json","../node_modules/console/package.json","../node_modules/constants/package.json","../node_modules/crypto/package.json","../node_modules/dgram/package.json","../node_modules/diagnostics_channel/package.json","../node_modules/dns/package.json","../node_modules/dns/promises/package.json","../node_modules/domain/package.json","../node_modules/dotenv/config/package.json","../node_modules/events/package.json","../node_modules/fs/package.json","../node_modules/fs/promises/package.json","../node_modules/hono/dist/types/client/package.json","../node_modules/hono/dist/types/helper/package.json","../node_modules/hono/dist/types/helper/websocket/package.json","../node_modules/hono/dist/types/request/package.json","../node_modules/hono/dist/types/utils/package.json","../node_modules/hono/ws/package.json","../node_modules/http/package.json","../node_modules/http2/package.json","../node_modules/https/package.json","../node_modules/inspector/package.json","../node_modules/inspector/promises/package.json","../node_modules/module/package.json","../node_modules/net/package.json","../node_modules/os/package.json","../node_modules/ox/_types/core/internal/mnemonic/package.json","../node_modules/ox/_types/core/internal/package.json","../node_modules/ox/_types/core/internal/rpcSchemas/package.json","../node_modules/ox/_types/core/package.json","../node_modules/ox/_types/package.json","../node_modules/ox/_types/tempo/package.json","../node_modules/ox/_types/webauthn/package.json","../node_modules/path/package.json","../node_modules/path/posix/package.json","../node_modules/path/win32/package.json","../node_modules/perf_hooks/package.json","../node_modules/process/package.json","../node_modules/punycode/package.json","../node_modules/querystring/package.json","../node_modules/readline/package.json","../node_modules/readline/promises/package.json","../node_modules/repl/package.json","../node_modules/stream/consumers/package.json","../node_modules/stream/iter/package.json","../node_modules/stream/package.json","../node_modules/stream/promises/package.json","../node_modules/stream/web/package.json","../node_modules/string_decoder/package.json","../node_modules/timers/package.json","../node_modules/timers/promises/package.json","../node_modules/tls/package.json","../node_modules/trace_events/package.json","../node_modules/tty/package.json","../node_modules/url/package.json","../node_modules/util/package.json","../node_modules/util/types/package.json","../node_modules/v8/package.json","../node_modules/viem/_types/account-abstraction/accounts/package.json","../node_modules/viem/_types/account-abstraction/package.json","../node_modules/viem/_types/account-abstraction/types/package.json","../node_modules/viem/_types/accounts/package.json","../node_modules/viem/_types/accounts/utils/package.json","../node_modules/viem/_types/actions/ens/package.json","../node_modules/viem/_types/actions/package.json","../node_modules/viem/_types/actions/public/package.json","../node_modules/viem/_types/actions/siwe/package.json","../node_modules/viem/_types/actions/test/package.json","../node_modules/viem/_types/actions/token/package.json","../node_modules/viem/_types/actions/wallet/package.json","../node_modules/viem/_types/celo/package.json","../node_modules/viem/_types/chains/definitions/package.json","../node_modules/viem/_types/chains/definitions/skale/package.json","../node_modules/viem/_types/chains/package.json","../node_modules/viem/_types/clients/decorators/package.json","../node_modules/viem/_types/clients/package.json","../node_modules/viem/_types/clients/transports/package.json","../node_modules/viem/_types/constants/package.json","../node_modules/viem/_types/errors/package.json","../node_modules/viem/_types/experimental/erc7895/actions/package.json","../node_modules/viem/_types/experimental/erc7895/package.json","../node_modules/viem/_types/experimental/package.json","../node_modules/viem/_types/op-stack/package.json","../node_modules/viem/_types/op-stack/types/package.json","../node_modules/viem/_types/tempo/package.json","../node_modules/viem/_types/tokens/package.json","../node_modules/viem/_types/types/package.json","../node_modules/viem/_types/utils/abi/package.json","../node_modules/viem/_types/utils/address/package.json","../node_modules/viem/_types/utils/authorization/package.json","../node_modules/viem/_types/utils/blob/package.json","../node_modules/viem/_types/utils/block/package.json","../node_modules/viem/_types/utils/chain/package.json","../node_modules/viem/_types/utils/data/package.json","../node_modules/viem/_types/utils/encoding/package.json","../node_modules/viem/_types/utils/ens/avatar/package.json","../node_modules/viem/_types/utils/ens/package.json","../node_modules/viem/_types/utils/errors/package.json","../node_modules/viem/_types/utils/formatters/package.json","../node_modules/viem/_types/utils/hash/package.json","../node_modules/viem/_types/utils/kzg/package.json","../node_modules/viem/_types/utils/package.json","../node_modules/viem/_types/utils/promise/package.json","../node_modules/viem/_types/utils/rpc/package.json","../node_modules/viem/_types/utils/signature/package.json","../node_modules/viem/_types/utils/siwe/package.json","../node_modules/viem/_types/utils/transaction/package.json","../node_modules/viem/_types/utils/unit/package.json","../node_modules/viem/_types/zksync/accounts/package.json","../node_modules/viem/_types/zksync/actions/package.json","../node_modules/viem/_types/zksync/constants/package.json","../node_modules/viem/_types/zksync/decorators/package.json","../node_modules/viem/_types/zksync/errors/package.json","../node_modules/viem/_types/zksync/package.json","../node_modules/viem/_types/zksync/types/package.json","../node_modules/viem/_types/zksync/utils/abi/package.json","../node_modules/viem/_types/zksync/utils/bridge/package.json","../node_modules/viem/_types/zksync/utils/package.json","../node_modules/viem/_types/zksync/utils/paymaster/package.json","../node_modules/vm/package.json","../node_modules/wasi/package.json","../node_modules/worker_threads/package.json","../node_modules/zlib/package.json","../node_modules/zod/package.json"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@warppay402/server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Instant MCP Tool & x402 Monetization SDK for Cloudflare Gateway and Base",
|
|
5
|
+
"homepage": "https://www.warppay402.com",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"x402",
|
|
19
|
+
"mcp",
|
|
20
|
+
"monetization",
|
|
21
|
+
"base",
|
|
22
|
+
"usdc",
|
|
23
|
+
"hono",
|
|
24
|
+
"ai-agents",
|
|
25
|
+
"warppay402"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/golfgolfgolf200/warppay402-server.git"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc --build",
|
|
36
|
+
"prepare": "npm run build",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"example": "tsx examples/basic-monetized-server.ts"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@hono/node-server": "^2.1.0",
|
|
42
|
+
"@xpaysh/x402": "^0.1.2",
|
|
43
|
+
"dotenv": "^17.4.2",
|
|
44
|
+
"hono": "^4.13.0",
|
|
45
|
+
"viem": "^2.55.17"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^26.2.0",
|
|
49
|
+
"tsx": "^4.23.9",
|
|
50
|
+
"typescript": "^7.0.2",
|
|
51
|
+
"vitest": "^4.1.10"
|
|
52
|
+
}
|
|
53
|
+
}
|