@forgemeshlabs/x402-ads-mcp 0.1.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/Dockerfile ADDED
@@ -0,0 +1,12 @@
1
+ FROM node:22-slim
2
+
3
+ WORKDIR /app
4
+ ENV NODE_ENV=production
5
+
6
+ COPY package*.json ./
7
+ RUN npm ci --omit=dev
8
+
9
+ COPY index.js README.md server.json glama.json ./
10
+ USER node
11
+
12
+ CMD ["node", "index.js"]
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 clawdbotworker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @forgemeshlabs/x402-ads-mcp
2
+
3
+ **Machine-commerce intent analytics for your agent.** What autonomous agents probe, want, and abandon across the x402 ecosystem — as MCP tools.
4
+
5
+ Wraps the [ForgeMesh x402 Ads & Intent Network](https://ads.forgemesh.io). Paid tools settle per call in USDC on Base mainnet over the [x402 protocol](https://x402.org) — no account, no API key; your wallet is the login. Publishers get reports on their own services **free**.
6
+
7
+ ## Install
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "intent": {
13
+ "command": "npx",
14
+ "args": ["-y", "@forgemeshlabs/x402-ads-mcp"],
15
+ "env": {
16
+ "WALLET_PRIVATE_KEY": "0x... (optional — enables paid analytics)",
17
+ "X402_ADS_PUBLISHER_KEY": "pub_... (optional — free reports on your own services)"
18
+ }
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ Both env vars are optional. With neither set, free tools work fully and paid tools return the x402 payment challenge (price, network, payTo) instead of settling — useful for inspection before spending anything.
25
+
26
+ ## Tools
27
+
28
+ | Tool | Price | What it returns |
29
+ |---|---|---|
30
+ | `get_network_counters` | free | Live network totals: 402s observed, agent-class requests, recommendations served |
31
+ | `preview_recommendations` | free | The exact recommendations block the middleware injects into a 402 |
32
+ | `get_terms` | free | Canonical terms + complete data-collection disclosure |
33
+ | `get_network_stats` | $0.005 | Network totals + monitor/indexer/agent classification split |
34
+ | `get_intent_trends` | $0.01 | Top endpoints & categories autonomous agents request |
35
+ | `get_category_demand` | $0.02 | Demand depth for one category: volume, buyer share, price points |
36
+ | `get_intent_report` | $0.05 / **free*** | Why-agents-didn't-buy funnel for one service |
37
+
38
+ \* `get_intent_report` is free with `X402_ADS_PUBLISHER_KEY` for services you contribute events to — the data co-op rule: your own data is free, forever.
39
+
40
+ ## Environment
41
+
42
+ | Variable | Required | Purpose |
43
+ |---|---|---|
44
+ | `WALLET_PRIVATE_KEY` | no | Base mainnet wallet holding USDC; enables automatic settlement of paid tools |
45
+ | `X402_ADS_PUBLISHER_KEY` | no | Publisher key from ads.forgemesh.io; free lane for your own reports |
46
+ | `X402_ADS_BASE_URL` | no | Override the network base URL (default `https://ads.forgemesh.io`) |
47
+ | `BASE_RPC_URL` | no | Override the Base RPC (default `https://mainnet.base.org`) |
48
+
49
+ Use a dedicated hot wallet holding only small working balances. The key never leaves your machine — payments are signed locally (EIP-3009) and settle on-chain.
50
+
51
+ ## The network in one sentence
52
+
53
+ **We measure machine commerce, not API content** — publishers running the [`@forgemeshlabs/x402-ads`](https://www.npmjs.com/package/@forgemeshlabs/x402-ads) middleware contribute anonymized 402 probe metadata; this MCP sells the aggregate demand signal back to agents and builders.
54
+
55
+ Full disclosure of what publishers send (and never send): https://ads.forgemesh.io/terms
56
+
57
+ ## License
58
+
59
+ MIT © ForgeMesh
package/glama.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "name": "x402-ads-mcp",
4
+ "version": "0.1.0",
5
+ "description": "Machine-commerce intent analytics over x402: agent demand trends, category depth, and why-agents-didn't-buy funnels for the x402 ecosystem. Free network counters and recommendation previews; paid analytics settle in USDC on Base.",
6
+ "homepage": "https://ads.forgemesh.io",
7
+ "repository": "https://github.com/forgemeshlabs/x402-ads-mcp",
8
+ "maintainers": [
9
+ "clawdbotworker"
10
+ ],
11
+ "transport": {
12
+ "type": "stdio",
13
+ "command": "x402-ads-mcp"
14
+ },
15
+ "build": {
16
+ "dockerfile": "Dockerfile",
17
+ "steps": [
18
+ "npm ci --omit=dev"
19
+ ],
20
+ "cmd": [
21
+ "node",
22
+ "index.js"
23
+ ]
24
+ },
25
+ "env": {
26
+ "WALLET_PRIVATE_KEY": {
27
+ "description": "Optional Base mainnet wallet private key for settling paid x402 analytics calls. Without it, paid tools return the x402 payment challenge instead of settling.",
28
+ "required": false
29
+ },
30
+ "X402_ADS_PUBLISHER_KEY": {
31
+ "description": "Optional publisher key from ads.forgemesh.io. Makes get_intent_report free for services you contribute events to.",
32
+ "required": false
33
+ },
34
+ "X402_ADS_BASE_URL": {
35
+ "description": "Optional intent network base URL.",
36
+ "required": false,
37
+ "default": "https://ads.forgemesh.io"
38
+ },
39
+ "BASE_RPC_URL": {
40
+ "description": "Optional Base mainnet RPC URL.",
41
+ "required": false,
42
+ "default": "https://mainnet.base.org"
43
+ }
44
+ }
45
+ }
package/index.js ADDED
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { McpServer } = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const { z } = require("zod");
7
+ const { x402Client, x402HTTPClient } = require("@x402/core/client");
8
+ const { ExactEvmScheme } = require("@x402/evm/exact/client");
9
+ const { toClientEvmSigner } = require("@x402/evm");
10
+ const { privateKeyToAccount } = require("viem/accounts");
11
+ const { createPublicClient, http } = require("viem");
12
+ const { base } = require("viem/chains");
13
+
14
+ const BASE_URL = (process.env.X402_ADS_BASE_URL || "https://ads.forgemesh.io").replace(/\/+$/, "");
15
+ const BASE_RPC_URL = process.env.BASE_RPC_URL || "https://mainnet.base.org";
16
+ const WINDOWS = ["24h", "7d", "30d", "all"];
17
+
18
+ const TOOL_SCHEMAS = {
19
+ get_network_counters: {},
20
+ preview_recommendations: {
21
+ service: z.string().max(120).optional().describe("Your service identifier, used only for self-exclusion in results"),
22
+ endpoint: z.string().max(300).optional().describe("The probed endpoint path, e.g. /api/forecast"),
23
+ category: z.string().max(60).optional().describe("Category to match recommendations against, e.g. finance, blockchain, images"),
24
+ },
25
+ get_network_stats: {},
26
+ get_intent_trends: {
27
+ window: z.enum(WINDOWS).optional().describe("Time window: 24h, 7d, 30d, or all (default 7d)"),
28
+ limit: z.number().int().min(1).max(100).optional().describe("Max rows, 1-100 (default 20)"),
29
+ },
30
+ get_category_demand: {
31
+ category: z.string().min(1).max(60).describe("Category to measure, e.g. finance, blockchain, images, tts"),
32
+ window: z.enum(WINDOWS).optional().describe("Time window: 24h, 7d, 30d, or all (default 30d)"),
33
+ },
34
+ get_intent_report: {
35
+ service: z.string().min(1).max(120).describe("Service identifier to report on"),
36
+ window: z.enum(WINDOWS).optional().describe("Time window: 24h, 7d, 30d, or all (default 30d)"),
37
+ },
38
+ get_terms: {},
39
+ };
40
+
41
+ const TOOLS = [
42
+ {
43
+ name: "get_network_counters",
44
+ title: "Get Network Counters",
45
+ description:
46
+ "Free. Live totals for the ForgeMesh machine-commerce network: 402 responses observed, agent-class requests, recommendations served, services reporting, and x402 services indexed.",
47
+ },
48
+ {
49
+ name: "preview_recommendations",
50
+ title: "Preview Recommendations",
51
+ description:
52
+ "Free. See the exact typed recommendations block (sponsored + similar x402 services) that the @forgemeshlabs/x402-ads middleware would inject into a 402 response for a given endpoint and category.",
53
+ },
54
+ {
55
+ name: "get_network_stats",
56
+ title: "Get Network Stats",
57
+ description:
58
+ "Paid, $0.005 USDC on Base via x402. Network-wide intent stats: total events, services, monitor/indexer/agent traffic classification split, and ad activity. Without WALLET_PRIVATE_KEY, returns the x402 payment challenge instead of settling.",
59
+ },
60
+ {
61
+ name: "get_intent_trends",
62
+ title: "Get Intent Trends",
63
+ description:
64
+ "Paid, $0.01 USDC on Base via x402. Google-Trends-for-agents: top requested x402 endpoints and categories by autonomous agents, split by traffic class. Without WALLET_PRIVATE_KEY, returns the x402 payment challenge instead of settling.",
65
+ },
66
+ {
67
+ name: "get_category_demand",
68
+ title: "Get Category Demand",
69
+ description:
70
+ "Paid, $0.02 USDC on Base via x402. Demand depth for one category: probe volume, distinct sources, buyer-class share, price points probed, daily series. Without WALLET_PRIVATE_KEY, returns the x402 payment challenge instead of settling.",
71
+ },
72
+ {
73
+ name: "get_intent_report",
74
+ title: "Get Intent Report",
75
+ description:
76
+ "Why-agents-didn't-buy funnel for one service: bounce funnel, traffic classes, top abandoned endpoints, retry signals. FREE with X402_ADS_PUBLISHER_KEY for services you contribute events to (the data co-op rule); otherwise $0.05 USDC on Base via x402. Without a publisher key or WALLET_PRIVATE_KEY, returns the x402 payment challenge.",
77
+ },
78
+ {
79
+ name: "get_terms",
80
+ title: "Get Terms & Data Disclosure",
81
+ description:
82
+ "Free. The network's canonical terms of service and complete data-collection disclosure: exactly what the middleware sends and never sends.",
83
+ },
84
+ ];
85
+
86
+ function walletClient() {
87
+ const key = process.env.WALLET_PRIVATE_KEY;
88
+ if (!key) return null;
89
+ const pk = key.startsWith("0x") ? key : "0x" + key;
90
+ const account = privateKeyToAccount(pk);
91
+ const coreClient = new x402Client().register("eip155:*", new ExactEvmScheme(toClientEvmSigner(account)));
92
+ return new x402HTTPClient(coreClient);
93
+ }
94
+
95
+ // The full x402 challenge is base64 JSON in the payment-required header;
96
+ // the 402 body only carries a friendly summary (price, network, message).
97
+ function slimChallenge(res, body) {
98
+ let decoded = null;
99
+ try {
100
+ const header = res.headers.get("payment-required");
101
+ if (header) decoded = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
102
+ } catch (_) {}
103
+ const accepts = Array.isArray(decoded?.accepts)
104
+ ? decoded.accepts.map((a) => ({
105
+ scheme: a.scheme,
106
+ network: a.network,
107
+ asset: a.asset,
108
+ amount: a.amount ?? a.maxAmountRequired,
109
+ payTo: a.payTo,
110
+ }))
111
+ : undefined;
112
+ return {
113
+ x402Version: decoded?.x402Version,
114
+ price: body?.price,
115
+ network: body?.network,
116
+ accepts,
117
+ };
118
+ }
119
+
120
+ async function createChainTimedPaymentPayload(httpClient, paymentRequired) {
121
+ try {
122
+ const publicClient = createPublicClient({ chain: base, transport: http(BASE_RPC_URL) });
123
+ const block = await publicClient.getBlock();
124
+ const chainNow = Number(block.timestamp);
125
+ const originalNow = Date.now;
126
+ const localNow = Math.floor(originalNow() / 1000);
127
+ const timeout = Number(paymentRequired.accepts?.[0]?.maxTimeoutSeconds || 300);
128
+ const signingNow = Math.min(Math.max(chainNow, localNow + 30 - timeout), chainNow + 600);
129
+ Date.now = () => signingNow * 1000;
130
+ try {
131
+ return await httpClient.createPaymentPayload(paymentRequired);
132
+ } finally {
133
+ Date.now = originalNow;
134
+ }
135
+ } catch (_) {
136
+ return httpClient.createPaymentPayload(paymentRequired);
137
+ }
138
+ }
139
+
140
+ // Challenge-first paid GET: publisher free lane → settle if wallet → structured 402 otherwise.
141
+ async function paidGet(path) {
142
+ const headers = {};
143
+ if (process.env.X402_ADS_PUBLISHER_KEY) headers["x-publisher-key"] = process.env.X402_ADS_PUBLISHER_KEY;
144
+ const url = BASE_URL + path;
145
+
146
+ const res = await fetch(url, { headers });
147
+ if (res.ok) {
148
+ const viaPublisherKey = !!process.env.X402_ADS_PUBLISHER_KEY;
149
+ return { paid: false, ...(viaPublisherKey ? { free_via_publisher_key: true } : {}), data: await res.json() };
150
+ }
151
+ if (res.status !== 402) {
152
+ const text = await res.text().catch(() => "");
153
+ throw new Error(`GET ${path} failed: ${res.status} ${text.slice(0, 240)}`);
154
+ }
155
+
156
+ let challengeBody;
157
+ try {
158
+ challengeBody = await res.clone().json();
159
+ } catch (_) {}
160
+
161
+ const httpClient = walletClient();
162
+ if (!httpClient) {
163
+ return {
164
+ payment_required: true,
165
+ challenge: slimChallenge(res, challengeBody),
166
+ how_to_pay:
167
+ "Set WALLET_PRIVATE_KEY (Base mainnet wallet holding USDC) to settle this x402 call automatically, or set X402_ADS_PUBLISHER_KEY to get reports on your own services free.",
168
+ };
169
+ }
170
+
171
+ const paymentRequired = httpClient.getPaymentRequiredResponse((name) => res.headers.get(name), challengeBody);
172
+ const paymentPayload = await createChainTimedPaymentPayload(httpClient, paymentRequired);
173
+ const paidRes = await fetch(url, {
174
+ headers: { ...headers, ...httpClient.encodePaymentSignatureHeader(paymentPayload) },
175
+ });
176
+ if (!paidRes.ok) {
177
+ const text = await paidRes.text().catch(() => paidRes.statusText);
178
+ throw new Error(`Paid call failed: ${paidRes.status} ${text.slice(0, 240)}`);
179
+ }
180
+ return {
181
+ paid: true,
182
+ payment_response: paidRes.headers.get("payment-response"),
183
+ data: await paidRes.json(),
184
+ };
185
+ }
186
+
187
+ async function freeGet(path, asText = false) {
188
+ const res = await fetch(BASE_URL + path);
189
+ if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`);
190
+ return asText ? res.text() : res.json();
191
+ }
192
+
193
+ function qs(params) {
194
+ const q = Object.entries(params)
195
+ .filter(([, v]) => v !== undefined && v !== null && v !== "")
196
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
197
+ .join("&");
198
+ return q ? `?${q}` : "";
199
+ }
200
+
201
+ async function callTool(name, args = {}) {
202
+ if (name === "get_network_counters") return freeGet("/v1/counters");
203
+
204
+ if (name === "preview_recommendations") {
205
+ const res = await fetch(BASE_URL + "/v1/decide", {
206
+ method: "POST",
207
+ headers: { "Content-Type": "application/json" },
208
+ body: JSON.stringify({
209
+ service: args.service || "mcp-preview",
210
+ endpoint: args.endpoint || "/api/example",
211
+ category: args.category,
212
+ }),
213
+ });
214
+ if (!res.ok) throw new Error(`POST /v1/decide failed: ${res.status}`);
215
+ return res.json();
216
+ }
217
+
218
+ if (name === "get_network_stats") return paidGet("/api/network/stats");
219
+
220
+ if (name === "get_intent_trends") return paidGet("/api/intent/trends" + qs({ window: args.window, limit: args.limit }));
221
+
222
+ if (name === "get_category_demand")
223
+ return paidGet("/api/intent/demand" + qs({ category: args.category, window: args.window }));
224
+
225
+ if (name === "get_intent_report")
226
+ return paidGet("/api/intent/report" + qs({ service: args.service, window: args.window }));
227
+
228
+ if (name === "get_terms") return { terms: await freeGet("/terms", true) };
229
+
230
+ throw new Error(`Unknown tool: ${name}`);
231
+ }
232
+
233
+ function textResult(value) {
234
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
235
+ }
236
+
237
+ const server = new McpServer({ name: "x402-ads-mcp", version: "0.1.0" });
238
+ server.server.onerror = (error) => {
239
+ console.error(error instanceof Error ? error.message : String(error));
240
+ };
241
+ for (const tool of TOOLS) {
242
+ server.registerTool(
243
+ tool.name,
244
+ {
245
+ title: tool.title,
246
+ description: tool.description,
247
+ inputSchema: TOOL_SCHEMAS[tool.name],
248
+ },
249
+ async (args) => {
250
+ try {
251
+ return textResult(await callTool(tool.name, args || {}));
252
+ } catch (error) {
253
+ return {
254
+ isError: true,
255
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
256
+ };
257
+ }
258
+ }
259
+ );
260
+ }
261
+
262
+ async function main() {
263
+ await server.connect(new StdioServerTransport());
264
+ process.stdin.resume();
265
+ const keepAlive = setInterval(() => {}, 2 ** 30);
266
+ process.stdin.on("end", () => clearInterval(keepAlive));
267
+ }
268
+
269
+ if (require.main === module) {
270
+ main().catch((error) => {
271
+ console.error(error instanceof Error ? error.message : String(error));
272
+ process.exit(1);
273
+ });
274
+ }
275
+
276
+ module.exports = { TOOLS, TOOL_SCHEMAS, callTool };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@forgemeshlabs/x402-ads-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.forgemeshlabs/x402-ads-mcp",
5
+ "description": "Machine-commerce intent analytics over x402: what autonomous agents probe, want, and abandon across the x402 ecosystem — trends, category demand, and why-agents-didn't-buy funnels, paid per call in USDC on Base. Free for publishers on their own traffic.",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "x402-ads-mcp": "index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "README.md",
13
+ "LICENSE",
14
+ "glama.json",
15
+ "server.json",
16
+ "Dockerfile"
17
+ ],
18
+ "scripts": {
19
+ "start": "node index.js",
20
+ "check": "node --check index.js",
21
+ "test": "npm run check && node scripts/test-mcp.js"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "x402",
30
+ "intent",
31
+ "analytics",
32
+ "machine-commerce",
33
+ "agent-economy",
34
+ "demand",
35
+ "402",
36
+ "forgemesh",
37
+ "coinbase",
38
+ "base",
39
+ "usdc",
40
+ "micropayments"
41
+ ],
42
+ "author": "clawdbotworker <clawdbotworker@gmail.com>",
43
+ "maintainers": [
44
+ "clawdbotworker <clawdbotworker@gmail.com>"
45
+ ],
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/forgemeshlabs/x402-ads-mcp.git"
50
+ },
51
+ "homepage": "https://ads.forgemesh.io",
52
+ "bugs": {
53
+ "url": "https://github.com/forgemeshlabs/x402-ads-mcp/issues"
54
+ },
55
+ "dependencies": {
56
+ "@modelcontextprotocol/sdk": "^1.10.1",
57
+ "@x402/core": "2.11.0",
58
+ "@x402/evm": "2.11.0",
59
+ "zod": "^3.25.76",
60
+ "viem": "^2.0.0"
61
+ },
62
+ "overrides": {
63
+ "ws": "8.21.0"
64
+ }
65
+ }
package/server.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "x402-ads-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Machine-commerce intent analytics over x402: agent demand trends, category depth, and why-agents-didn't-buy funnels for the x402 ecosystem. Free network counters and recommendation previews; paid analytics settle in USDC on Base.",
5
+ "homepage": "https://ads.forgemesh.io",
6
+ "repository": "https://github.com/forgemeshlabs/x402-ads-mcp",
7
+ "transport": {
8
+ "type": "stdio",
9
+ "command": "x402-ads-mcp"
10
+ }
11
+ }