> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arc.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send USDC with a transaction memo

> Attach memo metadata to a USDC transfer on Arc Testnet and query the emitted Memo events.

Use the `Memo` contract to add a memo to a USDC transfer on Arc Testnet. This
tutorial shows the full flow with viem, ethers, Python, and curl. You will
encode the inner USDC transfer, submit it with `Memo.memo(...)`, decode the
receipt, and query past memo events by `memoId`. To learn how the `Memo`
contract preserves your wallet as the sender and orders its events, see
[Transaction memos](/arc/concepts/transaction-memos).

## Prerequisites

Before you begin, ensure that you've:

* Installed [Node.js v22+](https://nodejs.org/) for the TypeScript examples, or
  Python 3.10+ for the Python example.
* Created an [Arc Testnet wallet](/arc/references/connect-to-arc) controlled by
  an externally owned account (EOA). Smart contract wallets aren't supported.
  See [Wallet types](/arc/concepts/transaction-memos#wallet-types) for details.
* Funded the wallet with testnet USDC from the
  [Circle Faucet](https://faucet.circle.com/).
* Chosen a recipient address on Arc Testnet.

## Step 1. Set up the project

Create a new project and install the dependencies for the client library you
want to use:

<CodeGroup>
  ```bash Node.js theme={null}
  mkdir arc-transaction-memo
  cd arc-transaction-memo
  npm init -y
  npm install dotenv ethers tsx typescript viem
  ```

  ```bash Python theme={null}
  mkdir arc-transaction-memo
  cd arc-transaction-memo
  python3 -m venv .venv
  source .venv/bin/activate
  pip install web3 python-dotenv
  ```
</CodeGroup>

Create an `.env` file:

```bash Shell theme={null}
touch .env
```

Add your configuration:

```text .env theme={null}
PRIVATE_KEY=YOUR_PRIVATE_KEY
RECIPIENT_ADDRESS=RECIPIENT_ADDRESS
RPC_URL=https://rpc.testnet.arc.network
```

Replace `YOUR_PRIVATE_KEY` with the `0x`-prefixed private key for the funded
wallet. Replace `RECIPIENT_ADDRESS` with the wallet that should receive USDC.

## Step 2. Review the contract address and ABI

Arc Testnet uses the following predeployed transaction memo contracts:

| Contract | Address                                                                                                                        |
| :------- | :----------------------------------------------------------------------------------------------------------------------------- |
| `Memo`   | [`0x5294E9927c3306DcBaDb03fe70b92e01cCede505`](https://testnet.arcscan.app/address/0x5294E9927c3306DcBaDb03fe70b92e01cCede505) |
| `USDC`   | [`0x3600000000000000000000000000000000000000`](https://testnet.arcscan.app/address/0x3600000000000000000000000000000000000000) |

For the full address list, see
[contract addresses](/arc/references/contract-addresses#transaction-extensions).

Create `memo-abi.json` in your project:

```json memo-abi.json theme={null}
[
  {
    "type": "function",
    "name": "memo",
    "stateMutability": "nonpayable",
    "inputs": [
      { "name": "target", "type": "address" },
      { "name": "data", "type": "bytes" },
      { "name": "memoId", "type": "bytes32" },
      { "name": "memoData", "type": "bytes" }
    ],
    "outputs": []
  },
  {
    "type": "event",
    "name": "BeforeMemo",
    "anonymous": false,
    "inputs": [{ "name": "memoIndex", "type": "uint256", "indexed": true }]
  },
  {
    "type": "event",
    "name": "Memo",
    "anonymous": false,
    "inputs": [
      { "name": "sender", "type": "address", "indexed": true },
      { "name": "target", "type": "address", "indexed": true },
      { "name": "callDataHash", "type": "bytes32", "indexed": false },
      { "name": "memoId", "type": "bytes32", "indexed": true },
      { "name": "memo", "type": "bytes", "indexed": false },
      { "name": "memoIndex", "type": "uint256", "indexed": false }
    ]
  }
]
```

The script you build in the next steps loads this ABI file from the project
root.

## Step 3. Configure the client connection

Create the script file for your client library. The first chunk reads the wallet
and recipient configuration from `.env`, sets the contract addresses, loads the
`Memo` ABI, and creates the clients that sign and send requests.

<Tabs>
  <Tab title="Viem">
    The example imports the `arcTestnet` chain definition from `viem/chains`, which
    requires viem v2.38 or later.

    Create `viem-memo.ts`:

    ```typescript TypeScript theme={null}
    import "dotenv/config";
    import { readFileSync } from "node:fs";
    import {
      type Abi,
      type Address,
      createPublicClient,
      createWalletClient,
      encodeFunctionData,
      erc20Abi,
      getAddress,
      http,
      keccak256,
      parseAbiItem,
      parseEventLogs,
      parseUnits,
      stringToHex,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    const rpcUrl = process.env.RPC_URL ?? "https://rpc.testnet.arc.network";
    const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
    const recipient = getAddress(process.env.RECIPIENT_ADDRESS as Address);

    const memoAddress = "0x5294E9927c3306DcBaDb03fe70b92e01cCede505";
    const usdcAddress = "0x3600000000000000000000000000000000000000";
    const memoAbi = JSON.parse(readFileSync("memo-abi.json", "utf8")) as Abi;

    const account = privateKeyToAccount(privateKey);
    const publicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(rpcUrl),
    });
    const walletClient = createWalletClient({
      account,
      chain: arcTestnet,
      transport: http(rpcUrl),
    });
    ```

    The public client reads chain state, and the wallet client signs and submits
    transactions from your funded account.
  </Tab>

  <Tab title="Ethers">
    Create `ethers-memo.ts`:

    ```typescript TypeScript theme={null}
    import "dotenv/config";
    import { readFileSync } from "node:fs";
    import { ethers } from "ethers";

    const rpcUrl = process.env.RPC_URL ?? "https://rpc.testnet.arc.network";
    const privateKey = process.env.PRIVATE_KEY as string;
    const recipient = ethers.getAddress(process.env.RECIPIENT_ADDRESS as string);

    const memoAddress = "0x5294E9927c3306DcBaDb03fe70b92e01cCede505";
    const usdcAddress = "0x3600000000000000000000000000000000000000";
    const explorerUrl = "https://testnet.arcscan.app";

    const memoAbi = JSON.parse(readFileSync("memo-abi.json", "utf8"));
    const erc20Abi = [
      "function transfer(address to, uint256 amount) returns (bool)",
    ];

    const provider = new ethers.JsonRpcProvider(rpcUrl, {
      chainId: 5042002,
      name: "arc-testnet",
    });
    const wallet = new ethers.Wallet(privateKey, provider);
    const memoInterface = new ethers.Interface(memoAbi);
    const erc20Interface = new ethers.Interface(erc20Abi);
    ```

    The provider reads chain state, the wallet signs transactions, and the two
    interfaces encode and decode `Memo` and ERC-20 calldata.
  </Tab>

  <Tab title="Python">
    Create `python-memo.py`:

    ```python Python theme={null}
    import json
    import os

    from dotenv import load_dotenv
    from web3 import Web3
    from web3.logs import DISCARD

    load_dotenv()

    rpc_url = os.getenv("RPC_URL", "https://rpc.testnet.arc.network")
    private_key = os.environ["PRIVATE_KEY"]
    recipient = Web3.to_checksum_address(os.environ["RECIPIENT_ADDRESS"])

    memo_address = Web3.to_checksum_address("0x5294E9927c3306DcBaDb03fe70b92e01cCede505")
    usdc_address = Web3.to_checksum_address("0x3600000000000000000000000000000000000000")
    explorer_url = "https://testnet.arcscan.app"

    with open("memo-abi.json", encoding="utf-8") as abi_file:
        memo_abi = json.load(abi_file)
    erc20_abi = [
        {
            "type": "function",
            "name": "transfer",
            "stateMutability": "nonpayable",
            "inputs": [
                {"name": "to", "type": "address"},
                {"name": "amount", "type": "uint256"},
            ],
            "outputs": [{"name": "", "type": "bool"}],
        },
    ]

    w3 = Web3(Web3.HTTPProvider(rpc_url))
    account = w3.eth.account.from_key(private_key)
    memo = w3.eth.contract(address=memo_address, abi=memo_abi)
    usdc = w3.eth.contract(address=usdc_address, abi=erc20_abi)
    ```

    The `Web3` instance connects to the RPC endpoint, and the two contract objects
    encode and decode `Memo` and ERC-20 calldata.
  </Tab>
</Tabs>

## Step 4. Encode the transfer and memo values

Add the values that define the memo transfer. The encoded ERC-20 `transfer` call
becomes the inner call that `Memo.memo(...)` forwards to USDC. The `memoId` is a
32-byte identifier your application uses to look the memo up later, and the memo
bytes carry the metadata itself.

<Tabs>
  <Tab title="Viem">
    Append to `viem-memo.ts`:

    ```typescript TypeScript theme={null}
    const transferData = encodeFunctionData({
      abi: erc20Abi,
      functionName: "transfer",
      args: [recipient, parseUnits("1", 6)],
    });
    const callDataHash = keccak256(transferData);
    const memoId = keccak256(stringToHex("invoice-2026-0001"));
    const memoBytes = stringToHex("order=2026-0001");
    ```
  </Tab>

  <Tab title="Ethers">
    Append to `ethers-memo.ts`:

    ```typescript TypeScript theme={null}
    const transferData = erc20Interface.encodeFunctionData("transfer", [
      recipient,
      ethers.parseUnits("1", 6),
    ]);
    const callDataHash = ethers.keccak256(transferData);
    const memoId = ethers.id("invoice-2026-0001");
    const memoBytes = ethers.toUtf8Bytes("order=2026-0001");
    ```
  </Tab>

  <Tab title="Python">
    Append to `python-memo.py`:

    ```python Python theme={null}
    transfer_data = usdc.functions.transfer(recipient, 1_000_000)._encode_transaction_data()
    transfer_bytes = bytes.fromhex(transfer_data[2:])
    call_data_hash = Web3.keccak(hexstr=transfer_data)
    memo_id = Web3.keccak(text="invoice-2026-0001")
    memo_bytes = b"order=2026-0001"
    ```
  </Tab>
</Tabs>

The transfer amount is `1` USDC, expressed with six decimals. The script also
hashes the transfer calldata so a later step can match it against the
`callDataHash` field of the emitted `Memo` event.

## Step 5. Confirm the `Memo` contract is deployed

Before you send the transaction, check that the `Memo` address has deployed
bytecode. If `eth_getCode` returns `0x`, the contract is not available on the
RPC endpoint you are using, and the script stops with an error.

<Tabs>
  <Tab title="Viem">
    Append to `viem-memo.ts`:

    ```typescript TypeScript theme={null}
    const memoCode = await publicClient.getCode({ address: memoAddress });
    if (!memoCode || memoCode === "0x") {
      throw new Error(`Memo contract is not deployed at ${memoAddress}`);
    }
    ```
  </Tab>

  <Tab title="Ethers">
    Append to `ethers-memo.ts`:

    ```typescript TypeScript theme={null}
    const memoCode = await provider.getCode(memoAddress);
    if (memoCode === "0x") {
      throw new Error(`Memo contract is not deployed at ${memoAddress}`);
    }
    ```
  </Tab>

  <Tab title="Python">
    Append to `python-memo.py`:

    ```python Python theme={null}
    memo_code = w3.eth.get_code(memo_address)
    if memo_code == b"":
        raise RuntimeError(f"Memo contract is not deployed at {memo_address}")
    ```
  </Tab>
</Tabs>

## Step 6. Send the memo transaction

Call `Memo.memo(...)` with the four arguments from Step 4: the USDC contract as
the target, the encoded transfer calldata, the `memoId`, and the memo bytes.
Then wait for the receipt and confirm the transaction succeeded.

<Tabs>
  <Tab title="Viem">
    Append to `viem-memo.ts`:

    ```typescript TypeScript theme={null}
    const hash = await walletClient.writeContract({
      address: memoAddress,
      abi: memoAbi,
      functionName: "memo",
      args: [usdcAddress, transferData, memoId, memoBytes],
    });

    const receipt = await publicClient.waitForTransactionReceipt({ hash });
    if (receipt.status !== "success") {
      throw new Error(`Memo transaction reverted: ${hash}`);
    }
    ```
  </Tab>

  <Tab title="Ethers">
    Append to `ethers-memo.ts`:

    ```typescript TypeScript theme={null}
    const tx = await wallet.sendTransaction({
      to: memoAddress,
      data: memoInterface.encodeFunctionData("memo", [
        usdcAddress,
        transferData,
        memoId,
        memoBytes,
      ]),
    });

    const receipt = await tx.wait();
    if (!receipt) {
      throw new Error("Transaction was not mined");
    }
    if (receipt.status !== 1) {
      throw new Error(`Memo transaction reverted: ${tx.hash}`);
    }

    console.log("Transaction:", `${explorerUrl}/tx/${tx.hash}`);
    console.log("Block:", receipt.blockNumber);
    ```
  </Tab>

  <Tab title="Python">
    web3.py builds the transaction explicitly, so this chunk also sets the nonce and
    the EIP-1559 fee fields before signing and sending:

    ```python Python theme={null}
    latest_block = w3.eth.get_block("latest")
    priority_fee = w3.to_wei(1, "gwei")
    base_fee = latest_block.get("baseFeePerGas", w3.eth.gas_price)

    transaction = memo.functions.memo(
        usdc_address,
        transfer_bytes,
        memo_id,
        memo_bytes,
    ).build_transaction(
        {
            "from": account.address,
            "nonce": w3.eth.get_transaction_count(account.address),
            "chainId": w3.eth.chain_id,
            "maxPriorityFeePerGas": priority_fee,
            "maxFeePerGas": (base_fee * 2) + priority_fee,
        }
    )

    signed = account.sign_transaction(transaction)
    raw_transaction = getattr(signed, "raw_transaction", None) or signed.rawTransaction
    tx_hash = w3.eth.send_raw_transaction(raw_transaction)
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    if receipt["status"] != 1:
        raise RuntimeError(f"Memo transaction reverted: {Web3.to_hex(tx_hash)}")

    print("Transaction:", f"{explorer_url}/tx/{Web3.to_hex(tx_hash)}")
    print("Block:", receipt["blockNumber"])
    ```
  </Tab>
</Tabs>

## Step 7. Decode and verify the memo events

A successful memo transfer emits one `BeforeMemo` event and one `Memo` event
alongside the USDC `Transfer`. Decode the receipt logs and verify that the
`Memo` event carries the sender, target, calldata hash, `memoId`, and memo bytes
the script sent.

<Tabs>
  <Tab title="Viem">
    Append to `viem-memo.ts`:

    ```typescript TypeScript theme={null}
    const events = parseEventLogs({
      abi: memoAbi,
      logs: receipt.logs,
    });

    const beforeMemoEvents = events.filter(
      (event) => event.eventName === "BeforeMemo",
    );
    const memoEvents = events.filter((event) => event.eventName === "Memo");
    if (beforeMemoEvents.length !== 1 || memoEvents.length !== 1) {
      throw new Error("Expected exactly one BeforeMemo event and one Memo event");
    }

    const memoArgs = memoEvents[0].args as {
      sender: Address;
      target: Address;
      callDataHash: `0x${string}`;
      memoId: `0x${string}`;
      memo: `0x${string}`;
      memoIndex: bigint;
    };
    if (getAddress(memoArgs.sender) !== account.address) {
      throw new Error(`Unexpected memo sender: ${memoArgs.sender}`);
    }
    if (getAddress(memoArgs.target) !== getAddress(usdcAddress)) {
      throw new Error(`Unexpected memo target: ${memoArgs.target}`);
    }
    if (memoArgs.callDataHash !== callDataHash) {
      throw new Error(`Unexpected callDataHash: ${memoArgs.callDataHash}`);
    }
    if (memoArgs.memoId !== memoId || memoArgs.memo !== memoBytes) {
      throw new Error("Memo event did not include the expected memoId and memo");
    }

    console.log(
      "Transaction:",
      `${arcTestnet.blockExplorers.default.url}/tx/${hash}`,
    );
    console.log("Block:", receipt.blockNumber.toString());
    console.log("Decoded memo events:", events);
    ```
  </Tab>

  <Tab title="Ethers">
    Append to `ethers-memo.ts`:

    ```typescript TypeScript theme={null}
    const beforeMemoEvents: ethers.LogDescription[] = [];
    const memoEvents: ethers.LogDescription[] = [];
    for (const log of receipt.logs) {
      if (log.address.toLowerCase() !== memoAddress.toLowerCase()) continue;

      const parsed = memoInterface.parseLog(log);
      if (!parsed) continue;

      if (parsed.name === "BeforeMemo") beforeMemoEvents.push(parsed);
      if (parsed.name === "Memo") memoEvents.push(parsed);
      console.log(parsed.name, parsed.args);
    }
    if (beforeMemoEvents.length !== 1 || memoEvents.length !== 1) {
      throw new Error("Expected exactly one BeforeMemo event and one Memo event");
    }

    const memoArgs = memoEvents[0].args;
    if (memoArgs.sender.toLowerCase() !== wallet.address.toLowerCase()) {
      throw new Error(`Unexpected memo sender: ${memoArgs.sender}`);
    }
    if (memoArgs.target.toLowerCase() !== usdcAddress.toLowerCase()) {
      throw new Error(`Unexpected memo target: ${memoArgs.target}`);
    }
    if (memoArgs.callDataHash !== callDataHash) {
      throw new Error(`Unexpected callDataHash: ${memoArgs.callDataHash}`);
    }
    if (
      memoArgs.memoId !== memoId ||
      ethers.hexlify(memoArgs.memo) !== ethers.hexlify(memoBytes)
    ) {
      throw new Error("Memo event did not include the expected memoId and memo");
    }
    ```
  </Tab>

  <Tab title="Python">
    Append to `python-memo.py`:

    ```python Python theme={null}
    before_memo_events = memo.events.BeforeMemo().process_receipt(receipt, errors=DISCARD)
    memo_events = memo.events.Memo().process_receipt(receipt, errors=DISCARD)
    if len(before_memo_events) != 1 or len(memo_events) != 1:
        raise RuntimeError("Expected exactly one BeforeMemo event and one Memo event")

    memo_args = memo_events[0]["args"]
    if Web3.to_checksum_address(memo_args["sender"]) != account.address:
        raise RuntimeError(f"Unexpected memo sender: {memo_args['sender']}")
    if Web3.to_checksum_address(memo_args["target"]) != usdc_address:
        raise RuntimeError(f"Unexpected memo target: {memo_args['target']}")
    if memo_args["callDataHash"] != call_data_hash:
        raise RuntimeError(f"Unexpected callDataHash: {memo_args['callDataHash'].hex()}")
    if memo_args["memoId"] != memo_id or memo_args["memo"] != memo_bytes:
        raise RuntimeError("Memo event did not include the expected memoId and memo")

    print("BeforeMemo:", dict(before_memo_events[0]["args"]))
    print("Memo:", dict(memo_args))
    ```
  </Tab>
</Tabs>

## Step 8. Query memo events by `memoId`

`memoId` is an indexed event field, so you can find the memo again later without
the transaction hash. Query the `Memo` logs for the `memoId` the script sent and
confirm exactly one match.

<Tabs>
  <Tab title="Viem">
    Append to `viem-memo.ts`:

    ```typescript TypeScript theme={null}
    const memoEvent = parseAbiItem(
      "event Memo(address indexed sender,address indexed target,bytes32 callDataHash,bytes32 indexed memoId,bytes memo,uint256 memoIndex)",
    );
    const matchingLogs = await publicClient.getLogs({
      address: memoAddress,
      event: memoEvent,
      args: { memoId },
      fromBlock: receipt.blockNumber,
      toBlock: receipt.blockNumber,
    });
    if (matchingLogs.length !== 1) {
      throw new Error(
        `Expected one Memo log for memoId, found ${matchingLogs.length}`,
      );
    }

    console.log("Memo events matching memoId:", matchingLogs);
    ```
  </Tab>

  <Tab title="Ethers">
    Append to `ethers-memo.ts`:

    ```typescript TypeScript theme={null}
    const memoTopic = memoInterface.getEvent("Memo")?.topicHash;
    if (!memoTopic) {
      throw new Error("Memo event topic not found");
    }

    const matchingLogs = await provider.getLogs({
      address: memoAddress,
      topics: [memoTopic, null, null, memoId],
      fromBlock: receipt.blockNumber,
      toBlock: receipt.blockNumber,
    });
    if (matchingLogs.length !== 1) {
      throw new Error(
        `Expected one Memo log for memoId, found ${matchingLogs.length}`,
      );
    }

    console.log(
      "Memo events matching memoId:",
      matchingLogs.map((log) => memoInterface.parseLog(log)),
    );
    ```
  </Tab>

  <Tab title="Python">
    Append to `python-memo.py`:

    ```python Python theme={null}
    matching_logs = memo.events.Memo().get_logs(
        argument_filters={"memoId": memo_id},
        from_block=receipt["blockNumber"],
        to_block=receipt["blockNumber"],
    )
    if len(matching_logs) != 1:
        raise RuntimeError(f"Expected one Memo log for memoId, found {len(matching_logs)}")

    print("Memo events matching memoId:", matching_logs)
    ```
  </Tab>
</Tabs>

## Step 9. Run the script

Run the completed script for your client library:

<CodeGroup>
  ```bash Viem theme={null}
  npx tsx --env-file=.env viem-memo.ts
  ```

  ```bash Ethers theme={null}
  npx tsx --env-file=.env ethers-memo.ts
  ```

  ```bash Python theme={null}
  python python-memo.py
  ```
</CodeGroup>

The exact formatting differs by client library, but successful output includes
the transaction URL, block number, decoded `BeforeMemo` and `Memo` event data,
and one historical log match for the same `memoId`:

```text theme={null}
Transaction: https://testnet.arcscan.app/tx/0x...
Block: 123456
BeforeMemo: { memoIndex: 42 }
Memo: {
  sender: "0xYourWallet...",
  target: "0x3600000000000000000000000000000000000000",
  callDataHash: "0x...",
  memoId: "0x...",
  memo: "0x...",
  memoIndex: 42
}
Memo events matching memoId: [ ... ]
```

## Step 10. Check JSON-RPC directly with curl

Use curl for read-only JSON-RPC checks, such as verifying deployed bytecode,
reading a transaction receipt, or querying `Memo` logs. Use a client library
such as viem, ethers, or web3.py to sign and submit the transaction itself.

<CodeGroup>
  ```bash eth_getCode theme={null}
  MEMO_ADDRESS=0x5294E9927c3306DcBaDb03fe70b92e01cCede505
  RPC_URL=https://rpc.testnet.arc.network

  curl --request POST "$RPC_URL" \
    --header "content-type: application/json" \
    --data '{
      "jsonrpc": "2.0",
      "method": "eth_getCode",
      "params": ["'"$MEMO_ADDRESS"'", "latest"],
      "id": 1
    }'
  ```

  ```bash eth_getTransactionReceipt theme={null}
  TRANSACTION_HASH=0xYOUR_TRANSACTION_HASH
  RPC_URL=https://rpc.testnet.arc.network

  curl --request POST "$RPC_URL" \
    --header "content-type: application/json" \
    --data '{
      "jsonrpc": "2.0",
      "method": "eth_getTransactionReceipt",
      "params": ["'"$TRANSACTION_HASH"'"],
      "id": 1
    }'
  ```

  ```bash eth_getLogs theme={null}
  MEMO_ADDRESS=0x5294E9927c3306DcBaDb03fe70b92e01cCede505
  MEMO_EVENT_TOPIC=0xeb15ee720798341c37739df41be53acfbbf70ae6802dade35457beec6e47a5e4
  MEMO_ID=0xYOUR_32_BYTE_MEMO_ID
  FROM_BLOCK=0xYOUR_RECEIPT_BLOCK_NUMBER
  TO_BLOCK=0xYOUR_RECEIPT_BLOCK_NUMBER
  RPC_URL=https://rpc.testnet.arc.network

  curl --request POST "$RPC_URL" \
    --header "content-type: application/json" \
    --data '{
      "jsonrpc": "2.0",
      "method": "eth_getLogs",
      "params": [{
        "address": "'"$MEMO_ADDRESS"'",
        "fromBlock": "'"$FROM_BLOCK"'",
        "toBlock": "'"$TO_BLOCK"'",
        "topics": ["'"$MEMO_EVENT_TOPIC"'", null, null, "'"$MEMO_ID"'"]
      }],
      "id": 1
    }'
  ```
</CodeGroup>

You can now attach memos to USDC transfers and reconcile them from the emitted
events. For the event schema, nested memo behavior, and the guardrails that
constrain memo calls, see [Transaction memos](/arc/concepts/transaction-memos).
