Skip to main content
Glama

azeth_execute_agreement

Execute due payments from on-chain agreements for recurring services, allowing payers, payees, or third-party keepers to trigger validated transactions with pro-rata accrual.

Instructions

Execute a due payment from an on-chain agreement. Anyone can call this — the payer, payee, or a third-party keeper.

Use this when: You are a service provider collecting a recurring payment owed to you, a payer triggering your own agreement manually, or a keeper bot executing due agreements.

Keeper support: When the "account" is a foreign address (not owned by your private key), execution routes through your own account or EOA automatically. No special configuration needed.

The contract validates all conditions on-chain: interval elapsed, active, within caps and limits. Pro-rata accrual means the payout scales with elapsed time (capped at 3x the interval).

Returns: Transaction hash, amount paid, execution count, and next execution time. If the agreement soft-fails (insufficient balance, guardian limit), it returns the failure reason without reverting.

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").
accountYesThe payer smart account whose agreement to execute: Ethereum address, participant name, "me", or "#N".
agreementIdYesThe agreement ID to execute (from azeth_create_payment_agreement or azeth_list_agreements).

Implementation Reference

  • Handler implementation for azeth_execute_agreement. It resolves the payer account, validates agreement executability using pre-flight contract checks, performs optional balance checks, executes the agreement transaction, and parses event logs to return detailed execution results.
      async (args) => {
        let client;
        try {
          client = await createClient(args.chain);
          const chain = resolveChain(args.chain);
    
          // Resolve the payer account
          let accountResolved;
          try {
            accountResolved = await resolveAddress(args.account, client);
          } catch (resolveErr) {
            return handleError(resolveErr);
          }
    
          const account = accountResolved.address;
          const agreementId = BigInt(args.agreementId);
    
          // Pre-flight check: call canExecutePayment before submitting a transaction
          let canExec: { executable: boolean; reason: string };
          try {
            canExec = await client.canExecutePayment(agreementId, account);
          } catch {
            return error('INVALID_INPUT', `Agreement #${args.agreementId} not found for account ${account}.`, 'Check the agreement ID with azeth_list_agreements.');
          }
    
          if (!canExec.executable) {
            // Map reason strings to appropriate error codes and messages
            const reason = canExec.reason.toLowerCase();
            if (reason.includes('not initialized') || reason.includes('not found') || reason.includes('agreement not exists')) {
              return error('INVALID_INPUT', `Agreement #${args.agreementId} not found for account ${account}.`, 'Check the agreement ID with azeth_list_agreements.');
            }
            if (reason.includes('not active')) {
              // Get agreement to provide more context
              try {
                const agreement = await client.getAgreement(agreementId, account);
                const now = BigInt(Math.floor(Date.now() / 1000));
                const status = deriveStatus(agreement, now);
                const decimals = tokenDecimals(agreement.token, chain);
                const totalPaid = formatUnits(agreement.totalPaid, decimals);
                const tokenSymbol = resolveTokenSymbol(agreement.token, chain);
                return error('INVALID_INPUT', `Agreement #${args.agreementId} is ${status}. Total paid: ${totalPaid} ${tokenSymbol}.`);
              } catch {
                return error('INVALID_INPUT', `Agreement #${args.agreementId} is not active.`);
              }
            }
            if (reason.includes('interval not elapsed')) {
              try {
                const nextTime = await client.getNextExecutionTime(agreementId, account);
                const nextDate = new Date(Number(nextTime) * 1000).toISOString();
                const now = Math.floor(Date.now() / 1000);
                const countdown = formatCountdown(Number(nextTime) - now);
                return error('INVALID_INPUT', `Agreement #${args.agreementId} is not due yet. Next execution: ${nextDate} (${countdown}).`);
              } catch {
                return error('INVALID_INPUT', `Agreement #${args.agreementId} is not due yet.`);
              }
            }
            if (reason.includes('max executions')) {
              return error('INVALID_INPUT', `Agreement #${args.agreementId} has reached maximum executions.`);
            }
            if (reason.includes('total cap')) {
              return error('INVALID_INPUT', `Agreement #${args.agreementId} has reached its total payment cap.`);
            }
            if (reason.includes('token not whitelisted')) {
              return error('GUARDIAN_REJECTED', `Agreement #${args.agreementId} cannot execute: token not whitelisted by guardian.`, 'Add the token to the guardian whitelist.');
            }
            if (reason.includes('exceeds max tx') || reason.includes('max tx amount')) {
              return error('GUARDIAN_REJECTED', `Agreement #${args.agreementId} cannot execute: payment exceeds per-transaction limit.`, 'Increase the guardian per-tx limit or reduce the agreement amount.');
            }
            if (reason.includes('daily spend') || reason.includes('daily limit')) {
              return error('GUARDIAN_REJECTED', `Agreement #${args.agreementId} cannot execute: daily spend limit exceeded.`, 'Wait until tomorrow or increase the daily limit via guardian.');
            }
            if (reason.includes('insufficient balance') || reason.includes('balance')) {
              return error('INSUFFICIENT_BALANCE', `Agreement #${args.agreementId} cannot execute: insufficient balance.`, 'Fund the payer account before retrying.');
            }
            // Generic fallback
            return error('INVALID_INPUT', `Agreement #${args.agreementId} cannot execute: ${canExec.reason}.`);
          }
    
          // ── Balance pre-check (non-fatal: proceed if check fails) ──
          const accountAddr = account;
          const agreementIdNum = agreementId;
          try {
            const agreement = await client.getAgreement(agreementIdNum, accountAddr);
            if (agreement) {
              const token = agreement.token;
              const amount = agreement.amount;
    
              if (token && amount) {
                const ETH_ZERO = '0x0000000000000000000000000000000000000000' as `0x${string}`;
                let balance: bigint;
    
                if (token === ETH_ZERO) {
                  balance = await client.publicClient.getBalance({ address: accountAddr });
                } else {
                  const erc20Abi = [{
                    type: 'function' as const,
                    name: 'balanceOf',
                    inputs: [{ name: 'account', type: 'address' }],
                    outputs: [{ name: '', type: 'uint256' }],
                    stateMutability: 'view' as const,
                  }] as const;
                  balance = await client.publicClient.readContract({
                    address: token,
                    abi: erc20Abi,
                    functionName: 'balanceOf',
                    args: [accountAddr],
                  }) as bigint;
                }
    
                if (balance < amount) {
                  const decimals = tokenDecimals(token, chain);
                  return error(
                    'INSUFFICIENT_BALANCE',
                    `Account ${accountAddr} has insufficient balance to execute agreement #${args.agreementId}. ` +
                    `Balance: ${formatUnits(balance, decimals)}, minimum needed: ${formatUnits(amount, decimals)}`,
                    'Deposit more funds into the smart account with azeth_deposit.',
                  );
                }
              }
            }
          } catch {
            // Non-fatal: if balance check fails, proceed and let the contract validate
          }
    
          // Capture pre-execution totalPaid as fallback for delta calculation
          let preExecutionTotal = 0n;
          try {
            const preExecAgreement = await client.getAgreement(agreementId, account);
            preExecutionTotal = preExecAgreement.totalPaid;
          } catch {
            // Non-fatal: delta fallback won't be available
          }
    
          // Execute the agreement
          const txHash = await client.executeAgreement(agreementId, account);
    
          // Primary: parse PaymentExecuted event from receipt for exact amount paid
          let executionAmount = 0n;
          try {
            const receipt = await client.publicClient.waitForTransactionReceipt({ hash: txHash, timeout: 120_000 });
            for (const log of receipt.logs) {
              try {
                const decoded = decodeEventLog({
                  abi: PaymentAgreementModuleAbi,
                  data: log.data,
                  topics: log.topics,
                });
                if (decoded.eventName === 'PaymentExecuted') {
                  const eventArgs = decoded.args as { agreementId: bigint; amount: bigint };
                  if (eventArgs.agreementId === agreementId) {
                    executionAmount = eventArgs.amount;
                    break;
                  }
                }
              } catch {
                // Not a PaymentExecuted event from this ABI — skip
              }
            }
          } catch {
            // Receipt fetch failed — fall back to delta approach below
          }
    
          // Enrich response with post-execution state
          const agreement = await client.getAgreement(agreementId, account);
          const decimals = tokenDecimals(agreement.token, chain);
          const tokenSymbol = resolveTokenSymbol(agreement.token, chain);
          const now = BigInt(Math.floor(Date.now() / 1000));
          const status = deriveStatus(agreement, now);
    
          // Fallback: if event parsing didn't yield an amount, use delta approach
          if (executionAmount === 0n) {
            executionAmount = agreement.totalPaid - preExecutionTotal;
          }
    
          let nextExecutionTime: string;
          let nextExecutionIn: string;
          if (status !== 'active') {
            nextExecutionTime = 'completed';
            nextExecutionIn = 'N/A (completed)';
          } else {
            try {
              const nextTime = await client.getNextExecutionTime(agreementId, account);
              nextExecutionTime = new Date(Number(nextTime) * 1000).toISOString();
              const nowSecs = Math.floor(Date.now() / 1000);
              nextExecutionIn = formatCountdown(Number(nextTime) - nowSecs);
            } catch {
              nextExecutionTime = 'unknown';
              nextExecutionIn = 'unknown';
            }
          }
    
          // Attempt payee name resolution
          const payeeName = await lookupPayeeName(client, agreement.payee);
    
          // USD conversion for stablecoins
          const amountPaidUSD = tokenAmountToUSD(executionAmount, agreement.token, chain);
          const totalPaidUSD = tokenAmountToUSD(agreement.totalPaid, agreement.token, chain);
    
          return success(
            {
              account,
              agreementId: args.agreementId.toString(),
              payee: agreement.payee,
              ...(payeeName ? { payeeName } : {}),
              token: agreement.token,
              tokenSymbol,
              amountPaid: formatUnits(executionAmount, decimals),
              ...(amountPaidUSD ? { amountPaidUSD } : {}),
              executionCount: agreement.executionCount.toString(),
              maxExecutions: agreement.maxExecutions === 0n ? 'unlimited' : agreement.maxExecutions.toString(),
              totalPaid: formatUnits(agreement.totalPaid, decimals),
              ...(totalPaidUSD ? { totalPaidUSD } : {}),
              totalCap: agreement.totalCap === 0n ? 'unlimited' : formatUnits(agreement.totalCap, decimals),
              remainingBudget: agreement.totalCap === 0n
                ? 'unlimited'
                : formatUnits(agreement.totalCap - agreement.totalPaid, decimals),
              nextExecutionTime,
              nextExecutionIn,
              active: agreement.active,
            },
            { txHash },
          );
        } catch (err) {
          if (err instanceof Error && /AA24/.test(err.message)) {
            return guardianRequiredError(
              'Agreement execution exceeds your standard spending limit.',
              { operation: 'execute_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`); }
        }
      },
    );
  • Registration of the 'azeth_execute_agreement' tool, including its description and input schema definition.
    server.registerTool(
      'azeth_execute_agreement',
      {
        description: [
          'Execute a due payment from an on-chain agreement. Anyone can call this — the payer, payee, or a third-party keeper.',
          '',
          'Use this when: You are a service provider collecting a recurring payment owed to you,',
          'a payer triggering your own agreement manually, or a keeper bot executing due agreements.',
          '',
          'Keeper support: When the "account" is a foreign address (not owned by your private key),',
          'execution routes through your own account or EOA automatically. No special configuration needed.',
          '',
          'The contract validates all conditions on-chain: interval elapsed, active, within caps and limits.',
          'Pro-rata accrual means the payout scales with elapsed time (capped at 3x the interval).',
          '',
          'Returns: Transaction hash, amount paid, execution count, and next execution time.',
          'If the agreement soft-fails (insufficient balance, guardian limit), it returns the failure reason without reverting.',
        ].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").'),
          account: z.string().describe('The payer smart account whose agreement to execute: Ethereum address, participant name, "me", or "#N".'),
          agreementId: z.coerce.number().int().min(0).describe('The agreement ID to execute (from azeth_create_payment_agreement or azeth_list_agreements).'),
        }),
      },

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