@a3stack/identity 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agent-id CLI — verify and inspect ERC-8004 agent identities
4
+ *
5
+ * Usage:
6
+ * npx @a3stack/identity verify <globalId>
7
+ * npx @a3stack/identity lookup <walletAddress>
8
+ * npx @a3stack/identity chains
9
+ */
10
+
11
+ import { verifyAgent, getMcpEndpoint, getAgentCount, parseAgentId, SUPPORTED_CHAINS } from "../src/index.js";
12
+ import { findAllRegistrations } from "../src/multichain.js";
13
+
14
+ const [cmd, ...args] = process.argv.slice(2);
15
+
16
+ const HELP = `
17
+ šŸ†” agent-id — ERC-8004 Agent Identity CLI
18
+
19
+ Commands:
20
+ verify <globalId> Verify an agent's on-chain identity
21
+ lookup <wallet> Find all registrations for a wallet address
22
+ chains List supported chains
23
+
24
+ Examples:
25
+ npx @a3stack/identity verify "eip155:8453:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432#2376"
26
+ npx @a3stack/identity lookup 0xYOUR_WALLET_ADDRESS
27
+ npx @a3stack/identity chains
28
+ `;
29
+
30
+ async function main() {
31
+ if (!cmd || cmd === "--help" || cmd === "-h") {
32
+ console.log(HELP);
33
+ return;
34
+ }
35
+
36
+ if (cmd === "verify") {
37
+ const globalId = args[0];
38
+ if (!globalId) {
39
+ console.error("Usage: agent-id verify <globalId>");
40
+ process.exit(1);
41
+ }
42
+
43
+ console.log(`\nšŸ” Verifying: ${globalId}\n`);
44
+
45
+ const ref = parseAgentId(globalId);
46
+ console.log(` Chain: ${ref.chainId}`);
47
+ console.log(` Registry: ${ref.registry}`);
48
+ console.log(` Agent ID: ${ref.agentId}`);
49
+
50
+ const result = await verifyAgent(globalId);
51
+
52
+ if (!result.valid) {
53
+ console.log(`\n āŒ FAILED: ${result.error}\n`);
54
+ process.exit(1);
55
+ }
56
+
57
+ console.log(`\n āœ… Verified!`);
58
+ console.log(` Owner: ${result.owner}`);
59
+ console.log(` Payment wallet: ${result.paymentWallet ?? "(defaults to owner)"}`);
60
+
61
+ if (result.registration) {
62
+ console.log(` Name: ${result.registration.name}`);
63
+ console.log(` Active: ${result.registration.active}`);
64
+ console.log(` x402 Payments: ${result.registration.x402Support}`);
65
+
66
+ if (result.registration.services?.length) {
67
+ console.log(`\n Services:`);
68
+ for (const s of result.registration.services) {
69
+ console.log(` - ${s.name}: ${s.endpoint}`);
70
+ }
71
+ }
72
+ }
73
+ console.log();
74
+
75
+ } else if (cmd === "lookup") {
76
+ const wallet = args[0] as `0x${string}`;
77
+ if (!wallet?.startsWith("0x")) {
78
+ console.error("Usage: agent-id lookup <0xWalletAddress>");
79
+ process.exit(1);
80
+ }
81
+
82
+ console.log(`\n🌐 Scanning all chains for ${wallet}...\n`);
83
+ const regs = await findAllRegistrations(wallet, { timeoutMs: 15000 });
84
+
85
+ if (regs.length === 0) {
86
+ console.log(" No registrations found.\n");
87
+ return;
88
+ }
89
+
90
+ console.log(` Found ${regs.length} registration(s):\n`);
91
+ for (const r of regs) {
92
+ console.log(` āœ… ${r.chainName.padEnd(12)} #${String(r.agentId).padEnd(6)} ${r.globalId}`);
93
+ }
94
+ console.log();
95
+
96
+ } else if (cmd === "chains") {
97
+ console.log("\n🌐 Supported ERC-8004 chains:\n");
98
+ for (const [id, chain] of Object.entries(SUPPORTED_CHAINS)) {
99
+ console.log(` ${chain.name.padEnd(15)} chain ${id}`);
100
+ }
101
+ console.log(`\n Total: ${Object.keys(SUPPORTED_CHAINS).length} chains\n`);
102
+
103
+ } else {
104
+ console.error(`Unknown command: ${cmd}`);
105
+ console.log(HELP);
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ main().catch((err) => {
111
+ console.error("Error:", err.message);
112
+ process.exit(1);
113
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@a3stack/identity",
3
+ "version": "0.1.0",
4
+ "description": "ERC-8004 agent identity registration, verification, and discovery",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "bin": {
17
+ "agent-id": "./bin/agent-id.ts"
18
+ },
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format esm,cjs --dts --outDir dist",
21
+ "typecheck": "tsc --noEmit"
22
+ },
23
+ "dependencies": {
24
+ "viem": "^2.39.3",
25
+ "zod": "^3.24.2"
26
+ },
27
+ "devDependencies": {
28
+ "tsup": "^8.0.0",
29
+ "typescript": "^5.4.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ }
34
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * ERC-8004 contract addresses and ABIs
3
+ */
4
+
5
+ /**
6
+ * ERC-8004 Identity Registry — deployed at same address on all supported chains
7
+ * The address 0x8004... is a vanity address (mirrors the EIP number)
8
+ * Verified on-chain: has bytecode on Base (8453) and Ethereum (1)
9
+ */
10
+ export const IDENTITY_REGISTRY_ADDRESS =
11
+ "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432" as const;
12
+
13
+ /**
14
+ * Chains where ERC-8004 is confirmed deployed (as of Feb 2026)
15
+ */
16
+ export const SUPPORTED_CHAINS: Record<
17
+ number,
18
+ { name: string; rpc: string; chainId: number }
19
+ > = {
20
+ 1: { name: "Ethereum", rpc: "https://eth.llamarpc.com", chainId: 1 },
21
+ 8453: { name: "Base", rpc: "https://mainnet.base.org", chainId: 8453 },
22
+ 10: { name: "Optimism", rpc: "https://mainnet.optimism.io", chainId: 10 },
23
+ 42161: { name: "Arbitrum", rpc: "https://arb1.arbitrum.io/rpc", chainId: 42161 },
24
+ 137: { name: "Polygon", rpc: "https://polygon-rpc.com", chainId: 137 },
25
+ 56: { name: "BNB Chain", rpc: "https://bsc-dataseed.binance.org", chainId: 56 },
26
+ 100: { name: "Gnosis", rpc: "https://rpc.gnosischain.com", chainId: 100 },
27
+ 42220: { name: "Celo", rpc: "https://forno.celo.org", chainId: 42220 },
28
+ 59144: { name: "Linea", rpc: "https://rpc.linea.build", chainId: 59144 },
29
+ 534352: { name: "Scroll", rpc: "https://rpc.scroll.io", chainId: 534352 },
30
+ 167000: { name: "Taiko", rpc: "https://rpc.mainnet.taiko.xyz", chainId: 167000 },
31
+ 43114: { name: "Avalanche", rpc: "https://api.avax.network/ext/bc/C/rpc", chainId: 43114 },
32
+ 5000: { name: "Mantle", rpc: "https://rpc.mantle.xyz", chainId: 5000 },
33
+ 1088: { name: "Metis", rpc: "https://andromeda.metis.io/?owner=1088", chainId: 1088 },
34
+ 2741: { name: "Abstract", rpc: "https://api.mainnet.abs.xyz", chainId: 2741 },
35
+ 143: { name: "Monad", rpc: "https://rpc.monad.xyz", chainId: 143 },
36
+ };
37
+
38
+ /**
39
+ * ERC-8004 Identity Registry ABI
40
+ */
41
+ export const IDENTITY_REGISTRY_ABI = [
42
+ // Core registration
43
+ {
44
+ name: "register",
45
+ type: "function",
46
+ stateMutability: "nonpayable",
47
+ inputs: [{ name: "agentURI", type: "string" }],
48
+ outputs: [{ name: "", type: "uint256" }],
49
+ },
50
+ {
51
+ name: "register",
52
+ type: "function",
53
+ stateMutability: "nonpayable",
54
+ inputs: [
55
+ { name: "agentURI", type: "string" },
56
+ { name: "metadataKeys", type: "string[]" },
57
+ { name: "metadataValues", type: "bytes[]" },
58
+ ],
59
+ outputs: [{ name: "", type: "uint256" }],
60
+ },
61
+ {
62
+ name: "setAgentURI",
63
+ type: "function",
64
+ stateMutability: "nonpayable",
65
+ inputs: [
66
+ { name: "agentId", type: "uint256" },
67
+ { name: "newURI", type: "string" },
68
+ ],
69
+ outputs: [],
70
+ },
71
+ // ERC-721 standard
72
+ {
73
+ name: "balanceOf",
74
+ type: "function",
75
+ stateMutability: "view",
76
+ inputs: [{ name: "owner", type: "address" }],
77
+ outputs: [{ name: "", type: "uint256" }],
78
+ },
79
+ {
80
+ name: "ownerOf",
81
+ type: "function",
82
+ stateMutability: "view",
83
+ inputs: [{ name: "tokenId", type: "uint256" }],
84
+ outputs: [{ name: "", type: "address" }],
85
+ },
86
+ {
87
+ name: "tokenURI",
88
+ type: "function",
89
+ stateMutability: "view",
90
+ inputs: [{ name: "tokenId", type: "uint256" }],
91
+ outputs: [{ name: "", type: "string" }],
92
+ },
93
+ // Transfer event (for extracting agentId from receipt)
94
+ {
95
+ name: "Transfer",
96
+ type: "event",
97
+ inputs: [
98
+ { name: "from", type: "address", indexed: true },
99
+ { name: "to", type: "address", indexed: true },
100
+ { name: "tokenId", type: "uint256", indexed: true },
101
+ ],
102
+ },
103
+ // Payment wallet
104
+ {
105
+ name: "setAgentWallet",
106
+ type: "function",
107
+ stateMutability: "nonpayable",
108
+ inputs: [
109
+ { name: "agentId", type: "uint256" },
110
+ { name: "newWallet", type: "address" },
111
+ { name: "deadline", type: "uint256" },
112
+ { name: "signature", type: "bytes" },
113
+ ],
114
+ outputs: [],
115
+ },
116
+ {
117
+ name: "getAgentWallet",
118
+ type: "function",
119
+ stateMutability: "view",
120
+ inputs: [{ name: "agentId", type: "uint256" }],
121
+ outputs: [{ name: "", type: "address" }],
122
+ },
123
+ {
124
+ name: "unsetAgentWallet",
125
+ type: "function",
126
+ stateMutability: "nonpayable",
127
+ inputs: [{ name: "agentId", type: "uint256" }],
128
+ outputs: [],
129
+ },
130
+ // On-chain metadata
131
+ {
132
+ name: "getMetadata",
133
+ type: "function",
134
+ stateMutability: "view",
135
+ inputs: [
136
+ { name: "agentId", type: "uint256" },
137
+ { name: "metadataKey", type: "string" },
138
+ ],
139
+ outputs: [{ name: "", type: "bytes" }],
140
+ },
141
+ {
142
+ name: "setMetadata",
143
+ type: "function",
144
+ stateMutability: "nonpayable",
145
+ inputs: [
146
+ { name: "agentId", type: "uint256" },
147
+ { name: "metadataKey", type: "string" },
148
+ { name: "metadataValue", type: "bytes" },
149
+ ],
150
+ outputs: [],
151
+ },
152
+ ] as const;
153
+
154
+ export const REGISTRATION_TYPE =
155
+ "https://eips.ethereum.org/EIPS/eip-8004#registration-v1";
156
+
157
+ /** Zero address — returned when no payment wallet is set */
158
+ export const ZERO_ADDRESS =
159
+ "0x0000000000000000000000000000000000000000" as const;
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @a3stack/identity
3
+ * ERC-8004 agent identity: registration, verification, and discovery
4
+ */
5
+
6
+ export { AgentIdentity } from "./registry.js";
7
+ export {
8
+ parseAgentId,
9
+ formatAgentId,
10
+ fetchRegistrationFile,
11
+ verifyAgent,
12
+ getMcpEndpoint,
13
+ getA2aEndpoint,
14
+ getAgentCount,
15
+ } from "./verify.js";
16
+ export {
17
+ IDENTITY_REGISTRY_ADDRESS,
18
+ IDENTITY_REGISTRY_ABI,
19
+ SUPPORTED_CHAINS,
20
+ REGISTRATION_TYPE,
21
+ } from "./constants.js";
22
+ export { findAllRegistrations } from "./multichain.js";
23
+ export type { ChainRegistration } from "./multichain.js";
24
+ export type {
25
+ AgentService,
26
+ AgentRegistrationFile,
27
+ AgentRegistrationRef,
28
+ AgentRef,
29
+ VerificationResult,
30
+ RegisterOptions,
31
+ RegisterResult,
32
+ IdentityConfig,
33
+ DiscoverOptions,
34
+ } from "./types.js";
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Multi-chain agent discovery — find all registrations across supported chains
3
+ */
4
+
5
+ import { createPublicClient, http } from "viem";
6
+ import {
7
+ IDENTITY_REGISTRY_ADDRESS,
8
+ IDENTITY_REGISTRY_ABI,
9
+ SUPPORTED_CHAINS,
10
+ } from "./constants.js";
11
+ import { fetchRegistrationFile } from "./verify.js";
12
+ import type { AgentRegistrationFile } from "./types.js";
13
+
14
+ export interface ChainRegistration {
15
+ chainId: number;
16
+ chainName: string;
17
+ agentId: number;
18
+ owner: `0x${string}`;
19
+ agentUri: string;
20
+ registration: AgentRegistrationFile | null;
21
+ globalId: string;
22
+ }
23
+
24
+ /**
25
+ * Find all ERC-8004 registrations for a wallet address across all supported chains.
26
+ * Scans Transfer events (mint from 0x0) on each chain in parallel.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * const regs = await findAllRegistrations("0x1be93C...");
31
+ * console.log(`Found ${regs.length} registrations across ${regs.map(r => r.chainName).join(", ")}`);
32
+ * ```
33
+ */
34
+ export async function findAllRegistrations(
35
+ walletAddress: `0x${string}`,
36
+ options?: {
37
+ /** Only scan these chain IDs (default: all supported) */
38
+ chainIds?: number[];
39
+ /** Fetch and parse registration files (slower, default: false) */
40
+ fetchRegistration?: boolean;
41
+ /** Registry address override */
42
+ registry?: `0x${string}`;
43
+ /** Timeout per chain in ms (default: 10000) */
44
+ timeoutMs?: number;
45
+ }
46
+ ): Promise<ChainRegistration[]> {
47
+ const registry = options?.registry ?? IDENTITY_REGISTRY_ADDRESS;
48
+ const chainIds = options?.chainIds ?? Object.keys(SUPPORTED_CHAINS).map(Number);
49
+ const fetchReg = options?.fetchRegistration ?? false;
50
+ const timeout = options?.timeoutMs ?? 10000;
51
+
52
+ const results: ChainRegistration[] = [];
53
+
54
+ // Scan all chains in parallel
55
+ const promises = chainIds.map(async (chainId) => {
56
+ const chain = SUPPORTED_CHAINS[chainId];
57
+ if (!chain) return;
58
+
59
+ try {
60
+ const client = createPublicClient({ transport: http(chain.rpc) });
61
+
62
+ // Check balance first (fast)
63
+ const balance = (await Promise.race([
64
+ client.readContract({
65
+ address: registry,
66
+ abi: IDENTITY_REGISTRY_ABI,
67
+ functionName: "balanceOf",
68
+ args: [walletAddress],
69
+ }),
70
+ new Promise<bigint>((_, reject) =>
71
+ setTimeout(() => reject(new Error("timeout")), timeout)
72
+ ),
73
+ ])) as bigint;
74
+
75
+ if (balance === 0n) return;
76
+
77
+ // Find the agent IDs via Transfer events (mint from 0x0)
78
+ const paddedAddress = walletAddress.toLowerCase().replace("0x", "0x000000000000000000000000");
79
+ const logs = await client.getLogs({
80
+ address: registry,
81
+ event: {
82
+ type: "event",
83
+ name: "Transfer",
84
+ inputs: [
85
+ { type: "address", indexed: true, name: "from" },
86
+ { type: "address", indexed: true, name: "to" },
87
+ { type: "uint256", indexed: true, name: "tokenId" },
88
+ ],
89
+ },
90
+ args: {
91
+ from: "0x0000000000000000000000000000000000000000" as `0x${string}`,
92
+ to: walletAddress,
93
+ },
94
+ fromBlock: 0n,
95
+ toBlock: "latest",
96
+ });
97
+
98
+ for (const log of logs) {
99
+ const agentId = Number(log.args.tokenId);
100
+
101
+ // Get the current owner (could have been transferred)
102
+ const currentOwner = (await client.readContract({
103
+ address: registry,
104
+ abi: IDENTITY_REGISTRY_ABI,
105
+ functionName: "ownerOf",
106
+ args: [BigInt(agentId)],
107
+ })) as `0x${string}`;
108
+
109
+ // Only include if still owned by the queried wallet
110
+ if (currentOwner.toLowerCase() !== walletAddress.toLowerCase()) continue;
111
+
112
+ // Get agent URI
113
+ const agentUri = (await client.readContract({
114
+ address: registry,
115
+ abi: IDENTITY_REGISTRY_ABI,
116
+ functionName: "tokenURI",
117
+ args: [BigInt(agentId)],
118
+ })) as string;
119
+
120
+ let registration: AgentRegistrationFile | null = null;
121
+ if (fetchReg) {
122
+ try {
123
+ registration = await fetchRegistrationFile(agentUri);
124
+ } catch { /* ignore fetch errors */ }
125
+ }
126
+
127
+ results.push({
128
+ chainId,
129
+ chainName: chain.name,
130
+ agentId,
131
+ owner: currentOwner,
132
+ agentUri,
133
+ registration,
134
+ globalId: `eip155:${chainId}:${registry}#${agentId}`,
135
+ });
136
+ }
137
+ } catch {
138
+ // Chain unreachable or other error — skip silently
139
+ }
140
+ });
141
+
142
+ await Promise.allSettled(promises);
143
+
144
+ // Sort by chainId for consistent ordering
145
+ return results.sort((a, b) => a.chainId - b.chainId);
146
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * AgentIdentity — register and manage ERC-8004 agent identity
3
+ */
4
+
5
+ import { createPublicClient, createWalletClient, http, parseEventLogs } from "viem";
6
+ import type { Account, Chain, WalletClient } from "viem";
7
+ import {
8
+ IDENTITY_REGISTRY_ADDRESS,
9
+ IDENTITY_REGISTRY_ABI,
10
+ REGISTRATION_TYPE,
11
+ ZERO_ADDRESS,
12
+ SUPPORTED_CHAINS,
13
+ } from "./constants.js";
14
+ import type {
15
+ IdentityConfig,
16
+ RegisterOptions,
17
+ RegisterResult,
18
+ AgentRegistrationFile,
19
+ } from "./types.js";
20
+
21
+ export class AgentIdentity {
22
+ private config: IdentityConfig;
23
+ private registry: `0x${string}`;
24
+ private rpcUrl: string;
25
+
26
+ constructor(config: IdentityConfig) {
27
+ this.config = config;
28
+ this.registry = config.registry ?? IDENTITY_REGISTRY_ADDRESS;
29
+
30
+ // Resolve RPC URL
31
+ const chainDefaults = SUPPORTED_CHAINS[config.chain.id];
32
+ this.rpcUrl =
33
+ config.rpc ??
34
+ config.chain.rpcUrls?.default?.http?.[0] ??
35
+ chainDefaults?.rpc ??
36
+ "";
37
+
38
+ if (!this.rpcUrl) {
39
+ throw new Error(
40
+ `No RPC URL provided for chain ${config.chain.id}. Pass rpc option or use a supported chain.`
41
+ );
42
+ }
43
+ }
44
+
45
+ private getPublicClient() {
46
+ return createPublicClient({
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ chain: this.config.chain as any,
49
+ transport: http(this.rpcUrl),
50
+ });
51
+ }
52
+
53
+ private getWalletClient() {
54
+ // The account must be a full viem Account for write operations
55
+ if (!this.config.account?.signTransaction && !this.config.account?.sign) {
56
+ throw new Error(
57
+ "Write operations require a full viem Account with signing capability. " +
58
+ "Use privateKeyToAccount() from viem/accounts."
59
+ );
60
+ }
61
+ return createWalletClient({
62
+ account: this.config.account as Account,
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ chain: this.config.chain as any,
65
+ transport: http(this.rpcUrl),
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Build the registration JSON and encode as data URI
71
+ */
72
+ buildRegistrationUri(options: RegisterOptions, agentId?: number): string {
73
+ const chainId = this.config.chain.id;
74
+ const registrations = options.existingRegistrations
75
+ ? [...options.existingRegistrations]
76
+ : [];
77
+
78
+ // Add current chain registration if we have an agentId
79
+ if (agentId !== undefined) {
80
+ registrations.unshift({
81
+ agentId,
82
+ agentRegistry: `eip155:${chainId}:${this.registry}`,
83
+ });
84
+ }
85
+
86
+ const file: AgentRegistrationFile = {
87
+ type: REGISTRATION_TYPE,
88
+ name: options.name,
89
+ description: options.description,
90
+ ...(options.image ? { image: options.image } : {}),
91
+ services: options.services ?? [],
92
+ x402Support: options.x402Support ?? false,
93
+ active: options.active ?? true,
94
+ registrations,
95
+ ...(options.supportedTrust ? { supportedTrust: options.supportedTrust } : {}),
96
+ };
97
+
98
+ const json = JSON.stringify(file);
99
+ return "data:application/json;base64," + Buffer.from(json).toString("base64");
100
+ }
101
+
102
+ /**
103
+ * Check if this wallet is already registered on the current chain
104
+ */
105
+ async isRegistered(): Promise<{ registered: boolean; agentId?: number }> {
106
+ const client = this.getPublicClient();
107
+ const balance = await client.readContract({
108
+ address: this.registry,
109
+ abi: IDENTITY_REGISTRY_ABI,
110
+ functionName: "balanceOf",
111
+ args: [this.config.account.address],
112
+ });
113
+
114
+ if (balance === 0n) {
115
+ return { registered: false };
116
+ }
117
+
118
+ // We'd need to scan Transfer events to find the agentId — return basic result
119
+ return { registered: true };
120
+ }
121
+
122
+ /**
123
+ * Register this agent on the current chain
124
+ */
125
+ async register(options: RegisterOptions): Promise<RegisterResult> {
126
+ const walletClient = this.getWalletClient();
127
+
128
+ // Build a temporary URI without agentId first (we don't know it yet)
129
+ // We'll build it with a placeholder, then update after we have the ID
130
+ const placeholderUri = this.buildRegistrationUri(options);
131
+
132
+ // Estimate gas if not provided
133
+ const publicClient = this.getPublicClient();
134
+
135
+ const { request } = await publicClient.simulateContract({
136
+ account: this.config.account as Account,
137
+ address: this.registry,
138
+ abi: IDENTITY_REGISTRY_ABI,
139
+ functionName: "register",
140
+ args: [placeholderUri],
141
+ });
142
+
143
+ const txHash = await walletClient.writeContract({
144
+ ...request,
145
+ ...(options.gasLimit ? { gas: options.gasLimit } : {}),
146
+ });
147
+
148
+ // Wait for receipt
149
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
150
+
151
+ // Extract agentId from Transfer event (ERC-721 mint: from=0x0)
152
+ const logs = parseEventLogs({
153
+ abi: IDENTITY_REGISTRY_ABI,
154
+ eventName: "Transfer",
155
+ logs: receipt.logs,
156
+ });
157
+
158
+ const mintLog = logs.find(
159
+ (log) =>
160
+ "args" in log &&
161
+ log.args.from === ZERO_ADDRESS
162
+ );
163
+
164
+ if (!mintLog || !("args" in mintLog)) {
165
+ throw new Error("Could not find Transfer event in registration receipt");
166
+ }
167
+
168
+ const agentId = Number(mintLog.args.tokenId);
169
+ const chainId = this.config.chain.id;
170
+
171
+ // Now update the URI with the actual agentId
172
+ const finalUri = this.buildRegistrationUri(options, agentId);
173
+ await this.setAgentURI(agentId, finalUri);
174
+
175
+ const globalId = `eip155:${chainId}:${this.registry}#${agentId}`;
176
+
177
+ return { agentId, txHash, globalId, agentUri: finalUri };
178
+ }
179
+
180
+ /**
181
+ * Update the agent's URI (registration file location)
182
+ */
183
+ async setAgentURI(agentId: number, newUri: string): Promise<`0x${string}`> {
184
+ const walletClient = this.getWalletClient();
185
+ const publicClient = this.getPublicClient();
186
+
187
+ const { request } = await publicClient.simulateContract({
188
+ account: this.config.account as Account,
189
+ address: this.registry,
190
+ abi: IDENTITY_REGISTRY_ABI,
191
+ functionName: "setAgentURI",
192
+ args: [BigInt(agentId), newUri],
193
+ });
194
+
195
+ return walletClient.writeContract(request);
196
+ }
197
+
198
+ /**
199
+ * Get the agentURI for a given agentId
200
+ */
201
+ async getAgentURI(agentId: number): Promise<string> {
202
+ const client = this.getPublicClient();
203
+ return client.readContract({
204
+ address: this.registry,
205
+ abi: IDENTITY_REGISTRY_ABI,
206
+ functionName: "tokenURI",
207
+ args: [BigInt(agentId)],
208
+ }) as Promise<string>;
209
+ }
210
+
211
+ /**
212
+ * Get the payment wallet for a given agentId
213
+ */
214
+ async getPaymentWallet(agentId: number): Promise<`0x${string}` | null> {
215
+ const client = this.getPublicClient();
216
+ const wallet = (await client.readContract({
217
+ address: this.registry,
218
+ abi: IDENTITY_REGISTRY_ABI,
219
+ functionName: "getAgentWallet",
220
+ args: [BigInt(agentId)],
221
+ })) as `0x${string}`;
222
+
223
+ return wallet === ZERO_ADDRESS ? null : wallet;
224
+ }
225
+
226
+ /**
227
+ * Get on-chain metadata for an agent
228
+ */
229
+ async getMetadata(agentId: number, key: string): Promise<string | null> {
230
+ const client = this.getPublicClient();
231
+ const raw = (await client.readContract({
232
+ address: this.registry,
233
+ abi: IDENTITY_REGISTRY_ABI,
234
+ functionName: "getMetadata",
235
+ args: [BigInt(agentId), key],
236
+ })) as `0x${string}`;
237
+
238
+ if (!raw || raw === "0x") return null;
239
+ return Buffer.from(raw.slice(2), "hex").toString("utf8");
240
+ }
241
+
242
+ /**
243
+ * Set on-chain metadata for an agent (requires ownership)
244
+ */
245
+ async setMetadata(agentId: number, key: string, value: string): Promise<`0x${string}`> {
246
+ const walletClient = this.getWalletClient();
247
+ const publicClient = this.getPublicClient();
248
+
249
+ const valueBytes = `0x${Buffer.from(value, "utf8").toString("hex")}` as `0x${string}`;
250
+ const { request } = await publicClient.simulateContract({
251
+ account: this.config.account as Account,
252
+ address: this.registry,
253
+ abi: IDENTITY_REGISTRY_ABI,
254
+ functionName: "setMetadata",
255
+ args: [BigInt(agentId), key, valueBytes],
256
+ });
257
+
258
+ return walletClient.writeContract(request);
259
+ }
260
+
261
+ /**
262
+ * Get the owner of an agent
263
+ */
264
+ async getOwner(agentId: number): Promise<`0x${string}`> {
265
+ const client = this.getPublicClient();
266
+ return client.readContract({
267
+ address: this.registry,
268
+ abi: IDENTITY_REGISTRY_ABI,
269
+ functionName: "ownerOf",
270
+ args: [BigInt(agentId)],
271
+ }) as Promise<`0x${string}`>;
272
+ }
273
+ }
package/src/types.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * ERC-8004 Agent Identity Types
3
+ */
4
+
5
+ export interface AgentService {
6
+ name: string;
7
+ endpoint: string;
8
+ version?: string;
9
+ skills?: string[];
10
+ domains?: string[];
11
+ }
12
+
13
+ export interface AgentRegistrationFile {
14
+ type: string;
15
+ name: string;
16
+ description: string;
17
+ image?: string;
18
+ services: AgentService[];
19
+ x402Support: boolean;
20
+ active: boolean;
21
+ registrations: AgentRegistrationRef[];
22
+ supportedTrust?: string[];
23
+ }
24
+
25
+ export interface AgentRegistrationRef {
26
+ agentId: number;
27
+ agentRegistry: string; // "eip155:{chainId}:{registryAddress}"
28
+ }
29
+
30
+ export interface AgentRef {
31
+ namespace: string; // "eip155"
32
+ chainId: number; // 8453
33
+ registry: `0x${string}`; // "0x8004..."
34
+ agentId: number; // 2376
35
+ }
36
+
37
+ export interface VerificationResult {
38
+ valid: boolean;
39
+ agentId: number;
40
+ owner: `0x${string}` | null;
41
+ paymentWallet: `0x${string}` | null;
42
+ registration: AgentRegistrationFile | null;
43
+ globalId: string;
44
+ error?: string;
45
+ }
46
+
47
+ export interface RegisterOptions {
48
+ name: string;
49
+ description: string;
50
+ image?: string;
51
+ services?: AgentService[];
52
+ x402Support?: boolean;
53
+ active?: boolean;
54
+ supportedTrust?: string[];
55
+ /** Extra cross-chain registrations to include in the file */
56
+ existingRegistrations?: AgentRegistrationRef[];
57
+ /** Store as data URI (on-chain) vs IPFS URL. Default: data URI */
58
+ storage?: "data-uri" | "ipfs";
59
+ /** Optional gas limit override */
60
+ gasLimit?: bigint;
61
+ }
62
+
63
+ export interface RegisterResult {
64
+ agentId: number;
65
+ txHash: `0x${string}`;
66
+ globalId: string;
67
+ agentUri: string;
68
+ }
69
+
70
+ export interface IdentityConfig {
71
+ /**
72
+ * viem Account (e.g. from privateKeyToAccount())
73
+ * For read-only operations, a partial account with just address works.
74
+ */
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ account: any;
77
+ /**
78
+ * viem Chain object (e.g. import { base } from "viem/chains")
79
+ * Any object with at least { id, name } works.
80
+ */
81
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
+ chain: any;
83
+ /** RPC URL override */
84
+ rpc?: string;
85
+ /** Registry contract address override */
86
+ registry?: `0x${string}`;
87
+ }
88
+
89
+ export interface DiscoverOptions {
90
+ chain?: {
91
+ id: number;
92
+ rpcUrls?: { default?: { http?: string[] } };
93
+ };
94
+ /** Filter agents that have this service type (e.g. "MCP") */
95
+ hasService?: string;
96
+ /** Filter by x402Support flag */
97
+ x402Support?: boolean;
98
+ /** Filter by active flag */
99
+ active?: boolean;
100
+ /** Max number of results */
101
+ limit?: number;
102
+ }
package/src/verify.ts ADDED
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Agent identity verification and resolution
3
+ */
4
+
5
+ import { createPublicClient, http } from "viem";
6
+ import {
7
+ IDENTITY_REGISTRY_ADDRESS,
8
+ IDENTITY_REGISTRY_ABI,
9
+ ZERO_ADDRESS,
10
+ SUPPORTED_CHAINS,
11
+ } from "./constants.js";
12
+ import type { AgentRef, VerificationResult, AgentRegistrationFile } from "./types.js";
13
+
14
+ /**
15
+ * Parse an ERC-8004 global agent ID string
16
+ * Format: "eip155:{chainId}:{registry}#{agentId}"
17
+ * Example: "eip155:8453:0x8004...#2376"
18
+ */
19
+ export function parseAgentId(globalId: string): AgentRef {
20
+ // Support both "eip155:8453:0x8004...#2376" and "eip155:8453:0x8004.../2376"
21
+ const normalized = globalId.replace("/", "#");
22
+ const match = normalized.match(/^(eip155):(\d+):(0x[0-9a-fA-F]+)#(\d+)$/);
23
+ if (!match) {
24
+ throw new Error(
25
+ `Invalid agent global ID format: "${globalId}"\n` +
26
+ `Expected: "eip155:{chainId}:{registry}#{agentId}" (e.g. "eip155:8453:0x8004...#2376")`
27
+ );
28
+ }
29
+ return {
30
+ namespace: match[1],
31
+ chainId: Number(match[2]),
32
+ registry: match[3] as `0x${string}`,
33
+ agentId: Number(match[4]),
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Format a parsed AgentRef back to a global ID string
39
+ */
40
+ export function formatAgentId(ref: AgentRef): string {
41
+ return `${ref.namespace}:${ref.chainId}:${ref.registry}#${ref.agentId}`;
42
+ }
43
+
44
+ /**
45
+ * Fetch and parse an agent's registration file from its URI
46
+ * Handles: data: URIs, https:// URLs, ipfs:// CIDs (via gateway)
47
+ */
48
+ export async function fetchRegistrationFile(
49
+ agentUri: string,
50
+ ipfsGateway = "https://ipfs.io/ipfs/"
51
+ ): Promise<AgentRegistrationFile> {
52
+ let json: string;
53
+
54
+ if (agentUri.startsWith("data:application/json;base64,")) {
55
+ const b64 = agentUri.slice("data:application/json;base64,".length);
56
+ json = Buffer.from(b64, "base64").toString("utf8");
57
+ } else if (agentUri.startsWith("data:application/json,")) {
58
+ json = decodeURIComponent(agentUri.slice("data:application/json,".length));
59
+ } else if (agentUri.startsWith("ipfs://")) {
60
+ const cid = agentUri.slice("ipfs://".length);
61
+ const response = await fetch(`${ipfsGateway}${cid}`);
62
+ if (!response.ok) {
63
+ throw new Error(`Failed to fetch IPFS URI: ${agentUri} (${response.status})`);
64
+ }
65
+ json = await response.text();
66
+ } else if (agentUri.startsWith("https://") || agentUri.startsWith("http://")) {
67
+ const response = await fetch(agentUri, {
68
+ headers: { Accept: "application/json" },
69
+ });
70
+ if (!response.ok) {
71
+ throw new Error(`Failed to fetch agent URI: ${agentUri} (${response.status})`);
72
+ }
73
+ json = await response.text();
74
+ } else {
75
+ throw new Error(
76
+ `Unsupported URI scheme: "${agentUri}". ` +
77
+ `Supported: data:, ipfs://, https://, http://`
78
+ );
79
+ }
80
+
81
+ const parsed = JSON.parse(json) as AgentRegistrationFile;
82
+ return parsed;
83
+ }
84
+
85
+ /**
86
+ * Verify an agent's identity from its global ID
87
+ * Performs full on-chain + off-chain verification
88
+ */
89
+ export async function verifyAgent(
90
+ globalIdOrRef:
91
+ | string
92
+ | { chainId: number; agentId: number; registry?: `0x${string}`; rpc?: string }
93
+ ): Promise<VerificationResult> {
94
+ let ref: AgentRef;
95
+
96
+ if (typeof globalIdOrRef === "string") {
97
+ ref = parseAgentId(globalIdOrRef);
98
+ } else {
99
+ ref = {
100
+ namespace: "eip155",
101
+ chainId: globalIdOrRef.chainId,
102
+ registry: globalIdOrRef.registry ?? IDENTITY_REGISTRY_ADDRESS,
103
+ agentId: globalIdOrRef.agentId,
104
+ };
105
+ }
106
+
107
+ const globalId = formatAgentId(ref);
108
+ const chainDefaults = SUPPORTED_CHAINS[ref.chainId];
109
+ const rpcUrl =
110
+ (typeof globalIdOrRef === "object" && "rpc" in globalIdOrRef
111
+ ? globalIdOrRef.rpc
112
+ : undefined) ??
113
+ chainDefaults?.rpc;
114
+
115
+ if (!rpcUrl) {
116
+ return {
117
+ valid: false,
118
+ agentId: ref.agentId,
119
+ owner: null,
120
+ paymentWallet: null,
121
+ registration: null,
122
+ globalId,
123
+ error: `No RPC URL available for chain ${ref.chainId}. Add to SUPPORTED_CHAINS or pass rpc option.`,
124
+ };
125
+ }
126
+
127
+ const client = createPublicClient({
128
+ transport: http(rpcUrl),
129
+ });
130
+
131
+ try {
132
+ // Step 1: Get owner
133
+ const owner = (await client.readContract({
134
+ address: ref.registry,
135
+ abi: IDENTITY_REGISTRY_ABI,
136
+ functionName: "ownerOf",
137
+ args: [BigInt(ref.agentId)],
138
+ })) as `0x${string}`;
139
+
140
+ // Step 2: Get payment wallet
141
+ const paymentWalletRaw = (await client.readContract({
142
+ address: ref.registry,
143
+ abi: IDENTITY_REGISTRY_ABI,
144
+ functionName: "getAgentWallet",
145
+ args: [BigInt(ref.agentId)],
146
+ })) as `0x${string}`;
147
+
148
+ const paymentWallet =
149
+ paymentWalletRaw === ZERO_ADDRESS ? null : paymentWalletRaw;
150
+
151
+ // Step 3: Get agent URI and fetch registration file
152
+ const agentUri = (await client.readContract({
153
+ address: ref.registry,
154
+ abi: IDENTITY_REGISTRY_ABI,
155
+ functionName: "tokenURI",
156
+ args: [BigInt(ref.agentId)],
157
+ })) as string;
158
+
159
+ let registration: AgentRegistrationFile | null = null;
160
+ try {
161
+ registration = await fetchRegistrationFile(agentUri);
162
+ } catch (e) {
163
+ return {
164
+ valid: false,
165
+ agentId: ref.agentId,
166
+ owner,
167
+ paymentWallet,
168
+ registration: null,
169
+ globalId,
170
+ error: `Failed to fetch registration file: ${(e as Error).message}`,
171
+ };
172
+ }
173
+
174
+ // Step 4: Verify back-reference (registration file should list this chain's registration)
175
+ const registryPrefix = `eip155:${ref.chainId}:${ref.registry.toLowerCase()}`;
176
+ const hasBackref = registration.registrations?.some(
177
+ (r) =>
178
+ r.agentId === ref.agentId &&
179
+ r.agentRegistry.toLowerCase() === registryPrefix
180
+ );
181
+
182
+ if (!hasBackref) {
183
+ return {
184
+ valid: false,
185
+ agentId: ref.agentId,
186
+ owner,
187
+ paymentWallet,
188
+ registration,
189
+ globalId,
190
+ error:
191
+ `Registration file does not contain a back-reference to this chain's registry. ` +
192
+ `Expected registrations entry: { agentId: ${ref.agentId}, agentRegistry: "${registryPrefix}" }`,
193
+ };
194
+ }
195
+
196
+ return {
197
+ valid: true,
198
+ agentId: ref.agentId,
199
+ owner,
200
+ paymentWallet,
201
+ registration,
202
+ globalId,
203
+ };
204
+ } catch (e) {
205
+ // ownerOf throws if token doesn't exist
206
+ const message = (e as Error).message;
207
+ return {
208
+ valid: false,
209
+ agentId: ref.agentId,
210
+ owner: null,
211
+ paymentWallet: null,
212
+ registration: null,
213
+ globalId,
214
+ error: message.includes("revert") || message.includes("ERC721")
215
+ ? `Agent #${ref.agentId} does not exist on chain ${ref.chainId}`
216
+ : message,
217
+ };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Get the MCP endpoint for an agent by resolving their registration
223
+ */
224
+ export async function getMcpEndpoint(globalId: string): Promise<string | null> {
225
+ const result = await verifyAgent(globalId);
226
+ if (!result.valid || !result.registration) return null;
227
+
228
+ const mcpService = result.registration.services.find(
229
+ (s) => s.name.toUpperCase() === "MCP"
230
+ );
231
+ return mcpService?.endpoint ?? null;
232
+ }
233
+
234
+ /**
235
+ * Get the A2A endpoint for an agent
236
+ */
237
+ export async function getA2aEndpoint(globalId: string): Promise<string | null> {
238
+ const result = await verifyAgent(globalId);
239
+ if (!result.valid || !result.registration) return null;
240
+
241
+ const a2aService = result.registration.services.find(
242
+ (s) => s.name.toUpperCase() === "A2A"
243
+ );
244
+ return a2aService?.endpoint ?? null;
245
+ }
246
+
247
+ /**
248
+ * Lightweight check: does this address own any agents on a given chain?
249
+ */
250
+ export async function getAgentCount(
251
+ address: `0x${string}`,
252
+ chainId: number,
253
+ rpc?: string
254
+ ): Promise<number> {
255
+ const chainDefaults = SUPPORTED_CHAINS[chainId];
256
+ const rpcUrl = rpc ?? chainDefaults?.rpc;
257
+ if (!rpcUrl) throw new Error(`No RPC for chain ${chainId}`);
258
+
259
+ const client = createPublicClient({ transport: http(rpcUrl) });
260
+ const balance = (await client.readContract({
261
+ address: IDENTITY_REGISTRY_ADDRESS,
262
+ abi: IDENTITY_REGISTRY_ABI,
263
+ functionName: "balanceOf",
264
+ args: [address],
265
+ })) as bigint;
266
+
267
+ return Number(balance);
268
+ }