hl-registry-integration

star 1

Horizen Labs Agent Marketplace integration on Base Sepolia — full pipeline from ZK proof submission (Kurier) through attestation relay to on-chain validation recording. Covers agent registration (IdentityRegistry), validation recording (ValidationGateway), attestation polling, and marketplace discovery.

HorizenLabs By HorizenLabs schedule Updated 4/17/2026

name: hl-registry-integration description: Horizen Labs Agent Marketplace integration on Base Sepolia — full pipeline from ZK proof submission (Kurier) through attestation relay to on-chain validation recording. Covers agent registration (IdentityRegistry), validation recording (ValidationGateway), attestation polling, and marketplace discovery.

Horizen Labs Agent Marketplace

Register AI agents on-chain and record ZK-verified validations on the HL Agent Marketplace (Base Sepolia).

Overview

The HL marketplace is a set of smart contracts on Base Sepolia that enable AI agents to:

  1. Register as on-chain entities (ERC-721 AgentCards via IdentityRegistry)
  2. Record validations backed by zkVerify proofs (via ValidationGateway)
  3. Be discoverable on the Agent Registry

Full Pipeline

1. Generate proof (e.g., Groth16, PLONK, etc.)
2. Submit to Kurier with chainId: 84532 (Base Sepolia)
3. Poll Kurier until status = "Aggregated"
4. Extract aggregationId + aggregationDetails from Kurier response
5. Wait for attestation relay to Base Sepolia (poll proofsAggregations)
6. Call ValidationGateway.recordValidation() with protocol fee
7. Agent visible at: https://agent-registry.horizenlabs.io/agent/{tokenIdHex}

Contracts (Base Sepolia)

Contract Address Purpose
IdentityRegistry 0x8004A818BFB912233c491871b3d84c89A494BD9e ERC-721 AgentCards — register agents
ValidationGateway (V2 proxy) 0xbbdcb0C9C3B9ce60555fdF50cFB99802E7c33920 Record validations (V2 — UUPS proxy, always call this address)
ValidationGateway (V2 impl, current) 0x3995875565be8e354f17dbfc3f300fc450157bb2 V2 implementation — never call directly. Check EIP-1967 slot 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc for current impl — HL has upgraded it (was 0x7d9f3830...)
ValidationGateway (V1) 0xD0248DAF7f362F7721EC77b9A7A74d9A8FEe361e DEPRECATED — registry ownership moved to V2 (reverts with "Not owner")
ValidationRegistry 0x75a7f712635D7918563659795450ddE6751D71BC Immutable validation storage (ownership held by V2 proxy)
zkVerify Attestation 0x0807C544D38aE7729f8798388d89Be6502A1e8A8 Proof aggregation relay from zkVerify

V2 Migration (March 30 2026, upgraded April 2026): HL deployed ValidationGatewayV2 as a new UUPS proxy and transferred ValidationRegistry ownership to it. The V1 gateway is bricked. The V2 implementation was then upgraded to add generic proof-type support (multi-verifier) and versionHash in the struct. Source: github.com/HorizenLabs/ai-agent-registry.

Current ABI (function selector 0x27d6cd1c):

  • (agentId, proofType, zkVerifyTxHash, zkVerifyBlockHash, domainId, attestationId, leaf, merklePath, leafCount, index, pubsBytes, vkHash, versionHash)
  • pubsBytes: bytes — raw LE-encoded public signals (was uint256[] in the first V2 iteration)
  • versionHash: bytes32 — for snarkjs/Groth16: SHA256("") = 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Key V2 gates:

  • Proof type config: proofType is a service-metric identifier (e.g. "pulse-sla", "aideal-mock") registered by the gateway owner via setProofTypeConfig(proofType, ctxHash, agentIdOffset, agentIdLength, agentIdLittleEndian, active). For snarkjs Groth16: ctxHash = keccak256("groth16"), agentIdOffset=0, agentIdLength=32, agentIdLittleEndian=true. Without an active config, "Unsupported proof type".
  • Submitter authorization: Caller must be in authorizedSubmitters[msg.sender] OR the ERC-8004 owner of agentId. Gateway owner authorizes via setAuthorizedSubmitter(wallet, true). Without this, "Not authorized submitter".
  • VK hash gating: allowedVkHashes[vkHash] must be true (set via setVkHash(hash, true)). Without this, "VK hash not allowed".
  • agentId binding: Circuit must have agentId as the first public signal (first 32 bytes LE of pubsBytes). Gateway extracts and matches against p.agentId. Without this, "Agent ID mismatch".
  • Leaf reconstruction: Gateway recomputes keccak256(ctxHash ‖ vkHash ‖ versionHash ‖ keccak256(pubsBytes)) and requires it to match leaf. Without this, "Leaf mismatch".
  • Replay protection: Each (attestationId, leaf) pair usable once — "Attestation already used" on reuse.
  • Fee: Must send >= protocolFee() as msg.value (currently 0.0001 ETH on testnet). Refunds any excess.

pubsBytes encoding: For snarkjs/Groth16, concatenate each public signal as a 32-byte little-endian scalar. Example for signals ["2969", "385"]:

function encodePubsBytes(signals) {
  const buffers = [];
  for (const sig of signals) {
    const buf = Buffer.alloc(32);
    let v = BigInt(sig);
    for (let i = 0; i < 32 && v > 0n; i++) { buf[i] = Number(v & 0xffn); v >>= 8n; }
    buffers.push(buf);
  }
  return "0x" + Buffer.concat(buffers).toString("hex");
}

Full registration checklist (3 admin calls by gateway owner):

  1. setProofTypeConfig("<your-proof-type>", keccak256("groth16"), 0, 32, true, true)
  2. setVkHash(<yourVkHash>, true)
  3. setAuthorizedSubmitter(<yourWallet>, true) (unless you own the agentId in the ERC-8004 registry)

How to compute vkHash (Groth16/snarkjs): Call Kurier's POST /api/v1/register-vk/{API_KEY} with { proofType: "groth16", proofOptions: { library: "snarkjs", curve: "bn128" }, vk: <yourVk> } — it returns the canonical vkHash zkVerify uses for leaf construction.

Sanity-check before recordValidation — ALL three must be true:

await readContract(gateway, "allowedVkHashes", [vkHash])          // → true
await readContract(gateway, "authorizedSubmitters", [yourWallet]) // → true
const cfg = await readContract(gateway, "proofTypeConfigs", [proofType])
cfg.ctxHash === keccak256("groth16") && cfg.active === true       // → true

Any being false means silent revert with no error data on-chain. Always verify all three before submitting.

Prerequisites

npm install viem

Wallet requirements:

  • A wallet with Base Sepolia ETH (for gas + 0.0002 ETH protocol fee per validation)
  • Get testnet ETH from a Base Sepolia faucet

Environment variables:

BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
PRIVATE_KEY=0x...              # Wallet with Base Sepolia ETH
AGENT_CARD_TOKEN_ID=           # Set after registration (step 2)
KURIER_API_KEY=                # From https://kurier.xyz
KURIER_API_URL=https://api-testnet.kurier.xyz  # MUST use testnet for Base Sepolia

Setup Viem Clients

import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia } from "viem/chains";

const rpcUrl = process.env.BASE_SEPOLIA_RPC_URL || "https://sepolia.base.org";

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

const walletClient = createWalletClient({
  account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
  chain: baseSepolia,
  transport: http(rpcUrl),
});

Step 1: Register an Agent (One-Time)

const IDENTITY_REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e";

const registerAbi = [
  {
    inputs: [{ name: "agentURI", type: "string" }],
    name: "register",
    outputs: [{ name: "agentId", type: "uint256" }],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    anonymous: false,
    inputs: [
      { indexed: true, name: "agentId", type: "uint256" },
      { indexed: false, name: "agentURI", type: "string" },
      { indexed: true, name: "owner", type: "address" },
    ],
    name: "Registered",
    type: "event",
  },
] as const;

// Encode metadata as base64 data URI
const metadata = {
  name: "My Agent",
  description: "What my agent does",
  services: [{
    name: "my-service",
    endpoint: "https://my-agent.example.com/api",
    version: "0.1.0",
    skills: ["skill-1", "skill-2"],
    domains: ["domain-1"],
  }],
  supportedTrust: ["zkVerify-groth16"],
  metadata: {
    proofSystem: "groth16",
    library: "snarkjs",
    curve: "bn128",
  },
};
const agentURI = `data:application/json;base64,${Buffer.from(JSON.stringify(metadata)).toString("base64")}`;

// IMPORTANT: Use register(), NOT safeMint or mint
const txHash = await walletClient.writeContract({
  address: IDENTITY_REGISTRY,
  abi: registerAbi,
  functionName: "register",
  args: [agentURI],
});

const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });

// Extract agentId from Registered event
import { parseEventLogs } from "viem";
const logs = parseEventLogs({ abi: registerAbi, logs: receipt.logs });
const event = logs.find((l) => l.eventName === "Registered");
const agentId = event?.args?.agentId; // e.g., 2094n
// Set AGENT_CARD_TOKEN_ID to this value in your .env

// Agent page: https://agent-registry.horizenlabs.io/agent/{agentId in hex}

Step 2: Submit Proof to Kurier

Submit a ZK proof to Kurier for verification and aggregation. Kurier handles submission to zkVerify and aggregation for relay to Base Sepolia.

import axios from "axios";

const KURIER_API_URL = process.env.KURIER_API_URL; // https://api-testnet.kurier.xyz
const KURIER_API_KEY = process.env.KURIER_API_KEY;

const submitRes = await axios.post(
  `${KURIER_API_URL}/api/v1/submit-proof/${KURIER_API_KEY}`,
  {
    proofType: "groth16",        // or other supported proof types
    vkRegistered: false,         // true if VK is pre-registered on zkVerify
    chainId: 84532,              // Base Sepolia — required for attestation relay
    proofOptions: { library: "snarkjs", curve: "bn128" },
    proofData: {
      proof: yourProof,          // proof object from snarkjs
      publicSignals: yourSignals,
      vk: yourVerificationKey,   // required if vkRegistered: false
    },
  }
);

const jobId = submitRes.data.jobId;

Step 3: Poll Kurier Until Aggregated

let aggregationId: number;
let aggregationDetails: Record<string, unknown>;

for (let i = 0; i < 180; i++) { // up to 15 min
  await new Promise((r) => setTimeout(r, 5000));

  const statusRes = await axios.get(
    `${KURIER_API_URL}/api/v1/job-status/${KURIER_API_KEY}/${jobId}`
  );
  const { status, ...data } = statusRes.data;

  if (status === "Failed") throw new Error("Proof verification failed");

  if (status === "Aggregated" || status === "AggregationPublished") {
    aggregationId = data.aggregationId;       // top-level sequential integer
    aggregationDetails = data.aggregationDetails;
    break;
  }
}

Kurier status lifecycle (with chainId set — the aggregation path):

[Queued → Valid] → IncludedInBlock → AggregationPending → Aggregated → (AggregationPublished)
  • Queued / Valid may not be observed at a 5s polling cadence — the proof typically reaches IncludedInBlock within the first poll.
  • AggregationPending is the longest-lived state (~1–3 minutes on Base Sepolia testnet).
  • Stop polling at AggregatedaggregationDetails is now populated with the Merkle proof you need for recordValidation.

Observed timing (Base Sepolia, April 2026): submit → Aggregated ≈ 3 minutes. Attestation relay to Base ≈ 0–5 min after that. Full pipeline (submit → recordValidation mined): ~3–5 min.

Important: With chainId set you do NOT see Finalized in the aggregation path — it goes straight from AggregationPending to Aggregated. Do not wait for Finalized.

Step 4: Wait for Attestation Relay

After Kurier reports Aggregated, the attestation must be relayed to Base Sepolia. This typically takes 1-5 minutes.

const ATTESTATION_ADDRESS = "0x0807C544D38aE7729f8798388d89Be6502A1e8A8";
const attestationAbi = [{
  inputs: [
    { name: "_domainId", type: "uint256" },
    { name: "_aggregationId", type: "uint256" },
  ],
  name: "proofsAggregations",
  outputs: [{ name: "", type: "bytes32" }],
  stateMutability: "view",
  type: "function",
}] as const;

async function waitForRelay(domainId: bigint, aggregationId: number): Promise<boolean> {
  for (let i = 0; i < 60; i++) { // up to 10 min
    const root = await publicClient.readContract({
      address: ATTESTATION_ADDRESS,
      abi: attestationAbi,
      functionName: "proofsAggregations",
      args: [domainId, BigInt(aggregationId)],
    });
    if (root !== "0x" + "0".repeat(64)) return true;
    await new Promise((r) => setTimeout(r, 10_000));
  }
  return false; // Timed out — save data and retry later
}

// domainId corresponds to the destination chain, NOT the proof type
// For Base Sepolia testnet: domainId = 2
const relayed = await waitForRelay(2n, aggregationId);
if (!relayed) throw new Error("Attestation relay timed out");

Important: domainId identifies the destination chain for the attestation relay, not the proof system. For Base Sepolia, use domainId = 2. This value is the same regardless of whether you submit a Groth16, PLONK, or any other proof type.

Step 5: Record Validation on-chain (V2 — current ABI)

// V2 UUPS proxy — always call this address.
const VALIDATION_GATEWAY = "0xbbdcb0C9C3B9ce60555fdF50cFB99802E7c33920";
// SHA256("") — versionHash for snarkjs/Groth16 proofs (V2 requirement)
const VERSION_HASH = "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

const gatewayAbi = [
  {
    inputs: [{
      name: "p",
      type: "tuple",
      components: [
        { name: "agentId", type: "uint256" },
        { name: "proofType", type: "string" },            // Service-metric identifier (e.g. "pulse-sla", "aideal-mock")
        { name: "zkVerifyTxHash", type: "bytes32" },      // extrinsic hash from Kurier
        { name: "zkVerifyBlockHash", type: "bytes32" },   // block hash from Kurier
        { name: "domainId", type: "uint256" },
        { name: "attestationId", type: "uint256" },
        { name: "leaf", type: "bytes32" },
        { name: "merklePath", type: "bytes32[]" },
        { name: "leafCount", type: "uint256" },
        { name: "index", type: "uint256" },
        { name: "pubsBytes", type: "bytes" },             // Raw LE-encoded public signals (NOT uint256[])
        { name: "vkHash", type: "bytes32" },              // Canonical zkVerify VK hash (must be allowlisted)
        { name: "versionHash", type: "bytes32" },         // SHA256("") for Groth16
      ],
    }],
    name: "recordValidation",
    outputs: [{ name: "validationId", type: "uint256" }],
    stateMutability: "payable",
    type: "function",
  },
  { inputs: [], name: "protocolFee", outputs: [{ name: "", type: "uint256" }], stateMutability: "view", type: "function" },
  { inputs: [{ name: "", type: "bytes32" }], name: "allowedVkHashes", outputs: [{ name: "", type: "bool" }], stateMutability: "view", type: "function" },
  { inputs: [{ name: "", type: "address" }], name: "authorizedSubmitters", outputs: [{ name: "", type: "bool" }], stateMutability: "view", type: "function" },
  { inputs: [{ name: "", type: "string" }], name: "proofTypeConfigs", outputs: [{ name: "ctxHash", type: "bytes32" }, { name: "agentIdOffset", type: "uint256" }, { name: "agentIdLength", type: "uint256" }, { name: "agentIdLittleEndian", type: "bool" }, { name: "active", type: "bool" }], stateMutability: "view", type: "function" },
] as const;

// Encode public signals as raw LE 32-byte scalars (this is what zkVerify/Groth16 natively uses)
function encodePubsBytes(signals: string[]): `0x${string}` {
  const buffers: Buffer[] = [];
  for (const sig of signals) {
    const buf = Buffer.alloc(32);
    let v = BigInt(sig);
    for (let i = 0; i < 32 && v > 0n; i++) { buf[i] = Number(v & 0xffn); v >>= 8n; }
    buffers.push(buf);
  }
  return `0x${Buffer.concat(buffers).toString("hex")}`;
}

// Compute vkHash via Kurier register-vk:
const vkRes = await fetch(`${KURIER_API_URL}/api/v1/register-vk/${KURIER_API_KEY}`, {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ proofType: "groth16", proofOptions: { library: "snarkjs", curve: "bn128" }, vk: yourVk }),
});
const { vkHash } = await vkRes.json();

// MANDATORY preflight — check ALL THREE gateway configs or get a silent revert:
const [vkAllowed, submitterAuth, ptCfg] = await Promise.all([
  publicClient.readContract({ address: VALIDATION_GATEWAY, abi: gatewayAbi, functionName: "allowedVkHashes", args: [vkHash] }),
  publicClient.readContract({ address: VALIDATION_GATEWAY, abi: gatewayAbi, functionName: "authorizedSubmitters", args: [walletClient.account.address] }),
  publicClient.readContract({ address: VALIDATION_GATEWAY, abi: gatewayAbi, functionName: "proofTypeConfigs", args: ["your-proof-type"] }),
]);
if (!vkAllowed) throw new Error(`vkHash not allowlisted — ask owner: setVkHash(${vkHash}, true)`);
if (!submitterAuth) throw new Error(`Submitter not authorized — ask owner: setAuthorizedSubmitter(${walletClient.account.address}, true)`);
if (!ptCfg[4]) throw new Error(`Proof type not active — ask owner: setProofTypeConfig("your-proof-type", keccak256("groth16"), 0, 32, true, true)`);

const fee = await publicClient.readContract({
  address: VALIDATION_GATEWAY, abi: gatewayAbi, functionName: "protocolFee",
}) as bigint;

const nonce = await publicClient.getTransactionCount({
  address: walletClient.account.address, blockTag: "pending",
});

const pubsBytes = encodePubsBytes(yourPublicSignals); // same array sent to Kurier

const txHash = await walletClient.writeContract({
  address: VALIDATION_GATEWAY, abi: gatewayAbi, functionName: "recordValidation",
  args: [{
    agentId:           BigInt(process.env.AGENT_CARD_TOKEN_ID!),
    proofType:         "your-proof-type",                      // service-metric id, NOT "groth16"
    zkVerifyTxHash:    kurierTxHash as `0x${string}`,
    zkVerifyBlockHash: kurierBlockHash as `0x${string}`,
    domainId:          2n,                                     // Base Sepolia
    attestationId:     BigInt(aggregationId),
    leaf:              aggregationDetails.leaf as `0x${string}`,
    merklePath:        aggregationDetails.merkleProof as `0x${string}`[],
    leafCount:         BigInt(aggregationDetails.numberOfLeaves),
    index:             BigInt(aggregationDetails.leafIndex),
    pubsBytes,
    vkHash:            vkHash as `0x${string}`,
    versionHash:       VERSION_HASH as `0x${string}`,
  }],
  value: fee,
  nonce,
});

const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
// receipt.status === "success" → validation recorded on-chain

Common pitfalls learned the hard way:

  • VK allowlist requests: Send the 32-byte hash from Kurier's register-vk response, not a contract address. Verify with allowedVkHashes(yourVkHash) returns true before the tx.
  • proofType is not the ZK system: "groth16" goes to Kurier's proofType field (ZK system). The gateway's proofType is a service-metric identifier (e.g. "pulse-sla", "aideal-mock"). Using "groth16" for the gateway will revert with "Unsupported proof type".
  • Silent reverts with no error data usually mean ABI mismatch. The V2 gateway's implementation was upgraded (check EIP-1967 slot for current impl address) — the newer ABI uses bytes pubsBytes instead of uint256[] publicSignals and adds versionHash. Wrong ABI → wrong function selector → proxy reverts without a reason string.
  • Three admin calls are required, not just one: setVkHash, setAuthorizedSubmitter, setProofTypeConfig. Check all three before asking users to debug.

Kurier → Contract Parameter Mapping

Kurier Response Field Contract Parameter Type Notes
aggregationId (top-level) attestationId uint256 Sequential integer (e.g., 22435). NOT receipt (which is a Merkle root hash)
Hardcode 2 for Base Sepolia domainId uint256 Destination chain for attestation relay, not proof type
aggregationDetails.leaf leaf bytes32 Proof leaf in Merkle tree
aggregationDetails.numberOfLeaves leafCount uint256 Tree size
aggregationDetails.leafIndex index uint256 Leaf position
aggregationDetails.merkleProof merklePath bytes32[] Merkle inclusion proof

Persisting Pipeline State for UI Polling

When building a UI that shows verification progress to users, persist the pipeline status at each step so the frontend can poll and display a live timeline. Recommended DB fields and when to update them:

Pipeline Step Suggested Status DB Fields to Update
Proof submitted to Kurier SUBMITTED zkVerifyJobId
txHash appears in Kurier response FINALIZED zkVerifyExtrinsicHash (for zkVerify explorer link)
Kurier returns Aggregated AGGREGATED aggregationId
Waiting for attestation relay RELAYING
recordValidation tx sent RECORDING
Receipt confirmed VERIFIED validationTxHash, validationId
Any step throws FAILED onChainError (error message)

Key design points:

  1. Update status at each transition — don't wait until the end. The pipeline takes 5-15 minutes; users need intermediate feedback.
  2. Store zkVerifyExtrinsicHash early — it appears in the Kurier job-status response at IncludedInBlock or later (as txHash). This lets you show a zkVerify explorer link before aggregation completes.
  3. Store aggregationId separately — you need it for the attestation relay check and recordValidation call, but it's also useful for debugging.
  4. Save validationTxHash on success — this is the Base Sepolia transaction hash for the recordValidation call, used for BaseScan explorer links.
  5. On failure, save the error and stop — store the error message so the UI can display it. Don't leave the status in an intermediate state.
  6. Frontend polls a status API — expose an endpoint that returns the current status + all tracking fields. The frontend polls every 3-5 seconds and renders a timeline/stepper component.

Example Prisma schema fields:

model Report {
  // ... your existing fields ...
  onChainStatus        String?   // PENDING | SUBMITTED | FINALIZED | AGGREGATED | RELAYING | RECORDING | VERIFIED | FAILED
  onChainError         String?
  zkVerifyJobId        String?   // Kurier job ID
  zkVerifyExtrinsicHash String?  // zkVerify blockchain tx hash (for explorer link)
  aggregationId        Int?      // Kurier aggregation ID (= attestationId in contracts)
  validationTxHash     String?   // Base Sepolia tx hash from recordValidation
  validationId         Int?      // On-chain validation ID from ValidationGateway
}

Troubleshooting

Nonce errors (Nonce provided for the transaction is lower than the current nonce)

This happens when sending transactions in quick succession (e.g., register then immediately record a validation). Fix: always fetch the pending nonce before writeContract:

const nonce = await publicClient.getTransactionCount({
  address: walletClient.account.address,
  blockTag: "pending",
});
// pass nonce to writeContract

recordValidation reverts

Common causes:

  • Attestation not yet relayed — poll proofsAggregations first, wait for non-zero return
  • Wrong attestationId — use the top-level aggregationId from Kurier (a sequential integer like 22435), NOT the receipt field (which is a bytes32 Merkle root)
  • Insufficient value — must send protocolFee() ETH with the call (currently 0.0002 ETH)
  • Wrong domainId — use 2 for Base Sepolia testnet

Kurier returns 4xx errors

  • Using mainnet URL with testnet chainIdhttps://api.kurier.xyz rejects chainId: 84532. Use https://api-testnet.kurier.xyz for Base Sepolia
  • Invalid API key — verify your key at https://kurier.xyz

Attestation relay takes too long

The relay from zkVerify to Base Sepolia typically takes 1-5 minutes after Kurier reports Aggregated. If it takes longer:

  • Save the aggregationId and aggregationDetails to disk
  • Retry the attestation polling + recordValidation later
  • The relay is performed by the Attestation Bot, not Hyperbridge

Marketplace URLs

Resource URL Pattern
Agent page https://agent-registry.horizenlabs.io/agent/{tokenIdHex}
BaseScan tx https://sepolia.basescan.org/tx/{txHash}
BaseScan contract https://sepolia.basescan.org/address/{address}
zkVerify Explorer https://zkverify-testnet.subscan.io/extrinsic/{txHash}

Token ID hex conversion: decimal 2094 → 0x82ehttps://agent-registry.horizenlabs.io/agent/0x82e

Contract ABIs

See contracts.md for full ABI definitions and additional functions.

Install via CLI
npx skills add https://github.com/HorizenLabs/hl-claude-marketplace --skill hl-registry-integration
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator