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.'),
        }),
      },

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