paygate-mcp 8.3.0 → 8.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/README.md CHANGED
@@ -110,6 +110,8 @@ Agent → PayGate (auth + billing) → Your MCP Server (stdio or HTTP)
110
110
  - **Admin Notifications** — `GET /admin/notifications` scans all keys for actionable issues: expired/expiring keys, zero credits, credit depletion velocity, suspended keys, high error rates, and rate limit pressure — with severity filtering and priority sorting
111
111
  - **System Dashboard** — `GET /admin/dashboard` system-wide overview with key counts (active/suspended/revoked/expired), credit summary (allocated/spent/remaining), usage breakdown with deny reasons, top consumers, top tools, notification counts, and uptime
112
112
  - **Key Lifecycle Report** — `GET /admin/lifecycle` aggregated lifecycle trends with daily creation/revocation/suspension buckets, average key lifetime, and at-risk keys (expiring, expired, zero credits)
113
+ - **Cost Analysis** — `GET /admin/costs` cost-centric view with per-tool and per-namespace cost breakdowns, hourly spending trends, top spenders, average cost per call, and namespace filtering
114
+ - **Rate Limit Analysis** — `GET /admin/rate-limits` rate limit utilization analysis with per-key and per-tool breakdown, denial trends, most throttled keys, and current window utilization
113
115
  - **Config Hot Reload** — `POST /config/reload` reloads pricing, rate limits, webhooks, quotas, and behavior flags from config file without server restart
114
116
  - **Webhook Events** — POST batched usage events to any URL for external billing/alerting
115
117
  - **Config File Mode** — Load all settings from a JSON file (`--config`)
@@ -2502,6 +2504,94 @@ curl "http://localhost:3402/admin/lifecycle?since=2026-02-01&until=2026-02-28" \
2502
2504
 
2503
2505
  Shows aggregated lifecycle event counts, daily trend buckets (sorted chronologically), average key lifetime in hours (for revoked keys), and at-risk keys with their risk category (`expired`, `expiring_soon`, `zero_credits`). Supports `?since=` and `?until=` date filters. Excludes suspended and revoked keys from at-risk list. Read-only.
2504
2506
 
2507
+ ### Cost Analysis
2508
+
2509
+ Get a cost-centric breakdown of credit usage across tools, namespaces, and time:
2510
+
2511
+ ```bash
2512
+ # Full cost analysis
2513
+ curl http://localhost:3402/admin/costs \
2514
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2515
+
2516
+ # Filter by namespace
2517
+ curl "http://localhost:3402/admin/costs?namespace=prod" \
2518
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2519
+
2520
+ # Filter by time range
2521
+ curl "http://localhost:3402/admin/costs?since=2026-02-01" \
2522
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2523
+ ```
2524
+
2525
+ ```json
2526
+ {
2527
+ "summary": {
2528
+ "totalCredits": 4250,
2529
+ "totalCalls": 312,
2530
+ "totalAllowed": 298,
2531
+ "totalDenied": 14,
2532
+ "avgCostPerCall": 13.62
2533
+ },
2534
+ "perTool": [
2535
+ { "tool": "generate_report", "calls": 85, "credits": 1700, "avgCost": 20 },
2536
+ { "tool": "query_data", "calls": 142, "credits": 1420, "avgCost": 10 }
2537
+ ],
2538
+ "perNamespace": [
2539
+ { "namespace": "prod", "calls": 210, "credits": 3150 },
2540
+ { "namespace": "staging", "calls": 102, "credits": 1100 }
2541
+ ],
2542
+ "hourlyTrends": [
2543
+ { "hour": "2026-02-26T14:00:00.000Z", "calls": 23, "credits": 345, "denied": 1 },
2544
+ { "hour": "2026-02-26T15:00:00.000Z", "calls": 31, "credits": 465, "denied": 0 }
2545
+ ],
2546
+ "topSpenders": [
2547
+ { "key": "pg_a1b2...c3d4", "name": "ml-pipeline", "credits": 1800, "calls": 90 },
2548
+ { "key": "pg_e5f6...g7h8", "name": "batch-worker", "credits": 1200, "calls": 120 }
2549
+ ]
2550
+ }
2551
+ ```
2552
+
2553
+ Returns per-tool cost breakdown (with average cost per call), per-namespace spending, hourly trend buckets (last 24 hours), and top 10 spenders ranked by credits consumed. Supports `?since=` and `?namespace=` query filters. Keys without an explicit namespace appear under `default`. Read-only.
2554
+
2555
+ ### Rate Limit Analysis
2556
+
2557
+ Analyze rate limit utilization across keys and tools:
2558
+
2559
+ ```bash
2560
+ curl http://localhost:3402/admin/rate-limits \
2561
+ -H "X-Admin-Key: YOUR_ADMIN_KEY"
2562
+ ```
2563
+
2564
+ ```json
2565
+ {
2566
+ "config": {
2567
+ "globalLimitPerMin": 60,
2568
+ "windowMs": 60000
2569
+ },
2570
+ "summary": {
2571
+ "totalCalls": 450,
2572
+ "totalRateLimited": 12,
2573
+ "rateLimitRate": 0.0267
2574
+ },
2575
+ "perKey": [
2576
+ { "name": "ml-pipeline", "calls": 200, "rateLimited": 8, "currentWindowUsed": 45, "currentWindowRemaining": 15 },
2577
+ { "name": "batch-worker", "calls": 150, "rateLimited": 4, "currentWindowUsed": 12, "currentWindowRemaining": 48 }
2578
+ ],
2579
+ "perTool": [
2580
+ { "tool": "generate_report", "calls": 180, "rateLimited": 10 },
2581
+ { "tool": "query_data", "calls": 270, "rateLimited": 2 }
2582
+ ],
2583
+ "hourlyTrends": [
2584
+ { "hour": "2026-02-26T14", "calls": 52, "rateLimited": 3 },
2585
+ { "hour": "2026-02-26T15", "calls": 48, "rateLimited": 1 }
2586
+ ],
2587
+ "mostThrottled": [
2588
+ { "name": "ml-pipeline", "rateLimited": 8, "calls": 200, "throttleRate": 0.04 }
2589
+ ]
2590
+ }
2591
+ ```
2592
+
2593
+ Returns rate limit configuration, denial summary with throttle rate, per-key breakdown with current sliding window utilization, per-tool denial counts, hourly denial trends (last 24 hours), and top 10 most throttled keys ranked by denial count. Handles unlimited rate limits (globalLimitPerMin: 0). Read-only.
2594
+
2505
2595
  ### IP Allowlisting
2506
2596
 
2507
2597
  Restrict API keys to specific IP addresses or CIDR ranges:
package/dist/server.d.ts CHANGED
@@ -245,6 +245,8 @@ export declare class PayGateServer {
245
245
  private handleAdminNotifications;
246
246
  private handleSystemDashboard;
247
247
  private handleKeyLifecycleReport;
248
+ private handleCostAnalysis;
249
+ private handleRateLimitAnalysis;
248
250
  private handleGetNotes;
249
251
  private handleAddNote;
250
252
  private handleDeleteNote;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -816,6 +816,18 @@ class PayGateServer {
816
816
  res.writeHead(405, { 'Content-Type': 'application/json' });
817
817
  res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
818
818
  return;
819
+ case '/admin/costs':
820
+ if (req.method === 'GET')
821
+ return this.handleCostAnalysis(req, res);
822
+ res.writeHead(405, { 'Content-Type': 'application/json' });
823
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
824
+ return;
825
+ case '/admin/rate-limits':
826
+ if (req.method === 'GET')
827
+ return this.handleRateLimitAnalysis(req, res);
828
+ res.writeHead(405, { 'Content-Type': 'application/json' });
829
+ res.end(JSON.stringify({ error: 'Method not allowed. Use GET.' }));
830
+ return;
819
831
  // ─── Plugin endpoints ──────────────────────────────────────────────
820
832
  case '/plugins':
821
833
  return this.handleListPlugins(req, res);
@@ -1359,6 +1371,8 @@ class PayGateServer {
1359
1371
  adminNotifications: 'GET /admin/notifications — Actionable notifications for expiring keys, low credits, high error rates, and rate limit pressure (requires X-Admin-Key)',
1360
1372
  systemDashboard: 'GET /admin/dashboard — System-wide overview with key stats, credit summary, usage breakdown, top consumers, and uptime (requires X-Admin-Key)',
1361
1373
  keyLifecycle: 'GET /admin/lifecycle — Key lifecycle report with creation/revocation/expiry trends, average lifetime, and at-risk keys (requires X-Admin-Key)',
1374
+ costAnalysis: 'GET /admin/costs — Cost analysis with per-tool, per-namespace breakdown, hourly trends, and top spenders (requires X-Admin-Key)',
1375
+ rateLimitAnalysis: 'GET /admin/rate-limits — Rate limit utilization analysis with per-key and per-tool breakdown, denial trends, and most throttled keys (requires X-Admin-Key)',
1362
1376
  ...(this.oauth ? {
1363
1377
  oauthMetadata: 'GET /.well-known/oauth-authorization-server — OAuth 2.1 server metadata',
1364
1378
  oauthRegister: 'POST /oauth/register — Register OAuth client',
@@ -4658,6 +4672,232 @@ class PayGateServer {
4658
4672
  atRisk: atRiskKeys,
4659
4673
  }));
4660
4674
  }
4675
+ // ─── /admin/costs — Cost analysis ─────────────────────────────────────────
4676
+ handleCostAnalysis(req, res) {
4677
+ if (!this.checkAdmin(req, res))
4678
+ return;
4679
+ const urlParts = req.url?.split('?') || [];
4680
+ const params = new URLSearchParams(urlParts[1] || '');
4681
+ const since = params.get('since') || undefined;
4682
+ const namespace = params.get('namespace') || undefined;
4683
+ // Get all usage events
4684
+ const events = this.gate.meter.getEvents(since, namespace);
4685
+ // ── Per-tool cost breakdown ──
4686
+ const toolCosts = new Map();
4687
+ for (const e of events) {
4688
+ if (!toolCosts.has(e.tool))
4689
+ toolCosts.set(e.tool, { calls: 0, credits: 0, denied: 0, avgCost: 0 });
4690
+ const t = toolCosts.get(e.tool);
4691
+ t.calls++;
4692
+ if (e.allowed) {
4693
+ t.credits += e.creditsCharged;
4694
+ }
4695
+ else {
4696
+ t.denied++;
4697
+ }
4698
+ }
4699
+ for (const [, t] of toolCosts) {
4700
+ const allowed = t.calls - t.denied;
4701
+ t.avgCost = allowed > 0 ? Math.round(t.credits / allowed * 100) / 100 : 0;
4702
+ }
4703
+ const perTool = Array.from(toolCosts.entries())
4704
+ .map(([tool, stats]) => ({ tool, ...stats }))
4705
+ .sort((a, b) => b.credits - a.credits);
4706
+ // ── Per-namespace cost breakdown ──
4707
+ const nsCosts = new Map();
4708
+ for (const e of events) {
4709
+ const ns = e.namespace || 'default';
4710
+ if (!nsCosts.has(ns))
4711
+ nsCosts.set(ns, { calls: 0, credits: 0, denied: 0 });
4712
+ const n = nsCosts.get(ns);
4713
+ n.calls++;
4714
+ if (e.allowed) {
4715
+ n.credits += e.creditsCharged;
4716
+ }
4717
+ else {
4718
+ n.denied++;
4719
+ }
4720
+ }
4721
+ const perNamespace = Array.from(nsCosts.entries())
4722
+ .map(([ns, stats]) => ({ namespace: ns, ...stats }))
4723
+ .sort((a, b) => b.credits - a.credits);
4724
+ // ── Hourly trends (last 24 buckets) ──
4725
+ const hourBuckets = new Map();
4726
+ for (const e of events) {
4727
+ const hour = e.timestamp.slice(0, 13); // YYYY-MM-DDTHH
4728
+ if (!hourBuckets.has(hour))
4729
+ hourBuckets.set(hour, { calls: 0, credits: 0, denied: 0 });
4730
+ const h = hourBuckets.get(hour);
4731
+ h.calls++;
4732
+ if (e.allowed) {
4733
+ h.credits += e.creditsCharged;
4734
+ }
4735
+ else {
4736
+ h.denied++;
4737
+ }
4738
+ }
4739
+ const hourlyTrends = Array.from(hourBuckets.entries())
4740
+ .map(([hour, stats]) => ({ hour, ...stats }))
4741
+ .sort((a, b) => a.hour.localeCompare(b.hour))
4742
+ .slice(-24);
4743
+ // ── Top spenders (by credits) ──
4744
+ const keyCosts = new Map();
4745
+ for (const e of events) {
4746
+ const name = e.keyName || e.apiKey.slice(0, 10);
4747
+ if (!keyCosts.has(name))
4748
+ keyCosts.set(name, { calls: 0, credits: 0, denied: 0 });
4749
+ const k = keyCosts.get(name);
4750
+ k.calls++;
4751
+ if (e.allowed) {
4752
+ k.credits += e.creditsCharged;
4753
+ }
4754
+ else {
4755
+ k.denied++;
4756
+ }
4757
+ }
4758
+ const topSpenders = Array.from(keyCosts.entries())
4759
+ .map(([name, stats]) => ({ name, ...stats }))
4760
+ .sort((a, b) => b.credits - a.credits)
4761
+ .slice(0, 10);
4762
+ // ── Totals ──
4763
+ let totalCredits = 0;
4764
+ let totalCalls = 0;
4765
+ let totalDenied = 0;
4766
+ for (const e of events) {
4767
+ totalCalls++;
4768
+ if (e.allowed) {
4769
+ totalCredits += e.creditsCharged;
4770
+ }
4771
+ else {
4772
+ totalDenied++;
4773
+ }
4774
+ }
4775
+ const avgCostPerCall = totalCalls - totalDenied > 0
4776
+ ? Math.round(totalCredits / (totalCalls - totalDenied) * 100) / 100
4777
+ : 0;
4778
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4779
+ res.end(JSON.stringify({
4780
+ summary: {
4781
+ totalCredits,
4782
+ totalCalls,
4783
+ totalAllowed: totalCalls - totalDenied,
4784
+ totalDenied,
4785
+ avgCostPerCall,
4786
+ },
4787
+ perTool,
4788
+ perNamespace,
4789
+ hourlyTrends,
4790
+ topSpenders,
4791
+ }));
4792
+ }
4793
+ // ─── /admin/rate-limits — Rate limit utilization analysis ───────────────
4794
+ handleRateLimitAnalysis(req, res) {
4795
+ if (!this.checkAdmin(req, res))
4796
+ return;
4797
+ const rateLimiter = this.gate.rateLimiter;
4798
+ const globalLimit = rateLimiter.globalLimit;
4799
+ // Get all usage events to analyze rate limit denials
4800
+ const events = this.gate.meter.getEvents();
4801
+ // ── Summary ──
4802
+ let totalCalls = 0;
4803
+ let totalRateLimited = 0;
4804
+ for (const e of events) {
4805
+ totalCalls++;
4806
+ if (!e.allowed && e.denyReason?.includes('rate_limited')) {
4807
+ totalRateLimited++;
4808
+ }
4809
+ }
4810
+ const rateLimitRate = totalCalls > 0
4811
+ ? Math.round(totalRateLimited / totalCalls * 10000) / 10000
4812
+ : 0;
4813
+ // ── Per-key breakdown ──
4814
+ // Build a name→fullKey map from the key store for rate limiter status lookups
4815
+ const allRecords = this.gate.store.getAllRecords();
4816
+ const nameToFullKey = new Map();
4817
+ for (const rec of allRecords) {
4818
+ nameToFullKey.set(rec.name, rec.key);
4819
+ }
4820
+ const keyMap = new Map();
4821
+ for (const e of events) {
4822
+ const name = e.keyName || e.apiKey.slice(0, 10);
4823
+ if (!keyMap.has(name))
4824
+ keyMap.set(name, { name, calls: 0, rateLimited: 0 });
4825
+ const k = keyMap.get(name);
4826
+ k.calls++;
4827
+ if (!e.allowed && e.denyReason?.includes('rate_limited')) {
4828
+ k.rateLimited++;
4829
+ }
4830
+ }
4831
+ // Add current window utilization using full key from store
4832
+ const perKey = Array.from(keyMap.values()).map(k => {
4833
+ const fullKey = nameToFullKey.get(k.name) || '';
4834
+ const status = fullKey ? rateLimiter.getStatus(fullKey) : { used: 0, remaining: globalLimit > 0 ? globalLimit : Infinity };
4835
+ return {
4836
+ name: k.name,
4837
+ calls: k.calls,
4838
+ rateLimited: k.rateLimited,
4839
+ currentWindowUsed: status.used,
4840
+ currentWindowRemaining: status.remaining,
4841
+ };
4842
+ }).sort((a, b) => b.calls - a.calls);
4843
+ // ── Per-tool breakdown ──
4844
+ const toolMap = new Map();
4845
+ for (const e of events) {
4846
+ if (!toolMap.has(e.tool))
4847
+ toolMap.set(e.tool, { calls: 0, rateLimited: 0 });
4848
+ const t = toolMap.get(e.tool);
4849
+ t.calls++;
4850
+ if (!e.allowed && e.denyReason?.includes('rate_limited')) {
4851
+ t.rateLimited++;
4852
+ }
4853
+ }
4854
+ const perTool = Array.from(toolMap.entries())
4855
+ .map(([tool, stats]) => ({ tool, ...stats }))
4856
+ .sort((a, b) => b.rateLimited - a.rateLimited);
4857
+ // ── Hourly trends (rate limit denials) ──
4858
+ const hourBuckets = new Map();
4859
+ for (const e of events) {
4860
+ const hour = e.timestamp.slice(0, 13); // YYYY-MM-DDTHH
4861
+ if (!hourBuckets.has(hour))
4862
+ hourBuckets.set(hour, { calls: 0, rateLimited: 0 });
4863
+ const h = hourBuckets.get(hour);
4864
+ h.calls++;
4865
+ if (!e.allowed && e.denyReason?.includes('rate_limited')) {
4866
+ h.rateLimited++;
4867
+ }
4868
+ }
4869
+ const hourlyTrends = Array.from(hourBuckets.entries())
4870
+ .map(([hour, stats]) => ({ hour, ...stats }))
4871
+ .sort((a, b) => a.hour.localeCompare(b.hour))
4872
+ .slice(-24);
4873
+ // ── Most throttled keys (by rate limit denials) ──
4874
+ const mostThrottled = Array.from(keyMap.values())
4875
+ .filter(k => k.rateLimited > 0)
4876
+ .map(k => ({
4877
+ name: k.name,
4878
+ rateLimited: k.rateLimited,
4879
+ calls: k.calls,
4880
+ throttleRate: Math.round(k.rateLimited / k.calls * 10000) / 10000,
4881
+ }))
4882
+ .sort((a, b) => b.rateLimited - a.rateLimited)
4883
+ .slice(0, 10);
4884
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4885
+ res.end(JSON.stringify({
4886
+ config: {
4887
+ globalLimitPerMin: globalLimit,
4888
+ windowMs: 60000,
4889
+ },
4890
+ summary: {
4891
+ totalCalls,
4892
+ totalRateLimited,
4893
+ rateLimitRate,
4894
+ },
4895
+ perKey,
4896
+ perTool,
4897
+ hourlyTrends,
4898
+ mostThrottled,
4899
+ }));
4900
+ }
4661
4901
  // ─── /keys/notes — Timestamped notes on API keys ─────────────────────────
4662
4902
  handleGetNotes(req, res) {
4663
4903
  if (!this.checkAdmin(req, res))