mpp32-mcp-server 1.4.0 → 1.5.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,40 @@ 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.5.0] - 2026-06-01
8
+
9
+ ### Added
10
+
11
+ * **`try_solana_token_intelligence_free` MCP tool — zero-friction preview.**
12
+ Hits the backend `/api/intelligence/demo` endpoint. Returns the same alpha
13
+ score, rug risk, whale activity, smart money signals, pump probability, and
14
+ market data payload as the paid endpoint, but requires no `MPP32_AGENT_KEY`
15
+ and no Solana private key. Rate-limited to 10 calls/minute per IP. Intended
16
+ as the first call new users (and Claude itself) make when evaluating MPP32 —
17
+ see the data quality, THEN configure paid access. This closes the
18
+ conversion-funnel gap where new agents previously hit a 402 wall on their
19
+ very first call before ever seeing the oracle's output.
20
+
21
+ ### Changed
22
+
23
+ * **`get_solana_token_intelligence` description updated** to point new users
24
+ with no keys configured at `try_solana_token_intelligence_free` first.
25
+ * **`get_mpp32_diagnostics` output updated** to mention the free demo when
26
+ the user is not yet ready to pay end-to-end.
27
+
28
+ ### Backend (server-side compatibility, no client action required)
29
+
30
+ * MPP32 backend now defaults to PayAI (`https://facilitator.payai.network`)
31
+ as the x402 facilitator, with Coinbase CDP as a documented fallback. PayAI
32
+ is the only public facilitator that supports Solana **mainnet**
33
+ (`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`); the previous default
34
+ (`x402.org/facilitator`) is testnet/devnet-only and was silently failing
35
+ every mainnet settlement. The backend now refuses to boot in production
36
+ if no configured facilitator supports the configured network — making this
37
+ exact regression class impossible. No MCP client change required: the
38
+ signer's network detection (`x402-signers.ts`) and protocol handling are
39
+ unchanged.
40
+
7
41
  ## [1.4.0] - 2026-05-21
8
42
 
9
43
  ### Added
package/README.md CHANGED
@@ -22,6 +22,12 @@ One install. Pay any x402 endpoint on Solana from your agent. Browse a federated
22
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
24
 
25
+ ## Try it free first (no keys, no setup)
26
+
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
+
29
+ Just install the server and ask your agent: _"Use mpp32 to get a free intelligence preview for SOL."_
30
+
25
31
  ## Why this beats running your own integrations
26
32
 
27
33
  Most agent stacks stop at "the model can call a function." That works until the function costs money. The moment your agent needs premium data, a paid model, a trading signal, or a token analytics call, you are back to building accounts, storing API keys, watching budgets, and writing custom 402 handlers for every provider.
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.4.0";
6
+ const SERVER_VERSION = "1.5.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
@@ -252,7 +252,7 @@ server.tool("get_mpp32_diagnostics", "Report what the mpp32-mcp-server detected
252
252
  `- x402 (USDC on Solana) payment: ${SOLANA_PRIVATE_KEY ? "yes" : "no — set MPP32_SOLANA_PRIVATE_KEY"}`,
253
253
  `- x402 (USDC on Base/EVM) payment: ${PRIVATE_KEY ? "yes" : "no — set MPP32_PRIVATE_KEY"}`,
254
254
  ``,
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."}`,
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."}`,
256
256
  ``,
257
257
  `**If a variable shows NOT SET but you set it in claude_desktop_config.json:**`,
258
258
  `1. Confirm the file path Claude Desktop actually reads:`,
@@ -450,7 +450,7 @@ server.tool("call_mpp32_endpoint", "Call any HTTP-callable service in the MPP32
450
450
  return await callViaLegacyProxy(slug, method, parsedBody, query, path);
451
451
  });
452
452
  // ── 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.", {
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.", {
454
454
  token: z
455
455
  .string()
456
456
  .describe("Solana token mint address or ticker symbol (e.g. SOL, BONK, JUP, M32, or full base58 address)."),
@@ -466,6 +466,63 @@ server.tool("get_solana_token_intelligence", "Get real-time Solana token intelli
466
466
  // Legacy path — direct call to /api/intelligence with manual 402 handling.
467
467
  return await legacyIntelligenceCall(token, walletAddress);
468
468
  });
469
+ // ── Tool 3b: Free Intelligence Demo ────────────────────────────────────────
470
+ // No payment, no key. Hits /api/intelligence/demo, which is rate-limited per
471
+ // IP. Intended as the first call new users (and Claude itself) make when
472
+ // trying MPP32 — they see real alpha scores and signals BEFORE encountering
473
+ // any payment wall. This is the conversion funnel fix: agents today bounce
474
+ // 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.", {
476
+ token: z
477
+ .string()
478
+ .describe("Solana token mint address or ticker symbol (e.g. SOL, BONK, JUP, M32, or full base58 address)."),
479
+ }, async ({ token }) => {
480
+ try {
481
+ const res = await fetchWithTimeout(`${API_URL}/api/intelligence/demo`, {
482
+ method: "POST",
483
+ headers: { "Content-Type": "application/json" },
484
+ body: JSON.stringify({ token }),
485
+ });
486
+ const text = await res.text();
487
+ let formatted;
488
+ try {
489
+ formatted = JSON.stringify(JSON.parse(text), null, 2);
490
+ }
491
+ catch {
492
+ formatted = text;
493
+ }
494
+ if (res.status === 429) {
495
+ return {
496
+ content: [{
497
+ 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%.`,
499
+ }],
500
+ };
501
+ }
502
+ if (!res.ok) {
503
+ return {
504
+ content: [{
505
+ type: "text",
506
+ text: `Demo returned HTTP ${res.status}:\n\n\`\`\`json\n${formatted}\n\`\`\``,
507
+ }],
508
+ };
509
+ }
510
+ return {
511
+ content: [{
512
+ type: "text",
513
+ text: `**MPP32 Intelligence Oracle (FREE DEMO)** — \`${token}\`\n\n${formatted}\n\n---\n_Demo result. Same payload as the paid endpoint. Rate-limited to 10/min/IP. For unlimited usage and dashboard attribution, set MPP32_AGENT_KEY (get one at ${API_URL}/agent-console) and use \`get_solana_token_intelligence\`._`,
514
+ }],
515
+ };
516
+ }
517
+ catch (err) {
518
+ return {
519
+ content: [{
520
+ type: "text",
521
+ text: `Network error reaching ${API_URL}: ${err instanceof Error ? err.message : String(err)}`,
522
+ }],
523
+ };
524
+ }
525
+ });
469
526
  // ── Tool 4: M32-gated Whale Tracker ───────────────────────────────────────
470
527
  server.tool("get_m32_whale_tracker", "M32-gated whale analysis for any Solana token. Returns top 20 holders, concentration risk, holder distribution, and buy/sell pressure. Requires the caller to hold 1,000,000+ M32 tokens (balance verified on-chain via X-Wallet-Address header). Free for qualifying holders — no payment required. Returns 403 if the wallet holds insufficient M32.", {
471
528
  token: z
@@ -575,7 +632,10 @@ server.tool("scan_portfolio_m32", "M32-gated full wallet portfolio scan. Discove
575
632
  }
576
633
  });
577
634
  // ── Tool 7: get_pivx_dao_intelligence ─────────────────────────────────────
578
- server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance intelligence. Returns active budget proposals with masternode voting tallies (Yes/No/Abstain 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. Data sourced from pivx.org/proposals and the PIVX blockchain. Free — no payment required.", {
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.", {
579
639
  filter: z
580
640
  .enum(["all", "passing", "failing"])
581
641
  .default("all")
@@ -588,52 +648,31 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
588
648
  .describe("Include network stats and deflation metrics (default: true)."),
589
649
  }, async ({ filter, includeStats }) => {
590
650
  try {
591
- const statusFilter = filter === "passing" || filter === "failing" ? `?status=${filter}` : "";
592
- const endpoint = includeStats !== false ? "/api/governance" : `/api/governance/proposals${statusFilter}`;
593
- const res = await fetchWithTimeout(`${API_URL}${endpoint}`, {
594
- headers: buildHeaders(),
595
- });
596
- if (!res.ok) {
597
- const err = await res.json().catch(() => null);
598
- return {
599
- content: [{
600
- type: "text",
601
- text: `Error fetching PIVX governance data: ${err?.error?.message ?? `HTTP ${res.status}`}`,
602
- }],
603
- };
604
- }
605
- const json = await res.json();
606
- const data = json.data;
607
- // Build readable output
651
+ const gov = await fetchPivxGovernance();
608
652
  const lines = [];
609
653
  lines.push("# PIVX DAO Governance Intelligence");
610
654
  lines.push("");
611
- // Network stats
612
- const network = data.network;
613
- if (network) {
655
+ if (includeStats !== false) {
656
+ const n = gov.network;
614
657
  lines.push("## Network Overview");
615
- lines.push(`- **Masternodes Online:** ${network.masternodeCount}`);
616
- lines.push(`- **Passing Threshold:** ${network.passingThreshold} votes (10% of masternodes)`);
617
- lines.push(`- **Monthly Budget:** ${Number(network.monthlyBudgetPiv).toLocaleString()} PIV (~$${Number(network.monthlyBudgetUsd).toLocaleString()})`);
618
- lines.push(`- **Budget Allocated:** ${Number(network.budgetAllocatedPiv).toLocaleString()} PIV (${network.budgetAllocatedPercent}%)`);
619
- if (network.blockHeight)
620
- lines.push(`- **Block Height:** ${Number(network.blockHeight).toLocaleString()}`);
621
- if (network.totalSupply)
622
- lines.push(`- **Total Supply:** ${Math.round(Number(network.totalSupply)).toLocaleString()} PIV`);
658
+ lines.push(`- **Masternodes Online:** ${n.masternodeCount.toLocaleString()}`);
659
+ lines.push(`- **Passing Threshold:** ${n.passingThreshold} votes (10% of masternodes)`);
660
+ lines.push(`- **Monthly Budget:** ${n.monthlyBudgetPiv.toLocaleString()} PIV (~$${n.monthlyBudgetUsd.toLocaleString()})`);
661
+ lines.push(`- **Budget Allocated:** ${n.budgetAllocatedPiv.toLocaleString()} PIV (${n.budgetAllocatedPercent}%)`);
662
+ if (n.blockHeight)
663
+ lines.push(`- **Block Height:** ${n.blockHeight.toLocaleString()}`);
664
+ if (n.totalSupply)
665
+ lines.push(`- **Total Supply:** ${Math.round(n.totalSupply).toLocaleString()} PIV`);
623
666
  lines.push("");
624
- }
625
- // Deflation stats
626
- const deflation = data.deflation;
627
- if (deflation) {
667
+ const d = gov.deflation;
628
668
  lines.push("## Deflation / Fee Burn Metrics");
629
- lines.push(`- **Unallocated PIV This Cycle:** ${Number(deflation.unallocatedPivPerCycle).toLocaleString()} PIV (never minted)`);
630
- lines.push(`- **Annual Unallocated (est.):** ${Number(deflation.annualUnallocatedPiv).toLocaleString()} PIV`);
631
- lines.push(`- **Effective Inflation Reduction:** ${deflation.effectiveInflationReduction}`);
632
- lines.push(`- **Proposal Submission Fee:** ${deflation.proposalFeeBurnPiv} PIV (burned/destroyed)`);
669
+ lines.push(`- **Unallocated PIV This Cycle:** ${d.unallocatedPivPerCycle.toLocaleString()} PIV (never minted)`);
670
+ lines.push(`- **Annual Unallocated (est.):** ${d.annualUnallocatedPiv.toLocaleString()} PIV`);
671
+ lines.push(`- **Effective Inflation Reduction:** ${d.effectiveInflationReduction}`);
672
+ lines.push(`- **Proposal Submission Fee:** ${d.proposalFeeBurnPiv} PIV (burned/destroyed)`);
633
673
  lines.push("");
634
674
  }
635
- // Proposals
636
- let proposals = (data.proposals ?? []);
675
+ let proposals = gov.proposals;
637
676
  if (filter === "passing")
638
677
  proposals = proposals.filter((p) => p.status === "passing");
639
678
  else if (filter === "failing")
@@ -646,11 +685,11 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
646
685
  const fundedTag = p.funded ? " (Funded)" : "";
647
686
  lines.push(`### ${p.name} — ${status}${fundedTag}`);
648
687
  lines.push(`- **Votes:** ${p.yesVotes} Yes / ${p.noVotes} No (${p.netYesPercent}% net yes)`);
649
- lines.push(`- **Monthly Payment:** ${Number(p.monthlyPaymentPiv).toLocaleString()} PIV (~$${Number(p.monthlyPaymentUsd).toLocaleString()})`);
650
- if (Number(p.totalPaymentPiv) > Number(p.monthlyPaymentPiv)) {
651
- lines.push(`- **Total Budget:** ${Number(p.totalPaymentPiv).toLocaleString()} PIV`);
688
+ lines.push(`- **Monthly Payment:** ${p.monthlyPaymentPiv.toLocaleString()} PIV (~$${p.monthlyPaymentUsd.toLocaleString()})`);
689
+ if (p.totalPaymentPiv > p.monthlyPaymentPiv) {
690
+ lines.push(`- **Total Budget:** ${p.totalPaymentPiv.toLocaleString()} PIV`);
652
691
  }
653
- if (Number(p.installmentsRemaining) > 0) {
692
+ if (p.installmentsRemaining > 0) {
654
693
  lines.push(`- **Installments Remaining:** ${p.installmentsRemaining}`);
655
694
  }
656
695
  if (p.budgetPercent)
@@ -663,12 +702,8 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
663
702
  else {
664
703
  lines.push("No proposals found matching the filter.");
665
704
  }
666
- // Meta
667
- const meta = data.meta;
668
- if (meta) {
669
- lines.push("---");
670
- lines.push(`Source: ${meta.source} | ${meta.timestamp}${meta.cacheHit ? " (cached)" : ""}`);
671
- }
705
+ lines.push("---");
706
+ lines.push(`Source: ${gov.source} | ${gov.timestamp}${gov.cacheHit ? " (cached)" : ""}`);
672
707
  return {
673
708
  content: [{ type: "text", text: lines.join("\n") }],
674
709
  };
@@ -677,7 +712,7 @@ server.tool("get_pivx_dao_intelligence", "Get real-time PIVX DAO governance inte
677
712
  return {
678
713
  content: [{
679
714
  type: "text",
680
- text: `Failed to fetch PIVX governance data: ${err instanceof Error ? err.message : String(err)}`,
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.`,
681
716
  }],
682
717
  };
683
718
  }
@@ -0,0 +1,42 @@
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>;
@@ -0,0 +1,232 @@
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mpp32-mcp-server",
3
- "version": "1.4.0",
3
+ "version": "1.5.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",
@@ -76,6 +76,7 @@
76
76
  "@solana-program/token": "^0.13.0",
77
77
  "@solana/kit": "^6.9.0",
78
78
  "bs58": "^6.0.0",
79
+ "cheerio": "^1.2.0",
79
80
  "tweetnacl": "^1.0.3",
80
81
  "viem": "^2.48.11",
81
82
  "zod": "^3.23.0"