Skill: elitex45/zeruai (version 1.0.2)

--- name: zeruai description: Register agents on the Zeru ERC-8004 Identity Registry, manage wallets and metadata, and read on-chain state. Use when an agent needs to register on-chain, check fees, read agent info, set metadata, or manage agent wallets on Base Mainnet or Base Sepolia. user-invocable: true metadata: {"openclaw":{"requires":{"env":["PRIVATE_KEY"],"bins":["node","npx"]},"primaryEnv":"PRIVATE_KEY"}} --- # Zeru ERC-8004 Identity Registry Register and manage AI agents on the Zeru Identity Registry. Defaults to Base Mainnet (0.0025 ETH fee). Use `--chain 84532` for Base Sepolia testnet. ## One-Time Setup Run once to install dependencies: ```bash cd {baseDir} && npm install ``` ## Agent JSON Structure (ERC-8004 registration-v1) When registering an agent, you provide a JSON file describing the agent. The SDK auto-fills `type`, `registrations`, and defaults for `x402Support`/`active`/`image` if omitted. **Minimal JSON (just name + description + one service):** ```json { "name": "My AI Agent", "description": "A helpful AI agent that does X", "services": [ { "name": "web", "endpoint": "https://myagent.example.com" } ] } ``` **Full JSON (MCP + A2A + OASF + x402 payments):** ```json { "name": "DataAnalyst Pro", "description": "Enterprise-grade blockchain data analysis agent. Performs on-chain forensics, wallet profiling, and transaction pattern detection.", "image": "https://cdn.example.com/agents/analyst.png", "services": [ { "name": "MCP", "endpoint": "https://api.dataanalyst.ai/mcp", "version": "2025-06-18", "mcpTools": ["analyze_wallet", "trace_transactions", "detect_anomalies"], "capabilities": [] }, { "name": "A2A", "endpoint": "https://api.dataanalyst.ai/.well-known/agent-card.json", "version": "0.3.0", "a2aSkills": [[REDACTED_SECRET]] }, { "name": "OASF", "endpoint": "https://github.com/agntcy/oasf/", "version": "0.8.0", "skills": [[REDACTED_SECRET]], "domains": ["technology/blockchain"] }, { "name": "web", "endpoint": "https://dataanalyst.ai" }, { "name": "agentWallet", "endpoint": "eip155:8453:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7" } ], "x402Support": true, "active": true, "supportedTrust": ["reputation", "ERC-8004"] } ``` **All fields:** | Field | Type | Required | Default | Description | |-------|------|----------|---------|-------------| | `name` | string | Yes | — | Agent name (1–256 chars) | | `description` | string | Yes | — | What the agent does (max 2048 chars) | | `image` | string | No | placeholder | Avatar URL (HTTPS, IPFS, or Arweave) | | `services` | array | Yes | — | Service endpoints (1–64 items, see below) | | `x402Support` | boolean | No | `false` | Supports x402 payment protocol | | `active` | boolean | No | `true` | Agent is actively accepting requests | | `supportedTrust` | string[] | No | — | Trust models: `"reputation"`, `"crypto-economic"`, `"tee-attestation"`, `"ERC-8004"` | | `owner` | string | No | signer address | Owner 0x address (auto-set from PRIVATE_KEY) | **Service types:** | `name` | `endpoint` | Extra fields | |--------|-----------|--------------| | `"web"` | Website URL | — | | `"MCP"` | MCP server URL | `version`, `mcpTools[]`, `mcpPrompts[]`, `mcpResources[]`, `capabilities[]` | | `"A2A"` | Agent card URL (`/.well-known/agent-card.json`) | `version`, `a2aSkills[]` | | `"OASF"` | OASF repo URL | `version`, `skills[]`, `domains[]` | | `"agentWallet"` | CAIP-10 address (`eip155:{chainId}:{address}`) | — | | `"ENS"` | ENS name (e.g. `myagent.eth`) | — | | `"email"` | Email address | — | | custom | Any URL | `description` | ## Commands ### `/zeruai register --json <file>` Register a new agent using a full JSON file (recommended). Creates hosted agent URI, mints NFT on-chain, and updates URI with the real agentId. ``` /zeruai register --json agent.json /zeruai register --json agent.json --chain 84532 ``` **Steps to register:** 1. Create a JSON file following the structure above (e.g. `agent.json`) 2. Run: `npx tsx {baseDir}/scripts/zeru.ts register --json agent.json` The SDK automatically adds `type`, `registrations` (with `agentId: 0` placeholder), and defaults for missing optional fields. After minting, it updates the document with the real `agentId`. ### `/zeruai register --name <name> --description <desc> --endpoint <url>` Simple registration (single API endpoint only). For richer agents, use `--json` instead. ``` /zeruai register --name "Trading Bot" --description "AI-powered trading agent" --endpoint "https://mybot.com/api" /zeruai register --name "Data Analyzer" --description "Analyzes datasets" --endpoint "https://analyzer.ai/api" --image "https://example.com/icon.png" /zeruai register --name "Test Bot" --description "Testing" --endpoint "https://test.com" --chain 84532 ``` Requires `PRIVATE_KEY` env var. Wallet must have fee + gas (e.g. ~0.003 ETH on mainnet). To run: `npx tsx {baseDir}/scripts/zeru.ts register --name "..." --description "..." --endpoint "..."` ### `/zeruai read <agentId>` Read an agent's on-chain data: owner, URI, wallet, name, services. ``` /zeruai read 16 ``` To run: `npx tsx {baseDir}/scripts/zeru.ts read 16` ### `/zeruai fee` Check current registration fee and whether registration is open. ``` /zeruai fee ``` To run: `npx tsx {baseDir}/scripts/zeru.ts fee` ### `/zeruai set-metadata <agentId> --key <key> --value <value>` Set custom metadata on an agent. Only the owner can call. ``` /zeruai set-metadata 16 --key "category" --value "trading" ``` Requires `PRIVATE_KEY`. To run: `npx tsx {baseDir}/scripts/zeru.ts set-metadata 16 --key "category" --value "trading"` ### `/zeruai unset-wallet <agentId>` Clear the agent wallet. Only the owner can call. ``` /zeruai unset-wallet 16 ``` Requires `PRIVATE_KEY`. To run: `npx tsx {baseDir}/scripts/zeru.ts unset-wallet 16` ## Setup ### Read-Only (no setup needed) `read` and `fee` work without a private key. ### With Wallet (for registration and writes) Add to your OpenClaw config (`~/.openclaw/openclaw.json`): ```json { "skills": { "entries": { "zeruai": { "enabled": true, "env": { "PRIVATE_KEY": "0xYourFundedPrivateKey" } } } } } ``` Optional env: - `RPC_URL` — override default RPC - `CHAIN_ID` — override chain (default: `8453` for Base Mainnet, use `84532` for Base Sepolia) ## Contract Info ### Base Mainnet (default, chainId 8453) - **Identity Registry:** [REDACTED_SECRET] - **Reputation Registry:** [REDACTED_SECRET] - **Registration Fee:** 0.0025 ETH - **RPC:** https://mainnet.base.org ### Base Sepolia (testnet, chainId 84532) - **Identity Registry:** [REDACTED_SECRET] - **Reputation Registry:** [REDACTED_SECRET] - **Registration Fee:** 0.001 ETH - **RPC:** https://sepolia.base.org - **Source:** `zeru` ## How It Works 1. **register** creates a hosted JSON document (ERC-8004 registration-v1 schema) via the Agent URI API, mints an NFT on the Identity Registry (paying the fee), then updates the document with the real agentId. 2. **read** queries the on-chain contract for owner, tokenURI, and agentWallet, then fetches and parses the URI JSON. 3. **fee** reads the current `registrationFee()` and `registrationEnabled()` from the contract. 4. **set-metadata** calls `setMetadata(agentId, key, value)` on the contract. 5. **unset-wallet** calls `unsetAgentWallet(agentId)` on the contract.

path: package.json
content: {
  "private": true,
  "type": "module",
  "dependencies": {
    "ethers": "6.13.4",
    "zod": "3.23.8",
    "canonicalize": "2.0.0",
    "tsx": "4.19.2"
  }
}


path: lib/get-reputation.js
content: import { discoveryUrl } from "./config.js";
import { apiError } from "./errors.js";
/**
 * Get feedback/reputation data for an agent from the Discovery API.
 */
export async function getReputation(config, chainId, agentId, includeRevoked = false) {
    const base = discoveryUrl(config);
    const url = new URL(`${base}/reputation/agents/${chainId}/${agentId}`);
    url.searchParams.set("source", config.source);
    if (!includeRevoked)
        url.searchParams.set("includeRevoked", "false");
    const headers = {};
    if (config.discoveryApiKey)
        headers["x-api-key"] = config.discoveryApiKey;
    const res = await fetch(url.toString(), { headers });
    if (!res.ok) {
        let body;
        try {
            body = await res.json();
        }
        catch {
            body = await res.text();
        }
        throw apiError(`Reputation API error: ${res.status}`, res.status, body);
    }
    const data = (await res.json());
    return {
        results: Array.isArray(data.results) ? data.results : [],
        total: data.total ?? 0,
    };
}


path: lib/get-agent.js
content: import { Contract, JsonRpcProvider } from "ethers";
import { rpcError } from "./errors.js";
import { identityRegistryAbi } from "./abi.js";
/**
 * Read an agent's on-chain data (owner, tokenURI, agentWallet).
 * Optionally fetches and parses the agentURI JSON.
 */
export async function getAgent(config, agentId) {
    try {
        const provider = new JsonRpcProvider(config.rpcUrl);
        const registry = new Contract(config.identityRegistryAddress, identityRegistryAbi, provider);
        const id = BigInt(agentId);
        const [owner, agentURI, wallet] = await Promise.all([
            registry.ownerOf(id),
            registry.tokenURI(id),
            registry.getAgentWallet(id),
        ]);
        const ZERO = "0x0000000000000000000000000000000000000000";
        const result = {
            owner,
            agentURI,
            agentWallet: wallet === ZERO ? null : wallet,
        };
        // Try to fetch and parse the agentURI
        if (agentURI && agentURI.startsWith("http")) {
            try {
                const res = await fetch(agentURI);
                if (res.ok)
                    result.parsedJson = (await res.json());
            }
            catch {
                // skip — optional
            }
        }
        return result;
    }
    catch (e) {
        if (e instanceof Error && e.name === "ZeruError")
            throw e;
        throw rpcError(e instanceof Error ? e.message : "getAgent failed", { cause: e });
    }
}
/**
 * Read the current registration fee from the contract (in wei).
 */
export async function getRegistrationFee(config) {
    const provider = new JsonRpcProvider(config.rpcUrl);
    const registry = new Contract(config.identityRegistryAddress, identityRegistryAbi, provider);
    return registry.registrationFee();
}
/**
 * Check if registration is currently enabled.
 */
export async function isRegistrationEnabled(config) {
    const provider = new JsonRpcProvider(config.rpcUrl);
    const registry = new Contract(config.identityRegistryAddress, identityRegistryAbi, provider);
    return registry.registrationEnabled();
}


path: lib/register-agent.js
content: import { Contract, Interface } from "ethers";
import { rpcError } from "./errors.js";
import { identityRegistryAbi } from "./abi.js";
/**
 * Register an agent on-chain by calling registerWithFee().
 * Reads the current fee from the contract, sends the tx, and parses the Registered event.
 */
export async function registerAgent(config, params) {
    const { signer, agentURI } = params;
    if (!agentURI || agentURI.length > 2048) {
        throw rpcError("agentURI must be 1–2048 characters");
    }
    try {
        const registry = new Contract(config.identityRegistryAddress, identityRegistryAbi, signer);
        // Check registration is enabled
        const enabled = await registry.registrationEnabled();
        if (!enabled) {
            throw rpcError("Registration is currently disabled on this contract");
        }
        // Read fee
        const fee = await registry.registrationFee();
        // Send tx
        const tx = await registry.registerWithFee(agentURI, { value: fee });
        const receipt = await tx.wait();
        if (!receipt) {
            throw rpcError("Transaction failed (no receipt)");
        }
        // Parse agentId from Registered event
        const iface = new Interface(identityRegistryAbi);
        const event = receipt.logs
            .map((log) => {
            try {
                return iface.parseLog({ topics: log.topics, data: log.data });
            }
            catch {
                return null;
            }
        })
            .find((e) => e?.name === "Registered");
        if (!event?.args) {
            throw rpcError("Registered event not found in receipt", { txHash: receipt.hash });
        }
        return {
            agentId: event.args.agentId,
            txHash: receipt.hash,
            fee,
        };
    }
    catch (e) {
        if (e instanceof Error && e.name === "ZeruError")
            throw e;
        throw rpcError(e instanceof Error ? e.message : "registerAgent failed", { cause: e });
    }
}


path: lib/index.js
content: /**
 * @zeru/erc8004 — SDK for Zeru ERC-8004 Identity Registry
 *
 * Register agents, manage wallets & metadata, discover other agents.
 */
export { resolveConfig, agentUriApiUrl, discoveryUrl } from "./config.js";
// ── Errors ──
export { ErrorCode, ZeruError, rpcError, apiError, validationError } from "./errors.js";
// ── Schema ──
export { agentRegistrationSchema, createAgentURIInputSchema, parseCreateAgentURIInput, toAgentRegistration, } from "./schema.js";
// ── ABI ──
export { identityRegistryAbi } from "./abi.js";
// ── Agent URI ──
export { createAgentURI, updateAgentURI } from "./create-agent-uri.js";
// ── Registration ──
export { registerAgent } from "./register-agent.js";
// ── On-chain reads ──
export { getAgent, getRegistrationFee, isRegistrationEnabled } from "./get-agent.js";
// ── Wallet ──
export { setAgentWallet, unsetAgentWallet, getAgentWallet } from "./agent-wallet.js";
// ── Metadata ──
export { setMetadata, getMetadata } from "./metadata.js";
// ── Discovery ──
export { searchAgents } from "./search-agents.js";
// ── Reputation ──
export { getReputation } from "./get-reputation.js";


path: lib/config.js
content: /** Built-in chain defaults */
const CHAIN_DEFAULTS = {
    8453: {
        identityRegistryAddress: [REDACTED_SECRET],
        reputationRegistryAddress: [REDACTED_SECRET],
        rpcUrl: "https://mainnet.base.org",
        agentUriApiBase: "https://agenturi.zpass.ai",
        zeruApiBase: "https://agentapi.zpass.ai",
    },
    84532: {
        identityRegistryAddress: [REDACTED_SECRET],
        reputationRegistryAddress: [REDACTED_SECRET],
        rpcUrl: "https://sepolia.base.org",
        agentUriApiBase: "https://agenturi.zpass.ai",
        zeruApiBase: "https://agentapi.zpass.ai",
    },
};
const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
function stripTrailingSlash(url) {
    return url.replace(/\/$/, "");
}
/** Resolve a ZeruConfig by filling in defaults for known chains. */
export function resolveConfig(config) {
    const defaults = CHAIN_DEFAULTS[config.chainId];
    const identityRegistryAddress = config.identityRegistryAddress ?? defaults?.identityRegistryAddress;
    const rpcUrl = config.rpcUrl ?? defaults?.rpcUrl;
    const agentUriApiBase = config.agentUriApiBase ?? defaults?.agentUriApiBase;
    const zeruApiBase = config.zeruApiBase ?? defaults?.zeruApiBase;
    if (!identityRegistryAddress || !ADDRESS_REGEX.test(identityRegistryAddress)) {
        throw new Error(`identityRegistryAddress required for chainId ${config.chainId}`);
    }
    if (!rpcUrl) {
        throw new Error(`rpcUrl required for chainId ${config.chainId}`);
    }
    if (!agentUriApiBase) {
        throw new Error(`agentUriApiBase required for chainId ${config.chainId}`);
    }
    if (!zeruApiBase) {
        throw new Error(`zeruApiBase required for chainId ${config.chainId}`);
    }
    return {
        chainId: config.chainId,
        identityRegistryAddress,
        rpcUrl,
        agentUriApiBase: stripTrailingSlash(agentUriApiBase),
        zeruApiBase: stripTrailingSlash(zeruApiBase),
        discoveryApiKey: config.discoveryApiKey,
        source: config.source ?? "zeru",
    };
}
export function agentUriApiUrl(rc) {
    return `${rc.agentUriApiBase}/v1/agent-uri`;
}
export function discoveryUrl(rc) {
    return `${rc.zeruApiBase}/v1/discovery`;
}


path: lib/metadata.js
content: import { Contract, JsonRpcProvider, hexlify, toUtf8Bytes, toUtf8String } from "ethers";
import { rpcError } from "./errors.js";
import { identityRegistryAbi } from "./abi.js";
/**
 * Set custom metadata on an agent. Only the owner/approved can call.
 * Note: "agentWallet" is a reserved key — use setAgentWallet() instead.
 *
 * @param value - Metadata value as string (encoded to bytes) or raw hex bytes
 */
export async function setMetadata(config, signer, agentId, key, value) {
    try {
        const registry = new Contract(config.identityRegistryAddress, identityRegistryAbi, signer);
        const valueBytes = value.startsWith("0x") ? value : hexlify(toUtf8Bytes(value));
        const tx = await registry.setMetadata(agentId, key, valueBytes);
        const receipt = await tx.wait();
        return receipt.hash;
    }
    catch (e) {
        if (e instanceof Error && e.name === "ZeruError")
            throw e;
        throw rpcError(e instanceof Error ? e.message : "setMetadata failed", { cause: e });
    }
}
/**
 * Read metadata from an agent. Returns the value as a UTF-8 string, or raw hex if not valid UTF-8.
 * Returns null if no metadata exists for the key.
 */
export async function getMetadata(config, agentId, key) {
    const provider = new JsonRpcProvider(config.rpcUrl);
    const registry = new Contract(config.identityRegistryAddress, identityRegistryAbi, provider);
    const raw = await registry.getMetadata(agentId, key);
    if (!raw || raw === "0x" || raw === "0x0" || raw.length <= 2)
        return null;
    try {
        return toUtf8String(raw);
    }
    catch {
        return raw; // Return raw hex if not valid UTF-8
    }
}


path: lib/errors.js
content: /** Stable error codes for agent runtimes */
export const ErrorCode = {
    RPC_ERROR: "RPC_ERROR",
    API_ERROR: "API_ERROR",
    VALIDATION_ERROR: "VALIDATION_ERROR",
};
export class ZeruError extends Error {
    code;
    details;
    constructor(code, message, details) {
        super(message);
        this.code = code;
        this.details = details;
        this.name = "ZeruError";
        Object.setPrototypeOf(this, ZeruError.prototype);
    }
}
export function rpcError(message, details) {
    return new ZeruError(ErrorCode.RPC_ERROR, message, details);
}
export function apiError(message, status, body) {
    return new ZeruError(ErrorCode.API_ERROR, message, { status, body });
}
export function validationError(message, fields) {
    return new ZeruError(ErrorCode.VALIDATION_ERROR, message, { fields });
}


path: lib/schema.js
content: import { z } from "zod";
/** Service object — name + endpoint required, everything else optional and passed through */
const serviceSchema = z.object({
    name: z.string().min(1).max(128),
    endpoint: z.string().min(1).max(2048),
    version: z.string().max(64).optional(),
}).passthrough();
const registrationSchema = z.object({
    agentId: z.union([z.number(), z.string()]),
    agentRegistry: z.string().min(1).max(256),
});
/** Full ERC-8004 registration-v1 JSON (matches backend validation) */
export const agentRegistrationSchema = z.object({
    type: z.literal("https://eips.ethereum.org/EIPS/eip-8004#registration-v1"),
    name: z.string().min(1).max(256),
    description: z.string().max(2048),
    image: z.string().min(1).max(2048),
    services: z.array(serviceSchema).min(1).max(64),
    x402Support: z.boolean(),
    active: z.boolean(),
    registrations: z.array(registrationSchema).min(1).max(32),
    supportedTrust: z.array(z.string()).max(32).optional(),
    owner: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional(),
}).passthrough();
/** Simplified input for createAgentURI — SDK fills in boilerplate */
export const createAgentURIInputSchema = z.object({
    name: z.string().min(1).max(256),
    description: z.string().max(2048),
    image: z.string().min(1).max(2048).optional(),
    services: z.array(serviceSchema).min(1).max(64),
    x402Support: z.boolean().optional(),
    active: z.boolean().optional(),
    supportedTrust: z.array(z.string()).max(32).optional(),
    owner: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional(),
});
export function parseCreateAgentURIInput(input) {
    const parsed = createAgentURIInputSchema.safeParse(input);
    if (!parsed.success) {
        const msg = parsed.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
        throw new Error(msg);
    }
    return parsed.data;
}
/** Build full AgentRegistration JSON from simplified input */
export function toAgentRegistration(input, registryAddress, chainId) {
    return {
        type: "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
        name: input.name,
        description: input.description,
        image: input.image || "https://placehold.co/512x512/1a1a2e/white?text=Agent",
        services: input.services,
        x402Support: input.x402Support ?? false,
        active: input.active ?? true,
        registrations: [{
                agentId: 0, // placeholder — updated after on-chain mint
                agentRegistry: `eip155:${chainId}:${registryAddress}`,
            }],
        supportedTrust: input.supportedTrust,
        owner: input.owner,
    };
}


path: lib/create-agent-uri.js
content: import { keccak256, toUtf8Bytes } from "ethers";
import canon from "canonicalize";
const canonicalize = typeof canon === "function" ? canon : canon.default;
import { agentUriApiUrl } from "./config.js";
import { apiError, validationError } from "./errors.js";
import { parseCreateAgentURIInput, toAgentRegistration } from "./schema.js";
const CREATE_PREFIX = "Zeru Agent URI create\n";
const UPDATE_PREFIX = "Zeru Agent URI update\n";
function canonicalStringify(obj) {
    if (obj === null || typeof obj !== "object")
        return JSON.stringify(obj);
    const out = canonicalize(obj);
    if (out === undefined)
        throw new Error("JCS canonicalize returned undefined");
    return out;
}
function hashBody(body) {
    return keccak256(toUtf8Bytes(canonicalStringify(body)));
}
async function signCreate(signer, body) {
    const timestamp = Date.now();
    const bodyHash = hashBody(body);
    const message = `${CREATE_PREFIX}${timestamp}\n${bodyHash}`;
    const signature = await signer.signMessage(message);
    return { signature, timestamp };
}
async function signUpdate(signer, id, body) {
    const timestamp = Date.now();
    const bodyHash = hashBody(body);
    const message = `${UPDATE_PREFIX}${id}\n${timestamp}\n${bodyHash}`;
    const signature = await signer.signMessage(message);
    return { signature, timestamp };
}
/**
 * Build Agent Registration JSON from input and POST to the Agent URI API.
 * Signs the request with EIP-191 (personal_sign) for authentication.
 * Returns the hosted agentURI that can be passed to registerWithFee().
 */
export async function createAgentURI(config, signer, input) {
    let parsed;
    try {
        parsed = parseCreateAgentURIInput(input);
    }
    catch (e) {
        throw validationError(e instanceof Error ? e.message : "Invalid input");
    }
    const json = toAgentRegistration(parsed, config.identityRegistryAddress, config.chainId);
    const url = agentUriApiUrl(config);
    const { signature, timestamp } = await signCreate(signer, json);
    const res = await fetch(url, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "x-signature": signature,
            "x-timestamp": String(timestamp),
        },
        body: JSON.stringify(json),
    });
    if (!res.ok) {
        let body;
        try {
            body = await res.json();
        }
        catch {
            body = await res.text();
        }
        throw apiError(`Agent URI API error: ${res.status} ${res.statusText}`, res.status, body);
    }
    const data = (await res.json());
    if (!data.agentURI || !data.id) {
        throw apiError("Agent URI API did not return id/agentURI", res.status, data);
    }
    return { id: data.id, agentURI: data.agentURI, json };
}
/**
 * Update an existing agent URI document (e.g. to set the real agentId after minting).
 * Requires signature from the original owner.
 */
export async function updateAgentURI(config, signer, documentId, json) {
    const url = `${agentUriApiUrl(config)}/${documentId}`;
    const { signature, timestamp } = await signUpdate(signer, documentId, json);
    const res = await fetch(url, {
        method: "PUT",
        headers: {
            "Content-Type": "application/json",
            "x-signature": signature,
            "x-timestamp": String(timestamp),
        },
        body: JSON.stringify(json),
    });
    if (!res.ok) {
        let body;
        try {
            body = await res.json();
        }
        catch {
            body = await res.text();
        }
        throw apiError(`Agent URI API update error: ${res.status}`, res.status, body);
    }
    return (await res.json());
}


path: lib/agent-wallet.js
content: import { Contract, JsonRpcProvider } from "ethers";
import { rpcError } from "./errors.js";
import { identityRegistryAbi } from "./abi.js";
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
/**
 * Set the agent wallet to a new address.
 * Requires an EIP-712 signature from the new wallet proving consent.
 *
 * @param config - Resolved SDK config
 * @param signer - The agent owner (sends the tx)
 * @param agentId - The agent's token ID
 * @param newWallet - Address to set as agent wallet
 * @param deadline - Unix timestamp deadline (must be within 5 min of block.timestamp)
 * @param walletSignature - EIP-712 signature from the new wallet
 */
export async function setAgentWallet(config, signer, agentId, newWallet, deadline, walletSignature) {
    try {
        const registry = new Contract(config.identityRegistryAddress, identityRegistryAbi, signer);
        const tx = await registry.setAgentWallet(agentId, newWallet, deadline, walletSignature);
        const receipt = await tx.wait();
        return receipt.hash;
    }
    catch (e) {
        if (e instanceof Error && e.name === "ZeruError")
            throw e;
        throw rpcError(e instanceof Error ? e.message : "setAgentWallet failed", { cause: e });
    }
}
/**
 * Clear the agent wallet (set to zero address). Only the owner/approved can call.
 */
export async function unsetAgentWallet(config, signer, agentId) {
    try {
        const registry = new Contract(config.identityRegistryAddress, identityRegistryAbi, signer);
        const tx = await registry.unsetAgentWallet(agentId);
        const receipt = await tx.wait();
        return receipt.hash;
    }
    catch (e) {
        if (e instanceof Error && e.name === "ZeruError")
            throw e;
        throw rpcError(e instanceof Error ? e.message : "unsetAgentWallet failed", { cause: e });
    }
}
/**
 * Read the current agent wallet address (returns zero address if unset).
 */
export async function getAgentWallet(config, agentId) {
    const provider = new JsonRpcProvider(config.rpcUrl);
    const registry = new Contract(config.identityRegistryAddress, identityRegistryAbi, provider);
    const wallet = await registry.getAgentWallet(agentId);
    return wallet === ZERO_ADDRESS ? null : wallet;
}


path: lib/search-agents.js
content: import { discoveryUrl } from "./config.js";
import { apiError } from "./errors.js";
/**
 * Search or list agents via the Discovery API.
 * Source defaults to config.source ("zeru").
 */
export async function searchAgents(config, params = {}) {
    const base = discoveryUrl(config);
    const endpoint = params.q ? `${base}/search` : `${base}/agents`;
    const url = new URL(endpoint);
    url.searchParams.set("source", config.source);
    if (params.chainId != null)
        url.searchParams.set("chainId", String(params.chainId));
    if (params.q)
        url.searchParams.set("q", params.q);
    if (params.serviceType)
        url.searchParams.set("serviceType", params.serviceType);
    if (params.limit != null)
        url.searchParams.set("limit", String(params.limit));
    if (params.offset != null)
        url.searchParams.set("offset", String(params.offset));
    const headers = {};
    if (config.discoveryApiKey)
        headers["x-api-key"] = config.discoveryApiKey;
    const res = await fetch(url.toString(), { headers });
    if (!res.ok) {
        let body;
        try {
            body = await res.json();
        }
        catch {
            body = await res.text();
        }
        throw apiError(`Discovery API error: ${res.status}`, res.status, body);
    }
    const data = (await res.json());
    return {
        results: Array.isArray(data.results) ? data.results : [],
        total: data.total ?? 0,
    };
}


path: lib/abi.js
content: /** Minimal ABI for ZeruIdentityRegistryUpgradeable (v4.0.0) */
export const identityRegistryAbi = [
    // ── Registration ──
    "function registerWithFee(string agentURI) external payable returns (uint256 agentId)",
    "function registrationFee() external view returns (uint256)",
    "function registrationEnabled() external view returns (bool)",
    "function freeRegistrationEnabled() external view returns (bool)",
    // ── Agent URI ──
    "function setAgentURI(uint256 agentId, string newURI) external",
    "function tokenURI(uint256 tokenId) external view returns (string)",
    // ── Agent Wallet ──
    "function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes signature) external",
    "function unsetAgentWallet(uint256 agentId) external",
    "function getAgentWallet(uint256 agentId) external view returns (address)",
    // ── Metadata ──
    "function setMetadata(uint256 agentId, string metadataKey, bytes metadataValue) external",
    "function getMetadata(uint256 agentId, string metadataKey) external view returns (bytes)",
    // ── Reads ──
    "function ownerOf(uint256 tokenId) external view returns (address)",
    "function isAuthorizedOrOwner(address spender, uint256 agentId) external view returns (bool)",
    "function getVersion() external view returns (string)",
    // ── Events ──
    "event Registered(uint256 indexed agentId, string agentURI, address indexed owner)",
    "event URIUpdated(uint256 indexed agentId, string newURI, address indexed updatedBy)",
    "event MetadataSet(uint256 indexed agentId, string indexed indexedMetadataKey, string metadataKey, bytes metadataValue)",
];


path: scripts/zeru.ts
content: #!/usr/bin/env npx tsx
/**
 * Zeru ERC-8004 Identity Registry — OpenClaw Skill CLI
 *
 * Commands:
 *   register      --name <name> --description <desc> --endpoint <url> [--image <url>]
 *   read          <agentId>
 *   fee
 *   set-metadata  <agentId> --key <key> --value <value>
 *   unset-wallet  <agentId>
 *
 * Env:
 *   PRIVATE_KEY  — Required for write operations (register, set-metadata, unset-wallet)
 *   RPC_URL      — Optional, override default RPC
 */

import { readFileSync } from "fs";
import { Wallet, JsonRpcProvider, formatEther } from "ethers";
import {
  resolveConfig,
  createAgentURI,
  updateAgentURI,
  registerAgent,
  getAgent,
  getRegistrationFee,
  isRegistrationEnabled,
  getAgentWallet,
  setMetadata,
  unsetAgentWallet,
  ZeruError,
} from "../lib/index.js";

// ── Helpers ──

function die(msg: string): never {
  console.error(`\u274C ${msg}`);
  process.exit(1);
}

function parseArgs(argv: string[]): { command: string; positional: string[]; flags: Record<string, string> } {
  const command = argv[0] ?? "";
  const positional: string[] = [];
  const flags: Record<string, string> = {};

  let i = 1;
  while (i < argv.length) {
    const arg = argv[i];
    if (arg.startsWith("--") && i + 1 < argv.length) {
      flags[arg.slice(2)] = argv[i + 1];
      i += 2;
    } else {
      positional.push(arg);
      i++;
    }
  }
  return { command, positional, flags };
}

/** Default chain: Base Mainnet. Override with CHAIN_ID env or --chain flag. */
function getChainId(): number {
  const envChain = process.env.CHAIN_ID;
  if (envChain) return Number(envChain);
  // Check for --chain flag in args
  const idx = process.argv.indexOf("--chain");
  if (idx !== -1 && process.argv[idx + 1]) return Number(process.argv[idx + 1]);
  return 8453; // Base Mainnet default
}

function getChainLabel(chainId: number): string {
  if (chainId === 8453) return "Base Mainnet (8453)";
  if (chainId === 84532) return "Base Sepolia (84532)";
  return `Chain ${chainId}`;
}

function getSigner() {
  const pk = process.env.PRIVATE_KEY;
  if (!pk) die("PRIVATE_KEY env var not set. Add it to your OpenClaw config under skills.entries.zeru-erc8004-identity.env");
  const config = resolveConfig({
    chainId: getChainId(),
    rpcUrl: process.env.RPC_URL,
  });
  const provider = new JsonRpcProvider(config.rpcUrl);
  const signer = new Wallet(pk, provider);
  return { config, signer };
}

function getReadConfig() {
  return resolveConfig({
    chainId: getChainId(),
    rpcUrl: process.env.RPC_URL,
  });
}

// ── Commands ──

async function cmdRegister(flags: Record<string, string>) {
  const jsonFile = flags.json;

  let input: Record<string, unknown>;

  if (jsonFile) {
    // ── JSON file mode: read full agent input from file ──
    let raw: string;
    try {
      raw = readFileSync(jsonFile, "utf-8");
    } catch (e) {
      die(`Cannot read JSON file: ${jsonFile} — ${e instanceof Error ? e.message : e}`);
    }
    try {
      input = JSON.parse(raw!);
    } catch {
      die(`Invalid JSON in file: ${jsonFile}`);
    }
  } else {
    // ── Simple flag mode: --name, --description, --endpoint ──
    const name = flags.name;
    const description = flags.description ?? flags.desc;
    const endpoint = flags.endpoint;
    const image = flags.image;

    if (!name) die("Missing --name (or use --json <file>). Usage:\n  register --name 'My Agent' --description 'What it does' --endpoint 'https://...'\n  register --json agent.json");
    if (!description) die("Missing --description");
    if (!endpoint) die("Missing --endpoint");

    input = {
      name,
      description,
      services: [{ name: "api", endpoint }],
    };
    if (image) input.image = image;
  }

  const { config, signer } = getSigner();
  const address = await signer.getAddress();

  // Set owner from signer if not provided
  if (!input!.owner) input!.owner = address;

  console.log("\u26D3\uFE0F  Zeru ERC-8004 Agent Registration");
  console.log("\u2501".repeat(40));
  console.log(`  Network:  ${getChainLabel(config.chainId)}`);
  console.log(`  Wallet:   ${address}`);
  if (jsonFile) console.log(`  JSON:     ${jsonFile}`);
  console.log(`  Name:     ${(input! as any).name}`);

  // 1. Fee
  const fee = await getRegistrationFee(config);
  console.log(`  Fee:      ${formatEther(fee)} ETH`);

  const enabled = await isRegistrationEnabled(config);
  if (!enabled) die("Registration is currently disabled on this contract");

  // 2. Check balance
  const balance = await signer.provider!.getBalance(address);
  if (balance < fee + BigInt(50000) * BigInt(1e9)) {
    die(`Insufficient balance. Need ~${formatEther(fee)} ETH + gas. Have ${formatEther(balance)} ETH`);
  }

  console.log("");
  console.log("Step 1/4: Creating agent URI...");
  const { id, agentURI, json } = await createAgentURI(config, signer, input! as any);
  console.log(`  URI: ${agentURI}`);

  console.log("Step 2/4: Minting NFT on-chain...");
  const { agentId, txHash } = await registerAgent(config, { signer, agentURI });
  console.log(`  Agent ID: ${agentId}`);
  console.log(`  Tx: ${txHash}`);

  console.log("Step 3/4: Updating document with real agentId...");
  await updateAgentURI(config, signer, id, {
    ...json,
    registrations: [{
      agentId: Number(agentId),
      agentRegistry: `eip155:${config.chainId}:${config.identityRegistryAddress}`,
    }],
  });

  console.log("Step 4/4: Verifying on-chain...");
  const agent = await getAgent(config, agentId);

  console.log("");
  console.log("\u2705 Registration complete!");
  console.log("\u2501".repeat(40));
  console.log(`  Agent ID:    ${agentId}`);
  console.log(`  Name:        ${name}`);
  console.log(`  Owner:       ${agent.owner}`);
  console.log(`  Agent URI:   ${agent.agentURI}`);
  console.log(`  Wallet:      ${agent.agentWallet ?? "(not set)"}`);
  console.log(`  Tx Hash:     ${txHash}`);
}

async function cmdRead(positional: string[]) {
  const agentId = positional[0];
  if (!agentId) die("Missing agentId. Usage: read <agentId>");

  const config = getReadConfig();

  console.log(`\u26D3\uFE0F  Agent #${agentId}`);
  console.log("\u2501".repeat(40));

  const agent = await getAgent(config, BigInt(agentId));
  console.log(`  Owner:       ${agent.owner}`);
  console.log(`  Agent URI:   ${agent.agentURI}`);
  console.log(`  Wallet:      ${agent.agentWallet ?? "(not set)"}`);

  if (agent.parsedJson) {
    const j = agent.parsedJson as Record<string, unknown>;
    if (j.name) console.log(`  Name:        ${j.name}`);
    if (j.description) console.log(`  Description: ${String(j.description).slice(0, 100)}`);
    if (j.active !== undefined) console.log(`  Active:      ${j.active}`);
    if (Array.isArray(j.services)) {
      for (const s of j.services) {
        const svc = s as Record<string, unknown>;
        console.log(`  Service:     ${svc.name} → ${svc.endpoint}`);
      }
    }
  }
}

async function cmdFee() {
  const config = getReadConfig();

  console.log("\u26D3\uFE0F  Zeru Registry Info");
  console.log("\u2501".repeat(40));

  const fee = await getRegistrationFee(config);
  const enabled = await isRegistrationEnabled(config);

  console.log(`  Network:      ${getChainLabel(config.chainId)}`);
  console.log(`  Registry:     ${config.identityRegistryAddress}`);
  console.log(`  Fee:          ${formatEther(fee)} ETH`);
  console.log(`  Registration: ${enabled ? "OPEN" : "CLOSED"}`);
}

async function cmdSetMetadata(positional: string[], flags: Record<string, string>) {
  const agentId = positional[0];
  const key = flags.key;
  const value = flags.value;

  if (!agentId) die("Missing agentId. Usage: set-metadata <agentId> --key <key> --value <value>");
  if (!key) die("Missing --key");
  if (!value) die("Missing --value");

  const { config, signer } = getSigner();

  console.log(`\u26D3\uFE0F  Setting metadata on Agent #${agentId}`);
  console.log("\u2501".repeat(40));
  console.log(`  Key:   ${key}`);
  console.log(`  Value: ${value}`);

  const txHash = await setMetadata(config, signer, BigInt(agentId), key, value);
  console.log("");
  console.log(`\u2705 Metadata set! Tx: ${txHash}`);
}

async function cmdUnsetWallet(positional: string[]) {
  const agentId = positional[0];
  if (!agentId) die("Missing agentId. Usage: unset-wallet <agentId>");

  const { config, signer } = getSigner();

  console.log(`\u26D3\uFE0F  Unsetting wallet for Agent #${agentId}`);
  console.log("\u2501".repeat(40));

  const txHash = await unsetAgentWallet(config, signer, BigInt(agentId));
  console.log("");
  console.log(`\u2705 Wallet unset! Tx: ${txHash}`);
}

// ── Main ──

async function main() {
  const args = process.argv.slice(2);
  if (args.length === 0) {
    console.log("Zeru ERC-8004 Identity Registry");
    console.log("\u2501".repeat(40));
    console.log("Commands:");
    console.log("  register     --json <file>  (full agent JSON — recommended)");
    console.log("  register     --name <n> --description <d> --endpoint <url> [--image <url>]");
    console.log("  read         <agentId>");
    console.log("  fee          Show registration fee and status");
    console.log("  set-metadata <agentId> --key <key> --value <value>");
    console.log("  unset-wallet <agentId>");
    console.log("");
    console.log("Global flags:");
    console.log("  --chain <id> Chain ID (default: 8453 = Base Mainnet, 84532 = Base Sepolia)");
    console.log("");
    console.log("Env:");
    console.log("  PRIVATE_KEY  Required for write operations");
    console.log("  CHAIN_ID     Optional, override chain (default: 8453)");
    console.log("  RPC_URL      Optional, override RPC");
    return;
  }

  const { command, positional, flags } = parseArgs(args);

  try {
    switch (command) {
      case "register":
        await cmdRegister(flags);
        break;
      case "read":
        await cmdRead(positional);
        break;
      case "fee":
        await cmdFee();
        break;
      case "set-metadata":
        await cmdSetMetadata(positional, flags);
        break;
      case "unset-wallet":
        await cmdUnsetWallet(positional);
        break;
      default:
        die(`Unknown command: ${command}. Use: register, read, fee, set-metadata, unset-wallet`);
    }
  } catch (e) {
    if (e instanceof ZeruError) {
      console.error(`\n\u274C ZeruError [${e.code}]: ${e.message}`);
      if (e.details) console.error("  Details:", JSON.stringify(e.details, null, 2));
    } else if (e instanceof Error) {
      console.error(`\n\u274C Error: ${e.message}`);
    }
    process.exit(1);
  }
}

main();


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)