Register an Agent
This guide walks you through registering an AI Agent on the MIZUHIKI Agent Registry: minting an ERC-8004 Agent NFT that records the agent's owner, trading wallet, and instructions, and verifying that the owner is a KYC'd entity through MIZUHIKI ID.
The Agent Registry is deployed on the Awaji testnet for development and testing. Do not register agents that control real funds.
Register from your browser
Register an agent and browse the live registry right here with no setup required. The "Register" form builds the ERC-8004 agent registration JSON in your browser and you officially register this on the Awaji testnet with your own wallet (e.g. MetaMask).
The "Dashboard" tab indexes the registry and shows each agent's owner KYC status live from the verifier.
Prefer to do it programmatically? The rest of this guide walks through the same flow with viem.
How the registry works
MizuhikiAgentIdentityRegistry is an ERC-721 contract (Mizuhiki Agent / MAGENT). Each agent is a token that ties together:
- Owner — this is the address that holds the Agent NFT (
ownerOf(agentId)). Transferring the NFT token transfers the agent to the new account (as per standard ERC-721 ownership). - Trading wallet — this is the address the agent trades from. It is bound separately and must sign an EIP-712 message proving it controls the key (
setAgentWallet). Transferring the agent automatically clears the trading wallet. - Instructions & metadata — arbitrary on-chain
key → bytesentries viasetMetadata/getMetadata.agentURIis an optional off-chain argument (similar to ERC-721tokenURI).
KYC is not stored on the registry. To check whether an agent is owned by a KYC'd entity, read ownerOf(agentId) and check that address against the MIZUHIKI Verified SBT — see Verify owner KYC status below.
Deployments
All contracts below are deployed on the MIZUHIKI Testnet Awaji — Chain ID 6497Click to Copy, RPC https://rpc.awaji.mizuhiki.ioClick to Copy. The registry is ERC-8004 (Trustless Agents), built on ERC-721.
| Contract | Address |
|---|---|
Agent Registry — MizuhikiAgentIdentityRegistry | 0xBF1a6870dfeeB66B25951b1DdBcB936C6D2954F7Click to Copy |
Agent Verifier — MizuhikiAgentVerifier | 0x92566308971c7780Fa83eE77Ef00B87ec5D6fD48Click to Copy |
| KYC SBT — registered on the verifier | 0x7828549D695777Bb01567aE7954F7586243bd294Click to Copy |
This guide uses the Agent Registry. View the verified contract on Blockscout ↗
Prerequisites
- Node.js v22+ — check with
node --version. - An owner wallet — funded with testnet
MIZUfor gas via the faucet. This wallet will own the Agent NFT. - A trading wallet — the address your agent executes from. You need its private key to sign the binding message in step 4.
Install dependencies:
npm install viem
1. Set up the clients
Define the Awaji chain and create a public client (reads) and a wallet client (writes) for the owner account.
import { createPublicClient, createWalletClient, http, defineChain } from "viem";
import { privateKeyToAccount } from "viem/accounts";
export const awaji = defineChain({
id: 6497,
name: "MIZUHIKI Testnet Awaji",
nativeCurrency: { decimals: 18, name: "Mizu", symbol: "MIZU" },
rpcUrls: { default: { http: ["https://rpc.awaji.mizuhiki.io"] } },
blockExplorers: { default: { name: "Explorer", url: "https://awaji.blockscout.com" } },
});
export const REGISTRY = "0xBF1a6870dfeeB66B25951b1DdBcB936C6D2954F7";
// Owner account — owns the Agent NFT and sends the transactions
export const owner = privateKeyToAccount(process.env.OWNER_PRIVATE_KEY);
export const publicClient = createPublicClient({ chain: awaji, transport: http() });
export const ownerClient = createWalletClient({ account: owner, chain: awaji, transport: http() });
The functions used in this guide:
export const registryAbi = [
{ type: "function", name: "register", stateMutability: "nonpayable",
inputs: [
{ name: "agentURI", type: "string" },
{ name: "metadata", type: "tuple[]", components: [
{ name: "metadataKey", type: "string" },
{ name: "metadataValue", type: "bytes" },
]},
],
outputs: [{ name: "agentId", type: "uint256" }] },
{ type: "function", name: "ownerOf", stateMutability: "view",
inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ type: "address" }] },
{ type: "function", name: "walletNonce", stateMutability: "view",
inputs: [{ type: "uint256" }], outputs: [{ type: "uint256" }] },
{ type: "function", name: "setAgentWallet", stateMutability: "nonpayable",
inputs: [
{ name: "agentId", type: "uint256" }, { name: "newWallet", type: "address" },
{ name: "deadline", type: "uint256" }, { name: "signature", type: "bytes" },
], outputs: [] },
{ type: "function", name: "getAgentWallet", stateMutability: "view",
inputs: [{ name: "agentId", type: "uint256" }], outputs: [{ type: "address" }] },
{ type: "function", name: "setMetadata", stateMutability: "nonpayable",
inputs: [
{ name: "agentId", type: "uint256" }, { name: "metadataKey", type: "string" },
{ name: "metadataValue", type: "bytes" },
], outputs: [] },
{ type: "function", name: "getMetadata", stateMutability: "view",
inputs: [{ name: "agentId", type: "uint256" }, { name: "metadataKey", type: "string" }],
outputs: [{ type: "bytes" }] },
{ type: "event", name: "Registered",
inputs: [
{ name: "agentId", type: "uint256", indexed: true },
{ name: "agentURI", type: "string", indexed: false },
{ name: "owner", type: "address", indexed: true },
] },
];
2. Register an agent
Calling register mints a new Agent NFT to the owner and assigns a sequential agentId. You can pass an off-chain agentURI and any initial on-chain metadata entries. Each metadata value is bytes, so encode strings with stringToHex.
agentWallet is a reserved metadata key — it is managed only through setAgentWallet (step 4) and cannot be set via register or setMetadata.
import { stringToHex, parseEventLogs } from "viem";
import { awaji, REGISTRY, owner, publicClient, ownerClient } from "./config.js";
import { registryAbi } from "./abi.js";
const hash = await ownerClient.writeContract({
address: REGISTRY,
abi: registryAbi,
functionName: "register",
args: [
"https://example.com/agents/the-best-trader.json", // off-chain metadata (optional, can be "")
[
{ metadataKey: "name", metadataValue: stringToHex("The Best Trader") },
{ metadataKey: "instructions", metadataValue: stringToHex("Do not trade on sports.") },
],
],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
// Read the assigned agentId from the Registered event
const [registered] = parseEventLogs({ abi: registryAbi, eventName: "Registered", logs: receipt.logs });
const agentId = registered.args.agentId;
console.log(`✓ Registered agent #${agentId} owned by ${registered.args.owner}`);
There are also register() and register(string agentURI) overloads if you don't need initial on-chain metadata.
3. Store agent instructions
Instructions and other metadata are plain on-chain key → bytes entries you can update at any time. Set them with setMetadata and read them back with getMetadata.
import { stringToHex, hexToString } from "viem";
import { REGISTRY, publicClient, ownerClient } from "./config.js";
import { registryAbi } from "./abi.js";
const agentId = 1n; // the id from step 2
// Update the instructions
await ownerClient.writeContract({
address: REGISTRY,
abi: registryAbi,
functionName: "setMetadata",
args: [agentId, "instructions", stringToHex("Do not trade on sports or politics.")],
});
// Read them back
const raw = await publicClient.readContract({
address: REGISTRY, abi: registryAbi, functionName: "getMetadata",
args: [agentId, "instructions"],
});
console.log("Instructions:", hexToString(raw));
4. Bind the trading wallet
The trading wallet is bound separately from the owner. The trading wallet must sign an EIP-712 SetAgentWallet message proving it controls the key.
The owner (of the agent NFT) then submits the setAgentWallet transaction with this EIP-712 SetAgentWallet signature.
import { privateKeyToAccount } from "viem/accounts";
import { awaji, REGISTRY, publicClient, ownerClient } from "./config.js";
import { registryAbi } from "./abi.js";
const agentId = 1n;
// The wallet the agent will trade from
const trading = privateKeyToAccount(process.env.TRADING_PRIVATE_KEY);
// Per-agent nonce gives replay protection
const nonce = await publicClient.readContract({
address: REGISTRY, abi: registryAbi, functionName: "walletNonce", args: [agentId],
});
const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // valid for 1 hour
// The trading wallet signs the EIP-712 typed data
const signature = await trading.signTypedData({
domain: {
name: "MizuhikiAgentIdentityRegistry",
version: "1",
chainId: awaji.id,
verifyingContract: REGISTRY,
},
types: {
SetAgentWallet: [
{ name: "agentId", type: "uint256" },
{ name: "newWallet", type: "address" },
{ name: "deadline", type: "uint256" },
{ name: "nonce", type: "uint256" },
],
},
primaryType: "SetAgentWallet",
message: { agentId, newWallet: trading.address, deadline, nonce },
});
// The owner submits the binding transaction
const hash = await ownerClient.writeContract({
address: REGISTRY,
abi: registryAbi,
functionName: "setAgentWallet",
args: [agentId, trading.address, deadline, signature],
});
await publicClient.waitForTransactionReceipt({ hash });
const wallet = await publicClient.readContract({
address: REGISTRY, abi: registryAbi, functionName: "getAgentWallet", args: [agentId],
});
console.log(`✓ Agent #${agentId} now trades from ${wallet}`);
Requiring a signature from the trading wallet prevents an owner from binding an address they don't actually control. For the same reason, transferring the Agent NFT clears the bound wallet — the new owner must re-bind.
5. Verify owner KYC status
To confirm an agent is owned by a KYC'd entity, read its owner and check that address against the MIZUHIKI Verified SBT. The SBT exposes both balanceOf(address) and isVerified(address).
import { REGISTRY, publicClient } from "./config.js";
import { registryAbi } from "./abi.js";
// MIZUHIKI Verified SBT — see the compliance docs for the canonical address & interface
const MIZUHIKI_VERIFIED_SBT = "0x7828549D695777Bb01567aE7954F7586243bd294";
const agentId = 1n;
const owner = await publicClient.readContract({
address: REGISTRY, abi: registryAbi, functionName: "ownerOf", args: [agentId],
});
const isVerified = await publicClient.readContract({
address: MIZUHIKI_VERIFIED_SBT,
abi: [{ type: "function", name: "isVerified", stateMutability: "view",
inputs: [{ name: "account", type: "address" }], outputs: [{ type: "bool" }] }],
functionName: "isVerified",
args: [owner],
});
console.log(`Agent #${agentId} owner ${owner} — KYC'd: ${isVerified ? "✓" : "✗"}`);
The Verified SBT address and verification interface (balanceOf / isVerified) are documented in Create a Compliant dApp and MIZUHIKI Verified Addresses. Use that page as the source of truth, and gate access from your own contract by requiring balanceOf(owner) > 0.
Contract reference
| Function | Purpose |
|---|---|
register() · register(agentURI) · register(agentURI, metadata[]) | Mint a new Agent NFT; returns agentId. |
ownerOf(agentId) | The agent's owner (ERC-721). Use for KYC checks. |
setAgentWallet(agentId, newWallet, deadline, signature) | Bind the trading wallet (EIP-712 signed by newWallet). |
getAgentWallet(agentId) · unsetAgentWallet(agentId) | Read / clear the bound trading wallet. |
setMetadata(agentId, key, value) · getMetadata(agentId, key) | Set / read on-chain metadata (e.g. instructions). |
setAgentURI(agentId, newURI) · tokenURI(agentId) | Set / read the off-chain metadata URI. |
walletNonce(agentId) | Per-agent nonce for setAgentWallet replay protection. |
Events: Registered(agentId, agentURI, owner), AgentWalletSet(agentId, newWallet), MetadataSet(agentId, metadataKey, metadataValue), URIUpdated(agentId, newURI, updatedBy).
Next steps
- AI Agent Registry Overview — concepts and architecture.
- MIZUHIKI ID Overview — how KYC verification works on MIZUHIKI.
- Create a Compliant dApp — gate access to verified users from your own contract.