Skip to main content
Glama

azeth_cancel_agreement

Cancel active payment agreements to stop recurring subscriptions or data feeds. Only the payer can cancel agreements immediately without penalties or refunds.

Instructions

Cancel an active payment agreement. Only the payer (agreement creator) can cancel.

Use this when: You want to stop a recurring payment subscription or data feed. Cancellation is immediate — no timelock, no penalty. Already-paid amounts are not refunded.

Returns: Transaction hash and final agreement state (total paid, execution count).

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").
agreementIdYesThe agreement ID to cancel.
smartAccountNoYOUR smart account that owns the agreement: address or "#N". Only your own accounts can be cancelled. Defaults to first smart account.

Implementation Reference

  • Implementation of the azeth_cancel_agreement tool handler, which validates the agreement status, performs the cancellation transaction, and returns the final state.
    server.registerTool(
      'azeth_cancel_agreement',
      {
        description: [
          'Cancel an active payment agreement. Only the payer (agreement creator) can cancel.',
          '',
          'Use this when: You want to stop a recurring payment subscription or data feed.',
          'Cancellation is immediate — no timelock, no penalty. Already-paid amounts are not refunded.',
          '',
          'Returns: Transaction hash and final agreement state (total paid, execution count).',
        ].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").'),
          agreementId: z.coerce.number().int().min(0).describe('The agreement ID to cancel.'),
          smartAccount: z.string().optional().describe('YOUR smart account that owns the agreement: address or "#N". Only your own accounts can be cancelled. Defaults to first smart account.'),
        }),
      },
      async (args) => {
        let client;
        try {
          client = await createClient(args.chain);
          const chain = resolveChain(args.chain);
    
          // Resolve to the caller's OWN smart account (not arbitrary addresses).
          // Only the payer can cancel their own agreements — resolveSmartAccount
          // restricts resolution to accounts owned by the caller's private key.
          let account: `0x${string}`;
          if (args.smartAccount) {
            try {
              const resolved = await resolveSmartAccount(args.smartAccount, client);
              if (!resolved) {
                account = await client.resolveSmartAccount();
              } else {
                account = resolved;
              }
            } catch (resolveErr) {
              return handleError(resolveErr);
            }
          } else {
            account = await client.resolveSmartAccount();
          }
    
          const agreementId = BigInt(args.agreementId);
    
          // Pre-flight: check agreement exists and is active
          let agreement;
          try {
            agreement = await client.getAgreement(agreementId, account);
          } catch {
            return error('INVALID_INPUT', `Agreement #${args.agreementId} not found for account ${account}.`, 'Check the agreement ID with azeth_list_agreements.');
          }
    
          // Zero payee means no agreement exists at this ID for this account
          if (agreement.payee === '0x0000000000000000000000000000000000000000') {
            return error(
              'AGREEMENT_NOT_FOUND',
              `Agreement #${args.agreementId} not found for your account ${account}.`,
              'The agreement may belong to a different account. Use azeth_list_agreements to see your agreements.',
            );
          }
    
          const now = BigInt(Math.floor(Date.now() / 1000));
          const status = deriveStatus(agreement, now);
          if (status !== 'active') {
            const decimals = tokenDecimals(agreement.token, chain);
            const tokenSymbol = resolveTokenSymbol(agreement.token, chain);
            return error('INVALID_INPUT', `Agreement #${args.agreementId} is already ${status}. Total paid: ${formatUnits(agreement.totalPaid, decimals)} ${tokenSymbol}.`);
          }
    
          // Cancel
          const txHash = await client.cancelAgreement(agreementId, account);
    
          // Get final state
          const finalAgreement = await client.getAgreement(agreementId, account);
          const decimals = tokenDecimals(finalAgreement.token, chain);
          const tokenSymbol = resolveTokenSymbol(finalAgreement.token, chain);
    
          return success(
            {
              agreementId: args.agreementId.toString(),
              status: 'cancelled',
              payee: finalAgreement.payee,
              token: finalAgreement.token,
              tokenSymbol,
              totalPaid: formatUnits(finalAgreement.totalPaid, decimals),
              executionCount: finalAgreement.executionCount.toString(),
              maxExecutions: finalAgreement.maxExecutions === 0n ? 'unlimited' : finalAgreement.maxExecutions.toString(),
            },
            { txHash },
          );
        } catch (err) {
          if (err instanceof Error && /AA24/.test(err.message)) {
            return guardianRequiredError(
              'Agreement cancellation requires guardian co-signature.',
              { operation: 'cancel_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`); }
        }
      },
    );
Behavior5/5

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

No annotations provided, so description carries full burden. Discloses critical behavioral traits: immediate effect ('no timelock'), financial consequences ('no penalty', 'not refunded'), authorization rules ('Only the payer'), and exact return value structure ('Transaction hash and final agreement state').

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?

Four sentences with zero waste. Front-loaded with action, followed by usage condition, behavioral warnings, and return specification. Clear logical flow from intent to execution to consequence.

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?

Complete coverage for a 3-parameter destructive operation. Compensates for missing output schema by documenting return values. Covers authorization, side effects, and financial implications necessary for safe blockchain operation 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?

Schema has 100% description coverage establishing baseline 3. Description adds semantic context about ownership ('Only the payer... can cancel') which reinforces the smartAccount parameter constraints, justifying the bump above baseline.

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 ('Cancel an active payment agreement') and explicitly distinguishes from sibling operations like create_payment_agreement or execute_agreement by stating it stops 'recurring payment subscription or data feed' and that 'Only the payer... can cancel'.

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?

Contains explicit 'Use this when:' clause defining the trigger condition (stopping subscriptions/feeds). Also includes prerequisite/authorization constraint ('Only the payer... can cancel') that guides agent away from using this tool for agreements the user doesn't own.

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