panal-mcp 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/dist/limits.d.ts +33 -5
- package/dist/limits.js +82 -13
- package/dist/server.d.ts +8 -2
- package/dist/server.js +155 -51
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
|
|
|
37
37
|
| `panal_get_agent` | Ficha completa: precio, skills, estado y nombre único leído de la cadena |
|
|
38
38
|
| `panal_get_task` | Estado de un encargo en el escrow |
|
|
39
39
|
| `panal_marketplace_stats` | Cifras del marketplace |
|
|
40
|
-
| `panal_wallet` | Saldo y presupuesto restante *(escritura)* |
|
|
40
|
+
| `panal_wallet` | Saldo en cada moneda (MON y $PANAL), presupuesto restante y lo que el escrow te debe *(escritura)* |
|
|
41
41
|
| `panal_quote_hire` | Presupuesta un encargo sin pagar, y comprueba que el agente responde *(escritura)* |
|
|
42
42
|
| `panal_hire` | Contrata y bloquea el pago *(escritura)* |
|
|
43
43
|
| `panal_send_brief` | Reenvía el encargo si no llegó al contratar *(escritura)* |
|
|
@@ -46,7 +46,7 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
|
|
|
46
46
|
| `panal_quote_ask` / `panal_ask` | Pregunta suelta a un agente, sin abrir un encargo *(escritura)* |
|
|
47
47
|
| `panal_cancel_task` | Recupera tu dinero si nadie empezó y venció el plazo *(escritura)* |
|
|
48
48
|
| `panal_open_dispute` | Abre disputa si lo entregado no vale *(escritura)* |
|
|
49
|
-
| `panal_withdraw` | Retira lo que el escrow te debe *(escritura)* |
|
|
49
|
+
| `panal_withdraw` | Retira lo que el escrow te debe, en todas las monedas *(escritura)* |
|
|
50
50
|
|
|
51
51
|
Las cuatro últimas existen por lo mismo: un encargo puede torcerse, y hasta que las
|
|
52
52
|
hubo la única salida era esperar. El escrow es de **pago tirado** (*pull*), así que lo
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
85
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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(),
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|
17
|
-
* MCP_DAILY_BUDGET_WEI
|
|
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
|
|
17
|
-
* MCP_DAILY_BUDGET_WEI
|
|
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
|
*
|
|
@@ -28,8 +34,8 @@ import { createInterface } from 'node:readline';
|
|
|
28
34
|
import { fileURLToPath } from 'node:url';
|
|
29
35
|
import { formatEther, isAddress, keccak256, parseEther, toBytes } from 'viem';
|
|
30
36
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
31
|
-
import { NATIVE_CURRENCY, TaskStatus, createPanalClient, } from '@panal/sdk';
|
|
32
|
-
import { QuoteBook, SpendLedger, limitsFromEnv } from './limits.js';
|
|
37
|
+
import { NATIVE_CURRENCY, TaskStatus, createPanalClient, erc20Abi, } from '@panal/sdk';
|
|
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
|
/**
|
|
@@ -88,8 +94,67 @@ function writesBlockedReason() {
|
|
|
88
94
|
// ---------------------------------------------------------------------------
|
|
89
95
|
// Presentación
|
|
90
96
|
// ---------------------------------------------------------------------------
|
|
97
|
+
/** ¿Es la moneda nativa de la cadena, o un token? */
|
|
98
|
+
function esNativa(currency) {
|
|
99
|
+
return currency.toLowerCase() === NATIVE_CURRENCY.toLowerCase();
|
|
100
|
+
}
|
|
91
101
|
function symbolOf(currency) {
|
|
92
|
-
return currency
|
|
102
|
+
return esNativa(currency) ? 'MON' : '$PANAL';
|
|
103
|
+
}
|
|
104
|
+
/** Las monedas que este servidor sabe manejar: las que tienen presupuesto. */
|
|
105
|
+
function monedasConocidas() {
|
|
106
|
+
return [...limits.porMoneda.keys()];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Saldo de una wallet en una moneda: a la cadena si es la nativa, al contrato
|
|
110
|
+
* del token si es un ERC-20.
|
|
111
|
+
*
|
|
112
|
+
* `null` es «no se pudo leer», y se distingue de cero a propósito. Enseñar 0
|
|
113
|
+
* cuando la consulta falla es afirmar que no hay fondos, que es una respuesta
|
|
114
|
+
* distinta: llevaría a no intentar un pago que sí se podía hacer.
|
|
115
|
+
*/
|
|
116
|
+
async function saldoDe(owner, currency) {
|
|
117
|
+
try {
|
|
118
|
+
if (esNativa(currency))
|
|
119
|
+
return await panal.publicClient.getBalance({ address: owner });
|
|
120
|
+
return (await panal.publicClient.readContract({
|
|
121
|
+
address: currency,
|
|
122
|
+
abi: erc20Abi,
|
|
123
|
+
functionName: 'balanceOf',
|
|
124
|
+
args: [owner],
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* ¿Cabe este gasto en el presupuesto de SU moneda? Devuelve el motivo, o null.
|
|
133
|
+
*
|
|
134
|
+
* Cada moneda lleva su cuenta. Sumar MON y $PANAL en un solo número exigiría un
|
|
135
|
+
* tipo de cambio que nadie tiene, y el resultado era que tres consultas de
|
|
136
|
+
* $PANAL agotaban un tope puesto pensando en MON.
|
|
137
|
+
*
|
|
138
|
+
* Sin presupuesto para una moneda, se dice que no. La alternativa —tirar del
|
|
139
|
+
* tope de otra— es exactamente la conversión a ojo que este cambio elimina.
|
|
140
|
+
*/
|
|
141
|
+
function revisarPresupuesto(currency, amount, symbol) {
|
|
142
|
+
const lim = limitFor(limits, currency);
|
|
143
|
+
if (!lim) {
|
|
144
|
+
return (`That agent charges in a token this server has no budget for (${currency}). ` +
|
|
145
|
+
'Only MON and $PANAL have caps configured; there is no exchange rate to reuse another one.');
|
|
146
|
+
}
|
|
147
|
+
if (amount > lim.maxPerTaskWei) {
|
|
148
|
+
return (`That costs ${formatEther(amount)} ${symbol} and the per-item cap for ${symbol} is ` +
|
|
149
|
+
`${formatEther(lim.maxPerTaskWei)}. Raise ${lim.envMaxPerTask} if that is intended.`);
|
|
150
|
+
}
|
|
151
|
+
const spent = ledger.spentToday(currency);
|
|
152
|
+
if (spent + amount > lim.dailyBudgetWei) {
|
|
153
|
+
return (`That would blow today's ${symbol} budget: ${formatEther(spent)} of ` +
|
|
154
|
+
`${formatEther(lim.dailyBudgetWei)} spent and this costs ${formatEther(amount)} ${symbol}. ` +
|
|
155
|
+
`Raise ${lim.envDailyBudget} if that is intended.`);
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
93
158
|
}
|
|
94
159
|
/** Días desde un timestamp en segundos. */
|
|
95
160
|
function daysSince(ts) {
|
|
@@ -336,28 +401,54 @@ const READ_TOOLS = [
|
|
|
336
401
|
const WRITE_TOOLS = [
|
|
337
402
|
{
|
|
338
403
|
name: 'panal_wallet',
|
|
339
|
-
description: 'State of the wallet this server would pay with: address, balance
|
|
340
|
-
'is left. Call it before hiring to know
|
|
404
|
+
description: 'State of the wallet this server would pay with: address, the balance of EVERY currency Panal ' +
|
|
405
|
+
'takes (MON and $PANAL), and how much of today\'s budget is left. Call it before hiring to know ' +
|
|
406
|
+
'whether there are funds.',
|
|
341
407
|
inputSchema: { type: 'object', properties: {} },
|
|
342
408
|
handler: async () => {
|
|
343
409
|
const blocked = writesBlockedReason();
|
|
344
410
|
if (blocked)
|
|
345
411
|
return blocked;
|
|
346
412
|
const address = account.address;
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
|
|
413
|
+
const monedas = monedasConocidas();
|
|
414
|
+
// Un saldo por moneda, no solo el nativo. Antes se leía únicamente MON y
|
|
415
|
+
// justo debajo se listaban los topes de $PANAL: quien lo leía daba por
|
|
416
|
+
// comprobado un saldo que nadie había mirado, y podía contratar en $PANAL
|
|
417
|
+
// sin tener con qué pagar. Las lecturas son independientes, así que van
|
|
418
|
+
// en paralelo.
|
|
419
|
+
const [saldos, pendientes] = await Promise.all([
|
|
420
|
+
Promise.all(monedas.map((m) => saldoDe(address, m))),
|
|
421
|
+
// Lo acreditado en el escrow no aparece en el balance de la wallet: es
|
|
422
|
+
// pull payment, así que un reembolso se queda ahí quieto hasta que se
|
|
423
|
+
// reclama. Sin decirlo, el dinero devuelto es indistinguible del
|
|
424
|
+
// perdido — y también hay que preguntarlo moneda a moneda.
|
|
425
|
+
Promise.all(monedas.map((m) => panal.getPendingWithdrawal(address, m).catch(() => null))),
|
|
426
|
+
]);
|
|
427
|
+
const importe = (v) => (v === null ? 'could not be read' : formatEther(v));
|
|
428
|
+
const balances = monedas.map((m, i) => ` ${symbolOf(m)}: ${importe(saldos[i] ?? null)}`);
|
|
429
|
+
// Un presupuesto por moneda: MON y $PANAL no valen lo mismo y no hay
|
|
430
|
+
// tipo de cambio, así que se enseñan por separado o no se entienden.
|
|
431
|
+
const presupuestos = monedas.map((moneda) => {
|
|
432
|
+
const lim = limitFor(limits, moneda);
|
|
433
|
+
const spent = ledger.spentToday(moneda);
|
|
434
|
+
const left = lim.dailyBudgetWei > spent ? lim.dailyBudgetWei - spent : 0n;
|
|
435
|
+
return (` ${symbolOf(moneda)}: per job ${formatEther(lim.maxPerTaskWei)} · today ${formatEther(spent)} of ` +
|
|
436
|
+
`${formatEther(lim.dailyBudgetWei)} spent · ${formatEther(left)} left`);
|
|
437
|
+
});
|
|
438
|
+
// Solo se nombra lo que hay que cobrar, o lo que no se pudo comprobar. Un
|
|
439
|
+
// «0» por cada moneda sería ruido en la respuesta más leída del servidor.
|
|
440
|
+
const enEscrow = monedas
|
|
441
|
+
.map((m, i) => ({ sym: symbolOf(m), v: pendientes[i] ?? null }))
|
|
442
|
+
.filter(({ v }) => v === null || v > 0n)
|
|
443
|
+
.map(({ sym, v }) => ` ${sym}: ${importe(v)}`);
|
|
353
444
|
return [
|
|
354
445
|
`Wallet: ${address}`,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
446
|
+
'Balance:',
|
|
447
|
+
...balances,
|
|
448
|
+
'Caps and budgets (each currency is counted on its own):',
|
|
449
|
+
...presupuestos,
|
|
450
|
+
enEscrow.length ? 'Waiting in the escrow — collect it with panal_withdraw:' : null,
|
|
451
|
+
...enEscrow,
|
|
361
452
|
]
|
|
362
453
|
.filter(Boolean)
|
|
363
454
|
.join('\n');
|
|
@@ -390,15 +481,9 @@ const WRITE_TOOLS = [
|
|
|
390
481
|
return `${agent.metadata.name || address} is delisted and does not take jobs.`;
|
|
391
482
|
const amount = agent.pricePerTask;
|
|
392
483
|
const symbol = symbolOf(agent.currency);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
}
|
|
484
|
+
const negativa = revisarPresupuesto(agent.currency, amount, symbol);
|
|
485
|
+
if (negativa)
|
|
486
|
+
return negativa;
|
|
402
487
|
// El tope del brief se pregunta ANTES de presupuestar. Si no cabe, el
|
|
403
488
|
// agente lo rechazaría con un 400 al recibirlo — pero eso ocurre ya con
|
|
404
489
|
// el pago bloqueado, porque el encargo se entrega después de crear la
|
|
@@ -476,22 +561,17 @@ const WRITE_TOOLS = [
|
|
|
476
561
|
const quote = redeemed.quote;
|
|
477
562
|
// Los mismos topes que contratar. Una consulta es barata, pero nada
|
|
478
563
|
// impide encadenar mil: el presupuesto del día es lo que lo impide.
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
}
|
|
564
|
+
const negativa = revisarPresupuesto(quote.currency, quote.amount, quote.symbol);
|
|
565
|
+
if (negativa)
|
|
566
|
+
return negativa;
|
|
488
567
|
try {
|
|
489
568
|
// `maxSpend` va atado al importe del presupuesto que la persona
|
|
490
569
|
// aprobó, no al tope del servidor: si el agente sube el precio entre
|
|
491
570
|
// el presupuesto y el sí, la firma no se produce.
|
|
492
571
|
const res = await panal.askAgent(quote.worker, quote.brief, { maxSpend: quote.amount });
|
|
493
|
-
// Se registra lo REALMENTE pagado,
|
|
494
|
-
|
|
572
|
+
// Se registra lo REALMENTE pagado, y en SU moneda: un gasto anotado en
|
|
573
|
+
// el contador equivocado agota un presupuesto que nadie tocó.
|
|
574
|
+
ledger.record(quote.currency, res.paid);
|
|
495
575
|
log(`consulta pagada a ${quote.worker}: ${formatEther(res.paid)} ${quote.symbol}`);
|
|
496
576
|
return [
|
|
497
577
|
`Paid ${formatEther(res.paid)} ${quote.symbol} to ${quote.agentName}.`,
|
|
@@ -541,14 +621,13 @@ const WRITE_TOOLS = [
|
|
|
541
621
|
const quote = redeemed.quote;
|
|
542
622
|
// Los topes se re-evalúan al contratar: entre el presupuesto y el "sí"
|
|
543
623
|
// pueden haber pasado minutos y otros encargos.
|
|
544
|
-
const
|
|
545
|
-
if (
|
|
546
|
-
return
|
|
547
|
-
}
|
|
624
|
+
const negativa = revisarPresupuesto(quote.currency, quote.amount, quote.symbol);
|
|
625
|
+
if (negativa)
|
|
626
|
+
return negativa;
|
|
548
627
|
const deadline = BigInt(Math.floor(Date.now() / 1000) + limits.deadlineHours * 3600);
|
|
549
628
|
try {
|
|
550
629
|
const result = await panal.hire({ agent: quote.worker, brief: quote.brief, deadline });
|
|
551
|
-
ledger.record(result.amount);
|
|
630
|
+
ledger.record(quote.currency, result.amount);
|
|
552
631
|
log(`contratada #${result.taskId} a ${quote.worker} por ${formatEther(result.amount)} ${quote.symbol}`);
|
|
553
632
|
// Contratar sin entregar el encargo deja la tarea a medias: el pago
|
|
554
633
|
// bloqueado y el agente esperando un texto que nadie le manda. Se hace
|
|
@@ -808,9 +887,9 @@ const WRITE_TOOLS = [
|
|
|
808
887
|
},
|
|
809
888
|
{
|
|
810
889
|
name: 'panal_withdraw',
|
|
811
|
-
description: 'Collect what the escrow owes this wallet — refunds from cancelled jobs
|
|
812
|
-
'payment: it credits balances and never pushes
|
|
813
|
-
'Call panal_wallet first to see
|
|
890
|
+
description: 'Collect what the escrow owes this wallet — refunds from cancelled jobs — in EVERY currency it ' +
|
|
891
|
+
'holds, one transaction each. The escrow is pull payment: it credits balances and never pushes ' +
|
|
892
|
+
'them, so money sits there until this is called. Call panal_wallet first to see what is waiting.',
|
|
814
893
|
inputSchema: {
|
|
815
894
|
type: 'object',
|
|
816
895
|
properties: {
|
|
@@ -826,11 +905,36 @@ const WRITE_TOOLS = [
|
|
|
826
905
|
return 'I will not move funds without confirmed_by_user: true.';
|
|
827
906
|
}
|
|
828
907
|
try {
|
|
829
|
-
|
|
830
|
-
|
|
908
|
+
// Se retira MONEDA A MONEDA. El escrow lleva un saldo por cada una y
|
|
909
|
+
// `withdraw` solo vacía la que se le pide; preguntando únicamente por
|
|
910
|
+
// la nativa, un reembolso en $PANAL quedaba invisible y esta misma
|
|
911
|
+
// herramienta contestaba que no había nada que cobrar. El dinero seguía
|
|
912
|
+
// en el contrato, pero desde aquí no había forma de sacarlo.
|
|
913
|
+
const monedas = monedasConocidas();
|
|
914
|
+
const pendientes = [];
|
|
915
|
+
for (const moneda of monedas) {
|
|
916
|
+
const importe = await panal.getPendingWithdrawal(account.address, moneda);
|
|
917
|
+
if (importe > 0n)
|
|
918
|
+
pendientes.push({ moneda, importe });
|
|
919
|
+
}
|
|
920
|
+
if (!pendientes.length)
|
|
831
921
|
return 'The escrow owes this wallet nothing: there is nothing to collect.';
|
|
832
|
-
|
|
833
|
-
|
|
922
|
+
// Cada moneda es una transacción aparte, y si una falla se sigue con
|
|
923
|
+
// las demás: quedarse sin cobrar el $PANAL porque el MON se quedó sin
|
|
924
|
+
// gas es perder dos veces.
|
|
925
|
+
const lineas = [];
|
|
926
|
+
for (const { moneda, importe } of pendientes) {
|
|
927
|
+
const sym = symbolOf(moneda);
|
|
928
|
+
try {
|
|
929
|
+
const hash = await panal.withdraw(moneda);
|
|
930
|
+
lineas.push(`Collected ${formatEther(importe)} ${sym} from the escrow.\n tx: ${EXPLORER}/tx/${hash}`);
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
lineas.push(`Could not collect the ${formatEther(importe)} ${sym}: ${err instanceof Error ? err.message : err}` +
|
|
934
|
+
'\n It is still credited in the escrow; calling this again will retry it.');
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return lineas.join('\n');
|
|
834
938
|
}
|
|
835
939
|
catch (err) {
|
|
836
940
|
return `Could not withdraw: ${err instanceof Error ? err.message : err}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panal-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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
|
}
|