# Control USDT0

> Transfer and swap USDT0 from a Flare personal account using fee-only 0xFE custom instructions with Viem.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

Source: https://dev.flare.network/smart-accounts/guides/typescript-viem/control-usdt0-ts

This guide shows how to control [USDT0](https://docs.usdt0.to/) held by a Flare [personal account](/smart-accounts/overview) through an XRPL wallet. We will read the account's USDT0 balance, transfer USDT0 to another EVM address, and swap USDT0 for FXRP on [SparkDEX](https://sparkdex.ai/) — all through fee-only [`0xFE` custom instructions](/smart-accounts/custom-instruction).

Unlike the minting demos in the [Custom Instruction TypeScript guide](/smart-accounts/guides/typescript-viem/custom-instruction-ts), these flows set `netMintAmountXrp: 0`. The XRPL payment covers only direct minting and executor fees; no FXRP is minted. The personal account already holds USDT0 (and native FLR for any call values), and the user operation moves that ERC-20 balance.

Read the [Custom Instruction TypeScript guide](/smart-accounts/guides/typescript-viem/custom-instruction-ts) first if you are not familiar with the `0xFE` hash-commitment flow, `sendHashInstruction`, or [`executeDirectMintingWithData`](/fassets/reference/IAssetManager#executedirectmintingwithdata).

The full code is available on [GitHub](https://github.com/flare-foundation/flare-viem-starter/tree/main/src/usdt0).

info

The code in this guide is set up for the Coston2 testnet. Despite that, we refer to the network as Flare and its currency as FLR, rather than Coston2 and C2FLR.

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

Before running `transfer.ts` or `swap-usdt0-to-fxrp.ts`:

1.  Configure `.env` in [flare-viem-starter](https://github.com/flare-foundation/flare-viem-starter) with `XRPL_TESTNET_RPC_URL`, `XRPL_SEED`, and `PRIVATE_KEY` (executor wallet for the Flare submission step).
2.  Derive your personal account address with the [State Lookup guide](/smart-accounts/guides/typescript-viem/state-lookup-ts#personal-account-of-an-xrpl-address) (or the read-only `balance.ts` script below).
3.  Fund that EVM address with FLR and USDT0 from the [Coston2 faucet](https://faucet.flare.network/coston2).

The XRPL wallet must hold enough XRP to cover the fee-only payment amount returned by `computeDirectMintingPaymentAmountXrp({ netMintAmountXrp: 0 })`.

## Addresses and Config[​](#addresses-and-config "Direct link to Addresses and Config")

Constants live in the `src/usdt0/config.ts` file.

On Coston2, the scripts use:

Constant

Value

Notes

`USDT0_ADDRESS`

[`0xC1A5B41512496B80903D1f32d6dEa3a73212E71F`](https://coston2-explorer.flare.network/address/0xC1A5B41512496B80903D1f32d6dEa3a73212E71F)

Coston2 USDT0. Mainnet is [`0xe7cd86e13AC4309349F30B3435a9d337750fC82D`](https://flare-explorer.flare.network/address/0xe7cd86e13AC4309349F30B3435a9d337750fC82D).

`SWAP_ROUTER_ADDRESS`

[`0x8a1E35F5c98C4E85B36B7B253222eE17773b2781`](https://coston2-explorer.flare.network/address/0x8a1E35F5c98C4E85B36B7B253222eE17773b2781?tab=contract)

SparkDEX Uniswap V3 SwapRouter

`POOL_FEE`

`500`

0.05% fee tier for the USDT0/FXRP pool

`DEFAULT_AMOUNT_IN_UNITS`

`1`

Transfer / swap size in whole USDT0 units

`DEFAULT_AMOUNT_OUT_MINIMUM_UNITS`

`"0.3"`

Demo slippage floor in whole FXRP units — tune against the live pool before larger amounts

FXRP is resolved at runtime with `getFxrpAddress()` from the [Flare Contract Registry](/network/guides/flare-contracts-registry), not hardcoded.

The EOA-oriented SparkDEX pattern (without smart accounts) is documented in [Swap USDT0 to FXRP](/fxrp/token-interactions/usdt0-fxrp-swap).

<details>
<summary>src/usdt0/config.ts</summary>

src/usdt0/config.ts

src/usdt0/config.ts

```
import type { Address } from "viem";/** USDT0 on Coston2. Mainnet: 0xe7cd86e13AC4309349F30B3435a9d337750fC82D */export const USDT0_ADDRESS =  "0xC1A5B41512496B80903D1f32d6dEa3a73212E71F" as Address;/** SparkDEX Uniswap V3 SwapRouter. See https://dev.flare.network/fxrp/token-interactions/usdt0-fxrp-swap */export const SWAP_ROUTER_ADDRESS =  "0x8a1E35F5c98C4E85B36B7B253222eE17773b2781" as Address;/** USDT0/FXRP pool fee tier (0.05%). */export const POOL_FEE = 500;/** Default swap / transfer size in whole USDT0 units (scaled by on-chain decimals). */export const DEFAULT_AMOUNT_IN_UNITS = 1;/** * Minimum FXRP out in whole FXRP units. Conservative floor for demo swaps — * tune against current pool price before running on mainnet-sized amounts. */export const DEFAULT_AMOUNT_OUT_MINIMUM_UNITS = "0.3";/** Swap deadline offset from now (seconds). */export const SWAP_DEADLINE_SECONDS = 20 * 60;/** Hardcoded EVM recipient for the transfer demo. */export const DEFAULT_TRANSFER_RECIPIENT =  "0x1cdacde0c68e0a508ae85279375070a88554871b" as Address;
```

</details>

## Flow Overview[​](#flow-overview "Direct link to Flow Overview")

Both `transfer.ts` and `swap-usdt0-to-fxrp.ts` follow the same three-step `0xFE` protocol used elsewhere in the starter:

1.  **User side** — encode the `Call[]`, commit `keccak256(PackedUserOperation)` in a 42-byte XRPL memo, send a fee-only Payment.
2.  **Executor side** — fetch an FDC Payment proof and call [`executeDirectMintingWithData`](/fassets/reference/IAssetManager#executedirectmintingwithdata) with the UserOp bytes.
3.  **Confirmation** — read `UserOperationExecuted` from the receipt and compare balances.

## Check Balance[​](#check-balance "Direct link to Check Balance")

The `balance.ts` script is read-only. It derives the personal account from the `XRPL_SEED` environment variable, then prints USDT0 `decimals`, `balanceOf`, and `allowance` to the SparkDEX SwapRouter.

```
const personalAccount = await getPersonalAccountAddress(xrplWallet.address);const decimals = await readUsdt0Decimals();const [balance, allowance] = await Promise.all([  readUsdt0Balance(personalAccount),  readUsdt0Allowance(personalAccount, SWAP_ROUTER_ADDRESS),]);
```

Helpers in `src/usdt0/utils.ts` wrap Viem `readContract` calls against the USDT0 ERC-20 ABI.

Run:

```
pnpm run script src/usdt0/balance.ts
```

If the balance is zero, faucet USDT0 to the printed personal account address before continuing.

## Transfer USDT0[​](#transfer-usdt0 "Direct link to Transfer USDT0")

The `transfer.ts` script builds a single-call UserOp that invokes `USDT0.transfer(recipient, amount)` from the personal account.

The payment amount is fee-only:

```
const [personalAccount, memoOnlyAmountXrp, decimals] = await Promise.all([  getPersonalAccountAddress(xrplWallet.address),  computeDirectMintingPaymentAmountXrp({ netMintAmountXrp: 0 }),  readUsdt0Decimals(),]);const amount = toTokenAmount(DEFAULT_AMOUNT_IN_UNITS, decimals);
```

The call batch is one ERC-20 transfer:

```
const customInstruction: Call[] = [  {    target: USDT0_ADDRESS,    value: 0n,    data: encodeFunctionData({      abi: ERC20Abi,      functionName: "transfer",      args: [recipient, amount],    }),  },];
```

Then the script runs the three `0xFE` steps inline:

```
const userSide = await sendHashInstruction({  label: "transfer-usdt0",  customInstruction,  amountXrp: memoOnlyAmountXrp,  personalAccount,  xrplClient,  xrplWallet,});const { receipt } = await executeDirectMintingWithData({  xrplTransactionHash: userSide.xrplTransactionHash,  data: userSide.data,  value: userSide.totalCallValue,  xrplClient,  label: "transfer-usdt0",});const event = findUserOperationExecuted(  receipt,  personalAccount,  userSide.nonce,);
```

The `DEFAULT_TRANSFER_RECIPIENT` constant in `config.ts` is a hardcoded demo address — change it before sending real funds.

No destination tags

XRPL payments targeting smart accounts must not use a destination tag.

Run:

```
pnpm run script src/usdt0/transfer.ts
```

## Swap USDT0 to FXRP[​](#swap-usdt0-to-fxrp "Direct link to Swap USDT0 to FXRP")

The `swap-usdt0-to-fxrp.ts` script swaps USDT0 for FXRP on SparkDEX from the personal account. The XRPL payment is again fee-only; the FXRP received comes from the DEX pool, not from an FAssets mint.

The UserOp batches two calls atomically:

1.  `USDT0.approve(SwapRouter, amountIn)`
2.  `SwapRouter.exactInputSingle` with `tokenIn = USDT0`, `tokenOut = FXRP`, `recipient = personalAccount`

```
const customInstruction: Call[] = [  {    target: USDT0_ADDRESS,    value: 0n,    data: encodeFunctionData({      abi: ERC20Abi,      functionName: "approve",      args: [SWAP_ROUTER_ADDRESS, amountIn],    }),  },  {    target: SWAP_ROUTER_ADDRESS,    value: 0n,    data: encodeFunctionData({      abi: swapRouterAbi,      functionName: "exactInputSingle",      args: [        {          tokenIn: USDT0_ADDRESS,          tokenOut: fxrpAddress,          fee: POOL_FEE,          recipient: personalAccount,          deadline,          amountIn,          amountOutMinimum,          sqrtPriceLimitX96: 0n,        },      ],    }),  },];
```

The `amountOutMinimum` value is derived from `DEFAULT_AMOUNT_OUT_MINIMUM_UNITS` (`0.3` FXRP in the demo). Treat it as a conservative floor for testnet demos; adjust it against the current pool price for larger amounts.

After execution, the script logs USDT0 spent and FXRP received by comparing balances before and after.

Run:

```
pnpm run script src/usdt0/swap-usdt0-to-fxrp.ts
```

## Full Scripts[​](#full-scripts "Direct link to Full Scripts")

The repository with the examples is available on [GitHub](https://github.com/flare-foundation/flare-viem-starter). Shared helpers (`sendHashInstruction`, `executeDirectMintingWithData`, FDC proof helpers) live under `src/utils`.

<details>
<summary>src/usdt0/balance.ts</summary>

src/usdt0/balance.ts

src/usdt0/balance.ts

```
import { formatUnits } from "viem";import { Wallet } from "xrpl";import { getPersonalAccountAddress } from "../utils/smart-accounts";import { SWAP_ROUTER_ADDRESS, USDT0_ADDRESS } from "./config";import {  readUsdt0Allowance,  readUsdt0Balance,  readUsdt0Decimals,} from "./utils";// Read-only: prints the personal account's USDT0 balance and allowance to the// SparkDEX SwapRouter. Faucet USDT0 to the personal account EVM address via// https://faucet.flare.network/coston2 if the balance is zero.async function main() {  const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!);  const personalAccount = await getPersonalAccountAddress(xrplWallet.address);  const decimals = await readUsdt0Decimals();  console.log("Personal account address:", personalAccount, "\n");  console.log("USDT0 address:", USDT0_ADDRESS, "\n");  console.log("USDT0 decimals:", decimals, "\n");  console.log("Spender (SwapRouter):", SWAP_ROUTER_ADDRESS, "\n");  const [balance, allowance] = await Promise.all([    readUsdt0Balance(personalAccount),    readUsdt0Allowance(personalAccount, SWAP_ROUTER_ADDRESS),  ]);  console.log("USDT0 balance:", formatUnits(balance, decimals), "\n");  console.log(    "USDT0 allowance (→ SwapRouter):",    formatUnits(allowance, decimals),    "\n",  );}void main()  .then(() => process.exit(0))  .catch((error) => {    console.error(error);    process.exit(1);  });
```

</details>
<details>
<summary>src/usdt0/transfer.ts</summary>

src/usdt0/transfer.ts

src/usdt0/transfer.ts

```
import { encodeFunctionData, formatUnits } from "viem";import { Client, Wallet } from "xrpl";import { abi as ERC20Abi } from "../abis/ERC20";import { computeDirectMintingPaymentAmountXrp } from "../utils/fassets";import {  executeDirectMintingWithData,  findUserOperationExecuted,  getPersonalAccountAddress,  sendHashInstruction,  type Call,} from "../utils/smart-accounts";import {  DEFAULT_AMOUNT_IN_UNITS,  DEFAULT_TRANSFER_RECIPIENT,  USDT0_ADDRESS,} from "./config";import { readUsdt0Balance, readUsdt0Decimals, toTokenAmount } from "./utils";// NOTE: For this example to work, faucet C2FLR and USDT0 to your personal// account address (https://faucet.flare.network/coston2). Use balance.ts or// state-lookup.ts to print the personal account EVM address.//// Transfers USDT0 from the personal account via a fee-only 0xFE UserOp// (no FXRP mint). 0xFE is a three-step protocol; this script runs all three// steps inline.async function main() {  const recipient = DEFAULT_TRANSFER_RECIPIENT;  const xrplClient = new Client(process.env.XRPL_TESTNET_RPC_URL!);  const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!);  const [personalAccount, memoOnlyAmountXrp, decimals] = await Promise.all([    getPersonalAccountAddress(xrplWallet.address),    computeDirectMintingPaymentAmountXrp({ netMintAmountXrp: 0 }),    readUsdt0Decimals(),  ]);  const amount = toTokenAmount(DEFAULT_AMOUNT_IN_UNITS, decimals);  console.log("Personal account address:", personalAccount, "\n");  console.log("USDT0 address:", USDT0_ADDRESS, "\n");  console.log("USDT0 decimals:", decimals, "\n");  console.log("Recipient:", recipient, "\n");  console.log("Memo-only amount (XRP, fees only):", memoOnlyAmountXrp, "\n");  console.log("Transfer amount:", formatUnits(amount, decimals), "USDT0\n");  const [usdt0Before, recipientBefore] = await Promise.all([    readUsdt0Balance(personalAccount),    readUsdt0Balance(recipient),  ]);  console.log(    "Personal USDT0 before:",    formatUnits(usdt0Before, decimals),    "\n",  );  console.log(    "Recipient USDT0 before:",    formatUnits(recipientBefore, decimals),    "\n",  );  if (usdt0Before < amount) {    throw new Error(      `Insufficient USDT0 on ${personalAccount}: have ${formatUnits(usdt0Before, decimals)}, need ${formatUnits(amount, decimals)}`,    );  }  const customInstruction: Call[] = [    {      target: USDT0_ADDRESS,      value: 0n,      data: encodeFunctionData({        abi: ERC20Abi,        functionName: "transfer",        args: [recipient, amount],      }),    },  ];  // --- 1. USER SIDE -------------------------------------------------------  const userSide = await sendHashInstruction({    label: "transfer-usdt0",    customInstruction,    amountXrp: memoOnlyAmountXrp,    personalAccount,    xrplClient,    xrplWallet,  });  // --- 2. EXECUTOR SIDE ---------------------------------------------------  const { receipt } = await executeDirectMintingWithData({    xrplTransactionHash: userSide.xrplTransactionHash,    data: userSide.data,    value: userSide.totalCallValue,    xrplClient,    label: "transfer-usdt0",  });  // --- 3. CONFIRMATION ----------------------------------------------------  const event = findUserOperationExecuted(    receipt,    personalAccount,    userSide.nonce,  );  console.log("UserOperationExecuted:", event, "\n");  const [usdt0After, recipientAfter] = await Promise.all([    readUsdt0Balance(personalAccount),    readUsdt0Balance(recipient),  ]);  console.log("Personal USDT0 after:", formatUnits(usdt0After, decimals), "\n");  console.log(    "Recipient USDT0 after:",    formatUnits(recipientAfter, decimals),    "\n",  );  console.log(    "USDT0 sent:",    formatUnits(usdt0Before - usdt0After, decimals),    "\n",  );}void main()  .then(() => process.exit(0))  .catch((error) => {    console.error(error);    process.exit(1);  });
```

</details>
<details>
<summary>src/usdt0/swap-usdt0-to-fxrp.ts</summary>

src/usdt0/swap-usdt0-to-fxrp.ts

src/usdt0/swap-usdt0-to-fxrp.ts

```
import { encodeFunctionData, formatUnits } from "viem";import { Client, Wallet } from "xrpl";import { abi as ERC20Abi } from "../abis/ERC20";import { abi as swapRouterAbi } from "../abis/ISwapRouter";import {  computeDirectMintingPaymentAmountXrp,  getFxrpBalance,  getFxrpDecimals,} from "../utils/fassets";import { getFxrpAddress } from "../utils/flare-contract-registry";import {  executeDirectMintingWithData,  findUserOperationExecuted,  getPersonalAccountAddress,  sendHashInstruction,  type Call,} from "../utils/smart-accounts";import {  DEFAULT_AMOUNT_IN_UNITS,  DEFAULT_AMOUNT_OUT_MINIMUM_UNITS,  POOL_FEE,  SWAP_DEADLINE_SECONDS,  SWAP_ROUTER_ADDRESS,  USDT0_ADDRESS,} from "./config";import { readUsdt0Balance, readUsdt0Decimals, toTokenAmount } from "./utils";// NOTE: For this example to work, faucet C2FLR and USDT0 to your personal// account address (https://faucet.flare.network/coston2). Use balance.ts or// state-lookup.ts to print the personal account EVM address.//// Swaps USDT0 to FXRP via SparkDEX's Uniswap V3 router. The XRPL payment// carries only direct-minting fees (no FXRP mint); the swap itself is a// batched 0xFE UserOp: USDT0.approve(router) then router.exactInputSingle.// Both calls run atomically from the personal account.//// 0xFE is a three-step protocol; this script runs all three steps inline.async function main() {  const deadline = BigInt(    Math.floor(Date.now() / 1000) + SWAP_DEADLINE_SECONDS,  );  const xrplClient = new Client(process.env.XRPL_TESTNET_RPC_URL!);  const xrplWallet = Wallet.fromSeed(process.env.XRPL_SEED!);  const [    personalAccount,    fxrpAddress,    memoOnlyAmountXrp,    usdt0Decimals,    fxrpDecimals,  ] = await Promise.all([    getPersonalAccountAddress(xrplWallet.address),    getFxrpAddress(),    computeDirectMintingPaymentAmountXrp({ netMintAmountXrp: 0 }),    readUsdt0Decimals(),    getFxrpDecimals(),  ]);  const amountIn = toTokenAmount(DEFAULT_AMOUNT_IN_UNITS, usdt0Decimals);  const amountOutMinimum = toTokenAmount(    DEFAULT_AMOUNT_OUT_MINIMUM_UNITS,    fxrpDecimals,  );  console.log("Personal account address:", personalAccount, "\n");  console.log("FXRP address:", fxrpAddress, "\n");  console.log("USDT0 address:", USDT0_ADDRESS, "\n");  console.log("USDT0 decimals:", usdt0Decimals, "\n");  console.log("FXRP decimals:", fxrpDecimals, "\n");  console.log("Swap router:", SWAP_ROUTER_ADDRESS, "\n");  console.log("Memo-only amount (XRP, fees only):", memoOnlyAmountXrp, "\n");  console.log("Amount in:", formatUnits(amountIn, usdt0Decimals), "USDT0");  console.log(    "Amount out minimum:",    formatUnits(amountOutMinimum, fxrpDecimals),    "FXRP\n",  );  const [usdt0Before, fxrpBefore] = await Promise.all([    readUsdt0Balance(personalAccount),    getFxrpBalance(personalAccount),  ]);  console.log("USDT0 before:", formatUnits(usdt0Before, usdt0Decimals), "\n");  console.log("FXRP before:", formatUnits(fxrpBefore, fxrpDecimals), "\n");  if (usdt0Before < amountIn) {    throw new Error(      `Insufficient USDT0 on ${personalAccount}: have ${formatUnits(usdt0Before, usdt0Decimals)}, need ${formatUnits(amountIn, usdt0Decimals)}`,    );  }  const customInstruction: Call[] = [    {      target: USDT0_ADDRESS,      value: 0n,      data: encodeFunctionData({        abi: ERC20Abi,        functionName: "approve",        args: [SWAP_ROUTER_ADDRESS, amountIn],      }),    },    {      target: SWAP_ROUTER_ADDRESS,      value: 0n,      data: encodeFunctionData({        abi: swapRouterAbi,        functionName: "exactInputSingle",        args: [          {            tokenIn: USDT0_ADDRESS,            tokenOut: fxrpAddress,            fee: POOL_FEE,            recipient: personalAccount,            deadline,            amountIn,            amountOutMinimum,            sqrtPriceLimitX96: 0n,          },        ],      }),    },  ];  // --- 1. USER SIDE -------------------------------------------------------  const userSide = await sendHashInstruction({    label: "swap-usdt0-to-fxrp",    customInstruction,    amountXrp: memoOnlyAmountXrp,    personalAccount,    xrplClient,    xrplWallet,  });  // --- 2. EXECUTOR SIDE ---------------------------------------------------  const { receipt } = await executeDirectMintingWithData({    xrplTransactionHash: userSide.xrplTransactionHash,    data: userSide.data,    value: userSide.totalCallValue,    xrplClient,    label: "swap-usdt0-to-fxrp",  });  // --- 3. CONFIRMATION ----------------------------------------------------  const event = findUserOperationExecuted(    receipt,    personalAccount,    userSide.nonce,  );  console.log("UserOperationExecuted:", event, "\n");  const [usdt0After, fxrpAfter] = await Promise.all([    readUsdt0Balance(personalAccount),    getFxrpBalance(personalAccount),  ]);  console.log("USDT0 after:", formatUnits(usdt0After, usdt0Decimals), "\n");  console.log("FXRP after:", formatUnits(fxrpAfter, fxrpDecimals), "\n");  console.log(    "USDT0 spent:",    formatUnits(usdt0Before - usdt0After, usdt0Decimals),    "\n",  );  console.log(    "FXRP received:",    formatUnits(fxrpAfter - fxrpBefore, fxrpDecimals),    "\n",  );}void main()  .then(() => process.exit(0))  .catch((error) => {    console.error(error);    process.exit(1);  });
```

</details>

## Expected output[​](#expected-output "Direct link to Expected output")

Values will differ slightly, but the output should follow the same format. Exact fee amounts depend on live `AssetManagerFXRP` direct-minting parameters.

### `balance.ts`[​](#balancets "Direct link to balancets")

```
> pnpm run script src/usdt0/balance.tsPersonal account address: 0xFd2f0eb6b9fA4FE5bb1F7B26fEE3c647ed103d9FUSDT0 address: 0xC1A5B41512496B80903D1f32d6dEa3a73212E71FUSDT0 decimals: 6Spender (SwapRouter): 0x8a1E35F5c98C4E85B36B7B253222eE17773b2781USDT0 balance: 10USDT0 allowance (→ SwapRouter): 0
```

### `transfer.ts`[​](#transferts "Direct link to transferts")

```
> pnpm run script src/usdt0/transfer.tsPersonal account address: 0xFd2f0eb6b9fA4FE5bb1F7B26fEE3c647ed103d9FUSDT0 address: 0xC1A5B41512496B80903D1f32d6dEa3a73212E71FUSDT0 decimals: 6Recipient: 0x1cdacde0c68e0a508ae85279375070a88554871bMemo-only amount (XRP, fees only): 0.2Transfer amount: 1 USDT0Personal USDT0 before: 10Recipient USDT0 before: 0UserOperationExecuted: { ... }Personal USDT0 after: 9Recipient USDT0 after: 1USDT0 sent: 1
```

### `swap-usdt0-to-fxrp.ts`[​](#swap-usdt0-to-fxrpts "Direct link to swap-usdt0-to-fxrpts")

```
> pnpm run script src/usdt0/swap-usdt0-to-fxrp.tsPersonal account address: 0xFd2f0eb6b9fA4FE5bb1F7B26fEE3c647ed103d9FFXRP address: 0x0b6A3645c240605887a5532109323A3E12273dc7USDT0 address: 0xC1A5B41512496B80903D1f32d6dEa3a73212E71FUSDT0 decimals: 6FXRP decimals: 6Swap router: 0x8a1E35F5c98C4E85B36B7B253222eE17773b2781Memo-only amount (XRP, fees only): 0.2Amount in: 1 USDT0Amount out minimum: 0.3 FXRPUSDT0 before: 9FXRP before: 0UserOperationExecuted: { ... }USDT0 after: 8FXRP after: 0.48USDT0 spent: 1FXRP received: 0.48
```

What's next

-   Re-read the [Custom Instruction](/smart-accounts/custom-instruction) protocol details and failure handling.
-   Swap as an EOA (without smart accounts) in [Swap USDT0 to FXRP](/fxrp/token-interactions/usdt0-fxrp-swap).
-   Compare with [gasless USD₮0 transfers](/network/guides/gasless-usdt0-transfers) if you need EIP-3009 meta-transactions instead of XRPL memo authorization.
