Skip to main content
Send tokens from a user’s modular wallet. The examples below transfer 1 USDC on Arc Testnet, with gas sponsored by Gas Station.
For a complete working app that transfers tokens, see the Circle Smart Account example in the modular wallets web SDK repository.

Before you begin

Before you begin, ensure that you’ve:
  • Created a modular wallet and have access to the smartAccount and bundlerClient from that tutorial.
  • Funded the smart account address with USDC on Arc Testnet from faucet.circle.com.
  • Configured a Gas Station policy in the Circle Console if you plan to sponsor gas on mainnet. Testnet usage is sponsored automatically.
Before shipping to production, also set up passkey recovery so users can restore access if they lose their passkey.

Steps

1

Send the user operation

A user operation packages a contract call, in this case an ERC-20 transfer, for the bundler to submit through the user’s MSCA. Setting paymaster: true (or Paymaster.True() on iOS and Android) sponsors gas through your Gas Station policy.Testnet usage is sponsored automatically. For mainnet, configure a policy in the Circle Console first.
import {
  encodeTransfer,
  ContractAddress,
} from "@circle-fin/modular-wallets-core";
import { parseUnits } from "viem";

const recipient = "0x..."; // recipient address

const { to, data } = encodeTransfer(
  recipient,
  ContractAddress.ArcTestnet_USDC,
  parseUnits("1", 6), // 1 USDC, 6 decimals
);

const userOpHash = await bundlerClient.sendUserOperation({
  // calls is an array. Pass multiple entries to batch them atomically.
  calls: [{ to, data }],
  paymaster: true,
});
import CircleModularWalletsCore

Task {
    do {
        let recipient = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

        let result = Utils.encodeTransfer(
            to: recipient,
            token: ArcTestnetToken.USDC.name,
            amount: BigInt(1_000_000) // 1 USDC, 6 decimals
        )

        let userOpHash = try await bundlerClient.sendUserOperation(
            account: smartAccount,
            calls: [
                EncodeCallDataArg(
                    to: result.to,
                    value: BigInt(0),
                    data: result.data
                )
            ],
            paymaster: Paymaster.True()
        )
    } catch {
        print(error)
    }
}
CoroutineScope(Dispatchers.IO).launch {
    try {
        val recipient = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

        val result = encodeTransfer(
            recipient,
            Token.ArcTestnet_USDC.name,
            parseUnits("1", 6), // 1 USDC, 6 decimals
        )

        val userOpHash = bundlerClient.sendUserOperation(
            context,
            smartAccount,
            arrayOf(
                EncodeCallDataArg(
                    to = result.to,
                    value = BigInteger.ZERO,
                    data = result.data,
                ),
            ),
            paymaster = Paymaster.True(),
        )
    } catch (e: Exception) {
        e.printStackTrace()
    }
}
try {
    String recipient = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

    EncodeTransferResult result = encodeTransfer(
        recipient,
        Token.ArcTestnet_USDC.name(),
        parseUnits("1", 6) // 1 USDC, 6 decimals
    );

    EncodeCallDataArg transferCall = new EncodeCallDataArg(
        result.getTo(),
        BigInteger.ZERO,
        result.getData()
    );

    CompletableFuture<String> userOpHashFuture = new CompletableFuture<>();
    bundlerClient.sendUserOperation(
        context,
        smartAccount,
        new EncodeCallDataArg[] { transferCall },
        new UserOperationV07(),
        new Paymaster.True(),
        new CustomContinuation<>(userOpHashFuture)
    );
    String userOpHash = userOpHashFuture.join();
} catch (Exception e) {
    e.printStackTrace();
}
sendUserOperation returns a userOpHash, a 66-character hex string that identifies the user operation.encodeTransfer supports the tokens listed in the ContractAddress enum, including USDC. See the full list in the Web, iOS, or Android SDK reference.
For tokens not listed in ContractAddress, or when batching multiple contract calls in a single user operation, build calls manually. Each entry is a { to, data } pair executed by the MSCA. See Batch and parallel user operations for the pattern.
2

Wait for the receipt

Poll for the receipt to confirm the transfer landed onchain.
const { receipt } = await bundlerClient.waitForUserOperationReceipt({
  hash: userOpHash,
});

console.log("Transaction hash:", receipt.transactionHash);
        let receipt = try await bundlerClient.waitForUserOperationReceipt(
            userOpHash: userOpHash
        )

        print("Transaction hash: \(receipt.transactionHash)")
        val receipt = bundlerClient.waitForUserOperationReceipt(userOpHash)

        println("Transaction hash: ${receipt.transactionHash}")
    CompletableFuture<UserOperationReceipt> receiptFuture = new CompletableFuture<>();
    bundlerClient.waitForUserOperationReceipt(
        userOpHash,
        new CustomContinuation<>(receiptFuture)
    );
    UserOperationReceipt receipt = receiptFuture.join();

    System.out.println("Transaction hash: " + receipt.getTransactionHash());
The receipt contains transactionHash, the onchain transaction hash. Look it up on the block explorer to confirm the transfer landed.
waitForUserOperationReceipt polls locally. To be notified of transfer activity on the wallet asynchronously, including inbound transfers from external senders, set up a webhook endpoint.
You’ve now transferred 1 USDC on behalf of your user, with gas paid by your paymaster policy.