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
| 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"). | |
| payee | Yes | Recipient: Ethereum address, participant name, "me", or "#N" (account index). | |
| token | Yes | Payment token address. Use an ERC-20 contract address (e.g., USDC) or 0x0000000000000000000000000000000000000000 for native ETH. | |
| amount | Yes | Payment amount per interval in human-readable units (e.g., "10.00" for 10 USDC). | |
| intervalSeconds | Yes | Time between payments in seconds (minimum 60). E.g., 86400 for daily, 604800 for weekly. | |
| maxExecutions | No | Maximum number of payments. 0 or omit for unlimited. | |
| decimals | No | Token decimals. Defaults to 6 (USDC). Use 18 for WETH or native ETH. |
Implementation Reference
- src/tools/payments.ts:551-678 (handler)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`); } } }, ); - src/tools/payments.ts:525-550 (registration)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.'), }), },