Skip to main content
Glama

azeth_pay

Automatically pay for x402-gated HTTP services using USDC. Handles 402 payment protocol detection, processes payments when needed, and returns API responses with payment details.

Instructions

Pay for an x402-gated HTTP service. Makes the request, handles 402 payment automatically, and returns the response.

Use this when: You need to access a paid API or service that uses the x402 payment protocol (HTTP 402). The tool automatically detects if you have an active payment agreement (subscription) with the service. If an agreement exists, access is granted without additional payment. Otherwise, a fresh USDC payment is signed.

Returns: Whether payment was made, the payment method used (x402/session/none), the HTTP status, and the response body.

Note: Requires USDC balance to pay (unless an agreement grants access). Set maxAmount to cap spending. Only HTTPS URLs to public endpoints are accepted. The payer account is determined by the AZETH_PRIVATE_KEY environment variable.

Example: { "url": "https://api.example.com/data" } or { "url": "https://api.example.com/data", "maxAmount": "1.00" }

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
chainNoTarget chain. Defaults to AZETH_CHAIN env var or "baseSepolia". Accepts "base", "baseSepolia", "ethereumSepolia", "ethereum" (and aliases like "base-sepolia", "eth-sepolia", "sepolia", "eth", "mainnet").
urlYesThe HTTPS URL of the x402-gated service to access. Must be a public endpoint.
methodNoHTTP method. Defaults to "GET".
bodyNoRequest body for POST/PUT/PATCH requests (JSON string, max 100KB).
maxAmountNoMaximum USDC amount willing to pay (e.g., "5.00"). Rejects if service costs more.
smartAccountNoSmart account to pay from. Use "#1", "#2", etc. (index from azeth_accounts) or a full address. Defaults to your first smart account.

Implementation Reference

  • The handler function for the azeth_pay tool. It validates the URL, resolves the smart account, calls client.fetch402, handles streaming of the response body with size limits, and returns the formatted response.
      async (args) => {
        let validated: ValidatedUrl;
        try {
          validated = await validateExternalUrl(args.url);
        } catch (err) {
          return handleError(err);
        }
    
        let client;
        try {
          client = await createClient(args.chain);
    
          // Apply smart account selection if specified
          if (args.smartAccount) {
            const selectionErr = applySmartAccountSelection(client, args.smartAccount);
            if (selectionErr) return selectionErr;
          }
          let maxAmount: bigint | undefined;
          if (args.maxAmount) {
            try {
              maxAmount = parseUnits(args.maxAmount, 6);
            } catch {
              return error('INVALID_INPUT', 'Invalid maxAmount format — must be a valid decimal number (e.g., "10.50")');
            }
          }
    
          // M-16 fix (Audit #8): Pass the validated URL (post-SSRF check) to fetch402
          // instead of the original args.url. The validated.url has already been
          // checked for SSRF and has the same value, but using it ensures the URL
          // that was validated is the URL that is fetched.
          const result = await client.fetch402(validated.url, {
            method: args.method,
            body: args.body,
            maxAmount,
          });
    
          // F-5/H-1: Stream response body with size limit. Uses Uint8Array chunks
          // to avoid O(n²) string concatenation on large responses.
          const chunks: Uint8Array[] = [];
          let totalBytes = 0;
          const reader = result.response.body?.getReader();
          if (reader) {
            try {
              while (totalBytes < MAX_RESPONSE_SIZE) {
                const { done, value } = await reader.read();
                if (done) break;
                chunks.push(value);
                totalBytes += value.byteLength;
              }
            } finally {
              reader.cancel().catch(() => {}); // release the stream
            }
          }
          // Concatenate chunks once and decode
          const merged = new Uint8Array(Math.min(totalBytes, MAX_RESPONSE_SIZE));
          let offset = 0;
          for (const chunk of chunks) {
            const remaining = merged.byteLength - offset;
            if (remaining <= 0) break;
            const slice = chunk.byteLength <= remaining ? chunk : chunk.subarray(0, remaining);
            merged.set(slice, offset);
            offset += slice.byteLength;
          }
          const responseBody = new TextDecoder().decode(merged);
          // For non-JSON responses (e.g., HTML pages), strip tags and truncate aggressively
          // to avoid flooding AI context with large HTML payloads.
          let truncatedBody: string;
          const trimmed = responseBody.trimStart();
          if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
            // JSON — keep at full limit
            truncatedBody = safeTruncate(responseBody, MAX_RESPONSE_SIZE);
          } else {
            // Non-JSON (likely HTML) — strip tags, collapse whitespace, limit to 2KB
            const stripped = responseBody.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
            truncatedBody = safeTruncate(stripped, 2_000);
          }
    
          return success({
            paid: result.paymentMade,
            amount: result.amount?.toString(),
            paymentMethod: result.paymentMethod,
            statusCode: result.response.status,
            body: truncatedBody,
          });
        } catch (err) {
          if (err instanceof Error && /AA24/.test(err.message)) {
            return guardianRequiredError(
              'Payment amount exceeds your standard spending limit.',
              { operation: 'payment' },
            );
          }
          // Format raw USDC amounts in budget/guardian errors for readability
          if (err instanceof AzethError && err.details) {
            const formatted = { ...err.details };
            let changed = false;
            for (const [key, val] of Object.entries(formatted)) {
              if (/amount/i.test(key) && typeof val === 'bigint') {
                formatted[key] = formatTokenAmount(val, 6, 2) + ' USDC';
                changed = true;
              } else if (/amount/i.test(key) && typeof val === 'string' && /^\d{7,}$/.test(val)) {
                try {
                  formatted[key] = formatTokenAmount(BigInt(val), 6, 2) + ' USDC';
                  changed = true;
                } catch { /* keep original */ }
              }
            }
            if (changed) {
              // Rewrite the message for BUDGET_EXCEEDED errors with formatted amounts
              if (err.code === 'BUDGET_EXCEEDED' && formatted.required && formatted.max) {
                const newMsg = `Payment of ${formatted.required} exceeds maximum of ${formatted.max}`;
                return handleError(new AzethError(newMsg, err.code, formatted));
              }
              return handleError(new AzethError(err.message, err.code, formatted));
            }
          }
          return handleError(err);
        } finally {
          try { await client?.destroy(); } catch (e) { process.stderr.write(`[azeth-mcp] destroy error: ${e instanceof Error ? e.message : String(e)}\n`); }
        }
      },
    );
  • The input schema for the azeth_pay tool, defined using zod.
    inputSchema: z.object({
      chain: z.string().optional().describe('Target chain. Defaults to AZETH_CHAIN env var or "baseSepolia". Accepts "base", "baseSepolia", "ethereumSepolia", "ethereum" (and aliases like "base-sepolia", "eth-sepolia", "sepolia", "eth", "mainnet").'),
      url: z.string().url().max(2048).describe('The HTTPS URL of the x402-gated service to access. Must be a public endpoint.'),
      method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().describe('HTTP method. Defaults to "GET".'),
      body: z.string().max(100_000).optional().describe('Request body for POST/PUT/PATCH requests (JSON string, max 100KB).'),
      maxAmount: z.string().max(32).optional().describe('Maximum USDC amount willing to pay (e.g., "5.00"). Rejects if service costs more.'),
      smartAccount: z.string().optional().describe('Smart account to pay from. Use "#1", "#2", etc. (index from azeth_accounts) or a full address. Defaults to your first smart account.'),
    }),
  • The registration block for the azeth_pay tool in the MCP server.
    server.registerTool(
      'azeth_pay',
      {
        description: [
          'Pay for an x402-gated HTTP service. Makes the request, handles 402 payment automatically, and returns the response.',
          '',
          'Use this when: You need to access a paid API or service that uses the x402 payment protocol (HTTP 402).',
          'The tool automatically detects if you have an active payment agreement (subscription) with the service.',
          'If an agreement exists, access is granted without additional payment. Otherwise, a fresh USDC payment is signed.',
          '',
          'Returns: Whether payment was made, the payment method used (x402/session/none), the HTTP status, and the response body.',
          '',
          'Note: Requires USDC balance to pay (unless an agreement grants access). Set maxAmount to cap spending.',
          'Only HTTPS URLs to public endpoints are accepted. The payer account is determined by the AZETH_PRIVATE_KEY environment variable.',
          '',
          'Example: { "url": "https://api.example.com/data" } or { "url": "https://api.example.com/data", "maxAmount": "1.00" }',
        ].join('\n'),

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/azeth-protocol/mcp-azeth'

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