diff --git a/README.md b/README.md index 6b8353c..0eb8f22 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,11 @@ Connect it to Claude Desktop or Claude Code and trade with natural language. | `account_positions` | Get open perpetual positions with unrealized P&L. | | `token_metadata` | Look up symbol, decimals, and type for any denom. | +### x402 Payments +| Tool | Description | +|---|---| +| `x402_fetch` | Fetch an x402-gated API endpoint, automatically signing and paying the required USDC quote using the Injective EVM wallet if a 402 is returned. | + ### Native USDC and CCTP | Tool | Description | |---|---| diff --git a/package-lock.json b/package-lock.json index 9a81ea5..b503975 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@injectivelabs/networks": "^1.14.27", "@injectivelabs/sdk-ts": "^1.14.27", "@injectivelabs/utils": "^1.14.27", + "@injectivelabs/x402": "^0.0.1", "@modelcontextprotocol/sdk": "^1.0.4", "decimal.js": "^10.4.3", "zod": "^3.22.0" @@ -750,6 +751,27 @@ "store2": "^2.14.4" } }, + "node_modules/@injectivelabs/x402": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@injectivelabs/x402/-/x402-0.0.1.tgz", + "integrity": "sha512-uHxTHz/bsX3kvIYPLXC/x0pZ6jcluRqowGLbXsEuFlVeG7rDNUXimrbNScnhRqZcPkMfjEHtMwiRuCOP6Kau4A==", + "license": "MIT", + "dependencies": { + "viem": "^2.39.3", + "zod": "^3.23.8" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "express": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", diff --git a/package.json b/package.json index 1d8c171..aafc7f3 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@injectivelabs/networks": "^1.14.27", "@injectivelabs/sdk-ts": "^1.14.27", "@injectivelabs/utils": "^1.14.27", + "@injectivelabs/x402": "^0.0.1", "@modelcontextprotocol/sdk": "^1.0.4", "decimal.js": "^10.4.3", "zod": "^3.22.0" diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 219052e..556cb2d 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -29,6 +29,7 @@ import { authz, TRADING_MSG_TYPES } from '../authz/index.js' import { usdc } from '../usdc/index.js' import { rfq } from '../rfq/index.js' import { frontendGuidanceTopics, guidance } from '../guidance/index.js' +import { createInjectiveClient as x402CreateClient, parsePaymentRequired } from '@injectivelabs/x402/client' const injAddress = z.string().regex(INJ_ADDRESS_RE, 'Must be a valid inj1... address (42 chars)') const numericString = z.string().regex(/^\d+(\.\d+)?$/, 'Must be a positive numeric string') @@ -987,6 +988,66 @@ server.tool( }, ) +// ─── x402 Payment Tools ──────────────────────────────────────────────────────── + +server.tool( + 'x402_fetch', + 'Fetch data from an x402-gated API endpoint. Automatically handles 402 Payment Required ' + + 'responses by signing a USDC payment using the Injective EVM wallet, submitting it to the facilitator, ' + + 'and retrying the request. IMPORTANT: Real on-chain payment with real funds.', + { + address: injAddress.describe('The inj1... address of your trading wallet.'), + password: z.string().describe('Keystore password to decrypt the private key for signing. SECURITY: Never log, store, or echo this. Use secret inputs only.'), + url: z.string().url().describe('The URL of the x402-gated API endpoint.'), + maxAmount: z.number().optional().describe('A safety limit on the maximum USDC amount you are willing to pay for this request.'), + }, + async ({ address, password, url, maxAmount }) => { + const privateKeyHex = wallets.unlock(address, password) + + if (maxAmount !== undefined) { + const preflight = await fetch(url) + if (preflight.status === 402) { + const authHeader = preflight.headers.get('WWW-Authenticate') || preflight.headers.get('402-Payment-Required') + if (authHeader) { + const required = parsePaymentRequired(authHeader) + if (required.accepts && required.accepts.length > 0) { + // USDC is 6 decimals. We compare decimal representations or convert string to number. + // Amount is usually a string representing the smallest unit (e.g. 500000 = 0.5 USDC). + const rawAmount = Number(required.accepts[0].amount) + const tokenDecimals = 6 // USDC + const usdAmount = rawAmount / Math.pow(10, tokenDecimals) + if (usdAmount > maxAmount) { + throw new Error(`Payment required (${usdAmount} USDC) exceeds your maxAmount safety limit of ${maxAmount} USDC.`) + } + } + } + } + } + + const x402Client = x402CreateClient({ privateKey: privateKeyHex as `0x${string}` }) + const response = await x402Client.fetch(url) + + const text = await response.text() + let data; + try { + data = JSON.parse(text) + } catch { + data = text + } + + return { + content: [{ + type: 'text', + text: JSON.stringify({ + status: response.status, + url: response.url, + data + }, null, 2), + }], + } + }, +) + // ─── Start ─────────────────────────────────────────────────────────────────── const transport = new StdioServerTransport()