Skip to main content
Glama

x402_pay

Fetch URLs requiring payment by automatically handling HTTP 402 responses using the x402 protocol. Pays with your Agent Wallet or reuses existing sessions to avoid repeated on-chain costs.

Instructions

Fetch a URL and automatically handle HTTP 402 Payment Required responses. If an active x402 V2 session covers this URL, the session token is used instead of making a new payment (no on-chain cost). If no session exists, the Agent Wallet pays the required amount and retries. Payment is rejected if it exceeds your wallet's spend limits or the max_payment_eth cap. Powered by the x402 protocol on Base network. Tip: Use x402_session_start to pay once for a session and save on repeated calls.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlYesURL to fetch (HTTP 402 responses are handled automatically)
methodNoHTTP method (default: GET)GET
headersNoAdditional request headers
bodyNoRequest body string (for POST/PUT/PATCH)
max_payment_ethNoMaximum payment cap in ETH (e.g. "0.001")
timeout_msNoTimeout in milliseconds (default: 30000)
skip_session_checkNoSkip session auto-detection and force a fresh x402 payment

Implementation Reference

  • The main handler function for the `x402_pay` tool. It performs the URL fetch, handles auto-session detection, manages payments via `agentwallet-sdk`, and formats the response.
    export async function handleX402Pay(
      input: X402PayInput
    ): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> {
      try {
        const wallet = getWallet();
        const config = getConfig();
        const timeoutMs = input.timeout_ms ?? 30000;
    
        // ── Auto-session detection (x402 V2 behaviour) ──────────────────────
        // If there's an active session for this URL and the caller hasn't
        // explicitly asked to skip it, use the session token instead of paying.
        if (!input.skip_session_check) {
          const activeSession = findSessionForUrl(input.url);
          if (activeSession) {
            const sessionHeaders = buildSessionHeaders(activeSession);
            const method = input.method ?? 'GET';
            const mergedHeaders: Record<string, string> = {
              'Accept': 'application/json, text/plain, */*',
              ...sessionHeaders,
              ...(input.headers ?? {}),
            };
    
            if (input.body && ['POST', 'PUT', 'PATCH'].includes(method)) {
              if (!mergedHeaders['Content-Type']) {
                mergedHeaders['Content-Type'] = 'application/json';
              }
            }
    
            const requestInit: RequestInit = {
              method,
              headers: mergedHeaders,
              ...(input.body ? { body: input.body } : {}),
              signal: AbortSignal.timeout(timeoutMs),
            };
    
            const response = await fetch(input.url, requestInit);
    
            // If server accepted the session (2xx/3xx), record it and return
            if (response.status !== 402) {
              const responseText = await response.text();
              recordSessionCall(activeSession.sessionId);
    
              const MAX_LEN = 8000;
              const truncated = responseText.length > MAX_LEN;
              const displayText = truncated
                ? responseText.slice(0, MAX_LEN) + '\n\n... [response truncated]'
                : responseText;
    
              const ttlRemaining = activeSession.expiresAt - Math.floor(Date.now() / 1000);
    
              let out = `🌐 **x402 Fetch Result** (session)\n\n`;
              out += `  URL:        ${input.url}\n`;
              out += `  Method:     ${method}\n`;
              out += `  Status:     ${response.status} ${response.statusText}\n`;
              out += `  Network:    ${chainName(config.chainId)}\n`;
              out += `\n🔐 **Session Used** (no payment)\n`;
              out += `  Session ID: ${activeSession.sessionId}\n`;
              if (activeSession.label) out += `  Label:      ${activeSession.label}\n`;
              out += `  TTL:        ${Math.ceil(ttlRemaining / 60)}m remaining\n`;
              out += `  Calls:      ${activeSession.callCount}\n`;
              out += `\n📄 **Response Body**\n`;
              out += '```\n' + displayText + '\n```';
    
              return { content: [textContent(out)] };
            }
    
            // Server returned 402 despite session headers — fall through to payment
            // (session may be invalid on the server side)
          }
        }
    
        // ── Standard x402 payment flow ────────────────────────────────────────
    
        // Parse optional max payment cap
        let maxPaymentWei: bigint | undefined;
        if (input.max_payment_eth) {
          const cap = parseFloat(input.max_payment_eth);
          if (isNaN(cap) || cap <= 0) {
            throw new Error(`Invalid max_payment_eth: "${input.max_payment_eth}"`);
          }
          maxPaymentWei = BigInt(Math.round(cap * 1e18));
        }
    
        // Track payment result
        let paymentMade = false;
        let paymentAmount = 0n;
        let paymentTxHash = '';
        let paymentRecipient = '';
    
        // Create x402 client with budget controls
        const x402Client = createX402Client(wallet, {
          autoPay: true,
          maxRetries: 1,
          // If cap is set, use it as globalPerRequestMax
          globalPerRequestMax: maxPaymentWei,
          onBeforePayment: (req, url) => {
            const amount = BigInt(req.amount);
            if (maxPaymentWei && amount > maxPaymentWei) {
              throw new Error(
                `Payment required (${amount} wei) exceeds max_payment_eth cap ` +
                `(${maxPaymentWei} wei = ${input.max_payment_eth} ETH). ` +
                `Increase max_payment_eth or the payment will not proceed.`
              );
            }
            return true;
          },
          onPaymentComplete: (log) => {
            paymentMade = true;
            paymentAmount = log.amount;
            paymentTxHash = log.txHash;
            paymentRecipient = log.recipient;
          },
        });
    
        // Build request options
        const method = input.method ?? 'GET';
        const headers: Record<string, string> = {
          'Accept': 'application/json, text/plain, */*',
          ...(input.headers ?? {}),
        };
    
        if (input.body && ['POST', 'PUT', 'PATCH'].includes(method)) {
          if (!headers['Content-Type']) {
            headers['Content-Type'] = 'application/json';
          }
        }
    
        const requestInit: RequestInit = {
          method,
          headers,
          ...(input.body ? { body: input.body } : {}),
          signal: AbortSignal.timeout(timeoutMs),
        };
    
        // Execute request with x402 handling
        const response = await x402Client.fetch(input.url, requestInit);
        const responseText = await response.text();
    
        // Truncate very large responses for readability
        const MAX_RESPONSE_LEN = 8000;
        const truncated = responseText.length > MAX_RESPONSE_LEN;
        const displayText = truncated
          ? responseText.slice(0, MAX_RESPONSE_LEN) + '\n\n... [response truncated]'
          : responseText;
    
        let out = `🌐 **x402 Fetch Result**\n\n`;
        out += `  URL:     ${input.url}\n`;
        out += `  Method:  ${method}\n`;
        out += `  Status:  ${response.status} ${response.statusText}\n`;
        out += `  Network: ${chainName(config.chainId)}\n`;
    
        if (paymentMade) {
          out += `\n💳 **Payment Made**\n`;
          out += `  Amount:    ${paymentAmount.toString()} (base units)\n`;
          out += `  Recipient: ${paymentRecipient}\n`;
          out += `  TX Hash:   ${paymentTxHash}\n`;
          out += `\n💡 Tip: Use x402_session_start to pay once for a session and skip per-call payments.\n`;
        } else {
          out += `\n✅ No payment required\n`;
        }
    
        out += `\n📄 **Response Body**\n`;
        out += '```\n' + displayText + '\n```';
    
        return { content: [textContent(out)] };
      } catch (error: unknown) {
        // Check for AbortError (timeout)
        if (error instanceof Error && error.name === 'AbortError') {
          return {
            content: [textContent(`❌ x402_pay failed: Request timed out after ${input.timeout_ms ?? 30000}ms`)],
            isError: true,
          };
        }
        return {
          content: [textContent(formatError(error, 'x402_pay'))],
          isError: true,
        };
      }
    }
  • Zod schema definition for the input parameters of the `x402_pay` tool.
    export const X402PaySchema = z.object({
      url: z
        .string()
        .url()
        .describe('URL to fetch. If it returns HTTP 402, payment is handled automatically.'),
      method: z
        .enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
        .optional()
        .default('GET')
        .describe('HTTP method (default: GET)'),
      headers: z
        .record(z.string())
        .optional()
        .describe('Additional HTTP request headers as key-value pairs'),
      body: z
        .string()
        .optional()
        .describe('Request body (for POST/PUT/PATCH). Use JSON string for JSON APIs.'),
      max_payment_eth: z
        .string()
        .optional()
        .describe(
          'Maximum ETH equivalent to pay for this request. ' +
          'Rejects the payment if the required amount exceeds this. ' +
          'E.g. "0.001" to cap at 0.001 ETH.'
        ),
      timeout_ms: z
        .number()
        .int()
        .min(1000)
        .max(60000)
        .optional()
        .default(30000)
        .describe('Request timeout in milliseconds (default: 30000, max: 60000)'),
      skip_session_check: z
        .boolean()
        .optional()
        .default(false)
        .describe(
          'Skip auto-session detection and always make a fresh x402 payment. ' +
          'Default: false. When false, if an active session covers this URL, ' +
          'the session token is used instead of paying again (x402 V2 behaviour).'
        ),
    });
  • The tool definition object for `x402_pay`, which includes the MCP tool registration information such as name, description, and the manual input schema.
    export const x402PayTool = {
      name: 'x402_pay',
      description:
        'Fetch a URL and automatically handle HTTP 402 Payment Required responses. ' +
        'If an active x402 V2 session covers this URL, the session token is used instead ' +
        'of making a new payment (no on-chain cost). ' +
        'If no session exists, the Agent Wallet pays the required amount and retries. ' +
        'Payment is rejected if it exceeds your wallet\'s spend limits or the max_payment_eth cap. ' +
        'Powered by the x402 protocol on Base network. ' +
        'Tip: Use x402_session_start to pay once for a session and save on repeated calls.',
      inputSchema: {
        type: 'object' as const,
        properties: {
          url: {
            type: 'string',
            description: 'URL to fetch (HTTP 402 responses are handled automatically)',
          },
          method: {
            type: 'string',
            enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
            description: 'HTTP method (default: GET)',
            default: 'GET',
          },
          headers: {
            type: 'object',
            additionalProperties: { type: 'string' },
            description: 'Additional request headers',
          },
          body: {
            type: 'string',
            description: 'Request body string (for POST/PUT/PATCH)',
          },
          max_payment_eth: {
            type: 'string',
            description: 'Maximum payment cap in ETH (e.g. "0.001")',
          },
          timeout_ms: {
            type: 'number',
            description: 'Timeout in milliseconds (default: 30000)',
            default: 30000,
          },
          skip_session_check: {
            type: 'boolean',
            description: 'Skip session auto-detection and force a fresh x402 payment',
            default: false,
          },
        },
        required: ['url'],
      },
    };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/up2itnow0822/claw-pay-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server