@forgemeshlabs/x402-ads-mcp 0.2.6 → 0.2.8

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 CHANGED
@@ -49,7 +49,7 @@ One $0.10 USDC payment on Base; the paying wallet becomes your identity and your
49
49
  | `get_terms` | free | Canonical terms + complete data-collection disclosure |
50
50
  | `get_network_stats` | $0.005 | Network totals + monitor/indexer/agent classification split |
51
51
  | `get_intent_trends` | $0.01 | Top endpoints & categories autonomous agents request |
52
- | `get_category_demand` | $0.02 | Demand depth for one category: volume, buyer share, price points |
52
+ | `get_category_demand` | $0.02 | Demand depth for one category: probes, agent probes, conversions, price points (404 no_data is not charged) |
53
53
  | `get_intent_report` | $0.05 / **free*** | Why-agents-didn't-buy funnel for one service |
54
54
 
55
55
  \* `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.
package/glama.json CHANGED
@@ -26,5 +26,5 @@
26
26
  "mcp",
27
27
  "autonomous-agents"
28
28
  ],
29
- "version": "0.2.6"
29
+ "version": "0.2.8"
30
30
  }
package/index.js CHANGED
@@ -32,7 +32,7 @@ const TOOL_SCHEMAS = {
32
32
  limit: z.number().int().min(1).max(100).optional().describe("Max rows, 1-100 (default 20)"),
33
33
  },
34
34
  get_category_demand: {
35
- category: z.string().min(1).max(60).regex(ID_PATTERN).describe("Category to measure, e.g. finance, blockchain, images, tts"),
35
+ category: z.string().min(1).max(60).regex(ID_PATTERN).describe("Category as reported by publishers, e.g. blockchain, developer-tools (get_intent_trends lists categories with data)"),
36
36
  window: z.enum(WINDOWS).optional().describe("Time window: 24h, 7d, 30d, or all (default 30d)"),
37
37
  },
38
38
  get_intent_report: {
@@ -89,7 +89,7 @@ const TOOLS = [
89
89
  openWorldHint: true,
90
90
  },
91
91
  description:
92
- "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.",
92
+ "Paid, $0.005 USDC on Base via x402. x402 network traffic stats, no parameters: network-wide totals (x402 events, services), a breakdown of traffic by user-agent class (agent, browser, bot) and ad_activity. Use it to benchmark your service's agent-traffic share. Without WALLET_PRIVATE_KEY, returns the x402 payment challenge instead of settling.",
93
93
  },
94
94
  {
95
95
  name: "get_intent_trends",
@@ -101,7 +101,7 @@ const TOOLS = [
101
101
  openWorldHint: true,
102
102
  },
103
103
  description:
104
- "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.",
104
+ "Paid, $0.01 USDC on Base via x402. x402 agent demand trends: the most-requested x402 endpoints (service, endpoint, probes, agent_probes) and the categories with data, in a time window (24h, 7d, 30d or all; limit 1-100). Without WALLET_PRIVATE_KEY, returns the x402 payment challenge instead of settling.",
105
105
  },
106
106
  {
107
107
  name: "get_category_demand",
@@ -113,7 +113,7 @@ const TOOLS = [
113
113
  openWorldHint: true,
114
114
  },
115
115
  description:
116
- "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.",
116
+ "Paid, $0.02 USDC on Base via x402. x402 category demand depth for one API category: summary (probes, agent_probes, conversions), price points probed and a daily series. The category must be one publishers report (get_intent_trends lists categories that have data); a category with no reported traffic in the window answers 404 no_data and is not charged. Without WALLET_PRIVATE_KEY, returns the x402 payment challenge instead of settling.",
117
117
  },
118
118
  {
119
119
  name: "get_intent_report",
@@ -125,7 +125,7 @@ const TOOLS = [
125
125
  openWorldHint: true,
126
126
  },
127
127
  description:
128
- "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 your registered services (the service id is bound to your key at registration); otherwise $0.05 USDC on Base via x402. Without a publisher key or WALLET_PRIVATE_KEY, returns the x402 payment challenge.",
128
+ "x402 conversion funnel report for one service (why agents probe but do not pay): funnel (total_probes, bounces, paid, conversion_rate), traffic_classes and top_abandoned_endpoints. Data exists only for services that report through the publisher middleware; a service with no events in the window answers 404 no_data and is not charged. FREE with X402_ADS_PUBLISHER_KEY for your registered services (the service id is bound to your key at registration); otherwise $0.05 USDC on Base via x402. Without a publisher key or WALLET_PRIVATE_KEY, returns the x402 payment challenge.",
129
129
  },
130
130
  {
131
131
  name: "get_terms",
@@ -186,7 +186,7 @@ async function paidGet(path) {
186
186
  const viaPublisherKey = !!process.env.X402_ADS_PUBLISHER_KEY;
187
187
  return { paid: false, ...(viaPublisherKey ? { free_via_publisher_key: true } : {}), data: JSON.parse(res.text) };
188
188
  }
189
- if (res.status !== 402) throw new Error(`GET ${path} failed: ${res.status} ${res.text.slice(0, 200)}`);
189
+ if (res.status !== 402) throw guard.httpError(res, `GET ${path} failed: `, false); // nothing was signed, so nothing was charged
190
190
 
191
191
  const httpClient = walletClient();
192
192
  if (!httpClient) {
@@ -210,7 +210,7 @@ async function paidGet(path) {
210
210
 
211
211
  async function freeGet(path, asText = false) {
212
212
  const res = await guard.fetchBounded(path);
213
- if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`);
213
+ if (!res.ok) throw guard.httpError(res, `GET ${path} failed: `, false);
214
214
  return asText ? res.text : JSON.parse(res.text);
215
215
  }
216
216
 
@@ -238,7 +238,7 @@ async function callTool(name, args = {}) {
238
238
  category: args.category,
239
239
  }),
240
240
  });
241
- if (!res.ok) throw new Error(`POST /v1/decide failed: ${res.status}`);
241
+ if (!res.ok) throw guard.httpError(res, "POST /v1/decide failed: ", false);
242
242
  return JSON.parse(res.text);
243
243
  }
244
244
 
@@ -280,7 +280,7 @@ for (const tool of TOOLS) {
280
280
  } catch (error) {
281
281
  return {
282
282
  isError: true,
283
- content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
283
+ content: [{ type: "text", text: error && error.status !== undefined ? JSON.stringify({ error: error.message, status: error.status, ...(typeof error.charged === "boolean" ? { charged: error.charged } : {}), upstream: error.upstream, untrusted: true }) : error instanceof Error ? error.message : String(error) }],
284
284
  };
285
285
  }
286
286
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgemeshlabs/x402-ads-mcp",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "mcpName": "io.github.forgemeshlabs/x402-ads-mcp",
5
5
  "description": "Install one middleware. Monetize unused 402 responses with recommendations, discovery, and machine commerce analytics. MCP server: 7 tools for agent demand analytics, paid per call in USDC on Base; free for publishers on their own traffic.",
6
6
  "main": "index.js",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/forgemeshlabs/x402-ads-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.2.6",
9
+ "version": "0.2.8",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@forgemeshlabs/x402-ads-mcp",
14
- "version": "0.2.6",
14
+ "version": "0.2.8",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
package/x402-guard.js CHANGED
@@ -99,6 +99,21 @@ function createGuard(opts) {
99
99
  }
100
100
  }
101
101
 
102
+ // Keep the service's refusal intact: HTTP status, charged flag, and the body in err.upstream only.
103
+ // The message is short and fixed; upstream content is untrusted and never interpolated into prose.
104
+ function httpError(res, prefix, charged) {
105
+ let upstream;
106
+ try { upstream = JSON.parse(res.text); } catch { upstream = res.text.slice(0, 500); }
107
+ if (typeof upstream !== "string" && JSON.stringify(upstream).length > 1500) upstream = JSON.stringify(upstream).slice(0, 1500);
108
+ const code = upstream && typeof upstream === "object" && typeof upstream.error === "string" && /^[A-Za-z0-9_.:-]{1,64}$/.test(upstream.error) ? `: ${upstream.error}` : "";
109
+ const err = new Error(`${prefix}HTTP ${res.status}${code}`);
110
+ err.status = res.status;
111
+ err.upstream = upstream;
112
+ if (charged !== undefined) err.charged = charged;
113
+ else if (upstream && typeof upstream === "object" && typeof upstream.charged === "boolean") err.charged = upstream.charged;
114
+ return err;
115
+ }
116
+
102
117
  function parseJson(res) {
103
118
  try { return JSON.parse(res.text); } catch { throw new Error(`HTTP ${res.status}: non-JSON response`); }
104
119
  }
@@ -111,7 +126,7 @@ function createGuard(opts) {
111
126
  const init = { method, headers: { ...headers, ...(body ? { "content-type": "application/json" } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) };
112
127
  const res = await fetchBounded(url, init);
113
128
  if (res.status !== 402) {
114
- if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.text.slice(0, 200)}`);
129
+ if (!res.ok) throw httpError(res, "", false); // nothing was signed, so nothing was charged
115
130
  return parseJson(res);
116
131
  }
117
132
  let challenge;
@@ -119,7 +134,7 @@ function createGuard(opts) {
119
134
  const paymentRequired = httpClient.getPaymentRequiredResponse((n) => res.headers.get(n), challenge);
120
135
  const payload = await httpClient.createPaymentPayload(paymentRequired); // policy runs (and reserves budget) inside
121
136
  const paid = await fetchBounded(url, { ...init, headers: { ...init.headers, ...httpClient.encodePaymentSignatureHeader(payload) } });
122
- if (!paid.ok) throw new Error(`Payment failed — HTTP ${paid.status}: ${paid.text.slice(0, 200)}`);
137
+ if (!paid.ok) throw httpError(paid, "Payment failed — ");
123
138
  let settle = null;
124
139
  try { settle = httpClient.getPaymentSettleResponse((n) => paid.headers.get(n)) || null; } catch { settle = null; }
125
140
  const contentType = paid.headers.get("content-type") || "";
@@ -134,6 +149,7 @@ function createGuard(opts) {
134
149
  policy,
135
150
  fetchBounded,
136
151
  callPaid,
152
+ httpError,
137
153
  limits: () => ({ maxPriceUsd, sessionBudgetUsd, spentUsd, origin, payTo: [...payTo] }),
138
154
  };
139
155
  }