Skip to main content
Glama

azeth_transfer

Transfer ETH or ERC-20 tokens from your Azeth smart account to another address using Ethereum addresses, participant names, or account indices for payments, funding, or moving assets.

Instructions

Send ETH or ERC-20 tokens FROM your Azeth smart account to another address.

Use this when: You need to pay another participant, fund an account, or move tokens between addresses.

The "to" field accepts: an Ethereum address, a participant name (resolved via trust registry), "me" (your first smart account), or "#N" (Nth account index from azeth_accounts).

IMPORTANT: This sends FROM your smart account, not your EOA. Ensure your smart account is funded. Use azeth_deposit first to fund your smart account if needed. One EOA can own multiple smart accounts — specify which one, or defaults to first.

Returns: Transaction hash, sender smart account address, recipient address (with resolution info), and amount sent.

Note: This is a state-changing operation. The tool shows the resolved address before executing. For ETH transfers, omit the token parameter. For ERC-20 tokens, provide the token contract address AND decimals. The amount is in human-readable units (e.g., "1.5" for 1.5 ETH or "100" for 100 USDC). The sender account is determined by the AZETH_PRIVATE_KEY environment variable.

Example: { "to": "Alice", "amount": "0.001" } or { "to": "0x1234...abcd", "amount": "10", "token": "0x036C...CF7e", "decimals": 6 }

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").
toYesRecipient: Ethereum address, participant name, "me", or "#N" (account index).
amountYesAmount to send in human-readable units (e.g., "1.5" for 1.5 ETH, "100" for 100 USDC).
tokenNoERC-20 token contract address. Omit for native ETH transfer.
decimalsNoToken decimals for ERC-20 transfers. REQUIRED when token is specified. Use 6 for USDC, 18 for WETH.
smartAccountNoSmart account to transfer from: address, name, or "#N". If omitted, uses your first smart account.

Implementation Reference

  • The handler function for azeth_transfer, which resolves addresses, validates balances, executes the transfer, and handles post-transaction receipt processing.
      async (args) => {
        if (args.token && !validateAddress(args.token)) {
          return error('INVALID_INPUT', `Invalid token address: "${args.token}".`, 'Must be 0x-prefixed followed by 40 hex characters.');
        }
        if (args.token && args.decimals === undefined) {
          return error('INVALID_INPUT', 'decimals is required when token address is provided.', 'Use 6 for USDC, 18 for WETH.');
        }
    
        let client;
        try {
          client = await createClient(args.chain);
    
          // Resolve "to": address, name, "me", "#N"
          let toResolved;
          try {
            toResolved = await resolveAddress(args.to, client, 'account');
          } catch (resolveErr) {
            return handleError(resolveErr);
          }
    
          // Resolve smartAccount: address, name, "#N"
          let fromAccount: `0x${string}` | undefined;
          if (args.smartAccount) {
            try {
              fromAccount = await resolveSmartAccount(args.smartAccount, client);
            } catch (resolveErr) {
              return handleError(resolveErr);
            }
          }
    
          const tokenAddress = args.token as `0x${string}` | undefined;
          const decimals = args.decimals ?? 18;
          let amount: bigint;
          try {
            amount = tokenAddress
              ? parseUnits(args.amount, decimals)
              : parseEther(args.amount);
          } catch {
            return error('INVALID_INPUT', 'Invalid amount format — must be a valid decimal number');
          }
    
          // Pre-flight: check balance before submitting UserOp
          try {
            const senderAccount = fromAccount ?? await client.resolveSmartAccount();
            if (tokenAddress) {
              // Direct ERC-20 balanceOf call — getBalance() keys by symbol, not address
              const available = await client.publicClient.readContract({
                address: tokenAddress,
                abi: erc20Abi,
                functionName: 'balanceOf',
                args: [senderAccount],
              });
              if (available < amount) {
                const { formatUnits } = await import('viem');
                return error(
                  'INSUFFICIENT_BALANCE',
                  `Insufficient token balance: have ${formatUnits(available, decimals)}, need ${args.amount}.`,
                  `Fund your smart account (${senderAccount}) before retrying.`,
                );
              }
            } else {
              const balance = await client.getBalance(senderAccount);
              if (balance.eth < amount) {
                return error(
                  'INSUFFICIENT_BALANCE',
                  `Insufficient ETH balance: have ${formatEther(balance.eth)} ETH, need ${args.amount} ETH.`,
                  `Fund your smart account (${fromAccount ?? senderAccount}) before retrying.`,
                );
              }
            }
          } catch {
            // Balance check is best-effort; proceed and let the bundler return details on failure
          }
    
          const result = await client.transfer(
            { to: toResolved.address, amount, token: tokenAddress },
            fromAccount,
          );
    
          // Enrich response with transaction receipt data (gas, events)
          let receiptData: Record<string, unknown> = {};
          try {
            const receipt = await client.publicClient.getTransactionReceipt({ hash: result.txHash as `0x${string}` });
            const gasUsed = receipt.gasUsed;
            const effectiveGasPrice = receipt.effectiveGasPrice;
            const gasCostWei = gasUsed * effectiveGasPrice;
            const { formatTokenAmount } = await import('@azeth/common');
    
            // Decode known events from logs
            const events: Array<{ name: string; args: Record<string, string> }> = [];
            try {
              const { decodeEventLog } = await import('viem');
              const { GuardianModuleAbi, ReputationModuleAbi: RepAbi } = await import('@azeth/common/abis');
              const knownAbis = [GuardianModuleAbi, RepAbi];
              for (const log of receipt.logs) {
                for (const abi of knownAbis) {
                  try {
                    const decoded = decodeEventLog({ abi, data: log.data, topics: log.topics });
                    const stringArgs: Record<string, string> = {};
                    for (const [k, v] of Object.entries(decoded.args as Record<string, unknown>)) {
                      stringArgs[k] = typeof v === 'bigint' ? v.toString() : String(v);
                    }
                    events.push({ name: decoded.eventName, args: stringArgs });
                    break; // matched this log
                  } catch {
                    // This ABI doesn't match this log — try next
                  }
                }
              }
            } catch {
              // Event decoding failure is non-fatal
            }
    
            receiptData = {
              gasUsed: gasUsed.toString(),
              gasCostETH: formatTokenAmount(gasCostWei, 18, 8),
              ...(events.length > 0 ? { events } : {}),
            };
          } catch {
            // Receipt fetch failure is non-fatal
          }
    
          return success(
            {
              txHash: result.txHash,
              from: result.from,
              to: result.to,
              amount: args.amount,
              token: result.token,
              ...(toResolved.resolvedFrom ? {
                resolvedTo: `"${toResolved.resolvedFrom}" → ${toResolved.address}`,
                ...(toResolved.name ? { resolvedName: toResolved.name } : {}),
                ...(toResolved.tokenId ? { resolvedTokenId: toResolved.tokenId } : {}),
              } : {}),
              ...receiptData,
            },
            { txHash: result.txHash },
          );
        } catch (err) {
          if (err instanceof Error && /AA24/.test(err.message)) {
            return guardianRequiredError(
              'Transfer amount exceeds your standard spending limit.',
              {
                operation: 'transfer',
                amount: `${args.amount} ${args.token ? 'tokens' : 'ETH'}`,
                limit: 'Check with azeth_get_guardrails',
              },
            );
          }
          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`); }
        }
      },
    );
  • Input schema validation for azeth_transfer using Zod.
    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").'),
      to: z.string().describe('Recipient: Ethereum address, participant name, "me", or "#N" (account index).'),
      amount: z.string().describe('Amount to send in human-readable units (e.g., "1.5" for 1.5 ETH, "100" for 100 USDC).'),
      token: z.string().optional().describe('ERC-20 token contract address. Omit for native ETH transfer.'),
      decimals: z.coerce.number().int().min(0).max(18).optional().describe('Token decimals for ERC-20 transfers. REQUIRED when token is specified. Use 6 for USDC, 18 for WETH.'),
      smartAccount: z.string().optional().describe('Smart account to transfer from: address, name, or "#N". If omitted, uses your first smart account.'),
    }),
  • Tool registration for azeth_transfer.
    server.registerTool(
      'azeth_transfer',
      {
        description: [
          'Send ETH or ERC-20 tokens FROM your Azeth smart account to another address.',
          '',
          'Use this when: You need to pay another participant, fund an account, or move tokens between addresses.',
          '',
          'The "to" field accepts: an Ethereum address, a participant name (resolved via trust registry),',
          '"me" (your first smart account), or "#N" (Nth account index from azeth_accounts).',
          '',
          'IMPORTANT: This sends FROM your smart account, not your EOA. Ensure your smart account is funded.',
          'Use azeth_deposit first to fund your smart account if needed.',
          'One EOA can own multiple smart accounts — specify which one, or defaults to first.',
          '',
          'Returns: Transaction hash, sender smart account address, recipient address (with resolution info), and amount sent.',
          '',
          'Note: This is a state-changing operation. The tool shows the resolved address before executing.',
          'For ETH transfers, omit the token parameter. For ERC-20 tokens, provide the token contract address AND decimals.',
          'The amount is in human-readable units (e.g., "1.5" for 1.5 ETH or "100" for 100 USDC).',
          'The sender account is determined by the AZETH_PRIVATE_KEY environment variable.',
          '',
          'Example: { "to": "Alice", "amount": "0.001" } or { "to": "0x1234...abcd", "amount": "10", "token": "0x036C...CF7e", "decimals": 6 }',
        ].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").'),
          to: z.string().describe('Recipient: Ethereum address, participant name, "me", or "#N" (account index).'),
          amount: z.string().describe('Amount to send in human-readable units (e.g., "1.5" for 1.5 ETH, "100" for 100 USDC).'),
          token: z.string().optional().describe('ERC-20 token contract address. Omit for native ETH transfer.'),
          decimals: z.coerce.number().int().min(0).max(18).optional().describe('Token decimals for ERC-20 transfers. REQUIRED when token is specified. Use 6 for USDC, 18 for WETH.'),
          smartAccount: z.string().optional().describe('Smart account to transfer from: address, name, or "#N". If omitted, uses your first smart account.'),
        }),
      },
Behavior5/5

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

No annotations provided, so description carries full burden. Explicitly states 'This is a state-changing operation,' discloses return values (tx hash, sender/recipient addresses, amount), notes that resolved address is shown before executing, and explains sender determination via AZETH_PRIVATE_KEY env var.

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

Conciseness4/5

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

Lengthy but well-structured with clear logical sections: purpose, usage conditions, input formats, warnings, returns, and examples. Every sentence conveys necessary information for safe blockchain operations, though slightly verbose.

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?

Excellent coverage for complex tool with no output schema and 6 parameters. Documents return values, prerequisites, environment variable dependencies (AZETH_PRIVATE_KEY, AZETH_CHAIN), and safety guardrails (showing resolved address pre-execution). Complete despite lack of structured annotations.

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% coverage (baseline 3). Description adds significant value: concrete JSON examples, explains conditional logic (omit token for ETH, required for ERC-20), clarifies relationship between token and decimals parameters, and documents valid 'to' field formats (address, name, 'me', '#N').

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?

Clear specific verb ('Send') and resource ('ETH or ERC-20 tokens'). Explicitly distinguishes from siblings by specifying it sends FROM smart account (not EOA), contrasting with azeth_deposit (funding) and referencing azeth_accounts for the #N syntax.

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?

Explicit 'Use this when' section lists specific scenarios (pay participant, fund account, move tokens). Clear prerequisite guidance: 'Use azeth_deposit first to fund your smart account if needed.' Also warns about EOA vs smart account distinction.

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