@panal/sdk 0.18.2 → 0.19.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Panal and Kreno (founder: Gustavo Chura C.)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/client.d.ts CHANGED
@@ -145,6 +145,19 @@ export declare class PanalClient {
145
145
  * por otro motivo— no se bloquea nada: que hable el error de verdad.
146
146
  */
147
147
  private comprobarReserva;
148
+ /**
149
+ * El gas con el que se firma: `eth_estimateGas` + 10 %, y nunca por encima
150
+ * de `TOPE_GAS_RETIRADA`.
151
+ *
152
+ * Hace falta porque viem no estima: le pide al nodo que rellene la
153
+ * transacción (`eth_fillTransaction`), el de Monad a veces infla el límite, y
154
+ * Monad cobra el límite ENTERO. Cinco retiradas de MON perdieron lo retirado
155
+ * así entre julio y septiembre. Se pasa explícito a TODO lo que firma una
156
+ * cuenta local en nombre de un programa que nadie está mirando: contratar,
157
+ * aprobar, entregar, disputar, darse de alta. Antes solo la retirada y el
158
+ * tablón, y un agente entrega en cada encargo.
159
+ */
160
+ private gasFijo;
148
161
  /**
149
162
  * Espera el recibo y comprueba que la transacción SALIÓ BIEN.
150
163
  *
@@ -337,14 +350,24 @@ export declare class PanalClient {
337
350
  */
338
351
  readBoardBrief(taskId: bigint): Promise<string>;
339
352
  /**
340
- * Entrega una tarea del tablón: deja el texto en el buzón y ancla su hash.
341
- *
342
- * EN ESE ORDEN, y no al revés. El cliente recoge la entrega del buzón, porque
343
- * cuando publicó el encargo no sabía quién lo iba a coger ni dónde vive su
344
- * servidor. Si se anclara primero y el buzón fallara después, el cliente
345
- * vería una entrega en la cadena que no puede descargar. Dejándola primero,
346
- * un fallo del buzón no ancla nada y se puede reintentar; y el buzón acepta
347
- * repetir la misma entrega, porque da el mismo hash.
353
+ * Entrega una tarea del tablón hecha por una cuenta SIN servidor propio:
354
+ * deja el texto en su buzón y ancla su hash.
355
+ *
356
+ * DÓNDE, Y POR QUÉ ESTO SE CORRIGIÓ. El cliente recoge una entrega del
357
+ * endpoint que el TRABAJADOR tiene publicado en el registro (`bot:`), igual
358
+ * que en cualquier encargo: es lo que hace la web, y así entregan en ella las
359
+ * personas que cogen del tablón. La primera versión la dejaba en el buzón del
360
+ * tablón (la dirección cero), donde el cliente no mira nunca: habría anclado
361
+ * el hash de una entrega que nadie podía descargar.
362
+ *
363
+ * Así que va al buzón de quien entrega. Y si su `bot:` es un servidor propio
364
+ * —la plantilla de create-panal-agent—, esto se niega: ahí la entrega la sirve
365
+ * ese servidor, y lo que toca es guardarla donde él la sirve y anclarla con
366
+ * `deliverResult`. Dejarla en un buzón no la haría visible.
367
+ *
368
+ * EN ESTE ORDEN, buzón y después cadena: si el buzón falla no se ancla nada y
369
+ * se puede reintentar; al revés, el cliente vería una entrega en la cadena que
370
+ * no puede descargar. El buzón acepta repetir la misma entrega.
348
371
  */
349
372
  deliverBoardResult(taskId: bigint, resultText: string): Promise<{
350
373
  txHash: Hex;
@@ -424,6 +447,13 @@ export declare class PanalClient {
424
447
  * cuesta ni una petición.
425
448
  */
426
449
  skillsPermitidas?: string[];
450
+ /**
451
+ * En qué moneda se paga, y por tanto en qué unidades va `maxSpend`.
452
+ * Por defecto $PANAL. Solo se cotiza con quien cobre en ESTA: comparar
453
+ * precios de monedas distintas era comparar 1 USDC con 1 wei de $PANAL,
454
+ * y un tope en una moneda no dice nada de otra.
455
+ */
456
+ asset?: Address;
427
457
  }): Promise<AskResult & {
428
458
  agent: Address;
429
459
  skill: string;
package/dist/client.js CHANGED
@@ -29,6 +29,13 @@ import { TaskStatus, formatAgentMetadata, parseAgentMetadata, } from './types.js
29
29
  * $PANAL 103.511. Ver `withdraw()` para lo que pasó sin este tope.
30
30
  */
31
31
  const TOPE_GAS_RETIRADA = 300000n;
32
+ /**
33
+ * El tope para dar de alta o cambiar la ficha. Guardar texto en la cadena
34
+ * cuesta por cada 32 bytes, y una ficha con el logo dentro es grande: aquí el
35
+ * tope solo está para cortar un disparate, no para medir lo normal. Lo normal
36
+ * lo da `eth_estimateGas`, que es fiable; el que inflaba era `eth_fillTransaction`.
37
+ */
38
+ const TOPE_GAS_FICHA = 30000000n;
32
39
  /** Cuántos agentes se leen por llamada al registry. */
33
40
  const REGISTRY_PAGE = 50n;
34
41
  /** Tope duro de agentes recorridos, por si el registro crece mucho. */
@@ -167,6 +174,27 @@ export class PanalClient {
167
174
  `faltan ${formatEther(reserva - saldo)} MON. No se ha enviado nada.`);
168
175
  }
169
176
  }
177
+ /**
178
+ * El gas con el que se firma: `eth_estimateGas` + 10 %, y nunca por encima
179
+ * de `TOPE_GAS_RETIRADA`.
180
+ *
181
+ * Hace falta porque viem no estima: le pide al nodo que rellene la
182
+ * transacción (`eth_fillTransaction`), el de Monad a veces infla el límite, y
183
+ * Monad cobra el límite ENTERO. Cinco retiradas de MON perdieron lo retirado
184
+ * así entre julio y septiembre. Se pasa explícito a TODO lo que firma una
185
+ * cuenta local en nombre de un programa que nadie está mirando: contratar,
186
+ * aprobar, entregar, disputar, darse de alta. Antes solo la retirada y el
187
+ * tablón, y un agente entrega en cada encargo.
188
+ */
189
+ async gasFijo(que, llamada, tope = TOPE_GAS_RETIRADA) {
190
+ const estimado = await this.publicClient.estimateContractGas({ ...llamada, account: this.account });
191
+ const gas = (estimado * 11n + 9n) / 10n;
192
+ if (gas > tope) {
193
+ throw new Error(`${que}: la estimación de gas salió en ${estimado}, muy por encima de lo normal. ` +
194
+ 'Monad cobra el límite entero: no se ha enviado nada.');
195
+ }
196
+ return gas;
197
+ }
170
198
  /**
171
199
  * Espera el recibo y comprueba que la transacción SALIÓ BIEN.
172
200
  *
@@ -456,22 +484,19 @@ export class PanalClient {
456
484
  if (!isNative) {
457
485
  // approve por el importe exacto, no infinito: si el escrow tuviera un
458
486
  // fallo, la exposición se limita a este encargo.
487
+ const aFirmar = { address: agent.currency, abi: erc20Abi, functionName: 'approve', args: [this.addresses.escrow, amount] };
459
488
  const approveHash = await wallet.writeContract({
460
- address: agent.currency,
461
- abi: erc20Abi,
462
- functionName: 'approve',
463
- args: [this.addresses.escrow, amount],
489
+ ...aFirmar,
490
+ gas: await this.gasFijo('El approve', aFirmar),
464
491
  chain: chainFor(this.network),
465
492
  account: this.account,
466
493
  });
467
494
  await this.publicClient.waitForTransactionReceipt({ hash: approveHash });
468
495
  }
496
+ const aFirmar = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'createTask', args: [agent.address, taskHash, deadline, agent.currency, amount], value: isNative ? amount : 0n };
469
497
  const txHash = await wallet.writeContract({
470
- address: this.addresses.escrow,
471
- abi: escrowAbi,
472
- functionName: 'createTask',
473
- args: [agent.address, taskHash, deadline, agent.currency, amount],
474
- value: isNative ? amount : 0n,
498
+ ...aFirmar,
499
+ gas: await this.gasFijo('El encargo', aFirmar),
475
500
  chain: chainFor(this.network),
476
501
  account: this.account,
477
502
  });
@@ -498,11 +523,10 @@ export class PanalClient {
498
523
  if (task.status !== TaskStatus.Delivered) {
499
524
  throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se aprueba lo entregado.`);
500
525
  }
526
+ const aFirmar = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'approveAndRelease', args: [taskId, rating] };
501
527
  const hash = await wallet.writeContract({
502
- address: this.addresses.escrow,
503
- abi: escrowAbi,
504
- functionName: 'approveAndRelease',
505
- args: [taskId, rating],
528
+ ...aFirmar,
529
+ gas: await this.gasFijo('La aprobación', aFirmar),
506
530
  chain: chainFor(this.network),
507
531
  account: this.account,
508
532
  });
@@ -531,11 +555,10 @@ export class PanalClient {
531
555
  if (task.status !== TaskStatus.Open) {
532
556
  throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se cancela lo que sigue abierto.`);
533
557
  }
558
+ const aFirmar = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'cancelTask', args: [taskId] };
534
559
  const hash = await wallet.writeContract({
535
- address: this.addresses.escrow,
536
- abi: escrowAbi,
537
- functionName: 'cancelTask',
538
- args: [taskId],
560
+ ...aFirmar,
561
+ gas: await this.gasFijo('La cancelación', aFirmar),
539
562
  chain: chainFor(this.network),
540
563
  account: this.account,
541
564
  });
@@ -559,11 +582,10 @@ export class PanalClient {
559
582
  if (task.status !== TaskStatus.Delivered) {
560
583
  throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se disputa lo entregado.`);
561
584
  }
585
+ const aFirmar = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'openDispute', args: [taskId] };
562
586
  const hash = await wallet.writeContract({
563
- address: this.addresses.escrow,
564
- abi: escrowAbi,
565
- functionName: 'openDispute',
566
- args: [taskId],
587
+ ...aFirmar,
588
+ gas: await this.gasFijo('La disputa', aFirmar),
567
589
  chain: chainFor(this.network),
568
590
  account: this.account,
569
591
  });
@@ -591,6 +613,7 @@ export class PanalClient {
591
613
  ...llamada,
592
614
  abi: registryAbi,
593
615
  functionName: 'registerAgent',
616
+ gas: await this.gasFijo('El alta', llamada, TOPE_GAS_FICHA),
594
617
  chain: chainFor(this.network),
595
618
  account: this.account,
596
619
  });
@@ -600,11 +623,10 @@ export class PanalClient {
600
623
  /** Cambia el nombre, la descripción, las skills o el endpoint publicados. */
601
624
  async updateMetadata(metadata) {
602
625
  const wallet = this.wallet();
626
+ const aFirmar = { address: this.addresses.registry, abi: registryAbi, functionName: 'updateMetadata', args: [formatAgentMetadata(metadata)] };
603
627
  const hash = await wallet.writeContract({
604
- address: this.addresses.registry,
605
- abi: registryAbi,
606
- functionName: 'updateMetadata',
607
- args: [formatAgentMetadata(metadata)],
628
+ ...aFirmar,
629
+ gas: await this.gasFijo('El cambio de ficha', aFirmar, TOPE_GAS_FICHA),
608
630
  chain: chainFor(this.network),
609
631
  account: this.account,
610
632
  });
@@ -614,11 +636,10 @@ export class PanalClient {
614
636
  /** Cambia el precio por tarea, y opcionalmente la moneda en la que cobras. */
615
637
  async updatePrice(pricePerTask, currency = NATIVE_CURRENCY) {
616
638
  const wallet = this.wallet();
639
+ const aFirmar = { address: this.addresses.registry, abi: registryAbi, functionName: 'updatePrice', args: [pricePerTask, currency] };
617
640
  const hash = await wallet.writeContract({
618
- address: this.addresses.registry,
619
- abi: registryAbi,
620
- functionName: 'updatePrice',
621
- args: [pricePerTask, currency],
641
+ ...aFirmar,
642
+ gas: await this.gasFijo('El cambio de precio', aFirmar),
622
643
  chain: chainFor(this.network),
623
644
  account: this.account,
624
645
  });
@@ -632,11 +653,10 @@ export class PanalClient {
632
653
  */
633
654
  async setActive(active) {
634
655
  const wallet = this.wallet();
656
+ const aFirmar = { address: this.addresses.registry, abi: registryAbi, functionName: 'setActive', args: [active] };
635
657
  const hash = await wallet.writeContract({
636
- address: this.addresses.registry,
637
- abi: registryAbi,
638
- functionName: 'setActive',
639
- args: [active],
658
+ ...aFirmar,
659
+ gas: await this.gasFijo('El cambio de estado', aFirmar),
640
660
  chain: chainFor(this.network),
641
661
  account: this.account,
642
662
  });
@@ -660,11 +680,10 @@ export class PanalClient {
660
680
  throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se entrega lo que sigue abierto.`);
661
681
  }
662
682
  const resultHash = keccak256(toBytes(resultText));
683
+ const aFirmar = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'deliverResult', args: [taskId, resultHash] };
663
684
  const txHash = await wallet.writeContract({
664
- address: this.addresses.escrow,
665
- abi: escrowAbi,
666
- functionName: 'deliverResult',
667
- args: [taskId, resultHash],
685
+ ...aFirmar,
686
+ gas: await this.gasFijo('La entrega', aFirmar),
668
687
  chain: chainFor(this.network),
669
688
  account: this.account,
670
689
  });
@@ -799,11 +818,11 @@ export class PanalClient {
799
818
  throw new Error(`${yo} no es un agente activo en el registro, y claimTask solo acepta agentes activos. ` +
800
819
  'Regístrate (o reactívate) antes de coger trabajo.');
801
820
  }
821
+ const llamada = { address: this.addresses.escrow, abi: escrowAbi, functionName: 'claimTask', args: [taskId] };
822
+ const gas = await this.gasFijo(`Coger la tarea #${taskId}`, llamada);
802
823
  const txHash = await wallet.writeContract({
803
- address: this.addresses.escrow,
804
- abi: escrowAbi,
805
- functionName: 'claimTask',
806
- args: [taskId],
824
+ ...llamada,
825
+ gas,
807
826
  chain: chainFor(this.network),
808
827
  account: this.account,
809
828
  });
@@ -849,14 +868,24 @@ export class PanalClient {
849
868
  return brief;
850
869
  }
851
870
  /**
852
- * Entrega una tarea del tablón: deja el texto en el buzón y ancla su hash.
871
+ * Entrega una tarea del tablón hecha por una cuenta SIN servidor propio:
872
+ * deja el texto en su buzón y ancla su hash.
873
+ *
874
+ * DÓNDE, Y POR QUÉ ESTO SE CORRIGIÓ. El cliente recoge una entrega del
875
+ * endpoint que el TRABAJADOR tiene publicado en el registro (`bot:`), igual
876
+ * que en cualquier encargo: es lo que hace la web, y así entregan en ella las
877
+ * personas que cogen del tablón. La primera versión la dejaba en el buzón del
878
+ * tablón (la dirección cero), donde el cliente no mira nunca: habría anclado
879
+ * el hash de una entrega que nadie podía descargar.
880
+ *
881
+ * Así que va al buzón de quien entrega. Y si su `bot:` es un servidor propio
882
+ * —la plantilla de create-panal-agent—, esto se niega: ahí la entrega la sirve
883
+ * ese servidor, y lo que toca es guardarla donde él la sirve y anclarla con
884
+ * `deliverResult`. Dejarla en un buzón no la haría visible.
853
885
  *
854
- * EN ESE ORDEN, y no al revés. El cliente recoge la entrega del buzón, porque
855
- * cuando publicó el encargo no sabía quién lo iba a coger ni dónde vive su
856
- * servidor. Si se anclara primero y el buzón fallara después, el cliente
857
- * vería una entrega en la cadena que no puede descargar. Dejándola primero,
858
- * un fallo del buzón no ancla nada y se puede reintentar; y el buzón acepta
859
- * repetir la misma entrega, porque da el mismo hash.
886
+ * EN ESTE ORDEN, buzón y después cadena: si el buzón falla no se ancla nada y
887
+ * se puede reintentar; al revés, el cliente vería una entrega en la cadena que
888
+ * no puede descargar. El buzón acepta repetir la misma entrega.
860
889
  */
861
890
  async deliverBoardResult(taskId, resultText) {
862
891
  const wallet = this.wallet();
@@ -868,9 +897,18 @@ export class PanalClient {
868
897
  if (task.status !== TaskStatus.Open) {
869
898
  throw new Error(`La tarea #${taskId} está "${TaskStatus[task.status]}": solo se entrega lo que sigue abierto.`);
870
899
  }
900
+ const ficha = await this.leerAgente(yo);
901
+ const canal = ficha.metadata.botUrl?.replace(/\/+$/, '') ?? '';
902
+ if (!canal) {
903
+ throw new Error(`${yo} no publica ningún canal (\`bot:\`): el cliente no tendría de dónde recoger la entrega.`);
904
+ }
905
+ if (!canal.toLowerCase().startsWith(this.buzonUrl.toLowerCase() + '/')) {
906
+ throw new Error(`${yo} recibe en ${canal}, su propio servidor: el cliente recoge la entrega de ahí. ` +
907
+ 'Guárdala donde tu servidor sirve /result/:taskId y ánclala con deliverResult(). No se ha enviado nada.');
908
+ }
871
909
  const expira = Math.floor(Date.now() / 1000) + VENTANA_FIRMA_S;
872
910
  const firma = await wallet.signMessage({ account: this.account, message: entregaSignMessage(taskId, expira) });
873
- const { status, text } = await fetchLimited(`${this.buzonUrl}/${TABLON}/entrega/${taskId}`, {
911
+ const { status, text } = await fetchLimited(`${canal}/entrega/${taskId}`, {
874
912
  method: 'POST',
875
913
  headers: { 'content-type': 'application/json' },
876
914
  body: JSON.stringify({ entrega: resultText, address: yo, signature: firma, expira }),
@@ -978,6 +1016,7 @@ export class PanalClient {
978
1016
  ? ` Se probó también con ${variantesDeSkill(skill).slice(1).map((v) => `"${v}"`).join(' y ')}.`
979
1017
  : ''));
980
1018
  }
1019
+ const moneda = getAddress(options.asset ?? this.addresses.panalToken);
981
1020
  const quotes = [];
982
1021
  const rechazos = [];
983
1022
  for (const agent of candidates) {
@@ -988,7 +1027,10 @@ export class PanalClient {
988
1027
  ...options,
989
1028
  envelope: heredado,
990
1029
  });
991
- if (BigInt(accept.amount) <= tope)
1030
+ if (getAddress(accept.asset) !== moneda) {
1031
+ rechazos.push(`${agent.metadata.name || agent.address}: cobra en ${accept.asset}, no en ${moneda}`);
1032
+ }
1033
+ else if (BigInt(accept.amount) <= tope)
992
1034
  quotes.push({ agent, endpoint, accept });
993
1035
  else
994
1036
  rechazos.push(`${agent.metadata.name || agent.address}: pide ${accept.amount}, por encima del tope`);
@@ -1010,6 +1052,7 @@ export class PanalClient {
1010
1052
  maxSpend: tope,
1011
1053
  chainId: chainFor(this.network).id,
1012
1054
  expectedPayee: elegido.agent.address,
1055
+ asset: moneda,
1013
1056
  quote: elegido.accept,
1014
1057
  envelope: siguiente,
1015
1058
  });
@@ -1070,12 +1113,7 @@ export class PanalClient {
1070
1113
  functionName: 'withdraw',
1071
1114
  args: [currency],
1072
1115
  };
1073
- const estimado = await this.publicClient.estimateContractGas({ ...llamada, account: this.account });
1074
- const gas = (estimado * 11n + 9n) / 10n;
1075
- if (gas > TOPE_GAS_RETIRADA) {
1076
- throw new Error(`La estimación de gas para retirar salió en ${estimado}, muy por encima de lo normal (~55.000 en MON, ` +
1077
- `~104.000 en $PANAL). Monad cobra el límite entero: no se ha enviado nada.`);
1078
- }
1116
+ const gas = await this.gasFijo('La retirada', llamada);
1079
1117
  const hash = await wallet.writeContract({
1080
1118
  ...llamada,
1081
1119
  gas,
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Las monedas con las que se paga una consulta x402 en Panal.
3
+ *
4
+ * POR QUÉ UNA LISTA CERRADA
5
+ *
6
+ * La cotización la escribe el agente: qué token, qué importe y cómo se llama.
7
+ * Fiarse de eso para enseñar el precio era el agujero. Un agente podía cobrar
8
+ * en un token de 6 decimales y llamarlo «$PANAL»: 1.000 USDC se pintaban
9
+ * «0,000000001 $PANAL» y la persona firmaba un permiso por 1.000 USDC creyendo
10
+ * que no pagaba nada.
11
+ *
12
+ * Aquí se fija, para cada red, qué tokens se aceptan y con qué nombre y
13
+ * decimales. El nombre y los decimales salen SIEMPRE de esta lista, nunca de
14
+ * la cotización, y un token que no está aquí no se paga. Es la misma lista
15
+ * para todas las capas —web, app, MCP y agentes— para que ninguna pueda
16
+ * enseñar un precio distinto del que se firma.
17
+ *
18
+ * Las cuatro de token cobran con `permit` (EIP-2612), comprobado en mainnet:
19
+ * firma gratis, y el gas lo pone el agente que cobra. MON, la moneda nativa,
20
+ * no tiene `permit`: quien paga MANDA una transferencia (paga él su gas, una
21
+ * fracción de céntimo) y la presenta. Ver `native-transfer` en x402-server.ts.
22
+ *
23
+ * - MON — la moneda de Monad. Nadie puede congelarla.
24
+ * - $PANAL — el token de Panal.
25
+ * - GHO — de Aave. Respaldo cripto, sin lista negra ni pausa: nadie puede
26
+ * congelarlo. La opción estable que no depende de una empresa.
27
+ * - USDC — de Circle. Respaldo en dólares; Circle puede congelar
28
+ * direcciones y pausar el token.
29
+ * - AUSD — de Agora. Respaldo en dólares; Agora puede congelar
30
+ * direcciones y pausar transferencias.
31
+ *
32
+ * En x402 que USDC y AUSD se puedan congelar pesa poco: Panal no guarda nada,
33
+ * el dinero va directo de quien paga al agente, y si alguien es congelado es
34
+ * en su propia wallet. Cada uno elige con qué paga o cobra.
35
+ */
36
+ import { type Address } from 'viem';
37
+ import type { PanalNetwork } from './chains.js';
38
+ /**
39
+ * Cómo se cobra en una moneda: con una firma de `permit` que ejecuta quien
40
+ * cobra, o con una transferencia que manda quien paga.
41
+ */
42
+ export type X402Scheme = 'eip2612-permit' | 'native-transfer';
43
+ /** La moneda nativa, como la escribe Panal en todas partes: la dirección cero. */
44
+ export declare const X402_NATIVE: Address;
45
+ export interface X402Currency {
46
+ /** Como se enseña: `$PANAL`, `GHO`, `USDC`, `AUSD`. */
47
+ symbol: string;
48
+ address: Address;
49
+ decimals: number;
50
+ scheme: X402Scheme;
51
+ /** Si su emisor puede congelar direcciones o pausar el token. */
52
+ issuerCanFreeze: boolean;
53
+ }
54
+ /** Las monedas aceptadas en una red, en el orden en que se ofrecen. */
55
+ export declare function x402Currencies(network?: PanalNetwork): readonly X402Currency[];
56
+ /** La red de un chainId de Monad, o `null` si no es ninguna de las dos. */
57
+ export declare function networkOfChain(chainId: number): PanalNetwork | null;
58
+ /**
59
+ * La moneda de un token, o `null` si Panal no acepta pagar en él.
60
+ *
61
+ * Por dirección: el nombre lo puede escribir cualquiera, la dirección no.
62
+ */
63
+ export declare function x402Currency(token: string, network?: PanalNetwork): X402Currency | null;
64
+ /**
65
+ * Lo mismo, admitiendo también el símbolo: `USDC`, `gho`, `$PANAL` o `PANAL`.
66
+ * Es para configurar un agente a mano (`X402_TOKEN=USDC`), no para leer lo que
67
+ * llega de fuera: eso va siempre por dirección.
68
+ */
69
+ export declare function x402CurrencyByName(name: string, network?: PanalNetwork): X402Currency | null;
70
+ /** Un importe en unidades mínimas, en texto legible: `0.05`. */
71
+ export declare function formatX402Amount(amount: bigint | string, currency: X402Currency): string;
72
+ /** `0.05` → unidades mínimas de esa moneda. Lanza si el texto no es un número. */
73
+ export declare function parseX402Amount(text: string, currency: X402Currency): bigint;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Las monedas con las que se paga una consulta x402 en Panal.
3
+ *
4
+ * POR QUÉ UNA LISTA CERRADA
5
+ *
6
+ * La cotización la escribe el agente: qué token, qué importe y cómo se llama.
7
+ * Fiarse de eso para enseñar el precio era el agujero. Un agente podía cobrar
8
+ * en un token de 6 decimales y llamarlo «$PANAL»: 1.000 USDC se pintaban
9
+ * «0,000000001 $PANAL» y la persona firmaba un permiso por 1.000 USDC creyendo
10
+ * que no pagaba nada.
11
+ *
12
+ * Aquí se fija, para cada red, qué tokens se aceptan y con qué nombre y
13
+ * decimales. El nombre y los decimales salen SIEMPRE de esta lista, nunca de
14
+ * la cotización, y un token que no está aquí no se paga. Es la misma lista
15
+ * para todas las capas —web, app, MCP y agentes— para que ninguna pueda
16
+ * enseñar un precio distinto del que se firma.
17
+ *
18
+ * Las cuatro de token cobran con `permit` (EIP-2612), comprobado en mainnet:
19
+ * firma gratis, y el gas lo pone el agente que cobra. MON, la moneda nativa,
20
+ * no tiene `permit`: quien paga MANDA una transferencia (paga él su gas, una
21
+ * fracción de céntimo) y la presenta. Ver `native-transfer` en x402-server.ts.
22
+ *
23
+ * - MON — la moneda de Monad. Nadie puede congelarla.
24
+ * - $PANAL — el token de Panal.
25
+ * - GHO — de Aave. Respaldo cripto, sin lista negra ni pausa: nadie puede
26
+ * congelarlo. La opción estable que no depende de una empresa.
27
+ * - USDC — de Circle. Respaldo en dólares; Circle puede congelar
28
+ * direcciones y pausar el token.
29
+ * - AUSD — de Agora. Respaldo en dólares; Agora puede congelar
30
+ * direcciones y pausar transferencias.
31
+ *
32
+ * En x402 que USDC y AUSD se puedan congelar pesa poco: Panal no guarda nada,
33
+ * el dinero va directo de quien paga al agente, y si alguien es congelado es
34
+ * en su propia wallet. Cada uno elige con qué paga o cobra.
35
+ */
36
+ import { formatUnits, getAddress, parseUnits } from 'viem';
37
+ /** La moneda nativa, como la escribe Panal en todas partes: la dirección cero. */
38
+ export const X402_NATIVE = '0x0000000000000000000000000000000000000000';
39
+ const MON = {
40
+ symbol: 'MON',
41
+ address: X402_NATIVE,
42
+ decimals: 18,
43
+ scheme: 'native-transfer',
44
+ issuerCanFreeze: false,
45
+ };
46
+ const MAINNET = [
47
+ {
48
+ symbol: '$PANAL',
49
+ address: getAddress('0x2e2e44e7fa6178822d4397299f719e89d1a67777'),
50
+ decimals: 18,
51
+ scheme: 'eip2612-permit',
52
+ issuerCanFreeze: false,
53
+ },
54
+ MON,
55
+ {
56
+ symbol: 'GHO',
57
+ address: getAddress('0xfc421ad3c883bf9e7c4f42de845c4e4405799e73'),
58
+ decimals: 18,
59
+ scheme: 'eip2612-permit',
60
+ issuerCanFreeze: false,
61
+ },
62
+ {
63
+ symbol: 'USDC',
64
+ address: getAddress('0x754704bc059f8c67012fed69bc8a327a5aafb603'),
65
+ decimals: 6,
66
+ scheme: 'eip2612-permit',
67
+ issuerCanFreeze: true,
68
+ },
69
+ {
70
+ symbol: 'AUSD',
71
+ address: getAddress('0x00000000efe302beaa2b3e6e1b18d08d69a9012a'),
72
+ decimals: 6,
73
+ scheme: 'eip2612-permit',
74
+ issuerCanFreeze: true,
75
+ },
76
+ ];
77
+ /** En testnet no hay ninguno de estos tokens, pero MON sí. */
78
+ const TESTNET = [MON];
79
+ /** Las monedas aceptadas en una red, en el orden en que se ofrecen. */
80
+ export function x402Currencies(network = 'mainnet') {
81
+ return network === 'mainnet' ? MAINNET : TESTNET;
82
+ }
83
+ /** La red de un chainId de Monad, o `null` si no es ninguna de las dos. */
84
+ export function networkOfChain(chainId) {
85
+ return chainId === 143 ? 'mainnet' : chainId === 10143 ? 'testnet' : null;
86
+ }
87
+ /**
88
+ * La moneda de un token, o `null` si Panal no acepta pagar en él.
89
+ *
90
+ * Por dirección: el nombre lo puede escribir cualquiera, la dirección no.
91
+ */
92
+ export function x402Currency(token, network = 'mainnet') {
93
+ const t = token.trim().toLowerCase();
94
+ return x402Currencies(network).find((c) => c.address.toLowerCase() === t) ?? null;
95
+ }
96
+ /**
97
+ * Lo mismo, admitiendo también el símbolo: `USDC`, `gho`, `$PANAL` o `PANAL`.
98
+ * Es para configurar un agente a mano (`X402_TOKEN=USDC`), no para leer lo que
99
+ * llega de fuera: eso va siempre por dirección.
100
+ */
101
+ export function x402CurrencyByName(name, network = 'mainnet') {
102
+ const n = name.trim().replace(/^\$/, '').toUpperCase();
103
+ return x402Currencies(network).find((c) => c.symbol.replace(/^\$/, '').toUpperCase() === n) ?? x402Currency(name, network);
104
+ }
105
+ /** Un importe en unidades mínimas, en texto legible: `0.05`. */
106
+ export function formatX402Amount(amount, currency) {
107
+ return formatUnits(BigInt(amount), currency.decimals);
108
+ }
109
+ /** `0.05` → unidades mínimas de esa moneda. Lanza si el texto no es un número. */
110
+ export function parseX402Amount(text, currency) {
111
+ return parseUnits(text.trim(), currency.decimals);
112
+ }
package/dist/index.d.ts CHANGED
@@ -27,14 +27,16 @@ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink,
27
27
  export type { Agent, AgentLinkKey, AgentLinks, AgentMetadata, Task } from './types.js';
28
28
  export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
29
29
  export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
30
+ export { X402_NATIVE, formatX402Amount, networkOfChain, parseX402Amount, x402Currencies, x402Currency, x402CurrencyByName, } from './currencies.js';
31
+ export type { X402Currency, X402Scheme } from './currencies.js';
30
32
  export type { AskResult, PayAndAskOptions, PermitDomain, X402Accept, X402Quote } from './x402.js';
31
33
  export { assertPublicUrl, fetchBytesLimited, fetchLimited, isPrivateIp } from './net.js';
32
34
  export { ATTACH_BLOCK, FILES_BLOCK, MAX_FILE_BYTES, FileVerificationError, appendAttachmentsManifest, appendFilesManifest, attachmentFrom, buildAttachmentsManifest, buildFilesManifest, downloadDeliveredFile, fileUrl, matchAttachment, parseAttachmentsManifest, parseFilesManifest, sanitizeFileName, stripFilesManifest, verifyFileBytes, } from './files.js';
33
35
  export type { AttachedFile, DeliveredFile, DownloadOptions, HashedFile } from './files.js';
34
36
  export { MAX_IMAGEN_BYTES, MIMES_IMAGEN, PROVEEDORES, LlmError, dialectoDe, esImagenSoportada, llmChat, resolverLlm, } from './llm.js';
35
37
  export type { LlmConfig, LlmDialecto, LlmImagen, LlmPeticion, LlmProveedor } from './llm.js';
36
- export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaymentHeader, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, } from './x402-server.js';
37
- export type { SettleDeps, SettleResult, X402Payment, X402ServerAccept, X402ServerQuote, } from './x402-server.js';
38
+ export { X402_VERSION, X402_SERVER_SCHEME, X402_NATIVE_SCHEME, buildNativeQuote, buildQuote, enqueueByPayer, newQuoteSecret, parsePaymentHeader, parseX402Header, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, verifyNativePayment, } from './x402-server.js';
39
+ export type { NativeReplayGuard, SettleDeps, SettleResult, X402NativeAccept, X402NativePayment, X402Payment, X402ServerAccept, X402ServerQuote, } from './x402-server.js';
38
40
  export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
39
41
  export type { CallEnvelope } from './envelope.js';
40
42
  export type { UrlGuardOptions } from './net.js';
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ export { TaskStatus, TASK_STATUS_LABEL, formatAgentMetadata, normalizeAgentLink,
25
25
  export { erc20Abi, escrowAbi, registryAbi } from './abis.js';
26
26
  // x402: pagar a otro agente por una consulta, sin escrow y sin humano.
27
27
  export { X402_SCHEME, X402Error, payAndAsk, quoteAsk } from './x402.js';
28
+ export { X402_NATIVE, formatX402Amount, networkOfChain, parseX402Amount, x402Currencies, x402Currency, x402CurrencyByName, } from './currencies.js';
28
29
  export { assertPublicUrl, fetchBytesLimited, fetchLimited, isPrivateIp } from './net.js';
29
30
  // Archivos: entregar un PDF o un vídeo anclando SU hash, no el del enlace.
30
31
  // Y en la otra dirección, adjuntar una foto al encargo con la misma garantía.
@@ -34,7 +35,7 @@ export { ATTACH_BLOCK, FILES_BLOCK, MAX_FILE_BYTES, FileVerificationError, appen
34
35
  export { MAX_IMAGEN_BYTES, MIMES_IMAGEN, PROVEEDORES, LlmError, dialectoDe, esImagenSoportada, llmChat, resolverLlm, } from './llm.js';
35
36
  // x402: la otra mitad, cobrar por llamada. Portada del bot de LexPanal, donde
36
37
  // lleva meses cobrando en produccion.
37
- export { X402_VERSION, X402_SERVER_SCHEME, buildQuote, enqueueByPayer, parsePaymentHeader, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, } from './x402-server.js';
38
+ export { X402_VERSION, X402_SERVER_SCHEME, X402_NATIVE_SCHEME, buildNativeQuote, buildQuote, enqueueByPayer, newQuoteSecret, parsePaymentHeader, parseX402Header, permitNonce, permitTypedData, readPermitDomain, resourceId, splitSignature, verifyAndSettle, verifyNativePayment, } from './x402-server.js';
38
39
  // El sobre que viaja entre agentes: profundidad, presupuesto y detección de ciclos.
39
40
  export { ENVELOPE_HEADERS, DEFAULT_DEPTH, MAX_DEPTH, BudgetExhausted, DepthExhausted, LoopDetected, assertCanServe, descend, envelopeHeaders, newEnvelope, parseEnvelope, remainingBudget, } from './envelope.js';
40
41
  // La ficha de GET /agent.json: un solo formato, y lectores que perdonan el
package/dist/tablon.d.ts CHANGED
@@ -15,7 +15,11 @@
15
15
  *
16
16
  * GET <buzón>/0x000…000/lista los anuncios, sin firma
17
17
  * GET <buzón>/0x000…000/encargo/:taskId el encargo, solo para quien lo cogió
18
- * POST <buzón>/0x000…000/entrega/:taskId lo entregado, para que lo recoja el cliente
18
+ *
19
+ * La ENTREGA no va al tablón. El cliente la recoge del endpoint que el
20
+ * trabajador publica en el registro, como en cualquier encargo: quien entrega
21
+ * sin servidor propio la deja en SU buzón (`<buzón>/<su dirección>/entrega`), y
22
+ * un agente con servidor la sirve él. Ver `deliverBoardResult`.
19
23
  *
20
24
  * DOS TEXTOS, Y NO ES REDUNDANCIA. El ANUNCIO se lee sin coger nada y lo firma
21
25
  * el cliente, así que el buzón no lo puede cambiar. El ENCARGO solo lo ve quien
package/dist/tablon.js CHANGED
@@ -15,7 +15,11 @@
15
15
  *
16
16
  * GET <buzón>/0x000…000/lista los anuncios, sin firma
17
17
  * GET <buzón>/0x000…000/encargo/:taskId el encargo, solo para quien lo cogió
18
- * POST <buzón>/0x000…000/entrega/:taskId lo entregado, para que lo recoja el cliente
18
+ *
19
+ * La ENTREGA no va al tablón. El cliente la recoge del endpoint que el
20
+ * trabajador publica en el registro, como en cualquier encargo: quien entrega
21
+ * sin servidor propio la deja en SU buzón (`<buzón>/<su dirección>/entrega`), y
22
+ * un agente con servidor la sirve él. Ver `deliverBoardResult`.
19
23
  *
20
24
  * DOS TEXTOS, Y NO ES REDUNDANCIA. El ANUNCIO se lee sin coger nada y lo firma
21
25
  * el cliente, así que el buzón no lo puede cambiar. El ENCARGO solo lo ve quien