Skill: wzc1206/tron-x402-payment (version 1.0.0)

--- name: tron-x402-payment description: "Pay for x402-enabled Agent endpoints using TRC20 tokens (USDT/USDD) on TRON" version: 1.1.0 author: open-aibank homepage: https://x402.org metadata: {"clawdbot":{"emoji":"💳","env":["TRON_PRIVATE_KEY"]}} tags: [crypto, payments, x402, agents, api, usdt, usdd, tron] requires_tools: [x402_tron_invoke] # Tool implementation mapping: x402_tron_invoke -> dist/x402_tron_invoke.js arguments: url: description: "Base URL of the agent (v2) or full URL (v1/Discovery)" required: true entrypoint: description: "Entrypoint name to invoke (e.g., 'chat', 'search')" required: false input: description: "Input object to send to the entrypoint (will be wrapped in {\"input\": ...} for v2)" required: false --- # x402 Payment Protocol for TRON Agents Invoke x402-enabled AI agent endpoints with automatic TRC20 token payments on TRON. Currently recommended tokens: **USDT**, **USDD**. ## Quick Start The tool `x402_tron_invoke` is implemented by the compiled script `dist/x402_tron_invoke.js`. The script is pre-built and ready to run. You can execute it directly from the command line: ```bash # v2 Invoke node dist/x402_tron_invoke.js --url https://api.example.com --entrypoint chat --input '{"prompt": "hi"}' # Direct/Discovery node dist/x402_tron_invoke.js --url https://api.example.com/.well-known/agent.json ``` --- ## How It Works The `x402_tron_invoke` tool: 1. Constructs the endpoint URL: - If `entrypoint` is provided: `{url}/entrypoints/{entrypoint}/invoke` (v2) - Otherwise: Uses `{url}` as-is (v1 / Discovery) 2. Makes a request (POST for v2, GET default for v1) 3. If 402 Payment Required is returned: - Parses payment requirements - Checks wallet balance and allowance - Performs an **infinite approval** if allowance is insufficient - Signs the payment permit (EIP-712 / TRON Typed Data) - Retries the request with `X-PAYMENT` header 4. Returns the response ## Prerequisites - **Wallet**: A TRON private key must be available. The skill automatically looks for it in: 1. `TRON_PRIVATE_KEY` environment variable. 2. `~/.mcporter/mcporter.json` (AIBank standard). 3. `x402-config.json` in the current/home directory. - **Tokens**: Wallet needs USDT/USDD and some TRX for gas. - **TronGrid API Key**: Required for **Mainnet** to avoid rate limits (`TRON_GRID_API_KEY`). --- ## Tool Reference ### x402_tron_invoke Invokes an HTTP endpoint with automatic payment handling. **Modes:** 1. **v2 Agent Invoke** (Recommended): Provide `url` (Base URL) + `entrypoint`. * Constructs: `{url}/entrypoints/{entrypoint}/invoke` * Wraps input: `{"input": <input>}` * Method: `POST` 2. **v1 / Direct / Discovery**: Provide `url` (Full URL) without `entrypoint`. * Uses the URL as-is. * Method: `GET` (default) or specified via `method`. * **Agent Advice**: Use this mode for discovery. If `url` returns 404, try appending `/.well-known/agent.json` or `/entrypoints`. 3. **Status Check**: Provide `--check` or `--status`. * Verifies if `TRON_PRIVATE_KEY` is correctly configured and outputs the associated wallet address. * Checks if `TRON_GRID_API_KEY` is present (Required for Mainnet). * **Agent Advice**: ALWAYS use this instead of `env` or `echo $TRON_PRIVATE_KEY`. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `url` | string | Yes* | Base URL (v2) or Full URL (v1/Discovery). *Not required for `--check`. | | `entrypoint` | string | No | Entrypoint name. Required for v2 Invoke. | | `input` | object | No | Input data. | | `method` | string | No | HTTP method. Default: `POST` (v2), `GET` (Direct). | | `network` | string | No | `mainnet`, `nile`, `shasta` (Default: `nile`). | | `check` | boolean | No | Verify wallet configuration and output address. | ### Example: Chat with Agent (v2 Invoke) ```bash node dist/x402_tron_invoke.js --url https://api.example.com --entrypoint chat --input '{"prompt": "Tell me a joke"}' ``` *(Sends `POST https://api.example.com/entrypoints/chat/invoke`)* ### Example: Agent Discovery (Direct) 1. **Manifest**: Fetch agent metadata. ```bash node dist/x402_tron_invoke.js --url https://api.example.com/.well-known/agent.json ``` 2. **List Entrypoints**: List available functions. ```bash node dist/x402_tron_invoke.js --url https://api.example.com/entrypoints ``` Each entrypoint typically returns: - **Path**: `/entrypoints/{name}/invoke` - **Pricing**: Cost in tokens (e.g., 1000 units) - **Network**: Usually `nile` or `mainnet` - **Input Schema**: Expected JSON format for the `input` parameter ### Example: Raw URL (Legacy) ```bash node dist/x402_tron_invoke.js --url https://api.example.com/chat --method POST --input '{"prompt": "Tell me a joke"}' ``` --- ## Cost Reference (USDT/USDD) | Token | Network | Contract Address | Decimals | |-------|---------|------------------|----------| | USDT | Mainnet | [REDACTED_SECRET] | 6 | | USDT | Nile | [REDACTED_SECRET] | 6 | | USDT | Shasta | [REDACTED_SECRET] | 6 | | USDD | Mainnet | [REDACTED_SECRET] | 18 | | USDD | Nile | [REDACTED_SECRET] | 18 | | Amount (raw) | Value (USDT, 6 dec) | Typical Use | |--------------|---------------------|-------------| | 1000 | $0.001 | Single API call | | 5000 | $0.005 | Multiple results | | 10000 | $0.01 | Premium request | | 1000000 | $1.00 | Large batch | --- ## Understanding 402 Responses When payment is required, agents return: ```json { "error": "X-PAYMENT header is required", "accepts": [{ "scheme": "exact", "network": "nile", "maxAmountRequired": "1000", "payTo": "T...", "asset": "T..." }], "x402Version": 1 } ``` The tool handles this automatically. --- ## Supported Tokens This tool supports TRC20 tokens compatible with the x402 protocol. **Recommended:** - **USDT** (Tether) - **USDD** (Decentralized USD) ## Security Rules for Agents - **No Private Key Output**: The Agent MUST NOT print, echo, or output the `TRON_PRIVATE_KEY` or any other signing secret to the dialogue context. - **Internal Loading Only**: The Agent should rely on the skill or underlying scripts to load the key internally. - **No Export Commands**: The Agent MUST NOT execute shell commands that contain the private key as a literal string. - **Sanitized Logs**: Ensure that logs or error messages do not leak the key. - **Silent Environment Checks**: If you need to verify if an environment variable is set, use a "Silent Check". - **Correct**: `[[ -n $TRON_PRIVATE_KEY ]] && echo "Configured" || echo "Missing"` - **Forbidden**: `echo $TRON_PRIVATE_KEY`, `env`, `printenv`, `set`, `export`. - **Disabled Commands List**: Do NOT use the following commands in any environment containing sensitive keys: - `env` / `printenv` - `echo $VARIABLE_NAME` - `set` / `export` (without arguments) - **Use the Check Tool**: To safely verify wallet status, use `node dist/x402_tron_invoke.js --check`. ### ❌ Negative Example (CRITICAL FAILURE) > "I will check your private key now: `echo $TRON_PRIVATE_KEY`" -> **STOP! This leaks the secret to the logs and UI.** ### ✅ Positive Example > "I will verify the wallet configuration: `node dist/x402_tron_invoke.js --check`" -> **SAFE. Only the public address is shown.** ## Troubleshooting ### "Private key not found" Ensure the `TRON_PRIVATE_KEY` environment variable is set or a valid `x402-config.json` exists in the expected locations. ### "Insufficient Allowance" The tool will attempt to broadcast an **infinite approve** transaction. This requires TRX for gas. Ensure your wallet has TRX. ### "Transaction Failed" Check if you have enough Token balance (USDT/USDD) and TRX for gas. --- ## Binary and Image Handling If the endpoint returns an image (Content-Type: `image/*`) or binary data (`application/octet-stream`): 1. The data is automatically saved to a temporary file (e.g., `/tmp/x402_image_...`). 2. The tool returns a JSON object with: - `file_path`: Path to the temporary file. - `content_type`: The MIME type of the content. - `bytes`: File size in bytes. 3. **Important**: The Agent is responsible for deleting the temporary file after it has been used. --- ## Network Reference | Network | Chain ID | CAIP-2 | USDT Contract | USDD Contract | |---------|----------|--------|---------------|---------------| | TRON Mainnet | 0x2b6653dc | `eip155:728126428`, `tron:mainnet` | [REDACTED_SECRET] | [REDACTED_SECRET] | | TRON Nile | 0xcd8690dc | `eip155:3448148188`, `tron:nile` | [REDACTED_SECRET] | [REDACTED_SECRET] | | TRON Shasta | 0x94a9059e | `eip155:2494104990`, `tron:shasta` | [REDACTED_SECRET] | - |

path: package.json
content: {
  "name": "tron-x402-payment-skill",
  "version": "1.0.1",
  "description": "x402 payment skill for TRON",
  "type": "module",
  "scripts": {
    "build": "npx @vercel/ncc build src/x402_tron_invoke.ts -o dist && mv dist/index.js dist/x402_tron_invoke.js",
    "start": "node dist/x402_tron_invoke.js"
  },
  "devDependencies": {
    "@vercel/ncc": "^0.38.1",
    "typescript": "^5.9.3"
  },
  "dependencies": {
    "@open-aibank/x402-tron": "^0.1.6",
    "tronweb": "^5.3.0",
    "ts-node": "^10.9.1"
  }
}


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "ts-node": {
    "esm": true,
    "experimentalSpecifierResolution": "node"
  },
  "include": ["src/**/*"]
}

path: src/x402_tron_invoke.ts
content: #!/usr/bin/env node
import {
  TronClientSigner,
  X402Client,
  X402FetchClient,
  ExactTronClientMechanism
} from '@open-aibank/x402-tron';
// @ts-ignore
import TronWeb from 'tronweb';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

async function findPrivateKey(): Promise<string | undefined> {
  // 1. Check environment variable
  if (process.env.TRON_PRIVATE_KEY) {
    return process.env.TRON_PRIVATE_KEY;
  }

  // 2. Check local config files silently
  const configFiles = [
    path.join(process.cwd(), 'x402-config.json'),
    path.join(os.homedir(), '.x402-config.json')
  ];

  for (const file of configFiles) {
    if (fs.existsSync(file)) {
      try {
        const config = JSON.parse(fs.readFileSync(file, 'utf8'));
        const key = config.private_key || config.tron_private_key;
        if (key) return key;
      } catch (e) {
        // ignore malformed config
      }
    }
  }

  // 3. Check mcporter config (AIBank standard)
  const mcporterPath = path.join(os.homedir(), '.mcporter', 'mcporter.json');
  if (fs.existsSync(mcporterPath)) {
    try {
      const config = JSON.parse(fs.readFileSync(mcporterPath, 'utf8'));
      // Try to find the key in any likely server config, prioritizing tron-mcp-server
      const server = config.mcpServers?.['tron-mcp-server'];
      if (server?.env?.TRON_PRIVATE_KEY) {
        return server.env.TRON_PRIVATE_KEY;
      }
      
      // Fallback: check all servers for the env var
      if (config.mcpServers) {
        for (const serverName in config.mcpServers) {
          const s = config.mcpServers[serverName];
          if (s?.env?.TRON_PRIVATE_KEY) {
            return s.env.TRON_PRIVATE_KEY;
          }
        }
      }
    } catch (e) {
      // ignore
    }
  }

  return undefined;
}

async function findApiKey(): Promise<string | undefined> {
  // 1. Check environment variable
  if (process.env.TRON_GRID_API_KEY) {
    return process.env.TRON_GRID_API_KEY;
  }

  // 2. Check local config files silently
  const configFiles = [
    path.join(process.cwd(), 'x402-config.json'),
    path.join(os.homedir(), '.x402-config.json')
  ];

  for (const file of configFiles) {
    if (fs.existsSync(file)) {
      try {
        const config = JSON.parse(fs.readFileSync(file, 'utf8'));
        const key = config.tron_grid_api_key || config.api_key;
        if (key) return key;
      } catch (e) {
        // ignore malformed config
      }
    }
  }

  // 3. Check mcporter config (AIBank standard)
  const mcporterPath = path.join(os.homedir(), '.mcporter', 'mcporter.json');
  if (fs.existsSync(mcporterPath)) {
    try {
      const config = JSON.parse(fs.readFileSync(mcporterPath, 'utf8'));
      // Try to find the key in any likely server config, prioritizing tron-mcp-server
      const server = config.mcpServers?.['tron-mcp-server'];
      if (server?.env?.TRON_GRID_API_KEY) {
        return server.env.TRON_GRID_API_KEY;
      }
      
      // Fallback: check all servers for the env var
      if (config.mcpServers) {
        for (const serverName in config.mcpServers) {
          const s = config.mcpServers[serverName];
          if (s?.env?.TRON_GRID_API_KEY) {
            return s.env.TRON_GRID_API_KEY;
          }
        }
      }
    } catch (e) {
      // ignore
    }
  }

  return undefined;
}

async function main() {
  // Parse arguments
  const args = process.argv.slice(2);
  const options: Record<string, string> = {};
  
  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg.startsWith('--')) {
      const key = arg.slice(2);
      const value = args[i + 1];
      if (value && !value.startsWith('--')) {
        options[key] = value;
        i++;
      } else {
        options[key] = 'true';
      }
    }
  }

  const url = options.url;
  const entrypoint = options.entrypoint;
  const inputRaw = options.input;
  const methodArg = options.method;
  const networkName = options.network || 'nile';
  const checkStatus = options.check === 'true' || options.status === 'true';

  if (checkStatus) {
    const privateKey = await findPrivateKey();
    if (!privateKey) {
      console.error('[ERROR] TRON_PRIVATE_KEY is missing or not configured.');
      process.exit(1);
    }

    const apiKey = await findApiKey();
    if (networkName === 'mainnet' && !apiKey) {
      console.error('[WARNING] TRON_GRID_API_KEY is missing. Mainnet requests may be rate-limited or fail.');
    } else if (apiKey) {
      console.error('[OK] TRON_GRID_API_KEY is configured.');
    }

    try {
      const networks: Record<string, any> = {
        mainnet: { fullHost: 'https://api.trongrid.io' },
        shasta: { fullHost: 'https://api.shasta.trongrid.io' },
        nile: { fullHost: 'https://nile.trongrid.io' }
      };
      const config = networks[networkName] || networks.nile;
      const tronWeb = new TronWeb({
        fullHost: config.fullHost,
        privateKey: privateKey
      });
      const address = tronWeb.address.fromPrivateKey(privateKey);
      console.error(`[OK] Wallet configured (Address: ${address}) on ${networkName}`);
      process.exit(0);
    } catch (e) {
      console.error(`[ERROR] Failed to derive address from key: ${(e as any).message}`);
      process.exit(1);
    }
  }

  if (!url) {
    console.error('Error: --url is required');
    process.exit(1);
  }

  let finalUrl = url;
  let finalMethod = methodArg || 'GET';
  let finalBody: string | undefined = undefined;

  // Logic 1: v2 Agent Invoke (url + entrypoint)
  if (entrypoint) {
    // Remove trailing slash if present
    const baseUrl = url.endsWith('/') ? url.slice(0, -1) : url;
    finalUrl = `${baseUrl}/entrypoints/${entrypoint}/invoke`;
    finalMethod = 'POST';

    // Parse input if provided
    let inputData = {};
    if (inputRaw) {
      try {
        inputData = JSON.parse(inputRaw);
      } catch (e) {
        // Fallback for simple string input or invalid JSON
        inputData = inputRaw;
      }
    }
    finalBody = JSON.stringify({ input: inputData });
  }
  // Logic 2: Direct/Legacy
  else {
    if (methodArg) {
      finalMethod = methodArg.toUpperCase();
    }
    
    if (inputRaw) {
      finalBody = inputRaw;
    }
  }

  const privateKey = await findPrivateKey();
  if (!privateKey) {
    console.error('Error: Payment credentials not found. Missing environment variable: TRON_PRIVATE_KEY.');
    console.error('Please set TRON_PRIVATE_KEY before running this tool.');
    console.error('Alternatively, configure ~/.mcporter/mcporter.json or x402-config.json.');
    console.error('Agent: DO NOT attempt to search for or read private keys from files manually.');
    process.exit(1);
  }

  // Network configuration
  const networks: Record<string, any> = {
    mainnet: {
      fullHost: 'https://api.trongrid.io',
      network: 'mainnet'
    },
    shasta: {
      fullHost: 'https://api.shasta.trongrid.io',
      network: 'shasta'
    },
    nile: {
      fullHost: 'https://nile.trongrid.io',
      network: 'nile'
    }
  };

  const config = networks[networkName] || networks.nile;

  // Check for TRON_GRID_API_KEY to avoid rate limits
  const apiKey = await findApiKey();
  if (apiKey) {
    console.error('[x402] Using TRON_GRID_API_KEY for connection.');
  }

  try {
    // 1. Initialize TronWeb
    // Redirect console.log to console.error to prevent library pollution of STDOUT
    // The library uses console.log for debug info (e.g. signing details, allowance checks)
    // which violates the x402 Agent protocol (STDOUT is exclusive for final JSON).
    const originalConsoleLog = console.log;
    console.log = console.error;

    const tronWebOptions: any = {
      fullHost: config.fullHost,
      privateKey: privateKey
    };

    if (apiKey) {
      tronWebOptions.headers = { 'TRON-PRO-API-KEY': apiKey };
    }

    const tronWeb = new TronWeb(tronWebOptions);

    // 2. Initialize Signer
    // Use config.network to ensure we use a valid/supported network name (e.g. 'nile' instead of 'foo')
    const signer = TronClientSigner.fromTronWeb(tronWeb, config.network);
    console.error(`[x402] Initialized signer for address: ${signer.getAddress()} on network: ${config.network}`);

    // 3. Initialize Mechanism
    const mechanism = new ExactTronClientMechanism(signer);

    // 4. Initialize Core Client
    const client = new X402Client();
    // Register ONLY for the configured network to avoid signing for wrong networks
    client.register(`tron:${config.network}`, mechanism);

    // 5. Initialize Fetch Client
    const fetchClient = new X402FetchClient(client);

    // 6. Execute Request
    console.error(`[x402] Requesting: ${finalMethod} ${finalUrl}`);
    
    const requestInit: RequestInit = {
      method: finalMethod,
      headers: {
        'Content-Type': 'application/json'
      }
    };

    if (finalBody) {
      requestInit.body = finalBody;
    }

    const response = await fetchClient.request(finalUrl, requestInit);

    // 7. Output Result
    console.error(`[x402] Response Status: ${response.status}`);
    
    // Check for payment response headers (settlement info)
    const paymentResponse = response.headers.get('PAYMENT-RESPONSE');
    if (paymentResponse) {
      try {
        const settlement = JSON.parse(Buffer.from(paymentResponse, 'base64').toString());
        console.error(`[x402] Payment Settled: ${settlement.txHash || 'Confirmed'}`);
      } catch (e) {
        // ignore
      }
    }

    const contentType = response.headers.get('content-type') || '';
    let responseBody;
    
    if (contentType.includes('application/json')) {
      responseBody = await response.json();
    } else if (contentType.includes('image/') || contentType.includes('application/octet-stream')) {
      const arrayBuffer = await response.arrayBuffer();
      const buffer = Buffer.from(arrayBuffer);
      
      const tmpDir = os.tmpdir();
      const isImage = contentType.includes('image/');
      const ext = isImage ? (contentType.split('/')[1]?.split(';')[0] || 'bin') : 'bin';
      const prefix = isImage ? 'x402_image' : 'x402_binary';
      const fileName = `${prefix}_${Date.now()}_${Math.random().toString(36).substring(7)}.${ext}`;
      const filePath = path.join(tmpDir, fileName);
      
      fs.writeFileSync(filePath, buffer);
      console.error(`[x402] Binary data saved to temporary file: ${filePath}`);
      console.error(`[x402] Please delete this file after use.`);

      responseBody = {
        file_path: filePath,
        content_type: contentType,
        bytes: buffer.length
      };
    } else {
      responseBody = await response.text();
    }

    // Print final result to STDOUT for the agent to consume
    // Use process.stdout directly to bypass the console.log redirection
    process.stdout.write(JSON.stringify({
      status: response.status,
      headers: Object.fromEntries(response.headers.entries()),
      body: responseBody,
      isBase64: false
    }, null, 2) + '\n');

  } catch (error: any) {
    let message = error.message || 'Unknown error';
    let stack = error.stack || '';

    // Sanitize any potential private key leaks in error messages/stacks
    if (privateKey) {
      const escapedKey = privateKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      const keyRegex = new RegExp(escapedKey, 'g');
      message = message.replace(keyRegex, '[REDACTED]');
      stack = stack.replace(keyRegex, '[REDACTED]');
    }

    console.error(`[x402] Error: ${message}`);
    process.stdout.write(JSON.stringify({
      error: message,
      stack: stack
    }, null, 2) + '\n');
    process.exit(1);
  }
}

main();


path: dist/package.json
content: type: module

Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)