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'),
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Outstanding behavioral disclosure given zero annotations. Documents: (1) automatic agreement detection logic, (2) dual payment paths (existing agreement vs fresh USDC signature), (3) return value structure (payment method, HTTP status, body), (4) authentication mechanism (AZETH_PRIVATE_KEY env var), and (5) spending caps. Covers all critical operational behaviors an agent needs to understand.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Perfectly structured for a complex tool. Front-loaded with purpose and usage, followed by behavioral logic, return values, constraints, auth details, and examples. No redundant text; every sentence addresses a distinct operational concern (payment flow, prerequisites, security, configuration).

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Fully compensates for missing annotations and output schema. For a 6-parameter payment tool with complex stateful logic (agreements, USDC, signing), the description provides complete coverage: input constraints, output structure, error conditions (insufficient balance), and environmental dependencies.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 100% schema coverage, baseline is 3. Description adds valuable semantic context: maxAmount is for 'capping spending' (budget protection), URLs must be 'HTTPS' (security constraint), and smartAccount defaults to env var context. Example JSON provides concrete usage patterns beyond dry schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Excellent purpose clarity: states specific verbs ('Pay', 'Makes', 'handles') and resource ('x402-gated HTTP service'). The x402/HTTP 402 specificity clearly distinguishes it from sibling tools like azeth_transfer (direct transfers) and azeth_smart_pay (likely smart contract payments).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Strong positive guidance with explicit 'Use this when' statement targeting paid APIs using x402 protocol. Includes clear constraints ('Only HTTPS URLs', 'Requires USDC balance', 'Set maxAmount to cap spending') that implicitly define when-not-to-use. Lacks explicit naming of alternative sibling tools (e.g., 'use azeth_create_payment_agreement to set up subscriptions first'), but constraints provide sufficient guardrails.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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