azeth_smart_pay
Discover and automatically pay for services by capability, selecting high-reputation providers and handling fallbacks if needed.
Instructions
Discover the best service for a capability and pay for it automatically.
Use this when: You need a service by CAPABILITY (e.g., "price-feed", "market-data", "translation") and want Azeth to pick the highest-reputation provider, handle payment, and fall back to alternatives if needed.
How it differs from azeth_pay:
azeth_smart_pay: "I need price-feed data" → Azeth discovers the best service, pays it, returns the data.
azeth_pay: "I need data from https://specific-service.com/api" → You know which service, Azeth pays it.
Flow: Discovers services ranked by reputation → tries the best one → if it fails, tries the next. Set autoFeedback: true to automatically submit a reputation opinion based on service quality after payment. Note: autoFeedback defaults to false in MCP context (ephemeral client). Enable it if the MCP server has a bundler configured.
Returns: The response data, which service was used, how many attempts were needed, and payment details.
Example: { "capability": "price-feed" } or { "capability": "translation", "maxAmount": "0.50", "method": "POST", "body": "{"text": "hello"}" }
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"). | |
| capability | Yes | Service capability to discover (e.g., "price-feed", "market-data", "translation", "compute"). | |
| 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 per service (e.g., "1.00"). Rejects if service costs more. | |
| minReputation | No | Minimum reputation score (0-100) to consider. Services below this are excluded. | |
| autoFeedback | No | Automatically submit a reputation opinion after payment based on service quality. Defaults to false. | |
| 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:375-520 (handler)The handler implementation for the azeth_smart_pay MCP tool, which discovers services by capability, executes a request, and handles payments.
server.registerTool( 'azeth_smart_pay', { description: [ 'Discover the best service for a capability and pay for it automatically.', '', 'Use this when: You need a service by CAPABILITY (e.g., "price-feed", "market-data", "translation")', 'and want Azeth to pick the highest-reputation provider, handle payment, and fall back to alternatives if needed.', '', 'How it differs from azeth_pay:', '- azeth_smart_pay: "I need price-feed data" → Azeth discovers the best service, pays it, returns the data.', '- azeth_pay: "I need data from https://specific-service.com/api" → You know which service, Azeth pays it.', '', 'Flow: Discovers services ranked by reputation → tries the best one → if it fails, tries the next.', 'Set autoFeedback: true to automatically submit a reputation opinion based on service quality after payment.', 'Note: autoFeedback defaults to false in MCP context (ephemeral client). Enable it if the MCP server has a bundler configured.', '', 'Returns: The response data, which service was used, how many attempts were needed, and payment details.', '', 'Example: { "capability": "price-feed" } or { "capability": "translation", "maxAmount": "0.50", "method": "POST", "body": "{\"text\": \"hello\"}" }', ].join('\n'), 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").'), capability: z.string().min(1).max(256).describe('Service capability to discover (e.g., "price-feed", "market-data", "translation", "compute").'), 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 per service (e.g., "1.00"). Rejects if service costs more.'), minReputation: z.coerce.number().min(0).max(100).optional().describe('Minimum reputation score (0-100) to consider. Services below this are excluded.'), autoFeedback: z.boolean().optional().describe('Automatically submit a reputation opinion after payment based on service quality. Defaults to false.'), 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.'), }), }, async (args) => { 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., "1.00")'); } } // Disable autoFeedback in MCP context: the client is ephemeral (destroyed // after this call) and may not have a bundler URL for UserOp submission. // Feedback should be submitted by long-lived AzethKit instances instead. const result = await client.smartFetch402(args.capability, { method: args.method, body: args.body, maxAmount, minReputation: args.minReputation, autoFeedback: args.autoFeedback ?? false, }); // Stream response body with size limit (same pattern as azeth_pay) 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(() => {}); } } 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 let truncatedBody: string; const trimmedSmart = responseBody.trimStart(); if (trimmedSmart.startsWith('{') || trimmedSmart.startsWith('[')) { truncatedBody = safeTruncate(responseBody, MAX_RESPONSE_SIZE); } else { 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, service: { name: result.service.name, endpoint: result.service.endpoint, tokenId: result.service.tokenId.toString(), reputation: result.service.reputation, }, attemptsCount: result.attemptsCount, autoFeedback: args.autoFeedback ?? false, }); } catch (err) { if (err instanceof Error && /AA24/.test(err.message)) { return guardianRequiredError( 'Payment amount exceeds your standard spending limit.', { operation: 'smart_payment' }, ); } // Format raw USDC amounts in guardian/payment 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) { 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`); } } }, );