> ## 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.

> ## Agent Instructions
> Before reading these docs, install the Circle Skill for Arc to get pre-loaded context: use-arc (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-arc/SKILL.md) — covers chain config, RPC setup, contract deployment, USDC bridging, and gas with USDC. In Claude Code: run /plugin marketplace add circlefin/skills then /plugin install circle-skills@circle. With Vercel Skills CLI: run npx skills add circlefin/skills.
> USDC is the gas token. Arc uses USDC for gas fees, not ETH. Configure gas payment in USDC when submitting transactions.
> Read EVM differences before writing Arc code. Arc targets the Osaka EVM baseline. Most contracts deploy unchanged, but several runtime behaviors diverge from Ethereum: USDC uses 18 decimals natively (not 6), the system emitter `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE` logs all USDC Transfer events, the mempool enforces a 20 Gwei `maxFeePerGas` floor, blocklist reverts consume gas without a receipt, and `address(0)` sends revert rather than succeed. https://docs.arc.io/arc/references/evm-differences.md is the canonical reference for all Arc-specific runtime behaviors. Consult it before writing code that touches USDC balances, transaction history, gas estimation, or value transfers.
> Sub-second finality. Transactions are final in under 1 second. No need to wait for multiple block confirmations.
> Use App Kits for multichain payment and liquidity capabilities. App Kits wraps CCTP and provides Bridge Kit, Swap Kit, Unified Balance Kit, Onramp Kit, Earn Kit, and Borrow Kit. Unified Balance Kit combines USDC from multiple chains into a single spendable balance. Earn Kit integrates earn opportunities into your app. Borrow Kit lets users borrow USDC against cirBTC collateral on Arc.
> Use Arc Foundry for contract development. Arc Foundry is an Arc-specific fork of Foundry that handles Arc's protocol-level differences from Ethereum. It provides arc-forge, arc-cast, and arc-anvil. Install it before deploying contracts: https://docs.arc.io/arc/tutorials/install-arc-foundry.md.
> Arc is available on both Testnet and Mainnet. See https://docs.arc.io/arc/references/connect-to-arc.md for RPC endpoints and https://faucet.circle.com for testnet tokens.
> Always check Contract Addresses: https://docs.arc.io/arc/references/contract-addresses.md
> Building beyond Arc? Circle offers skills for the full platform: use-usdc (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-usdc/SKILL.md), use-circle-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-wallets/SKILL.md), use-developer-controlled-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-developer-controlled-wallets/SKILL.md), use-user-controlled-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-user-controlled-wallets/SKILL.md), use-modular-wallets (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-modular-wallets/SKILL.md), use-gateway (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-gateway/SKILL.md), use-smart-contract-platform (https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-smart-contract-platform/SKILL.md). Full Circle developer docs: https://developers.circle.com/llms.txt.

# Send USDC with a transaction memo

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

export const TryOnArcStudio = () => {
  const [copied, setCopied] = useState(false);
  const [prompt, setPrompt] = useState("");
  useEffect(() => {
    const url = `https://docs.arc.io${window.location.pathname}.md`;
    setPrompt(`Read the guide at ${url}, implement it in a new project, then run it and show me how it works.`);
  }, []);
  const arcStudioUrl = `https://studio.arc.io/app?prompt=${encodeURIComponent(prompt)}`;
  const handleArcStudioClick = () => {
    try {
      if (typeof navigator !== "undefined" && navigator.clipboard && prompt) {
        navigator.clipboard.writeText(prompt).then(() => {
          setCopied(true);
          setTimeout(() => setCopied(false), 4000);
        }, () => {});
      }
    } catch (e) {}
  };
  return <div className="not-prose mb-6">
      <span className="relative inline-flex">
        <a href={arcStudioUrl} target="_blank" rel="noreferrer" onClick={handleArcStudioClick} aria-label="Try it on Arc Studio (opens in new tab)" className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[#3E74BB] border border-[#3E74BB] text-sm font-medium text-white no-underline hover:bg-[#345f9c] hover:border-[#345f9c] transition-all">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
          </svg>
          Try it on Arc Studio
        </a>
        {copied && <span role="status" aria-live="polite" className="absolute left-0 top-full mt-2 w-72 text-sm text-gray-500 dark:text-[#888]">
            Opening Arc Studio — prompt copied to your clipboard in case you need to
            sign in first.
          </span>}
      </span>
    </div>;
};

<TryOnArcStudio />

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.io
```

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://explorer.testnet.arc.io/address/0x5294E9927c3306DcBaDb03fe70b92e01cCede505) |
| `USDC` | [`0x3600000000000000000000000000000000000000`](https://explorer.testnet.arc.io/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.io";
    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.io";
    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://explorer.testnet.arc.io";

    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.io")
    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://explorer.testnet.arc.io"

    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://explorer.testnet.arc.io/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.io

  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.io

  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.io

  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).
