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
| Name | Required | Description | Default |
|---|---|---|---|
| chain | No | 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 | Yes | The agreement ID to cancel. | |
| smartAccount | No | YOUR smart account that owns the agreement: address or "#N". Only your own accounts can be cancelled. Defaults to first smart account. |
Implementation Reference
- src/tools/agreements.ts:416-518 (handler)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`); } } }, );