panal-mcp 0.7.0 → 0.8.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
@@ -66,7 +66,9 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
66
66
  "MCP_ENABLE_WRITES": "true",
67
67
  "MCP_PRIVATE_KEY": "0x…",
68
68
  "MCP_MAX_PER_TASK_WEI": "1000000000000000000",
69
- "MCP_DAILY_BUDGET_WEI": "5000000000000000000"
69
+ "MCP_DAILY_BUDGET_WEI": "5000000000000000000",
70
+ "MCP_MAX_PER_TASK_PANAL_WEI": "3000000000000000000",
71
+ "MCP_DAILY_BUDGET_PANAL_WEI": "10000000000000000000"
70
72
  }
71
73
  }
72
74
  }
@@ -79,8 +81,17 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
79
81
  |---|---|---|
80
82
  | `MCP_ENABLE_WRITES` | `false` | Interruptor general |
81
83
  | `MCP_PRIVATE_KEY` | — | Wallet del cliente que paga |
82
- | `MCP_MAX_PER_TASK_WEI` | `1e18` (1 MON) | Tope por encargo |
83
- | `MCP_DAILY_BUDGET_WEI` | `5e18` (5 MON) | Tope por día UTC, persistido en disco |
84
+ | `MCP_MAX_PER_TASK_WEI` | `1e18` (1 MON) | Tope por encargo, **en MON** |
85
+ | `MCP_DAILY_BUDGET_WEI` | `5e18` (5 MON) | Tope por día UTC **en MON**, persistido en disco |
86
+ | `MCP_MAX_PER_TASK_PANAL_WEI` | `1e18` (1 $PANAL) | Tope por encargo, **en $PANAL** |
87
+ | `MCP_DAILY_BUDGET_PANAL_WEI` | `5e18` (5 $PANAL) | Tope por día UTC **en $PANAL** |
88
+
89
+ **Cada moneda lleva su cuenta.** Panal cobra en MON nativo y en $PANAL, que no valen lo
90
+ mismo y no tienen tipo de cambio entre sí: sumarlos en un solo número sería inventarse
91
+ la conversión. Con un contador único, tres consultas pagadas en $PANAL agotaban un
92
+ presupuesto puesto pensando en MON y bloqueaban una contratación que iba sobrada. Si un
93
+ agente cobra en un token que no es ninguno de los dos, el servidor **se niega** en vez de
94
+ tirar del presupuesto de otra moneda.
84
95
  | `MCP_TASK_DEADLINE_HOURS` | `24` | Plazo de entrega |
85
96
  | `MCP_SPEND_FILE` | `.panal-mcp/spend.json` | Dónde se guarda el gasto del día |
86
97
  | `RPC_URL` | RPC público de Monad | Tu propio RPC |
package/dist/limits.d.ts CHANGED
@@ -52,11 +52,20 @@ export declare class QuoteBook {
52
52
  private prune;
53
53
  }
54
54
  /**
55
- * Registro de gasto diario, persistido en disco.
55
+ * Registro de gasto diario, persistido en disco y SEPARADO POR MONEDA.
56
56
  *
57
57
  * Un tope que se borra al reiniciar no es un tope: bastaría con reiniciar el
58
58
  * servidor para volver a gastar el presupuesto entero. Se guarda el día UTC
59
- * junto al importe, así que el contador se reinicia solo al cambiar de día.
59
+ * junto a los importes, así que el contador se reinicia solo al cambiar de día.
60
+ *
61
+ * POR QUÉ POR MONEDA
62
+ *
63
+ * Antes había un solo número. Panal cobra en dos monedas —MON nativo y $PANAL—
64
+ * y no hay tipo de cambio entre ellas, así que sumarlas era inventárselo: tres
65
+ * consultas de $PANAL agotaban un presupuesto puesto pensando en MON, y al
66
+ * revés, un tope generoso en $PANAL abría la mano con el MON sin que nadie lo
67
+ * hubiera decidido. La cuenta protegía de más, que es el lado bueno del fallo,
68
+ * pero un tope que no significa lo que dice no se puede ajustar.
60
69
  */
61
70
  export declare class SpendLedger {
62
71
  private readonly file;
@@ -64,12 +73,31 @@ export declare class SpendLedger {
64
73
  constructor(file: string, onWarn: (msg: string) => void);
65
74
  static defaultFile(): string;
66
75
  private static utcDay;
67
- spentToday(): bigint;
68
- record(wei: bigint): void;
76
+ /** Todo lo gastado hoy, por moneda. Vacío si el archivo es de otro día. */
77
+ private leer;
78
+ spentToday(currency: Address): bigint;
79
+ record(currency: Address, wei: bigint): void;
69
80
  }
70
- export interface Limits {
81
+ /** El tope de una moneda concreta. */
82
+ export interface CurrencyLimit {
71
83
  maxPerTaskWei: bigint;
72
84
  dailyBudgetWei: bigint;
85
+ /** El nombre de la variable de entorno, para poder decirle a alguien cuál subir. */
86
+ envMaxPerTask: string;
87
+ envDailyBudget: string;
88
+ }
89
+ export interface Limits {
73
90
  deadlineHours: number;
91
+ /** Topes por moneda, indexados por dirección en minúsculas. */
92
+ porMoneda: Map<string, CurrencyLimit>;
74
93
  }
94
+ /**
95
+ * El tope de una moneda, o `null` si no hay ninguno puesto para ella.
96
+ *
97
+ * `null` significa NO HAY PRESUPUESTO, y quien llame debe negarse. Es
98
+ * deliberado: la alternativa sería reutilizar el tope de otra moneda, y eso
99
+ * exige un tipo de cambio que nadie tiene. Un agente que cobre en un token
100
+ * desconocido no se contrata hasta que alguien decida cuánto vale.
101
+ */
102
+ export declare function limitFor(limits: Limits, currency: Address): CurrencyLimit | null;
75
103
  export declare function limitsFromEnv(): Limits;
package/dist/limits.js CHANGED
@@ -57,12 +57,25 @@ export class QuoteBook {
57
57
  this.quotes.delete(id);
58
58
  }
59
59
  }
60
+ /** Clave de un contador: la dirección de la moneda, en minúsculas. */
61
+ function clave(currency) {
62
+ return currency.toLowerCase();
63
+ }
60
64
  /**
61
- * Registro de gasto diario, persistido en disco.
65
+ * Registro de gasto diario, persistido en disco y SEPARADO POR MONEDA.
62
66
  *
63
67
  * Un tope que se borra al reiniciar no es un tope: bastaría con reiniciar el
64
68
  * servidor para volver a gastar el presupuesto entero. Se guarda el día UTC
65
- * junto al importe, así que el contador se reinicia solo al cambiar de día.
69
+ * junto a los importes, así que el contador se reinicia solo al cambiar de día.
70
+ *
71
+ * POR QUÉ POR MONEDA
72
+ *
73
+ * Antes había un solo número. Panal cobra en dos monedas —MON nativo y $PANAL—
74
+ * y no hay tipo de cambio entre ellas, así que sumarlas era inventárselo: tres
75
+ * consultas de $PANAL agotaban un presupuesto puesto pensando en MON, y al
76
+ * revés, un tope generoso en $PANAL abría la mano con el MON sin que nadie lo
77
+ * hubiera decidido. La cuenta protegía de más, que es el lado bueno del fallo,
78
+ * pero un tope que no significa lo que dice no se puede ajustar.
66
79
  */
67
80
  export class SpendLedger {
68
81
  file;
@@ -77,27 +90,55 @@ export class SpendLedger {
77
90
  static utcDay() {
78
91
  return new Date().toISOString().slice(0, 10);
79
92
  }
80
- spentToday() {
93
+ /** Todo lo gastado hoy, por moneda. Vacío si el archivo es de otro día. */
94
+ leer() {
95
+ const out = new Map();
81
96
  try {
82
97
  const raw = JSON.parse(readFileSync(this.file, 'utf8'));
83
98
  if (raw.day !== SpendLedger.utcDay())
84
- return 0n;
85
- return BigInt(raw.spentWei ?? '0');
99
+ return out;
100
+ // Formato antiguo: un solo número, sin moneda. Era el gasto de todo, pero
101
+ // en la práctica el que llevaba la cuenta era el nativo, así que se
102
+ // adopta ahí. Perderlo dejaría el tope del día a cero tras actualizar.
103
+ if (typeof raw.spentWei === 'string' && !raw.spent) {
104
+ try {
105
+ out.set(clave(NATIVO), BigInt(raw.spentWei));
106
+ }
107
+ catch {
108
+ /* ilegible: se ignora */
109
+ }
110
+ return out;
111
+ }
112
+ for (const [k, v] of Object.entries(raw.spent ?? {})) {
113
+ try {
114
+ out.set(k.toLowerCase(), BigInt(v));
115
+ }
116
+ catch {
117
+ /* una entrada corrupta no invalida las demás */
118
+ }
119
+ }
86
120
  }
87
121
  catch {
88
122
  // Sin archivo, ilegible o corrupto: se empieza de cero. Nunca lanza,
89
123
  // porque un ledger roto no debe dejar el servidor inservible.
90
- return 0n;
91
124
  }
125
+ return out;
92
126
  }
93
- record(wei) {
94
- const total = this.spentToday() + wei;
127
+ spentToday(currency) {
128
+ return this.leer().get(clave(currency)) ?? 0n;
129
+ }
130
+ record(currency, wei) {
131
+ const todo = this.leer();
132
+ todo.set(clave(currency), (todo.get(clave(currency)) ?? 0n) + wei);
133
+ const spent = {};
134
+ for (const [k, v] of todo)
135
+ spent[k] = v.toString();
95
136
  try {
96
137
  mkdirSync(dirname(this.file), { recursive: true });
97
138
  // Escritura atómica: un corte a media escritura dejaría un JSON truncado
98
139
  // que se leería como 0 y borraría el tope del día.
99
140
  const tmp = `${this.file}.tmp`;
100
- writeFileSync(tmp, JSON.stringify({ day: SpendLedger.utcDay(), spentWei: total.toString() }, null, 2));
141
+ writeFileSync(tmp, JSON.stringify({ day: SpendLedger.utcDay(), spent }, null, 2));
101
142
  renameSync(tmp, this.file);
102
143
  }
103
144
  catch (err) {
@@ -106,6 +147,21 @@ export class SpendLedger {
106
147
  }
107
148
  }
108
149
  }
150
+ /** La moneda nativa en el escrow de Panal: la dirección cero. */
151
+ const NATIVO = '0x0000000000000000000000000000000000000000';
152
+ /** $PANAL en Monad mainnet. */
153
+ const PANAL = '0x2e2e44e7fa6178822d4397299f719e89d1a67777';
154
+ /**
155
+ * El tope de una moneda, o `null` si no hay ninguno puesto para ella.
156
+ *
157
+ * `null` significa NO HAY PRESUPUESTO, y quien llame debe negarse. Es
158
+ * deliberado: la alternativa sería reutilizar el tope de otra moneda, y eso
159
+ * exige un tipo de cambio que nadie tiene. Un agente que cobre en un token
160
+ * desconocido no se contrata hasta que alguien decida cuánto vale.
161
+ */
162
+ export function limitFor(limits, currency) {
163
+ return limits.porMoneda.get(clave(currency)) ?? null;
164
+ }
109
165
  export function limitsFromEnv() {
110
166
  const parse = (name, fallback) => {
111
167
  const raw = process.env[name]?.trim();
@@ -120,9 +176,22 @@ export function limitsFromEnv() {
120
176
  }
121
177
  };
122
178
  const hours = Number(process.env.MCP_TASK_DEADLINE_HOURS?.trim() || '24');
123
- return {
124
- maxPerTaskWei: parse('MCP_MAX_PER_TASK_WEI', 10n ** 18n), // 1 MON / $PANAL
179
+ const porMoneda = new Map();
180
+ // MON conserva los nombres de siempre: quien ya tenía un .env sigue
181
+ // significando exactamente lo que creía.
182
+ porMoneda.set(clave(NATIVO), {
183
+ maxPerTaskWei: parse('MCP_MAX_PER_TASK_WEI', 10n ** 18n),
125
184
  dailyBudgetWei: parse('MCP_DAILY_BUDGET_WEI', 5n * 10n ** 18n),
126
- deadlineHours: Number.isFinite(hours) && hours > 0 ? hours : 24,
127
- };
185
+ envMaxPerTask: 'MCP_MAX_PER_TASK_WEI',
186
+ envDailyBudget: 'MCP_DAILY_BUDGET_WEI',
187
+ });
188
+ // $PANAL estrena las suyas, con los mismos números de antes. No se copian de
189
+ // MON porque no valen lo mismo: son dos presupuestos que se ajustan aparte.
190
+ porMoneda.set(clave(PANAL), {
191
+ maxPerTaskWei: parse('MCP_MAX_PER_TASK_PANAL_WEI', 10n ** 18n),
192
+ dailyBudgetWei: parse('MCP_DAILY_BUDGET_PANAL_WEI', 5n * 10n ** 18n),
193
+ envMaxPerTask: 'MCP_MAX_PER_TASK_PANAL_WEI',
194
+ envDailyBudget: 'MCP_DAILY_BUDGET_PANAL_WEI',
195
+ });
196
+ return { deadlineHours: Number.isFinite(hours) && hours > 0 ? hours : 24, porMoneda };
128
197
  }
package/dist/server.d.ts CHANGED
@@ -13,8 +13,14 @@
13
13
  * MCP_PRIVATE_KEY=0x… wallet dedicada del CLIENTE, con fondos
14
14
  *
15
15
  * Topes (todos opcionales, con valores conservadores por defecto):
16
- * MCP_MAX_PER_TASK_WEI por encargo (default 1e18 = 1 MON/$PANAL)
17
- * MCP_DAILY_BUDGET_WEI por día UTC (default 5e18)
16
+ * MCP_MAX_PER_TASK_WEI por encargo, en MON (default 1e18)
17
+ * MCP_DAILY_BUDGET_WEI por día UTC, en MON (default 5e18)
18
+ * MCP_MAX_PER_TASK_PANAL_WEI por encargo, en $PANAL (default 1e18)
19
+ * MCP_DAILY_BUDGET_PANAL_WEI por día UTC, en $PANAL (default 5e18)
20
+ *
21
+ * Cada moneda lleva su cuenta. MON y $PANAL no valen lo mismo y no hay tipo de
22
+ * cambio entre ellos, así que un solo contador para los dos era una suma
23
+ * inventada: gastar en una agotaba el presupuesto de la otra.
18
24
  * MCP_TASK_DEADLINE_HOURS plazo de entrega (default 24)
19
25
  * MCP_SPEND_FILE dónde persistir el gasto del día
20
26
  *
package/dist/server.js CHANGED
@@ -13,8 +13,14 @@
13
13
  * MCP_PRIVATE_KEY=0x… wallet dedicada del CLIENTE, con fondos
14
14
  *
15
15
  * Topes (todos opcionales, con valores conservadores por defecto):
16
- * MCP_MAX_PER_TASK_WEI por encargo (default 1e18 = 1 MON/$PANAL)
17
- * MCP_DAILY_BUDGET_WEI por día UTC (default 5e18)
16
+ * MCP_MAX_PER_TASK_WEI por encargo, en MON (default 1e18)
17
+ * MCP_DAILY_BUDGET_WEI por día UTC, en MON (default 5e18)
18
+ * MCP_MAX_PER_TASK_PANAL_WEI por encargo, en $PANAL (default 1e18)
19
+ * MCP_DAILY_BUDGET_PANAL_WEI por día UTC, en $PANAL (default 5e18)
20
+ *
21
+ * Cada moneda lleva su cuenta. MON y $PANAL no valen lo mismo y no hay tipo de
22
+ * cambio entre ellos, así que un solo contador para los dos era una suma
23
+ * inventada: gastar en una agotaba el presupuesto de la otra.
18
24
  * MCP_TASK_DEADLINE_HOURS plazo de entrega (default 24)
19
25
  * MCP_SPEND_FILE dónde persistir el gasto del día
20
26
  *
@@ -29,7 +35,7 @@ import { fileURLToPath } from 'node:url';
29
35
  import { formatEther, isAddress, keccak256, parseEther, toBytes } from 'viem';
30
36
  import { privateKeyToAccount } from 'viem/accounts';
31
37
  import { NATIVE_CURRENCY, TaskStatus, createPanalClient, } from '@panal/sdk';
32
- import { QuoteBook, SpendLedger, limitsFromEnv } from './limits.js';
38
+ import { QuoteBook, SpendLedger, limitFor, limitsFromEnv } from './limits.js';
33
39
  import { briefSignMessage, expiraEn, fetchAgentLimits, fetchResultText, pushBrief, resultSignMessage, } from './fetch-result.js';
34
40
  const SERVER_NAME = 'panal';
35
41
  /**
@@ -91,6 +97,34 @@ function writesBlockedReason() {
91
97
  function symbolOf(currency) {
92
98
  return currency.toLowerCase() === NATIVE_CURRENCY.toLowerCase() ? 'MON' : '$PANAL';
93
99
  }
100
+ /**
101
+ * ¿Cabe este gasto en el presupuesto de SU moneda? Devuelve el motivo, o null.
102
+ *
103
+ * Cada moneda lleva su cuenta. Sumar MON y $PANAL en un solo número exigiría un
104
+ * tipo de cambio que nadie tiene, y el resultado era que tres consultas de
105
+ * $PANAL agotaban un tope puesto pensando en MON.
106
+ *
107
+ * Sin presupuesto para una moneda, se dice que no. La alternativa —tirar del
108
+ * tope de otra— es exactamente la conversión a ojo que este cambio elimina.
109
+ */
110
+ function revisarPresupuesto(currency, amount, symbol) {
111
+ const lim = limitFor(limits, currency);
112
+ if (!lim) {
113
+ return (`That agent charges in a token this server has no budget for (${currency}). ` +
114
+ 'Only MON and $PANAL have caps configured; there is no exchange rate to reuse another one.');
115
+ }
116
+ if (amount > lim.maxPerTaskWei) {
117
+ return (`That costs ${formatEther(amount)} ${symbol} and the per-item cap for ${symbol} is ` +
118
+ `${formatEther(lim.maxPerTaskWei)}. Raise ${lim.envMaxPerTask} if that is intended.`);
119
+ }
120
+ const spent = ledger.spentToday(currency);
121
+ if (spent + amount > lim.dailyBudgetWei) {
122
+ return (`That would blow today's ${symbol} budget: ${formatEther(spent)} of ` +
123
+ `${formatEther(lim.dailyBudgetWei)} spent and this costs ${formatEther(amount)} ${symbol}. ` +
124
+ `Raise ${lim.envDailyBudget} if that is intended.`);
125
+ }
126
+ return null;
127
+ }
94
128
  /** Días desde un timestamp en segundos. */
95
129
  function daysSince(ts) {
96
130
  return Math.max(0, Math.floor(Date.now() / 1000 - ts) / 86_400);
@@ -344,8 +378,16 @@ const WRITE_TOOLS = [
344
378
  if (blocked)
345
379
  return blocked;
346
380
  const address = account.address;
347
- const [balance, spent] = [await panal.publicClient.getBalance({ address }), ledger.spentToday()];
348
- const left = limits.dailyBudgetWei > spent ? limits.dailyBudgetWei - spent : 0n;
381
+ const balance = await panal.publicClient.getBalance({ address });
382
+ // Un presupuesto por moneda: MON y $PANAL no valen lo mismo y no hay
383
+ // tipo de cambio, así que se enseñan por separado o no se entienden.
384
+ const presupuestos = [...limits.porMoneda.entries()].map(([moneda, lim]) => {
385
+ const spent = ledger.spentToday(moneda);
386
+ const left = lim.dailyBudgetWei > spent ? lim.dailyBudgetWei - spent : 0n;
387
+ const sym = symbolOf(moneda);
388
+ return (` ${sym}: per job ${formatEther(lim.maxPerTaskWei)} · today ${formatEther(spent)} of ` +
389
+ `${formatEther(lim.dailyBudgetWei)} spent · ${formatEther(left)} left`);
390
+ });
349
391
  // Lo acreditado en el escrow no aparece en el balance de la wallet: es
350
392
  // pull payment, así que un reembolso se queda ahí quieto hasta que se
351
393
  // reclama. Sin decirlo, el dinero devuelto es indistinguible del perdido.
@@ -353,8 +395,8 @@ const WRITE_TOOLS = [
353
395
  return [
354
396
  `Wallet: ${address}`,
355
397
  `Balance: ${formatEther(balance)} MON`,
356
- `Per-job cap: ${formatEther(limits.maxPerTaskWei)}`,
357
- `Today's budget: spent ${formatEther(spent)} of ${formatEther(limits.dailyBudgetWei)} · ${formatEther(left)} left`,
398
+ 'Caps and budgets (each currency is counted on its own):',
399
+ ...presupuestos,
358
400
  pending > 0n
359
401
  ? `Waiting in the escrow: ${formatEther(pending)} MON — collect it with panal_withdraw`
360
402
  : null,
@@ -390,15 +432,9 @@ const WRITE_TOOLS = [
390
432
  return `${agent.metadata.name || address} is delisted and does not take jobs.`;
391
433
  const amount = agent.pricePerTask;
392
434
  const symbol = symbolOf(agent.currency);
393
- if (amount > limits.maxPerTaskWei) {
394
- return (`That agent charges ${formatEther(amount)} ${symbol} and this server per-job cap is ` +
395
- `${formatEther(limits.maxPerTaskWei)}. It cannot be hired unless the person raises MCP_MAX_PER_TASK_WEI.`);
396
- }
397
- const spent = ledger.spentToday();
398
- if (spent + amount > limits.dailyBudgetWei) {
399
- return (`That would blow today's budget: ${formatEther(spent)} of ${formatEther(limits.dailyBudgetWei)} spent ` +
400
- `and this costs ${formatEther(amount)} ${symbol}.`);
401
- }
435
+ const negativa = revisarPresupuesto(agent.currency, amount, symbol);
436
+ if (negativa)
437
+ return negativa;
402
438
  // El tope del brief se pregunta ANTES de presupuestar. Si no cabe, el
403
439
  // agente lo rechazaría con un 400 al recibirlo — pero eso ocurre ya con
404
440
  // el pago bloqueado, porque el encargo se entrega después de crear la
@@ -476,22 +512,17 @@ const WRITE_TOOLS = [
476
512
  const quote = redeemed.quote;
477
513
  // Los mismos topes que contratar. Una consulta es barata, pero nada
478
514
  // impide encadenar mil: el presupuesto del día es lo que lo impide.
479
- if (quote.amount > limits.maxPerTaskWei) {
480
- return (`That question costs ${formatEther(quote.amount)} ${quote.symbol} and this server per-item cap is ` +
481
- `${formatEther(limits.maxPerTaskWei)}. Raise MCP_MAX_PER_TASK_WEI if that is intended.`);
482
- }
483
- const spent = ledger.spentToday();
484
- if (spent + quote.amount > limits.dailyBudgetWei) {
485
- return (`That would blow today's budget: ${formatEther(spent)} of ${formatEther(limits.dailyBudgetWei)} spent ` +
486
- `and this costs ${formatEther(quote.amount)} ${quote.symbol}.`);
487
- }
515
+ const negativa = revisarPresupuesto(quote.currency, quote.amount, quote.symbol);
516
+ if (negativa)
517
+ return negativa;
488
518
  try {
489
519
  // `maxSpend` va atado al importe del presupuesto que la persona
490
520
  // aprobó, no al tope del servidor: si el agente sube el precio entre
491
521
  // el presupuesto y el sí, la firma no se produce.
492
522
  const res = await panal.askAgent(quote.worker, quote.brief, { maxSpend: quote.amount });
493
- // Se registra lo REALMENTE pagado, que es lo que devuelve el SDK.
494
- ledger.record(res.paid);
523
+ // Se registra lo REALMENTE pagado, y en SU moneda: un gasto anotado en
524
+ // el contador equivocado agota un presupuesto que nadie tocó.
525
+ ledger.record(quote.currency, res.paid);
495
526
  log(`consulta pagada a ${quote.worker}: ${formatEther(res.paid)} ${quote.symbol}`);
496
527
  return [
497
528
  `Paid ${formatEther(res.paid)} ${quote.symbol} to ${quote.agentName}.`,
@@ -541,14 +572,13 @@ const WRITE_TOOLS = [
541
572
  const quote = redeemed.quote;
542
573
  // Los topes se re-evalúan al contratar: entre el presupuesto y el "sí"
543
574
  // pueden haber pasado minutos y otros encargos.
544
- const spent = ledger.spentToday();
545
- if (spent + quote.amount > limits.dailyBudgetWei) {
546
- return `Today's budget ran out in the meantime (${formatEther(spent)} of ${formatEther(limits.dailyBudgetWei)}).`;
547
- }
575
+ const negativa = revisarPresupuesto(quote.currency, quote.amount, quote.symbol);
576
+ if (negativa)
577
+ return negativa;
548
578
  const deadline = BigInt(Math.floor(Date.now() / 1000) + limits.deadlineHours * 3600);
549
579
  try {
550
580
  const result = await panal.hire({ agent: quote.worker, brief: quote.brief, deadline });
551
- ledger.record(result.amount);
581
+ ledger.record(quote.currency, result.amount);
552
582
  log(`contratada #${result.taskId} a ${quote.worker} por ${formatEther(result.amount)} ${quote.symbol}`);
553
583
  // Contratar sin entregar el encargo deja la tarea a medias: el pago
554
584
  // bloqueado y el agente esperando un texto que nadie le manda. Se hace
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panal-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Servidor MCP de Panal: busca y contrata agentes de IA on-chain desde Claude",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "build": "tsc -p tsconfig.json",
42
42
  "typecheck": "tsc -p tsconfig.json --noEmit",
43
43
  "start": "tsx src/server.ts",
44
- "test": "tsx test/mcp.test.ts",
44
+ "test": "tsx test/mcp.test.ts && tsx test/limites.test.ts && tsx test/monedas.test.ts",
45
45
  "test:limites": "tsx test/limites.test.ts"
46
46
  }
47
47
  }