Skip to content

Support onchain transactions with your agent built with XMTP

This package provides an XMTP content type to support sending transactions to a wallet for execution.

The wallet send calls content type is built into the Agent SDK. No installation is required.

How token transactions work

Before diving into the code, it helps to understand how token transactions work under the hood.

Tokens on EVM-compatible chains follow the ERC-20 standard. Every token lives as its own smart contract on a specific chain. To work with a token, you'll need three pieces of information:

  1. The chain your token lives on (e.g., Base)
  2. The token's contract address on that chain (you can look these up on token trackers like Basescan Tokens)
  3. The token's decimal places which tell you how to convert between raw values and human-readable amounts

Take USDC on Base as an example: its contract address is 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 and it uses 6 decimals. So when you see a raw value of 1000000, that's actually 1 USDC.

The ERC-20 ABI gives you a few essential methods to interact with any token:

  • balanceOf to check how many tokens a given address holds
  • decimals to find out how many decimal places it uses
  • transfer to move tokens from one address to another

The good news is that the XMTP Agent SDK handles most of this for you, so you don't need to deal with raw contract calls yourself.

Set up your token configuration

All the examples below share the same setup. You define the chain you're working with, the token contract address, and query the token's decimals:

import { formatUnits, hexToNumber, parseUnits } from 'viem';
import { base } from 'viem/chains';
import {
  Agent,
  createERC20TransferCalls,
  getERC20Balance,
  getERC20Decimals,
  validHex,
} from '@xmtp/agent-sdk';
 
const CHAIN = base;
const USDC_TOKEN_CONTRACT =
  '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const;
const USDC_DECIMALS = await getERC20Decimals({
  chain: CHAIN,
  tokenAddress: USDC_TOKEN_CONTRACT,
});

Check a token balance

Use getERC20Balance to look up how many tokens an address holds. Here's an example that checks the sender's USDC balance:

const senderAddress = await ctx.getSenderAddress();
const senderBalance = await getERC20Balance({
  chain: CHAIN,
  tokenAddress: USDC_TOKEN_CONTRACT,
  address: validHex(senderAddress),
});
await ctx.conversation.sendText(
  `Your USDC balance is: ${formatUnits(senderBalance, USDC_DECIMALS)}`
);

The formatUnits utility from viem converts the raw token amount into a human-readable value.

Create a transaction request

Use createERC20TransferCalls to build a transaction request for an ERC-20 token transfer:

const senderAddress = await ctx.getSenderAddress();
const receiverAddress = agent.address;
const amount = parseUnits('0.1', USDC_DECIMALS);
 
const walletSendCalls = createERC20TransferCalls({
  chain: CHAIN,
  tokenAddress: USDC_TOKEN_CONTRACT,
  from: validHex(senderAddress),
  to: validHex(receiverAddress),
  amount,
  description: `Transfer "0.1 USDC" on chain "${CHAIN.name}".`,
});

The parseUnits function does the opposite of formatUnits: it converts a human-readable amount (like "0.1") into the raw token value (100000) based on the token's decimals.

Send a transaction request

Once you've built the transaction request, send it to the conversation. The receiving wallet will be prompted to approve and execute it:

await ctx.conversation.sendWalletSendCalls(walletSendCalls);

Handle transaction confirmations

After a transaction has been confirmed onchain, you'll receive a transaction-reference event. You can use it to notify the user with a link to the block explorer:

agent.on('transaction-reference', async (ctx) => {
  const { networkId, reference } = ctx.message.content;
  const networkIdDecimal = hexToNumber(validHex(networkId));
  await ctx.conversation.sendMarkdown(
    `Transaction confirmed!\n` +
      `Chain ID: [${networkIdDecimal}](https://chainlist.org/chain/${networkIdDecimal})\n` +
      `Transaction Hash: [${reference}](https://basescan.org/tx/${reference})`
  );
});

You are welcome to provide feedback on this implementation by commenting on XIP-59: Trigger on-chain calls via wallet_sendCalls.