Skip to main content
Glama

azeth_subscribe_service

Set up recurring payments for x402-gated services by creating on-chain payment agreements. Automatically detects subscriptions for future service calls without manual agreement management.

Instructions

Subscribe to an x402-gated service by creating a payment agreement.

Use this when: You want to set up a subscription instead of paying per-request. The tool fetches the service URL, parses the 402 payment-agreement extension terms, and creates an on-chain payment agreement matching those terms.

Returns: The agreement ID, transaction hash, and subscription details.

Note: The service must advertise payment-agreement terms in its 402 response. After subscribing, subsequent calls to azeth_pay will automatically detect the agreement. No need to pass an agreementId — the server recognizes your wallet via SIWx authentication.

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 subscribe to.
intervalSecondsNoOverride the suggested interval (seconds, minimum 60). Defaults to the service suggestion.
maxExecutionsNoMaximum number of payments. 0 or omit for unlimited.
totalCapNoMaximum total payout in human-readable token units (e.g., "100.00").

Implementation Reference

  • The handler logic for the azeth_subscribe_service tool.
      async (args) => {
        let validated: ValidatedUrl;
        try {
          validated = await validateExternalUrl(args.url);
        } catch (err) {
          return handleError(err);
        }
    
        // Business-rule validation (moved from Zod to handler for consistent error format)
        if (args.intervalSeconds !== undefined && args.intervalSeconds < 60) {
          return error('INVALID_INPUT', 'intervalSeconds must be at least 60 (1 minute).', 'Common values: 86400 (daily), 604800 (weekly), 2592000 (monthly).');
        }
        if (args.maxExecutions !== undefined && args.maxExecutions < 0) {
          return error('INVALID_INPUT', 'maxExecutions must be 0 or greater.', '0 means unlimited. Omit for unlimited.');
        }
    
        // Contract requires at least one cap condition to prevent unlimited payments.
        // endTime is set automatically (30 days from now) so we only check user-provided caps.
        if (!args.maxExecutions && !args.totalCap) {
          return error('INVALID_INPUT',
            'At least one limit is required: maxExecutions or totalCap.',
            'The contract requires a cap condition to prevent unlimited payments. E.g., maxExecutions: 30 for monthly billing.');
        }
    
        let client;
        try {
          client = await createClient(args.chain);
    
          // Fetch the URL to get 402 response with agreement terms
          const response = await fetch(validated.url, {
            method: 'GET',
            signal: AbortSignal.timeout(15_000),
          });
    
          if (response.status !== 402) {
            return error('INVALID_INPUT', `Service at ${args.url} did not return 402 — it may not require payment.`);
          }
    
          // Parse PAYMENT-REQUIRED header (v2) or X-Payment-Required (v1)
          const reqHeader = response.headers.get('PAYMENT-REQUIRED') ?? response.headers.get('X-Payment-Required');
          if (!reqHeader) {
            return error('INVALID_INPUT', 'Service returned 402 but no payment requirement header.');
          }
    
          let requirement: Record<string, unknown>;
          try {
            // x402v2 base64-encodes the PAYMENT-REQUIRED header; v1 sends raw JSON.
            let jsonStr: string;
            try {
              jsonStr = atob(reqHeader);
              if (!jsonStr.startsWith('{') && !jsonStr.startsWith('[')) throw new Error('not base64 JSON');
            } catch {
              jsonStr = reqHeader;
            }
            requirement = JSON.parse(jsonStr);
          } catch {
            return error('INVALID_INPUT', 'Failed to parse payment requirement header.');
          }
    
          // Look for payment-agreement extension in the requirement
          const extensions = requirement.extensions as Record<string, Record<string, unknown>> | undefined;
          const agreementExt = extensions?.['payment-agreement'];
    
          if (!agreementExt?.acceptsAgreements) {
            return error('INVALID_INPUT', 'Service does not advertise payment-agreement terms. Use azeth_pay for one-time payment.');
          }
    
          const terms = agreementExt.terms as {
            payee: string;
            token: string;
            moduleAddress: string;
            minAmountPerInterval: string;
            suggestedInterval: number;
          };
    
          if (!terms?.payee || !terms?.token) {
            return error('INVALID_INPUT', 'Service agreement terms are incomplete (missing payee or token).');
          }
          // Audit #13 H-4 fix: Validate payee and token are valid Ethereum addresses
          if (!isAddress(terms.payee)) {
            return error('INVALID_INPUT', 'Invalid payee address from service.');
          }
          if (!isAddress(terms.token)) {
            return error('INVALID_INPUT', 'Invalid token address from service.');
          }
    
          // Parse amount
          const amount = BigInt(terms.minAmountPerInterval);
          const interval = args.intervalSeconds ?? terms.suggestedInterval;
    
          // Calculate totalCap if provided
          let totalCap: bigint | undefined;
          if (args.totalCap) {
            try {
              totalCap = parseUnits(args.totalCap, 6);
            } catch {
              return error('INVALID_INPUT', 'Invalid totalCap format — must be a valid decimal number.');
            }
          }
    
          const result = await client.createPaymentAgreement({
            payee: terms.payee as `0x${string}`,
            token: terms.token as `0x${string}`,
            amount,
            interval,
            maxExecutions: args.maxExecutions,
            totalCap,
          });
    
          return success(
            {
              agreementId: result.agreementId.toString(),
              txHash: result.txHash,
              subscription: {
                payee: terms.payee,
                token: terms.token,
                amountPerInterval: terms.minAmountPerInterval,
                intervalSeconds: interval,
                maxExecutions: args.maxExecutions ?? 0,
                serviceUrl: args.url,
              },
            },
            { txHash: result.txHash },
          );
        } catch (err) {
          if (err instanceof Error && /AA24/.test(err.message)) {
            return guardianRequiredError(
              'Subscription creation exceeds your standard spending limit.',
              { operation: 'subscribe_service' },
            );
          }
          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 definition for the azeth_subscribe_service tool.
    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 subscribe to.'),
      intervalSeconds: z.coerce.number().int().optional().describe('Override the suggested interval (seconds, minimum 60). Defaults to the service suggestion.'),
      maxExecutions: z.coerce.number().int().optional().describe('Maximum number of payments. 0 or omit for unlimited.'),
      totalCap: z.string().max(32).optional().describe('Maximum total payout in human-readable token units (e.g., "100.00").'),
    }),
  • The registration of the azeth_subscribe_service tool.
    'azeth_subscribe_service',
    {
      description: [
        'Subscribe to an x402-gated service by creating a payment agreement.',
        '',
        'Use this when: You want to set up a subscription instead of paying per-request.',
        'The tool fetches the service URL, parses the 402 payment-agreement extension terms,',
        'and creates an on-chain payment agreement matching those terms.',
        '',
        'Returns: The agreement ID, transaction hash, and subscription details.',
        '',
        'Note: The service must advertise payment-agreement terms in its 402 response.',
        'After subscribing, subsequent calls to azeth_pay will automatically detect the agreement.',
        'No need to pass an agreementId — the server recognizes your wallet via SIWx authentication.',
      ].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").'),
        url: z.string().url().max(2048).describe('The HTTPS URL of the x402-gated service to subscribe to.'),
        intervalSeconds: z.coerce.number().int().optional().describe('Override the suggested interval (seconds, minimum 60). Defaults to the service suggestion.'),
        maxExecutions: z.coerce.number().int().optional().describe('Maximum number of payments. 0 or omit for unlimited.'),
        totalCap: z.string().max(32).optional().describe('Maximum total payout in human-readable token units (e.g., "100.00").'),
      }),
    },
Behavior5/5

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

Zero annotations provided, yet description comprehensively discloses: (1) mutation nature ('creating an on-chain payment agreement'), (2) prerequisite ('service must advertise payment-agreement terms in its 402 response'), (3) side effects on sibling tools ('subsequent calls to azeth_pay will automatically detect the agreement'), and (4) authentication mechanism ('SIWx authentication').

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

Conciseness4/5

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

Well-structured with clear sections: definition, usage trigger, behavioral mechanics, return values, and operational notes. Every sentence advances understanding. Minor efficiency could be gained by condensing the returns section, but overall zero waste.

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?

Excellent coverage given zero annotations and no output schema. Describes return structure ('agreement ID, transaction hash, and subscription details'), integration with sibling payment flows, failure prerequisites, and post-conditions. Appropriate complexity level for a blockchain subscription primitive.

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

Parameters3/5

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

Schema coverage is 100%, establishing baseline 3. Description adds narrative context linking parameters to workflow phases (e.g., 'fetches the service URL,' 'matching those terms'), but does not introduce syntax, format constraints, or semantic mappings beyond what the schema already documents.

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?

Opens with specific verb-resource pair ('Subscribe to an x402-gated service') and clarifies mechanism ('by creating a payment agreement'). Implicitly distinguishes from sibling azeth_pay by contrasting 'subscription' with 'paying per-request', and from azeth_create_payment_agreement by emphasizing automatic fetching/parsing of terms.

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?

Provides explicit 'Use this when' clause that clearly signals subscription vs. per-request payment patterns. While it doesn't explicitly state 'do not use when' exclusions, the conditional logic ('instead of paying per-request') effectively guides selection between this and azeth_pay.

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