a3stack 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.
Files changed (3) hide show
  1. package/bin/a3stack.js +391 -0
  2. package/dist/cli.js +391 -0
  3. package/package.json +42 -0
package/bin/a3stack.js ADDED
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { createPublicClient, http, getAddress } from "viem";
5
+ var REGISTRY_ADDRESS = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432";
6
+ var REGISTRY_ABI = [
7
+ { inputs: [{ name: "agentId", type: "uint256" }], name: "ownerOf", outputs: [{ type: "address" }], stateMutability: "view", type: "function" },
8
+ { inputs: [{ name: "agentId", type: "uint256" }], name: "tokenURI", outputs: [{ type: "string" }], stateMutability: "view", type: "function" },
9
+ { inputs: [{ name: "owner", type: "address" }], name: "balanceOf", outputs: [{ type: "uint256" }], stateMutability: "view", type: "function" }
10
+ ];
11
+ var TRANSFER_EVENT = {
12
+ type: "event",
13
+ name: "Transfer",
14
+ inputs: [
15
+ { type: "address", indexed: true, name: "from" },
16
+ { type: "address", indexed: true, name: "to" },
17
+ { type: "uint256", indexed: true, name: "tokenId" }
18
+ ]
19
+ };
20
+ var SUPPORTED_CHAINS = {
21
+ 1: { name: "Ethereum", rpc: "https://eth.llamarpc.com" },
22
+ 10: { name: "Optimism", rpc: "https://mainnet.optimism.io" },
23
+ 56: { name: "BNB Chain", rpc: "https://bsc-dataseed.binance.org" },
24
+ 100: { name: "Gnosis", rpc: "https://rpc.gnosischain.com" },
25
+ 130: { name: "Unichain", rpc: "https://mainnet.unichain.org" },
26
+ 137: { name: "Polygon", rpc: "https://polygon-rpc.com" },
27
+ 196: { name: "X Layer", rpc: "https://rpc.xlayer.tech" },
28
+ 480: { name: "World Chain", rpc: "https://worldchain-mainnet.g.alchemy.com/public" },
29
+ 690: { name: "Redstone", rpc: "https://rpc.redstonechain.com" },
30
+ 8453: { name: "Base", rpc: "https://mainnet.base.org" },
31
+ 42161: { name: "Arbitrum", rpc: "https://arb1.arbitrum.io/rpc" },
32
+ 42170: { name: "Arbitrum Nova", rpc: "https://nova.arbitrum.io/rpc" },
33
+ 43114: { name: "Avalanche", rpc: "https://api.avax.network/ext/bc/C/rpc" },
34
+ 57073: { name: "Ink", rpc: "https://rpc-gel.inkonchain.com" },
35
+ 81457: { name: "Blast", rpc: "https://rpc.blast.io" },
36
+ 534352: { name: "Scroll", rpc: "https://rpc.scroll.io" },
37
+ 7777777: { name: "Zora", rpc: "https://rpc.zora.energy" }
38
+ };
39
+ function parseGlobalId(id) {
40
+ const match = id.match(/^eip155:(\d+):(0x[a-fA-F0-9]{40})#(\d+)$/);
41
+ if (!match) throw new Error(`Invalid global ID format: ${id}
42
+ Expected: eip155:<chainId>:<registry>#<agentId>`);
43
+ return { chainId: Number(match[1]), registry: match[2], agentId: BigInt(match[3]) };
44
+ }
45
+ function getClient(chainId) {
46
+ const chain = SUPPORTED_CHAINS[chainId];
47
+ if (!chain) throw new Error(`Unsupported chain: ${chainId}. Run 'a3stack chains' to see supported chains.`);
48
+ return createPublicClient({ transport: http(chain.rpc) });
49
+ }
50
+ function bold(s) {
51
+ return `\x1B[1m${s}\x1B[0m`;
52
+ }
53
+ function green(s) {
54
+ return `\x1B[32m${s}\x1B[0m`;
55
+ }
56
+ function red(s) {
57
+ return `\x1B[31m${s}\x1B[0m`;
58
+ }
59
+ function dim(s) {
60
+ return `\x1B[2m${s}\x1B[0m`;
61
+ }
62
+ function cyan(s) {
63
+ return `\x1B[36m${s}\x1B[0m`;
64
+ }
65
+ function yellow(s) {
66
+ return `\x1B[33m${s}\x1B[0m`;
67
+ }
68
+ async function verify(globalId) {
69
+ console.log(`
70
+ ${bold("\u{1F50D} Verifying agent identity")}
71
+ `);
72
+ const ref = parseGlobalId(globalId);
73
+ const chainName = SUPPORTED_CHAINS[ref.chainId]?.name ?? `Chain ${ref.chainId}`;
74
+ console.log(` Chain: ${cyan(chainName)} (${ref.chainId})`);
75
+ console.log(` Registry: ${dim(ref.registry)}`);
76
+ console.log(` Agent ID: ${bold("#" + ref.agentId.toString())}
77
+ `);
78
+ const client = getClient(ref.chainId);
79
+ let owner;
80
+ try {
81
+ owner = await client.readContract({
82
+ address: ref.registry,
83
+ abi: REGISTRY_ABI,
84
+ functionName: "ownerOf",
85
+ args: [ref.agentId]
86
+ });
87
+ } catch (e) {
88
+ console.log(` ${red("\u2717")} Agent not found or reverted
89
+ `);
90
+ process.exit(1);
91
+ }
92
+ console.log(` ${green("\u2713")} ${bold("Verified on-chain")}`);
93
+ console.log(` Owner: ${owner}`);
94
+ try {
95
+ const uri = await client.readContract({
96
+ address: ref.registry,
97
+ abi: REGISTRY_ABI,
98
+ functionName: "tokenURI",
99
+ args: [ref.agentId]
100
+ });
101
+ if (uri.startsWith("data:application/json")) {
102
+ const json = JSON.parse(Buffer.from(uri.split(",")[1], "base64").toString());
103
+ if (json.name) console.log(` Name: ${bold(json.name)}`);
104
+ if (json.description) console.log(` Desc: ${json.description}`);
105
+ } else if (uri.startsWith("ipfs://") || uri.startsWith("http")) {
106
+ console.log(` URI: ${dim(uri)}`);
107
+ try {
108
+ const fetchUrl = uri.startsWith("ipfs://") ? `https://gateway.pinata.cloud/ipfs/${uri.slice(7)}` : uri;
109
+ const res = await fetch(fetchUrl, { signal: AbortSignal.timeout(5e3) });
110
+ if (res.ok) {
111
+ const json = await res.json();
112
+ if (json.name) console.log(` Name: ${bold(json.name)}`);
113
+ if (json.description) console.log(` Desc: ${json.description}`);
114
+ if (json.services?.length) {
115
+ console.log(`
116
+ ${bold("Services:")}`);
117
+ for (const s of json.services) {
118
+ console.log(` ${cyan("\u2192")} ${s.name}: ${s.endpoint}`);
119
+ }
120
+ }
121
+ }
122
+ } catch {
123
+ }
124
+ }
125
+ } catch {
126
+ }
127
+ console.log();
128
+ }
129
+ async function lookup(wallet) {
130
+ const addr = getAddress(wallet);
131
+ console.log(`
132
+ ${bold("\u{1F310} Scanning all chains")} for ${dim(addr)}
133
+ `);
134
+ const results = [];
135
+ const promises = Object.entries(SUPPORTED_CHAINS).map(async ([id, chain]) => {
136
+ const chainId = Number(id);
137
+ try {
138
+ const client = createPublicClient({ transport: http(chain.rpc) });
139
+ const balance = await client.readContract({
140
+ address: REGISTRY_ADDRESS,
141
+ abi: REGISTRY_ABI,
142
+ functionName: "balanceOf",
143
+ args: [addr]
144
+ });
145
+ if (balance === 0n) return;
146
+ const logs = await client.getLogs({
147
+ address: REGISTRY_ADDRESS,
148
+ event: TRANSFER_EVENT,
149
+ args: {
150
+ from: "0x0000000000000000000000000000000000000000",
151
+ to: addr
152
+ },
153
+ fromBlock: 0n,
154
+ toBlock: "latest"
155
+ });
156
+ for (const log of logs) {
157
+ const agentId = log.args.tokenId;
158
+ results.push({
159
+ chain: chain.name,
160
+ chainId,
161
+ agentId,
162
+ globalId: `eip155:${chainId}:${REGISTRY_ADDRESS}#${agentId}`
163
+ });
164
+ }
165
+ } catch {
166
+ }
167
+ });
168
+ await Promise.allSettled(promises);
169
+ if (results.length === 0) {
170
+ console.log(` No registrations found.
171
+ `);
172
+ return;
173
+ }
174
+ results.sort((a, b) => a.chainId - b.chainId);
175
+ console.log(` Found ${green(results.length.toString())} registration(s):
176
+ `);
177
+ for (const r of results) {
178
+ console.log(` ${green("\u2713")} ${r.chain.padEnd(15)} ${yellow("#" + r.agentId.toString().padEnd(6))} ${dim(r.globalId)}`);
179
+ }
180
+ console.log();
181
+ }
182
+ async function probe(globalId) {
183
+ console.log(`
184
+ ${bold("\u{1F52C} Probing agent")} ${dim(globalId)}
185
+ `);
186
+ const ref = parseGlobalId(globalId);
187
+ const chainName = SUPPORTED_CHAINS[ref.chainId]?.name ?? `Chain ${ref.chainId}`;
188
+ const client = getClient(ref.chainId);
189
+ let owner;
190
+ try {
191
+ owner = await client.readContract({
192
+ address: ref.registry,
193
+ abi: REGISTRY_ABI,
194
+ functionName: "ownerOf",
195
+ args: [ref.agentId]
196
+ });
197
+ } catch {
198
+ console.log(` ${red("\u2717")} Agent not found
199
+ `);
200
+ process.exit(1);
201
+ }
202
+ console.log(` ${green("\u2713")} Identity verified on ${cyan(chainName)}`);
203
+ console.log(` Owner: ${dim(owner)}`);
204
+ try {
205
+ const uri = await client.readContract({
206
+ address: ref.registry,
207
+ abi: REGISTRY_ABI,
208
+ functionName: "tokenURI",
209
+ args: [ref.agentId]
210
+ });
211
+ let metadata = null;
212
+ if (uri.startsWith("data:application/json")) {
213
+ metadata = JSON.parse(Buffer.from(uri.split(",")[1], "base64").toString());
214
+ } else if (uri.startsWith("ipfs://") || uri.startsWith("http")) {
215
+ const fetchUrl = uri.startsWith("ipfs://") ? `https://gateway.pinata.cloud/ipfs/${uri.slice(7)}` : uri;
216
+ const res = await fetch(fetchUrl, { signal: AbortSignal.timeout(8e3) });
217
+ if (res.ok) metadata = await res.json();
218
+ }
219
+ if (metadata) {
220
+ console.log(` Name: ${bold(metadata.name ?? "unnamed")}`);
221
+ if (metadata.description) console.log(` Desc: ${metadata.description}`);
222
+ if (metadata.active !== void 0) console.log(` Active: ${metadata.active ? green("yes") : red("no")}`);
223
+ if (metadata.x402Support) console.log(` x402: ${green("accepts payments")}`);
224
+ if (metadata.paymentWallet) console.log(` Pay to: ${dim(metadata.paymentWallet)}`);
225
+ if (metadata.services?.length) {
226
+ console.log(`
227
+ ${bold("Endpoints:")}`);
228
+ for (const s of metadata.services) {
229
+ console.log(` ${cyan("\u2192")} ${s.name.padEnd(8)} ${s.endpoint}`);
230
+ if (s.name.toUpperCase() === "MCP" && metadata.x402Support) {
231
+ try {
232
+ const r = await fetch(s.endpoint, {
233
+ method: "POST",
234
+ headers: { "Content-Type": "application/json" },
235
+ body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
236
+ signal: AbortSignal.timeout(5e3)
237
+ });
238
+ if (r.status === 402) {
239
+ console.log(` ${yellow("$")} Payment required (HTTP 402)`);
240
+ const header = r.headers.get("x-payment-required");
241
+ if (header) {
242
+ try {
243
+ const decoded = JSON.parse(Buffer.from(header, "base64").toString());
244
+ const first = Array.isArray(decoded.accepts) ? decoded.accepts[0] : decoded;
245
+ if (first.maxAmountRequired) console.log(` ${yellow("$")} Amount: ${first.maxAmountRequired} (${first.network ?? "unknown"})`);
246
+ } catch {
247
+ }
248
+ }
249
+ } else if (r.ok) {
250
+ console.log(` ${green("\u2713")} Endpoint reachable (free)`);
251
+ }
252
+ } catch {
253
+ console.log(` ${dim("\u26A0 Endpoint not reachable")}`);
254
+ }
255
+ }
256
+ }
257
+ }
258
+ if (metadata.registrations?.length) {
259
+ console.log(`
260
+ ${bold("Cross-chain IDs:")}`);
261
+ for (const reg of metadata.registrations) {
262
+ console.log(` ${dim("\u2022")} Agent #${reg.agentId} @ ${dim(reg.agentRegistry)}`);
263
+ }
264
+ }
265
+ }
266
+ } catch {
267
+ }
268
+ console.log();
269
+ }
270
+ async function showChains() {
271
+ console.log(`
272
+ ${bold("\u{1F310} Supported ERC-8004 chains")}
273
+ `);
274
+ console.log(` ${dim("Chain".padEnd(16))} ${dim("ID".padEnd(10))} ${dim("Registry")}
275
+ `);
276
+ for (const [id, chain] of Object.entries(SUPPORTED_CHAINS)) {
277
+ console.log(` ${chain.name.padEnd(16)} ${id.padEnd(10)} ${dim(REGISTRY_ADDRESS)}`);
278
+ }
279
+ console.log(`
280
+ ${bold(Object.keys(SUPPORTED_CHAINS).length.toString())} chains \u2014 same registry address on all.
281
+ `);
282
+ }
283
+ async function count(chainIdStr) {
284
+ const chainIds = chainIdStr ? [Number(chainIdStr)] : Object.keys(SUPPORTED_CHAINS).map(Number);
285
+ console.log(`
286
+ ${bold("\u{1F4CA} Agent count")} ${dim("(scanning Transfer events)")}
287
+ `);
288
+ const promises = chainIds.map(async (chainId) => {
289
+ const chain = SUPPORTED_CHAINS[chainId];
290
+ if (!chain) return { chainId, name: `Unknown (${chainId})`, count: -1 };
291
+ try {
292
+ const client = createPublicClient({ transport: http(chain.rpc) });
293
+ const logs = await client.getLogs({
294
+ address: REGISTRY_ADDRESS,
295
+ event: TRANSFER_EVENT,
296
+ args: { from: "0x0000000000000000000000000000000000000000" },
297
+ fromBlock: 0n,
298
+ toBlock: "latest"
299
+ });
300
+ return { chainId, name: chain.name, count: logs.length };
301
+ } catch {
302
+ return { chainId, name: chain.name, count: -1 };
303
+ }
304
+ });
305
+ const results = await Promise.all(promises);
306
+ results.sort((a, b) => b.count - a.count);
307
+ for (const r of results) {
308
+ if (r.count >= 0) {
309
+ console.log(` ${r.name.padEnd(16)} ${yellow(r.count.toString().padStart(6))} agents`);
310
+ } else {
311
+ console.log(` ${r.name.padEnd(16)} ${dim(" error")}`);
312
+ }
313
+ }
314
+ console.log();
315
+ }
316
+ function scaffoldInit() {
317
+ console.log(`
318
+ ${bold("\u{1F680} Scaffold a new A3Stack agent")}
319
+ `);
320
+ console.log(`${dim("Coming in v0.2.0 \u2014 for now, install the packages directly:")}
321
+ `);
322
+ console.log(` ${cyan("npm install @a3stack/core viem")}
323
+ `);
324
+ console.log(` Then in your code:
325
+ `);
326
+ console.log(` ${dim('import { A3Stack } from "@a3stack/core";')}`);
327
+ console.log(` ${dim("")}`);
328
+ console.log(` ${dim("const agent = new A3Stack({")}`);
329
+ console.log(` ${dim(" privateKey: process.env.PRIVATE_KEY,")}`);
330
+ console.log(` ${dim(" chainId: 8453, // Base")}`);
331
+ console.log(` ${dim("});")}`);
332
+ console.log(` ${dim("")}`);
333
+ console.log(` ${dim('await agent.register({ name: "my-agent" });')}`);
334
+ console.log(` ${dim('console.log("Registered:", agent.globalId);')}
335
+ `);
336
+ console.log(` Docs: ${cyan("https://a3stack.arcabot.ai")}
337
+ `);
338
+ }
339
+ var HELP = `
340
+ ${bold("a3stack")} \u2014 identity, payments, and data for AI agents
341
+
342
+ ${bold("Commands:")}
343
+ ${cyan("verify")} <globalId> Verify an agent's on-chain identity
344
+ ${cyan("lookup")} <wallet> Find all chain registrations for a wallet
345
+ ${cyan("probe")} <globalId> Discover an agent's capabilities & endpoints
346
+ ${cyan("chains")} List supported ERC-8004 chains
347
+ ${cyan("count")} [chainId] Count registered agents (all chains or one)
348
+ ${cyan("init")} Scaffold a new agent project
349
+
350
+ ${bold("Examples:")}
351
+ npx a3stack verify "eip155:8453:${REGISTRY_ADDRESS}#2376"
352
+ npx a3stack lookup 0xYOUR_WALLET_ADDRESS
353
+ npx a3stack probe "eip155:8453:${REGISTRY_ADDRESS}#2376"
354
+ npx a3stack count 8453
355
+
356
+ ${dim(`v0.1.0 \u2022 https://a3stack.arcabot.ai \u2022 github.com/arcabotai/a3stack`)}
357
+ `;
358
+ var [cmd, ...args] = process.argv.slice(2);
359
+ async function main() {
360
+ if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
361
+ console.log(HELP);
362
+ return;
363
+ }
364
+ if (cmd === "--version" || cmd === "-v") {
365
+ console.log("a3stack v0.1.0");
366
+ return;
367
+ }
368
+ switch (cmd) {
369
+ case "verify":
370
+ return verify(args[0]);
371
+ case "lookup":
372
+ return lookup(args[0]);
373
+ case "probe":
374
+ return probe(args[0]);
375
+ case "chains":
376
+ return showChains();
377
+ case "count":
378
+ return count(args[0]);
379
+ case "init":
380
+ return scaffoldInit();
381
+ default:
382
+ console.error(`${red("Unknown command:")} ${cmd}
383
+ `);
384
+ console.log(HELP);
385
+ process.exit(1);
386
+ }
387
+ }
388
+ main().catch((err) => {
389
+ console.error(`${red("Error:")} ${err.message}`);
390
+ process.exit(1);
391
+ });
package/dist/cli.js ADDED
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { createPublicClient, http, getAddress } from "viem";
5
+ var REGISTRY_ADDRESS = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432";
6
+ var REGISTRY_ABI = [
7
+ { inputs: [{ name: "agentId", type: "uint256" }], name: "ownerOf", outputs: [{ type: "address" }], stateMutability: "view", type: "function" },
8
+ { inputs: [{ name: "agentId", type: "uint256" }], name: "tokenURI", outputs: [{ type: "string" }], stateMutability: "view", type: "function" },
9
+ { inputs: [{ name: "owner", type: "address" }], name: "balanceOf", outputs: [{ type: "uint256" }], stateMutability: "view", type: "function" }
10
+ ];
11
+ var TRANSFER_EVENT = {
12
+ type: "event",
13
+ name: "Transfer",
14
+ inputs: [
15
+ { type: "address", indexed: true, name: "from" },
16
+ { type: "address", indexed: true, name: "to" },
17
+ { type: "uint256", indexed: true, name: "tokenId" }
18
+ ]
19
+ };
20
+ var SUPPORTED_CHAINS = {
21
+ 1: { name: "Ethereum", rpc: "https://eth.llamarpc.com" },
22
+ 10: { name: "Optimism", rpc: "https://mainnet.optimism.io" },
23
+ 56: { name: "BNB Chain", rpc: "https://bsc-dataseed.binance.org" },
24
+ 100: { name: "Gnosis", rpc: "https://rpc.gnosischain.com" },
25
+ 130: { name: "Unichain", rpc: "https://mainnet.unichain.org" },
26
+ 137: { name: "Polygon", rpc: "https://polygon-rpc.com" },
27
+ 196: { name: "X Layer", rpc: "https://rpc.xlayer.tech" },
28
+ 480: { name: "World Chain", rpc: "https://worldchain-mainnet.g.alchemy.com/public" },
29
+ 690: { name: "Redstone", rpc: "https://rpc.redstonechain.com" },
30
+ 8453: { name: "Base", rpc: "https://mainnet.base.org" },
31
+ 42161: { name: "Arbitrum", rpc: "https://arb1.arbitrum.io/rpc" },
32
+ 42170: { name: "Arbitrum Nova", rpc: "https://nova.arbitrum.io/rpc" },
33
+ 43114: { name: "Avalanche", rpc: "https://api.avax.network/ext/bc/C/rpc" },
34
+ 57073: { name: "Ink", rpc: "https://rpc-gel.inkonchain.com" },
35
+ 81457: { name: "Blast", rpc: "https://rpc.blast.io" },
36
+ 534352: { name: "Scroll", rpc: "https://rpc.scroll.io" },
37
+ 7777777: { name: "Zora", rpc: "https://rpc.zora.energy" }
38
+ };
39
+ function parseGlobalId(id) {
40
+ const match = id.match(/^eip155:(\d+):(0x[a-fA-F0-9]{40})#(\d+)$/);
41
+ if (!match) throw new Error(`Invalid global ID format: ${id}
42
+ Expected: eip155:<chainId>:<registry>#<agentId>`);
43
+ return { chainId: Number(match[1]), registry: match[2], agentId: BigInt(match[3]) };
44
+ }
45
+ function getClient(chainId) {
46
+ const chain = SUPPORTED_CHAINS[chainId];
47
+ if (!chain) throw new Error(`Unsupported chain: ${chainId}. Run 'a3stack chains' to see supported chains.`);
48
+ return createPublicClient({ transport: http(chain.rpc) });
49
+ }
50
+ function bold(s) {
51
+ return `\x1B[1m${s}\x1B[0m`;
52
+ }
53
+ function green(s) {
54
+ return `\x1B[32m${s}\x1B[0m`;
55
+ }
56
+ function red(s) {
57
+ return `\x1B[31m${s}\x1B[0m`;
58
+ }
59
+ function dim(s) {
60
+ return `\x1B[2m${s}\x1B[0m`;
61
+ }
62
+ function cyan(s) {
63
+ return `\x1B[36m${s}\x1B[0m`;
64
+ }
65
+ function yellow(s) {
66
+ return `\x1B[33m${s}\x1B[0m`;
67
+ }
68
+ async function verify(globalId) {
69
+ console.log(`
70
+ ${bold("\u{1F50D} Verifying agent identity")}
71
+ `);
72
+ const ref = parseGlobalId(globalId);
73
+ const chainName = SUPPORTED_CHAINS[ref.chainId]?.name ?? `Chain ${ref.chainId}`;
74
+ console.log(` Chain: ${cyan(chainName)} (${ref.chainId})`);
75
+ console.log(` Registry: ${dim(ref.registry)}`);
76
+ console.log(` Agent ID: ${bold("#" + ref.agentId.toString())}
77
+ `);
78
+ const client = getClient(ref.chainId);
79
+ let owner;
80
+ try {
81
+ owner = await client.readContract({
82
+ address: ref.registry,
83
+ abi: REGISTRY_ABI,
84
+ functionName: "ownerOf",
85
+ args: [ref.agentId]
86
+ });
87
+ } catch (e) {
88
+ console.log(` ${red("\u2717")} Agent not found or reverted
89
+ `);
90
+ process.exit(1);
91
+ }
92
+ console.log(` ${green("\u2713")} ${bold("Verified on-chain")}`);
93
+ console.log(` Owner: ${owner}`);
94
+ try {
95
+ const uri = await client.readContract({
96
+ address: ref.registry,
97
+ abi: REGISTRY_ABI,
98
+ functionName: "tokenURI",
99
+ args: [ref.agentId]
100
+ });
101
+ if (uri.startsWith("data:application/json")) {
102
+ const json = JSON.parse(Buffer.from(uri.split(",")[1], "base64").toString());
103
+ if (json.name) console.log(` Name: ${bold(json.name)}`);
104
+ if (json.description) console.log(` Desc: ${json.description}`);
105
+ } else if (uri.startsWith("ipfs://") || uri.startsWith("http")) {
106
+ console.log(` URI: ${dim(uri)}`);
107
+ try {
108
+ const fetchUrl = uri.startsWith("ipfs://") ? `https://gateway.pinata.cloud/ipfs/${uri.slice(7)}` : uri;
109
+ const res = await fetch(fetchUrl, { signal: AbortSignal.timeout(5e3) });
110
+ if (res.ok) {
111
+ const json = await res.json();
112
+ if (json.name) console.log(` Name: ${bold(json.name)}`);
113
+ if (json.description) console.log(` Desc: ${json.description}`);
114
+ if (json.services?.length) {
115
+ console.log(`
116
+ ${bold("Services:")}`);
117
+ for (const s of json.services) {
118
+ console.log(` ${cyan("\u2192")} ${s.name}: ${s.endpoint}`);
119
+ }
120
+ }
121
+ }
122
+ } catch {
123
+ }
124
+ }
125
+ } catch {
126
+ }
127
+ console.log();
128
+ }
129
+ async function lookup(wallet) {
130
+ const addr = getAddress(wallet);
131
+ console.log(`
132
+ ${bold("\u{1F310} Scanning all chains")} for ${dim(addr)}
133
+ `);
134
+ const results = [];
135
+ const promises = Object.entries(SUPPORTED_CHAINS).map(async ([id, chain]) => {
136
+ const chainId = Number(id);
137
+ try {
138
+ const client = createPublicClient({ transport: http(chain.rpc) });
139
+ const balance = await client.readContract({
140
+ address: REGISTRY_ADDRESS,
141
+ abi: REGISTRY_ABI,
142
+ functionName: "balanceOf",
143
+ args: [addr]
144
+ });
145
+ if (balance === 0n) return;
146
+ const logs = await client.getLogs({
147
+ address: REGISTRY_ADDRESS,
148
+ event: TRANSFER_EVENT,
149
+ args: {
150
+ from: "0x0000000000000000000000000000000000000000",
151
+ to: addr
152
+ },
153
+ fromBlock: 0n,
154
+ toBlock: "latest"
155
+ });
156
+ for (const log of logs) {
157
+ const agentId = log.args.tokenId;
158
+ results.push({
159
+ chain: chain.name,
160
+ chainId,
161
+ agentId,
162
+ globalId: `eip155:${chainId}:${REGISTRY_ADDRESS}#${agentId}`
163
+ });
164
+ }
165
+ } catch {
166
+ }
167
+ });
168
+ await Promise.allSettled(promises);
169
+ if (results.length === 0) {
170
+ console.log(` No registrations found.
171
+ `);
172
+ return;
173
+ }
174
+ results.sort((a, b) => a.chainId - b.chainId);
175
+ console.log(` Found ${green(results.length.toString())} registration(s):
176
+ `);
177
+ for (const r of results) {
178
+ console.log(` ${green("\u2713")} ${r.chain.padEnd(15)} ${yellow("#" + r.agentId.toString().padEnd(6))} ${dim(r.globalId)}`);
179
+ }
180
+ console.log();
181
+ }
182
+ async function probe(globalId) {
183
+ console.log(`
184
+ ${bold("\u{1F52C} Probing agent")} ${dim(globalId)}
185
+ `);
186
+ const ref = parseGlobalId(globalId);
187
+ const chainName = SUPPORTED_CHAINS[ref.chainId]?.name ?? `Chain ${ref.chainId}`;
188
+ const client = getClient(ref.chainId);
189
+ let owner;
190
+ try {
191
+ owner = await client.readContract({
192
+ address: ref.registry,
193
+ abi: REGISTRY_ABI,
194
+ functionName: "ownerOf",
195
+ args: [ref.agentId]
196
+ });
197
+ } catch {
198
+ console.log(` ${red("\u2717")} Agent not found
199
+ `);
200
+ process.exit(1);
201
+ }
202
+ console.log(` ${green("\u2713")} Identity verified on ${cyan(chainName)}`);
203
+ console.log(` Owner: ${dim(owner)}`);
204
+ try {
205
+ const uri = await client.readContract({
206
+ address: ref.registry,
207
+ abi: REGISTRY_ABI,
208
+ functionName: "tokenURI",
209
+ args: [ref.agentId]
210
+ });
211
+ let metadata = null;
212
+ if (uri.startsWith("data:application/json")) {
213
+ metadata = JSON.parse(Buffer.from(uri.split(",")[1], "base64").toString());
214
+ } else if (uri.startsWith("ipfs://") || uri.startsWith("http")) {
215
+ const fetchUrl = uri.startsWith("ipfs://") ? `https://gateway.pinata.cloud/ipfs/${uri.slice(7)}` : uri;
216
+ const res = await fetch(fetchUrl, { signal: AbortSignal.timeout(8e3) });
217
+ if (res.ok) metadata = await res.json();
218
+ }
219
+ if (metadata) {
220
+ console.log(` Name: ${bold(metadata.name ?? "unnamed")}`);
221
+ if (metadata.description) console.log(` Desc: ${metadata.description}`);
222
+ if (metadata.active !== void 0) console.log(` Active: ${metadata.active ? green("yes") : red("no")}`);
223
+ if (metadata.x402Support) console.log(` x402: ${green("accepts payments")}`);
224
+ if (metadata.paymentWallet) console.log(` Pay to: ${dim(metadata.paymentWallet)}`);
225
+ if (metadata.services?.length) {
226
+ console.log(`
227
+ ${bold("Endpoints:")}`);
228
+ for (const s of metadata.services) {
229
+ console.log(` ${cyan("\u2192")} ${s.name.padEnd(8)} ${s.endpoint}`);
230
+ if (s.name.toUpperCase() === "MCP" && metadata.x402Support) {
231
+ try {
232
+ const r = await fetch(s.endpoint, {
233
+ method: "POST",
234
+ headers: { "Content-Type": "application/json" },
235
+ body: JSON.stringify({ jsonrpc: "2.0", method: "ping", id: 1 }),
236
+ signal: AbortSignal.timeout(5e3)
237
+ });
238
+ if (r.status === 402) {
239
+ console.log(` ${yellow("$")} Payment required (HTTP 402)`);
240
+ const header = r.headers.get("x-payment-required");
241
+ if (header) {
242
+ try {
243
+ const decoded = JSON.parse(Buffer.from(header, "base64").toString());
244
+ const first = Array.isArray(decoded.accepts) ? decoded.accepts[0] : decoded;
245
+ if (first.maxAmountRequired) console.log(` ${yellow("$")} Amount: ${first.maxAmountRequired} (${first.network ?? "unknown"})`);
246
+ } catch {
247
+ }
248
+ }
249
+ } else if (r.ok) {
250
+ console.log(` ${green("\u2713")} Endpoint reachable (free)`);
251
+ }
252
+ } catch {
253
+ console.log(` ${dim("\u26A0 Endpoint not reachable")}`);
254
+ }
255
+ }
256
+ }
257
+ }
258
+ if (metadata.registrations?.length) {
259
+ console.log(`
260
+ ${bold("Cross-chain IDs:")}`);
261
+ for (const reg of metadata.registrations) {
262
+ console.log(` ${dim("\u2022")} Agent #${reg.agentId} @ ${dim(reg.agentRegistry)}`);
263
+ }
264
+ }
265
+ }
266
+ } catch {
267
+ }
268
+ console.log();
269
+ }
270
+ async function showChains() {
271
+ console.log(`
272
+ ${bold("\u{1F310} Supported ERC-8004 chains")}
273
+ `);
274
+ console.log(` ${dim("Chain".padEnd(16))} ${dim("ID".padEnd(10))} ${dim("Registry")}
275
+ `);
276
+ for (const [id, chain] of Object.entries(SUPPORTED_CHAINS)) {
277
+ console.log(` ${chain.name.padEnd(16)} ${id.padEnd(10)} ${dim(REGISTRY_ADDRESS)}`);
278
+ }
279
+ console.log(`
280
+ ${bold(Object.keys(SUPPORTED_CHAINS).length.toString())} chains \u2014 same registry address on all.
281
+ `);
282
+ }
283
+ async function count(chainIdStr) {
284
+ const chainIds = chainIdStr ? [Number(chainIdStr)] : Object.keys(SUPPORTED_CHAINS).map(Number);
285
+ console.log(`
286
+ ${bold("\u{1F4CA} Agent count")} ${dim("(scanning Transfer events)")}
287
+ `);
288
+ const promises = chainIds.map(async (chainId) => {
289
+ const chain = SUPPORTED_CHAINS[chainId];
290
+ if (!chain) return { chainId, name: `Unknown (${chainId})`, count: -1 };
291
+ try {
292
+ const client = createPublicClient({ transport: http(chain.rpc) });
293
+ const logs = await client.getLogs({
294
+ address: REGISTRY_ADDRESS,
295
+ event: TRANSFER_EVENT,
296
+ args: { from: "0x0000000000000000000000000000000000000000" },
297
+ fromBlock: 0n,
298
+ toBlock: "latest"
299
+ });
300
+ return { chainId, name: chain.name, count: logs.length };
301
+ } catch {
302
+ return { chainId, name: chain.name, count: -1 };
303
+ }
304
+ });
305
+ const results = await Promise.all(promises);
306
+ results.sort((a, b) => b.count - a.count);
307
+ for (const r of results) {
308
+ if (r.count >= 0) {
309
+ console.log(` ${r.name.padEnd(16)} ${yellow(r.count.toString().padStart(6))} agents`);
310
+ } else {
311
+ console.log(` ${r.name.padEnd(16)} ${dim(" error")}`);
312
+ }
313
+ }
314
+ console.log();
315
+ }
316
+ function scaffoldInit() {
317
+ console.log(`
318
+ ${bold("\u{1F680} Scaffold a new A3Stack agent")}
319
+ `);
320
+ console.log(`${dim("Coming in v0.2.0 \u2014 for now, install the packages directly:")}
321
+ `);
322
+ console.log(` ${cyan("npm install @a3stack/core viem")}
323
+ `);
324
+ console.log(` Then in your code:
325
+ `);
326
+ console.log(` ${dim('import { A3Stack } from "@a3stack/core";')}`);
327
+ console.log(` ${dim("")}`);
328
+ console.log(` ${dim("const agent = new A3Stack({")}`);
329
+ console.log(` ${dim(" privateKey: process.env.PRIVATE_KEY,")}`);
330
+ console.log(` ${dim(" chainId: 8453, // Base")}`);
331
+ console.log(` ${dim("});")}`);
332
+ console.log(` ${dim("")}`);
333
+ console.log(` ${dim('await agent.register({ name: "my-agent" });')}`);
334
+ console.log(` ${dim('console.log("Registered:", agent.globalId);')}
335
+ `);
336
+ console.log(` Docs: ${cyan("https://a3stack.arcabot.ai")}
337
+ `);
338
+ }
339
+ var HELP = `
340
+ ${bold("a3stack")} \u2014 identity, payments, and data for AI agents
341
+
342
+ ${bold("Commands:")}
343
+ ${cyan("verify")} <globalId> Verify an agent's on-chain identity
344
+ ${cyan("lookup")} <wallet> Find all chain registrations for a wallet
345
+ ${cyan("probe")} <globalId> Discover an agent's capabilities & endpoints
346
+ ${cyan("chains")} List supported ERC-8004 chains
347
+ ${cyan("count")} [chainId] Count registered agents (all chains or one)
348
+ ${cyan("init")} Scaffold a new agent project
349
+
350
+ ${bold("Examples:")}
351
+ npx a3stack verify "eip155:8453:${REGISTRY_ADDRESS}#2376"
352
+ npx a3stack lookup 0xYOUR_WALLET_ADDRESS
353
+ npx a3stack probe "eip155:8453:${REGISTRY_ADDRESS}#2376"
354
+ npx a3stack count 8453
355
+
356
+ ${dim(`v0.1.0 \u2022 https://a3stack.arcabot.ai \u2022 github.com/arcabotai/a3stack`)}
357
+ `;
358
+ var [cmd, ...args] = process.argv.slice(2);
359
+ async function main() {
360
+ if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
361
+ console.log(HELP);
362
+ return;
363
+ }
364
+ if (cmd === "--version" || cmd === "-v") {
365
+ console.log("a3stack v0.1.0");
366
+ return;
367
+ }
368
+ switch (cmd) {
369
+ case "verify":
370
+ return verify(args[0]);
371
+ case "lookup":
372
+ return lookup(args[0]);
373
+ case "probe":
374
+ return probe(args[0]);
375
+ case "chains":
376
+ return showChains();
377
+ case "count":
378
+ return count(args[0]);
379
+ case "init":
380
+ return scaffoldInit();
381
+ default:
382
+ console.error(`${red("Unknown command:")} ${cmd}
383
+ `);
384
+ console.log(HELP);
385
+ process.exit(1);
386
+ }
387
+ }
388
+ main().catch((err) => {
389
+ console.error(`${red("Error:")} ${err.message}`);
390
+ process.exit(1);
391
+ });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "a3stack",
3
+ "version": "0.1.0",
4
+ "description": "A3Stack CLI — identity, payments, and data for AI agents",
5
+ "type": "module",
6
+ "bin": {
7
+ "a3stack": "./bin/a3stack.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "dist/"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/cli.ts --format esm --outDir dist",
15
+ "postbuild": "cp dist/cli.js bin/a3stack.js"
16
+ },
17
+ "dependencies": {
18
+ "viem": "^2.39.3"
19
+ },
20
+ "devDependencies": {
21
+ "tsup": "^8.0.0",
22
+ "typescript": "^5.4.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "keywords": [
28
+ "ai",
29
+ "agent",
30
+ "erc-8004",
31
+ "identity",
32
+ "x402",
33
+ "payments",
34
+ "mcp"
35
+ ],
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/arcabotai/a3stack"
40
+ },
41
+ "homepage": "https://a3stack.arcabot.ai"
42
+ }