@rickydata/agent0-mcp 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 (70) hide show
  1. package/.env.example +17 -0
  2. package/Dockerfile +25 -0
  3. package/README.md +85 -0
  4. package/dist/auth/sdk-client.d.ts +47 -0
  5. package/dist/auth/sdk-client.js +142 -0
  6. package/dist/auth/sdk-client.js.map +1 -0
  7. package/dist/auth/token.d.ts +5 -0
  8. package/dist/auth/token.js +6 -0
  9. package/dist/auth/token.js.map +1 -0
  10. package/dist/auth/wallet-derivation.d.ts +26 -0
  11. package/dist/auth/wallet-derivation.js +76 -0
  12. package/dist/auth/wallet-derivation.js.map +1 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +140 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/tools/a2a.d.ts +3 -0
  17. package/dist/tools/a2a.js +170 -0
  18. package/dist/tools/a2a.js.map +1 -0
  19. package/dist/tools/discovery.d.ts +3 -0
  20. package/dist/tools/discovery.js +465 -0
  21. package/dist/tools/discovery.js.map +1 -0
  22. package/dist/tools/index.d.ts +3 -0
  23. package/dist/tools/index.js +38 -0
  24. package/dist/tools/index.js.map +1 -0
  25. package/dist/tools/payments.d.ts +3 -0
  26. package/dist/tools/payments.js +124 -0
  27. package/dist/tools/payments.js.map +1 -0
  28. package/dist/tools/registration.d.ts +3 -0
  29. package/dist/tools/registration.js +324 -0
  30. package/dist/tools/registration.js.map +1 -0
  31. package/dist/tools/reputation.d.ts +3 -0
  32. package/dist/tools/reputation.js +147 -0
  33. package/dist/tools/reputation.js.map +1 -0
  34. package/dist/utils/chains.d.ts +10 -0
  35. package/dist/utils/chains.js +33 -0
  36. package/dist/utils/chains.js.map +1 -0
  37. package/dist/utils/trust-labels.d.ts +10 -0
  38. package/dist/utils/trust-labels.js +48 -0
  39. package/dist/utils/trust-labels.js.map +1 -0
  40. package/dist/utils/validation.d.ts +12 -0
  41. package/dist/utils/validation.js +19 -0
  42. package/dist/utils/validation.js.map +1 -0
  43. package/package.json +32 -0
  44. package/src/auth/sdk-client.ts +171 -0
  45. package/src/auth/token.ts +19 -0
  46. package/src/auth/wallet-derivation.ts +91 -0
  47. package/src/index.ts +184 -0
  48. package/src/tools/a2a.ts +205 -0
  49. package/src/tools/discovery.ts +517 -0
  50. package/src/tools/index.ts +45 -0
  51. package/src/tools/payments.ts +146 -0
  52. package/src/tools/registration.ts +389 -0
  53. package/src/tools/reputation.ts +183 -0
  54. package/src/utils/chains.ts +42 -0
  55. package/src/utils/trust-labels.ts +53 -0
  56. package/src/utils/validation.ts +20 -0
  57. package/tests/a2a.test.ts +234 -0
  58. package/tests/chains.test.ts +57 -0
  59. package/tests/discovery.test.ts +455 -0
  60. package/tests/e2e.test.ts +234 -0
  61. package/tests/payments.test.ts +148 -0
  62. package/tests/registration.test.ts +313 -0
  63. package/tests/reputation.test.ts +231 -0
  64. package/tests/sdk-client.test.ts +143 -0
  65. package/tests/tool-router.test.ts +28 -0
  66. package/tests/trust-labels.test.ts +229 -0
  67. package/tests/validation.test.ts +132 -0
  68. package/tests/wallet-derivation.test.ts +109 -0
  69. package/tsconfig.json +8 -0
  70. package/vitest.config.ts +8 -0
@@ -0,0 +1,389 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import {
3
+ getAuthenticatedSDK,
4
+ hasAuthentication,
5
+ getAuthStatus,
6
+ setDerivedKey,
7
+ setChainId,
8
+ } from "../auth/sdk-client.js";
9
+ import {
10
+ getDerivationMessage,
11
+ deriveWalletFromSignature,
12
+ verifyDerivationSignature,
13
+ } from "../auth/wallet-derivation.js";
14
+ import { getChainName } from "../utils/chains.js";
15
+
16
+ export const registrationTools: Tool[] = [
17
+ {
18
+ name: "configure_wallet",
19
+ description:
20
+ "Configure wallet for ERC-8004 write operations (register, feedback, etc.). " +
21
+ "Provide either a private key directly OR a wallet signature for key derivation. " +
22
+ "Also sets the target chain. Must be called before any write operation.",
23
+ inputSchema: {
24
+ type: "object" as const,
25
+ properties: {
26
+ privateKey: {
27
+ type: "string",
28
+ description:
29
+ "Hex private key (0x-prefixed). Use this if you have a dedicated agent key.",
30
+ },
31
+ signature: {
32
+ type: "string",
33
+ description:
34
+ "Wallet signature of the derivation message (from personal_sign). " +
35
+ "Use get_derivation_message first, then sign it with your wallet.",
36
+ },
37
+ signerAddress: {
38
+ type: "string",
39
+ description:
40
+ "The wallet address that produced the signature (for verification).",
41
+ },
42
+ chainId: {
43
+ type: "number",
44
+ description:
45
+ "Target chain ID (default: 11155111 Sepolia). Use 1 for Ethereum Mainnet, 8453 for Base.",
46
+ },
47
+ },
48
+ },
49
+ },
50
+ {
51
+ name: "get_derivation_message",
52
+ description:
53
+ "Get the message that must be signed with your wallet to derive an ERC-8004 agent key. " +
54
+ "Sign this message with personal_sign, then pass the signature to configure_wallet.",
55
+ inputSchema: {
56
+ type: "object" as const,
57
+ properties: {},
58
+ },
59
+ },
60
+ {
61
+ name: "get_auth_status",
62
+ description:
63
+ "Check if a wallet is configured for write operations. " +
64
+ "Shows key source, chain ID, and whether the SDK is in read-only mode.",
65
+ inputSchema: {
66
+ type: "object" as const,
67
+ properties: {},
68
+ },
69
+ },
70
+ {
71
+ name: "register_agent",
72
+ description:
73
+ "Register a new AI agent on-chain via ERC-8004. Requires configured wallet. " +
74
+ "Creates the agent with name, description, endpoints, and capabilities. " +
75
+ "Returns the on-chain agent ID and URI.",
76
+ inputSchema: {
77
+ type: "object" as const,
78
+ properties: {
79
+ name: {
80
+ type: "string",
81
+ description: "Agent name",
82
+ },
83
+ description: {
84
+ type: "string",
85
+ description: "Agent description",
86
+ },
87
+ image: {
88
+ type: "string",
89
+ description: "Agent image URL (optional)",
90
+ },
91
+ mcpEndpoint: {
92
+ type: "string",
93
+ description: "MCP server endpoint URL (optional)",
94
+ },
95
+ a2aEndpoint: {
96
+ type: "string",
97
+ description: "A2A agent card URL (optional)",
98
+ },
99
+ active: {
100
+ type: "boolean",
101
+ description: "Set agent as active (default: true)",
102
+ },
103
+ x402support: {
104
+ type: "boolean",
105
+ description: "Agent supports x402 payments (default: false)",
106
+ },
107
+ trustReputation: {
108
+ type: "boolean",
109
+ description: "Enable reputation trust model (default: true)",
110
+ },
111
+ trustCryptoEconomic: {
112
+ type: "boolean",
113
+ description: "Enable crypto-economic trust model (default: false)",
114
+ },
115
+ trustTEE: {
116
+ type: "boolean",
117
+ description: "Enable TEE attestation trust model (default: false)",
118
+ },
119
+ metadata: {
120
+ type: "object",
121
+ description: "Additional metadata key-value pairs",
122
+ },
123
+ registrationMethod: {
124
+ type: "string",
125
+ enum: ["ipfs", "onchain"],
126
+ description:
127
+ "Registration method: 'ipfs' (default, stores on IPFS) or 'onchain' (data URI, higher gas)",
128
+ },
129
+ },
130
+ required: ["name", "description"],
131
+ },
132
+ },
133
+ {
134
+ name: "update_agent",
135
+ description:
136
+ "Update an existing agent's properties (description, endpoints, metadata). " +
137
+ "Requires configured wallet and agent ownership.",
138
+ inputSchema: {
139
+ type: "object" as const,
140
+ properties: {
141
+ agentId: {
142
+ type: "string",
143
+ description: "Agent ID in chainId:tokenId format",
144
+ },
145
+ description: {
146
+ type: "string",
147
+ description: "Updated description",
148
+ },
149
+ image: {
150
+ type: "string",
151
+ description: "Updated image URL",
152
+ },
153
+ mcpEndpoint: {
154
+ type: "string",
155
+ description: "Updated MCP endpoint",
156
+ },
157
+ a2aEndpoint: {
158
+ type: "string",
159
+ description: "Updated A2A endpoint",
160
+ },
161
+ active: {
162
+ type: "boolean",
163
+ description: "Set agent active/inactive",
164
+ },
165
+ metadata: {
166
+ type: "object",
167
+ description: "Metadata to merge (key-value pairs)",
168
+ },
169
+ },
170
+ required: ["agentId"],
171
+ },
172
+ },
173
+ ];
174
+
175
+ // ============================================================================
176
+ // HANDLERS
177
+ // ============================================================================
178
+
179
+ function requireAuth(): { sdk: ReturnType<typeof getAuthenticatedSDK>; error?: string } {
180
+ if (!hasAuthentication()) {
181
+ return {
182
+ sdk: null,
183
+ error:
184
+ "No wallet configured. Call configure_wallet first with a private key or signature.",
185
+ };
186
+ }
187
+ const sdk = getAuthenticatedSDK();
188
+ if (!sdk) {
189
+ return { sdk: null, error: "Failed to initialize authenticated SDK." };
190
+ }
191
+ return { sdk };
192
+ }
193
+
194
+ async function handleConfigureWallet(
195
+ args: Record<string, unknown>,
196
+ ): Promise<unknown> {
197
+ // Set chain if provided
198
+ if (args.chainId) {
199
+ setChainId(args.chainId as number);
200
+ }
201
+
202
+ if (args.privateKey) {
203
+ // Direct private key
204
+ const key = args.privateKey as string;
205
+ if (!key.startsWith("0x") || key.length !== 66) {
206
+ return { error: "Invalid private key format. Must be 0x-prefixed 32-byte hex." };
207
+ }
208
+ setDerivedKey(key);
209
+ const status = getAuthStatus();
210
+ return {
211
+ success: true,
212
+ method: "direct_key",
213
+ chainId: status.chainId,
214
+ chain: getChainName(status.chainId),
215
+ };
216
+ }
217
+
218
+ if (args.signature) {
219
+ const signature = args.signature as string;
220
+ const signerAddress = args.signerAddress as string | undefined;
221
+
222
+ // Verify signature if address provided
223
+ if (signerAddress) {
224
+ const valid = verifyDerivationSignature(signature, signerAddress);
225
+ if (!valid) {
226
+ return {
227
+ error: `Signature verification failed. The signature was not produced by ${signerAddress}.`,
228
+ };
229
+ }
230
+ }
231
+
232
+ // Derive key from signature
233
+ const derived = deriveWalletFromSignature(signature);
234
+ setDerivedKey(derived.privateKey);
235
+
236
+ const status = getAuthStatus();
237
+ return {
238
+ success: true,
239
+ method: "derived_from_signature",
240
+ derivedAddress: derived.address,
241
+ chainId: status.chainId,
242
+ chain: getChainName(status.chainId),
243
+ note: "Derived address is your ERC-8004 agent identity. It is deterministic from your wallet signature.",
244
+ };
245
+ }
246
+
247
+ // No key or signature — just set chain
248
+ if (args.chainId) {
249
+ return {
250
+ success: true,
251
+ method: "chain_only",
252
+ chainId: args.chainId,
253
+ chain: getChainName(args.chainId as number),
254
+ note: "Chain updated. No signing key configured — read-only mode.",
255
+ };
256
+ }
257
+
258
+ return {
259
+ error:
260
+ "Provide either privateKey or signature. Use get_derivation_message to get the message to sign.",
261
+ };
262
+ }
263
+
264
+ async function handleRegisterAgent(
265
+ args: Record<string, unknown>,
266
+ ): Promise<unknown> {
267
+ const { sdk, error } = requireAuth();
268
+ if (error || !sdk) return { error };
269
+
270
+ const agent = sdk.createAgent(
271
+ args.name as string,
272
+ args.description as string,
273
+ (args.image as string) ?? undefined,
274
+ );
275
+
276
+ // Configure endpoints
277
+ if (args.mcpEndpoint) {
278
+ await agent.setMCP(args.mcpEndpoint as string);
279
+ }
280
+ if (args.a2aEndpoint) {
281
+ await agent.setA2A(args.a2aEndpoint as string);
282
+ }
283
+
284
+ // Trust models
285
+ agent.setTrust(
286
+ (args.trustReputation as boolean) ?? true,
287
+ (args.trustCryptoEconomic as boolean) ?? false,
288
+ (args.trustTEE as boolean) ?? false,
289
+ );
290
+
291
+ // Active status
292
+ agent.setActive((args.active as boolean) ?? true);
293
+
294
+ // Metadata
295
+ if (args.metadata) {
296
+ agent.setMetadata(args.metadata as Record<string, unknown>);
297
+ }
298
+
299
+ // Register
300
+ const method = (args.registrationMethod as string) ?? "ipfs";
301
+ const tx =
302
+ method === "onchain"
303
+ ? await agent.registerOnChain()
304
+ : await agent.registerIPFS();
305
+ const mined = await tx.waitConfirmed({ timeoutMs: 180_000 });
306
+ const regFile = mined.result;
307
+
308
+ return {
309
+ success: true,
310
+ agentId: regFile.agentId,
311
+ agentURI: regFile.agentURI,
312
+ txHash: mined.receipt.transactionHash,
313
+ method,
314
+ chain: getChainName(await sdk.chainId()),
315
+ };
316
+ }
317
+
318
+ async function handleUpdateAgent(
319
+ args: Record<string, unknown>,
320
+ ): Promise<unknown> {
321
+ const { sdk, error } = requireAuth();
322
+ if (error || !sdk) return { error };
323
+
324
+ const agentId = args.agentId as string;
325
+ if (!agentId) return { error: "agentId is required" };
326
+
327
+ const agent = await sdk.loadAgent(agentId);
328
+
329
+ if (args.description !== undefined) {
330
+ agent.updateInfo(undefined, args.description as string, undefined);
331
+ }
332
+ if (args.image !== undefined) {
333
+ agent.updateInfo(undefined, undefined, args.image as string);
334
+ }
335
+ if (args.mcpEndpoint) {
336
+ await agent.setMCP(args.mcpEndpoint as string);
337
+ }
338
+ if (args.a2aEndpoint) {
339
+ await agent.setA2A(args.a2aEndpoint as string);
340
+ }
341
+ if (args.active !== undefined) {
342
+ agent.setActive(args.active as boolean);
343
+ }
344
+ if (args.metadata) {
345
+ agent.setMetadata(args.metadata as Record<string, unknown>);
346
+ }
347
+
348
+ // Re-register to update
349
+ const tx = await agent.registerIPFS();
350
+ const mined = await tx.waitConfirmed({ timeoutMs: 180_000 });
351
+ const regFile = mined.result;
352
+
353
+ return {
354
+ success: true,
355
+ agentId,
356
+ agentURI: regFile.agentURI,
357
+ txHash: mined.receipt.transactionHash,
358
+ chain: getChainName(await sdk.chainId()),
359
+ };
360
+ }
361
+
362
+ // ============================================================================
363
+ // DISPATCH
364
+ // ============================================================================
365
+
366
+ export async function handleRegistrationTool(
367
+ name: string,
368
+ args: Record<string, unknown>,
369
+ ): Promise<unknown> {
370
+ switch (name) {
371
+ case "configure_wallet":
372
+ return handleConfigureWallet(args);
373
+ case "get_derivation_message":
374
+ return {
375
+ message: getDerivationMessage(),
376
+ instructions:
377
+ "Sign this message with personal_sign using your wallet, " +
378
+ "then pass the signature to configure_wallet.",
379
+ };
380
+ case "get_auth_status":
381
+ return getAuthStatus();
382
+ case "register_agent":
383
+ return handleRegisterAgent(args);
384
+ case "update_agent":
385
+ return handleUpdateAgent(args);
386
+ default:
387
+ return { error: `Unknown registration tool: ${name}` };
388
+ }
389
+ }
@@ -0,0 +1,183 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { FeedbackFileInput } from "agent0-sdk";
3
+ import {
4
+ getAuthenticatedSDK,
5
+ hasAuthentication,
6
+ } from "../auth/sdk-client.js";
7
+ import { getChainName } from "../utils/chains.js";
8
+
9
+ export const reputationTools: Tool[] = [
10
+ {
11
+ name: "give_feedback",
12
+ description:
13
+ "Submit on-chain feedback/review for an ERC-8004 agent. Requires configured wallet. " +
14
+ "Creates a permanent, verifiable review with a score (0-100), tags, and optional text.",
15
+ inputSchema: {
16
+ type: "object" as const,
17
+ properties: {
18
+ agentId: {
19
+ type: "string",
20
+ description: "Agent ID in chainId:tokenId format (e.g. '11155111:42')",
21
+ },
22
+ value: {
23
+ type: "number",
24
+ description: "Feedback score from 0 (worst) to 100 (best)",
25
+ },
26
+ tag1: {
27
+ type: "string",
28
+ description: "Primary tag (e.g. 'quality', 'reliability', 'enterprise')",
29
+ },
30
+ tag2: {
31
+ type: "string",
32
+ description: "Secondary tag (optional)",
33
+ },
34
+ endpoint: {
35
+ type: "string",
36
+ description: "Specific endpoint being reviewed (optional)",
37
+ },
38
+ text: {
39
+ type: "string",
40
+ description: "Free-text review content (stored off-chain via IPFS)",
41
+ },
42
+ mcpTool: {
43
+ type: "string",
44
+ description: "Specific MCP tool being reviewed (optional)",
45
+ },
46
+ a2aSkills: {
47
+ type: "array",
48
+ items: { type: "string" },
49
+ description: "A2A skills being reviewed (optional)",
50
+ },
51
+ },
52
+ required: ["agentId", "value"],
53
+ },
54
+ },
55
+ {
56
+ name: "revoke_feedback",
57
+ description:
58
+ "Revoke previously submitted feedback. Only the original reviewer can revoke. " +
59
+ "Requires configured wallet.",
60
+ inputSchema: {
61
+ type: "object" as const,
62
+ properties: {
63
+ agentId: {
64
+ type: "string",
65
+ description: "Agent ID in chainId:tokenId format",
66
+ },
67
+ feedbackIndex: {
68
+ type: "number",
69
+ description: "The feedback index to revoke (from the reviewer's feedback list)",
70
+ },
71
+ },
72
+ required: ["agentId", "feedbackIndex"],
73
+ },
74
+ },
75
+ ];
76
+
77
+ // ============================================================================
78
+ // HANDLERS
79
+ // ============================================================================
80
+
81
+ function requireAuth() {
82
+ if (!hasAuthentication()) {
83
+ return {
84
+ sdk: null as ReturnType<typeof getAuthenticatedSDK>,
85
+ error: "No wallet configured. Call configure_wallet first.",
86
+ };
87
+ }
88
+ const sdk = getAuthenticatedSDK();
89
+ if (!sdk) {
90
+ return { sdk: null as ReturnType<typeof getAuthenticatedSDK>, error: "Failed to initialize authenticated SDK." };
91
+ }
92
+ return { sdk, error: undefined };
93
+ }
94
+
95
+ async function handleGiveFeedback(
96
+ args: Record<string, unknown>,
97
+ ): Promise<unknown> {
98
+ const { sdk, error } = requireAuth();
99
+ if (error || !sdk) return { error };
100
+
101
+ const agentId = args.agentId as string;
102
+ const value = args.value as number;
103
+
104
+ if (value < 0 || value > 100) {
105
+ return { error: "Feedback value must be between 0 and 100" };
106
+ }
107
+
108
+ // Build off-chain feedback file if rich fields provided
109
+ let feedbackFile: FeedbackFileInput | undefined;
110
+ if (args.text || args.mcpTool || args.a2aSkills) {
111
+ feedbackFile = sdk.prepareFeedbackFile({
112
+ text: args.text as string | undefined,
113
+ mcpTool: args.mcpTool as string | undefined,
114
+ a2aSkills: args.a2aSkills as string[] | undefined,
115
+ });
116
+ }
117
+
118
+ const tx = await sdk.giveFeedback(
119
+ agentId,
120
+ value,
121
+ (args.tag1 as string) ?? undefined,
122
+ (args.tag2 as string) ?? undefined,
123
+ (args.endpoint as string) ?? undefined,
124
+ feedbackFile,
125
+ );
126
+
127
+ const mined = await tx.waitConfirmed({ timeoutMs: 120_000 });
128
+
129
+ const parts = agentId.split(":");
130
+ const chainId = parts.length === 2 ? parseInt(parts[0], 10) : await sdk.chainId();
131
+
132
+ return {
133
+ success: true,
134
+ agentId,
135
+ value,
136
+ tags: [args.tag1, args.tag2].filter(Boolean),
137
+ txHash: mined.receipt.transactionHash,
138
+ chain: getChainName(chainId),
139
+ };
140
+ }
141
+
142
+ async function handleRevokeFeedback(
143
+ args: Record<string, unknown>,
144
+ ): Promise<unknown> {
145
+ const { sdk, error } = requireAuth();
146
+ if (error || !sdk) return { error };
147
+
148
+ const agentId = args.agentId as string;
149
+ const feedbackIndex = args.feedbackIndex as number;
150
+
151
+ const tx = await sdk.revokeFeedback(agentId, feedbackIndex);
152
+ const mined = await tx.waitConfirmed({ timeoutMs: 120_000 });
153
+
154
+ const parts = agentId.split(":");
155
+ const chainId = parts.length === 2 ? parseInt(parts[0], 10) : await sdk.chainId();
156
+
157
+ return {
158
+ success: true,
159
+ agentId,
160
+ feedbackIndex,
161
+ txHash: mined.receipt.transactionHash,
162
+ isRevoked: true,
163
+ chain: getChainName(chainId),
164
+ };
165
+ }
166
+
167
+ // ============================================================================
168
+ // DISPATCH
169
+ // ============================================================================
170
+
171
+ export async function handleReputationTool(
172
+ name: string,
173
+ args: Record<string, unknown>,
174
+ ): Promise<unknown> {
175
+ switch (name) {
176
+ case "give_feedback":
177
+ return handleGiveFeedback(args);
178
+ case "revoke_feedback":
179
+ return handleRevokeFeedback(args);
180
+ default:
181
+ return { error: `Unknown reputation tool: ${name}` };
182
+ }
183
+ }
@@ -0,0 +1,42 @@
1
+ /** Known chain configurations for ERC-8004 agent registry lookups. */
2
+ export interface ChainConfig {
3
+ chainId: number;
4
+ name: string;
5
+ rpcUrl: string;
6
+ explorerUrl: string;
7
+ }
8
+
9
+ export const CHAINS: Record<number, ChainConfig> = {
10
+ 1: {
11
+ chainId: 1,
12
+ name: "Ethereum Mainnet",
13
+ rpcUrl: "https://eth.llamarpc.com",
14
+ explorerUrl: "https://etherscan.io",
15
+ },
16
+ 8453: {
17
+ chainId: 8453,
18
+ name: "Base",
19
+ rpcUrl: "https://mainnet.base.org",
20
+ explorerUrl: "https://basescan.org",
21
+ },
22
+ 42161: {
23
+ chainId: 42161,
24
+ name: "Arbitrum One",
25
+ rpcUrl: "https://arb1.arbitrum.io/rpc",
26
+ explorerUrl: "https://arbiscan.io",
27
+ },
28
+ 10: {
29
+ chainId: 10,
30
+ name: "Optimism",
31
+ rpcUrl: "https://mainnet.optimism.io",
32
+ explorerUrl: "https://optimistic.etherscan.io",
33
+ },
34
+ };
35
+
36
+ export function getChain(chainId: number): ChainConfig | undefined {
37
+ return CHAINS[chainId];
38
+ }
39
+
40
+ export function getChainName(chainId: number): string {
41
+ return CHAINS[chainId]?.name ?? `Chain ${chainId}`;
42
+ }
@@ -0,0 +1,53 @@
1
+ export interface TrustLabel {
2
+ emoji: string;
3
+ label: string;
4
+ display: string;
5
+ }
6
+
7
+ /**
8
+ * Compute a human-readable trust label from review count and average score.
9
+ * First-match-wins rules (order matters).
10
+ */
11
+ export function computeTrustLabel(count: number, avg: number): TrustLabel {
12
+ if (count >= 5 && avg < -50)
13
+ return {
14
+ emoji: "\u{1F534}",
15
+ label: "Untrusted",
16
+ display: `\u{1F534} Untrusted -- ${avg}/100 (${count} reviews)`,
17
+ };
18
+ if (avg < 0)
19
+ return {
20
+ emoji: "\u{1F7E0}",
21
+ label: "Caution",
22
+ display: `\u{1F7E0} Caution -- ${avg}/100 (${count} reviews)`,
23
+ };
24
+ if (count >= 20 && avg >= 80)
25
+ return {
26
+ emoji: "\u2B50",
27
+ label: "Highly Trusted",
28
+ display: `\u2B50 Highly Trusted -- ${avg}/100 (${count} reviews)`,
29
+ };
30
+ if (count >= 10 && avg >= 70)
31
+ return {
32
+ emoji: "\u{1F7E2}",
33
+ label: "Trusted",
34
+ display: `\u{1F7E2} Trusted -- ${avg}/100 (${count} reviews)`,
35
+ };
36
+ if (count >= 5 && avg >= 50)
37
+ return {
38
+ emoji: "\u{1F7E2}",
39
+ label: "Established",
40
+ display: `\u{1F7E2} Established -- ${avg}/100 (${count} reviews)`,
41
+ };
42
+ if (count > 0)
43
+ return {
44
+ emoji: "\u{1F535}",
45
+ label: "Emerging",
46
+ display: `\u{1F535} Emerging -- ${avg}/100 (${count} reviews)`,
47
+ };
48
+ return {
49
+ emoji: "\u26AA",
50
+ label: "No Data",
51
+ display: "\u26AA No Data -- 0/100 (0 reviews)",
52
+ };
53
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Validate an Ethereum address (basic checksum-agnostic check).
3
+ */
4
+ export function isValidAddress(address: string): boolean {
5
+ return /^0x[0-9a-fA-F]{40}$/.test(address);
6
+ }
7
+
8
+ /**
9
+ * Validate a bytes32 hex string (e.g., agent ID).
10
+ */
11
+ export function isValidBytes32(hex: string): boolean {
12
+ return /^0x[0-9a-fA-F]{64}$/.test(hex);
13
+ }
14
+
15
+ /**
16
+ * Validate a positive integer chain ID.
17
+ */
18
+ export function isValidChainId(chainId: number): boolean {
19
+ return Number.isInteger(chainId) && chainId > 0;
20
+ }