@true402.dev/mcp-server 0.2.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 true402
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 ADDED
@@ -0,0 +1,72 @@
1
+ # @true402.dev/mcp-server
2
+
3
+ MCP server for the **[true402](https://true402.dev)** machine-native marketplace — give your agent pay-per-call access to AI inference and web tools over [x402](https://x402.org) (HTTP 402 micropayments in USDC on Base).
4
+
5
+ No accounts, no API keys. The agent's **wallet is its identity**: each paid tool returns an HTTP 402 challenge, the server signs an EIP-3009 USDC authorization, and the call settles on-chain. Configure a funded wallet to auto-pay, or run without one and the paid tools will surface the exact payment requirements instead of failing.
6
+
7
+ ## Tools
8
+
9
+ | Tool | Price | What it does |
10
+ |------|-------|--------------|
11
+ | `chat` | per-token + 3% | OpenAI-compatible LLM inference across many models |
12
+ | `list_models` | free | List available models + pricing |
13
+ | `seo_audit` | $0.02 | SEO + GEO (generative-engine-optimization) audit of a page → structured report |
14
+ | `web_extract` | $0.005 | Fetch a URL → clean text + markdown + links + metadata |
15
+ | `link_preview` | $0.003 | Fetch a URL → Open Graph / unfurl card |
16
+ | `robots_check` | $0.003 | A site's AI-crawler policy (GPTBot, ClaudeBot, Google-Extended, PerplexityBot, …) + sitemaps + llms.txt |
17
+ | `headers_check` | $0.003 | HTTP security-headers analysis (HSTS, CSP, …) + a 0–100 score |
18
+
19
+ Prices are illustrative; the live 402 challenge is authoritative.
20
+
21
+ ## Install
22
+
23
+ Requires Node.js ≥ 20. Runs over stdio — point any MCP client at it via `npx`.
24
+
25
+ ### Claude Desktop / Claude Code
26
+
27
+ Add to your MCP config (`claude_desktop_config.json`, or `.mcp.json` for Claude Code):
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "true402": {
33
+ "command": "npx",
34
+ "args": ["-y", "@true402.dev/mcp-server"],
35
+ "env": {
36
+ "WALLET_PRIVATE_KEY": "0xYOUR_FUNDED_BASE_WALLET_KEY"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### Cursor / Cline / other MCP clients
44
+
45
+ Same idea — command `npx`, args `["-y", "@true402.dev/mcp-server"]`, and the `WALLET_PRIVATE_KEY` env var.
46
+
47
+ ## Configuration
48
+
49
+ | Env var | Default | Description |
50
+ |---------|---------|-------------|
51
+ | `SERVER_URL` | `https://true402.dev/api` | true402 API base. Override to point at a self-hosted instance. |
52
+ | `WALLET_PRIVATE_KEY` | _(none)_ | A funded **Base** wallet private key used to sign x402 payments. Needs **USDC** (gas is sponsored by the facilitator — no ETH required). Without it, paid tools return the 402 requirements instead of paying. |
53
+
54
+ > **Security:** the key is read only from the environment and is never logged, echoed, or returned. Use a dedicated low-balance wallet — fund it with only what you intend to spend.
55
+
56
+ ## How payment works
57
+
58
+ 1. The tool calls the true402 endpoint with no payment.
59
+ 2. The server replies `402` with accepted payment options (USDC on Base).
60
+ 3. This MCP server signs an EIP-3009 `transferWithAuthorization` with your wallet.
61
+ 4. It retries with the signed `X-PAYMENT` header; the service verifies and responds.
62
+ 5. Settlement happens on-chain. Your wallet pays only USDC — the facilitator sponsors gas.
63
+
64
+ ## Links
65
+
66
+ - Marketplace: <https://true402.dev>
67
+ - Live tool catalog: <https://true402.dev/api/v1/services>
68
+ - x402 protocol: <https://x402.org>
69
+
70
+ ## License
71
+
72
+ MIT
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * true402 MCP Server
4
+ *
5
+ * Exposes the true402 machine-native marketplace as MCP tools for Claude and other
6
+ * MCP-compatible clients: LLM inference plus the paid web stalls (SEO/GEO audit, web
7
+ * extract, link preview, robots/AI-crawler check, security-headers check). Each paid
8
+ * tool is x402-gated (USDC on Base); set WALLET_PRIVATE_KEY to auto-pay. Uses stdio.
9
+ *
10
+ * Environment variables:
11
+ * SERVER_URL - true402 API base (default: https://true402.dev/api)
12
+ * WALLET_PRIVATE_KEY - funded Base wallet key for x402 payment signing (optional;
13
+ * without it, paid tools surface the 402 requirements instead)
14
+ */
15
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * true402 MCP Server
4
+ *
5
+ * Exposes the true402 machine-native marketplace as MCP tools for Claude and other
6
+ * MCP-compatible clients: LLM inference plus the paid web stalls (SEO/GEO audit, web
7
+ * extract, link preview, robots/AI-crawler check, security-headers check). Each paid
8
+ * tool is x402-gated (USDC on Base); set WALLET_PRIVATE_KEY to auto-pay. Uses stdio.
9
+ *
10
+ * Environment variables:
11
+ * SERVER_URL - true402 API base (default: https://true402.dev/api)
12
+ * WALLET_PRIVATE_KEY - funded Base wallet key for x402 payment signing (optional;
13
+ * without it, paid tools surface the 402 requirements instead)
14
+ */
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { registerModelsTool } from "./tools/models.js";
18
+ import { registerChatTool } from "./tools/chat.js";
19
+ import { registerStallTools } from "./tools/stalls.js";
20
+ const SERVER_URL = process.env.SERVER_URL ?? "https://true402.dev/api";
21
+ const WALLET_PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY;
22
+ const server = new McpServer({
23
+ name: "true402",
24
+ version: "0.2.0",
25
+ });
26
+ // Register tools
27
+ registerModelsTool(server, SERVER_URL);
28
+ registerChatTool(server, SERVER_URL, WALLET_PRIVATE_KEY);
29
+ // Non-LLM paid stalls: seo_audit, web_extract, link_preview,
30
+ // robots_check, headers_check (all x402-gated, USDC on Base).
31
+ registerStallTools(server, SERVER_URL, WALLET_PRIVATE_KEY);
32
+ // Start server with stdio transport
33
+ async function main() {
34
+ const transport = new StdioServerTransport();
35
+ await server.connect(transport);
36
+ }
37
+ main().catch((error) => {
38
+ console.error("Fatal error:", error);
39
+ process.exit(1);
40
+ });
@@ -0,0 +1,8 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Register the chat tool.
4
+ * Calls POST /v1/chat/completions on the true402 endpoint.
5
+ * Handles the x402 payment flow (402 -> sign -> retry with X-Payment header)
6
+ * via the shared {@link payAndFetch} helper.
7
+ */
8
+ export declare function registerChatTool(server: McpServer, baseUrl: string, walletPrivateKey: string | undefined): void;
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { payAndFetch } from "../x402-pay.js";
3
+ /**
4
+ * Register the chat tool.
5
+ * Calls POST /v1/chat/completions on the true402 endpoint.
6
+ * Handles the x402 payment flow (402 -> sign -> retry with X-Payment header)
7
+ * via the shared {@link payAndFetch} helper.
8
+ */
9
+ export function registerChatTool(server, baseUrl, walletPrivateKey) {
10
+ server.tool("chat", "Send a chat completion request to an LLM via true402 (PAID x402 service, USDC on Base). Requires a funded wallet (WALLET_PRIVATE_KEY) on the MCP server.", {
11
+ model: z.string().describe("Model ID (e.g. gpt-4o, claude-3-5-sonnet)"),
12
+ messages: z
13
+ .array(z.object({
14
+ role: z.string().describe("Message role: system, user, or assistant"),
15
+ content: z.string().describe("Message content"),
16
+ }))
17
+ .describe("Chat messages"),
18
+ max_tokens: z
19
+ .number()
20
+ .optional()
21
+ .describe("Maximum tokens to generate"),
22
+ }, async ({ model, messages, max_tokens }) => {
23
+ const body = { model, messages };
24
+ if (max_tokens !== undefined) {
25
+ body.max_tokens = max_tokens;
26
+ }
27
+ const result = await payAndFetch(baseUrl, "/v1/chat/completions", body, walletPrivateKey);
28
+ if (!result.ok) {
29
+ return {
30
+ content: [{ type: "text", text: result.message }],
31
+ isError: true,
32
+ };
33
+ }
34
+ return {
35
+ content: [
36
+ {
37
+ type: "text",
38
+ text: JSON.stringify(result.data, null, 2),
39
+ },
40
+ ],
41
+ };
42
+ });
43
+ }
@@ -0,0 +1,6 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Register the list_models tool.
4
+ * Calls GET /v1/models on the true402 endpoint.
5
+ */
6
+ export declare function registerModelsTool(server: McpServer, baseUrl: string): void;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Register the list_models tool.
3
+ * Calls GET /v1/models on the true402 endpoint.
4
+ */
5
+ export function registerModelsTool(server, baseUrl) {
6
+ server.tool("list_models", "List available LLM models with pricing from true402", {}, async () => {
7
+ const response = await fetch(`${baseUrl}/v1/models`);
8
+ if (!response.ok) {
9
+ const text = await response.text();
10
+ return {
11
+ content: [
12
+ {
13
+ type: "text",
14
+ text: `Error fetching models: ${response.status} ${text}`,
15
+ },
16
+ ],
17
+ isError: true,
18
+ };
19
+ }
20
+ const data = await response.json();
21
+ return {
22
+ content: [
23
+ {
24
+ type: "text",
25
+ text: JSON.stringify(data, null, 2),
26
+ },
27
+ ],
28
+ };
29
+ });
30
+ }
@@ -0,0 +1,6 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * Register all five non-LLM paid stalls (seo_audit, web_extract, link_preview,
4
+ * robots_check, headers_check) on the given MCP server.
5
+ */
6
+ export declare function registerStallTools(server: McpServer, baseUrl: string, walletPrivateKey: string | undefined): void;
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ import { payAndFetch } from "../x402-pay.js";
3
+ /**
4
+ * Register a single paid stall as an MCP tool. The handler runs the shared
5
+ * x402 pay-and-retry flow and maps the result into the MCP content shape.
6
+ */
7
+ function registerStall(server, baseUrl, walletPrivateKey, spec) {
8
+ const handler = async (args) => {
9
+ const body = spec.buildBody(args);
10
+ const result = await payAndFetch(baseUrl, spec.path, body, walletPrivateKey);
11
+ if (!result.ok) {
12
+ return {
13
+ content: [{ type: "text", text: result.message }],
14
+ isError: true,
15
+ };
16
+ }
17
+ return {
18
+ content: [
19
+ {
20
+ type: "text",
21
+ text: JSON.stringify(result.data, null, 2),
22
+ },
23
+ ],
24
+ };
25
+ };
26
+ server.tool(spec.toolName, spec.description, spec.inputSchema, handler);
27
+ }
28
+ // A required http/https page URL — shared by every stall.
29
+ const urlField = z
30
+ .string()
31
+ .url()
32
+ .describe("Absolute http(s) URL of the page to process");
33
+ /**
34
+ * Register all five non-LLM paid stalls (seo_audit, web_extract, link_preview,
35
+ * robots_check, headers_check) on the given MCP server.
36
+ */
37
+ export function registerStallTools(server, baseUrl, walletPrivateKey) {
38
+ // 1. SEO + GEO audit.
39
+ registerStall(server, baseUrl, walletPrivateKey, {
40
+ toolName: "seo_audit",
41
+ path: "/v1/seo-audit",
42
+ description: "Audit a web page for SEO + GEO (generative-engine-optimization). " +
43
+ "Returns a structured JSON report: meta tags, an SEO score with " +
44
+ "per-category breakdown + issues, a GEO score with breakdown + " +
45
+ "issues, and a combined percentage. PAID x402 service (USDC on " +
46
+ "Base) — needs a funded wallet on the MCP server.",
47
+ inputSchema: {
48
+ url: urlField,
49
+ mode: z
50
+ .enum(["both", "seo", "geo"])
51
+ .optional()
52
+ .describe("Which audit(s) to run: 'both' (default), 'seo' only, or 'geo' only"),
53
+ },
54
+ buildBody: ({ url, mode }) => {
55
+ const body = { url };
56
+ if (mode !== undefined)
57
+ body.mode = mode;
58
+ return body;
59
+ },
60
+ });
61
+ // 2. Clean web extraction (readable text + markdown + links + metadata).
62
+ registerStall(server, baseUrl, walletPrivateKey, {
63
+ toolName: "web_extract",
64
+ path: "/v1/web-extract",
65
+ description: "Fetch a URL and return its clean readable text, a light markdown " +
66
+ "rendering, all links (in document order), and metadata (title, " +
67
+ "description, word count, byte size). PAID x402 service (USDC on " +
68
+ "Base) — needs a funded wallet on the MCP server.",
69
+ inputSchema: { url: urlField },
70
+ buildBody: ({ url }) => ({ url }),
71
+ });
72
+ // 3. Link preview / Open Graph card.
73
+ registerStall(server, baseUrl, walletPrivateKey, {
74
+ toolName: "link_preview",
75
+ path: "/v1/link-preview",
76
+ description: "Fetch a URL and return its link-preview / Open Graph unfurl card: " +
77
+ "title, description, image, siteName, type, canonical URL, favicon, " +
78
+ "and theme color (image/canonical/favicon resolved to absolute URLs). " +
79
+ "PAID x402 service (USDC on Base) — needs a funded wallet on the MCP " +
80
+ "server.",
81
+ inputSchema: { url: urlField },
82
+ buildBody: ({ url }) => ({ url }),
83
+ });
84
+ // 4. AI-crawler / robots.txt policy check.
85
+ registerStall(server, baseUrl, walletPrivateKey, {
86
+ toolName: "robots_check",
87
+ path: "/v1/robots-check",
88
+ description: "Report a site's AI-crawler policy (GPTBot, ClaudeBot, Google-Extended, " +
89
+ "PerplexityBot, and ~15 other AI bots: allow | block | unspecified), " +
90
+ "plus declared sitemaps and whether an llms.txt is present. Pass any " +
91
+ "URL on the target site. PAID x402 service (USDC on Base) — needs a " +
92
+ "funded wallet on the MCP server.",
93
+ inputSchema: { url: urlField },
94
+ buildBody: ({ url }) => ({ url }),
95
+ });
96
+ // 5. HTTP security-headers analysis.
97
+ registerStall(server, baseUrl, walletPrivateKey, {
98
+ toolName: "headers_check",
99
+ path: "/v1/headers-check",
100
+ description: "Analyse a URL's HTTP security headers (HSTS, CSP, X-Frame-Options, " +
101
+ "and more) into a present/missing breakdown plus a 0-100 score, along " +
102
+ "with status, HTTPS flag, and server banner. PAID x402 service (USDC " +
103
+ "on Base) — needs a funded wallet on the MCP server.",
104
+ inputSchema: { url: urlField },
105
+ buildBody: ({ url }) => ({ url }),
106
+ });
107
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Shared x402 payment helper for the true402 MCP server.
3
+ *
4
+ * Encapsulates the full pay-and-retry flow used by every paid stall:
5
+ * 1. POST the request body (no payment header)
6
+ * 2. If the server replies 402, parse the payment requirements
7
+ * 3. Pick the EVM (scheme: "exact") option, sign an EIP-3009
8
+ * TransferWithAuthorization with the configured wallet
9
+ * 4. Retry with the base64-encoded `X-PAYMENT` header
10
+ * 5. Return the parsed JSON (or a structured error)
11
+ *
12
+ * The signer is network-aware: it derives the EIP-712 domain (chainId,
13
+ * verifyingContract, name, version) from the 402 requirement itself, so it
14
+ * works on Base mainnet, Base Sepolia, or any future network the server
15
+ * advertises — instead of being hardcoded to one chain.
16
+ *
17
+ * SECURITY: the wallet private key is never logged, echoed, or returned.
18
+ */
19
+ import { type Hex } from "viem";
20
+ /**
21
+ * EVM payment requirement from a 402 response (scheme: "exact").
22
+ */
23
+ export interface EVMPaymentRequirement {
24
+ scheme: "exact";
25
+ network: string;
26
+ /**
27
+ * Amount in USDC base units. x402 v2 advertises this as `amount`; v1 used
28
+ * `maxAmountRequired`. We read either so the client works against both.
29
+ */
30
+ amount?: string;
31
+ maxAmountRequired?: string;
32
+ asset: string;
33
+ payTo: string;
34
+ facilitatorUrl: string;
35
+ maxTimeoutSeconds: number;
36
+ mimeType: string;
37
+ description?: string;
38
+ resource?: {
39
+ url: string;
40
+ method: string;
41
+ };
42
+ /**
43
+ * Optional EIP-712 USDC domain advertised by the server.
44
+ * Base mainnet USDC uses name "USD Coin"; Base Sepolia test USDC uses "USDC".
45
+ */
46
+ extra?: {
47
+ name?: string;
48
+ version?: string;
49
+ };
50
+ }
51
+ /**
52
+ * Sign an EIP-3009 TransferWithAuthorization and return the x402 payment
53
+ * payload. The EIP-712 domain is derived from the requirement (network +
54
+ * asset + advertised domain name/version) so signatures are valid on the
55
+ * exact chain the 402 demands.
56
+ */
57
+ export declare function signEIP3009Payment(privateKey: Hex, requirement: EVMPaymentRequirement): Promise<object>;
58
+ /**
59
+ * Discriminated result of {@link payAndFetch}.
60
+ *
61
+ * - `ok: true` -> `data` holds the parsed JSON returned by the endpoint.
62
+ * - `ok: false` -> `message` is a human/agent-readable explanation and
63
+ * `paymentRequired` is true when the failure was a 402 we could not satisfy
64
+ * (so the caller can surface the requirements clearly).
65
+ */
66
+ export type PayAndFetchResult = {
67
+ ok: true;
68
+ data: unknown;
69
+ } | {
70
+ ok: false;
71
+ message: string;
72
+ paymentRequired?: boolean;
73
+ };
74
+ /**
75
+ * Perform the full x402 flow against `${baseUrl}${path}` with the given JSON
76
+ * `body`. Returns the parsed JSON on success, or a structured error.
77
+ *
78
+ * If no wallet key is configured and the endpoint demands payment, this does
79
+ * NOT crash — it returns `{ ok: false, paymentRequired: true, message }` with
80
+ * the raw requirements embedded so an agent knows it must fund a wallet.
81
+ */
82
+ export declare function payAndFetch(baseUrl: string, path: string, body: unknown, walletPrivateKey: string | undefined): Promise<PayAndFetchResult>;
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Shared x402 payment helper for the true402 MCP server.
3
+ *
4
+ * Encapsulates the full pay-and-retry flow used by every paid stall:
5
+ * 1. POST the request body (no payment header)
6
+ * 2. If the server replies 402, parse the payment requirements
7
+ * 3. Pick the EVM (scheme: "exact") option, sign an EIP-3009
8
+ * TransferWithAuthorization with the configured wallet
9
+ * 4. Retry with the base64-encoded `X-PAYMENT` header
10
+ * 5. Return the parsed JSON (or a structured error)
11
+ *
12
+ * The signer is network-aware: it derives the EIP-712 domain (chainId,
13
+ * verifyingContract, name, version) from the 402 requirement itself, so it
14
+ * works on Base mainnet, Base Sepolia, or any future network the server
15
+ * advertises — instead of being hardcoded to one chain.
16
+ *
17
+ * SECURITY: the wallet private key is never logged, echoed, or returned.
18
+ */
19
+ import { encodePacked, keccak256 } from "viem";
20
+ import { privateKeyToAccount } from "viem/accounts";
21
+ const TRANSFER_WITH_AUTHORIZATION_TYPES = {
22
+ TransferWithAuthorization: [
23
+ { name: "from", type: "address" },
24
+ { name: "to", type: "address" },
25
+ { name: "value", type: "uint256" },
26
+ { name: "validAfter", type: "uint256" },
27
+ { name: "validBefore", type: "uint256" },
28
+ { name: "nonce", type: "bytes32" },
29
+ ],
30
+ };
31
+ // CAIP-2 chain id -> EVM numeric chainId.
32
+ const CHAIN_IDS = {
33
+ "eip155:8453": 8453, // Base mainnet
34
+ "eip155:84532": 84532, // Base Sepolia
35
+ "eip155:1": 1, // Ethereum mainnet
36
+ };
37
+ // Fallback numeric chainId when the network string is unknown (Base mainnet).
38
+ const DEFAULT_CHAIN_ID = 8453;
39
+ /**
40
+ * Resolve the numeric chainId for an x402 `network` value. Accepts CAIP-2
41
+ * ("eip155:8453"), a bare numeric id ("8453"), or the friendly names the
42
+ * server config understands ("base-mainnet" / "base-sepolia" / "ethereum").
43
+ */
44
+ function resolveChainId(network) {
45
+ if (CHAIN_IDS[network] !== undefined)
46
+ return CHAIN_IDS[network];
47
+ const colon = network.indexOf(":");
48
+ if (colon !== -1) {
49
+ const n = Number(network.slice(colon + 1));
50
+ if (Number.isFinite(n) && n > 0)
51
+ return n;
52
+ }
53
+ switch (network) {
54
+ case "base-mainnet":
55
+ case "base":
56
+ return 8453;
57
+ case "base-sepolia":
58
+ return 84532;
59
+ case "ethereum":
60
+ case "mainnet":
61
+ return 1;
62
+ default: {
63
+ const n = Number(network);
64
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_CHAIN_ID;
65
+ }
66
+ }
67
+ }
68
+ /**
69
+ * Sign an EIP-3009 TransferWithAuthorization and return the x402 payment
70
+ * payload. The EIP-712 domain is derived from the requirement (network +
71
+ * asset + advertised domain name/version) so signatures are valid on the
72
+ * exact chain the 402 demands.
73
+ */
74
+ export async function signEIP3009Payment(privateKey, requirement) {
75
+ const account = privateKeyToAccount(privateKey);
76
+ const now = Math.floor(Date.now() / 1000);
77
+ const validAfter = BigInt(now - 60); // valid 60s in the past (clock skew tolerance)
78
+ const validBefore = BigInt(now + requirement.maxTimeoutSeconds);
79
+ // Generate a random nonce (bytes32)
80
+ const randomBytes = new Uint8Array(32);
81
+ crypto.getRandomValues(randomBytes);
82
+ const nonce = keccak256(encodePacked(["bytes32", "uint256"], [
83
+ ("0x" +
84
+ Array.from(randomBytes)
85
+ .map((b) => b.toString(16).padStart(2, "0"))
86
+ .join("")),
87
+ BigInt(now),
88
+ ]));
89
+ // v2 advertises `amount`; v1 used `maxAmountRequired` — accept either, like a1-pay/pay.mjs.
90
+ const rawAmount = requirement.amount ?? requirement.maxAmountRequired;
91
+ if (rawAmount === undefined) {
92
+ throw new Error("Payment requirement is missing an amount");
93
+ }
94
+ const value = BigInt(rawAmount);
95
+ const message = {
96
+ from: account.address,
97
+ to: requirement.payTo,
98
+ value,
99
+ validAfter,
100
+ validBefore,
101
+ nonce: nonce,
102
+ };
103
+ // Build the EIP-712 domain from the requirement so we sign for the right
104
+ // chain + USDC contract. Prefer the domain name/version the server
105
+ // advertises in `extra`; fall back to network-derived defaults.
106
+ const chainId = resolveChainId(requirement.network);
107
+ const domainName = requirement.extra?.name ?? (chainId === 84532 ? "USDC" : "USD Coin");
108
+ const domainVersion = requirement.extra?.version ?? "2";
109
+ const domain = {
110
+ name: domainName,
111
+ version: domainVersion,
112
+ chainId,
113
+ verifyingContract: requirement.asset,
114
+ };
115
+ // Sign EIP-712 typed data
116
+ const signature = await account.signTypedData({
117
+ domain,
118
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
119
+ primaryType: "TransferWithAuthorization",
120
+ message,
121
+ });
122
+ // Build x402 payment payload
123
+ return {
124
+ x402Version: 2,
125
+ scheme: "exact",
126
+ network: requirement.network,
127
+ payload: {
128
+ signature,
129
+ authorization: {
130
+ from: account.address,
131
+ to: requirement.payTo,
132
+ value: value.toString(),
133
+ validAfter: validAfter.toString(),
134
+ validBefore: validBefore.toString(),
135
+ nonce,
136
+ },
137
+ },
138
+ };
139
+ }
140
+ /**
141
+ * Perform the full x402 flow against `${baseUrl}${path}` with the given JSON
142
+ * `body`. Returns the parsed JSON on success, or a structured error.
143
+ *
144
+ * If no wallet key is configured and the endpoint demands payment, this does
145
+ * NOT crash — it returns `{ ok: false, paymentRequired: true, message }` with
146
+ * the raw requirements embedded so an agent knows it must fund a wallet.
147
+ */
148
+ export async function payAndFetch(baseUrl, path, body, walletPrivateKey) {
149
+ const url = `${baseUrl}${path}`;
150
+ // Step 1: first request, no payment header.
151
+ let firstResponse;
152
+ try {
153
+ firstResponse = await fetch(url, {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/json" },
156
+ body: JSON.stringify(body),
157
+ });
158
+ }
159
+ catch (err) {
160
+ return {
161
+ ok: false,
162
+ message: `Network error reaching ${url}: ${err instanceof Error ? err.message : String(err)}`,
163
+ };
164
+ }
165
+ // Step 2: non-402 short-circuit (success OR a non-payment error).
166
+ if (firstResponse.status !== 402) {
167
+ const data = await safeJson(firstResponse);
168
+ if (!firstResponse.ok) {
169
+ return {
170
+ ok: false,
171
+ message: `Request failed: ${firstResponse.status} ${stringify(data)}`,
172
+ };
173
+ }
174
+ return { ok: true, data };
175
+ }
176
+ // Step 3: 402 Payment Required. Parse the requirements first so we can
177
+ // surface them no matter what.
178
+ const paymentData = (await safeJson(firstResponse));
179
+ if (!walletPrivateKey) {
180
+ return {
181
+ ok: false,
182
+ paymentRequired: true,
183
+ message: [
184
+ "Payment required (HTTP 402). No wallet private key configured.",
185
+ "This is a PAID x402 service (USDC on Base). Set WALLET_PRIVATE_KEY",
186
+ "on the MCP server to a funded wallet to enable automatic payment.",
187
+ "",
188
+ "Payment requirements:",
189
+ stringify(paymentData),
190
+ ].join("\n"),
191
+ };
192
+ }
193
+ // Find the EVM payment option (scheme: "exact").
194
+ const evmRequirement = paymentData.accepts?.find((r) => r.scheme === "exact");
195
+ if (!evmRequirement) {
196
+ // Lightning (BOLT11) payment is a TODO — not implemented here.
197
+ return {
198
+ ok: false,
199
+ paymentRequired: true,
200
+ message: [
201
+ "Payment required (HTTP 402) but no EVM (scheme: exact) option found.",
202
+ "Only EVM (USDC on Base) payments are currently supported; Lightning",
203
+ "(BOLT11) is not yet implemented in this client.",
204
+ "",
205
+ "Available payment options:",
206
+ stringify(paymentData.accepts),
207
+ ].join("\n"),
208
+ };
209
+ }
210
+ // Step 4: sign the EIP-3009 authorization and base64-encode the payload.
211
+ let xPaymentHeader;
212
+ try {
213
+ const signedPayload = await signEIP3009Payment(walletPrivateKey, evmRequirement);
214
+ xPaymentHeader = Buffer.from(JSON.stringify(signedPayload)).toString("base64");
215
+ }
216
+ catch (err) {
217
+ // Never include the key material in the error.
218
+ return {
219
+ ok: false,
220
+ message: `Failed to sign payment: ${err instanceof Error ? err.message : String(err)}`,
221
+ };
222
+ }
223
+ // Step 5: retry with the X-PAYMENT header.
224
+ let retryResponse;
225
+ try {
226
+ retryResponse = await fetch(url, {
227
+ method: "POST",
228
+ headers: {
229
+ "Content-Type": "application/json",
230
+ "X-PAYMENT": xPaymentHeader,
231
+ },
232
+ body: JSON.stringify(body),
233
+ });
234
+ }
235
+ catch (err) {
236
+ return {
237
+ ok: false,
238
+ message: `Network error on paid retry to ${url}: ${err instanceof Error ? err.message : String(err)}`,
239
+ };
240
+ }
241
+ const retryData = await safeJson(retryResponse);
242
+ if (!retryResponse.ok) {
243
+ return {
244
+ ok: false,
245
+ message: `Payment sent but request failed: ${retryResponse.status} ${stringify(retryData)}`,
246
+ };
247
+ }
248
+ return { ok: true, data: retryData };
249
+ }
250
+ /** Parse a response body as JSON, falling back to text wrapped in an object. */
251
+ async function safeJson(response) {
252
+ const text = await response.text();
253
+ if (!text)
254
+ return {};
255
+ try {
256
+ return JSON.parse(text);
257
+ }
258
+ catch {
259
+ return { _raw: text };
260
+ }
261
+ }
262
+ function stringify(value) {
263
+ return JSON.stringify(value, null, 2);
264
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@true402.dev/mcp-server",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for the true402 machine-native marketplace — pay-per-call AI + web tools over x402 (USDC on Base): LLM inference, SEO/GEO audit, web extract, link preview, robots/AI-crawler check, security headers.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "true402-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "dev": "tsx src/index.ts",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "x402",
24
+ "ai-agent",
25
+ "agent",
26
+ "micropayments",
27
+ "usdc",
28
+ "base",
29
+ "llm",
30
+ "seo",
31
+ "geo",
32
+ "web-scraping",
33
+ "tools"
34
+ ],
35
+ "homepage": "https://true402.dev",
36
+ "license": "MIT",
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.12.1",
42
+ "viem": "^2.21.0",
43
+ "zod": "^3.23.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.0.0",
47
+ "tsx": "^4.19.0",
48
+ "typescript": "^5.7.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=20.0.0"
52
+ }
53
+ }