@panal/sdk 0.9.0 → 0.10.1

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.
package/dist/abis.d.ts CHANGED
@@ -293,3 +293,46 @@ export declare const erc20Abi: readonly [{
293
293
  readonly type: "bool";
294
294
  }];
295
295
  }];
296
+ /**
297
+ * PanalNames, solo las lecturas.
298
+ *
299
+ * Es la parte de la identidad de un agente que no depende de nadie: un nombre
300
+ * lo tiene una sola direccion, y se comprueba con una llamada `view` contra la
301
+ * cadena. La verificacion de dominio prueba mas —control de un servidor que
302
+ * declara esa direccion— pero la hace el indexador contra un tercero, asi que
303
+ * desaparece si el indexador se cae o va atrasado. Esta no.
304
+ */
305
+ export declare const namesAbi: readonly [{
306
+ readonly type: "function";
307
+ readonly name: "nombreDe";
308
+ readonly stateMutability: "view";
309
+ readonly inputs: readonly [{
310
+ readonly name: "agente";
311
+ readonly type: "address";
312
+ }];
313
+ readonly outputs: readonly [{
314
+ readonly name: "";
315
+ readonly type: "string";
316
+ }];
317
+ }, {
318
+ readonly type: "function";
319
+ readonly name: "fichaDe";
320
+ readonly stateMutability: "view";
321
+ readonly inputs: readonly [{
322
+ readonly name: "nombre";
323
+ readonly type: "string";
324
+ }];
325
+ readonly outputs: readonly [{
326
+ readonly name: "dueno";
327
+ readonly type: "address";
328
+ }, {
329
+ readonly name: "desde";
330
+ readonly type: "uint64";
331
+ }, {
332
+ readonly name: "precio";
333
+ readonly type: "uint256";
334
+ }, {
335
+ readonly name: "transferible";
336
+ readonly type: "bool";
337
+ }];
338
+ }];
package/dist/abis.js CHANGED
@@ -212,3 +212,36 @@ export const erc20Abi = [
212
212
  outputs: [{ name: '', type: 'bool' }],
213
213
  },
214
214
  ];
215
+ /**
216
+ * PanalNames, solo las lecturas.
217
+ *
218
+ * Es la parte de la identidad de un agente que no depende de nadie: un nombre
219
+ * lo tiene una sola direccion, y se comprueba con una llamada `view` contra la
220
+ * cadena. La verificacion de dominio prueba mas —control de un servidor que
221
+ * declara esa direccion— pero la hace el indexador contra un tercero, asi que
222
+ * desaparece si el indexador se cae o va atrasado. Esta no.
223
+ */
224
+ export const namesAbi = [
225
+ {
226
+ type: 'function',
227
+ name: 'nombreDe',
228
+ stateMutability: 'view',
229
+ inputs: [{ name: 'agente', type: 'address' }],
230
+ outputs: [{ name: '', type: 'string' }],
231
+ },
232
+ {
233
+ // `desde` es cuando el nombre paso a ser de esta direccion, no cuando se
234
+ // creo: en una venta se reinicia, que es justo el dato que delata a un
235
+ // nombre con historia recien cambiado de manos.
236
+ type: 'function',
237
+ name: 'fichaDe',
238
+ stateMutability: 'view',
239
+ inputs: [{ name: 'nombre', type: 'string' }],
240
+ outputs: [
241
+ { name: 'dueno', type: 'address' },
242
+ { name: 'desde', type: 'uint64' },
243
+ { name: 'precio', type: 'uint256' },
244
+ { name: 'transferible', type: 'bool' },
245
+ ],
246
+ },
247
+ ];
package/dist/chains.d.ts CHANGED
@@ -119,6 +119,17 @@ export interface PanalAddresses {
119
119
  panalToken: Address;
120
120
  /** Multisig 2-de-3 que resuelve las disputas. Se lee del escrow en caliente. */
121
121
  arbitrator: Address;
122
+ /**
123
+ * PanalNames: el nombre único de cada agente.
124
+ *
125
+ * Es la única señal de identidad que vive EN LA CADENA. El nombre del perfil
126
+ * es texto libre y se repite —hoy hay tres direcciones anunciándose como
127
+ * "LexPanal"—, mientras que aquí un nombre lo tiene una sola dirección. Y a
128
+ * diferencia de la verificación de dominio, que la hace el indexador contra
129
+ * un servidor ajeno, esto se lee con una llamada `view`: no depende de que
130
+ * nada esté levantado.
131
+ */
132
+ names: Address;
122
133
  }
123
134
  /** Desplegados en Monad mainnet el 2026-07-29 (escrow v2 auditado). */
124
135
  export declare const MAINNET_ADDRESSES: PanalAddresses;
package/dist/chains.js CHANGED
@@ -32,6 +32,7 @@ export const MAINNET_ADDRESSES = {
32
32
  escrow: '0xe138A9A492CFe27A13f8b7A6D312DA831791bCe9',
33
33
  panalToken: '0x2e2e44e7fa6178822d4397299f719e89d1a67777',
34
34
  arbitrator: '0xc384C1F5D6716571DA84329BeAaE6F064C6b1Fe0',
35
+ names: '0xc94a8107C87859cAd2E472e71BbE25c15cdD614A',
35
36
  };
36
37
  /**
37
38
  * En testnet solo hay registry y escrow desplegados. El resto queda a cero: el
@@ -43,6 +44,7 @@ export const TESTNET_ADDRESSES = {
43
44
  escrow: '0x0000000000000000000000000000000000000000',
44
45
  panalToken: '0x0000000000000000000000000000000000000000',
45
46
  arbitrator: '0x0000000000000000000000000000000000000000',
47
+ names: '0x0000000000000000000000000000000000000000',
46
48
  };
47
49
  /** `address(0)` significa MON nativo en todo el protocolo. */
48
50
  export const NATIVE_CURRENCY = '0x0000000000000000000000000000000000000000';
package/dist/client.d.ts CHANGED
@@ -65,6 +65,31 @@ export interface HireResult {
65
65
  /** El hash del brief que quedó registrado, para poder probarlo después. */
66
66
  taskHash: Hex;
67
67
  }
68
+ /**
69
+ * ¿Esto que manda el indexador es un nombre de PanalNames?
70
+ *
71
+ * Se valida como todo lo que llega de un servicio: si viene a medias se
72
+ * descarta, porque un `origen` inventado haria que la web avisara de una venta
73
+ * que no existio, o peor, que callara una que si.
74
+ */
75
+ /**
76
+ * La skill pedida y, detrás, versiones cada vez más generales de ella.
77
+ *
78
+ * `searchAgents` exige que TODAS las palabras aparezcan, así que cuantas más
79
+ * lleve la skill, menos gente la cumple. Quien escribe estas cadenas suele ser
80
+ * un modelo, y un modelo pide "Spanish tax law" donde el mercado vende "tax".
81
+ *
82
+ * Se recorta POR LA IZQUIERDA porque en inglés el núcleo del sintagma va al
83
+ * final: "Spanish tax law" → "tax law" → "law" sigue hablando de lo mismo.
84
+ * Recortar por la derecha dejaría "Spanish", que casa con cualquier cosa
85
+ * española y con nada de impuestos: peor que no encontrar a nadie, porque se
86
+ * pagaría al agente equivocado.
87
+ *
88
+ * Nunca baja de una palabra y nunca devuelve duplicados, así que en el caso
89
+ * normal —una o dos palabras, que es lo que el prompt pide— esto es una sola
90
+ * búsqueda y no cambia nada.
91
+ */
92
+ export declare function variantesDeSkill(skill: string): string[];
68
93
  export declare class PanalClient {
69
94
  readonly network: PanalNetwork;
70
95
  readonly addresses: PanalAddresses;
@@ -91,8 +116,34 @@ export declare class PanalClient {
91
116
  * ninguno» de «no lo sé», porque en el segundo caso toca leer la cadena.
92
117
  */
93
118
  private buscarEnIndice;
94
- /** Un agente concreto, con su metadata ya interpretada. */
119
+ /**
120
+ * Un agente concreto, con su metadata interpretada y su nombre único.
121
+ *
122
+ * El nombre se lee de PanalNames EN LA CADENA, y por eso está aquí y no solo
123
+ * en la ruta del indexador: el nombre del perfil es texto libre y se repite
124
+ * —ahora mismo hay tres direcciones anunciándose como "LexPanal"—, mientras
125
+ * que un nombre de PanalNames lo tiene una sola dirección. Es la única señal
126
+ * de identidad que sigue en pie si el indexador se cae, porque no depende de
127
+ * que nadie esté levantado: es una llamada `view`.
128
+ *
129
+ * No sustituye a la verificación de dominio, que prueba más: control de un
130
+ * servidor que declara esta misma dirección. Prueba unicidad, no quién hay
131
+ * detrás. Y como los nombres se venden, `desdeTs` importa tanto como el
132
+ * nombre — pero `origen` se queda sin saber por esta ruta, porque sale de los
133
+ * eventos del contrato y no de una lectura.
134
+ */
95
135
  getAgent(address: Address): Promise<Agent>;
136
+ /** La ficha del registry, sin las lecturas de más. Lo que usa `listAgents`. */
137
+ private leerAgente;
138
+ /**
139
+ * El nombre de PanalNames de una dirección, o null si no tiene.
140
+ *
141
+ * Nunca lanza: un agente sin nombre es lo normal, y que el contrato de
142
+ * nombres no esté desplegado —testnet— tampoco puede impedir leer una ficha.
143
+ * Devolver null y seguir es lo correcto; caerse aquí convertiría un dato
144
+ * adicional en un fallo de la operación entera.
145
+ */
146
+ private nombreEnCadena;
96
147
  /**
97
148
  * Busca agentes activos por texto libre sobre nombre, descripción y skills.
98
149
  *
@@ -264,6 +315,7 @@ export declare class PanalClient {
264
315
  depth?: number;
265
316
  }): Promise<AskResult & {
266
317
  agent: Address;
318
+ skill: string;
267
319
  }>;
268
320
  /**
269
321
  * Dónde escucha el x402 de un agente.
package/dist/client.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * y en uso: el caso de "quiero probar esto ahora" no debería exigir un .env.
17
17
  */
18
18
  import { createPublicClient, createWalletClient, formatEther, getAddress, http, keccak256, toBytes } from 'viem';
19
- import { erc20Abi, escrowAbi, registryAbi } from './abis.js';
19
+ import { erc20Abi, escrowAbi, namesAbi, registryAbi } from './abis.js';
20
20
  import { assertPublicUrl, fetchLimited } from './net.js';
21
21
  import { X402Error, payAndAsk, quoteAsk } from './x402.js';
22
22
  import { descend, newEnvelope, remainingBudget } from './envelope.js';
@@ -33,6 +33,35 @@ const REGISTRY_MAX = 500;
33
33
  * descarta, porque un `origen` inventado haria que la web avisara de una venta
34
34
  * que no existio, o peor, que callara una que si.
35
35
  */
36
+ /**
37
+ * La skill pedida y, detrás, versiones cada vez más generales de ella.
38
+ *
39
+ * `searchAgents` exige que TODAS las palabras aparezcan, así que cuantas más
40
+ * lleve la skill, menos gente la cumple. Quien escribe estas cadenas suele ser
41
+ * un modelo, y un modelo pide "Spanish tax law" donde el mercado vende "tax".
42
+ *
43
+ * Se recorta POR LA IZQUIERDA porque en inglés el núcleo del sintagma va al
44
+ * final: "Spanish tax law" → "tax law" → "law" sigue hablando de lo mismo.
45
+ * Recortar por la derecha dejaría "Spanish", que casa con cualquier cosa
46
+ * española y con nada de impuestos: peor que no encontrar a nadie, porque se
47
+ * pagaría al agente equivocado.
48
+ *
49
+ * Nunca baja de una palabra y nunca devuelve duplicados, así que en el caso
50
+ * normal —una o dos palabras, que es lo que el prompt pide— esto es una sola
51
+ * búsqueda y no cambia nada.
52
+ */
53
+ export function variantesDeSkill(skill) {
54
+ const palabras = skill.trim().split(/\s+/).filter(Boolean);
55
+ if (palabras.length <= 1)
56
+ return [skill.trim()].filter(Boolean);
57
+ const out = [];
58
+ for (let i = 0; i < palabras.length; i++) {
59
+ const v = palabras.slice(i).join(' ');
60
+ if (!out.includes(v))
61
+ out.push(v);
62
+ }
63
+ return out;
64
+ }
36
65
  function esNombre(v) {
37
66
  if (v === null || typeof v !== 'object')
38
67
  return false;
@@ -101,7 +130,11 @@ export class PanalClient {
101
130
  if (page.length < Number(REGISTRY_PAGE))
102
131
  break;
103
132
  }
104
- return Promise.all(addresses.map((address) => this.getAgent(address)));
133
+ // `leerAgente` y no `getAgent`: este listado ya son N lecturas en paralelo
134
+ // contra un RPC que corta sobre 50 concurrentes, y buscar el nombre de cada
135
+ // uno las triplicaria. Quien quiera el nombre de uno concreto llama a
136
+ // getAgent(); quien quiera el de todos tiene el indexador, que ya lo trae.
137
+ return Promise.all(addresses.map((address) => this.leerAgente(address)));
105
138
  }
106
139
  /**
107
140
  * Los agentes que dice el indexador, o null si no se puede contar con él.
@@ -182,8 +215,29 @@ export class PanalClient {
182
215
  return null;
183
216
  }
184
217
  }
185
- /** Un agente concreto, con su metadata ya interpretada. */
218
+ /**
219
+ * Un agente concreto, con su metadata interpretada y su nombre único.
220
+ *
221
+ * El nombre se lee de PanalNames EN LA CADENA, y por eso está aquí y no solo
222
+ * en la ruta del indexador: el nombre del perfil es texto libre y se repite
223
+ * —ahora mismo hay tres direcciones anunciándose como "LexPanal"—, mientras
224
+ * que un nombre de PanalNames lo tiene una sola dirección. Es la única señal
225
+ * de identidad que sigue en pie si el indexador se cae, porque no depende de
226
+ * que nadie esté levantado: es una llamada `view`.
227
+ *
228
+ * No sustituye a la verificación de dominio, que prueba más: control de un
229
+ * servidor que declara esta misma dirección. Prueba unicidad, no quién hay
230
+ * detrás. Y como los nombres se venden, `desdeTs` importa tanto como el
231
+ * nombre — pero `origen` se queda sin saber por esta ruta, porque sale de los
232
+ * eventos del contrato y no de una lectura.
233
+ */
186
234
  async getAgent(address) {
235
+ const base = await this.leerAgente(address);
236
+ const nombre = await this.nombreEnCadena(address);
237
+ return nombre ? { ...base, nombre } : base;
238
+ }
239
+ /** La ficha del registry, sin las lecturas de más. Lo que usa `listAgents`. */
240
+ async leerAgente(address) {
187
241
  const raw = (await this.publicClient.readContract({
188
242
  address: this.addresses.registry,
189
243
  abi: registryAbi,
@@ -201,6 +255,42 @@ export class PanalClient {
201
255
  metadata: parseAgentMetadata(raw.metadataURI),
202
256
  };
203
257
  }
258
+ /**
259
+ * El nombre de PanalNames de una dirección, o null si no tiene.
260
+ *
261
+ * Nunca lanza: un agente sin nombre es lo normal, y que el contrato de
262
+ * nombres no esté desplegado —testnet— tampoco puede impedir leer una ficha.
263
+ * Devolver null y seguir es lo correcto; caerse aquí convertiría un dato
264
+ * adicional en un fallo de la operación entera.
265
+ */
266
+ async nombreEnCadena(address) {
267
+ const names = this.addresses.names;
268
+ if (!names || names === '0x0000000000000000000000000000000000000000')
269
+ return null;
270
+ try {
271
+ const nombre = (await this.publicClient.readContract({
272
+ address: names,
273
+ abi: namesAbi,
274
+ functionName: 'nombreDe',
275
+ args: [getAddress(address)],
276
+ }));
277
+ if (!nombre)
278
+ return null;
279
+ const ficha = (await this.publicClient.readContract({
280
+ address: names,
281
+ abi: namesAbi,
282
+ functionName: 'fichaDe',
283
+ args: [nombre],
284
+ }));
285
+ // `origen` a propósito ausente: por esta ruta no se sabe, y ponerle
286
+ // 'reclamado' seria inventarse justo la parte que avisa de una compra
287
+ // reciente.
288
+ return { nombre, desdeTs: Number(ficha[1]) };
289
+ }
290
+ catch {
291
+ return null;
292
+ }
293
+ }
204
294
  /**
205
295
  * Busca agentes activos por texto libre sobre nombre, descripción y skills.
206
296
  *
@@ -582,11 +672,31 @@ export class PanalClient {
582
672
  // Se busca por SKILL, no por texto libre: encontrar a alguien porque la
583
673
  // palabra aparece en su descripción no sirve para delegar. Si el indexador
584
674
  // no está, `searchAgents` cae solo a la cadena con su texto libre.
585
- const candidates = (await this.searchAgents(skill, { skill }))
586
- .filter((a) => !excluded.has(a.address.toLowerCase()) && a.metadata.botUrl)
587
- .slice(0, options.maxCandidates ?? 5);
588
- if (!candidates.length)
589
- throw new X402Error(`Ningún agente activo con la skill "${skill}" publica endpoint.`);
675
+ //
676
+ // La búsqueda exige que TODAS las palabras casen, así que una skill de más
677
+ // de dos palabras no encuentra a nadie casi nunca: quien la escribe es un
678
+ // modelo, y un modelo pide "Spanish tax law" donde el mercado vende "tax".
679
+ // Se reintenta quitando palabras POR LA IZQUIERDA porque en inglés el
680
+ // núcleo va al final: "Spanish tax law" → "tax law" → "law". Así se
681
+ // generaliza sin perder de qué se estaba hablando; recortar por la derecha
682
+ // dejaría "Spanish", que casaría con cualquier cosa española.
683
+ let candidates = [];
684
+ let usada = skill;
685
+ for (const intento of variantesDeSkill(skill)) {
686
+ candidates = (await this.searchAgents(intento, { skill: intento }))
687
+ .filter((a) => !excluded.has(a.address.toLowerCase()) && a.metadata.botUrl)
688
+ .slice(0, options.maxCandidates ?? 5);
689
+ if (candidates.length) {
690
+ usada = intento;
691
+ break;
692
+ }
693
+ }
694
+ if (!candidates.length) {
695
+ throw new X402Error(`Ningún agente activo con la skill "${skill}" publica endpoint.` +
696
+ (variantesDeSkill(skill).length > 1
697
+ ? ` Se probó también con ${variantesDeSkill(skill).slice(1).map((v) => `"${v}"`).join(' y ')}.`
698
+ : ''));
699
+ }
590
700
  const quotes = [];
591
701
  const rechazos = [];
592
702
  for (const agent of candidates) {
@@ -622,7 +732,10 @@ export class PanalClient {
622
732
  quote: elegido.accept,
623
733
  envelope: siguiente,
624
734
  });
625
- return { ...result, agent: elegido.agent.address };
735
+ // `skill` es la que de verdad encontró al vendedor, que puede no ser la que
736
+ // pediste: quien llama necesita poder decirlo en su log, o cada búsqueda
737
+ // ensanchada es un cambio de comportamiento invisible.
738
+ return { ...result, agent: elegido.agent.address, skill: usada };
626
739
  }
627
740
  /**
628
741
  * Dónde escucha el x402 de un agente.
package/dist/types.d.ts CHANGED
@@ -84,8 +84,15 @@ export interface NombreDeAgente {
84
84
  * Importa tanto como el nombre: en una venta lo único que viaja es el
85
85
  * nombre, y la reputación se queda con el vendedor. Un `lint` comprado la
86
86
  * semana pasada no hizo las tareas que hicieron valer ese nombre.
87
+ *
88
+ * OPCIONAL porque no siempre se puede saber. Se deduce de los eventos del
89
+ * contrato de nombres, así que lo tiene el indexador; leyendo la cadena solo
90
+ * hay `nombreDe()` y `fichaDe()`, que dicen cuál es el nombre y desde cuándo
91
+ * es suyo, no cómo llegó a serlo. Cuando falta es `undefined`, y hay que
92
+ * tratarlo como «no lo sé», nunca como «reclamado»: suponer el origen limpio
93
+ * es justo el error que la advertencia existe para evitar.
87
94
  */
88
- origen: 'reclamado' | 'comprado' | 'recibido';
95
+ origen?: 'reclamado' | 'comprado' | 'recibido';
89
96
  }
90
97
  /** Una tarea del escrow. */
91
98
  export interface Task {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panal/sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "SDK de Panal: contrata agentes de IA autonomos on-chain en Monad",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,6 +45,7 @@
45
45
  "scripts": {
46
46
  "build": "tsc -p tsconfig.json",
47
47
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
- "test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts"
48
+ "test": "tsx test/sdk.test.ts && tsx test/x402.test.ts && tsx test/x402-server.test.ts && tsx test/envelope.test.ts && tsx test/files.test.ts && tsx test/skill.test.ts",
49
+ "test:nombres": "tsx test/nombres.test.ts"
49
50
  }
50
51
  }