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
| Name | Required | Description | Default |
|---|---|---|---|
| chain | No | 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 | Yes | The HTTPS URL of the x402-gated service to access. Must be a public endpoint. | |
| method | No | HTTP method. Defaults to "GET". | |
| body | No | Request body for POST/PUT/PATCH requests (JSON string, max 100KB). | |
| maxAmount | No | Maximum USDC amount willing to pay (e.g., "5.00"). Rejects if service costs more. | |
| smartAccount | No | Smart account to pay from. Use "#1", "#2", etc. (index from azeth_accounts) or a full address. Defaults to your first smart account. |
Implementation Reference
- src/tools/payments.ts:250-370 (handler)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`); } } }, ); - src/tools/payments.ts:241-248 (schema)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.'), }), - src/tools/payments.ts:224-240 (registration)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'),