@agentgate-builder/perpradar 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agentgate-builder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # PerpRadar: perp funding rates, open interest and positioning for trading agents (x402 API + MCP)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Perpetual futures funding radar across the onchain perp venues **Hyperliquid, dYdX v4, Paradex and Aevo**. One call returns
4
+ funding normalized to **8h % and APR**, open interest, mark/index premium, the cross-venue median, spread, the cheapest venue to
5
+ be long or short and a positioning read (crowded long / long bias / neutral / short bias / crowded short). The full tier adds
6
+ 7-day funding history with z-score and percentile, the funding regime and carry; the scan finds funding extremes and the widest
7
+ cross-venue spreads across the whole market.
8
+
9
+ > **Not financial advice. Past performance does not guarantee future results.** Read-only market analytics from public venue
10
+ > data. PerpRadar never trades, never holds funds and never asks for keys.
11
+
12
+ - API: https://api.youraitools.co/perpradar (fallback https://perpradar.agentgate.workers.dev)
13
+ - Remote MCP: `https://api.youraitools.co/perpradar/mcp` (tool `perp_positioning`, free preview)
14
+ - stdio MCP: `npx -y @agentgate-builder/perpradar` (tools `perp_funding`, `perp_funding_scan`)
15
+ - OpenAPI: `/openapi.json` · x402 manifest: `/.well-known/x402`
16
+
17
+ ## Routes and prices
18
+
19
+ | Route | Price | Returns |
20
+ |---|---|---|
21
+ | `GET /v1/lite?symbol=BTC` | $0.005 | per-venue funding (8h %, APR) and OI, median and OI-weighted funding, spread, cheapest venue for longs/shorts, positioning |
22
+ | `GET /v1/full?symbol=ETH` | $0.03 | lite + mark/index premium, 24h volume, Hyperliquid 7-day and dYdX 100-hour funding history (mean, min, max, z-score, percentile), regime, carry |
23
+ | `GET /v1/scan?universe=all` or `?symbols=BTC,ETH,SOL` | $0.02 | highest / lowest funding, widest cross-venue spreads, market-wide OI-weighted funding |
24
+ | `GET /v1/preview?symbol=BTC` | free (10/min/IP) | positioning label, venues, confidence (no rates) |
25
+
26
+ All paid routes accept POST with the same fields as JSON. Payment: x402 v2 (`PAYMENT-SIGNATURE`; USDC on Base, Polygon,
27
+ Arbitrum One or Solana) or v1 (`X-PAYMENT`, Base), or a prepaid YourAITools credits key (`Authorization: Bearer yatc_...`).
28
+ Bad input (400), unknown symbols (404) and upstream failures (503) are never charged.
29
+
30
+ ## Fail-closed
31
+
32
+ A venue that errors or times out is listed in `sources_failed` and left out: never estimated. The cross-venue read needs at
33
+ least 2 venues (otherwise `positioning: "unknown"`, confidence low). No venue at all = HTTP 503 `upstream_unavailable`, not charged.
34
+
35
+ ## stdio MCP paid mode
36
+
37
+ Set `PERPRADAR_X402_PRIVATE_KEY` (a Base wallet with a little USDC) and install the optional peers:
38
+ `npx -y -p @agentgate-builder/perpradar -p @x402/fetch -p @x402/evm -p viem perpradar`. Spend guard: Base USDC only,
39
+ max $0.03 per payment, only to the PerpRadar payTo.
40
+
41
+ ## Develop
42
+
43
+ `npm test` (offline mocks) · `npm start` (Node server) · `wrangler deploy` (Cloudflare Worker).
44
+
45
+ MIT. Data: public market-data APIs of Hyperliquid, dYdX v4 indexer, Paradex and Aevo.
package/logo.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="AgentReady"><rect width="64" height="64" rx="14" fill="#0f172a"/><rect x="14" y="16" width="36" height="28" rx="6" fill="none" stroke="#38bdf8" stroke-width="3.2"/><circle cx="25" cy="29" r="3.2" fill="#38bdf8"/><circle cx="39" cy="29" r="3.2" fill="#38bdf8"/><path d="M32 8v8M22 50h20" stroke="#38bdf8" stroke-width="3.2" stroke-linecap="round"/><path d="M40 44l5 5 9-10" fill="none" stroke="#22c55e" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>
package/package.json CHANGED
@@ -1,6 +1,102 @@
1
1
  {
2
2
  "name": "@agentgate-builder/perpradar",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "mcpName": "co.youraitools/perpradar",
5
+ "description": "Perp funding rates, open interest and positioning for trading agents: MCP server + client for PerpRadar. Funding normalized to 8h and APR, OI, mark/index premium, cross-venue median and spread, 7-day funding history and z-score, carry and a universe scan for funding extremes across Hyperliquid, dYdX v4, Paradex and Aevo. Free MCP preview; full data via x402 pay-per-call API. Read-only analytics. Not financial advice.",
6
+ "type": "module",
7
+ "main": "src/lite.js",
8
+ "bin": {
9
+ "perpradar": "src/mcp-stdio.js",
10
+ "perpradar-mcp": "src/mcp-stdio.js"
11
+ },
12
+ "exports": {
13
+ ".": "./src/lite.js",
14
+ "./lite": "./src/lite.js",
15
+ "./mcp": "./src/mcp-stdio.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "src/mcp-stdio.js",
20
+ "src/lite.js",
21
+ "src/paid.js",
22
+ "src/deadline.js",
23
+ "src/version.js",
24
+ "README.md",
25
+ "LICENSE",
26
+ "logo.svg"
27
+ ],
28
+ "scripts": {
29
+ "test": "node --test test/*.test.js",
30
+ "start": "node src/node.js",
31
+ "deploy": "wrangler deploy",
32
+ "mcp": "node src/mcp-stdio.js",
33
+ "gen:examples": "node tools/gen-examples.mjs"
34
+ },
35
+ "keywords": [
36
+ "funding-rate",
37
+ "funding-rates",
38
+ "perpetual-futures",
39
+ "perps",
40
+ "open-interest",
41
+ "hyperliquid",
42
+ "dydx",
43
+ "paradex",
44
+ "aevo",
45
+ "crypto-trading",
46
+ "trading-bot",
47
+ "market-data",
48
+ "mcp",
49
+ "mcp-server",
50
+ "x402",
51
+ "usdc",
52
+ "ai-agents"
53
+ ],
54
+ "author": "agentgate-builder",
55
+ "license": "MIT",
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "https://agentgate-source.pages.dev/x402-perpradar/"
59
+ },
60
+ "homepage": "https://api.youraitools.co/perpradar",
61
+ "bugs": {
62
+ "url": "https://agentgate-source.pages.dev/x402-perpradar/",
63
+ "email": "hello@youraitools.co"
64
+ },
65
+ "engines": {
66
+ "node": ">=20"
67
+ },
68
+ "dependencies": {},
69
+ "devDependencies": {
70
+ "@coinbase/x402": "2.1.0",
71
+ "@hono/node-server": "^1.19.9",
72
+ "@x402/core": "2.28.0",
73
+ "@x402/evm": "2.28.0",
74
+ "@x402/extensions": "2.28.0",
75
+ "@x402/fetch": "2.28.0",
76
+ "@x402/hono": "2.28.0",
77
+ "hono": "^4.13.13",
78
+ "viem": "^2.0.0",
79
+ "wrangler": "^4.86.0",
80
+ "x402": "1.2.0",
81
+ "x402-fetch": "1.2.0"
82
+ },
83
+ "publishConfig": {
84
+ "access": "public"
85
+ },
86
+ "peerDependencies": {
87
+ "@x402/fetch": ">=2.28.0 <3",
88
+ "@x402/evm": ">=2.28.0 <3",
89
+ "viem": "^2.0.0"
90
+ },
91
+ "peerDependenciesMeta": {
92
+ "@x402/fetch": {
93
+ "optional": true
94
+ },
95
+ "@x402/evm": {
96
+ "optional": true
97
+ },
98
+ "viem": {
99
+ "optional": true
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,24 @@
1
+ // Hard deadlines so no request can hang: every awaited network step races a timer, and the Worker entry has a
2
+ // last-resort watchdog. Budgets (paid route, worst case): verify 8 s + work <= 20 s + settle 15 s < 45 s watchdog.
3
+ export const BUDGET = { supportedMs: 3000, verifyMs: 8000, settleMs: 15000, workMs: 20000, watchdogMs: 45000 };
4
+
5
+ export class DeadlineError extends Error {
6
+ constructor(what, ms) { super(`${what} timed out after ${ms} ms`); this.name = 'DeadlineError'; this.code = 'timeout'; }
7
+ }
8
+
9
+ /** Resolve/reject with `p`, or reject with DeadlineError after `ms`. The timer is always cleared. */
10
+ export function withDeadline(p, ms, what = 'operation') {
11
+ let t;
12
+ const timer = new Promise((_, rej) => { t = setTimeout(() => rej(new DeadlineError(what, ms)), ms); });
13
+ return Promise.race([Promise.resolve(p), timer]).finally(() => clearTimeout(t));
14
+ }
15
+
16
+ /** Wrap a Worker fetch handler: answer 503 (never charged, settlement only follows a 2xx) instead of hanging. */
17
+ export function watchdog(handler, ms = BUDGET.watchdogMs) {
18
+ return (request, env, ctx) => withDeadline(Promise.resolve().then(() => handler(request, env, ctx)), ms, 'request').catch((e) => {
19
+ console.error(`watchdog: ${request.method} ${new URL(request.url).pathname}: ${e && e.message}`);
20
+ return new Response(JSON.stringify({ error: e instanceof DeadlineError ? 'timeout' : 'internal_error', message: 'The request took too long or failed; it was not charged. Please retry.' }), {
21
+ status: 503, headers: { 'content-type': 'application/json', 'retry-after': '5', 'access-control-allow-origin': '*' },
22
+ });
23
+ });
24
+ }
package/src/lite.js ADDED
@@ -0,0 +1,29 @@
1
+ // PerpRadar client used by the npm package (library + stdio MCP server). Free tier: the hosted preview
2
+ // (positioning label, venues, confidence; 10/min per IP). Rates, OI, history and scans: paid API over x402 (see paid.js).
3
+ import { withDeadline } from './deadline.js';
4
+
5
+ export const API_BASE = 'https://api.youraitools.co/perpradar';
6
+ export const FALLBACK_API_BASE = 'https://perpradar.agentgate.workers.dev';
7
+ export const PAID_URL = `${API_BASE}/v1/full`;
8
+ export const PRICES = { lite: '$0.005', full: '$0.03', scan: '$0.02' };
9
+ export const DISCLAIMER = 'Not financial advice. Past performance does not guarantee future results. Read-only market analytics; PerpRadar never trades, holds funds or keys.';
10
+ export const FULL_DETAILS = `Per-venue funding (8h %, APR), open interest, premium, 7-day funding history and carry: paid API ${API_BASE}/v1/lite|full?symbol=<ticker> (${PRICES.lite} / ${PRICES.full} USDC over x402, or a prepaid credits key); universe scan ${API_BASE}/v1/scan?universe=all (${PRICES.scan}).`;
11
+
12
+ /** Validate the symbol argument locally (no network). Returns { symbol } or { error }. */
13
+ export function toSymbol(args = {}) {
14
+ const v = args.symbol ?? args.coin ?? args.asset ?? args.ticker;
15
+ if (typeof v !== 'string' || !v.trim()) return { error: '"symbol" is required: an asset ticker, e.g. BTC' };
16
+ let s = v.trim().toUpperCase().replace(/[\s_/]+/g, '-').replace(/-?(PERP|PERPETUAL|SWAP)$/, '').replace(/-?(USDT|USDC|USD)$/, '').replace(/-+$/, '');
17
+ if (!/^[A-Z0-9]{1,15}$/.test(s)) return { error: '"symbol" must be a ticker like BTC, ETH or SOL' };
18
+ return { symbol: s };
19
+ }
20
+
21
+ /** Free hosted preview. Returns the preview JSON or { error, message }. */
22
+ export async function preview(symbol, { fetchImpl = globalThis.fetch, apiBase = API_BASE, ms = 30000 } = {}) {
23
+ try {
24
+ const res = await withDeadline(fetchImpl(`${apiBase.replace(/\/+$/, '')}/v1/preview?symbol=${encodeURIComponent(symbol)}`, { headers: { accept: 'application/json', 'user-agent': 'perpradar-npm' } }), ms, 'PerpRadar preview');
25
+ const j = await res.json().catch(() => null);
26
+ if (!res.ok) return { error: j?.error || `http_${res.status}`, message: j?.message || `PerpRadar preview answered HTTP ${res.status}` };
27
+ return j;
28
+ } catch (e) { return { error: 'network', message: String(e?.message || e).slice(0, 200) }; }
29
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // PerpRadar MCP server over stdio (newline-delimited JSON-RPC 2.0).
3
+ // Free by default: the hosted free preview (positioning label, venues, confidence; 10/min per IP).
4
+ // Full data: set PERPRADAR_X402_PRIVATE_KEY (or X402_PRIVATE_KEY) to a Base wallet key holding a little USDC, and this
5
+ // server pays the hosted API per call over x402 (full $0.03, scan $0.02): https://api.youraitools.co/perpradar
6
+ // npx -y @agentgate-builder/perpradar (bin: perpradar)
7
+ import { createInterface } from 'node:readline';
8
+ import { toSymbol, preview, DISCLAIMER, FULL_DETAILS, API_BASE, PRICES } from './lite.js';
9
+ import { createPaidClient } from './paid.js';
10
+ import { VERSION_STR as VERSION } from './version.js';
11
+
12
+ export { VERSION };
13
+ const SYM = { type: 'string', description: 'Asset ticker, e.g. BTC, ETH, SOL, HYPE' };
14
+ const RO = { readOnlyHint: true, openWorldHint: true, idempotentHint: false };
15
+ export const TOOLS = [
16
+ { name: 'perp_funding', title: 'Perp funding, OI and positioning for an asset', description: `Perpetual futures funding read for one asset across Hyperliquid, dYdX v4, Paradex and Aevo. Free by default: cross-venue positioning label (crowded long ... crowded short), venues and confidence. With PERPRADAR_X402_PRIVATE_KEY set, returns the paid full tier (${PRICES.full} USDC on Base per call over x402): per-venue funding (8h %, APR), open interest, premium, 7-day funding history with z-score, regime and carry. Read-only; not financial advice.`, inputSchema: { type: 'object', properties: { symbol: SYM }, required: ['symbol'] }, annotations: RO },
17
+ { name: 'perp_funding_scan', title: 'Scan perp funding extremes and spreads', description: `Paid only (needs PERPRADAR_X402_PRIVATE_KEY, ${PRICES.scan} USDC on Base per call): highest and lowest funding, widest cross-venue spreads and market-wide OI-weighted funding across Hyperliquid, dYdX and Paradex; pass symbols (comma list) for a watchlist or leave empty for the whole universe. Read-only; not financial advice.`, inputSchema: { type: 'object', properties: { symbols: { type: 'string', description: 'Optional comma-separated tickers (max 25); empty = whole universe' }, limit: { type: 'integer', description: '1-25, default 10' } } }, annotations: RO },
18
+ ];
19
+ const PROTOCOLS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
20
+ const summarizePreview = (r, note) => `${note ? note + '\n' : ''}PerpRadar ${r.symbol}: ${String(r.positioning).replace(/_/g, ' ')}. ${r.positioning_note || ''} Venues: ${(r.venues_listed || []).join(', ')}; confidence ${r.confidence}.\n${FULL_DETAILS} Set PERPRADAR_X402_PRIVATE_KEY to get them here.\n${DISCLAIMER}`;
21
+ const summarizeFull = (r) => `PerpRadar ${r.symbol}: ${String(r.positioning).replace(/_/g, ' ')}; median funding ${r.funding?.median_apr_pct}% APR (spread ${r.funding?.cross_venue_spread_apr_pct}%), OI $${r.open_interest_usd_total}. Regime: ${r.regime?.vs_recent}.\n${(r.venues || []).map((v) => `- ${v.venue}: ${v.funding_8h_pct}%/8h (${v.funding_apr_pct}% APR), OI $${v.open_interest_usd}, premium ${v.premium_pct}%`).join('\n')}\n${DISCLAIMER}`;
22
+ const summarizeScan = (r) => `PerpRadar scan (${r.mode}, ${r.assets} assets): market OI-weighted funding ${r.market?.oi_weighted_funding_apr_pct}% APR (${r.market?.read}).\nHighest: ${(r.highest_funding || []).map((x) => `${x.symbol} ${x.median_apr_pct}%`).join(', ')}\nLowest: ${(r.lowest_funding || []).map((x) => `${x.symbol} ${x.median_apr_pct}%`).join(', ')}\nWidest spreads: ${(r.widest_spreads || []).map((x) => `${x.symbol} ${x.spread_apr_pct}%`).join(', ')}\n${DISCLAIMER}`;
23
+
24
+ let paidClient;
25
+ const paid = async (opts) => { if (paidClient === undefined) paidClient = await createPaidClient(opts); return paidClient; };
26
+
27
+ export async function handle(m, opts = {}) {
28
+ if (!m || m.jsonrpc !== '2.0' || typeof m.method !== 'string') return { jsonrpc: '2.0', id: m?.id ?? null, error: { code: -32600, message: 'Invalid Request' } };
29
+ const note = m.id === undefined || m.id === null;
30
+ const ok = (result) => (note ? null : { jsonrpc: '2.0', id: m.id, result });
31
+ const err = (text) => ok({ isError: true, content: [{ type: 'text', text }] });
32
+ switch (m.method) {
33
+ case 'initialize': {
34
+ const client = await paid(opts);
35
+ const mode = client && !client.error ? `Paid mode: full data from ${client.apiBase} over x402.` : `Free preview mode; rates and scans at ${API_BASE} (x402).`;
36
+ return ok({ protocolVersion: PROTOCOLS.includes(m.params?.protocolVersion) ? m.params.protocolVersion : '2025-06-18', capabilities: { tools: { listChanged: false } }, serverInfo: { name: 'perpradar', title: 'PerpRadar', version: VERSION }, instructions: `Call perp_funding with an asset ticker for the cross-venue perp funding read. ${mode} Not financial advice.` });
37
+ }
38
+ case 'ping': return ok({});
39
+ case 'tools/list': return ok({ tools: TOOLS });
40
+ case 'tools/call': {
41
+ const name = m.params?.name;
42
+ if (!TOOLS.some((t) => t.name === name)) return note ? null : { jsonrpc: '2.0', id: m.id, error: { code: -32602, message: `Unknown tool: ${name}` } };
43
+ const args = m.params?.arguments || {};
44
+ const client = await paid(opts);
45
+ if (name === 'perp_funding_scan') {
46
+ if (!client || client.error) return err(`perp_funding_scan is paid only: set PERPRADAR_X402_PRIVATE_KEY (Base USDC) or call ${API_BASE}/v1/scan?universe=all with any x402 client (${PRICES.scan}).${client?.error ? ` (${client.error})` : ''}`);
47
+ const lim = args.limit === undefined ? 10 : Number(args.limit);
48
+ const input = args.symbols ? { symbols: String(args.symbols), limit: lim } : { universe: 'all', limit: lim };
49
+ try {
50
+ const r = await client.call('scan', input);
51
+ if (r.result && !r.error) return ok({ content: [{ type: 'text', text: summarizeScan(r.result) }], structuredContent: { ...r.result, payer: client.payer }, isError: false });
52
+ return err(`${r.error}${r.detail ? `: ${r.detail}` : ''}`);
53
+ } catch (e) { return err(`paid call failed: ${String(e?.message || e).slice(0, 200)}`); }
54
+ }
55
+ const t = toSymbol(args);
56
+ if (t.error) return err(`bad_request: ${t.error}`);
57
+ let fallbackNote = '';
58
+ if (client?.error) fallbackNote = client.error;
59
+ else if (client) {
60
+ try {
61
+ const r = await client.call('full', { symbol: t.symbol });
62
+ if (r.result && !r.error) return ok({ content: [{ type: 'text', text: summarizeFull(r.result) }], structuredContent: { ...r.result, payer: client.payer, ...(r.paymentResponse ? { payment_response: r.paymentResponse } : {}) }, isError: false });
63
+ fallbackNote = `Paid call failed (${r.error}${r.detail ? `: ${r.detail}` : ''}); showing the free preview.`;
64
+ } catch (e) { fallbackNote = `Paid call failed (${String(e?.message || e).slice(0, 200)}); showing the free preview.`; }
65
+ }
66
+ const r = await preview(t.symbol, { fetchImpl: opts.fetchImpl, apiBase: opts.apiBase || process.env.PERPRADAR_API_URL || API_BASE });
67
+ if (r.error) return err(`${r.error}: ${r.message}${fallbackNote ? ` (${fallbackNote})` : ''}`);
68
+ delete r.full;
69
+ return ok({ content: [{ type: 'text', text: summarizePreview(r, fallbackNote) }], structuredContent: fallbackNote ? { ...r, paid_error: fallbackNote } : r, isError: false });
70
+ }
71
+ default:
72
+ return m.method.startsWith('notifications/') || note ? null : { jsonrpc: '2.0', id: m.id, error: { code: -32601, message: `Method not found: ${m.method}` } };
73
+ }
74
+ }
75
+
76
+ export function serve({ input = process.stdin, output = process.stdout } = {}) {
77
+ const rl = createInterface({ input, crlfDelay: Infinity });
78
+ const write = (o) => output.write(JSON.stringify(o) + '\n');
79
+ rl.on('line', async (l) => {
80
+ if (!l.trim()) return;
81
+ let msg; try { msg = JSON.parse(l); } catch { return write({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }); }
82
+ const batch = Array.isArray(msg);
83
+ const replies = (await Promise.all((batch ? msg : [msg]).map((x) => handle(x)))).filter(Boolean);
84
+ if (replies.length) write(batch ? replies : replies[0]);
85
+ });
86
+ return rl;
87
+ }
88
+
89
+ const self = process.argv[1] || '';
90
+ if (import.meta.url === `file://${self}` || /perpradar(-mcp)?$/.test(self)) serve();
package/src/paid.js ADDED
@@ -0,0 +1,46 @@
1
+ // Optional paid tier for the stdio MCP server: the PerpRadar full tier from the hosted API, paid per call over x402
2
+ // with the user's own wallet. Only active when PERPRADAR_X402_PRIVATE_KEY (or X402_PRIVATE_KEY) is set.
3
+ // Needs the optional peer dependencies @x402/fetch, @x402/evm and viem (loaded lazily; the free path never imports them).
4
+ // Spend guard: Base USDC only, at most $0.03 per payment, and only to the PerpRadar pay-to address.
5
+ import { API_BASE } from './lite.js';
6
+
7
+ export const PAY_TO = '0x3c1A1E01e790Cd326495631B3b38Bd7068F08d6F';
8
+ export const NETWORK = 'eip155:8453';
9
+ export const MAX_PER_PAYMENT = '$0.03';
10
+ export const KEY_ENVS = ['PERPRADAR_X402_PRIVATE_KEY', 'X402_PRIVATE_KEY'];
11
+ export const INSTALL_HINT = 'Paid mode needs @x402/fetch, @x402/evm and viem next to this package, e.g. command "npx" with args ["-y", "-p", "@agentgate-builder/perpradar", "-p", "@x402/fetch", "-p", "@x402/evm", "-p", "viem", "perpradar"].';
12
+
13
+ export function keyFromEnv(env = process.env) {
14
+ for (const k of KEY_ENVS) { const v = (env[k] || '').trim(); if (v) return { name: k, value: v }; }
15
+ return null;
16
+ }
17
+
18
+ /** Returns a paying client, null (no key) or { error } (never throws, never echoes the key). */
19
+ export async function createPaidClient({ env = process.env, fetchImpl = globalThis.fetch, apiBase = (env.PERPRADAR_API_URL || API_BASE).replace(/\/+$/, ''), payTo = PAY_TO } = {}) {
20
+ const key = keyFromEnv(env);
21
+ if (!key) return null;
22
+ const hex = key.value.startsWith('0x') ? key.value : `0x${key.value}`;
23
+ if (!/^0x[0-9a-fA-F]{64}$/.test(hex)) return { error: `${key.name} is set but is not a 32-byte hex private key; using the free preview.` };
24
+ let wrap, ExactEvmScheme, privateKeyToAccount;
25
+ try {
26
+ ({ wrapFetchWithPaymentFromConfig: wrap } = await import('@x402/fetch'));
27
+ ({ ExactEvmScheme } = await import('@x402/evm'));
28
+ ({ privateKeyToAccount } = await import('viem/accounts'));
29
+ } catch {
30
+ return { error: `${key.name} is set, but ${INSTALL_HINT} Using the free preview.` };
31
+ }
32
+ let account;
33
+ try { account = privateKeyToAccount(hex); } catch { return { error: `${key.name} could not be loaded as an EVM private key; using the free preview.` }; }
34
+ const onlyUs = (_v, reqs) => reqs.filter((r) => r.network === NETWORK && String(r.payTo).toLowerCase() === payTo.toLowerCase());
35
+ const pay = wrap(fetchImpl, { schemes: [{ network: NETWORK, client: new ExactEvmScheme(account) }], policies: [onlyUs], spendControls: { maxAmountPerPayment: MAX_PER_PAYMENT } });
36
+ return {
37
+ payer: account.address, apiBase,
38
+ call: async (tier, input) => {
39
+ const res = await pay(`${apiBase}/v1/${tier}`, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify(input) });
40
+ const text = await res.text();
41
+ let json = null; try { json = JSON.parse(text); } catch { /* not JSON */ }
42
+ if (!res.ok) return { error: `paid API answered HTTP ${res.status}`, detail: json?.message || json?.error || text.slice(0, 300) };
43
+ return { result: json, paymentResponse: res.headers.get('payment-response') || res.headers.get('x-payment-response') || null };
44
+ },
45
+ };
46
+ }
package/src/version.js ADDED
@@ -0,0 +1 @@
1
+ export const VERSION_STR = '0.1.0';