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

# Monitor contract events

> Track onchain activity by monitoring contract 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 />

Track contract events and get event logs with the Circle Contracts API.

## Prerequisites

You need a deployed contract to monitor. If you completed the
[Deploy contracts](/arc/tutorials/deploy-contracts) tutorial, you can continue
with that contract. If your contract was deployed elsewhere, import it in
[Step 3](#step-3-import-a-contract-optional).

<Warning>
  Test against Arc Testnet only. Local EVM simulators run a standard EVM. They
  don't replicate EIP-7708 Transfer events, the USDC precompile, or blocklist
  enforcement.
</Warning>

## Step 1. Update your project

If you haven't already, add run scripts for monitoring contract events to your
`package.json`:

```shell theme={null}
npm pkg set scripts.webhook="tsx webhook-receiver.ts"
npm pkg set scripts.import-contract="tsx --env-file=.env import-contract.ts"
npm pkg set scripts.create-monitor="tsx --env-file=.env create-monitor.ts"
npm pkg set scripts.get-event-logs="tsx --env-file=.env get-event-logs.ts"
```

<Note>
  If you completed the Deploy contracts tutorial, your project already has the
  required SDKs installed. The npm scripts previously listed work with your
  existing setup.
</Note>

## Step 2. Set up your webhook

Event monitors send real-time updates to your webhook endpoint when events
happen.

<Tabs>
  <Tab title="webhook.site">
    1. Visit [webhook.site](https://webhook.site)
    2. Copy your unique webhook URL (for example, `https://webhook.site/your-uuid`)
  </Tab>

  <Tab title="ngrok">
    1. Install `ngrok` from [ngrok.com](https://ngrok.com)

    2. Create a webhook receiver script:

    <CodeGroup>
      ```ts webhook-receiver.ts theme={null}
      import express, { Request, Response } from "express";

      const app = express();
      app.use(express.json());

      app.post("/webhook", (req: Request, res: Response) => {
        console.log("Received webhook:");
        console.log(JSON.stringify(req.body, null, 2));
        res.status(200).json({ received: true });
      });

      const PORT = 3000;
      app.listen(PORT, () => {
        console.log(`Webhook receiver listening on port ${PORT}`);
        console.log(`Endpoint: http://localhost:${PORT}/webhook`);
      });
      ```

      ```python webhook_receiver.py theme={null}
      from flask import Flask, request, jsonify
      import json

      app = Flask(__name__)

      @app.route("/webhook", methods=["POST"])
      def webhook():
          data = request.get_json()
          print("Received webhook:")
          print(json.dumps(data, indent=2))
          return jsonify({"received": True}), 200

      if __name__ == "__main__":
          PORT = 3000
          print(f"Webhook receiver listening on port {PORT}")
          print(f"Endpoint: http://localhost:{PORT}/webhook")
          app.run(port=PORT)
      ```
    </CodeGroup>

    3. Start the webhook receiver:

    <CodeGroup>
      ```shell Node.js theme={null}
      npm run webhook
      ```

      ```shell Python theme={null}
      python webhook_receiver.py
      ```
    </CodeGroup>

    4. In a separate terminal, start `ngrok`:

    ```shell theme={null}
    ngrok http 3000
    ```

    5. Copy the HTTPS forwarding URL (for example,
       `https://abc123.ngrok-free.app/webhook`)

    <Note>
      If using `ngrok` for local testing, you can optionally set `WEBHOOK_URL` in
      your `.env` file to store your `ngrok` forwarding URL.
    </Note>
  </Tab>
</Tabs>

## Step 3. Register your webhook in Console

Register your webhook URL in the Developer Console:

1. Go to [Developer Console](https://console.circle.com)
2. Navigate to **Webhooks** (left sidebar)
3. Click **Add a webhook**
4. Enter your webhook URL (from Step 1) and create the webhook

<Note>
  Register your webhook before creating event monitors. This allows Circle to
  send notifications to your endpoint.
</Note>

## Step 4. Import a contract (optional)

If your contract was deployed elsewhere and is not yet available in the
Developer Console, import it first. If you deployed a contract using Circle
Contracts, including the [Deploy contracts](/arc/tutorials/deploy-contracts)
tutorial, skip this step. Your contract is already available in the Console.

<CodeGroup>
  ```ts import-contract.ts theme={null}
  import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform";

  const contractClient = initiateSmartContractPlatformClient({
    apiKey: process.env.CIRCLE_API_KEY,
    entitySecret: process.env.CIRCLE_ENTITY_SECRET,
  });

  async function importContract() {
    try {
      const response = await contractClient.importContract({
        blockchain: "ARC-TESTNET",
        address: process.env.CONTRACT_ADDRESS,
        name: "MyContract",
      });

      console.log(JSON.stringify(response.data, null, 2));
    } catch (error) {
      console.error("Error importing contract:", error.message);
      throw error;
    }
  }

  importContract();
  ```

  ```python import_contract.py theme={null}
  from circle.web3 import utils, smart_contract_platform
  import os
  import json

  client = utils.init_smart_contract_platform_client(
      api_key=os.getenv("CIRCLE_API_KEY"),
      entity_secret=os.getenv("CIRCLE_ENTITY_SECRET")
  )

  def import_contract():
      try:
          contracts_api = smart_contract_platform.ContractsApi(client)
          import_request = smart_contract_platform.ImportContractRequest(
              blockchain="ARC-TESTNET",
              address=os.getenv("CONTRACT_ADDRESS"),
              name="MyContract",
          )

          response = contracts_api.import_contract(import_contract_request=import_request)

          print(json.dumps(response.data.to_dict(), indent=2))

      except Exception as error:
          print(f"Error importing contract: {error}")
          raise error

  import_contract()
  ```
</CodeGroup>

**Run the script:**

<CodeGroup>
  ```shell Node.js theme={null}
  npm run import-contract
  ```

  ```shell Python theme={null}
  python import_contract.py
  ```
</CodeGroup>

<Note>
  If the contract is already imported, you'll see an error: `contract already
      exists`. This means the contract is already available in the Console and you
  can proceed to create an event monitor.
</Note>

## Step 5. Create an event monitor

Event monitors track specific contract events. They send updates to your webhook
endpoint. This example monitors `Transfer` events:

<CodeGroup>
  ```ts create-monitor.ts theme={null}
  import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform";

  const contractClient = initiateSmartContractPlatformClient({
    apiKey: process.env.CIRCLE_API_KEY,
    entitySecret: process.env.CIRCLE_ENTITY_SECRET,
  });

  async function createEventMonitor() {
    try {
      const response = await contractClient.createEventMonitor({
        blockchain: "ARC-TESTNET",
        contractAddress: process.env.CONTRACT_ADDRESS,
        eventSignature: "Transfer(address,address,uint256)",
      });

      console.log(JSON.stringify(response.data, null, 2));
    } catch (error) {
      console.error("Error creating event monitor:", error.message);
      throw error;
    }
  }

  createEventMonitor();
  ```

  ```python create_monitor.py theme={null}
  from circle.web3 import utils, smart_contract_platform
  import os
  import json

  client = utils.init_smart_contract_platform_client(
      api_key=os.getenv("CIRCLE_API_KEY"),
      entity_secret=os.getenv("CIRCLE_ENTITY_SECRET")
  )

  def create_event_monitor():
      try:
          event_monitors_api = smart_contract_platform.EventMonitorsApi(client)
          monitor_request = smart_contract_platform.CreateEventMonitorRequest(
              blockchain="ARC-TESTNET",
              contract_address=os.getenv("CONTRACT_ADDRESS"),
              event_signature="Transfer(address,address,uint256)",
          )

          response = event_monitors_api.create_event_monitor(
              create_event_monitor_request=monitor_request
          )

          print(json.dumps(response.data.to_dict(), indent=2))
      except Exception as error:
          print(f"Error creating event monitor: {error}")
          raise

  create_event_monitor()
  ```
</CodeGroup>

**Run the script:**

<CodeGroup>
  ```shell Node.js theme={null}
  npm run create-monitor
  ```

  ```shell Python theme={null}
  python create_monitor.py
  ```
</CodeGroup>

**Response:**

```json theme={null}
{
  "eventMonitor": {
    "id": "019bf984-b4da-7026-a3d2-674ce371a933",
    "contractName": "TestERC20Token",
    "contractId": "019bf8be-7be5-7a3e-89cc-05bcd7413f20",
    "contractAddress": "0x281156899E5BD6fEcf1C0831Ee24894eEEAea2f8",
    "blockchain": "ARC-TESTNET",
    "eventSignature": "Transfer(address,address,uint256)",
    "eventSignatureHash": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
    "isEnabled": true,
    "createDate": "2026-01-26T08:56:22.490638Z",
    "updateDate": "2026-01-26T08:56:22.490638Z"
  }
}
```

<Warning>
  If you're monitoring USDC transfers, the `contractAddress` filter alone
  isn't sufficient:

  * **Set up a monitor for the system emitter to capture native sends.** Plain
    native USDC sends emit from `0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE`, not
    from the ERC-20 USDC contract or any user-deployed contract. A monitor
    filtered on the ERC-20 contract address silently misses native sends.
  * **Don't count logs from both the system emitter and the ERC-20 contract.** An
    ERC-20 `transfer()` emits a log from both. Use the system emitter only to
    avoid counting the same transfer twice.
</Warning>

## Step 6. Receive webhook notifications

When events occur, Circle sends updates to your endpoint. Here is what a
`Transfer` event looks like:

```json theme={null}
{
  "subscriptionId": "f0332621-a117-4b7b-bdf0-5c61a4681826",
  "notificationId": "5c5eea9f-398f-426f-a4a5-1bdc28b36d2c",
  "notificationType": "contracts.eventLog",
  "notification": {
    "contractAddress": "0x4abcffb90897fe7ce86ed689d1178076544a021b",
    "blockchain": "ARC-TESTNET",
    "txHash": "0xe15d6dbb50178f60930b8a3e3e775f3c022505ea2e351b6c2c2985d2405c8ebc",
    "userOpHash": "0x78c3e8185ff9abfc7197a8432d9b79566123616c136001e609102c97e732e55e",
    "blockHash": "0x0ad6bf57a110d42620defbcb9af98d6223f060de588ed96ae495ddeaf3565c8d",
    "blockHeight": 22807198,
    "eventSignature": "Transfer(address,address,uint256)",
    "eventSignatureHash": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
    "topics": [
      "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
      "0x0000000000000000000000000000000000000000000000000000000000000000",
      "0x000000000000000000000000bcf83d3b112cbf43b19904e376dd8dee01fe2758"
    ],
    "data": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
    "firstConfirmDate": "2026-01-21T06:53:12Z"
  },
  "timestamp": "2026-01-21T06:53:13.194467201Z",
  "version": 2
}
```

**Key fields:**

* `notificationType`: Always `"contracts.eventLog"` for event monitor webhooks
* `notification.eventSignature`: The event that was emitted
* `notification.contractAddress`: Address of the contract that emitted the event
* `notification.blockchain`: The blockchain network (for example, `ARC-TESTNET`)
* `notification.txHash`: Transaction hash where the event occurred
* `notification.userOpHash`: User operation hash (for ERC-4337 smart contract
  accounts). When present, `tx.from` at the RPC level is the bundler.
  Attributing from it identifies the bundler, not the user. Check the Transfer
  event's indexed topics for the token sender rather than using `tx.from`.
* `notification.blockHash`: Hash of the block containing the transaction
* `notification.blockHeight`: Block number where the event occurred
* `notification.eventSignatureHash`: Keccak256 hash of the event signature
* `notification.topics`: Indexed event parameters (for example, `from` and `to`
  addresses)
* `notification.data`: Non-indexed event parameters (for example, token amount)
* `notification.firstConfirmDate`: Timestamp when the event was first confirmed
* `timestamp`: Timestamp when the webhook was sent
* `version`: Webhook payload version

<Tip>
  You can verify webhook delivery status in the [Developer
  Console](https://console.circle.com) under Contracts → Monitoring.
</Tip>

## Step 7. Retrieve event logs

You can also query event logs with the API. This is useful for past events or if
you prefer polling.

<Note>
  **Webhooks vs Polling**: Webhooks send real-time updates (push). Polling needs
  periodic API calls (pull). Use webhooks for production and polling for testing
  or past queries.
</Note>

<CodeGroup>
  ```ts get-event-logs.ts theme={null}
  import { initiateSmartContractPlatformClient } from "@circle-fin/smart-contract-platform";

  const contractClient = initiateSmartContractPlatformClient({
    apiKey: process.env.CIRCLE_API_KEY,
    entitySecret: process.env.CIRCLE_ENTITY_SECRET,
  });

  async function getEventLogs() {
    try {
      const response = await contractClient.listEventLogs({
        contractAddress: process.env.CONTRACT_ADDRESS,
        blockchain: "ARC-TESTNET",
        pageSize: 10,
      });

      console.log(JSON.stringify(response.data, null, 2));
    } catch (error) {
      console.error("Error fetching event logs:", error.message);
      throw error;
    }
  }

  getEventLogs();
  ```

  ```python get_event_logs.py theme={null}
  from circle.web3 import utils, smart_contract_platform
  import os
  import json

  client = utils.init_smart_contract_platform_client(
      api_key=os.getenv("CIRCLE_API_KEY"),
      entity_secret=os.getenv("CIRCLE_ENTITY_SECRET")
  )

  event_monitors_api = smart_contract_platform.EventMonitorsApi(client)

  def get_event_logs():
      try:
          response = event_monitors_api.list_event_logs(
              contract_address=os.getenv("CONTRACT_ADDRESS"),
              blockchain="ARC-TESTNET",
              page_size=10
          )

          print(json.dumps(response.data.to_dict(), indent=2, default=str))

      except Exception as error:
          print(f"Error fetching event logs: {error}")
          raise

  get_event_logs()
  ```
</CodeGroup>

**Run the script:**

<CodeGroup>
  ```shell Node.js theme={null}
  npm run get-event-logs
  ```

  ```shell Python theme={null}
  python get_event_logs.py
  ```
</CodeGroup>

<Note>
  Replace `CONTRACT_ADDRESS` with your contract address. You can get this
  address when you deploy the contract, or by listing your contracts with
  `listContracts()`.
</Note>

**Response:**

```json theme={null}
{
  "eventLogs": [
    {
      "id": "019bf987-f901-7145-9e95-55f177b05b24",
      "subscriptionId": "019bf984-b4da-7026-a3d2-674ce371a933",
      "contractId": "019bf8be-7be5-7a3e-89cc-05bcd7413f20",
      "contractName": "TestERC20Token",
      "blockchain": "ARC-TESTNET",
      "txHash": "0x3bfbab5d5ce0d1a5d682cbc742d3940cf59db0369d173b71ba2a3b8f43bfbcb1",
      "logIndex": "50",
      "blockHash": "0x7d12148f9331556b31f84f58a41b7ff16eaaa47940f9e86733037d7ab74d858e",
      "blockHeight": 23686153,
      "contractAddress": "0x281156899E5BD6fEcf1C0831Ee24894eEEAea2f8",
      "eventSignature": "Transfer(address,address,uint256)",
      "eventSignatureHash": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
      "topics": [
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
        "0x0000000000000000000000000000000000000000000000000000000000000000",
        "0x000000000000000000000000bcf83d3b112cbf43b19904e376dd8dee01fe2758"
      ],
      "data": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000",
      "decodedTopics": null,
      "decodedData": null,
      "userOpHash": "0x66befac1a371fcdddf1566215e4677127e111dff9253f306f7096fed8642a208",
      "firstConfirmDate": "2026-01-26T08:59:55Z",
      "createDate": "2026-01-26T08:59:56.545962Z",
      "updateDate": "2026-01-26T08:59:56.545962Z"
    }
  ]
}
```

<Note>
  You can view, update, and delete event monitors with the Circle Contracts API.
  See the [API
  Reference](https://developers.circle.com/api-reference/contracts/smart-contract-platform/get-event-monitors)
  for details on managing your monitors.
</Note>

## Summary

After completing this tutorial, you've successfully:

* Set up webhook endpoints using webhook.site or `ngrok`
* Registered webhooks in the Developer Console
* Created event monitors for specific contract events
* Received real-time webhook updates for contract events
* Retrieved past event logs with the Circle SDK
