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

# Register your first AI agent

> Register AI agents with onchain identity, build reputation, and verify credentials using ERC-8004 on Arc Testnet.

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 />

This quickstart guides you through registering an AI agent using the
[ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) standard on Arc Testnet.
You'll create developer-controlled wallets, register your agent's identity,
record reputation events, and verify credentials. Select the tab that matches
your preferred setup.

For the ERC-8004 registry contract addresses on Arc, see
[Contract addresses](/arc/references/contract-addresses#erc-8004).

<Tabs>
  <Tab title="Circle Wallets">
    ## Prerequisites

    Before you begin, make sure you have:

    1. A [Circle Developer Console](https://console.circle.com) account
    2. An API key created in the Console: **Keys → Create a key → API key → Standard
       Key**
    3. Your entity secret generated and registered using the
       [Circle Console](https://console.circle.com/wallets/dev/configurator/entity-secret)
       or the
       [SDK](https://developers.circle.com/wallets/dev-controlled/register-entity-secret#create-an-entity-secret-using-the-sdk)

    <Accordion icon="circle-info" title="New to Circle dev-controlled wallets? See what credentials you need.">
      Circle wallets use three credentials. An [**API
      key**](https://developers.circle.com/api-reference/keys) authenticates your
      requests to Circle APIs. An [**entity
      secret**](https://developers.circle.com/wallets/dev-controlled/entity-secret-management)
      authorizes each wallet operation. A [**wallet set
      ID**](https://developers.circle.com/wallets/dev-controlled/create-your-first-wallet)
      groups your wallets.
    </Accordion>

    ## Step 1. Set up your project

    Create a project directory, install dependencies, and configure your
    environment.

    ### 1.1. Create the project and install dependencies

    <CodeGroup>
      ```shell Node.js theme={null}
      mkdir erc8004-quickstart
      cd erc8004-quickstart
      npm init -y
      npm pkg set type=module
      npm pkg set scripts.start="tsx --env-file=.env index.ts"

      npm install @circle-fin/developer-controlled-wallets viem
      npm install --save-dev tsx typescript @types/node
      ```

      ```shell Python theme={null}
      mkdir erc8004-quickstart
      cd erc8004-quickstart
      python3 -m venv .venv
      source .venv/bin/activate

      pip install circle-developer-controlled-wallets web3 python-dotenv
      ```
    </CodeGroup>

    ### 1.2. Configure TypeScript (optional)

    <Tip>
      This step is optional. It helps prevent missing types in your IDE or editor.
    </Tip>

    Create a `tsconfig.json` file:

    ```shell theme={null}
    npx tsc --init
    ```

    Then, update the `tsconfig.json` file:

    ```shell theme={null}
    cat <<'EOF' > tsconfig.json
    {
      "compilerOptions": {
        "target": "ESNext",
        "module": "ESNext",
        "moduleResolution": "bundler",
        "strict": true,
        "types": ["node"]
      }
    }
    EOF
    ```

    ### 1.3. Set environment variables

    Create a `.env` file in the project directory and add your Circle credentials:

    ```text theme={null}
    CIRCLE_API_KEY=YOUR_API_KEY
    CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
    ```

    Where `YOUR_API_KEY` is your Circle Developer API key and `YOUR_ENTITY_SECRET`
    is your registered Entity Secret.

    The `npm run start` command loads these variables from `.env` using Node.js
    native env-file support.

    <Tip>
      Prefer editing `.env` files in your IDE or editor so credentials are not
      leaked to your shell history.
    </Tip>

    ## Step 2. Create developer-controlled wallets

    In this step, you create two Arc Testnet dev-controlled wallets for the ERC-8004
    flow. One wallet owns the agent and the other records reputation. If you already
    have two Arc Testnet dev-controlled wallets for this flow, skip to
    [Step 3](#step-3-prepare-agent-metadata). Per ERC-8004, agent owners cannot
    record reputation for their own agents to prevent self-dealing.

    The Step 2 through 7 code snippets explain the flow in smaller pieces. They are
    not cumulative and will not run if pasted together. To run the full workflow end
    to end, use the [complete script](#full-agent-registration-script) at the end of
    this tutorial.

    <CodeGroup>
      ```typescript index.ts theme={null}
      import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";

      const circleClient = initiateDeveloperControlledWalletsClient({
        apiKey: process.env.CIRCLE_API_KEY!,
        entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
      });

      const walletSet = await circleClient.createWalletSet({
        name: "ERC8004 Agent Wallets",
      });

      const walletsResponse = await circleClient.createWallets({
        blockchains: ["ARC-TESTNET"],
        count: 2,
        walletSetId: walletSet.data?.walletSet?.id ?? "",
        accountType: "SCA",
      });

      const ownerWallet = walletsResponse.data?.wallets?.[0]!;
      const validatorWallet = walletsResponse.data?.wallets?.[1]!;

      console.log(`Owner:     ${ownerWallet.address}`);
      console.log(`Validator: ${validatorWallet.address}`);
      ```

      ```python index.py theme={null}
      from circle.web3 import utils, developer_controlled_wallets
      import os
      from dotenv import load_dotenv

      load_dotenv()

      circle_client = utils.init_developer_controlled_wallets_client(
          api_key=os.getenv("CIRCLE_API_KEY"),
          entity_secret=os.getenv("CIRCLE_ENTITY_SECRET"),
      )

      wallet_sets_api = developer_controlled_wallets.WalletSetsApi(circle_client)
      wallets_api = developer_controlled_wallets.WalletsApi(circle_client)

      wallet_set = wallet_sets_api.create_wallet_set(
          developer_controlled_wallets.CreateWalletSetRequest.from_dict({
              "name": "ERC8004 Agent Wallets",
          })
      )
      wallet_set_id = wallet_set.data.wallet_set.actual_instance.id

      wallets_response = wallets_api.create_wallet(
          developer_controlled_wallets.CreateWalletRequest.from_dict({
              "blockchains": ["ARC-TESTNET"],
              "count": 2,
              "walletSetId": wallet_set_id,
              "accountType": "SCA",
          })
      )

      owner_wallet = wallets_response.data.wallets[0].actual_instance
      validator_wallet = wallets_response.data.wallets[1].actual_instance

      print(f"Owner:     {owner_wallet.address}")
      print(f"Validator: {validator_wallet.address}")
      ```
    </CodeGroup>

    ## Step 3. Prepare agent metadata

    Create a JSON file with metadata for your agent. The following structure is an
    example you can adapt for your use case. ERC-8004 registration stores a metadata
    URI, but the JSON fields at that URI are application-defined unless your
    integration follows a separate metadata convention.

    ```json agent-metadata.json theme={null}
    {
      "name": "DeFi Arbitrage Agent v1.0",
      "description": "Autonomous trading agent for cross-DEX arbitrage on Arc",
      "image": "ipfs://QmAgentAvatarHash...",
      "agent_type": "trading",
      "capabilities": [
        "arbitrage_detection",
        "liquidity_monitoring",
        "automated_execution"
      ],
      "version": "1.0.0"
    }
    ```

    Upload to IPFS using [Pinata](https://pinata.cloud),
    [NFT.Storage](https://nft.storage), [Web3.Storage](https://web3.storage) or your
    preferred IPFS tool. You'll receive an IPFS URI like `ipfs://QmYourHash...`.

    <Note>
      For this quickstart, you can skip uploading and use the example URI:
      `ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei`
    </Note>

    ## Step 4. Register your agent identity

    Call `register(metadataURI)` on the IdentityRegistry to mint an identity NFT for
    your agent.

    <CodeGroup>
      ```typescript index.ts theme={null}
      const IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e";

      const METADATA_URI =
        process.env.METADATA_URI ||
        "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei";

      const registerTx = await circleClient.createContractExecutionTransaction({
        walletAddress: ownerWallet.address!,
        blockchain: "ARC-TESTNET",
        contractAddress: IDENTITY_REGISTRY,
        abiFunctionSignature: "register(string)",
        abiParameters: [METADATA_URI],
        fee: { type: "level", config: { feeLevel: "MEDIUM" } },
      });

      // Poll until confirmed
      let txHash: string | undefined;
      for (let i = 0; i < 30; i++) {
        await new Promise((r) => setTimeout(r, 2000));
        const { data } = await circleClient.getTransaction({
          id: registerTx.data?.id!,
        });
        if (data?.transaction?.state === "COMPLETE") {
          txHash = data.transaction.txHash;
          break;
        }
        if (data?.transaction?.state === "FAILED")
          throw new Error("Registration failed");
      }

      console.log(`Registered: https://explorer.testnet.arc.io/tx/${txHash}`);
      ```

      ```python index.py theme={null}
      import time

      IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e"

      METADATA_URI = os.getenv("METADATA_URI") or \
          "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei"

      transactions_api = developer_controlled_wallets.TransactionsApi(circle_client)

      request = developer_controlled_wallets \
          .CreateContractExecutionTransactionForDeveloperRequest.from_dict({
              "walletAddress": owner_wallet.address,
              "blockchain": "ARC-TESTNET",
              "contractAddress": IDENTITY_REGISTRY,
              "abiFunctionSignature": "register(string)",
              "abiParameters": [METADATA_URI],
              "feeLevel": "MEDIUM",
          })

      response = transactions_api.create_developer_transaction_contract_execution(request)

      # Poll until confirmed
      tx_hash = None
      for _ in range(30):
          time.sleep(2)
          tx = transactions_api.get_transaction(id=response.data.id)
          if tx.data.transaction.state == "COMPLETE":
              tx_hash = tx.data.transaction.tx_hash
              break
          if tx.data.transaction.state == "FAILED":
              raise Exception("Registration failed")

      print(f"Registered: https://explorer.testnet.arc.io/tx/{tx_hash}")
      ```
    </CodeGroup>

    <Note>
      With Circle Gas Station, your application sponsors the transaction fees. On
      Arc, gas is approximately 0.006 `USDC-TESTNET` per transaction.
    </Note>

    ## Step 5. Retrieve your agent ID

    Query the `Transfer` event from the IdentityRegistry to find the token ID minted
    for your agent.

    <CodeGroup>
      ```typescript index.ts theme={null}
      import { createPublicClient, http, parseAbiItem, getContract } from "viem";
      import { arcTestnet } from "viem/chains";

      const publicClient = createPublicClient({
        chain: arcTestnet,
        transport: http(),
      });

      const latestBlock = await publicClient.getBlockNumber();
      const blockRange = 10000n; // RPC limit: eth_getLogs is often capped at 10,000 blocks
      const fromBlock = latestBlock > blockRange ? latestBlock - blockRange : 0n;

      const transferLogs = await publicClient.getLogs({
        address: IDENTITY_REGISTRY,
        event: parseAbiItem(
          "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)",
        ),
        args: { to: ownerWallet.address as `0x${string}` },
        fromBlock,
        toBlock: latestBlock,
      });

      if (transferLogs.length === 0) {
        throw new Error("No Transfer events found — registration may have failed");
      }

      const agentId = transferLogs[transferLogs.length - 1].args.tokenId!.toString();

      const identityContract = getContract({
        address: IDENTITY_REGISTRY,
        abi: [
          {
            name: "ownerOf",
            type: "function",
            stateMutability: "view",
            inputs: [{ name: "tokenId", type: "uint256" }],
            outputs: [{ name: "", type: "address" }],
          },
          {
            name: "tokenURI",
            type: "function",
            stateMutability: "view",
            inputs: [{ name: "tokenId", type: "uint256" }],
            outputs: [{ name: "", type: "string" }],
          },
        ],
        client: publicClient,
      });

      const owner = await identityContract.read.ownerOf([BigInt(agentId)]);
      const tokenURI = await identityContract.read.tokenURI([BigInt(agentId)]);

      console.log(`Agent ID: ${agentId}`);
      console.log(`Owner: ${owner}`);
      console.log(`Metadata: ${tokenURI}`);
      ```

      ```python index.py theme={null}
      from web3 import Web3

      RPC_URL = "https://rpc.testnet.arc.io/"
      w3 = Web3(Web3.HTTPProvider(RPC_URL))

      identity_abi = [
          {
              "anonymous": False,
              "inputs": [
                  {"indexed": True, "name": "from", "type": "address"},
                  {"indexed": True, "name": "to", "type": "address"},
                  {"indexed": True, "name": "tokenId", "type": "uint256"},
              ],
              "name": "Transfer",
              "type": "event",
          },
          {
              "inputs": [{"name": "tokenId", "type": "uint256"}],
              "name": "ownerOf",
              "outputs": [{"name": "", "type": "address"}],
              "stateMutability": "view",
              "type": "function",
          },
          {
              "inputs": [{"name": "tokenId", "type": "uint256"}],
              "name": "tokenURI",
              "outputs": [{"name": "", "type": "string"}],
              "stateMutability": "view",
              "type": "function",
          },
      ]

      identity_contract = w3.eth.contract(address=IDENTITY_REGISTRY, abi=identity_abi)

      latest_block = w3.eth.block_number
      from_block = max(0, latest_block - 10000)
      events = identity_contract.events.Transfer.create_filter(
          from_block=from_block,
          to_block=latest_block,
          argument_filters={"to": owner_wallet.address},
      ).get_all_entries()

      agent_id = events[-1]["args"]["tokenId"]
      on_chain_owner = identity_contract.functions.ownerOf(agent_id).call()
      token_uri = identity_contract.functions.tokenURI(agent_id).call()

      print(f"Agent ID: {agent_id}")
      print(f"Owner: {on_chain_owner}")
      print(f"Metadata: {token_uri}")
      ```
    </CodeGroup>

    Your AI agent now has a unique onchain identity.

    ## Step 6. Record agent reputation

    Build your agent's reputation by recording feedback. Use the **validator
    wallet**—per ERC-8004, agent owners cannot record reputation for their own
    agents.

    <CodeGroup>
      ```typescript index.ts theme={null}
      import { keccak256, toHex } from "viem";

      const REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713";

      const tag = "successful_trade";
      const feedbackHash = keccak256(toHex(tag));

      const reputationTx = await circleClient.createContractExecutionTransaction({
        walletAddress: validatorWallet.address!,
        blockchain: "ARC-TESTNET",
        contractAddress: REPUTATION_REGISTRY,
        abiFunctionSignature:
          "giveFeedback(uint256,int128,uint8,string,string,string,string,bytes32)",
        abiParameters: [agentId, "95", "0", tag, "", "", "", feedbackHash],
        fee: { type: "level", config: { feeLevel: "MEDIUM" } },
      });

      // Poll until confirmed (same pattern as Step 4)
      ```

      ```python index.py theme={null}
      REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713"

      tag = "successful_trade"
      feedback_hash = "0x" + w3.keccak(text=tag).hex()

      request = developer_controlled_wallets \
          .CreateContractExecutionTransactionForDeveloperRequest.from_dict({
              "walletAddress": validator_wallet.address,
              "blockchain": "ARC-TESTNET",
              "contractAddress": REPUTATION_REGISTRY,
              "abiFunctionSignature":
                  "giveFeedback(uint256,int128,uint8,string,string,string,string,bytes32)",
              "abiParameters": [
                  str(agent_id), "95", "0", tag, "", "", "", feedback_hash
              ],
              "feeLevel": "MEDIUM",
          })

      response = transactions_api.create_developer_transaction_contract_execution(request)

      # Poll until confirmed (same pattern as Step 4)
      ```
    </CodeGroup>

    <Note>
      **Production scoring**: This quickstart hardcodes `score: 95` for demonstration. In production, calculate scores dynamically based on agent behavior. For example, `score = loanRepaidOnTime ? 100 : 20` for lending protocols, or `score = slippagePct < 1 ? 95 : 60` for trading platforms.

      The ReputationRegistry stores attestations from external observers who witnessed
      the agent's actions. Your application logic calculates scores based on outcomes,
      then records them onchain.
    </Note>

    ## Step 7. Request and verify validation

    The ERC-8004 ValidationRegistry uses a two-step request/response flow. The
    **agent owner** requests validation from a validator, then the **validator**
    submits a response.

    <CodeGroup>
      ```typescript index.ts theme={null}
      const VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272";

      const requestURI = "ipfs://bafkreiexamplevalidationrequest";
      const requestHash = keccak256(
        toHex(`kyc_verification_request_agent_${agentId}`),
      );

      // Owner requests validation
      const validationReqTx = await circleClient.createContractExecutionTransaction({
        walletAddress: ownerWallet.address!,
        blockchain: "ARC-TESTNET",
        contractAddress: VALIDATION_REGISTRY,
        abiFunctionSignature: "validationRequest(address,uint256,string,bytes32)",
        abiParameters: [validatorWallet.address!, agentId, requestURI, requestHash],
        fee: { type: "level", config: { feeLevel: "MEDIUM" } },
      });

      // Poll until confirmed (same pattern as Step 4)

      // Validator responds (100 = passed, 0 = failed)
      const validationResTx = await circleClient.createContractExecutionTransaction({
        walletAddress: validatorWallet.address!,
        blockchain: "ARC-TESTNET",
        contractAddress: VALIDATION_REGISTRY,
        abiFunctionSignature:
          "validationResponse(bytes32,uint8,string,bytes32,string)",
        abiParameters: [
          requestHash,
          "100",
          "",
          "0x" + "0".repeat(64),
          "kyc_verified",
        ],
        fee: { type: "level", config: { feeLevel: "MEDIUM" } },
      });

      // Poll until confirmed, then verify:
      const validationContract = getContract({
        address: VALIDATION_REGISTRY,
        abi: [
          {
            name: "getValidationStatus",
            type: "function",
            stateMutability: "view",
            inputs: [{ name: "requestHash", type: "bytes32" }],
            outputs: [
              { name: "validatorAddress", type: "address" },
              { name: "agentId", type: "uint256" },
              { name: "response", type: "uint8" },
              { name: "responseHash", type: "bytes32" },
              { name: "tag", type: "string" },
              { name: "lastUpdate", type: "uint256" },
            ],
          },
        ],
        client: publicClient,
      });

      type ValidationStatus = readonly [
        `0x${string}`,
        bigint,
        number,
        `0x${string}`,
        string,
        bigint,
      ];

      const [valAddr, , response, , tag] =
        (await validationContract.read.getValidationStatus([
          requestHash,
        ])) as ValidationStatus;

      console.log(`Validator: ${valAddr}`);
      console.log(`Response: ${response} (100 = passed)`);
      console.log(`Tag: ${tag}`);
      ```

      ```python index.py theme={null}
      VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272"

      request_uri = "ipfs://bafkreiexamplevalidationrequest"
      request_hash = "0x" + w3.keccak(text=f"kyc_verification_request_agent_{agent_id}").hex()

      # Owner requests validation
      request = developer_controlled_wallets \
          .CreateContractExecutionTransactionForDeveloperRequest.from_dict({
              "walletAddress": owner_wallet.address,
              "blockchain": "ARC-TESTNET",
              "contractAddress": VALIDATION_REGISTRY,
              "abiFunctionSignature": "validationRequest(address,uint256,string,bytes32)",
              "abiParameters": [validator_wallet.address, str(agent_id), request_uri, request_hash],
              "feeLevel": "MEDIUM",
          })

      response = transactions_api.create_developer_transaction_contract_execution(request)

      # Poll until confirmed (same pattern as Step 4)

      # Validator responds (100 = passed, 0 = failed)
      request = developer_controlled_wallets \
          .CreateContractExecutionTransactionForDeveloperRequest.from_dict({
              "walletAddress": validator_wallet.address,
              "blockchain": "ARC-TESTNET",
              "contractAddress": VALIDATION_REGISTRY,
              "abiFunctionSignature": "validationResponse(bytes32,uint8,string,bytes32,string)",
              "abiParameters": [request_hash, "100", "", "0x" + "0" * 64, "kyc_verified"],
              "feeLevel": "MEDIUM",
          })

      response = transactions_api.create_developer_transaction_contract_execution(request)

      # Poll until confirmed, then verify:
      validation_abi = [{
          "inputs": [{"name": "requestHash", "type": "bytes32"}],
          "name": "getValidationStatus",
          "outputs": [
              {"name": "validatorAddress", "type": "address"},
              {"name": "agentId", "type": "uint256"},
              {"name": "response", "type": "uint8"},
              {"name": "responseHash", "type": "bytes32"},
              {"name": "tag", "type": "string"},
              {"name": "lastUpdate", "type": "uint256"},
          ],
          "stateMutability": "view",
          "type": "function",
      }]

      validation_contract = w3.eth.contract(address=VALIDATION_REGISTRY, abi=validation_abi)

      val_addr, _, val_response, _, val_tag, _ = \
          validation_contract.functions.getValidationStatus(
              bytes.fromhex(request_hash[2:])
          ).call()

      print(f"Validator: {val_addr}")
      print(f"Response: {val_response} (100 = passed)")
      print(f"Tag: {val_tag}")
      ```
    </CodeGroup>

    ## Full agent registration script

    The following complete script combines all the preceding steps into a single
    runnable file.

    <CodeGroup>
      ```typescript index.ts expandable theme={null}
      import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
      import {
        createPublicClient,
        http,
        parseAbiItem,
        getContract,
        keccak256,
        toHex,
      } from "viem";
      import { arcTestnet } from "viem/chains";

      const IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e";
      const REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713";
      const VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272";

      const METADATA_URI =
        process.env.METADATA_URI ||
        "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei";

      const circleClient = initiateDeveloperControlledWalletsClient({
        apiKey: process.env.CIRCLE_API_KEY!,
        entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
      });

      const publicClient = createPublicClient({
        chain: arcTestnet,
        transport: http(),
      });

      // Helper functions
      async function waitForTransaction(txId: string, label: string) {
        process.stdout.write(`  Waiting for ${label}`);
        for (let i = 0; i < 30; i++) {
          await new Promise((r) => setTimeout(r, 2000));
          const { data } = await circleClient.getTransaction({ id: txId });
          if (data?.transaction?.state === "COMPLETE") {
            const txHash = data.transaction.txHash;
            console.log(` ✓\n  Tx: https://explorer.testnet.arc.io/tx/${txHash}`);
            return txHash;
          }
          if (data?.transaction?.state === "FAILED") {
            throw new Error(`${label} failed onchain`);
          }
          process.stdout.write(".");
        }
        throw new Error(`${label} timed out`);
      }

      // Main invocation
      async function main() {
        console.log("\n── Step 1: Create wallets ──");

        const walletSet = await circleClient.createWalletSet({
          name: "ERC8004 Agent Wallets",
        });

        const walletsResponse = await circleClient.createWallets({
          blockchains: ["ARC-TESTNET"],
          count: 2,
          walletSetId: walletSet.data?.walletSet?.id ?? "",
          accountType: "SCA",
        });

        const ownerWallet = walletsResponse.data?.wallets?.[0]!;
        const validatorWallet = walletsResponse.data?.wallets?.[1]!;

        console.log(`  Owner:     ${ownerWallet.address} (${ownerWallet.id})`);
        console.log(
          `  Validator: ${validatorWallet.address} (${validatorWallet.id})`,
        );

        console.log("\n── Step 2: Register agent identity ──");
        console.log(`  Metadata URI: ${METADATA_URI}`);

        const registerTx = await circleClient.createContractExecutionTransaction({
          walletAddress: ownerWallet.address!,
          blockchain: "ARC-TESTNET",
          contractAddress: IDENTITY_REGISTRY,
          abiFunctionSignature: "register(string)",
          abiParameters: [METADATA_URI],
          fee: { type: "level", config: { feeLevel: "MEDIUM" } },
        });

        await waitForTransaction(registerTx.data?.id!, "registration");

        console.log("\n── Step 3: Retrieve agent ID ──");

        const latestBlock = await publicClient.getBlockNumber();
        const blockRange = 10000n; // RPC limit: eth_getLogs is often capped at 10,000 blocks
        const fromBlock = latestBlock > blockRange ? latestBlock - blockRange : 0n;

        const transferLogs = await publicClient.getLogs({
          address: IDENTITY_REGISTRY,
          event: parseAbiItem(
            "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)",
          ),
          args: { to: ownerWallet.address as `0x${string}` },
          fromBlock,
          toBlock: latestBlock,
        });

        if (transferLogs.length === 0) {
          throw new Error("No Transfer events found — registration may have failed");
        }

        const agentId =
          transferLogs[transferLogs.length - 1].args.tokenId!.toString();

        const identityContract = getContract({
          address: IDENTITY_REGISTRY,
          abi: [
            {
              name: "ownerOf",
              type: "function",
              stateMutability: "view",
              inputs: [{ name: "tokenId", type: "uint256" }],
              outputs: [{ name: "", type: "address" }],
            },
            {
              name: "tokenURI",
              type: "function",
              stateMutability: "view",
              inputs: [{ name: "tokenId", type: "uint256" }],
              outputs: [{ name: "", type: "string" }],
            },
          ],
          client: publicClient,
        });

        const owner = await identityContract.read.ownerOf([BigInt(agentId)]);
        const tokenURI = await identityContract.read.tokenURI([BigInt(agentId)]);

        console.log(`  Agent ID:     ${agentId}`);
        console.log(`  Owner:        ${owner}`);
        console.log(`  Metadata URI: ${tokenURI}`);

        console.log("\n── Step 4: Record reputation ──");

        const tag = "successful_trade";
        const feedbackHash = keccak256(toHex(tag));

        const reputationTx = await circleClient.createContractExecutionTransaction({
          walletAddress: validatorWallet.address!,
          blockchain: "ARC-TESTNET",
          contractAddress: REPUTATION_REGISTRY,
          abiFunctionSignature:
            "giveFeedback(uint256,int128,uint8,string,string,string,string,bytes32)",
          abiParameters: [agentId, "95", "0", tag, "", "", "", feedbackHash],
          fee: { type: "level", config: { feeLevel: "MEDIUM" } },
        });

        await waitForTransaction(reputationTx.data?.id!, "reputation");

        console.log("\n── Step 5: Verify reputation ──");

        const reputationLogs = await publicClient.getLogs({
          address: REPUTATION_REGISTRY,
          fromBlock: latestBlock - 1000n,
          toBlock: "latest",
        });

        console.log(`  Found ${reputationLogs.length} feedback event(s)`);

        // Owner requests; validator responds per ERC-8004
        console.log("\n── Step 6: Request validation ──");

        const requestURI = "ipfs://bafkreiexamplevalidationrequest";
        const requestHash = keccak256(
          toHex(`kyc_verification_request_agent_${agentId}`),
        );

        const validationReqTx = await circleClient.createContractExecutionTransaction(
          {
            walletAddress: ownerWallet.address!,
            blockchain: "ARC-TESTNET",
            contractAddress: VALIDATION_REGISTRY,
            abiFunctionSignature: "validationRequest(address,uint256,string,bytes32)",
            abiParameters: [
              validatorWallet.address!,
              agentId,
              requestURI,
              requestHash,
            ],
            fee: { type: "level", config: { feeLevel: "MEDIUM" } },
          },
        );

        await waitForTransaction(validationReqTx.data?.id!, "validation request");

        // Validator responds; 100 = passed, 0 = failed
        console.log("\n── Step 7: Validation response ──");

        const validationResTx = await circleClient.createContractExecutionTransaction(
          {
            walletAddress: validatorWallet.address!,
            blockchain: "ARC-TESTNET",
            contractAddress: VALIDATION_REGISTRY,
            abiFunctionSignature:
              "validationResponse(bytes32,uint8,string,bytes32,string)",
            abiParameters: [
              requestHash,
              "100",
              "",
              "0x" + "0".repeat(64),
              "kyc_verified",
            ],
            fee: { type: "level", config: { feeLevel: "MEDIUM" } },
          },
        );

        await waitForTransaction(validationResTx.data?.id!, "validation response");

        console.log("\n── Step 8: Check validation ──");

        const validationContract = getContract({
          address: VALIDATION_REGISTRY,
          abi: [
            {
              name: "getValidationStatus",
              type: "function",
              stateMutability: "view",
              inputs: [{ name: "requestHash", type: "bytes32" }],
              outputs: [
                { name: "validatorAddress", type: "address" },
                { name: "agentId", type: "uint256" },
                { name: "response", type: "uint8" },
                { name: "responseHash", type: "bytes32" },
                { name: "tag", type: "string" },
                { name: "lastUpdate", type: "uint256" },
              ],
            },
          ],
          client: publicClient,
        });

        type ValidationStatus = readonly [
          `0x${string}`,
          bigint,
          number,
          `0x${string}`,
          string,
          bigint,
        ];

        const [valAddr, , valResponse, , valTag] =
          (await validationContract.read.getValidationStatus([
            requestHash,
          ])) as ValidationStatus;

        console.log(`  Validator:  ${valAddr}`);
        console.log(`  Response:   ${valResponse} (100 = passed)`);
        console.log(`  Tag:        ${valTag}`);

        console.log("\n── Complete ──");
        console.log("  ✓ Identity registered");
        console.log("  ✓ Reputation recorded");
        console.log("  ✓ Validation requested and verified");
        console.log(
          `\n  Explorer: https://explorer.testnet.arc.io/address/${ownerWallet.address}\n`,
        );
      }

      main().catch((error) => {
        console.error("\nError:", error.message ?? error);
        process.exit(1);
      });
      ```

      ```python index.py expandable theme={null}
      from circle.web3 import utils, developer_controlled_wallets
      from web3 import Web3
      import os
      import time
      from dotenv import load_dotenv

      load_dotenv()

      IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e"
      REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713"
      VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272"
      RPC_URL = "https://rpc.testnet.arc.io/"

      METADATA_URI = os.getenv("METADATA_URI") or \
          "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei"

      circle_client = utils.init_developer_controlled_wallets_client(
          api_key=os.getenv("CIRCLE_API_KEY"),
          entity_secret=os.getenv("CIRCLE_ENTITY_SECRET"),
      )

      wallet_sets_api = developer_controlled_wallets.WalletSetsApi(circle_client)
      wallets_api = developer_controlled_wallets.WalletsApi(circle_client)
      transactions_api = developer_controlled_wallets.TransactionsApi(circle_client)

      w3 = Web3(Web3.HTTPProvider(RPC_URL))


      def wait_for_transaction(tx_id: str, label: str) -> str:
          print(f"  Waiting for {label}", end="", flush=True)
          for _ in range(30):
              time.sleep(2)
              tx = transactions_api.get_transaction(id=tx_id)
              state = tx.data.transaction.state
              if state == "COMPLETE":
                  tx_hash = tx.data.transaction.tx_hash
                  print(f" ✓\n  Tx: https://explorer.testnet.arc.io/tx/{tx_hash}")
                  return tx_hash
              if state == "FAILED":
                  raise Exception(f"{label} failed onchain")
              print(".", end="", flush=True)
          raise Exception(f"{label} timed out")


      def send_contract_tx(wallet_address, address, sig, params, label):
          request = developer_controlled_wallets \
              .CreateContractExecutionTransactionForDeveloperRequest.from_dict({
                  "walletAddress": wallet_address,
                  "blockchain": "ARC-TESTNET",
                  "contractAddress": address,
                  "abiFunctionSignature": sig,
                  "abiParameters": params,
                  "feeLevel": "MEDIUM",
              })
          response = transactions_api.create_developer_transaction_contract_execution(request)
          return wait_for_transaction(response.data.id, label)


      def main():
          # Step 1: Create wallets
          print("\n── Step 1: Create wallets ──")

          wallet_set = wallet_sets_api.create_wallet_set(
              developer_controlled_wallets.CreateWalletSetRequest.from_dict({
                  "name": "ERC8004 Agent Wallets",
              })
          )
          wallet_set_id = wallet_set.data.wallet_set.actual_instance.id

          wallets_response = wallets_api.create_wallet(
              developer_controlled_wallets.CreateWalletRequest.from_dict({
                  "blockchains": ["ARC-TESTNET"],
                  "count": 2,
                  "walletSetId": wallet_set_id,
                  "accountType": "SCA",
              })
          )

          owner_wallet = wallets_response.data.wallets[0].actual_instance
          validator_wallet = wallets_response.data.wallets[1].actual_instance

          print(f"  Owner:     {owner_wallet.address} ({owner_wallet.id})")
          print(f"  Validator: {validator_wallet.address} ({validator_wallet.id})")

          # Step 2: Register agent identity
          print("\n── Step 2: Register agent identity ──")
          print(f"  Metadata URI: {METADATA_URI}")

          send_contract_tx(
              owner_wallet.address, IDENTITY_REGISTRY,
              "register(string)", [METADATA_URI], "registration",
          )

          # Step 3: Retrieve agent ID
          print("\n── Step 3: Retrieve agent ID ──")

          identity_abi = [
              {
                  "anonymous": False,
                  "inputs": [
                      {"indexed": True, "name": "from", "type": "address"},
                      {"indexed": True, "name": "to", "type": "address"},
                      {"indexed": True, "name": "tokenId", "type": "uint256"},
                  ],
                  "name": "Transfer",
                  "type": "event",
              },
              {
                  "inputs": [{"name": "tokenId", "type": "uint256"}],
                  "name": "ownerOf",
                  "outputs": [{"name": "", "type": "address"}],
                  "stateMutability": "view",
                  "type": "function",
              },
              {
                  "inputs": [{"name": "tokenId", "type": "uint256"}],
                  "name": "tokenURI",
                  "outputs": [{"name": "", "type": "string"}],
                  "stateMutability": "view",
                  "type": "function",
              },
          ]

          identity_contract = w3.eth.contract(address=IDENTITY_REGISTRY, abi=identity_abi)

          latest_block = w3.eth.block_number
          from_block = max(0, latest_block - 10000)
          events = identity_contract.events.Transfer.create_filter(
              from_block=from_block,
              to_block=latest_block,
              argument_filters={"to": owner_wallet.address},
          ).get_all_entries()

          if not events:
              raise Exception("No Transfer events found — registration may have failed")

          agent_id = events[-1]["args"]["tokenId"]
          on_chain_owner = identity_contract.functions.ownerOf(agent_id).call()
          token_uri = identity_contract.functions.tokenURI(agent_id).call()

          print(f"  Agent ID:     {agent_id}")
          print(f"  Owner:        {on_chain_owner}")
          print(f"  Metadata URI: {token_uri}")

          # Step 4: Record reputation
          print("\n── Step 4: Record reputation ──")

          tag = "successful_trade"
          feedback_hash = "0x" + w3.keccak(text=tag).hex()

          send_contract_tx(
              validator_wallet.address, REPUTATION_REGISTRY,
              "giveFeedback(uint256,int128,uint8,string,string,string,string,bytes32)",
              [str(agent_id), "95", "0", tag, "", "", "", feedback_hash],
              "reputation",
          )

          # Step 5: Verify reputation
          print("\n── Step 5: Verify reputation ──")

          reputation_logs = w3.eth.get_logs({
              "address": REPUTATION_REGISTRY,
              "fromBlock": max(0, latest_block - 1000),
              "toBlock": "latest",
          })

          print(f"  Found {len(reputation_logs)} feedback event(s)")

          # Step 6: Request validation (owner requests; validator responds per ERC-8004)
          print("\n── Step 6: Request validation ──")

          request_uri = "ipfs://bafkreiexamplevalidationrequest"
          request_hash = "0x" + w3.keccak(text=f"kyc_verification_request_agent_{agent_id}").hex()

          send_contract_tx(
              owner_wallet.address, VALIDATION_REGISTRY,
              "validationRequest(address,uint256,string,bytes32)",
              [validator_wallet.address, str(agent_id), request_uri, request_hash],
              "validation request",
          )

          # Step 7: Validation response (validator responds; 100 = passed, 0 = failed)
          print("\n── Step 7: Validation response ──")

          send_contract_tx(
              validator_wallet.address, VALIDATION_REGISTRY,
              "validationResponse(bytes32,uint8,string,bytes32,string)",
              [request_hash, "100", "", "0x" + "0" * 64, "kyc_verified"],
              "validation response",
          )

          # Step 8: Check validation status
          print("\n── Step 8: Check validation ──")

          validation_abi = [{
              "inputs": [{"name": "requestHash", "type": "bytes32"}],
              "name": "getValidationStatus",
              "outputs": [
                  {"name": "validatorAddress", "type": "address"},
                  {"name": "agentId", "type": "uint256"},
                  {"name": "response", "type": "uint8"},
                  {"name": "responseHash", "type": "bytes32"},
                  {"name": "tag", "type": "string"},
                  {"name": "lastUpdate", "type": "uint256"},
              ],
              "stateMutability": "view",
              "type": "function",
          }]

          validation_contract = w3.eth.contract(
              address=VALIDATION_REGISTRY, abi=validation_abi
          )

          val_addr, _, val_response, _, val_tag, _ = \
              validation_contract.functions.getValidationStatus(
                  bytes.fromhex(request_hash[2:])
              ).call()

          print(f"  Validator:  {val_addr}")
          print(f"  Response:   {val_response} (100 = passed)")
          print(f"  Tag:        {val_tag}")

          print("\n── Complete ──")
          print("  ✓ Identity registered")
          print("  ✓ Reputation recorded")
          print("  ✓ Validation requested and verified")
          print(f"\n  Explorer: https://explorer.testnet.arc.io/address/{owner_wallet.address}\n")


      if __name__ == "__main__":
          try:
              main()
          except Exception as error:
              print(f"\nError: {error}")
              exit(1)
      ```
    </CodeGroup>

    Save it, then run:

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

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

    <Note>
      If you followed the Python workflow, run `deactivate` when you're done to exit
      the virtual environment.
    </Note>
  </Tab>

  <Tab title="Viem">
    ## Prerequisites

    Before you begin, make sure you have:

    1. Installed [Node.js v22+](https://nodejs.org/)
    2. Two self-managed EVM wallets for Arc Testnet
       * Testnet USDC in both wallets to pay for gas

    ## Step 1. Set up your project

    Create a project directory, install dependencies, and configure your
    environment.

    ### 1.1. Create the project and install dependencies

    ```shell theme={null}
    mkdir erc8004-quickstart
    cd erc8004-quickstart
    npm init -y
    npm pkg set type=module
    npm pkg set scripts.start="tsx --env-file=.env index.ts"

    npm install viem
    npm install --save-dev tsx typescript @types/node
    ```

    ### 1.2. Configure TypeScript (optional)

    <Tip>
      This step is optional. It helps prevent missing types in your IDE or editor.
    </Tip>

    Create a `tsconfig.json` file:

    ```shell theme={null}
    npx tsc --init
    ```

    Then, update the `tsconfig.json` file:

    ```shell theme={null}
    cat <<'EOF' > tsconfig.json
    {
      "compilerOptions": {
        "target": "ESNext",
        "module": "ESNext",
        "moduleResolution": "bundler",
        "strict": true,
        "types": ["node"]
      }
    }
    EOF
    ```

    ### 1.3. Set environment variables

    Create a `.env` file in the project directory:

    ```text .env theme={null}
    OWNER_PRIVATE_KEY=0xYOUR_OWNER_PRIVATE_KEY
    VALIDATOR_PRIVATE_KEY=0xYOUR_VALIDATOR_PRIVATE_KEY
    ```

    * `OWNER_PRIVATE_KEY` is the `0x`-prefixed private key for the Arc Testnet
      wallet that owns the agent and requests validation.
    * `VALIDATOR_PRIVATE_KEY` is the `0x`-prefixed private key for the Arc Testnet
      wallet that records reputation and submits the validation response.

    The `npm run start` command loads variables from `.env` using Node.js native
    env-file support.

    <Tip>
      Prefer editing `.env` files in your IDE or editor so credentials are not
      leaked to your shell history.
    </Tip>

    ## Step 2. Prepare your wallets

    In this step, you prepare two self-managed Arc Testnet wallets for the ERC-8004
    flow. One wallet owns the agent and the other records reputation. If you already
    have two funded Arc Testnet wallets for this flow, skip to
    [Step 3](#step-3-prepare-agent-metadata-2). Per ERC-8004, agent owners cannot
    record reputation for their own agents to prevent self-dealing.

    The Step 2 through 7 code snippets explain the flow in smaller pieces. They are
    not cumulative and will not run if pasted together. To run the full workflow end
    to end, use the [complete script](#full-agent-registration-script-2) at the end
    of this tutorial.

    ### 2.1. Create or fund your wallets

    Create two self-managed EVM wallets if you do not already have them. For
    example, you can generate throwaway wallets with Foundry:

    ```shell theme={null}
    cast wallet new --json
    ```

    Run it twice, once for the owner wallet and once for the validator wallet, then
    fund both wallets with Arc Testnet USDC so they can submit transactions.

    ### 2.2. Confirm wallet roles

    * the owner wallet registers the agent identity and requests validation
    * the validator wallet records reputation and submits the validation response

    ## Step 3. Prepare agent metadata

    Create a JSON file with metadata for your agent. The following structure is an
    example you can adapt for your use case. ERC-8004 registration stores a metadata
    URI, but the JSON fields at that URI are application-defined unless your
    integration follows a separate metadata convention.

    ```json agent-metadata.json theme={null}
    {
      "name": "DeFi Arbitrage Agent v1.0",
      "description": "Autonomous trading agent for cross-DEX arbitrage on Arc",
      "image": "ipfs://QmAgentAvatarHash...",
      "agent_type": "trading",
      "capabilities": [
        "arbitrage_detection",
        "liquidity_monitoring",
        "automated_execution"
      ],
      "version": "1.0.0"
    }
    ```

    Upload to IPFS using [Pinata](https://pinata.cloud),
    [NFT.Storage](https://nft.storage), [Web3.Storage](https://web3.storage) or your
    preferred IPFS tool. You'll receive an IPFS URI like `ipfs://QmYourHash...`.

    <Note>
      For this quickstart, you can skip uploading and use the example URI:
      `ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei`
    </Note>

    ## Step 4. Register your agent identity

    Call `register(metadataURI)` on the IdentityRegistry to mint an identity NFT for
    your agent.

    ```typescript index.ts theme={null}
    import {
      createPublicClient,
      createWalletClient,
      http,
      getContract,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    const IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e";
    const METADATA_URI =
      process.env.METADATA_URI ||
      "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei";

    const ownerAccount = privateKeyToAccount(
      process.env.OWNER_PRIVATE_KEY as `0x${string}`,
    );

    const publicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(),
    });

    const ownerWalletClient = createWalletClient({
      account: ownerAccount,
      chain: arcTestnet,
      transport: http(),
    });

    const identityContract = getContract({
      address: IDENTITY_REGISTRY,
      abi: [
        {
          name: "register",
          type: "function",
          stateMutability: "nonpayable",
          inputs: [{ name: "metadataURI", type: "string" }],
          outputs: [],
        },
      ],
      client: { public: publicClient, wallet: ownerWalletClient },
    });

    const registerTx = await identityContract.write.register([METADATA_URI], {
      account: ownerAccount,
    });

    await publicClient.waitForTransactionReceipt({ hash: registerTx });
    console.log(`Registered: https://explorer.testnet.arc.io/tx/${registerTx}`);
    ```

    ## Step 5. Retrieve your agent ID

    Query the `Transfer` event from the IdentityRegistry to find the token ID minted
    for your agent.

    ```typescript index.ts theme={null}
    import { parseAbiItem } from "viem";

    const latestBlock = await publicClient.getBlockNumber();
    const blockRange = 10000n;
    const fromBlock = latestBlock > blockRange ? latestBlock - blockRange : 0n;

    const transferLogs = await publicClient.getLogs({
      address: IDENTITY_REGISTRY,
      event: parseAbiItem(
        "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)",
      ),
      args: { to: ownerAccount.address },
      fromBlock,
      toBlock: latestBlock,
    });

    if (transferLogs.length === 0) {
      throw new Error("No Transfer events found — registration may have failed");
    }

    const agentId = transferLogs[transferLogs.length - 1].args.tokenId!;

    const identityReadContract = getContract({
      address: IDENTITY_REGISTRY,
      abi: [
        {
          name: "ownerOf",
          type: "function",
          stateMutability: "view",
          inputs: [{ name: "tokenId", type: "uint256" }],
          outputs: [{ name: "", type: "address" }],
        },
        {
          name: "tokenURI",
          type: "function",
          stateMutability: "view",
          inputs: [{ name: "tokenId", type: "uint256" }],
          outputs: [{ name: "", type: "string" }],
        },
      ],
      client: publicClient,
    });

    const owner = await identityReadContract.read.ownerOf([agentId]);
    const tokenURI = await identityReadContract.read.tokenURI([agentId]);

    console.log(`Agent ID: ${agentId}`);
    console.log(`Owner: ${owner}`);
    console.log(`Metadata URI: ${tokenURI}`);
    ```

    Your AI agent now has a unique onchain identity.

    ## Step 6. Record agent reputation

    Build your agent's reputation by recording feedback. Use the **validator
    wallet**—per ERC-8004, agent owners cannot record reputation for their own
    agents.

    ```typescript index.ts theme={null}
    import { keccak256, toHex } from "viem";

    const REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713";

    const validatorAccount = privateKeyToAccount(
      process.env.VALIDATOR_PRIVATE_KEY as `0x${string}`,
    );

    const validatorWalletClient = createWalletClient({
      account: validatorAccount,
      chain: arcTestnet,
      transport: http(),
    });

    const tag = "successful_trade";
    const feedbackHash = keccak256(toHex(tag));

    const reputationContract = getContract({
      address: REPUTATION_REGISTRY,
      abi: [
        {
          name: "giveFeedback",
          type: "function",
          stateMutability: "nonpayable",
          inputs: [
            { name: "agentId", type: "uint256" },
            { name: "score", type: "int128" },
            { name: "feedbackType", type: "uint8" },
            { name: "tag", type: "string" },
            { name: "metadataURI", type: "string" },
            { name: "evidenceURI", type: "string" },
            { name: "comment", type: "string" },
            { name: "feedbackHash", type: "bytes32" },
          ],
          outputs: [],
        },
      ],
      client: { public: publicClient, wallet: validatorWalletClient },
    });

    const reputationTx = await reputationContract.write.giveFeedback(
      [agentId, 95n, 0, tag, "", "", "", feedbackHash],
      { account: validatorAccount },
    );

    await publicClient.waitForTransactionReceipt({ hash: reputationTx });
    console.log(`Reputation: https://explorer.testnet.arc.io/tx/${reputationTx}`);
    ```

    <Note>
      **Production scoring**: This quickstart hardcodes `score: 95` for demonstration.
      In production, calculate scores dynamically based on agent behavior. For
      example, `score = loanRepaidOnTime ? 100 : 20` for lending protocols, or
      `score = slippagePct < 1 ? 95 : 60` for trading platforms.

      The ReputationRegistry stores attestations from external observers who witnessed
      the agent's actions. Your application logic calculates scores based on outcomes,
      then records them onchain.
    </Note>

    ## Step 7. Request and verify validation

    The ERC-8004 ValidationRegistry uses a two-step request/response flow. The
    **agent owner** requests validation from a validator, then the **validator**
    submits a response.

    ```typescript index.ts theme={null}
    const VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272";

    const requestURI = "ipfs://bafkreiexamplevalidationrequest";
    const requestHash = keccak256(
      toHex(`kyc_verification_request_agent_${agentId}`),
    );

    const validationContract = getContract({
      address: VALIDATION_REGISTRY,
      abi: [
        {
          name: "validationRequest",
          type: "function",
          stateMutability: "nonpayable",
          inputs: [
            { name: "validator", type: "address" },
            { name: "agentId", type: "uint256" },
            { name: "requestURI", type: "string" },
            { name: "requestHash", type: "bytes32" },
          ],
          outputs: [],
        },
        {
          name: "validationResponse",
          type: "function",
          stateMutability: "nonpayable",
          inputs: [
            { name: "requestHash", type: "bytes32" },
            { name: "response", type: "uint8" },
            { name: "responseURI", type: "string" },
            { name: "responseHash", type: "bytes32" },
            { name: "tag", type: "string" },
          ],
          outputs: [],
        },
        {
          name: "getValidationStatus",
          type: "function",
          stateMutability: "view",
          inputs: [{ name: "requestHash", type: "bytes32" }],
          outputs: [
            { name: "validatorAddress", type: "address" },
            { name: "agentId", type: "uint256" },
            { name: "response", type: "uint8" },
            { name: "responseHash", type: "bytes32" },
            { name: "tag", type: "string" },
            { name: "lastUpdate", type: "uint256" },
          ],
        },
      ],
      client: publicClient,
    });

    const validationRequestTx = await ownerWalletClient.writeContract({
      address: VALIDATION_REGISTRY,
      abi: validationContract.abi,
      functionName: "validationRequest",
      args: [validatorAccount.address, agentId, requestURI, requestHash],
      account: ownerAccount,
    });

    await publicClient.waitForTransactionReceipt({ hash: validationRequestTx });

    const validationResponseTx = await validatorWalletClient.writeContract({
      address: VALIDATION_REGISTRY,
      abi: validationContract.abi,
      functionName: "validationResponse",
      args: [requestHash, 100, "", `0x${"0".repeat(64)}`, "kyc_verified"],
      account: validatorAccount,
    });

    await publicClient.waitForTransactionReceipt({ hash: validationResponseTx });

    type ValidationStatus = readonly [
      `0x${string}`,
      bigint,
      number,
      `0x${string}`,
      string,
      bigint,
    ];

    const [valAddr, , response, , tag] =
      (await validationContract.read.getValidationStatus([
        requestHash,
      ])) as ValidationStatus;

    console.log(`Validator: ${valAddr}`);
    console.log(`Response: ${response} (100 = passed)`);
    console.log(`Tag: ${tag}`);
    ```

    ## Full agent registration script

    The following complete script combines all the preceding steps into a single
    runnable file.

    ```typescript index.ts expandable theme={null}
    import {
      createPublicClient,
      createWalletClient,
      getContract,
      http,
      keccak256,
      parseAbiItem,
      toHex,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    const IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e";
    const REPUTATION_REGISTRY = "0x8004B663056A597Dffe9eCcC1965A193B7388713";
    const VALIDATION_REGISTRY = "0x8004Cb1BF31DAf7788923b405b754f57acEB4272";

    const METADATA_URI =
      process.env.METADATA_URI ||
      "ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoei";

    const ownerAccount = privateKeyToAccount(
      process.env.OWNER_PRIVATE_KEY as `0x${string}`,
    );
    const validatorAccount = privateKeyToAccount(
      process.env.VALIDATOR_PRIVATE_KEY as `0x${string}`,
    );

    const publicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(),
    });

    const ownerWalletClient = createWalletClient({
      account: ownerAccount,
      chain: arcTestnet,
      transport: http(),
    });

    const validatorWalletClient = createWalletClient({
      account: validatorAccount,
      chain: arcTestnet,
      transport: http(),
    });

    const identityAbi = [
      {
        name: "register",
        type: "function",
        stateMutability: "nonpayable",
        inputs: [{ name: "metadataURI", type: "string" }],
        outputs: [],
      },
      {
        name: "ownerOf",
        type: "function",
        stateMutability: "view",
        inputs: [{ name: "tokenId", type: "uint256" }],
        outputs: [{ name: "", type: "address" }],
      },
      {
        name: "tokenURI",
        type: "function",
        stateMutability: "view",
        inputs: [{ name: "tokenId", type: "uint256" }],
        outputs: [{ name: "", type: "string" }],
      },
    ] as const;

    const reputationAbi = [
      {
        name: "giveFeedback",
        type: "function",
        stateMutability: "nonpayable",
        inputs: [
          { name: "agentId", type: "uint256" },
          { name: "score", type: "int128" },
          { name: "feedbackType", type: "uint8" },
          { name: "tag", type: "string" },
          { name: "metadataURI", type: "string" },
          { name: "evidenceURI", type: "string" },
          { name: "comment", type: "string" },
          { name: "feedbackHash", type: "bytes32" },
        ],
        outputs: [],
      },
    ] as const;

    const validationAbi = [
      {
        name: "validationRequest",
        type: "function",
        stateMutability: "nonpayable",
        inputs: [
          { name: "validator", type: "address" },
          { name: "agentId", type: "uint256" },
          { name: "requestURI", type: "string" },
          { name: "requestHash", type: "bytes32" },
        ],
        outputs: [],
      },
      {
        name: "validationResponse",
        type: "function",
        stateMutability: "nonpayable",
        inputs: [
          { name: "requestHash", type: "bytes32" },
          { name: "response", type: "uint8" },
          { name: "responseURI", type: "string" },
          { name: "responseHash", type: "bytes32" },
          { name: "tag", type: "string" },
        ],
        outputs: [],
      },
      {
        name: "getValidationStatus",
        type: "function",
        stateMutability: "view",
        inputs: [{ name: "requestHash", type: "bytes32" }],
        outputs: [
          { name: "validatorAddress", type: "address" },
          { name: "agentId", type: "uint256" },
          { name: "response", type: "uint8" },
          { name: "responseHash", type: "bytes32" },
          { name: "tag", type: "string" },
          { name: "lastUpdate", type: "uint256" },
        ],
      },
    ] as const;

    type ValidationStatus = readonly [
      `0x${string}`,
      bigint,
      number,
      `0x${string}`,
      string,
      bigint,
    ];

    async function waitForReceipt(hash: `0x${string}`, label: string) {
      console.log(`  Waiting for ${label}: ${hash}`);
      const receipt = await publicClient.waitForTransactionReceipt({ hash });
      console.log(`  ${label} confirmed in block ${receipt.blockNumber}`);
      console.log(`  Explorer: https://explorer.testnet.arc.io/tx/${hash}`);
      return receipt;
    }

    async function main() {
      console.log("\n── Step 1: Prepare wallets ──");
      console.log(`  Owner: ${ownerAccount.address}`);
      console.log(`  Validator: ${validatorAccount.address}`);

      console.log("\n── Step 2: Register agent identity ──");
      console.log(`  Metadata URI: ${METADATA_URI}`);

      const registerTx = await ownerWalletClient.writeContract({
        address: IDENTITY_REGISTRY,
        abi: identityAbi,
        functionName: "register",
        args: [METADATA_URI],
        account: ownerAccount,
      });

      const receipt = await waitForReceipt(registerTx, "Registration");

      console.log("\n── Step 3: Retrieve agent ID ──");

      const transferLogs = await publicClient.getLogs({
        address: IDENTITY_REGISTRY,
        event: parseAbiItem(
          "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)",
        ),
        args: { to: ownerAccount.address },
        fromBlock: receipt.blockNumber,
        toBlock: receipt.blockNumber,
      });

      if (transferLogs.length === 0) {
        throw new Error("No Transfer events found in the registration block");
      }

      const agentId = transferLogs[transferLogs.length - 1].args.tokenId;
      if (agentId == null) {
        throw new Error("Registration event did not include a tokenId");
      }

      const identityContract = getContract({
        address: IDENTITY_REGISTRY,
        abi: identityAbi,
        client: publicClient,
      });

      const owner = await identityContract.read.ownerOf([agentId]);
      const tokenURI = await identityContract.read.tokenURI([agentId]);

      console.log(`  Agent ID: ${agentId}`);
      console.log(`  Owner: ${owner}`);
      console.log(`  Metadata URI: ${tokenURI}`);

      console.log("\n── Step 4: Record reputation ──");

      const tag = "successful_trade";
      const feedbackHash = keccak256(toHex(tag));

      const reputationContract = getContract({
        address: REPUTATION_REGISTRY,
        abi: reputationAbi,
        client: { public: publicClient, wallet: validatorWalletClient },
      });

      const reputationTx = await reputationContract.write.giveFeedback(
        [agentId, 95n, 0, tag, "", "", "", feedbackHash],
        { account: validatorAccount },
      );

      const reputationReceipt = await waitForReceipt(reputationTx, "Reputation");

      console.log("\n── Step 5: Verify reputation ──");

      const fromBlock =
        reputationReceipt.blockNumber > 1000n
          ? reputationReceipt.blockNumber - 1000n
          : 0n;

      const reputationLogs = await publicClient.getLogs({
        address: REPUTATION_REGISTRY,
        fromBlock,
        toBlock: "latest",
      });

      console.log(`  Found ${reputationLogs.length} feedback event(s)`);

      console.log("\n── Step 6: Request validation ──");
      const requestURI = "ipfs://bafkreiexamplevalidationrequest";
      const requestHash = keccak256(
        toHex(`kyc_verification_request_agent_${agentId}`),
      );

      const validationRequestContract = getContract({
        address: VALIDATION_REGISTRY,
        abi: validationAbi,
        client: { public: publicClient, wallet: ownerWalletClient },
      });

      const validationRequestTx =
        await validationRequestContract.write.validationRequest(
          [validatorAccount.address, agentId, requestURI, requestHash],
          { account: ownerAccount },
        );

      await waitForReceipt(validationRequestTx, "Validation request");

      console.log("\n── Step 7: Validation response ──");

      const validationResponseContract = getContract({
        address: VALIDATION_REGISTRY,
        abi: validationAbi,
        client: { public: publicClient, wallet: validatorWalletClient },
      });

      const validationResponseTx =
        await validationResponseContract.write.validationResponse(
          [
            requestHash,
            100,
            "",
            `0x${"0".repeat(64)}` as `0x${string}`,
            "kyc_verified",
          ],
          { account: validatorAccount },
        );

      await waitForReceipt(validationResponseTx, "Validation response");

      console.log("\n── Step 8: Check validation ──");

      const validationReadContract = getContract({
        address: VALIDATION_REGISTRY,
        abi: validationAbi,
        client: publicClient,
      });

      const [valAddr, , response, , validationTag] =
        (await validationReadContract.read.getValidationStatus([
          requestHash,
        ])) as ValidationStatus;

      console.log(`  Validator: ${valAddr}`);
      console.log(`  Response: ${response} (100 = passed)`);
      console.log(`  Tag: ${validationTag}`);

      console.log("\n── Complete ──");
      console.log("  ✓ Identity registered");
      console.log("  ✓ Reputation recorded");
      console.log("  ✓ Validation requested and verified");
    }

    main().catch((error) => {
      console.error("\nError:", error.message ?? error);
      process.exit(1);
    });
    ```

    Save it, then run:

    ```shell theme={null}
    npm run start
    ```
  </Tab>
</Tabs>

## Summary

After completing this quickstart, you've successfully:

* Created or prepared two Arc Testnet wallets for the ERC-8004 flow
* Registered an AI agent with a unique onchain identity (ERC-721 token)
* Recorded reputation feedback from an external validator
* Requested validation from a validator and verified the response onchain
