coinopai-mcp 2.2.0 → 2.2.2
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 +20 -75
- package/index.js +87 -196
- package/package.json +3 -3
- package/server.json +3 -3
- package/x402-guard.js +141 -0
package/README.md
CHANGED
|
@@ -62,9 +62,9 @@ Trust the process less. Verify the record more.
|
|
|
62
62
|
|
|
63
63
|
The agent calls a tool → the MCP server receives an `HTTP 402` → automatically signs a USDC micropayment → retries with the payment header → data returned. Configure once, pay automatically from the configured low-balance wallet.
|
|
64
64
|
|
|
65
|
-
Current package: `coinopai-mcp@
|
|
65
|
+
Current package: `coinopai-mcp@2.2.2`.
|
|
66
66
|
|
|
67
|
-
Settlement note: the MCP pins `@x402/core` and `@x402/evm` to `2.11.0
|
|
67
|
+
Settlement note: the MCP pins `@x402/core` and `@x402/evm` to `2.11.0`. Successful object responses include x402 settlement metadata under `_payment`, including the on-chain transaction hash when available.
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
@@ -160,22 +160,22 @@ Fresh audits can return `pending_window` until the evaluation window matures.
|
|
|
160
160
|
|
|
161
161
|
## Tools
|
|
162
162
|
|
|
163
|
-
| Tool | What it does | Cost |
|
|
164
|
-
|
|
165
|
-
| `check_trade_preflight` | Gate check: market allowed, cooldown, regime, model context | $0.05 |
|
|
166
|
-
| `get_crypto_decision` | Probabilistic decision journal + `decision_id` | $0.15 |
|
|
167
|
-
| `audit_trade_decision` | Verify against real prices: verdict + PnL% | $0.07 |
|
|
168
|
-
| `get_crypto_signals` | Model context for BTC, ETH, SOL, XRP, ADA | $0.05 |
|
|
169
|
-
| `get_crypto_signal_history` | Up to 168h of context history for analysis | $0.05 |
|
|
170
|
-
| `get_crypto_forecast` | Conformally-calibrated 80% price range (~0.80 empirical coverage) for BTC, ETH, SOL, XRP, ADA | $0.05 |
|
|
171
|
-
| `get_perp_funding` | **Futures.** Live perp funding, mark/index, open interest, crowding (Kraken Futures + Hyperliquid) | $0.02 |
|
|
172
|
-
| `check_futures_risk` | **Futures.** Will this side / leverage / entry survive the calibrated range? Liquidation distance + verdict | $0.05 |
|
|
173
|
-
| `get_futures_decision` | **Futures.** LONG / SHORT / FLAT with stop and target on the calibrated range, leverage cap, liquidation, sizing, funding cost | $0.15 |
|
|
174
|
-
| `review_signal_anomaly` | Score signal features for unusual conditions; returns review labels, drivers, and component scores | $0.07 |
|
|
175
|
-
| `get_crypto_risk` | Market risk state and cooldown context | $0.02 |
|
|
176
|
-
| `search_agent_automations` | Search 819 agent automation prompts | $0.01 |
|
|
177
|
-
| `get_agent_automation` | Full prompt + workflow steps by slug | $0.01 |
|
|
178
|
-
| `list_automation_categories` | All 35 automation categories with counts | $0.005 |
|
|
163
|
+
| Tool | What it does | Cost |
|
|
164
|
+
|------|-------------|------|
|
|
165
|
+
| `check_trade_preflight` | Gate check: market allowed, cooldown, regime, model context | $0.05 |
|
|
166
|
+
| `get_crypto_decision` | Probabilistic decision journal + `decision_id` | $0.15 |
|
|
167
|
+
| `audit_trade_decision` | Verify against real prices: verdict + PnL% | $0.07 |
|
|
168
|
+
| `get_crypto_signals` | Model context for BTC, ETH, SOL, XRP, ADA | $0.05 |
|
|
169
|
+
| `get_crypto_signal_history` | Up to 168h of context history for analysis | $0.05 |
|
|
170
|
+
| `get_crypto_forecast` | Conformally-calibrated 80% price range (~0.80 empirical coverage) for BTC, ETH, SOL, XRP, ADA | $0.05 |
|
|
171
|
+
| `get_perp_funding` | **Futures.** Live perp funding, mark/index, open interest, crowding (Kraken Futures + Hyperliquid) | $0.02 |
|
|
172
|
+
| `check_futures_risk` | **Futures.** Will this side / leverage / entry survive the calibrated range? Liquidation distance + verdict | $0.05 |
|
|
173
|
+
| `get_futures_decision` | **Futures.** LONG / SHORT / FLAT with stop and target on the calibrated range, leverage cap, liquidation, sizing, funding cost | $0.15 |
|
|
174
|
+
| `review_signal_anomaly` | Score signal features for unusual conditions; returns review labels, drivers, and component scores | $0.07 |
|
|
175
|
+
| `get_crypto_risk` | Market risk state and cooldown context | $0.02 |
|
|
176
|
+
| `search_agent_automations` | Search 819 agent automation prompts | $0.01 |
|
|
177
|
+
| `get_agent_automation` | Full prompt + workflow steps by slug | $0.01 |
|
|
178
|
+
| `list_automation_categories` | All 35 automation categories with counts | $0.005 |
|
|
179
179
|
|
|
180
180
|
No API keys. No subscriptions. Pay per call in USDC.
|
|
181
181
|
|
|
@@ -242,6 +242,8 @@ Prepared MCP Registry identity: `io.github.forgemeshlabs/coinopai-mcp`. Refresh
|
|
|
242
242
|
3. Buy or bridge USDC ($1 = ~3 full verified cycles)
|
|
243
243
|
4. Use a dedicated low-balance Base wallet for agent payments and provide its private key locally via environment variable.
|
|
244
244
|
|
|
245
|
+
**Payment safeguards.** The server only signs for the CoinOpAI payee wallet on Base mainnet, in USDC, at or below $0.15 per call and $10 per session. It refuses any other payee, network, asset, or amount before signing. Env `X402_MAX_PRICE_USD` and `X402_SESSION_BUDGET_USD` can only lower these caps. The server talks only to `https://x402.coinopai.com` (no redirects, 60s timeout, 2 MB response cap). Use a dedicated, low-balance wallet.
|
|
246
|
+
|
|
245
247
|
> Your wallet key stays local. It never leaves your machine. Each payment is a signed micropayment — not a blanket approval.
|
|
246
248
|
|
|
247
249
|
---
|
|
@@ -331,63 +333,6 @@ Free-tier tool responses may carry a single disclosed sponsored card from [Lulu
|
|
|
331
333
|
|
|
332
334
|
---
|
|
333
335
|
|
|
334
|
-
## Affiliate Attribution (via Pyrimid)
|
|
335
|
-
|
|
336
|
-
High-value tools accept an optional `affiliate_id` parameter. When provided, payment routes through the [Pyrimid](https://pyrimid.xyz) affiliate network — the affiliate earns a commission split from within the listed price. **No extra cost to the caller.**
|
|
337
|
-
|
|
338
|
-
### How the split works
|
|
339
|
-
|
|
340
|
-
```
|
|
341
|
-
Direct call (no affiliate_id):
|
|
342
|
-
Caller pays $0.15 → CoinOpAI receives $0.15
|
|
343
|
-
|
|
344
|
-
Affiliate call (affiliate_id present):
|
|
345
|
-
Caller pays $0.15 → CoinOpAI: 79.2% ($0.1188)
|
|
346
|
-
→ Affiliate: 19.8% ($0.0297)
|
|
347
|
-
→ Protocol: 1.0% ($0.0015)
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
The buyer always pays the listed price. The split comes out of the vendor's portion.
|
|
351
|
-
|
|
352
|
-
### Usage
|
|
353
|
-
|
|
354
|
-
Pass `affiliate_id` in any supporting tool call:
|
|
355
|
-
|
|
356
|
-
```js
|
|
357
|
-
// As an agent or user
|
|
358
|
-
await mcp.call("get_crypto_decision", {
|
|
359
|
-
symbol: "BTC",
|
|
360
|
-
affiliate_id: "af_youraffiliateID"
|
|
361
|
-
})
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
### Building a wrapper? Set it once via env
|
|
365
|
-
|
|
366
|
-
If you're building an agent framework, MCP wrapper, or automation that embeds CoinOpAI tools, set your affiliate ID as an environment variable. Every call through your wrapper earns you a commission automatically.
|
|
367
|
-
|
|
368
|
-
```json
|
|
369
|
-
{
|
|
370
|
-
"mcpServers": {
|
|
371
|
-
"coinopai": {
|
|
372
|
-
"command": "npx",
|
|
373
|
-
"args": ["-y", "coinopai-mcp"],
|
|
374
|
-
"env": {
|
|
375
|
-
"WALLET_PRIVATE_KEY": "0x<agent-wallet-key>",
|
|
376
|
-
"PYRIMID_AFFILIATE_ID": "af_<your-affiliate-id>"
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
The tool-level `affiliate_id` argument takes precedence over the env var. Callers can always override.
|
|
384
|
-
|
|
385
|
-
### Without an affiliate_id
|
|
386
|
-
|
|
387
|
-
Normal x402 flow — CoinOpAI receives 100% of the listed price. Nothing changes for the caller.
|
|
388
|
-
|
|
389
|
-
---
|
|
390
|
-
|
|
391
336
|
## Disclaimer
|
|
392
337
|
|
|
393
338
|
Decision outputs are probabilistic context and journal entries for experimental automated workflows only. Not financial advice. Directional bias alone has not been validated as a standalone trading strategy. Results will vary. Never risk capital you can't afford to lose.
|
package/index.js
CHANGED
|
@@ -8,36 +8,17 @@ const { x402Client, x402HTTPClient } = require("@x402/core/client");
|
|
|
8
8
|
const { ExactEvmScheme } = require("@x402/evm/exact/client");
|
|
9
9
|
const { toClientEvmSigner } = require("@x402/evm");
|
|
10
10
|
const { privateKeyToAccount } = require("viem/accounts");
|
|
11
|
-
const { createPublicClient, createWalletClient, http, parseAbi } = require("viem");
|
|
12
|
-
const { base } = require("viem/chains");
|
|
13
11
|
const { attachSponsored, FREE_TIER_TOOLS } = require("./ads.js");
|
|
12
|
+
const { createGuard } = require("./x402-guard");
|
|
14
13
|
|
|
15
14
|
const BASE_URL = "https://x402.coinopai.com";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
]);
|
|
24
|
-
const USDC_ABI = parseAbi([
|
|
25
|
-
"function approve(address spender, uint256 amount) external returns (bool)",
|
|
26
|
-
]);
|
|
27
|
-
// Paths with registered Pyrimid product IDs — other paths fall back to standard x402
|
|
28
|
-
const PYRIMID_PRODUCTS = {
|
|
29
|
-
"/api/kronos/signals": { productId: 1n, priceUsdc: 50000n },
|
|
30
|
-
"/api/kronos/decision": { productId: 2n, priceUsdc: 150000n },
|
|
31
|
-
"/api/kronos/preflight": { productId: 4n, priceUsdc: 50000n },
|
|
32
|
-
"/api/kronos/audit": { productId: 5n, priceUsdc: 70000n },
|
|
33
|
-
"/api/kronos/risk": { productId: 6n, priceUsdc: 20000n },
|
|
34
|
-
"/api/kronos/history": { productId: 7n, priceUsdc: 50000n },
|
|
35
|
-
};
|
|
36
|
-
const IMAGEGEN_URL = "https://imagegen.coinopai.com";
|
|
37
|
-
const PYRIMID_PRODUCTS_IMAGEGEN = {
|
|
38
|
-
"/generate": { productId: 3n, priceUsdc: 100000n },
|
|
39
|
-
};
|
|
40
|
-
const BASE_RPC_URL = process.env.BASE_RPC_URL || "https://mainnet.base.org";
|
|
15
|
+
// Highest listed price on this backend is $0.15; the guard refuses anything above it.
|
|
16
|
+
const guard = createGuard({
|
|
17
|
+
baseUrl: BASE_URL,
|
|
18
|
+
payTo: ["0x1304EC1A8945365e43A5c18a734065f107B417cA"],
|
|
19
|
+
maxPriceUsd: 0.15,
|
|
20
|
+
sessionBudgetUsd: 10,
|
|
21
|
+
});
|
|
41
22
|
|
|
42
23
|
const TOOLS = [
|
|
43
24
|
{
|
|
@@ -52,7 +33,7 @@ const TOOLS = [
|
|
|
52
33
|
inputSchema: {
|
|
53
34
|
type: "object",
|
|
54
35
|
properties: {
|
|
55
|
-
query: { type: "string", description: "Search keyword (e.g. 'slack', 'notion', 'github')" },
|
|
36
|
+
query: { type: "string", maxLength: 200, description: "Search keyword (e.g. 'slack', 'notion', 'github')" },
|
|
56
37
|
limit: { type: "number", description: "Max results to return (default 20, max 50)" }
|
|
57
38
|
},
|
|
58
39
|
required: ["query"]
|
|
@@ -70,7 +51,7 @@ const TOOLS = [
|
|
|
70
51
|
inputSchema: {
|
|
71
52
|
type: "object",
|
|
72
53
|
properties: {
|
|
73
|
-
slug: { type: "string", description: "Automation slug (e.g. 'slack-to-notion')" }
|
|
54
|
+
slug: { type: "string", maxLength: 200, pattern: "^[A-Za-z0-9._-]+$", description: "Automation slug (e.g. 'slack-to-notion')" }
|
|
74
55
|
},
|
|
75
56
|
required: ["slug"]
|
|
76
57
|
}
|
|
@@ -98,7 +79,6 @@ const TOOLS = [
|
|
|
98
79
|
inputSchema: {
|
|
99
80
|
type: "object",
|
|
100
81
|
properties: {
|
|
101
|
-
affiliate_id: { type: "string", description: "Optional Pyrimid affiliate ID (af_xxxxx). Affiliate earns a commission from within the listed price — no extra cost to you." }
|
|
102
82
|
}
|
|
103
83
|
}
|
|
104
84
|
},
|
|
@@ -125,8 +105,7 @@ const TOOLS = [
|
|
|
125
105
|
inputSchema: {
|
|
126
106
|
type: "object",
|
|
127
107
|
properties: {
|
|
128
|
-
hours: { type: "number", description: "Hours of history to fetch (default 24, max 168)" }
|
|
129
|
-
affiliate_id: { type: "string", description: "Optional Pyrimid affiliate ID (af_xxxxx). Affiliate earns a commission from within the listed price — no extra cost to you." }
|
|
108
|
+
hours: { type: "number", description: "Hours of history to fetch (default 24, max 168)" }
|
|
130
109
|
}
|
|
131
110
|
}
|
|
132
111
|
},
|
|
@@ -142,8 +121,7 @@ const TOOLS = [
|
|
|
142
121
|
inputSchema: {
|
|
143
122
|
type: "object",
|
|
144
123
|
properties: {
|
|
145
|
-
symbol: { type: "string", description: "Symbol to evaluate: BTC, ETH, SOL, XRP, or ADA" }
|
|
146
|
-
affiliate_id: { type: "string", description: "Optional Pyrimid affiliate ID (af_xxxxx). Affiliate earns a commission from within the listed price — no extra cost to you." }
|
|
124
|
+
symbol: { type: "string", description: "Symbol to evaluate: BTC, ETH, SOL, XRP, or ADA" }
|
|
147
125
|
},
|
|
148
126
|
required: ["symbol"]
|
|
149
127
|
}
|
|
@@ -160,8 +138,7 @@ const TOOLS = [
|
|
|
160
138
|
inputSchema: {
|
|
161
139
|
type: "object",
|
|
162
140
|
properties: {
|
|
163
|
-
symbol: { type: "string", description: "Symbol to check: BTC, ETH, SOL, XRP, or ADA" }
|
|
164
|
-
affiliate_id: { type: "string", description: "Optional Pyrimid affiliate ID (af_xxxxx). Affiliate earns a commission from within the listed price — no extra cost to you." }
|
|
141
|
+
symbol: { type: "string", description: "Symbol to check: BTC, ETH, SOL, XRP, or ADA" }
|
|
165
142
|
},
|
|
166
143
|
required: ["symbol"]
|
|
167
144
|
}
|
|
@@ -179,8 +156,7 @@ const TOOLS = [
|
|
|
179
156
|
type: "object",
|
|
180
157
|
properties: {
|
|
181
158
|
decision_id: { type: "string", description: "UUID from a previous get_crypto_decision call" },
|
|
182
|
-
window: { type: "string", description: "Evaluation window: 1h, 4h, or 24h (default: 4h)" }
|
|
183
|
-
affiliate_id: { type: "string", description: "Optional Pyrimid affiliate ID (af_xxxxx). Affiliate earns a commission from within the listed price — no extra cost to you." }
|
|
159
|
+
window: { type: "string", description: "Evaluation window: 1h, 4h, or 24h (default: 4h)" }
|
|
184
160
|
},
|
|
185
161
|
required: ["decision_id"]
|
|
186
162
|
}
|
|
@@ -254,8 +230,7 @@ const TOOLS = [
|
|
|
254
230
|
inputSchema: {
|
|
255
231
|
type: "object",
|
|
256
232
|
properties: {
|
|
257
|
-
symbol: { type: "string", description: "Symbol: BTC, ETH, SOL, XRP, ADA (default: BTC)" }
|
|
258
|
-
affiliate_id: { type: "string", description: "Optional Pyrimid affiliate ID (af_xxxxx). Affiliate earns a commission from within the listed price — no extra cost to you." }
|
|
233
|
+
symbol: { type: "string", description: "Symbol: BTC, ETH, SOL, XRP, ADA (default: BTC)" }
|
|
259
234
|
}
|
|
260
235
|
}
|
|
261
236
|
},
|
|
@@ -299,213 +274,129 @@ function buildHttpClient() {
|
|
|
299
274
|
if (!key) throw new Error("WALLET_PRIVATE_KEY required — set a Base wallet private key with USDC funded");
|
|
300
275
|
const pk = key.startsWith("0x") ? key : "0x" + key;
|
|
301
276
|
const account = privateKeyToAccount(pk);
|
|
302
|
-
const coreClient = new x402Client().register("eip155:*", new ExactEvmScheme(toClientEvmSigner(account)));
|
|
303
|
-
return
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
307
|
-
|
|
308
|
-
async function createChainTimedPaymentPayload(httpClient, paymentRequired) {
|
|
309
|
-
try {
|
|
310
|
-
const publicClient = createPublicClient({ chain: base, transport: http(BASE_RPC_URL) });
|
|
311
|
-
const block = await publicClient.getBlock();
|
|
312
|
-
const chainNow = Number(block.timestamp);
|
|
313
|
-
const originalNow = Date.now;
|
|
314
|
-
const localNow = Math.floor(originalNow() / 1000);
|
|
315
|
-
const timeout = Number(paymentRequired.accepts?.[0]?.maxTimeoutSeconds || 300);
|
|
316
|
-
const lowerBound = localNow + 30 - timeout;
|
|
317
|
-
const upperBound = chainNow + 600;
|
|
318
|
-
const signingNow = Math.min(Math.max(chainNow, lowerBound), upperBound);
|
|
319
|
-
// x402 derives EIP-3009 validity windows from Date.now; choose a timestamp valid for both Base block time and facilitator wall-clock checks.
|
|
320
|
-
Date.now = () => signingNow * 1000;
|
|
321
|
-
try {
|
|
322
|
-
return await httpClient.createPaymentPayload(paymentRequired);
|
|
323
|
-
} finally {
|
|
324
|
-
Date.now = originalNow;
|
|
325
|
-
}
|
|
326
|
-
} catch (_) {
|
|
327
|
-
return httpClient.createPaymentPayload(paymentRequired);
|
|
328
|
-
}
|
|
277
|
+
const coreClient = new x402Client().register("eip155:*", new ExactEvmScheme(toClientEvmSigner(account))).registerPolicy(guard.policy);
|
|
278
|
+
return new x402HTTPClient(coreClient);
|
|
329
279
|
}
|
|
330
280
|
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (!
|
|
336
|
-
|
|
337
|
-
const transport = http(BASE_RPC_URL);
|
|
338
|
-
const publicClient = createPublicClient({ chain: base, transport });
|
|
339
|
-
const walletClient = createWalletClient({ account, chain: base, transport });
|
|
340
|
-
|
|
341
|
-
const approveHash = await walletClient.writeContract({
|
|
342
|
-
address: USDC_ADDRESS, abi: USDC_ABI,
|
|
343
|
-
functionName: "approve", args: [PYRIMID_ROUTER, product.priceUsdc],
|
|
344
|
-
});
|
|
345
|
-
await publicClient.waitForTransactionReceipt({ hash: approveHash });
|
|
346
|
-
await sleep(3000);
|
|
347
|
-
|
|
348
|
-
const routeHash = await walletClient.writeContract({
|
|
349
|
-
address: PYRIMID_ROUTER, abi: ROUTER_ABI,
|
|
350
|
-
functionName: "routePayment",
|
|
351
|
-
args: [PYRIMID_VENDOR_ID, product.productId, "0x00000000000000000000000000000000", account.address, product.priceUsdc],
|
|
352
|
-
});
|
|
353
|
-
await publicClient.waitForTransactionReceipt({ hash: routeHash });
|
|
354
|
-
await sleep(3000);
|
|
355
|
-
|
|
356
|
-
const paidRes = await fetch(url, { headers: { "X-Affiliate-ID": affiliateId, "X-Payment": routeHash } });
|
|
357
|
-
if (!paidRes.ok) {
|
|
358
|
-
const err = await paidRes.text().catch(() => paidRes.statusText);
|
|
359
|
-
throw new Error(`Pyrimid retry failed: ${paidRes.status} ${err.slice(0, 200)}`);
|
|
360
|
-
}
|
|
361
|
-
return paidRes.json();
|
|
281
|
+
// Argument validation — runs before any network call or wallet access.
|
|
282
|
+
const SYMBOLS = ["BTC", "ETH", "SOL", "XRP", "ADA"];
|
|
283
|
+
function str(v, name, { max = 200, pattern } = {}) {
|
|
284
|
+
if (typeof v !== "string" || v.length === 0 || v.length > max) throw new Error(`${name} must be a non-empty string of at most ${max} characters`);
|
|
285
|
+
if (pattern && !pattern.test(v)) throw new Error(`${name} has an invalid format`);
|
|
286
|
+
return v;
|
|
362
287
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
.filter(([, v]) => v !== undefined && v !== null && v !== "")
|
|
367
|
-
.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`);
|
|
368
|
-
return parts.length ? `?${parts.join("&")}` : "";
|
|
288
|
+
function num(v, name, min, max) {
|
|
289
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max) throw new Error(`${name} must be a number between ${min} and ${max}`);
|
|
290
|
+
return v;
|
|
369
291
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const method = opts.method || "GET";
|
|
377
|
-
const body = opts.body;
|
|
378
|
-
const bodyHeaders = body ? { "Content-Type": "application/json" } : {};
|
|
379
|
-
const bodyInit = body ? { body: JSON.stringify(body) } : {};
|
|
380
|
-
|
|
381
|
-
// Use Pyrimid affiliate flow when affiliate_id present and product is registered.
|
|
382
|
-
// Fall back to direct x402 if the affiliate route is unavailable or not yet cataloged.
|
|
383
|
-
if (method === "GET" && affiliateId && pyrimidProducts[pathname]) {
|
|
384
|
-
try {
|
|
385
|
-
return await callPyrimid(account, path, affiliateId, baseUrl, pyrimidProducts);
|
|
386
|
-
} catch (_) {
|
|
387
|
-
affiliateId = null;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Standard x402 EIP-3009 flow
|
|
392
|
-
const url = baseUrl + path;
|
|
393
|
-
const extraHeaders = affiliateId ? { "X-Affiliate-ID": affiliateId } : {};
|
|
394
|
-
const res = await fetch(url, { method, headers: { ...bodyHeaders, ...extraHeaders }, ...bodyInit });
|
|
395
|
-
|
|
396
|
-
if (res.status === 402) {
|
|
397
|
-
let body;
|
|
398
|
-
try { body = await res.clone().json(); } catch (_) {}
|
|
399
|
-
const paymentRequired = httpClient.getPaymentRequiredResponse(
|
|
400
|
-
(name) => res.headers.get(name), body
|
|
401
|
-
);
|
|
402
|
-
const paymentPayload = await createChainTimedPaymentPayload(httpClient, paymentRequired);
|
|
403
|
-
const paidRes = await fetch(url, {
|
|
404
|
-
method,
|
|
405
|
-
headers: { ...bodyHeaders, ...httpClient.encodePaymentSignatureHeader(paymentPayload), ...extraHeaders },
|
|
406
|
-
...bodyInit,
|
|
407
|
-
});
|
|
408
|
-
if (!paidRes.ok) {
|
|
409
|
-
const errBody = await paidRes.text().catch(() => paidRes.statusText);
|
|
410
|
-
throw new Error(`HTTP ${paidRes.status}: ${errBody.slice(0, 200)}`);
|
|
411
|
-
}
|
|
412
|
-
const data = await paidRes.json();
|
|
413
|
-
try {
|
|
414
|
-
const settleResponse = httpClient.getPaymentSettleResponse((name) => paidRes.headers.get(name));
|
|
415
|
-
if (settleResponse && data && typeof data === "object" && !Array.isArray(data)) {
|
|
416
|
-
return { ...data, _payment: settleResponse };
|
|
417
|
-
}
|
|
418
|
-
} catch (_) {}
|
|
419
|
-
return data;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (!res.ok) {
|
|
423
|
-
const errBody = await res.text().catch(() => res.statusText);
|
|
424
|
-
throw new Error(`HTTP ${res.status}: ${errBody.slice(0, 200)}`);
|
|
425
|
-
}
|
|
426
|
-
return res.json();
|
|
292
|
+
function optNum(v, name, min, max) { return v === undefined || v === null ? undefined : num(v, name, min, max); }
|
|
293
|
+
function symbol(v, dflt) {
|
|
294
|
+
if (v === undefined || v === null) return dflt;
|
|
295
|
+
const up = str(v, "symbol", { max: 10 }).toUpperCase();
|
|
296
|
+
if (!SYMBOLS.includes(up)) throw new Error(`symbol must be one of ${SYMBOLS.join(", ")}`);
|
|
297
|
+
return up;
|
|
427
298
|
}
|
|
428
299
|
|
|
429
300
|
async function main() {
|
|
430
|
-
let
|
|
431
|
-
function
|
|
432
|
-
if (!
|
|
433
|
-
return
|
|
301
|
+
let httpClient;
|
|
302
|
+
function getHttpClient() {
|
|
303
|
+
if (!httpClient) httpClient = buildHttpClient();
|
|
304
|
+
return httpClient;
|
|
434
305
|
}
|
|
435
306
|
|
|
436
307
|
const server = new Server(
|
|
437
|
-
{ name: "coinopai-mcp", version: "
|
|
308
|
+
{ name: "coinopai-mcp", version: require("./package.json").version },
|
|
438
309
|
{ capabilities: { tools: {} } }
|
|
439
310
|
);
|
|
440
311
|
|
|
441
312
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
442
313
|
|
|
443
314
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
444
|
-
const { name
|
|
315
|
+
const { name } = req.params;
|
|
316
|
+
const args = req.params.arguments || {};
|
|
445
317
|
try {
|
|
446
|
-
// Affiliate ID: tool arg takes precedence, then env fallback, then none
|
|
447
|
-
const affiliateId = args.affiliate_id || process.env.PYRIMID_AFFILIATE_ID || null;
|
|
448
|
-
// list_tools is free: plain fetch, no wallet, never touches callPaid
|
|
449
|
-
const paymentContext = name === "list_tools" ? null : getPaymentContext();
|
|
450
318
|
let data;
|
|
451
319
|
switch (name) {
|
|
452
320
|
case "list_tools": {
|
|
453
|
-
|
|
321
|
+
// Free: plain bounded fetch, no wallet
|
|
322
|
+
const r = await guard.fetchBounded("/menu");
|
|
454
323
|
if (!r.ok) throw new Error(`/menu returned ${r.status}`);
|
|
455
|
-
data =
|
|
324
|
+
try { data = JSON.parse(r.text); } catch (e) { throw new Error("/menu returned non-JSON: " + e.message); }
|
|
456
325
|
break;
|
|
457
326
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
data = await callPaid(
|
|
327
|
+
case "search_agent_automations": {
|
|
328
|
+
const query = { q: str(args.query, "query", { max: 200 }), limit: args.limit === undefined ? 20 : num(args.limit, "limit", 1, 50) };
|
|
329
|
+
data = await guard.callPaid(getHttpClient(), "/api/search", { query });
|
|
461
330
|
break;
|
|
331
|
+
}
|
|
462
332
|
case "get_agent_automation":
|
|
463
|
-
data = await callPaid(
|
|
333
|
+
data = await guard.callPaid(getHttpClient(), `/api/automation/${encodeURIComponent(str(args.slug, "slug", { max: 200, pattern: /^[A-Za-z0-9._-]+$/ }))}`);
|
|
464
334
|
break;
|
|
465
335
|
case "list_automation_categories":
|
|
466
|
-
data = await callPaid(
|
|
336
|
+
data = await guard.callPaid(getHttpClient(), "/api/categories");
|
|
467
337
|
break;
|
|
468
338
|
case "get_crypto_risk":
|
|
469
|
-
data = await callPaid(
|
|
339
|
+
data = await guard.callPaid(getHttpClient(), "/api/kronos/risk");
|
|
470
340
|
break;
|
|
471
|
-
// High-value endpoints — affiliate routing enabled
|
|
472
341
|
case "get_crypto_signals":
|
|
473
|
-
data = await callPaid(
|
|
342
|
+
data = await guard.callPaid(getHttpClient(), "/api/kronos/signals");
|
|
474
343
|
break;
|
|
475
344
|
case "get_crypto_signal_history":
|
|
476
|
-
data = await callPaid(
|
|
345
|
+
data = await guard.callPaid(getHttpClient(), "/api/kronos/history", { query: { hours: args.hours === undefined ? 24 : num(args.hours, "hours", 1, 168) } });
|
|
477
346
|
break;
|
|
478
347
|
case "get_crypto_decision":
|
|
479
|
-
data = await callPaid(
|
|
348
|
+
data = await guard.callPaid(getHttpClient(), "/api/kronos/decision", { query: { symbol: symbol(args.symbol, "BTC") } });
|
|
480
349
|
break;
|
|
481
350
|
case "check_trade_preflight":
|
|
482
|
-
data = await callPaid(
|
|
351
|
+
data = await guard.callPaid(getHttpClient(), "/api/kronos/preflight", { query: { symbol: symbol(args.symbol, "BTC") } });
|
|
483
352
|
break;
|
|
484
|
-
case "audit_trade_decision":
|
|
485
|
-
|
|
353
|
+
case "audit_trade_decision": {
|
|
354
|
+
const decisionId = str(args.decision_id, "decision_id", { max: 64, pattern: /^[A-Za-z0-9-]+$/ });
|
|
355
|
+
const windowArg = args.window === undefined ? "4h" : args.window;
|
|
356
|
+
if (!["1h", "4h", "24h"].includes(windowArg)) throw new Error("window must be one of 1h, 4h, 24h");
|
|
357
|
+
data = await guard.callPaid(getHttpClient(), "/api/kronos/audit", { query: { decision_id: decisionId, window: windowArg } });
|
|
486
358
|
break;
|
|
359
|
+
}
|
|
487
360
|
case "get_crypto_forecast":
|
|
488
|
-
data = await callPaid(
|
|
361
|
+
data = await guard.callPaid(getHttpClient(), "/api/kronos/forecast", { query: { symbol: symbol(args.symbol, "BTC") } });
|
|
489
362
|
break;
|
|
490
363
|
case "get_futures_decision":
|
|
491
|
-
data = await callPaid(
|
|
364
|
+
data = await guard.callPaid(getHttpClient(), "/api/kronos/futures/decision", { query: {
|
|
365
|
+
symbol: symbol(args.symbol, "BTC"),
|
|
366
|
+
equity: optNum(args.equity, "equity", 0, 1e12),
|
|
367
|
+
max_loss_pct: optNum(args.max_loss_pct, "max_loss_pct", 0, 1),
|
|
368
|
+
max_leverage: optNum(args.max_leverage, "max_leverage", 1, 5),
|
|
369
|
+
} });
|
|
492
370
|
break;
|
|
493
371
|
case "get_perp_funding":
|
|
494
|
-
data = await callPaid(
|
|
372
|
+
data = await guard.callPaid(getHttpClient(), "/api/kronos/futures/funding", { query: { symbol: args.symbol === undefined ? undefined : symbol(args.symbol) } });
|
|
495
373
|
break;
|
|
496
|
-
case "check_futures_risk":
|
|
497
|
-
|
|
374
|
+
case "check_futures_risk": {
|
|
375
|
+
const side = typeof args.side === "string" ? args.side.toUpperCase() : "";
|
|
376
|
+
if (side !== "LONG" && side !== "SHORT") throw new Error("side must be LONG or SHORT");
|
|
377
|
+
data = await guard.callPaid(getHttpClient(), "/api/kronos/futures/risk", { query: {
|
|
378
|
+
symbol: symbol(args.symbol, "BTC"),
|
|
379
|
+
side,
|
|
380
|
+
leverage: num(args.leverage, "leverage", 1, 125),
|
|
381
|
+
entry: optNum(args.entry, "entry", 0, 1e12),
|
|
382
|
+
notional: optNum(args.notional, "notional", 0, 1e12),
|
|
383
|
+
horizon_hours: optNum(args.horizon_hours, "horizon_hours", 0, 1e5),
|
|
384
|
+
} });
|
|
498
385
|
break;
|
|
499
|
-
|
|
500
|
-
|
|
386
|
+
}
|
|
387
|
+
case "review_signal_anomaly": {
|
|
388
|
+
if (!args.features || typeof args.features !== "object" || Array.isArray(args.features)) throw new Error("features must be an object of numeric feature values");
|
|
389
|
+
if (JSON.stringify(args.features).length > 4000) throw new Error("features is too large");
|
|
390
|
+
data = await guard.callPaid(getHttpClient(), "/api/anomaly", {
|
|
501
391
|
method: "POST",
|
|
502
392
|
body: {
|
|
503
|
-
symbol: args.symbol
|
|
504
|
-
window: args.window
|
|
505
|
-
features: args.features
|
|
393
|
+
symbol: str(args.symbol, "symbol", { max: 20, pattern: /^[A-Za-z0-9._-]+$/ }),
|
|
394
|
+
window: args.window === undefined ? "24h" : str(args.window, "window", { max: 20, pattern: /^[A-Za-z0-9]+$/ }),
|
|
395
|
+
features: args.features,
|
|
506
396
|
},
|
|
507
397
|
});
|
|
508
398
|
break;
|
|
399
|
+
}
|
|
509
400
|
default:
|
|
510
401
|
throw new Error("Unknown tool: " + name);
|
|
511
402
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coinopai-mcp",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"mcpName": "io.github.forgemeshlabs/coinopai-mcp",
|
|
5
5
|
"description": "Auditable market intelligence MCP with x402 payments, calibrated forecast ranges, anomaly review, risk context, and outcome audits",
|
|
6
6
|
"main": "index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"index.js",
|
|
12
12
|
"ads.js",
|
|
13
|
+
"x402-guard.js",
|
|
13
14
|
"README.md",
|
|
14
15
|
"server.json"
|
|
15
16
|
],
|
|
@@ -24,7 +25,6 @@
|
|
|
24
25
|
"model-context-protocol",
|
|
25
26
|
"x402",
|
|
26
27
|
"agent-commerce",
|
|
27
|
-
"affiliate-routing",
|
|
28
28
|
"monetization",
|
|
29
29
|
"forgemesh",
|
|
30
30
|
"crypto",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"url": "https://github.com/forgemeshlabs/coinopai-mcp/issues"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
52
52
|
"@x402/core": "2.11.0",
|
|
53
53
|
"@x402/evm": "2.11.0",
|
|
54
54
|
"lulu-ads": "^0.9.19",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/forgemeshlabs/coinopai-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.2.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "coinopai-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.2.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
]
|
|
36
36
|
}
|
|
37
37
|
]
|
|
38
|
-
}
|
|
38
|
+
}
|
package/x402-guard.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* x402-guard — client-side payment and HTTP guard for ForgeMesh MCP servers (vendored copy per repo).
|
|
4
|
+
*
|
|
5
|
+
* const { createGuard } = require("./x402-guard");
|
|
6
|
+
* const guard = createGuard({
|
|
7
|
+
* baseUrl: "https://x402.coinopai.com", // the ONLY origin this server talks to
|
|
8
|
+
* payTo: ["0x1304EC1A8945365e43A5c18a734065f107B417cA"], // wallets a 402 may name
|
|
9
|
+
* maxPriceUsd: 1.00, // per-call ceiling (env X402_MAX_PRICE_USD overrides, lower only)
|
|
10
|
+
* sessionBudgetUsd: 10.00, // cumulative ceiling per process (env X402_SESSION_BUDGET_USD)
|
|
11
|
+
* });
|
|
12
|
+
* const client = new x402Client().register("eip155:*", new ExactEvmScheme(signer)).registerPolicy(guard.policy);
|
|
13
|
+
* const data = await guard.callPaid(httpClient, "/api/route", { method: "POST", body: {...} });
|
|
14
|
+
*
|
|
15
|
+
* What it enforces, before any signature is produced:
|
|
16
|
+
* network = eip155:8453 (Base mainnet), asset = USDC on Base, payTo in the allowlist,
|
|
17
|
+
* amount <= maxPriceUsd, running total <= sessionBudgetUsd.
|
|
18
|
+
* What it enforces on every HTTP call: same origin as baseUrl, https, no redirects,
|
|
19
|
+
* REQUEST_TIMEOUT_MS, MAX_RESPONSE_BYTES. No global monkeypatching anywhere.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const BASE_MAINNET = "eip155:8453";
|
|
23
|
+
const USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
24
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
25
|
+
const MAX_RESPONSE_BYTES = 2_000_000;
|
|
26
|
+
|
|
27
|
+
function usdFromRequirement(r) {
|
|
28
|
+
const raw = r.amount ?? r.maxAmountRequired ?? r.value;
|
|
29
|
+
if (raw === undefined || raw === null) return NaN;
|
|
30
|
+
const s = String(raw);
|
|
31
|
+
if (s.startsWith("$")) return Number(s.slice(1));
|
|
32
|
+
return Number(BigInt(s)) / 1e6; // USDC atomic units
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function lowerCap(defaultUsd, envName) {
|
|
36
|
+
const env = Number(process.env[envName]);
|
|
37
|
+
return Number.isFinite(env) && env > 0 ? Math.min(env, defaultUsd) : defaultUsd;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createGuard(opts) {
|
|
41
|
+
if (!opts || !opts.baseUrl) throw new Error("x402-guard: baseUrl is required");
|
|
42
|
+
const base = new URL(opts.baseUrl);
|
|
43
|
+
if (base.protocol !== "https:") throw new Error("x402-guard: baseUrl must be https");
|
|
44
|
+
const origin = base.origin;
|
|
45
|
+
// An empty allowlist means "this server never signs payments": the policy refuses every 402 and
|
|
46
|
+
// only fetchBounded is useful. Pass payTo: [] explicitly for free-only servers.
|
|
47
|
+
if (!Array.isArray(opts.payTo)) throw new Error("x402-guard: payTo allowlist is required (use [] for a server that never signs)");
|
|
48
|
+
const payTo = new Set(opts.payTo.map((a) => String(a).toLowerCase()));
|
|
49
|
+
const maxPriceUsd = lowerCap(opts.maxPriceUsd ?? 1.0, "X402_MAX_PRICE_USD");
|
|
50
|
+
const sessionBudgetUsd = lowerCap(opts.sessionBudgetUsd ?? 10.0, "X402_SESSION_BUDGET_USD");
|
|
51
|
+
let spentUsd = 0; // reserved at signing time, never released: a signed authorization can be settled even if our retry fails
|
|
52
|
+
|
|
53
|
+
function policy(_version, requirements) {
|
|
54
|
+
if (payTo.size === 0) throw new Error("x402-guard refused to sign: this server has no payee allowlist and never signs payments");
|
|
55
|
+
const kept = requirements.filter((r) => {
|
|
56
|
+
if (r.network !== BASE_MAINNET) return false;
|
|
57
|
+
if (String(r.asset || "").toLowerCase() !== USDC_BASE) return false;
|
|
58
|
+
if (!payTo.has(String(r.payTo || "").toLowerCase())) return false;
|
|
59
|
+
const usd = usdFromRequirement(r);
|
|
60
|
+
if (!Number.isFinite(usd) || usd <= 0 || usd > maxPriceUsd) return false;
|
|
61
|
+
if (spentUsd + usd > sessionBudgetUsd) return false;
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
if (kept.length === 0) {
|
|
65
|
+
const seen = requirements.map((r) => `${r.network} ${r.payTo} $${usdFromRequirement(r)}`).join("; ");
|
|
66
|
+
throw new Error(`x402-guard refused to sign: no payment option passed the guard (network must be ${BASE_MAINNET}, asset USDC, payTo in allowlist, price <= $${maxPriceUsd}, session total <= $${sessionBudgetUsd}). Offered: ${seen}`);
|
|
67
|
+
}
|
|
68
|
+
// Hand the selector exactly one option so the amount we reserve is the amount that gets signed.
|
|
69
|
+
const chosen = kept.reduce((a, b) => (usdFromRequirement(b) < usdFromRequirement(a) ? b : a));
|
|
70
|
+
spentUsd += usdFromRequirement(chosen);
|
|
71
|
+
return [chosen];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function fetchBounded(url, init = {}) {
|
|
75
|
+
const target = new URL(String(url), origin);
|
|
76
|
+
if (target.origin !== origin) throw new Error(`x402-guard: refusing request to ${target.origin}; this server only talks to ${origin}`);
|
|
77
|
+
const ctrl = new AbortController();
|
|
78
|
+
const timer = setTimeout(() => ctrl.abort(), REQUEST_TIMEOUT_MS);
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(target.toString(), { ...init, signal: ctrl.signal, redirect: "error" });
|
|
81
|
+
const declared = Number(res.headers.get("content-length") || 0);
|
|
82
|
+
if (declared > MAX_RESPONSE_BYTES) { ctrl.abort(); throw new Error(`x402-guard: response too large (${declared} bytes)`); }
|
|
83
|
+
const chunks = [];
|
|
84
|
+
let received = 0;
|
|
85
|
+
if (res.body) {
|
|
86
|
+
const reader = res.body.getReader();
|
|
87
|
+
for (;;) {
|
|
88
|
+
const { done, value } = await reader.read();
|
|
89
|
+
if (done) break;
|
|
90
|
+
received += value.byteLength;
|
|
91
|
+
if (received > MAX_RESPONSE_BYTES) { ctrl.abort(); throw new Error("x402-guard: response too large"); }
|
|
92
|
+
chunks.push(value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const bytes = Buffer.concat(chunks);
|
|
96
|
+
return { status: res.status, ok: res.ok, headers: res.headers, bytes, text: bytes.toString("utf8") };
|
|
97
|
+
} finally {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseJson(res) {
|
|
103
|
+
try { return JSON.parse(res.text); } catch { throw new Error(`HTTP ${res.status}: non-JSON response`); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// GET or POST a paid route: unpaid request → 402 → guarded signature → retry. Returns parsed JSON;
|
|
107
|
+
// on a paid response attaches the settlement receipt as `_payment` when the body is an object.
|
|
108
|
+
async function callPaid(httpClient, path, { method = "GET", body, query, headers = {} } = {}) {
|
|
109
|
+
const url = new URL(path, origin);
|
|
110
|
+
if (query) for (const [k, v] of Object.entries(query)) if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
|
|
111
|
+
const init = { method, headers: { ...headers, ...(body ? { "content-type": "application/json" } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}) };
|
|
112
|
+
const res = await fetchBounded(url, init);
|
|
113
|
+
if (res.status !== 402) {
|
|
114
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.text.slice(0, 200)}`);
|
|
115
|
+
return parseJson(res);
|
|
116
|
+
}
|
|
117
|
+
let challenge;
|
|
118
|
+
try { challenge = JSON.parse(res.text); } catch { challenge = undefined; }
|
|
119
|
+
const paymentRequired = httpClient.getPaymentRequiredResponse((n) => res.headers.get(n), challenge);
|
|
120
|
+
const payload = await httpClient.createPaymentPayload(paymentRequired); // policy runs (and reserves budget) inside
|
|
121
|
+
const paid = await fetchBounded(url, { ...init, headers: { ...init.headers, ...httpClient.encodePaymentSignatureHeader(payload) } });
|
|
122
|
+
if (!paid.ok) throw new Error(`Payment failed — HTTP ${paid.status}: ${paid.text.slice(0, 200)}`);
|
|
123
|
+
let settle = null;
|
|
124
|
+
try { settle = httpClient.getPaymentSettleResponse((n) => paid.headers.get(n)) || null; } catch { settle = null; }
|
|
125
|
+
const contentType = paid.headers.get("content-type") || "";
|
|
126
|
+
// Non-JSON paid responses (audio, images) come back as raw bytes, never decoded as text.
|
|
127
|
+
if (!contentType.includes("json")) return { _binary: true, content_type: contentType, bytes: paid.bytes, _payment: settle };
|
|
128
|
+
const data = parseJson(paid);
|
|
129
|
+
if (settle && data && typeof data === "object" && !Array.isArray(data)) return { ...data, _payment: settle };
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
policy,
|
|
135
|
+
fetchBounded,
|
|
136
|
+
callPaid,
|
|
137
|
+
limits: () => ({ maxPriceUsd, sessionBudgetUsd, spentUsd, origin, payTo: [...payTo] }),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { createGuard, REQUEST_TIMEOUT_MS, MAX_RESPONSE_BYTES, BASE_MAINNET, USDC_BASE };
|