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 +14 -3
- package/dist/limits.d.ts +33 -5
- package/dist/limits.js +82 -13
- package/dist/server.d.ts +8 -2
- package/dist/server.js +62 -32
- package/package.json +2 -2
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
|
|
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
|
*
|
|
@@ -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
|
|
348
|
-
|
|
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
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
}
|
|
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,
|
|
494
|
-
|
|
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
|
|
545
|
-
if (
|
|
546
|
-
return
|
|
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.
|
|
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
|
}
|