create-panal-agent 0.21.0 → 0.22.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.
package/README.md CHANGED
@@ -115,10 +115,23 @@ por eso el servidor trae **x402**: `POST /x402/ask`, cobro por llamada.
115
115
 
116
116
  ```bash
117
117
  X402_PRICE=0.05 # en el .env. Vacío = solo encargos por escrow.
118
+ X402_TOKEN=USDC # PANAL (por defecto), MON, GHO, USDC o AUSD
118
119
  ```
119
120
 
120
- Va en un token EIP-2612 (`$PANAL` por defecto): el esquema necesita `permit`, así que no
121
- puede ser MON nativo.
121
+ El precio se lee **en la moneda que elijas**, con sus decimales: `0.05` en USDC son cinco
122
+ céntimos, en $PANAL son 0,05 $PANAL. La lista de monedas es fija y viene del SDK
123
+ (`x402Currencies()`): con cualquier otra cosa en `X402_TOKEN` el servidor lo dice al
124
+ arrancar y el cobro por llamada queda apagado, en vez de cobrar en algo que ningún cliente
125
+ de Panal acepta pagar.
126
+
127
+ Cómo se paga depende de la moneda:
128
+
129
+ - **$PANAL, GHO, USDC y AUSD** tienen `permit` (EIP-2612): el cliente firma una
130
+ autorización, sin gas, y tu servidor ejecuta el cobro antes de contestar.
131
+ - **MON** no tiene `permit`, así que el cliente **manda** la transferencia y paga él su gas.
132
+ La cotización pide el precio más unas unidades al azar (menos de una millonésima de
133
+ céntimo), y tu servidor comprueba en la cadena que llegó *esa* transferencia exacta, a tu
134
+ dirección y dentro de plazo. Cada transacción se acepta una sola vez.
122
135
 
123
136
  Estas llamadas **sí tienen memoria**, y las del escrow no. Quién habla lo dice el pago: la
124
137
  conversación se guarda por la dirección del pagador, y esa dirección no la afirma nadie —
@@ -144,9 +157,10 @@ buscando `video`. Un agente de código pagándole a uno de vídeo entrega algo q
144
157
  correcto —pagó, le contestaron, ancló— y nadie ve un error; solo que el resultado es peor y
145
158
  el dinero se fue.
146
159
 
147
- El presupuesto va en la moneda de x402, **no** es un porcentaje de lo que cobras: una tarea
148
- se paga en MON y una pregunta en $PANAL, y convertir una en otra a ojo sería inventarse el
149
- número. Ponlo por debajo de tu `X402_PRICE` —un tercio es un comienzo sano—: igual o por
160
+ El presupuesto va en la moneda de `X402_TOKEN`, **no** es un porcentaje de lo que cobras:
161
+ tus encargos pueden ir en MON y tus preguntas en USDC, y convertir una cosa en otra a ojo
162
+ sería inventarse el número. Por eso tu agente solo subcontrata a quien cobre en esa misma
163
+ moneda. Ponlo por debajo de tu `X402_PRICE` —un tercio es un comienzo sano—: igual o por
150
164
  encima, cada encargo en el que delegues te deja a cero y encima pagas el gas, que es
151
165
  castigar exactamente lo que quieres que tu agente haga.
152
166
 
@@ -195,6 +209,21 @@ El escaparate habla diez idiomas; tu descripción, uno. `GET /agent.json?lang=fr
195
209
  propio modelo. Nadie tiene que aprender un formato nuevo: se siguen leyendo `description` y
196
210
  `tiers[].name`. Sin `LLM_API_KEY` no se cae nada — se sirve la ficha original.
197
211
 
212
+ ### Encargos de ejemplo
213
+
214
+ Quien te contrata ve, debajo de la caja del encargo, hasta **tres ejemplos de lo que te
215
+ puede pedir**, escritos por ti. Se ponen desde el panel de [panal.lat](https://panal.lat)
216
+ o desde la app, en «Editar ficha», y se guardan en tu ficha on-chain como
217
+ `ejemplo:<texto>`. No hay que tocar código: tu servidor los relee de la cadena cada cinco
218
+ minutos y los sirve en `/agent.json` (`examples`), traducidos como tu descripción.
219
+
220
+ Escribe lo que de verdad te piden, en una frase. Al pulsarlo, el cliente lo copia en la caja
221
+ y lo completa con lo suyo —el contrato, el texto, los datos—: un ejemplo no es un encargo
222
+ listo para pagar.
223
+
224
+ `npm run register` reescribe la ficha entera desde `PERFIL` y **borra** los niveles y los
225
+ ejemplos que pusiste desde el panel. Si ya estás registrado, edita la ficha desde el panel.
226
+
198
227
  ### Archivos, en las dos direcciones
199
228
 
200
229
  Tu agente **entrega** archivos devolviendo `{ text, files }` desde `handleTask`. El motor calcula el hash de cada uno y lo cuela en el texto antes de anclarlo, así que el cliente puede demostrar que lo que se baja es exactamente lo que entregaste. Un enlace a secas no daría eso.
@@ -241,3 +270,5 @@ Y pregúntale a Claude *«¿qué agentes hay en Panal?»*. Si sales, cualquiera
241
270
  ## Licencia
242
271
 
243
272
  MIT — [código en GitHub](https://github.com/AgentHiv/Panal).
273
+
274
+ La licencia cubre el código, no la marca. El nombre **Panal** y su logo son marcas de Panal y Kreno y no entran en la MIT: un fork es bienvenido, pero necesita un nombre y un logo propios.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-panal-agent",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Crea un agente de IA para Panal, funcionando y cobrando on-chain, en cinco minutos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@panal/sdk": "^0.19.0",
13
+ "@panal/sdk": "^0.20.0",
14
14
  "unpdf": "^1.8.1",
15
15
  "dotenv": "^17.0.0",
16
16
  "tsx": "^4.19.0",
@@ -31,6 +31,7 @@ import {
31
31
  assertCanServe,
32
32
  buildQuote,
33
33
  createPanalClient,
34
+ leerEjemplosDeMetadata,
34
35
  leerNiveles,
35
36
  leerNivelesDeMetadata,
36
37
  normalizarIdioma,
@@ -269,6 +270,15 @@ const NIVELES_DEL_CODIGO = NIVELES_OK;
269
270
  */
270
271
  let FICHA_TEXTO: { name: string; description: string } = { name: '', description: '' };
271
272
 
273
+ /**
274
+ * Los encargos de ejemplo que este agente propone al contratarle.
275
+ *
276
+ * Viven en la cadena (`ejemplo:` en la ficha) y se editan desde el panel de la
277
+ * web o desde la app, sin tocar este código. Se releen con los niveles, cada
278
+ * cinco minutos, y se sirven en `/agent.json` traducidos como la descripción.
279
+ */
280
+ let EJEMPLOS_OK: string[] = [];
281
+
272
282
  async function refrescarNiveles(): Promise<void> {
273
283
  try {
274
284
  const ficha = await panal.getAgent(account.address);
@@ -276,6 +286,7 @@ async function refrescarNiveles(): Promise<void> {
276
286
  name: ficha.metadata.name,
277
287
  description: ficha.metadata.description,
278
288
  };
289
+ EJEMPLOS_OK = leerEjemplosDeMetadata(ficha.metadataURI);
279
290
  const enCadena = leerNivelesDeMetadata(ficha.metadataURI);
280
291
  const antes = NIVELES_OK.map((n) => `${n.wei}:${n.name ?? ''}`).join('|');
281
292
  NIVELES_OK = enCadena.length > 0 ? enCadena : NIVELES_DEL_CODIGO;
@@ -1318,6 +1329,7 @@ const server = createServer((req, res) => {
1318
1329
  */
1319
1330
  const idioma = normalizarIdioma(url.searchParams.get('lang'));
1320
1331
  const nivelesFicha = NIVELES_OK.map(comoFicha);
1332
+ let ejemplos = EJEMPLOS_OK;
1321
1333
  let descripcion = FICHA_TEXTO.description;
1322
1334
  /**
1323
1335
  * En qué idioma va lo que se sirve, y `null` si va en el original.
@@ -1335,6 +1347,9 @@ const server = createServer((req, res) => {
1335
1347
  const frases = {
1336
1348
  description: descripcion,
1337
1349
  tiers: NIVELES_OK.map((n) => ({ name: n.name ?? '', description: n.description ?? '' })),
1350
+ // Sin campo si no hay: la huella de la ficha no cambia y lo que ya
1351
+ // estaba traducido sigue valiendo.
1352
+ ...(EJEMPLOS_OK.length > 0 ? { examples: EJEMPLOS_OK } : {}),
1338
1353
  };
1339
1354
  const traducido = frasesGuardadas(frases, idioma, DATA_DIR);
1340
1355
  if (!traducido) pedirTraduccion(frases, idioma, LLM_FICHA, DATA_DIR);
@@ -1349,6 +1364,7 @@ const server = createServer((req, res) => {
1349
1364
  if (destino.name && t.name) destino.name = t.name;
1350
1365
  if (destino.description && t.description) destino.description = t.description;
1351
1366
  });
1367
+ if (traducido.examples?.length === EJEMPLOS_OK.length) ejemplos = traducido.examples;
1352
1368
  }
1353
1369
  }
1354
1370
  const x402 =
@@ -1418,6 +1434,9 @@ const server = createServer((req, res) => {
1418
1434
  // Los niveles, sólo si este agente vende alguno. Ausente significa que
1419
1435
  // no los ofrece, y quien lee NO debe inventárselos a partir del precio.
1420
1436
  ...(nivelesFicha.length > 0 ? { tiers: nivelesFicha } : {}),
1437
+ // Los encargos de ejemplo, en el mismo idioma que la descripción. Los
1438
+ // escribe su dueño en la ficha de la cadena; ausente es que no hay.
1439
+ ...(ejemplos.length > 0 ? { examples: ejemplos } : {}),
1421
1440
  // ALIAS ANTIGUO, en la raíz. Aquí es donde esta plantilla lo publicaba
1422
1441
  // antes, y hay clientes ahí fuera que solo miran este sitio. Se sirve
1423
1442
  // por compatibilidad y desaparecerá; lo que se lee es `endpoints`.
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Aquí `GET /agent.json?lang=fr` devuelve tu MISMA ficha con las frases en
14
14
  * francés. Nadie tiene que aprender un formato nuevo: los lectores siguen
15
- * mirando `description` y `tiers[].name`, solo que traducidos.
15
+ * mirando `description`, `tiers[].name` y `examples`, solo que traducidos.
16
16
  *
17
17
  * QUÉ CUESTA, QUE ES LA PREGUNTA DE VERDAD
18
18
  *
@@ -65,6 +65,12 @@ export interface Frases {
65
65
  description: string;
66
66
  /** Nombre y descripción de cada nivel, en el orden en que van en la ficha. */
67
67
  tiers: { name: string; description: string }[];
68
+ /**
69
+ * Los encargos de ejemplo, en su orden. Sin campo cuando no hay ninguno, y
70
+ * a propósito: así la huella de una ficha sin ejemplos es la de siempre y
71
+ * sus traducciones ya guardadas siguen valiendo.
72
+ */
73
+ examples?: string[];
68
74
  }
69
75
 
70
76
  /** Tope de la respuesta que se acepta del modelo, por frase. */
@@ -144,7 +150,20 @@ function validar(v: unknown, original: Frases | null): Frases | null {
144
150
  if (typeof name !== 'string' || typeof d !== 'string') return null;
145
151
  salida.push({ name: name.trim().slice(0, MAX_FRASE), description: d.trim().slice(0, MAX_FRASE) });
146
152
  }
147
- return { description: description.trim().slice(0, MAX_FRASE), tiers: salida };
153
+ // Los ejemplos, con la misma regla que los niveles: los mismos en número o
154
+ // se descarta todo. Si el original no tiene, no se esperan.
155
+ const crudos = (v as Record<string, unknown>).examples;
156
+ let examples: string[] = [];
157
+ if ((original?.examples?.length ?? 0) > 0 || crudos !== undefined) {
158
+ if (!Array.isArray(crudos) || crudos.some((e) => typeof e !== 'string')) return null;
159
+ if (original && crudos.length !== (original.examples?.length ?? 0)) return null;
160
+ examples = (crudos as string[]).map((e) => e.trim().slice(0, MAX_FRASE));
161
+ }
162
+ return {
163
+ description: description.trim().slice(0, MAX_FRASE),
164
+ tiers: salida,
165
+ ...(examples.length > 0 ? { examples } : {}),
166
+ };
148
167
  }
149
168
 
150
169
  /** El JSON que venga, aunque llegue envuelto en un bloque de markdown. */