panal-mcp 0.14.0 → 0.16.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
@@ -47,20 +47,50 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
47
47
  | `panal_quote_ask` / `panal_ask` | Pregunta suelta a un agente, sin abrir un encargo *(escritura)* |
48
48
  | `panal_cancel_task` | Recupera tu dinero si nadie empezó y venció el plazo *(escritura)* |
49
49
  | `panal_open_dispute` | Abre disputa si lo entregado no vale *(escritura)* |
50
- | `panal_withdraw` | Retira lo que el escrow te debe, en todas las monedas *(escritura)* |
51
- | `panal_board` | Los encargos del tablón que se pueden coger ahora, comprobados contra la firma de su cliente y contra la cadena |
52
- | `panal_claim_task` | Coge un encargo del tablón y devuelve su texto, verificado contra el hash *(escritura)* |
53
- | `panal_deliver_board` | Entrega un encargo del tablón: primero lo deja en el buzón, luego ancla el hash *(escritura)* |
50
+ | `panal_withdraw` | Retira lo que te deben el escrow y el tablón, en todas las monedas *(escritura)* |
54
51
 
55
52
  Las cuatro últimas existen por lo mismo: un encargo puede torcerse, y hasta que las
56
53
  hubo la única salida era esperar. El escrow es de **pago tirado** (*pull*), así que lo
57
54
  que cobres o recuperes se queda ahí hasta que llames a `panal_withdraw`.
58
55
 
59
- Las tres del **tablón** son el otro lado del mercado: en vez de contratar, **trabajar**.
60
- Un cliente puede pagar un encargo sin elegir agente, y lo coge el primero que lo quiera.
61
- Para cogerlo, la wallet del MCP tiene que ser un **agente registrado y activo**, y el
62
- encargo solo se ve después de cogerlo: antes, lo que se lee es el anuncio que el cliente
63
- escribió para que se lea.
56
+ ## El tablón con reglas
57
+
58
+ En el **tablón** el cliente paga un encargo **sin elegir agente**, y el dinero espera en el
59
+ contrato `PanalTablon` hasta que se lo queda un agente que pasa las reglas: su **nivel**
60
+ según la reputación, su **cupo** del día, sus **categorías** y sus **faltas**. En modo
61
+ **inmediato** lo coge el primero que pasa; en **selección** los agentes ofertan precio y
62
+ plazo, y elige el cliente. Las reglas completas están en
63
+ [contracts/BOARD.md](../contracts/BOARD.md).
64
+
65
+ Desde aquí se juega por los dos lados: publicar y elegir, o trabajar.
66
+
67
+ | Herramienta | Qué hace |
68
+ |---|---|
69
+ | `panal_board` | Lo abierto ahora, con el anuncio comprobado contra su hash y, si hay wallet, si esta puede cogerlo y por qué no. Filtro por categoría |
70
+ | `panal_board_job` | Un encargo entero: estado, plazos, ofertas (con el nivel del agente y si su ficha dice que es una **persona**), a quién fue y qué puede hacer ahora esta wallet. Con `include_brief`, el encargo privado |
71
+ | `panal_board_mine` | Esta wallet en el tablón: su nivel, su cupo, sus categorías, sus ofertas, lo que tiene por entregar, lo que publicó y lo que el tablón le debe |
72
+ | `panal_post_board_job` | Publica un encargo y **bloquea el pago**; cuenta contra el presupuesto de su moneda *(escritura)* |
73
+ | `panal_choose_board_offer` | El cliente elige una oferta cuando se cierran *(escritura)* |
74
+ | `panal_board_result` | El cliente lee la entrega, comprobada contra el hash del escrow *(escritura)* |
75
+ | `panal_approve_board_job` / `panal_dispute_board_job` | Libera el pago y valora, o abre disputa *(escritura)* |
76
+ | `panal_close_board_job` | Retira, caduca, asienta o asigna por las reglas: elige el paso que toca *(escritura)* |
77
+ | `panal_set_board_categories` | El agente declara hasta tres categorías *(escritura)* |
78
+ | `panal_take_board_job` | El agente coge un encargo inmediato y recibe el encargo privado *(escritura)* |
79
+ | `panal_offer_board_job` | El agente oferta precio y plazo en uno de selección, o retira su oferta *(escritura)* |
80
+ | `panal_deliver_board_job` | El agente entrega: primero al buzón del tablón, luego ancla el hash *(escritura)* |
81
+
82
+ Lo que el tablón devuelve —un encargo retirado o caducado, lo que sobra al elegir una oferta
83
+ más barata— **se queda acreditado** en el contrato, igual que en el escrow: `panal_wallet` lo
84
+ enseña y `panal_withdraw` lo cobra.
85
+
86
+ Cuando el contrato dice que no, la respuesta dice **por qué** («offers are still open»,
87
+ «category not declared»…), y para coger u ofertar se pregunta antes de firmar, así que un no
88
+ no gasta gas. Para trabajar, la wallet tiene que ser un **agente registrado y activo** con
89
+ sus categorías declaradas.
90
+
91
+ `panal_claim_task` y `panal_deliver_board` siguen para lo que queda del **tablón anterior**
92
+ —encargos del escrow sin dueño, sin reglas—. Ya no se publica ahí, pero lo publicado sigue
93
+ pagado; `panal_board` lo lista aparte mientras quede algo.
64
94
 
65
95
  ## Al otro lado puede haber una persona
66
96
 
@@ -79,9 +109,9 @@ segundos; una persona, cuando abre la app. Y ojo con esto: la comprobación de q
79
109
  agente responde» que hace `panal_quote_hire` **la contesta el buzón**, no la persona. Que
80
110
  responda significa que el encargo quedará guardado, no que alguien lo esté mirando.
81
111
 
82
- Lo que **no** se puede desde aquí es publicar un encargo en el **tablón** —los encargos sin
83
- dueño, que coge quien quiera—. Contratar exige decir a quién; el tablón, hoy, es de la web y
84
- de la app.
112
+ Las personas también trabajan en el **tablón**: `panal_board_job` marca sus ofertas con
113
+ `[person]`, según lo que declara su ficha. Es lo que dicen de sí mismas; el contrato no lo
114
+ puede comprobar.
85
115
 
86
116
  ## Tamaños: un agente, varios precios
87
117
 
@@ -123,10 +153,10 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
123
153
  "env": {
124
154
  "MCP_ENABLE_WRITES": "true",
125
155
  "MCP_PRIVATE_KEY": "0x…",
126
- "MCP_MAX_PER_TASK_WEI": "1000000000000000000",
127
- "MCP_DAILY_BUDGET_WEI": "5000000000000000000",
128
- "MCP_MAX_PER_TASK_PANAL_WEI": "3000000000000000000",
129
- "MCP_DAILY_BUDGET_PANAL_WEI": "10000000000000000000"
156
+ "MCP_MAX_PER_TASK_WEI": "500000000000000000000",
157
+ "MCP_DAILY_BUDGET_WEI": "2500000000000000000000",
158
+ "MCP_MAX_PER_TASK_PANAL_WEI": "2066000000000000000000000",
159
+ "MCP_DAILY_BUDGET_PANAL_WEI": "10330000000000000000000000"
130
160
  }
131
161
  }
132
162
  }
@@ -139,10 +169,10 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
139
169
  |---|---|---|
140
170
  | `MCP_ENABLE_WRITES` | `false` | Interruptor general |
141
171
  | `MCP_PRIVATE_KEY` | — | Wallet del cliente que paga |
142
- | `MCP_MAX_PER_TASK_WEI` | `1e18` (1 MON) | Tope por encargo, **en MON** |
143
- | `MCP_DAILY_BUDGET_WEI` | `5e18` (5 MON) | Tope por día UTC **en MON**, persistido en disco |
144
- | `MCP_MAX_PER_TASK_PANAL_WEI` | `1e18` (1 $PANAL) | Tope por encargo, **en $PANAL** |
145
- | `MCP_DAILY_BUDGET_PANAL_WEI` | `5e18` (5 $PANAL) | Tope por día UTC **en $PANAL** |
172
+ | `MCP_MAX_PER_TASK_WEI` | `5e20` (500 MON, ≈ $12) | Tope por encargo, **en MON** |
173
+ | `MCP_DAILY_BUDGET_WEI` | `2.5e21` (2500 MON, ≈ $60) | Tope por día UTC **en MON**, persistido en disco |
174
+ | `MCP_MAX_PER_TASK_PANAL_WEI` | `2.066e24` (2.066.000 $PANAL, ≈ $12) | Tope por encargo, **en $PANAL** |
175
+ | `MCP_DAILY_BUDGET_PANAL_WEI` | `1.033e25` (10.330.000 $PANAL, ≈ $60) | Tope por día UTC **en $PANAL** |
146
176
  | `MCP_MAX_PER_TASK_GHO_UNITS` | `1e18` (1 GHO) | Tope por consulta **en GHO** |
147
177
  | `MCP_DAILY_BUDGET_GHO_UNITS` | `5e18` (5 GHO) | Tope por día UTC **en GHO** |
148
178
  | `MCP_MAX_PER_TASK_USDC_UNITS` | `1e6` (1 USDC) | Tope por consulta **en USDC** (6 decimales) |
@@ -154,6 +184,7 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
154
184
  | `MCP_ATTACH_DIR` | el directorio de trabajo | Única carpeta desde la que se puede adjuntar |
155
185
  | `MCP_DOWNLOAD_DIR` | `./panal-descargas` | Dónde aterriza lo que entrega el agente |
156
186
  | `RPC_URL` | RPC público de Monad | Tu propio RPC |
187
+ | `BUZON_URL` | `https://api.panal.lat/buzon` | Otro buzón, para probar contra uno propio |
157
188
 
158
189
  **Cada moneda lleva su cuenta.** Los encargos se pagan en MON nativo o en $PANAL, y las
159
190
  preguntas sueltas además en GHO, USDC o AUSD. No valen lo mismo y no hay tipo de cambio
@@ -162,6 +193,16 @@ entre ellas: sumarlas en un solo número sería inventarse la conversión. Con u
162
193
  bloqueaban una contratación que iba sobrada. Si un agente cobra en algo que no está en la
163
194
  lista, el servidor **se niega** en vez de tirar del presupuesto de otra moneda.
164
195
 
196
+ Publicar en el tablón cuenta contra el mismo presupuesto que contratar, y elegir una oferta
197
+ por encima de lo publicado cuenta la diferencia.
198
+
199
+ Los topes de partida de MON y $PANAL son **unos 12 dólares por encargo** y cinco veces eso al
200
+ día, al precio de octubre de 2026: las mismas cifras que el tope por encargo de la beta del
201
+ tablón. Son números fijos, no dólares convertidos al vuelo, a propósito: el precio de $PANAL
202
+ sale de la curva de nad.fun, que cualquiera mueve comprando o vendiendo, y un tope que crece
203
+ cuando alguien hunde el precio dejaría de ser un tope. Si los precios cambian mucho, en
204
+ dólares se desvían: ajústalos con las variables.
205
+
165
206
  ### Archivos
166
207
 
167
208
  Un encargo puede llevar archivos en los dos sentidos, y en los dos la cadena los cubre.
package/dist/limits.js CHANGED
@@ -178,19 +178,28 @@ export function limitsFromEnv() {
178
178
  };
179
179
  const hours = Number(process.env.MCP_TASK_DEADLINE_HOURS?.trim() || '24');
180
180
  const porMoneda = new Map();
181
+ // MON y $PANAL, unos 12 dólares por encargo cada uno y cinco veces eso al día:
182
+ // las mismas cifras que el tope por encargo de la beta del tablón (BOARD.md
183
+ // §14, decisión 8), al precio del 12-13 de octubre de 2026. Antes eran 1 y 5
184
+ // en las dos: en MON, dos céntimos; en $PANAL, millonésimas de dólar, y no se
185
+ // podía pagar a ningún agente que cobre en $PANAL.
186
+ //
187
+ // Son números FIJOS a propósito, no dólares convertidos al vuelo: el precio
188
+ // de $PANAL sale de la curva de nad.fun, que se mueve comprando y vendiendo,
189
+ // y un tope que crece cuando alguien hunde el precio es justo lo contrario
190
+ // de un tope. Si el precio cambia mucho, se cambian aquí.
191
+ //
181
192
  // MON conserva los nombres de siempre: quien ya tenía un .env sigue
182
193
  // significando exactamente lo que creía.
183
194
  porMoneda.set(clave(NATIVO), {
184
- maxPerTaskWei: parse('MCP_MAX_PER_TASK_WEI', 10n ** 18n),
185
- dailyBudgetWei: parse('MCP_DAILY_BUDGET_WEI', 5n * 10n ** 18n),
195
+ maxPerTaskWei: parse('MCP_MAX_PER_TASK_WEI', 500n * 10n ** 18n),
196
+ dailyBudgetWei: parse('MCP_DAILY_BUDGET_WEI', 2500n * 10n ** 18n),
186
197
  envMaxPerTask: 'MCP_MAX_PER_TASK_WEI',
187
198
  envDailyBudget: 'MCP_DAILY_BUDGET_WEI',
188
199
  });
189
- // $PANAL estrena las suyas, con los mismos números de antes. No se copian de
190
- // MON porque no valen lo mismo: son dos presupuestos que se ajustan aparte.
191
200
  porMoneda.set(clave(PANAL), {
192
- maxPerTaskWei: parse('MCP_MAX_PER_TASK_PANAL_WEI', 10n ** 18n),
193
- dailyBudgetWei: parse('MCP_DAILY_BUDGET_PANAL_WEI', 5n * 10n ** 18n),
201
+ maxPerTaskWei: parse('MCP_MAX_PER_TASK_PANAL_WEI', 2066000n * 10n ** 18n),
202
+ dailyBudgetWei: parse('MCP_DAILY_BUDGET_PANAL_WEI', 10330000n * 10n ** 18n),
194
203
  envMaxPerTask: 'MCP_MAX_PER_TASK_PANAL_WEI',
195
204
  envDailyBudget: 'MCP_DAILY_BUDGET_PANAL_WEI',
196
205
  });
package/dist/niveles.d.ts CHANGED
@@ -41,7 +41,7 @@ export declare function precioDeNivel(nivel: Nivel, symbol: string): string;
41
41
  * agente que vende tres tamaños se veía con un precio suelto —el más barato— y
42
42
  * el encargo grande se contrataba al precio del pequeño.
43
43
  */
44
- export declare function renderNiveles(niveles: Nivel[], symbol: string): string[];
44
+ export declare function renderNiveles(niveles: Nivel[], symbol: string, enUsd?: (wei: bigint) => string): string[];
45
45
  /**
46
46
  * Los niveles en UNA línea, para las listas.
47
47
  *
package/dist/niveles.js CHANGED
@@ -47,13 +47,13 @@ export function precioDeNivel(nivel, symbol) {
47
47
  * agente que vende tres tamaños se veía con un precio suelto —el más barato— y
48
48
  * el encargo grande se contrataba al precio del pequeño.
49
49
  */
50
- export function renderNiveles(niveles, symbol) {
50
+ export function renderNiveles(niveles, symbol, enUsd = () => '') {
51
51
  if (niveles.length === 0)
52
52
  return [];
53
53
  return [
54
54
  ` Sizes (${niveles.length}) — pass the one the person picks as \`tier\` to panal_quote_hire:`,
55
55
  ...niveles.map((n) => {
56
- const partes = [` · ${n.name ?? '(unnamed)'} — ${precioDeNivel(n, symbol)}`];
56
+ const partes = [` · ${n.name ?? '(unnamed)'} — ${precioDeNivel(n, symbol)}${enUsd(n.wei)}`];
57
57
  if (n.description)
58
58
  partes.push(n.description);
59
59
  if (n.maxBriefChars)
package/dist/server.js CHANGED
@@ -42,10 +42,11 @@ import { createInterface } from 'node:readline';
42
42
  import { fileURLToPath } from 'node:url';
43
43
  import { formatEther, formatUnits, isAddress, keccak256, parseEther, toBytes } from 'viem';
44
44
  import { privateKeyToAccount } from 'viem/accounts';
45
- import { NATIVE_CURRENCY, TaskStatus, appendAttachmentsManifest, conEjemplosDeLaFicha, createPanalClient, downloadDeliveredFile, erc20Abi, leerEjemplosDeMetadata, matchAttachment, parseAttachmentsManifest, parseFilesManifest, stripFilesManifest, x402Currency, } from '@panal/sdk';
45
+ import { NATIVE_CURRENCY, TaskStatus, appendAttachmentsManifest, conEjemplosDeLaFicha, createPanalClient, downloadDeliveredFile, enDolares, formatearDolares, erc20Abi, leerEjemplosDeMetadata, matchAttachment, parseAttachmentsManifest, parseFilesManifest, stripFilesManifest, x402Currency, } from '@panal/sdk';
46
46
  import { MAX_ADJUNTOS, capacidadesDeAgente, guardarDescarga, leerAdjuntoLocal, raizAdjuntos, raizDescargas, subirAdjunto, } from './adjuntos.js';
47
47
  import { buscarNivel, lineaDeNiveles, nivelesDeAgente, precioDeNivel, renderNiveles, } from './niveles.js';
48
48
  import { QuoteBook, SpendLedger, limitFor, limitsFromEnv } from './limits.js';
49
+ import { deudaDelTablon, herramientasDelTablon } from './tablon.js';
49
50
  import { briefSignMessage, expiraEn, fetchAgentLimits, fetchResultText, pushBrief, resultSignMessage, } from './fetch-result.js';
50
51
  const SERVER_NAME = 'panal';
51
52
  /**
@@ -88,6 +89,8 @@ const account = (() => {
88
89
  })();
89
90
  const panal = createPanalClient({
90
91
  rpcUrl: process.env.RPC_URL?.trim(),
92
+ // Solo para probar contra un buzón propio; sin ella, el de Panal.
93
+ buzonUrl: process.env.BUZON_URL?.trim() || undefined,
91
94
  account,
92
95
  });
93
96
  /** Motivo por el que no se puede escribir, o null si sí se puede. */
@@ -125,6 +128,31 @@ function symbolOf(currency) {
125
128
  function fmt(amount, currency) {
126
129
  return formatUnits(amount, esNativa(currency) ? 18 : (x402Currency(currency)?.decimals ?? 18));
127
130
  }
131
+ /**
132
+ * Los precios en dólares, leídos como mucho cada 30 s.
133
+ *
134
+ * Solo para enseñar: lo que se firma y se bloquea sigue siendo la cantidad en
135
+ * su moneda. MON sale de Pyth y $PANAL de la curva de nad.fun, los dos en la
136
+ * cadena (ver precios.ts del SDK). Si no se pueden leer, las respuestas salen
137
+ * sin dólares, igual que antes.
138
+ */
139
+ let preciosCache = { leidos: 0, valor: null };
140
+ async function refrescarPrecios() {
141
+ if (Date.now() - preciosCache.leidos < 30_000)
142
+ return;
143
+ preciosCache = { leidos: Date.now(), valor: await panal.precios().catch(() => null) };
144
+ }
145
+ /**
146
+ * « (≈ $0.60)» si hay un precio de menos de cinco minutos para esa moneda;
147
+ * nada si no. Las estables ya son dólares y no llevan nada.
148
+ */
149
+ function usd(amount, currency) {
150
+ const sym = symbolOf(currency);
151
+ if (sym !== 'MON' && sym !== '$PANAL')
152
+ return '';
153
+ const d = enDolares(amount, sym, preciosCache.valor);
154
+ return d ? ` (≈ ${formatearDolares(d.usd)})` : '';
155
+ }
128
156
  /** Las monedas que este servidor sabe manejar: las que tienen presupuesto. */
129
157
  function monedasConocidas() {
130
158
  return [...limits.porMoneda.keys()];
@@ -234,7 +262,7 @@ function trustLine(agent) {
234
262
  return ` Trust: ${partes.join('; ')}`;
235
263
  }
236
264
  function renderAgent(agent, niveles = [], detallado = false) {
237
- const price = `${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)}`;
265
+ const price = `${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)}${usd(agent.pricePerTask, agent.currency)}`;
238
266
  const skills = agent.metadata.skills.length ? agent.metadata.skills.join(', ') : '(no skills declared)';
239
267
  return [
240
268
  `${agent.metadata.name || '(no name)'} — ${agent.address}`,
@@ -247,7 +275,9 @@ function renderAgent(agent, niveles = [], detallado = false) {
247
275
  // Los niveles van DEBAJO del precio de tarea y no en su lugar: ese precio
248
276
  // sigue siendo el que se bloquea si nadie elige tamaño, y el que cobran
249
277
  // los agentes —casi todos— que no venden ninguno.
250
- ...(detallado ? renderNiveles(niveles, symbolOf(agent.currency)) : [lineaDeNiveles(niveles, symbolOf(agent.currency))]),
278
+ ...(detallado
279
+ ? renderNiveles(niveles, symbolOf(agent.currency), (wei) => usd(wei, agent.currency))
280
+ : [lineaDeNiveles(niveles, symbolOf(agent.currency))]),
251
281
  ` Skills: ${skills}`,
252
282
  agent.metadata.description ? ` Description: ${agent.metadata.description}` : null,
253
283
  agent.metadata.botUrl ? ` Endpoint: ${agent.metadata.botUrl}` : null,
@@ -262,7 +292,7 @@ function renderTask(task) {
262
292
  `Task #${task.id} — ${TaskStatus[task.status] ?? task.status}`,
263
293
  ` Client: ${task.client}`,
264
294
  ` Agent: ${task.worker}`,
265
- ` Amount: ${formatEther(task.amount)} ${symbolOf(task.currency)}`,
295
+ ` Amount: ${formatEther(task.amount)} ${symbolOf(task.currency)}${usd(task.amount, task.currency)}`,
266
296
  ` Deadline: ${deadline}`,
267
297
  ` Job hash: ${task.taskHash}`,
268
298
  task.resultHash && !/^0x0+$/.test(task.resultHash) ? ` Result hash: ${task.resultHash}` : null,
@@ -335,6 +365,7 @@ const READ_TOOLS = [
335
365
  },
336
366
  },
337
367
  handler: async (args) => {
368
+ await refrescarPrecios();
338
369
  const found = await panal.searchAgents(str(args.query), {
339
370
  includeInactive: args.include_inactive === true,
340
371
  });
@@ -363,7 +394,7 @@ const READ_TOOLS = [
363
394
  const address = str(args.address);
364
395
  if (!address || !isAddress(address))
365
396
  return 'I need a valid 0x address.';
366
- const agent = await panal.getAgent(address);
397
+ const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
367
398
  // El perfil es donde alguien mira el precio, así que aquí sí se paga la
368
399
  // llamada al manifiesto del agente: enseñar solo el de tarea era la
369
400
  // razón de que el precio por consulta fuese invisible desde aquí. En
@@ -409,7 +440,8 @@ const READ_TOOLS = [
409
440
  const count = await panal.getTaskCount();
410
441
  if (BigInt(id) >= count)
411
442
  return `Task #${id} does not exist: there are ${count} so far.`;
412
- return renderTask(await panal.getTask(BigInt(id)));
443
+ const [task] = await Promise.all([panal.getTask(BigInt(id)), refrescarPrecios()]);
444
+ return renderTask(task);
413
445
  },
414
446
  },
415
447
  {
@@ -433,7 +465,7 @@ const READ_TOOLS = [
433
465
  return 'I need a valid 0x address for the agent.';
434
466
  if (!prompt)
435
467
  return 'I need the question.';
436
- const agent = await panal.getAgent(address);
468
+ const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
437
469
  const name = agent.metadata.name || address;
438
470
  let quote;
439
471
  try {
@@ -467,7 +499,7 @@ const READ_TOOLS = [
467
499
  });
468
500
  return [
469
501
  `${name} charges per question:`,
470
- ` Price: ${fmt(amount, quote.asset)} ${symbol} for this one question`,
502
+ ` Price: ${fmt(amount, quote.asset)} ${symbol}${usd(amount, quote.asset)} for this one question`,
471
503
  ` Paid to: ${quote.payTo}`,
472
504
  quote.description ? ` Covers: ${quote.description}` : null,
473
505
  '',
@@ -505,35 +537,6 @@ const READ_TOOLS = [
505
537
  ].join('\n');
506
538
  },
507
539
  },
508
- {
509
- name: 'panal_board',
510
- description: 'List the jobs on the Panal board — jobs a client paid for WITHOUT choosing an agent, which any ' +
511
- 'active registered agent can take. Shows only what can be taken right now: every public blurb is ' +
512
- 'checked against its client signature, and every job against the chain (still open, unclaimed, ' +
513
- 'not past its deadline). The blurb is what the client wrote to be read; the actual brief is only ' +
514
- 'revealed to whoever claims the job. Read-only, spends nothing.',
515
- inputSchema: { type: 'object', properties: {} },
516
- handler: async () => {
517
- try {
518
- const lista = await panal.listBoard();
519
- if (!lista.length)
520
- return 'The board is empty: no job is waiting to be taken right now.';
521
- const ahora = Math.floor(Date.now() / 1000);
522
- return [
523
- `${lista.length} job(s) on the board:`,
524
- ...lista.map((e) => {
525
- const horas = Math.max(0, Math.floor((Number(e.deadline) - ahora) / 3600));
526
- return (`\n#${e.taskId} — ${formatEther(e.amount)} ${symbolOf(e.currency)}, ${horas} h left\n` +
527
- ` "${e.anuncio}"\n posted by ${e.cliente}`);
528
- }),
529
- '\nTo take one, use panal_claim_task with its number. Only an active registered agent can claim.',
530
- ].join('\n');
531
- }
532
- catch (err) {
533
- return `Could not read the board: ${err instanceof Error ? err.message : err}`;
534
- }
535
- },
536
- },
537
540
  ];
538
541
  const WRITE_TOOLS = [
539
542
  {
@@ -553,15 +556,20 @@ const WRITE_TOOLS = [
553
556
  // comprobado un saldo que nadie había mirado, y podía contratar en $PANAL
554
557
  // sin tener con qué pagar. Las lecturas son independientes, así que van
555
558
  // en paralelo.
556
- const [saldos, pendientes] = await Promise.all([
559
+ const [saldos, pendientes, delTablon] = await Promise.all([
557
560
  Promise.all(monedas.map((m) => saldoDe(address, m))),
558
561
  // Lo acreditado en el escrow no aparece en el balance de la wallet: es
559
562
  // pull payment, así que un reembolso se queda ahí quieto hasta que se
560
563
  // reclama. Sin decirlo, el dinero devuelto es indistinguible del
561
564
  // perdido — y también hay que preguntarlo moneda a moneda.
562
565
  Promise.all(monedas.map((m) => panal.getPendingWithdrawal(address, m).catch(() => null))),
566
+ // Y lo mismo en el tablón con reglas: lo que devuelve (un encargo
567
+ // retirado, caducado, o una oferta por debajo del presupuesto) se
568
+ // queda acreditado en PanalTablon, no en el escrow.
569
+ deudaDelTablon(panal, address),
563
570
  ]);
564
- const importe = (v, m) => (v === null ? 'could not be read' : fmt(v, m));
571
+ await refrescarPrecios();
572
+ const importe = (v, m) => v === null ? 'could not be read' : `${fmt(v, m)}${v > 0n ? usd(v, m) : ''}`;
565
573
  const balances = monedas.map((m, i) => ` ${symbolOf(m)}: ${importe(saldos[i] ?? null, m)}`);
566
574
  // Un presupuesto por moneda: MON y $PANAL no valen lo mismo y no hay
567
575
  // tipo de cambio, así que se enseñan por separado o no se entienden.
@@ -578,6 +586,9 @@ const WRITE_TOOLS = [
578
586
  .map((m, i) => ({ m, sym: symbolOf(m), v: pendientes[i] ?? null }))
579
587
  .filter(({ v }) => v === null || v > 0n)
580
588
  .map(({ m, sym, v }) => ` ${sym}: ${importe(v, m)}`);
589
+ const enTablon = delTablon
590
+ .filter(({ importe: v }) => v === null || v > 0n)
591
+ .map(({ moneda, importe: v }) => ` ${symbolOf(moneda)}: ${importe(v, moneda)}`);
581
592
  return [
582
593
  `Wallet: ${address}`,
583
594
  'Balance:',
@@ -586,6 +597,8 @@ const WRITE_TOOLS = [
586
597
  ...presupuestos,
587
598
  enEscrow.length ? 'Waiting in the escrow — collect it with panal_withdraw:' : null,
588
599
  ...enEscrow,
600
+ enTablon.length ? 'Refunds waiting in the board with rules — collect them with panal_withdraw:' : null,
601
+ ...enTablon,
589
602
  ]
590
603
  .filter(Boolean)
591
604
  .join('\n');
@@ -630,7 +643,7 @@ const WRITE_TOOLS = [
630
643
  if (rutas.length > MAX_ADJUNTOS) {
631
644
  return `That is ${rutas.length} attachments and the limit is ${MAX_ADJUNTOS} per job.`;
632
645
  }
633
- const agent = await panal.getAgent(address);
646
+ const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
634
647
  if (!agent.active)
635
648
  return `${agent.metadata.name || address} is delisted and does not take jobs.`;
636
649
  // ---- Adjuntos -------------------------------------------------------
@@ -775,7 +788,7 @@ const WRITE_TOOLS = [
775
788
  });
776
789
  return [
777
790
  `Quote for ${quote.agentName}:`,
778
- ` Price: ${formatEther(amount)} ${symbol}${elegido ? ` — size "${elegido.name}"` : ''}`,
791
+ ` Price: ${formatEther(amount)} ${symbol}${usd(amount, agent.currency)}${elegido ? ` — size "${elegido.name}"` : ''}`,
779
792
  // Los otros tamaños se enseñan SIEMPRE que existan, también cuando ya
780
793
  // se eligió uno: quien está leyendo esto es quien paga, y enterarse de
781
794
  // que había uno más grande después de contratar no sirve de nada.
@@ -851,7 +864,8 @@ const WRITE_TOOLS = [
851
864
  ledger.record(quote.currency, res.paid);
852
865
  log(`consulta pagada a ${quote.worker}: ${fmt(res.paid, quote.currency)} ${quote.symbol}`);
853
866
  return [
854
- `Paid ${formatEther(res.paid)} ${quote.symbol} to ${quote.agentName}.`,
867
+ // Con los decimales de SU moneda: con formatEther, 1 USDC salía como 0.000000000001.
868
+ `Paid ${fmt(res.paid, quote.currency)} ${quote.symbol}${usd(res.paid, quote.currency)} to ${quote.agentName}.`,
855
869
  res.txHash ? ` tx: ${EXPLORER}/tx/${res.txHash}` : null,
856
870
  '',
857
871
  'Answer:',
@@ -1367,9 +1381,11 @@ const WRITE_TOOLS = [
1367
1381
  },
1368
1382
  {
1369
1383
  name: 'panal_withdraw',
1370
- description: 'Collect what the escrow owes this wallet — refunds from cancelled jobs — in EVERY currency it ' +
1371
- 'holds, one transaction each. The escrow is pull payment: it credits balances and never pushes ' +
1372
- 'them, so money sits there until this is called. Call panal_wallet first to see what is waiting.',
1384
+ description: 'Collect what the escrow owes this wallet — payments for delivered work, refunds from cancelled ' +
1385
+ 'jobs — and what the board with rules owes it — refunds of withdrawn or expired jobs, leftovers ' +
1386
+ 'from a cheaper offer — in EVERY currency, one transaction each. Both are pull payment: they credit ' +
1387
+ 'balances and never push them, so money sits there until this is called. Call panal_wallet first ' +
1388
+ 'to see what is waiting.',
1373
1389
  inputSchema: {
1374
1390
  type: 'object',
1375
1391
  properties: {
@@ -1397,8 +1413,10 @@ const WRITE_TOOLS = [
1397
1413
  if (importe > 0n)
1398
1414
  pendientes.push({ moneda, importe });
1399
1415
  }
1400
- if (!pendientes.length)
1401
- return 'The escrow owes this wallet nothing: there is nothing to collect.';
1416
+ const delTablon = (await deudaDelTablon(panal, account.address)).filter((d) => d.importe !== null && d.importe > 0n);
1417
+ if (!pendientes.length && !delTablon.length) {
1418
+ return 'Neither the escrow nor the board owes this wallet anything: there is nothing to collect.';
1419
+ }
1402
1420
  // Cada moneda es una transacción aparte, y si una falla se sigue con
1403
1421
  // las demás: quedarse sin cobrar el $PANAL porque el MON se quedó sin
1404
1422
  // gas es perder dos veces.
@@ -1407,13 +1425,24 @@ const WRITE_TOOLS = [
1407
1425
  const sym = symbolOf(moneda);
1408
1426
  try {
1409
1427
  const hash = await panal.withdraw(moneda);
1410
- lineas.push(`Collected ${formatEther(importe)} ${sym} from the escrow.\n tx: ${EXPLORER}/tx/${hash}`);
1428
+ lineas.push(`Collected ${fmt(importe, moneda)} ${sym} from the escrow.\n tx: ${EXPLORER}/tx/${hash}`);
1411
1429
  }
1412
1430
  catch (err) {
1413
- lineas.push(`Could not collect the ${formatEther(importe)} ${sym}: ${err instanceof Error ? err.message : err}` +
1431
+ lineas.push(`Could not collect the ${fmt(importe, moneda)} ${sym}: ${err instanceof Error ? err.message : err}` +
1414
1432
  '\n It is still credited in the escrow; calling this again will retry it.');
1415
1433
  }
1416
1434
  }
1435
+ for (const { moneda, importe } of delTablon) {
1436
+ const sym = symbolOf(moneda);
1437
+ try {
1438
+ const { txHash } = await panal.claimBoardRefund(moneda);
1439
+ lineas.push(`Collected ${formatEther(importe)} ${sym} from the board.\n tx: ${EXPLORER}/tx/${txHash}`);
1440
+ }
1441
+ catch (err) {
1442
+ lineas.push(`Could not collect the ${formatEther(importe)} ${sym} from the board: ${err instanceof Error ? err.message : err}` +
1443
+ '\n It is still credited in the board; calling this again will retry it.');
1444
+ }
1445
+ }
1417
1446
  return lineas.join('\n');
1418
1447
  }
1419
1448
  catch (err) {
@@ -1423,10 +1452,11 @@ const WRITE_TOOLS = [
1423
1452
  },
1424
1453
  {
1425
1454
  name: 'panal_claim_task',
1426
- description: 'Take a job from the Panal board: this wallet becomes its worker on chain, and the brief is returned ' +
1427
- 'so the work can start. Claiming is a commitment — the job must be delivered before its deadline — ' +
1428
- 'and it costs gas. The wallet must be an active registered agent, and cannot claim a job it posted. ' +
1429
- 'Deliver with panal_deliver_board.',
1455
+ description: 'Take a job LEFT FROM THE PREVIOUS BOARD (listed by panal_board under "Left from the previous board"; ' +
1456
+ 'jobs on the board with rules are taken with panal_take_board_job): this wallet becomes its worker on ' +
1457
+ 'chain, and the brief is returned so the work can start. Claiming is a commitment — the job must be ' +
1458
+ 'delivered before its deadline — and it costs gas. The wallet must be an active registered agent, and ' +
1459
+ 'cannot claim a job it posted. Deliver with panal_deliver_board.',
1430
1460
  inputSchema: {
1431
1461
  type: 'object',
1432
1462
  properties: {
@@ -1478,7 +1508,8 @@ const WRITE_TOOLS = [
1478
1508
  },
1479
1509
  {
1480
1510
  name: 'panal_deliver_board',
1481
- description: 'Deliver a job taken from the Panal board, for a wallet that receives through its Panal inbox (a ' +
1511
+ description: 'Deliver a job taken from the PREVIOUS board with panal_claim_task (board-with-rules jobs are delivered ' +
1512
+ 'with panal_deliver_board_job), for a wallet that receives through its Panal inbox (a ' +
1482
1513
  'person, or an agent without its own server). The text is left in THIS wallet\'s inbox — the endpoint ' +
1483
1514
  'it publishes, which is where the client collects any delivery — and only then its hash is anchored on ' +
1484
1515
  'chain. If the inbox refuses it, nothing is anchored and it can be retried. An agent with its own server ' +
@@ -1523,8 +1554,21 @@ const WRITE_TOOLS = [
1523
1554
  },
1524
1555
  },
1525
1556
  ];
1557
+ /** El tablón con reglas (tablon.ts), con los mismos guardas y presupuesto que el resto. */
1558
+ const TABLON = herramientasDelTablon({
1559
+ panal,
1560
+ cuenta: account?.address ?? null,
1561
+ escrituraCerrada: writesBlockedReason,
1562
+ revisarPresupuesto,
1563
+ registrarGasto: (moneda, importe) => ledger.record(moneda, importe),
1564
+ simbolo: symbolOf,
1565
+ usd,
1566
+ refrescarPrecios,
1567
+ log,
1568
+ explorer: EXPLORER,
1569
+ });
1526
1570
  /** Las de escritura se anuncian siempre: si no, el modelo no sabe que existen. */
1527
- const TOOLS = [...READ_TOOLS, ...WRITE_TOOLS];
1571
+ const TOOLS = [...READ_TOOLS, ...TABLON.lectura, ...WRITE_TOOLS, ...TABLON.escritura];
1528
1572
  function send(payload) {
1529
1573
  process.stdout.write(`${JSON.stringify(payload)}\n`);
1530
1574
  }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * El tablón con reglas (PanalTablon) en el MCP.
3
+ *
4
+ * Un cliente publica un encargo sin elegir agente: el dinero espera en el
5
+ * contrato y se lo queda un agente que pasa las reglas —su nivel según la
6
+ * reputación, su cupo del día, sus categorías y sus faltas—. En INMEDIATO lo
7
+ * coge el primero que pasa las reglas; en SELECCIÓN los agentes ofertan precio
8
+ * y plazo, y elige el cliente. Especificación: contracts/BOARD.md.
9
+ *
10
+ * Aquí viven las herramientas de los dos lados —quien publica y quien
11
+ * trabaja—, con los mismos guardas que el resto del servidor: lo que mueve
12
+ * dinero o firma necesita MCP_ENABLE_WRITES, una wallet y la confirmación de la
13
+ * persona, y lo que pone dinero nuevo cuenta contra el presupuesto de SU
14
+ * moneda. Lo demás (mirar el tablón, un encargo) funciona en solo lectura.
15
+ *
16
+ * Dos cosas que distinguen este tablón del de antes y que las herramientas
17
+ * dicen: en el escrow, el cliente de estas tareas es PanalTablon, así que
18
+ * aprobar y disputar se hacen a través de él; y los textos —anuncio, encargo,
19
+ * entrega— van por el BUZÓN DEL TABLÓN, comprobados contra los hashes que
20
+ * guarda la cadena.
21
+ */
22
+ import { type Address } from 'viem';
23
+ import { type PanalClient } from '@panal/sdk';
24
+ /** La misma forma que las herramientas de server.ts. */
25
+ export interface Herramienta {
26
+ name: string;
27
+ description: string;
28
+ inputSchema: Record<string, unknown>;
29
+ handler: (args: Record<string, unknown>) => Promise<string>;
30
+ }
31
+ export interface DepsTablon {
32
+ panal: PanalClient;
33
+ /** La wallet del servidor, si hay. */
34
+ cuenta: Address | null;
35
+ /** Motivo por el que no se puede escribir, o null. */
36
+ escrituraCerrada: () => string | null;
37
+ /** ¿Cabe este gasto en el presupuesto de su moneda? El motivo, o null. */
38
+ revisarPresupuesto: (moneda: Address, importe: bigint, simbolo: string) => string | null;
39
+ registrarGasto: (moneda: Address, importe: bigint) => void;
40
+ simbolo: (moneda: Address) => string;
41
+ usd: (importe: bigint, moneda: Address) => string;
42
+ refrescarPrecios: () => Promise<void>;
43
+ log: (m: string) => void;
44
+ explorer: string;
45
+ }
46
+ /**
47
+ * Por qué falló, en una línea. Si lo rechazó el contrato, con su error por el
48
+ * nombre y en claro: la primera línea de viem solo dice «reverted», y el
49
+ * motivo —que es lo que permite decidir qué hacer— venía debajo.
50
+ */
51
+ export declare function fallo(err: unknown): string;
52
+ /** «3 h» o «2 d». */
53
+ export declare function duracion(segundos: number): string;
54
+ /** «codigo», «código», «2» → 2. `null` si no es una categoría. */
55
+ export declare function categoriaDeTexto(entrada: string): number | null;
56
+ export declare function herramientasDelTablon(d: DepsTablon): {
57
+ lectura: Herramienta[];
58
+ escritura: Herramienta[];
59
+ };
60
+ /**
61
+ * Lo que el tablón le debe a una wallet, por moneda (MON y $PANAL). Para
62
+ * `panal_wallet` y `panal_withdraw`: es dinero suyo que no aparece en el saldo.
63
+ */
64
+ export declare function deudaDelTablon(panal: PanalClient, quien: Address): Promise<{
65
+ moneda: Address;
66
+ importe: bigint | null;
67
+ }[]>;