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

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

With no annotations provided, the description carries full disclosure burden and excels: it validates conditions on-chain (interval, caps), explains pro-rata accrual mechanics (capped at 3x), documents keeper routing logic for foreign accounts, and crucially discloses soft-failure modes (insufficient balance, guardian limits) that return reasons without reverting.

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

Conciseness5/5

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

Perfectly structured with front-loaded purpose, followed by use-case scenarios, keeper technical details, validation logic, and return values. No redundancy; every sentence advances understanding of when/how to use the tool or what happens during execution.

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 high complexity (pro-rata calculations, keeper mechanics, soft-failures) and no output schema, the description is complete: it documents return values (tx hash, amount, execution count, next time) and all behavioral edge cases necessary for safe invocation.

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?

With 100% schema coverage, baseline is 3. The description adds valuable semantic context: 'account' is clarified as potentially 'foreign' (not owned) triggering keeper routing, and 'agreementId' is explicitly linked to sibling tools (azeth_create_payment_agreement, azeth_list_agreements) helping users locate valid values.

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 the specific action 'Execute a due payment from an on-chain agreement' and immediately distinguishes this from sibling tools by clarifying this triggers payment execution versus creation (azeth_create_payment_agreement), cancellation (azeth_cancel_agreement), or querying (azeth_get_due_agreements).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit 'Use this when' guidance covering three distinct roles: service provider collecting payment, payer manually triggering, and keeper bot executing. It further clarifies keeper mechanics for foreign addresses, effectively describing prerequisites and routing behavior.

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