ERC-8004 contracts on Arc Testnet
| Contract | Address |
|---|---|
| IdentityRegistry | 0x8004A818BFB912233c491871b3d84c89A494BD9e |
| ReputationRegistry | 0x8004B663056A597Dffe9eCcC1965A193B7388713 |
| ValidationRegistry | 0x8004Cb1BF31DAf7788923b405b754f57acEB4272 |
- Circle Wallets
- Viem
Prerequisites
Before you begin, make sure you have:- A Circle Developer Console account
- An API key created in the Console: Keys → Create a key → API key → Standard Key
- Your Entity Secret registered
Step 1. Set up your project
Create a project directory, install dependencies, and configure your environment.1.1. Create the project and install dependencies
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
1.2. Configure TypeScript (optional)
This step is optional. It helps prevent missing types in your IDE or editor.
tsconfig.json file:npx tsc --init
tsconfig.json file: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:CIRCLE_API_KEY=YOUR_API_KEY
CIRCLE_ENTITY_SECRET=YOUR_ENTITY_SECRET
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.Prefer editing
.env files in your IDE or editor so credentials are not
leaked to your shell history.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. 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 at the end of this tutorial.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}`);
Step 3. Prepare agent metadata
Create a JSON file with metadata for your agent. The structure below 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.agent-metadata.json
{
"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"
}
ipfs://QmYourHash....For this quickstart, you can skip uploading and use the example URI:
ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoeiStep 4. Register your agent identity
Callregister(metadataURI) on the IdentityRegistry to mint an identity NFT for
your agent.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://testnet.arcscan.app/tx/${txHash}`);
With Circle Gas Station, your application sponsors the transaction fees. On
Arc, gas is approximately 0.006 USDC-TESTNET per transaction.
Step 5. Retrieve your agent ID
Query theTransfer event from the IdentityRegistry to find the token ID minted
for your agent.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}`);
Step 6. Record 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.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)
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.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.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}`);
Full agent registration script
The complete script below combines all the preceding steps into a single runnable file.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://testnet.arcscan.app/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://testnet.arcscan.app/address/${ownerWallet.address}\n`,
);
}
main().catch((error) => {
console.error("\nError:", error.message ?? error);
process.exit(1);
});
npm run start
If you followed the Python workflow, run
deactivate when you’re done to exit
the virtual environment.Prerequisites
Before you begin, make sure you have:- Installed Node.js v22+
- 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
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)
This step is optional. It helps prevent missing types in your IDE or editor.
tsconfig.json file:npx tsc --init
tsconfig.json file: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:.env
OWNER_PRIVATE_KEY=0xYOUR_OWNER_PRIVATE_KEY
VALIDATOR_PRIVATE_KEY=0xYOUR_VALIDATOR_PRIVATE_KEY
OWNER_PRIVATE_KEYis the0x-prefixed private key for the Arc Testnet wallet that owns the agent and requests validation.VALIDATOR_PRIVATE_KEYis the0x-prefixed private key for the Arc Testnet wallet that records reputation and submits the validation response.
npm run start command loads variables from .env using Node.js native
env-file support.Prefer editing
.env files in your IDE or editor so credentials are not
leaked to your shell history.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. 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 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:cast wallet new --json
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 structure below 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.agent-metadata.json
{
"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"
}
ipfs://QmYourHash....For this quickstart, you can skip uploading and use the example URI:
ipfs://bafkreibdi6623n3xpf7ymk62ckb4bo75o3qemwkpfvp5i25j66itxvsoeiStep 4. Register your agent identity
Callregister(metadataURI) on the IdentityRegistry to mint an identity NFT for
your agent.index.ts
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://testnet.arcscan.app/tx/${registerTx}`);
Step 5. Retrieve your agent ID
Query theTransfer event from the IdentityRegistry to find the token ID minted
for your agent.index.ts
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}`);
Step 6. Record 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.index.ts
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://testnet.arcscan.app/tx/${reputationTx}`);
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.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.index.ts
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 complete script below combines all the preceding steps into a single runnable file.index.ts
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://testnet.arcscan.app/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);
});
npm run start
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