mpp32-mcp-server 1.5.0 → 1.8.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/CHANGELOG.md CHANGED
@@ -4,6 +4,104 @@ All notable changes to `mpp32-mcp-server` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.8.0] - 2026-07-14
8
+
9
+ ### Fixed
10
+
11
+ * **Tempo payments now actually work.** `completeTempoPayment` called
12
+ `client.pay(...)`, a method that does not exist on the mppx client —
13
+ every Tempo payment attempt threw `client.pay is not a function` before
14
+ a transaction was ever signed. The signer now uses the real mppx API:
15
+ `Mppx.create({ methods: [tempo({ account })], polyfill: false })
16
+ .createCredential(response)`, driven by the raw `WWW-Authenticate`
17
+ challenge header. Verified end-to-end against Tempo Mainnet (chain 4217):
18
+ the client parses the live challenge, signs a TIP-20 pathUSD transfer,
19
+ and the backend's on-chain verification accepts/rejects it correctly.
20
+ * **Authorization header format.** mppx credentials serialize as the full
21
+ `Payment <base64url>` header value; the old code re-prefixed it, producing
22
+ the invalid `Payment Payment <…>`. The credential is now set verbatim.
23
+ * **No more fetch polyfill side effect.** `Mppx.create` polyfills
24
+ `globalThis.fetch` by default; the signer now passes `polyfill: false` so
25
+ the MCP server's own fetch is never wrapped.
26
+
27
+ ### Changed
28
+
29
+ * Tempo (pathUSD) is now enabled in MPP32 production alongside x402 on
30
+ Solana and Base — `MPP32_PRIVATE_KEY` (0x-prefixed EVM key) pays both
31
+ x402-on-Base and Tempo challenges automatically.
32
+ * **`mppx` promoted from optional peer dependency to a pinned direct
33
+ dependency (`0.4.12`).** With Tempo live in production, an `npx` user
34
+ must never hit "Tempo payment client not available. Install: npm install
35
+ mppx viem" — the signer now works out of the box. Adds mppx's five
36
+ transitive dependencies; `viem` was already bundled.
37
+
38
+ ## [1.7.0] - 2026-06-07
39
+
40
+ ### Changed
41
+
42
+ * **Supply-chain cleanup.** Dropped three direct dependencies that were
43
+ driving Socket.dev alerts on the published package:
44
+ * `tweetnacl` (unmaintained since 2020): the 32-byte-seed → 64-byte
45
+ keypair derivation now uses
46
+ `@solana/kit`'s `createKeyPairSignerFromPrivateKeyBytes` directly
47
+ via WebCrypto Ed25519.
48
+ * `cheerio` (and its 21 transitives, including the deprecated
49
+ `whatwg-encoding@3.1.1`): `get_pivx_dao_intelligence` now calls the
50
+ MPP32 backend's `/api/governance` endpoint instead of scraping
51
+ `pivx.org` client-side. Same payload; the backend has served it since
52
+ 1.4.0.
53
+ * `bs58` + `base-x`: replaced with the already-in-tree `@scure/base`
54
+ `base58` codec.
55
+ Net effect: published install drops from 181 to ~155 transitive
56
+ packages, zero deprecated, zero unmaintained-since-2020.
57
+ * **All direct dependencies pinned to exact versions** (no `^` ranges).
58
+ * **`engines.node` raised to `>=20.10.0`** (Node 18 reached end-of-life
59
+ in April 2025; WebCrypto Ed25519 is native on 20.10+).
60
+
61
+ ### Added
62
+
63
+ * **npm provenance.** Releases now ship with an [npm provenance
64
+ attestation](https://docs.npmjs.com/generating-provenance-statements)
65
+ linking the tarball to the exact GitHub Actions run that built it.
66
+ Verify with `npm audit signatures`.
67
+ * **`SECURITY.md` included in the published tarball**, describing
68
+ runtime behavior, the env vars the server reads, and the egress
69
+ allow-list.
70
+ * **`docs/EGRESS.md`** (repo) enumerates every outbound host the MCP
71
+ server reaches and why.
72
+ * **`socket.yml`** (repo) documents the small set of behavior alerts we
73
+ consciously accept (`envVars`, `networkAccess`, `hasBin`) with links
74
+ to where each behavior is implemented.
75
+
76
+ ### Removed
77
+
78
+ * `pivx-provider.ts` (the cheerio-based client-side scraper). Replaced
79
+ by a 25-line wrapper around `${MPP32_API_URL}/api/governance`.
80
+
81
+ ### Security
82
+
83
+ * No code-level vulnerability fixed; this release exists to eliminate
84
+ supply-chain-risk surface area on the published package.
85
+
86
+ ## [1.6.0] - 2026-06-02
87
+
88
+ ### Added
89
+
90
+ * **Auto Sign In With Solana (SIWS) at startup.** When both `MPP32_AGENT_KEY`
91
+ and `MPP32_SOLANA_PRIVATE_KEY` are configured, the MCP server proves wallet
92
+ ownership to the MPP32 backend on the first run and activates M32 holder
93
+ pricing on every subsequent paid query for the rest of the process lifetime.
94
+ The signed message is the canonical SIWS structure (single use nonce, five
95
+ minute expiry, domain bound to mpp32.org). Holders pay $0.0048 per
96
+ Intelligence Oracle query at the 1M tier and $0.0064 at the 250K tier with
97
+ zero extra setup beyond the Solana key already used for x402 payments.
98
+
99
+ ### Changed
100
+
101
+ * **`get_mpp32_diagnostics` output adds an "M32 holder pricing" capability
102
+ row** showing the verified wallet, the tier, and the active discount once
103
+ SIWS has run.
104
+
7
105
  ## [1.5.0] - 2026-06-01
8
106
 
9
107
  ### Added
package/README.md CHANGED
@@ -17,16 +17,23 @@ One install. Pay any x402 endpoint on Solana from your agent. Browse a federated
17
17
  |:-----|:-------|:--------|:------|
18
18
  | x402 | Production | Solana (mainnet) | USDC |
19
19
  | x402 | Production | Base | USDC |
20
- | Tempo | Envelope wired, gated off in production until the client flow is verified end to end | Ethereum L2 | pathUSD |
21
- | ACP / AP2 / AGTP | Envelopes wired, gated off in production | Multi chain | per protocol |
20
+ | Tempo | Production | Tempo (mainnet, chain 4217) | pathUSD |
21
+ | AP2 / AGTP | Production (authorization and identity layers) | Chain agnostic | per protocol |
22
+ | ACP | Envelope wired, gated off in production | Multi chain | checkout session |
22
23
 
23
- The MCP server ships signers for both Solana and Base out of the box. When a paid call returns a 402, the server picks the network that matches the key you configured and settles the payment in one round trip.
24
+ The MCP server ships signers for Solana, Base, and Tempo out of the box. When a paid call returns a 402, the server picks the rail that matches the key you configured and settles the payment in one round trip. `MPP32_SOLANA_PRIVATE_KEY` pays x402 on Solana; `MPP32_PRIVATE_KEY` (0x-prefixed EVM key) pays x402 on Base and Tempo pathUSD challenges.
24
25
 
25
- ## Try it free first (no keys, no setup)
26
+ ## Free Tier: 10 calls/day, no wallet required
26
27
 
27
- The MCP server exposes a **`try_solana_token_intelligence_free`** tool that returns the full Intelligence Oracle payload (alpha score, rug risk, whale activity, smart money signals, 24h pump probability, market data) for any Solana token with **zero configuration** — no `MPP32_AGENT_KEY`, no Solana key, no payment. Rate-limited to 10 calls/minute per IP. Use it to evaluate the oracle, then switch to `get_solana_token_intelligence` for unlimited attributed usage at $0.008/query (M32 holders save up to 40%).
28
+ Every agent session gets **10 free Intelligence Oracle calls per day** no wallet, no USDC, no payment setup. Just set `MPP32_AGENT_KEY` (free, instant, no signup at [mpp32.org/agent-console](https://mpp32.org/agent-console)) and start calling `get_solana_token_intelligence`. Free tier resets at midnight UTC.
28
29
 
29
- Just install the server and ask your agent: _"Use mpp32 to get a free intelligence preview for SOL."_
30
+ This lets you **test your full MCP integration end-to-end** before committing any crypto. Build working prototypes, evaluate the data quality, and ship to production the free tier covers typical development and light usage indefinitely.
31
+
32
+ After free tier: $0.008/query paid via x402 (USDC on Solana). M32 holders save up to 40%.
33
+
34
+ ### Quick anonymous test (no keys at all)
35
+
36
+ The **`try_solana_token_intelligence_free`** tool returns the same payload with zero configuration — just install and call. Rate-limited to 10/minute per IP. Good for a quick taste, but the 10/day free tier with an agent key is better for real work.
30
37
 
31
38
  ## Why this beats running your own integrations
32
39
 
@@ -257,7 +264,7 @@ Sessions are scoped, revocable, and rotate cleanly. The key is hashed at rest on
257
264
 
258
265
  x402 payments are verified on chain through the Solana facilitator (for SVM) or the Base facilitator (for EVM). MPP32 never holds custody of funds. Every paid call settles directly from the caller's wallet to the provider's wallet.
259
266
 
260
- Tempo, ACP, AP2, and AGTP envelopes are implemented in the proxy and tested against synthetic challenges, but the corresponding client signing flows are not yet exposed by this MCP. Those rails stay disabled in production until each has a verified end to end client flow. Treat the catalog as an x402 catalog today; the rest will light up as their clients land.
267
+ Tempo payments are verified on chain by the proxy's mppx integration: the client signs a TIP-20 pathUSD transfer against the live challenge and the server confirms the transfer on Tempo mainnet before releasing the response. AP2 and AGTP run as live authorization and identity layers on every paid route. The ACP envelope is implemented and tested against synthetic challenges but stays disabled in production until its checkout-session client flow is verified end to end.
261
268
 
262
269
  ## For API providers
263
270
 
package/SECURITY.md ADDED
@@ -0,0 +1,77 @@
1
+ # Security Policy — `mpp32-mcp-server`
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Report security issues privately to **`security@mpp32.org`**.
6
+
7
+ Include:
8
+
9
+ - A description of the issue, including the affected version of
10
+ `mpp32-mcp-server`.
11
+ - Steps to reproduce, or a minimal proof of concept.
12
+ - The Node.js version, OS, and MCP host (Claude Desktop, Cursor,
13
+ Windsurf, …) you tested against.
14
+ - Your name or handle if you'd like credit in the fix release.
15
+
16
+ We aim to acknowledge reports within **3 business days** and provide a
17
+ remediation timeline within **10 business days**. Please do not file public
18
+ GitHub issues for security vulnerabilities until a fix has shipped.
19
+
20
+ ## Supported versions
21
+
22
+ | Version | Supported |
23
+ |:---------|:-----------|
24
+ | `1.7.x` | ✅ current |
25
+ | `1.6.x` | High-severity only |
26
+ | `< 1.6` | ❌ |
27
+
28
+ ## What this package does at runtime
29
+
30
+ Understanding the package's behavior is the first step in any audit. The
31
+ MCP server is a stdio process. It:
32
+
33
+ 1. **Reads three environment variables** for authentication and signing:
34
+ `MPP32_AGENT_KEY`, `MPP32_PRIVATE_KEY` (EVM), and
35
+ `MPP32_SOLANA_PRIVATE_KEY`. Values are never logged, transmitted as
36
+ plaintext to third parties, or written to disk. See
37
+ `src/index.ts` for the exact validation rules.
38
+ 2. **Makes outbound HTTPS requests** to a small, fixed set of hosts
39
+ needed to discover services and settle x402 payments. The complete
40
+ egress allow-list is documented in [`docs/EGRESS.md`](../docs/EGRESS.md).
41
+ 3. **Signs Solana and EVM payment payloads locally** using
42
+ `@solana/kit` (WebCrypto Ed25519) and `viem` (secp256k1) inside the
43
+ user's Node process. Private keys never leave the machine.
44
+
45
+ There is no install script, no native code, no filesystem write, no
46
+ shell execution, and no dynamic `require`/`eval` in this package.
47
+
48
+ ## Provenance
49
+
50
+ Starting with `1.7.0`, npm releases are published from a GitHub Actions
51
+ workflow using OIDC and include an [npm provenance attestation][prov]
52
+ linking the published tarball to the exact commit and workflow run that
53
+ produced it. Verify with:
54
+
55
+ ```sh
56
+ npm audit signatures
57
+ ```
58
+
59
+ [prov]: https://docs.npmjs.com/generating-provenance-statements
60
+
61
+ ## Hardening already in place (backend)
62
+
63
+ The MCP server depends on a backend at `mpp32.org` for the federated
64
+ catalog and `/api/agent/execute`. Backend-side hardening is documented
65
+ in the [root SECURITY.md](../SECURITY.md):
66
+
67
+ - Production refuses to boot when `MPP_SECRET_KEY` is missing or matches
68
+ a committed default.
69
+ - All outbound URLs from user submissions and agent execute calls run
70
+ through an SSRF guard.
71
+ - Agent session API keys are hashed at rest with SHA-256.
72
+
73
+ ## Disclosure
74
+
75
+ We follow coordinated disclosure. Reporters who act in good faith and
76
+ follow this policy will not be subject to legal action for their
77
+ research.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { signX402Payment } from "./x402-signers.js";
6
- const SERVER_VERSION = "1.5.0";
6
+ const SERVER_VERSION = "1.8.0";
7
7
  // ── Env loading: trim and sanitize aggressively ─────────────────────────────
8
8
  // Copy-paste from Claude Desktop / Cursor / Windsurf JSON config UIs frequently
9
9
  // adds trailing \n, \r, NBSP, BOM, or wraps the value in literal quotes. Any
@@ -171,7 +171,11 @@ const server = new McpServer({
171
171
  version: SERVER_VERSION,
172
172
  });
173
173
  function buildHeaders(extra = {}) {
174
- const headers = {};
174
+ const headers = {
175
+ // Identifies this surface to the backend's usage tracking so MCP traffic
176
+ // can be measured separately from web/SDK/direct-API traffic.
177
+ "X-MPP32-Client": `mcp-server/${SERVER_VERSION}`,
178
+ };
175
179
  for (const [k, v] of Object.entries(extra)) {
176
180
  headers[k] = safeHeaderValue(k, v);
177
181
  }
@@ -181,6 +185,12 @@ function buildHeaders(extra = {}) {
181
185
  return headers;
182
186
  }
183
187
  function isHttpCallable(svc) {
188
+ // Prefer the server's authoritative `callable` flag when provided. The server
189
+ // knows things the URL cannot tell us — e.g. a native service that hasn't
190
+ // completed endpoint verification (the proxy will 403 it) or an M32
191
+ // token-gated service that needs an on-chain balance proof.
192
+ if (typeof svc.callable === "boolean")
193
+ return svc.callable;
184
194
  if (svc.source === "native")
185
195
  return true;
186
196
  const url = svc.endpointUrl ?? "";
@@ -190,6 +200,21 @@ function isHttpCallable(svc) {
190
200
  return false;
191
201
  return /^https?:\/\//.test(url);
192
202
  }
203
+ // Human-readable explanation for why a service is not callable through this MCP.
204
+ function notCallableLabel(svc) {
205
+ switch (svc.callableReason) {
206
+ case "pending_verification":
207
+ return "No — provider hasn't completed endpoint verification yet";
208
+ case "m32_token_gated":
209
+ return svc.m32Required
210
+ ? `Token-gated — hold ${svc.m32Required.toLocaleString()}+ M32 and use the dedicated tool`
211
+ : "Token-gated — requires M32 holdings via the dedicated tool";
212
+ case "listing_only":
213
+ return "No — listing only";
214
+ default:
215
+ return "No — listing only";
216
+ }
217
+ }
193
218
  // ── Tool 0: get_mpp32_diagnostics ───────────────────────────────────────────
194
219
  // Lets the user (and Claude) see exactly what the MCP process detected at
195
220
  // startup. The single most common failure mode is "I set the env var but it
@@ -251,8 +276,10 @@ server.tool("get_mpp32_diagnostics", "Report what the mpp32-mcp-server detected
251
276
  `- Federated service execution: ${AGENT_KEY ? "yes" : "no — set MPP32_AGENT_KEY"}`,
252
277
  `- x402 (USDC on Solana) payment: ${SOLANA_PRIVATE_KEY ? "yes" : "no — set MPP32_SOLANA_PRIVATE_KEY"}`,
253
278
  `- x402 (USDC on Base/EVM) payment: ${PRIVATE_KEY ? "yes" : "no — set MPP32_PRIVATE_KEY"}`,
279
+ `- M32 holder pricing (SIWS verified): ${siwsVerifiedAddress ? `yes — ${siwsTier} tier, ${siwsDiscountPercent}% off every paid query` : (AGENT_KEY && SOLANA_PRIVATE_KEY ? "pending — auto verification runs once at startup" : "no — set MPP32_AGENT_KEY + MPP32_SOLANA_PRIVATE_KEY")}`,
254
280
  ``,
255
- `**Ready to pay end-to-end:** ${readyToPay ? "YES — try `get_solana_token_intelligence` with token=\"M32\" to confirm." : "NO see the missing items above. Meanwhile, you can still call `try_solana_token_intelligence_free` (10/min/IP, no keys required) to evaluate the oracle."}`,
281
+ `**Ready to use:** ${AGENT_KEY ? "YES — you have 10 FREE Intelligence Oracle calls/day. Try `get_solana_token_intelligence` with token=\"M32\" now." : "SET MPP32_AGENT_KEY to get 10 FREE calls/day. Get one at " + API_URL + "/agent-console."}`,
282
+ `**Ready to pay (after free tier):** ${readyToPay ? "YES — x402 signing configured." : "NO — set MPP32_SOLANA_PRIVATE_KEY (or MPP32_PRIVATE_KEY for EVM) to pay after free tier exhausted."}`,
256
283
  ``,
257
284
  `**If a variable shows NOT SET but you set it in claude_desktop_config.json:**`,
258
285
  `1. Confirm the file path Claude Desktop actually reads:`,
@@ -369,7 +396,7 @@ server.tool("list_mpp32_services", "Browse the MPP32 federated catalog of 4,500+
369
396
  `- **Category:** ${s.category ?? "—"}`,
370
397
  `- **Price:** ${priceLabel}`,
371
398
  `- **Protocols:** ${protos}`,
372
- `- **Callable via this MCP:** ${callable ? "Yes — use `call_mpp32_endpoint`" : "No — listing only"}`,
399
+ `- **Callable via this MCP:** ${callable ? "Yes — use `call_mpp32_endpoint`" : notCallableLabel(s)}`,
373
400
  s.description ? `- **Description:** ${s.description}` : null,
374
401
  s.endpointUrl && !callable ? `- **Install / direct URL:** \`${s.endpointUrl}\`` : null,
375
402
  s.websiteUrl ? `- **Website:** ${s.websiteUrl}` : null,
@@ -450,7 +477,7 @@ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32
450
477
  return await callViaLegacyProxy(slug, method, parsedBody, query, path);
451
478
  });
452
479
  // ── Tool 3: get_solana_token_intelligence ───────────────────────────────────
453
- server.tool("get_solana_token_intelligence", "Get real-time Solana token intelligence from the MPP32 Intelligence Oracle. Returns alpha score (0-100), rug risk assessment, whale activity, smart money signals, 24h pump probability, projected ROI ranges, and aggregated DexScreener/Jupiter/CoinGecko market data. Costs $0.008 per query, paid automatically via x402 (USDC on Solana) or Tempo (pathUSD on Eth L2). M32 token holders receive up to 40% discount once their wallet is signature-verified. Set MPP32_AGENT_KEY in config to attribute calls to your dashboard. **New users with no keys configured: call `try_solana_token_intelligence_free` first** — it returns the same payload, rate-limited to 10/min, no payment or key required.", {
480
+ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelligence from the MPP32 Intelligence Oracle. Returns alpha score (0-100), rug risk assessment, whale activity, smart money signals, 24h pump probability, projected ROI ranges, and aggregated DexScreener/Jupiter/CoinGecko market data. **FREE TIER: Every agent session gets 10 free calls per day no wallet, no USDC, no payment setup required.** Just set MPP32_AGENT_KEY. After free tier: $0.008/query paid via x402 (USDC on Solana). M32 token holders receive up to 40% discount.", {
454
481
  token: z
455
482
  .string()
456
483
  .describe("Solana token mint address or ticker symbol (e.g. SOL, BONK, JUP, M32, or full base58 address)."),
@@ -472,7 +499,7 @@ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelli
472
499
  // trying MPP32 — they see real alpha scores and signals BEFORE encountering
473
500
  // any payment wall. This is the conversion funnel fix: agents today bounce
474
501
  // off the 402, so we let them taste the product first.
475
- server.tool("try_solana_token_intelligence_free", "FREE preview of the MPP32 Intelligence Oracle. No payment, no agent key, no Solana private key required. Returns the SAME payload as the paid endpoint (alpha score, rug risk, whale activity, smart money signals, pump probability, market data) for any Solana token. Rate-limited to 10 calls/minute per IP. Use this to evaluate the data quality. Once you're convinced, set MPP32_AGENT_KEY + a payment key and switch to `get_solana_token_intelligence` for unlimited, attributed usage and M32-holder discounts.", {
502
+ server.tool("try_solana_token_intelligence_free", "FREE preview of the MPP32 Intelligence Oracle quick anonymous test, no keys required. Rate-limited to 10 calls/minute per IP. Returns the same payload as the paid endpoint. **Better option: get an MPP32_AGENT_KEY from mpp32.org/agent-console and call `get_solana_token_intelligence` instead you get 10 FREE attributed calls per day, plus dashboard tracking, before any payment is needed.**", {
476
503
  token: z
477
504
  .string()
478
505
  .describe("Solana token mint address or ticker symbol (e.g. SOL, BONK, JUP, M32, or full base58 address)."),
@@ -480,7 +507,7 @@ server.tool("try_solana_token_intelligence_free", "FREE preview of the MPP32 Int
480
507
  try {
481
508
  const res = await fetchWithTimeout(`${API_URL}/api/intelligence/demo`, {
482
509
  method: "POST",
483
- headers: { "Content-Type": "application/json" },
510
+ headers: buildHeaders({ "Content-Type": "application/json" }),
484
511
  body: JSON.stringify({ token }),
485
512
  });
486
513
  const text = await res.text();
@@ -495,7 +522,7 @@ server.tool("try_solana_token_intelligence_free", "FREE preview of the MPP32 Int
495
522
  return {
496
523
  content: [{
497
524
  type: "text",
498
- text: `Demo rate limit reached (10 calls/minute per IP). Wait a minute and retry, or set up paid access for unlimited queries:\n\n1. Get an agent key: ${API_URL}/agent-console\n2. Add MPP32_AGENT_KEY + MPP32_SOLANA_PRIVATE_KEY to your MCP config\n3. Call \`get_solana_token_intelligence\` instead $0.008/query, M32 holders save up to 40%.`,
525
+ text: `Demo rate limit reached (10 calls/minute per IP). **Better option:** Get a free agent key at ${API_URL}/agent-console 10 FREE attributed calls/day with dashboard tracking, no payment setup required. After free tier, $0.008/query.`,
499
526
  }],
500
527
  };
501
528
  }
@@ -631,11 +658,25 @@ server.tool("scan_portfolio_m32", "M32-gated full wallet portfolio scan. Discove
631
658
  return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
632
659
  }
633
660
  });
634
- // ── Tool 7: get_pivx_dao_intelligence ─────────────────────────────────────
635
- // Self-contained: scrapes pivx.org/proposals + Chainz CryptoID directly.
636
- // Does NOT depend on any backend endpoint — works for every npm user out of the box.
637
- import { fetchPivxGovernance } from "./pivx-provider.js";
638
- server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance intelligence. Returns active budget proposals with masternode voting tallies (Yes/No counts, net yes percentages), budget allocation status, network deflation metrics (unallocated treasury PIV that are never minted), and masternode network health. PIVX is a fully community-governed cryptocurrency where Masternode owners vote on budget proposals every ~30 days (43,200 blocks per superblock cycle, 432,000 PIV max monthly budget). Data scraped live from pivx.org/proposals and the PIVX blockchain via Chainz CryptoID. Cached for 5 minutes. Free — no payment or API key required.", {
661
+ async function fetchPivxGovernance() {
662
+ const res = await fetchWithTimeout(`${API_URL}/api/governance`, {
663
+ timeoutMs: 15_000,
664
+ headers: buildHeaders({ Accept: "application/json" }),
665
+ });
666
+ if (!res.ok) {
667
+ throw new Error(`MPP32 governance endpoint returned HTTP ${res.status}`);
668
+ }
669
+ const { data } = (await res.json());
670
+ return {
671
+ proposals: data.proposals,
672
+ network: data.network,
673
+ deflation: data.deflation,
674
+ timestamp: data.meta.timestamp,
675
+ source: data.meta.source,
676
+ cacheHit: data.meta.cacheHit,
677
+ };
678
+ }
679
+ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance intelligence. Returns active budget proposals with masternode voting tallies (Yes/No counts, net yes percentages), budget allocation status, network deflation metrics (unallocated treasury PIV that are never minted), and masternode network health. PIVX is a fully community-governed cryptocurrency where Masternode owners vote on budget proposals every ~30 days (43,200 blocks per superblock cycle, 432,000 PIV max monthly budget). Data is served by the MPP32 backend, which aggregates pivx.org/proposals and the PIVX blockchain via Chainz CryptoID, and caches for 5 minutes. Free — no payment or API key required.", {
639
680
  filter: z
640
681
  .enum(["all", "passing", "failing"])
641
682
  .default("all")
@@ -712,7 +753,7 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
712
753
  return {
713
754
  content: [{
714
755
  type: "text",
715
- text: `Failed to fetch PIVX governance data: ${err instanceof Error ? err.message : String(err)}. The tool scrapes pivx.org/proposals directly — the site may be temporarily unreachable.`,
756
+ text: `Failed to fetch PIVX governance data: ${err instanceof Error ? err.message : String(err)}. The tool calls ${API_URL}/api/governance — the MPP32 backend or its upstream sources (pivx.org, chainz.cryptoid.info) may be temporarily unreachable.`,
716
757
  }],
717
758
  };
718
759
  }
@@ -901,10 +942,9 @@ async function signAndRetry(execUrl, reqBody, challenge) {
901
942
  catch (err) {
902
943
  // Fall through to Tempo if available
903
944
  if (challenge.wwwAuthenticate && PRIVATE_KEY) {
904
- const parsed = parseWwwAuthenticate(challenge.wwwAuthenticate);
905
945
  try {
906
- const token = await completeTempoPayment(parsed.params, PRIVATE_KEY);
907
- paymentHeaders["Authorization"] = `Payment ${token}`;
946
+ const token = await completeTempoPayment(challenge.wwwAuthenticate, PRIVATE_KEY);
947
+ paymentHeaders["Authorization"] = token;
908
948
  usedProtocol = "pathUSD (Tempo)";
909
949
  }
910
950
  catch (tempoErr) {
@@ -929,8 +969,8 @@ async function signAndRetry(execUrl, reqBody, challenge) {
929
969
  };
930
970
  }
931
971
  try {
932
- const token = await completeTempoPayment(parsed.params, PRIVATE_KEY);
933
- paymentHeaders["Authorization"] = `Payment ${token}`;
972
+ const token = await completeTempoPayment(challenge.wwwAuthenticate, PRIVATE_KEY);
973
+ paymentHeaders["Authorization"] = token;
934
974
  usedProtocol = "pathUSD (Tempo)";
935
975
  }
936
976
  catch (err) {
@@ -1237,10 +1277,9 @@ async function callViaLegacyProxy(slug, method, body, query, path) {
1237
1277
  }
1238
1278
  catch (err) {
1239
1279
  if (wwwAuth && PRIVATE_KEY) {
1240
- const parsed = parseWwwAuthenticate(wwwAuth);
1241
1280
  try {
1242
- const token = await completeTempoPayment(parsed.params, PRIVATE_KEY);
1243
- paymentHeaders["Authorization"] = `Payment ${token}`;
1281
+ const token = await completeTempoPayment(wwwAuth, PRIVATE_KEY);
1282
+ paymentHeaders["Authorization"] = token;
1244
1283
  usedProtocol = "pathUSD (Tempo)";
1245
1284
  }
1246
1285
  catch (te) {
@@ -1253,10 +1292,9 @@ async function callViaLegacyProxy(slug, method, body, query, path) {
1253
1292
  }
1254
1293
  }
1255
1294
  else if (wwwAuth && PRIVATE_KEY) {
1256
- const parsed = parseWwwAuthenticate(wwwAuth);
1257
1295
  try {
1258
- const token = await completeTempoPayment(parsed.params, PRIVATE_KEY);
1259
- paymentHeaders["Authorization"] = `Payment ${token}`;
1296
+ const token = await completeTempoPayment(wwwAuth, PRIVATE_KEY);
1297
+ paymentHeaders["Authorization"] = token;
1260
1298
  usedProtocol = "pathUSD (Tempo)";
1261
1299
  }
1262
1300
  catch (err) {
@@ -1383,9 +1421,8 @@ async function legacyIntelligenceCall(token, walletAddress) {
1383
1421
  catch (x402Err) {
1384
1422
  if (wwwAuth && PRIVATE_KEY) {
1385
1423
  try {
1386
- const parsed = parseWwwAuthenticate(wwwAuth);
1387
- const tempoToken = await completeTempoPayment(parsed.params, PRIVATE_KEY);
1388
- paymentHeaders["Authorization"] = `Payment ${tempoToken}`;
1424
+ const tempoToken = await completeTempoPayment(wwwAuth, PRIVATE_KEY);
1425
+ paymentHeaders["Authorization"] = tempoToken;
1389
1426
  usedProtocol = "pathUSD (Tempo)";
1390
1427
  }
1391
1428
  catch (tempoErr) {
@@ -1407,9 +1444,8 @@ async function legacyIntelligenceCall(token, walletAddress) {
1407
1444
  }
1408
1445
  else if (wwwAuth && PRIVATE_KEY) {
1409
1446
  try {
1410
- const parsed = parseWwwAuthenticate(wwwAuth);
1411
- const tempoToken = await completeTempoPayment(parsed.params, PRIVATE_KEY);
1412
- paymentHeaders["Authorization"] = `Payment ${tempoToken}`;
1447
+ const tempoToken = await completeTempoPayment(wwwAuth, PRIVATE_KEY);
1448
+ paymentHeaders["Authorization"] = tempoToken;
1413
1449
  usedProtocol = "pathUSD (Tempo)";
1414
1450
  }
1415
1451
  catch (tempoErr) {
@@ -1475,7 +1511,11 @@ function parseWwwAuthenticate(header) {
1475
1511
  }
1476
1512
  return { scheme, params };
1477
1513
  }
1478
- async function completeTempoPayment(challengeParams, privateKey) {
1514
+ // Signs a Tempo TIP-20 transfer for the challenge carried in a 402 response's
1515
+ // raw WWW-Authenticate header and returns the FULL Authorization header value
1516
+ // ("Payment <b64>", mppx Credential.serialize format) — callers must set it
1517
+ // verbatim, never re-prefix with "Payment ".
1518
+ async function completeTempoPayment(wwwAuthenticateHeader, privateKey) {
1479
1519
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1480
1520
  let mppxClient;
1481
1521
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1491,10 +1531,16 @@ async function completeTempoPayment(challengeParams, privateKey) {
1491
1531
  }
1492
1532
  try {
1493
1533
  const account = viemAccounts.privateKeyToAccount(privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`);
1534
+ // polyfill: false — never clobber the host process's globalThis.fetch.
1494
1535
  const client = mppxClient.Mppx.create({
1495
1536
  methods: [mppxClient.tempo({ account })],
1537
+ polyfill: false,
1538
+ });
1539
+ const challengeResponse = new Response(null, {
1540
+ status: 402,
1541
+ headers: { "WWW-Authenticate": wwwAuthenticateHeader },
1496
1542
  });
1497
- return (await client.pay(challengeParams));
1543
+ return (await client.createCredential(challengeResponse));
1498
1544
  }
1499
1545
  catch (payErr) {
1500
1546
  throw new Error(`Tempo payment failed: ${payErr instanceof Error ? payErr.message : String(payErr)}`);
@@ -1521,6 +1567,95 @@ async function completeX402Payment(paymentRequiredHeader, keys) {
1521
1567
  protocolUsed: result.protocolUsed,
1522
1568
  };
1523
1569
  }
1570
+ // ── Auto SIWS bootstrap ─────────────────────────────────────────────────────
1571
+ // When both MPP32_AGENT_KEY and MPP32_SOLANA_PRIVATE_KEY are configured the
1572
+ // MCP server proves wallet ownership to the MPP32 backend at startup, which
1573
+ // activates M32 holder pricing on every subsequent paid query for the rest of
1574
+ // the process lifetime. No user action required.
1575
+ let siwsVerifiedAddress = null;
1576
+ let siwsTier = null;
1577
+ let siwsDiscountPercent = 0;
1578
+ async function tryAutoSiws() {
1579
+ if (!AGENT_KEY || !SOLANA_PRIVATE_KEY)
1580
+ return;
1581
+ try {
1582
+ // Lazy import to keep startup fast when only catalog browsing is needed.
1583
+ const [kitMod, scureBase] = await Promise.all([
1584
+ import("@solana/kit"),
1585
+ import("@scure/base"),
1586
+ ]);
1587
+ const { base58 } = scureBase;
1588
+ const { createKeyPairFromBytes, createKeyPairFromPrivateKeyBytes, getAddressFromPublicKey, signBytes, } = kitMod;
1589
+ // Decode the private key. Supports JSON byte array, hex, and base58.
1590
+ let bytes;
1591
+ if (SOLANA_PRIVATE_KEY.startsWith("[")) {
1592
+ bytes = new Uint8Array(JSON.parse(SOLANA_PRIVATE_KEY));
1593
+ }
1594
+ else if (/^[0-9a-fA-F]+$/.test(SOLANA_PRIVATE_KEY) && SOLANA_PRIVATE_KEY.length % 2 === 0) {
1595
+ bytes = new Uint8Array(Buffer.from(SOLANA_PRIVATE_KEY, "hex"));
1596
+ }
1597
+ else {
1598
+ bytes = base58.decode(SOLANA_PRIVATE_KEY);
1599
+ }
1600
+ let keyPair;
1601
+ if (bytes.length === 32) {
1602
+ keyPair = await createKeyPairFromPrivateKeyBytes(bytes);
1603
+ }
1604
+ else if (bytes.length === 64) {
1605
+ keyPair = await createKeyPairFromBytes(bytes);
1606
+ }
1607
+ else {
1608
+ console.error(`[mpp32] SIWS skipped: Solana key has unexpected length ${bytes.length}`);
1609
+ return;
1610
+ }
1611
+ const walletAddress = await getAddressFromPublicKey(keyPair.publicKey);
1612
+ // Step 1: request a nonce bound to our existing agent session.
1613
+ const nonceRes = await fetchWithTimeout(`${API_URL}/api/auth/siws/nonce`, {
1614
+ method: "POST",
1615
+ headers: { "Content-Type": "application/json" },
1616
+ body: JSON.stringify({ wallet: walletAddress, agentKey: AGENT_KEY }),
1617
+ timeoutMs: 8_000,
1618
+ });
1619
+ if (!nonceRes.ok) {
1620
+ const text = await nonceRes.text().catch(() => "");
1621
+ console.error(`[mpp32] SIWS nonce request failed: HTTP ${nonceRes.status} ${text.slice(0, 200)}`);
1622
+ return;
1623
+ }
1624
+ const nonceBody = (await nonceRes.json());
1625
+ const message = nonceBody.data?.message;
1626
+ if (!message) {
1627
+ console.error("[mpp32] SIWS nonce response missing message");
1628
+ return;
1629
+ }
1630
+ // Step 2: sign the canonical message bytes via WebCrypto Ed25519.
1631
+ const signatureBytes = await signBytes(keyPair.privateKey, new TextEncoder().encode(message));
1632
+ const signature = base58.encode(signatureBytes);
1633
+ // Step 3: verify with the backend. Backend marks session walletVerified=true.
1634
+ const verifyRes = await fetchWithTimeout(`${API_URL}/api/auth/siws/verify`, {
1635
+ method: "POST",
1636
+ headers: { "Content-Type": "application/json" },
1637
+ body: JSON.stringify({ wallet: walletAddress, signature, agentKey: AGENT_KEY }),
1638
+ timeoutMs: 8_000,
1639
+ });
1640
+ if (!verifyRes.ok) {
1641
+ const text = await verifyRes.text().catch(() => "");
1642
+ console.error(`[mpp32] SIWS verify failed: HTTP ${verifyRes.status} ${text.slice(0, 200)}`);
1643
+ return;
1644
+ }
1645
+ const verifyBody = (await verifyRes.json());
1646
+ siwsVerifiedAddress = verifyBody.data?.walletAddress ?? walletAddress;
1647
+ siwsTier = verifyBody.data?.tier ?? "none";
1648
+ siwsDiscountPercent = verifyBody.data?.discountPercent ?? 0;
1649
+ const tierLabel = siwsDiscountPercent > 0
1650
+ ? `${siwsTier} tier (${siwsDiscountPercent}% off every paid query)`
1651
+ : "no holder tier (wallet holds zero M32, verification still active)";
1652
+ const shortAddr = `${siwsVerifiedAddress.slice(0, 6)}…${siwsVerifiedAddress.slice(-4)}`;
1653
+ console.error(`[mpp32] SIWS verified for ${shortAddr}: ${tierLabel}`);
1654
+ }
1655
+ catch (err) {
1656
+ console.error(`[mpp32] SIWS bootstrap error: ${err instanceof Error ? err.message : String(err)}`);
1657
+ }
1658
+ }
1524
1659
  // ── Start ───────────────────────────────────────────────────────────────────
1525
1660
  async function main() {
1526
1661
  const transport = new StdioServerTransport();
@@ -1533,6 +1668,9 @@ async function main() {
1533
1668
  .filter(Boolean)
1534
1669
  .join(", ") || "no keys (catalog-only legacy mode)";
1535
1670
  console.error(`[mpp32] MCP server v${SERVER_VERSION} on stdio. API ${API_URL}. Configured: ${features}. Timeout ${TIMEOUT_MS}ms.`);
1671
+ // Auto SIWS in the background. Does not block startup — if it fails the user
1672
+ // simply pays the standard rate instead of the holder rate.
1673
+ void tryAutoSiws();
1536
1674
  // Per-variable status so a user staring at this log can immediately see
1537
1675
  // whether their env vars made it through. Values are fingerprinted.
1538
1676
  const fp = (v) => !v ? "NOT SET" : v.length <= 12 ? `SET (${v.length}c)` : `SET (${v.slice(0, 6)}…${v.slice(-4)}, ${v.length}c)`;
@@ -25,7 +25,7 @@ export interface X402PaymentEnvelope {
25
25
  }
26
26
  export declare function isSvmNetwork(network: string): boolean;
27
27
  export declare function isEvmNetwork(network: string): boolean;
28
- export declare function signX402PaymentSvm(requirements: X402PaymentRequirements, rawKey: string, rpcUrlOverride?: string, echoedVersion?: number): Promise<string>;
28
+ export declare function signX402PaymentSvm(requirements: X402PaymentRequirements, rawKey: string, rpcUrlOverride?: string, echoedVersion?: number, computeUnitLimitOverride?: number): Promise<string>;
29
29
  export declare function signX402PaymentEvm(requirements: X402PaymentRequirements, rawKey: string, echoedVersion?: number): Promise<string>;
30
30
  export interface SignX402Args {
31
31
  paymentRequiredHeader: string;
@@ -18,12 +18,11 @@
18
18
  // { x402Version: 1, scheme: "exact", network, payload: <scheme payload> }
19
19
  // base64-encoded into the `X-Payment` HTTP header.
20
20
  async function loadSvmDeps() {
21
- const [kit, tokenProgram, computeBudgetProgram, bs58Mod, naclMod] = await Promise.all([
21
+ const [kit, tokenProgram, computeBudgetProgram, scureBase] = await Promise.all([
22
22
  import("@solana/kit"),
23
23
  import("@solana-program/token"),
24
24
  import("@solana-program/compute-budget"),
25
- import("bs58"),
26
- import("tweetnacl"),
25
+ import("@scure/base"),
27
26
  ]).catch((err) => {
28
27
  const msg = err instanceof Error ? err.message : String(err);
29
28
  throw new Error(`Could not load Solana signing libraries: ${msg}. ` +
@@ -33,6 +32,7 @@ async function loadSvmDeps() {
33
32
  return {
34
33
  address: kit.address,
35
34
  createKeyPairSignerFromBytes: kit.createKeyPairSignerFromBytes,
35
+ createKeyPairSignerFromPrivateKeyBytes: kit.createKeyPairSignerFromPrivateKeyBytes,
36
36
  createSolanaRpc: kit.createSolanaRpc,
37
37
  createTransactionMessage: kit.createTransactionMessage,
38
38
  setTransactionMessageFeePayer: kit.setTransactionMessageFeePayer,
@@ -46,8 +46,7 @@ async function loadSvmDeps() {
46
46
  TOKEN_PROGRAM_ADDRESS: tokenProgram.TOKEN_PROGRAM_ADDRESS,
47
47
  getSetComputeUnitLimitInstruction: computeBudgetProgram.getSetComputeUnitLimitInstruction,
48
48
  getSetComputeUnitPriceInstruction: computeBudgetProgram.getSetComputeUnitPriceInstruction,
49
- bs58: bs58Mod.default ?? bs58Mod,
50
- nacl: naclMod.default ?? naclMod,
49
+ base58: scureBase.base58,
51
50
  };
52
51
  }
53
52
  async function loadEvmDeps() {
@@ -94,24 +93,23 @@ function decodeSolanaSecret(raw, deps) {
94
93
  if (/^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0) {
95
94
  return new Uint8Array(Buffer.from(raw, "hex"));
96
95
  }
97
- return deps.bs58.decode(raw);
96
+ return deps.base58.decode(raw);
98
97
  }
99
98
  async function buildSolanaSigner(rawKey, deps) {
100
- let bytes = decodeSolanaSecret(rawKey, deps);
99
+ const bytes = decodeSolanaSecret(rawKey, deps);
101
100
  if (bytes.length === 32) {
102
- // 32-byte seed — kit's createKeyPairSignerFromBytes wants the 64-byte
103
- // expanded key. Derive via tweetnacl.
104
- const kp = deps.nacl.sign.keyPair.fromSeed(bytes);
105
- bytes = kp.secretKey;
101
+ // 32-byte seed — kit derives the public key via WebCrypto Ed25519.
102
+ return await deps.createKeyPairSignerFromPrivateKeyBytes(bytes);
106
103
  }
107
- else if (bytes.length !== 64) {
108
- throw new Error(`Solana private key must be a 32-byte seed or a 64-byte expanded key; got ${bytes.length} bytes.`);
104
+ if (bytes.length === 64) {
105
+ // 64-byte expanded key (seed || publicKey) kit's standard path.
106
+ return await deps.createKeyPairSignerFromBytes(bytes);
109
107
  }
110
- return await deps.createKeyPairSignerFromBytes(bytes);
108
+ throw new Error(`Solana private key must be a 32-byte seed or a 64-byte expanded key; got ${bytes.length} bytes.`);
111
109
  }
112
110
  // ── SVM signer ──────────────────────────────────────────────────────────────
113
111
  const DEFAULT_SOLANA_RPC = "https://api.mainnet-beta.solana.com";
114
- export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride, echoedVersion = 1) {
112
+ export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride, echoedVersion = 1, computeUnitLimitOverride) {
115
113
  if (requirements.scheme !== "exact") {
116
114
  throw new Error(`SVM x402 scheme "${requirements.scheme}" not implemented; only "exact" is supported.`);
117
115
  }
@@ -160,7 +158,12 @@ export async function signX402PaymentSvm(requirements, rawKey, rpcUrlOverride, e
160
158
  const rpc = deps.createSolanaRpc(rpcUrl);
161
159
  const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: "confirmed" }).send();
162
160
  const instructions = [
163
- deps.getSetComputeUnitLimitInstruction({ units: 150_000 }),
161
+ // Compute-unit limit must stay under the facilitator's fee-payer cap. PayAI
162
+ // rejects anything above ~60,000 CU as `compute_limit_too_high` (it pays the
163
+ // fee, so it bounds the budget). 50,000 is comfortably under that cap and far
164
+ // above the ~7,000 CU an SPL TransferChecked + compute-budget ixs consume.
165
+ // (Verified against PayAI /verify 2026-06-17: 150,000 was rejected outright.)
166
+ deps.getSetComputeUnitLimitInstruction({ units: computeUnitLimitOverride ?? 50_000 }),
164
167
  deps.getSetComputeUnitPriceInstruction({ microLamports: 1000n }),
165
168
  deps.getTransferCheckedInstruction({
166
169
  source: sourceAta,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpp32-mcp-server",
3
- "version": "1.5.0",
3
+ "version": "1.8.0",
4
4
  "mcpName": "io.github.MPP32/mpp32-mcp-server",
5
5
  "description": "Payment layer for AI agents. One MCP, five protocols, thousands of paid APIs your agent can call.",
6
6
  "type": "module",
@@ -20,7 +20,8 @@
20
20
  "dist/**/*.d.ts",
21
21
  "README.md",
22
22
  "CHANGELOG.md",
23
- "LICENSE"
23
+ "LICENSE",
24
+ "SECURITY.md"
24
25
  ],
25
26
  "sideEffects": false,
26
27
  "scripts": {
@@ -67,30 +68,26 @@
67
68
  "bugs": {
68
69
  "url": "https://github.com/MPP32/MPP32/issues"
69
70
  },
71
+ "funding": "https://mpp32.org",
70
72
  "engines": {
71
- "node": ">=18.0.0"
73
+ "node": ">=20.10.0"
72
74
  },
73
- "dependencies": {
74
- "@modelcontextprotocol/sdk": "^1.12.0",
75
- "@solana-program/compute-budget": "^0.15.0",
76
- "@solana-program/token": "^0.13.0",
77
- "@solana/kit": "^6.9.0",
78
- "bs58": "^6.0.0",
79
- "cheerio": "^1.2.0",
80
- "tweetnacl": "^1.0.3",
81
- "viem": "^2.48.11",
82
- "zod": "^3.23.0"
83
- },
84
- "peerDependencies": {
85
- "mppx": ">=0.4.0"
75
+ "publishConfig": {
76
+ "access": "public",
77
+ "provenance": true
86
78
  },
87
- "peerDependenciesMeta": {
88
- "mppx": {
89
- "optional": true
90
- }
79
+ "dependencies": {
80
+ "@modelcontextprotocol/sdk": "1.29.0",
81
+ "@scure/base": "1.2.6",
82
+ "@solana-program/compute-budget": "0.15.0",
83
+ "@solana-program/token": "0.13.0",
84
+ "@solana/kit": "6.9.0",
85
+ "mppx": "0.4.12",
86
+ "viem": "2.52.2",
87
+ "zod": "3.25.76"
91
88
  },
92
89
  "devDependencies": {
93
- "@types/node": "^22.0.0",
94
- "typescript": "^5.7.0"
90
+ "@types/node": "22.19.18",
91
+ "typescript": "5.9.3"
95
92
  }
96
93
  }
@@ -1,42 +0,0 @@
1
- export interface PivxProposal {
2
- name: string;
3
- url: string;
4
- status: "passing" | "failing";
5
- funded: boolean;
6
- netYesPercent: number;
7
- yesVotes: number;
8
- noVotes: number;
9
- monthlyPaymentPiv: number;
10
- monthlyPaymentUsd: number;
11
- totalPaymentPiv: number;
12
- installmentsRemaining: number;
13
- totalInstallments: number;
14
- budgetPercent: number;
15
- }
16
- export interface PivxNetworkStats {
17
- masternodeCount: number;
18
- passingThreshold: number;
19
- monthlyBudgetPiv: number;
20
- monthlyBudgetUsd: number;
21
- budgetAllocatedPiv: number;
22
- budgetAllocatedUsd: number;
23
- budgetAllocatedPercent: number;
24
- blockHeight: number;
25
- totalSupply: number;
26
- circulatingSupply: number;
27
- }
28
- export interface PivxGovernanceData {
29
- proposals: PivxProposal[];
30
- network: PivxNetworkStats;
31
- deflation: {
32
- unallocatedPivPerCycle: number;
33
- unallocatedPercent: number;
34
- annualUnallocatedPiv: number;
35
- proposalFeeBurnPiv: number;
36
- effectiveInflationReduction: string;
37
- };
38
- timestamp: string;
39
- source: string;
40
- cacheHit: boolean;
41
- }
42
- export declare function fetchPivxGovernance(): Promise<PivxGovernanceData>;
@@ -1,232 +0,0 @@
1
- import * as cheerio from "cheerio";
2
- // 5-minute cache
3
- const CACHE_TTL_MS = 5 * 60 * 1000;
4
- let cachedData = null;
5
- let cacheTimestamp = 0;
6
- const CHAINZ_BASE = "https://chainz.cryptoid.info/pivx/api.dws";
7
- async function chainzFetch(query) {
8
- const res = await fetch(`${CHAINZ_BASE}?q=${query}`, {
9
- signal: AbortSignal.timeout(8000),
10
- });
11
- if (!res.ok)
12
- throw new Error(`Chainz API ${query}: HTTP ${res.status}`);
13
- return (await res.text()).trim();
14
- }
15
- async function fetchNetworkStats() {
16
- const [masternodeCount, blockHeight, totalSupply, circulating] = await Promise.allSettled([
17
- chainzFetch("masternodecount"),
18
- chainzFetch("getblockcount"),
19
- chainzFetch("totalcoins"),
20
- chainzFetch("circulating"),
21
- ]);
22
- const mn = masternodeCount.status === "fulfilled"
23
- ? parseInt(masternodeCount.value, 10)
24
- : 0;
25
- const bh = blockHeight.status === "fulfilled"
26
- ? parseInt(blockHeight.value, 10)
27
- : 0;
28
- const ts = totalSupply.status === "fulfilled"
29
- ? parseFloat(totalSupply.value)
30
- : 0;
31
- const cs = circulating.status === "fulfilled"
32
- ? parseFloat(circulating.value)
33
- : 0;
34
- return {
35
- masternodeCount: isNaN(mn) ? 0 : mn,
36
- blockHeight: isNaN(bh) ? 0 : bh,
37
- totalSupply: isNaN(ts) ? 0 : ts,
38
- circulatingSupply: isNaN(cs) ? 0 : cs,
39
- passingThreshold: isNaN(mn) ? 0 : Math.ceil(mn * 0.1),
40
- };
41
- }
42
- function parseNumber(text) {
43
- const cleaned = text.replace(/[^0-9.\-]/g, "");
44
- const num = parseFloat(cleaned);
45
- return isNaN(num) ? 0 : num;
46
- }
47
- async function scrapePivxProposals() {
48
- const res = await fetch("https://pivx.org/proposals", {
49
- signal: AbortSignal.timeout(15000),
50
- headers: {
51
- "User-Agent": "MPP32-Governance-Oracle/1.0 (+https://mpp32.org)",
52
- Accept: "text/html",
53
- },
54
- });
55
- if (!res.ok)
56
- throw new Error(`pivx.org/proposals returned HTTP ${res.status}`);
57
- const html = await res.text();
58
- const $ = cheerio.load(html);
59
- const pageText = $("body").text();
60
- let monthlyBudgetPiv = 432000;
61
- let monthlyBudgetUsd = 0;
62
- let budgetAllocatedPiv = 0;
63
- let budgetAllocatedUsd = 0;
64
- let masternodeCount = 0;
65
- let passingThreshold = 0;
66
- const budgetMatch = pageText.match(/Monthly\s*Budget[:\s]*([\d,]+)\s*PIV/i);
67
- if (budgetMatch?.[1])
68
- monthlyBudgetPiv = parseNumber(budgetMatch[1]);
69
- const budgetUsdMatch = pageText.match(/Monthly\s*Budget[^$]*US?\$([\d,.]+)/i);
70
- if (budgetUsdMatch?.[1])
71
- monthlyBudgetUsd = parseNumber(budgetUsdMatch[1]);
72
- const allocatedMatch = pageText.match(/Budget\s*Allocated[:\s]*([\d,]+)\s*PIV/i);
73
- if (allocatedMatch?.[1])
74
- budgetAllocatedPiv = parseNumber(allocatedMatch[1]);
75
- const allocatedUsdMatch = pageText.match(/Budget\s*Allocated[^$]*US?\$([\d,.]+)/i);
76
- if (allocatedUsdMatch?.[1])
77
- budgetAllocatedUsd = parseNumber(allocatedUsdMatch[1]);
78
- const mnMatch = pageText.match(/([\d,]+)\s*masternodes?\s*online/i);
79
- if (mnMatch?.[1])
80
- masternodeCount = parseNumber(mnMatch[1]);
81
- const thresholdMatch = pageText.match(/Positive\s*votes\s*required[^:]*:\s*(\d+)/i);
82
- if (thresholdMatch?.[1])
83
- passingThreshold = parseInt(thresholdMatch[1], 10);
84
- const proposals = [];
85
- const seenHashes = new Set();
86
- $("table#js_table tbody tr[data-hash]").each((_i, el) => {
87
- const $row = $(el);
88
- const hash = $row.attr("data-hash") || "";
89
- if (!hash || seenHashes.has(hash))
90
- return;
91
- seenHashes.add(hash);
92
- const name = ($row.attr("data-title") || "").trim();
93
- if (!name)
94
- return;
95
- const cells = $row.find("td");
96
- if (cells.length < 5)
97
- return;
98
- const statusCell = $(cells[0]);
99
- const statusText = statusCell.text();
100
- const isPassing = /passing/i.test(statusText);
101
- const funded = /funded/i.test(statusText);
102
- let netYesPercent = 0;
103
- const netYesMatch = statusText.match(/([-\d.]+)%/);
104
- if (netYesMatch?.[1])
105
- netYesPercent = parseFloat(netYesMatch[1]);
106
- const nameCell = $(cells[1]);
107
- const link = nameCell.find("a").first();
108
- const url = link.attr("href") || "";
109
- const paymentCell = $(cells[2]);
110
- const monthlyPaymentPiv = parseNumber(paymentCell.attr("data-piv") || paymentCell.attr("data-order") || "0");
111
- const budgetPercent = parseFloat(paymentCell.attr("data-percent") || "0");
112
- let monthlyPaymentUsd = 0;
113
- const usdSpan = paymentCell.find(".curr-prefix").parent();
114
- if (usdSpan.length) {
115
- const usdText = usdSpan.text();
116
- const usdMatch = usdText.match(/US?\$\s*([\d,]+(?:\.\d+)?)/i);
117
- if (usdMatch?.[1])
118
- monthlyPaymentUsd = parseNumber(usdMatch[1]);
119
- }
120
- let installmentsRemaining = 1;
121
- const longLine = paymentCell.find(".long-line");
122
- if (longLine.length) {
123
- const installB = longLine.find("b").first();
124
- if (installB.length) {
125
- const n = parseInt(installB.text().trim(), 10);
126
- if (!isNaN(n))
127
- installmentsRemaining = n;
128
- }
129
- }
130
- let totalPaymentPiv = monthlyPaymentPiv;
131
- if (longLine.length) {
132
- const totalB = longLine.find("b").last();
133
- if (totalB.length) {
134
- const totalText = totalB.text();
135
- const totalMatch = totalText.match(/([\d,]+(?:\.\d+)?)\s*PIV/i);
136
- if (totalMatch?.[1])
137
- totalPaymentPiv = parseNumber(totalMatch[1]);
138
- }
139
- }
140
- const voteCell = $(cells[4]);
141
- const voteText = voteCell.text();
142
- let yesVotes = 0;
143
- let noVotes = 0;
144
- const voteMatch = voteText.match(/(\d+)\s*\/\s*(\d+)/);
145
- if (voteMatch?.[1] && voteMatch[2]) {
146
- yesVotes = parseInt(voteMatch[1], 10);
147
- noVotes = parseInt(voteMatch[2], 10);
148
- }
149
- proposals.push({
150
- name,
151
- url,
152
- status: isPassing ? "passing" : "failing",
153
- funded,
154
- netYesPercent,
155
- yesVotes,
156
- noVotes,
157
- monthlyPaymentPiv,
158
- monthlyPaymentUsd,
159
- totalPaymentPiv,
160
- installmentsRemaining,
161
- totalInstallments: installmentsRemaining,
162
- budgetPercent,
163
- });
164
- });
165
- return {
166
- proposals,
167
- budgetSummary: {
168
- monthlyBudgetPiv,
169
- monthlyBudgetUsd,
170
- budgetAllocatedPiv,
171
- budgetAllocatedUsd,
172
- masternodeCount,
173
- passingThreshold,
174
- },
175
- };
176
- }
177
- export async function fetchPivxGovernance() {
178
- if (cachedData && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
179
- return { ...cachedData, cacheHit: true };
180
- }
181
- const [scrapeResult, networkStats] = await Promise.allSettled([
182
- scrapePivxProposals(),
183
- fetchNetworkStats(),
184
- ]);
185
- const scraped = scrapeResult.status === "fulfilled" ? scrapeResult.value : null;
186
- const chainzStats = networkStats.status === "fulfilled" ? networkStats.value : {};
187
- if (!scraped) {
188
- throw new Error(`Failed to fetch PIVX governance data: ${scrapeResult.status === "rejected" ? scrapeResult.reason : "unknown"}`);
189
- }
190
- const budgetAllocatedPercent = scraped.budgetSummary.monthlyBudgetPiv > 0
191
- ? Math.round((scraped.budgetSummary.budgetAllocatedPiv /
192
- scraped.budgetSummary.monthlyBudgetPiv) *
193
- 10000) / 100
194
- : 0;
195
- const network = {
196
- masternodeCount: scraped.budgetSummary.masternodeCount ||
197
- chainzStats.masternodeCount ||
198
- 0,
199
- passingThreshold: scraped.budgetSummary.passingThreshold ||
200
- chainzStats.passingThreshold ||
201
- 0,
202
- monthlyBudgetPiv: scraped.budgetSummary.monthlyBudgetPiv,
203
- monthlyBudgetUsd: scraped.budgetSummary.monthlyBudgetUsd,
204
- budgetAllocatedPiv: scraped.budgetSummary.budgetAllocatedPiv,
205
- budgetAllocatedUsd: scraped.budgetSummary.budgetAllocatedUsd,
206
- budgetAllocatedPercent,
207
- blockHeight: chainzStats.blockHeight || 0,
208
- totalSupply: chainzStats.totalSupply || 0,
209
- circulatingSupply: chainzStats.circulatingSupply || 0,
210
- };
211
- const unallocatedPivPerCycle = network.monthlyBudgetPiv - network.budgetAllocatedPiv;
212
- const annualUnallocatedPiv = unallocatedPivPerCycle * 12;
213
- const data = {
214
- proposals: scraped.proposals,
215
- network,
216
- deflation: {
217
- unallocatedPivPerCycle,
218
- unallocatedPercent: 100 - budgetAllocatedPercent,
219
- annualUnallocatedPiv,
220
- proposalFeeBurnPiv: 50,
221
- effectiveInflationReduction: network.totalSupply > 0
222
- ? `${((annualUnallocatedPiv / network.totalSupply) * 100).toFixed(3)}%`
223
- : "N/A",
224
- },
225
- timestamp: new Date().toISOString(),
226
- source: "pivx.org/proposals + chainz.cryptoid.info",
227
- cacheHit: false,
228
- };
229
- cachedData = data;
230
- cacheTimestamp = Date.now();
231
- return data;
232
- }