Skip to main content
Glama

azeth_create_payment_agreement

Create automated recurring payments between participants for subscriptions, data feeds, or scheduled transfers using on-chain agreements.

Instructions

Set up a recurring payment agreement to another participant. Payments execute on a fixed interval.

Use this when: You need automated recurring payments (subscriptions, data feeds, scheduled transfers) between participants.

Returns: The agreement ID and creation transaction hash.

Note: This creates an on-chain agreement via the PaymentAgreementModule. The payee or anyone can call execute once each interval has elapsed. Requires sufficient token balance for each execution. The payer account is determined by the AZETH_PRIVATE_KEY environment variable.

Example: { "payee": "Alice", "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "amount": "1.00", "intervalSeconds": 86400 }

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").
payeeYesRecipient: Ethereum address, participant name, "me", or "#N" (account index).
tokenYesPayment token address. Use an ERC-20 contract address (e.g., USDC) or 0x0000000000000000000000000000000000000000 for native ETH.
amountYesPayment amount per interval in human-readable units (e.g., "10.00" for 10 USDC).
intervalSecondsYesTime between payments in seconds (minimum 60). E.g., 86400 for daily, 604800 for weekly.
maxExecutionsNoMaximum number of payments. 0 or omit for unlimited.
decimalsNoToken decimals. Defaults to 6 (USDC). Use 18 for WETH or native ETH.

Implementation Reference

  • The handler function for azeth_create_payment_agreement tool.
      async (args) => {
        if (!validateAddress(args.token)) {
          return error('INVALID_INPUT', `Invalid token address: "${args.token}".`, 'Must be 0x-prefixed followed by 40 hex characters.');
        }
        // Business-rule validation (moved from Zod to handler for consistent error format)
        if (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.');
        }
        // Native ETH: address(0) is valid — PaymentAgreementModule supports both ETH and ERC-20.
        // For ETH, default to 18 decimals if not explicitly provided.
        const isNativeETH = args.token === '0x0000000000000000000000000000000000000000';
    
        let client;
        try {
          client = await createClient(args.chain);
    
          // Resolve payee: address, name, "me", "#N"
          let payeeResolved;
          try {
            payeeResolved = await resolveAddress(args.payee, client);
          } catch (resolveErr) {
            return handleError(resolveErr);
          }
    
          const decimals = args.decimals ?? (isNativeETH ? 18 : 6);
          let amount: bigint;
          try {
            amount = parseUnits(args.amount, decimals);
          } catch {
            return error('INVALID_INPUT', 'Invalid amount format — must be a valid decimal number (e.g., "10.00")');
          }
    
          // Pre-flight: verify the token is whitelisted by the guardian module
          try {
            const chain = resolveChain(args.chain);
            const guardianAddr = AZETH_CONTRACTS[chain].guardianModule as `0x${string}`;
            const smartAccount = await client.resolveSmartAccount();
            const { GuardianModuleAbi } = await import('@azeth/common/abis');
            const isWhitelisted = await client.publicClient.readContract({
              address: guardianAddr,
              abi: GuardianModuleAbi,
              functionName: 'isTokenWhitelisted',
              args: [smartAccount, args.token as `0x${string}`],
            }) as boolean;
            if (!isWhitelisted) {
              const tokenLabel = isNativeETH ? 'Native ETH (address(0))' : `Token ${args.token}`;
              return error(
                'INVALID_INPUT',
                `${tokenLabel} is not whitelisted by your guardian. Agreement would be unexecutable.`,
                'Add it to the token whitelist via the guardian before creating an agreement.',
              );
            }
          } catch {
            // Non-fatal: if the whitelist check fails (RPC error, module not deployed),
            // proceed and let the contract handle validation at execution time.
          }
    
          const result = await client.createPaymentAgreement({
            payee: payeeResolved.address,
            token: args.token as `0x${string}`,
            amount,
            interval: args.intervalSeconds,
            maxExecutions: args.maxExecutions,
          });
    
          // Resolve token symbol for display
          const chain = resolveChain(args.chain);
          const tokens = TOKENS[chain];
          const tokenLower = args.token.toLowerCase();
          let tokenSymbol = 'TOKEN';
          if (isNativeETH) {
            tokenSymbol = 'ETH';
          } else if (tokenLower === tokens.USDC.toLowerCase()) {
            tokenSymbol = 'USDC';
          } else if (tokenLower === tokens.WETH.toLowerCase()) {
            tokenSymbol = 'WETH';
          }
    
          // Format interval for human readability
          const secs = args.intervalSeconds;
          let intervalHuman: string;
          if (secs >= 86400 && secs % 86400 === 0) {
            const days = secs / 86400;
            intervalHuman = days === 1 ? 'every day' : `every ${days} days`;
          } else if (secs >= 3600 && secs % 3600 === 0) {
            const hours = secs / 3600;
            intervalHuman = hours === 1 ? 'every hour' : `every ${hours} hours`;
          } else if (secs >= 60 && secs % 60 === 0) {
            const mins = secs / 60;
            intervalHuman = mins === 1 ? 'every minute' : `every ${mins} minutes`;
          } else {
            intervalHuman = `every ${secs} seconds`;
          }
    
          return success(
            {
              agreementId: result.agreementId.toString(),
              txHash: result.txHash,
              agreement: {
                payee: payeeResolved.address,
                ...(payeeResolved.resolvedFrom ? { payeeName: payeeResolved.resolvedFrom } : {}),
                token: args.token,
                tokenSymbol,
                amount: args.amount,
                amountFormatted: `${args.amount} ${tokenSymbol}`,
                intervalSeconds: args.intervalSeconds,
                intervalHuman,
                maxExecutions: args.maxExecutions ?? 0,
              },
            },
            { txHash: result.txHash },
          );
        } catch (err) {
          if (err instanceof Error && /AA24/.test(err.message)) {
            return guardianRequiredError(
              'Agreement creation exceeds your standard spending limit.',
              { operation: 'create_agreement' },
            );
          }
          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`); }
        }
      },
    );
  • Tool registration and schema for azeth_create_payment_agreement.
    server.registerTool(
      'azeth_create_payment_agreement',
      {
        description: [
          'Set up a recurring payment agreement to another participant. Payments execute on a fixed interval.',
          '',
          'Use this when: You need automated recurring payments (subscriptions, data feeds, scheduled transfers) between participants.',
          '',
          'Returns: The agreement ID and creation transaction hash.',
          '',
          'Note: This creates an on-chain agreement via the PaymentAgreementModule. The payee or anyone can call execute',
          'once each interval has elapsed. Requires sufficient token balance for each execution.',
          'The payer account is determined by the AZETH_PRIVATE_KEY environment variable.',
          '',
          'Example: { "payee": "Alice", "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "amount": "1.00", "intervalSeconds": 86400 }',
        ].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").'),
          payee: z.string().describe('Recipient: Ethereum address, participant name, "me", or "#N" (account index).'),
          token: z.string().regex(/^0x[0-9a-fA-F]{40}$/, 'Must be a valid Ethereum address (0x + 40 hex chars)').describe('Payment token address. Use an ERC-20 contract address (e.g., USDC) or 0x0000000000000000000000000000000000000000 for native ETH.'),
          amount: z.string().describe('Payment amount per interval in human-readable units (e.g., "10.00" for 10 USDC).'),
          intervalSeconds: z.coerce.number().int().describe('Time between payments in seconds (minimum 60). E.g., 86400 for daily, 604800 for weekly.'),
          maxExecutions: z.coerce.number().int().optional().describe('Maximum number of payments. 0 or omit for unlimited.'),
          decimals: z.coerce.number().int().min(0).max(18).optional().describe('Token decimals. Defaults to 6 (USDC). Use 18 for WETH or native ETH.'),
        }),
      },
Behavior4/5

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

With no annotations provided, the description carries full disclosure burden effectively. It specifies the on-chain module used (PaymentAgreementModule), execution permissions ('payee or anyone can call execute'), balance requirements, and critical auth context (AZETH_PRIVATE_KEY env var). Only minor gap is not mentioning that agreements are cancellable (relevant given azeth_cancel_agreement sibling).

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: purpose, usage trigger, returns, technical notes, and example. Slight redundancy between 'recurring' and 'fixed interval' in opening sentences, but overall information density is high with minimal 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?

Despite lacking an output schema, the description explicitly documents return values ('agreement ID and creation transaction hash'). It also covers environmental prerequisites (AZETH_PRIVATE_KEY, token balance), on-chain mechanics, and provides a concrete example, making it complete for a complex blockchain operation with 7 parameters.

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?

Schema has 100% coverage, establishing baseline 3. The description adds value through the JSON example showing semantic usage (e.g., 'Alice' as valid payee identifier, string formatting for amount, 86400 mapping to daily intervals), which helps agents understand parameter interchange formats beyond raw schema types.

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?

The description opens with a specific verb phrase ('Set up a recurring payment agreement') and clearly identifies the resource type and mechanism ('Payments execute on a fixed interval'). It effectively distinguishes this from sibling tools like azeth_pay (one-time) and azeth_transfer by emphasizing the recurring/automated nature.

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' guidance listing specific scenarios (subscriptions, data feeds, scheduled transfers). While it doesn't explicitly name sibling tools to avoid, the context makes clear this is for automated recurring flows versus one-time payments, which is sufficient given the extensive sibling list.

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