agentwallet-mcp 1.14.3 → 1.14.4

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
@@ -374,7 +374,7 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
374
374
  ```
375
375
 
376
376
  - **`budget`**: rolling dollar budgets (last 24 hours, last 30 days, lifetime) across every chain, counting x402 payments, stablecoin transfers and stablecoin approvals in registry stablecoins. A spend the budget cannot price (a token outside the registry) is refused while a budget is set, unless `"unpriced_spends": "allow"`.
377
- - **`native_budget`**: the same per chain, in the chain's own coin, counting native transfers and the native value sent into any contract (`wrap_eth`, `send_transaction` with value and calldata).
377
+ - **`native_budget`**: the same per chain, in the chain's own coin, counting native transfers and the native value sent into any contract (`wrap_eth`, `send_transaction` with value and calldata), including value carried on a token transfer or approval call (a payable token contract keeps it, so it is a second spend, to that contract).
378
378
  - **Approvals are spends.** An ERC-20 allowance (`approve_token`, `approve_permit2`, `increaseAllowance`, `approveAndCall`, Permit2 `approve`, or the same calldata through `send_transaction`) is what the spender will take later, so it is priced, checked and reserved on the ledger when it is granted, the spender is checked against the recipient lists, and an unlimited approval (2^128 base units or more) is refused outright, since no budget can count it; grant an exact amount. A zero-value call to any other contract (a router, a bridge) reserves nothing: it can only pull what an approval already granted.
379
379
  - **`hosts`**, **`per_host`**: which endpoints `pay_x402` may pay, and how much per host per day, per month and per payment. A denied host is never contacted, not even for a quote.
380
380
  - **`recipients`**: where funds may go (transfers, x402 `payTo`, approval spenders, contracts that receive native value). The allow list covers those funds destinations; the contract a zero-value call is aimed at (a token being approved, a router) is checked against the deny list and the chain list.
@@ -384,7 +384,7 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
384
384
 
385
385
  An unknown key stops the server, so a misspelled `budgets` cannot run unbounded. `spend_policy` reports the file, its sha256 (compare it with the file you reviewed), what each budget has spent and has left, and which tools the policy hid. `quote_x402` answers "would this be paid?" for a URL before anything is signed.
386
386
 
387
- **The ledger.** Budgets are enforced from an append-only, hash-chained JSONL file (`AGENTWALLET_LEDGER`, or the policy's `ledger`, or `agentwallet-ledger.jsonl` beside the policy when a budget needs one). A spend is reserved on it before the signature exists; a reservation whose signing fails is voided by a later line; the endpoint's or chain's answer is settled onto it afterwards. A spend that may already be on the wire (a broadcast that failed after the bytes left, a hosted request the API host took and then dropped) is kept counted as `unknown` rather than voided; a failure before anything left this process, including a policy or local-mode refusal, is always voided and never consumes budget. The chain is verified on every read, and a ledger that does not verify refuses every spend until the operator has looked at it. Several agentwallet-mcp processes may share one ledger (two MCP clients on one wallet): the file is re-read before every check, and the check-and-append step holds an exclusive lock file (`<ledger>.lock`) across processes, so the budget is one budget however many processes enforce it; a lock older than 30 seconds is treated as left by a dead process. A transaction signed without being broadcast (`sign_transaction`) is reserved and counted like a send, since whoever holds it can broadcast it. `list_payments` reads it, `export_receipts` turns it into CSV or JSONL, and `AGENTWALLET_WEBHOOK_URL` receives every line as it is written.
387
+ **The ledger.** Budgets are enforced from an append-only, hash-chained JSONL file (`AGENTWALLET_LEDGER`, or the policy's `ledger`, or `agentwallet-ledger.jsonl` beside the policy when a budget needs one). A spend is reserved on it before the signature exists; a reservation whose signing fails is voided by a later line; the endpoint's or chain's answer is settled onto it afterwards. A spend that may already be on the wire (a broadcast that failed after the bytes left, a hosted request the API host took and then dropped) is kept counted as `unknown` rather than voided; a failure before anything left this process, including a policy or local-mode refusal, is always voided and never consumes budget. The chain is verified on every read, and a ledger that does not verify refuses every spend until the operator has looked at it. Several agentwallet-mcp processes may share one ledger (two MCP clients on one wallet): the file is re-read before every check, and the check-and-append step holds an exclusive lock file (`<ledger>.lock`) across processes, so the budget is one budget however many processes enforce it; a lock older than 30 seconds is treated as left by a dead process. A transaction signed without being broadcast (`sign_transaction`) is reserved and counted like a send, since whoever holds it can broadcast it. A call that moves two things (a token transfer that also carries native value) is two lines, reserved in turn and released together when either is refused. `list_payments` reads it, `export_receipts` turns it into CSV or JSONL, and `AGENTWALLET_WEBHOOK_URL` receives every line as it is written.
388
388
 
389
389
  ### Encrypted keyfiles
390
390
 
package/build/index.js CHANGED
@@ -155,8 +155,12 @@ async function describeSend(body, chainId) {
155
155
  const token = layout.tokenIndex === undefined ? to : '0x' + wordAt(layout.tokenIndex).slice(24);
156
156
  const counterparty = '0x' + wordAt(layout.toIndex).slice(24);
157
157
  const amount = BigInt('0x' + wordAt(layout.amountIndex));
158
+ // The call's own value does not vanish because the calldata was
159
+ // recognised: the contract keeps whatever rides along. It is a second
160
+ // spend, native, to the contract (reported privately 2026-10-23, 1.14.4).
161
+ const carried = BigInt(value) > 0n ? { raw: value, to } : undefined;
158
162
  if (layout.kind === 'approval' && amount >= UNBOUNDED_APPROVAL) {
159
- return { kind: 'approval', to: counterparty, asset: token, raw: amount.toString(), decimals: 0, unbounded: true };
163
+ return { kind: 'approval', to: counterparty, asset: token, raw: amount.toString(), decimals: 0, unbounded: true, ...(carried ? { carried } : {}) };
160
164
  }
161
165
  // Decimals matter here only to price a registry stablecoin (resolved
162
166
  // offline) and to print the ledger amount. A token the registry and the
@@ -171,7 +175,7 @@ async function describeSend(body, chainId) {
171
175
  catch {
172
176
  decimals = 0;
173
177
  }
174
- return { kind: layout.kind, to: counterparty, asset: token, raw: amount.toString(), decimals };
178
+ return { kind: layout.kind, to: counterparty, asset: token, raw: amount.toString(), decimals, ...(carried ? { carried } : {}) };
175
179
  }
176
180
  }
177
181
  if (data && data !== '0x')
@@ -197,28 +201,40 @@ function assertSendTarget(d, chainId) {
197
201
  if (!t.ok)
198
202
  throw new Error(t.reason + ' Nothing was signed.');
199
203
  }
204
+ if (d.carried) {
205
+ const t = targetAllowed(POL(), d.carried.to); // the contract that keeps the value
206
+ if (!t.ok)
207
+ throw new Error(t.reason + ' Nothing was signed.');
208
+ }
200
209
  }
201
210
  /**
202
- * The spend a decoded send makes against the policy, or null when nothing
203
- * leaves the wallet: a zero-value contract call can only pull what an
204
- * approval already granted, and approvals are counted when they are granted.
205
- * An approval's recipient is its spender, so recipients.allow and .deny bind
206
- * who may be allowed to pull; the native value of a contract call is a spend
207
- * to that contract. Throws for an unbounded approval under a policy.
211
+ * The spends a decoded send makes against the policy, in the order they are
212
+ * reserved; empty when nothing leaves the wallet: a zero-value contract call
213
+ * can only pull what an approval already granted, and approvals are counted
214
+ * when they are granted. An approval's recipient is its spender, so
215
+ * recipients.allow and .deny bind who may be allowed to pull; the native
216
+ * value of a contract call is a spend to that contract; the native value
217
+ * carried on a token or approval call is a second spend, native, to the
218
+ * called contract (1.14.4). Throws for an unbounded approval under a policy.
208
219
  */
209
- function spendInputOf(d, chainId, tool, wallet, ctx = {}) {
210
- if (d.kind === 'contract' && BigInt(d.raw) === 0n)
211
- return null;
220
+ function spendInputsOf(d, chainId, tool, wallet, ctx = {}) {
212
221
  if (d.kind === 'approval' && d.unbounded && POLICY.path) {
213
222
  throw new Error(`policy: an unlimited approval of ${assetLabel(chainId, d.asset)} to ${d.to} cannot be counted against any budget, and under a spend policy every approval is a bounded spend. ` +
214
223
  `Approve an exact amount instead. Nothing was signed.`);
215
224
  }
216
- return {
217
- tool, mode: anyLocalMode() ? 'local' : 'hosted', wallet: anyLocalMode() ? 'local' : wallet, chainId,
218
- asset: d.asset, assetLabel: assetLabel(chainId, d.asset), raw: d.raw, decimals: d.decimals,
219
- usd: d.unbounded ? null : usdOf(chainId, d.asset, d.raw, d.decimals),
220
- to: d.to, host: ctx.host ?? null, resource: ctx.resource ?? null, method: ctx.method ?? methodOf(d.kind),
221
- };
225
+ const base = { tool, mode: anyLocalMode() ? 'local' : 'hosted', wallet: anyLocalMode() ? 'local' : wallet, chainId, host: ctx.host ?? null, resource: ctx.resource ?? null };
226
+ const out = [];
227
+ if (!(d.kind === 'contract' && BigInt(d.raw) === 0n)) {
228
+ out.push({
229
+ ...base, asset: d.asset, assetLabel: assetLabel(chainId, d.asset), raw: d.raw, decimals: d.decimals,
230
+ usd: d.unbounded ? null : usdOf(chainId, d.asset, d.raw, d.decimals),
231
+ to: d.to, method: ctx.method ?? methodOf(d.kind),
232
+ });
233
+ }
234
+ if (d.carried) {
235
+ out.push({ ...base, asset: '', assetLabel: assetLabel(chainId, ''), raw: d.carried.raw, decimals: nativeDecimals(chainId), usd: null, to: d.carried.to, method: 'native' });
236
+ }
237
+ return out;
222
238
  }
223
239
  // ─── Configuration ──────────────────────────────────────────────
224
240
  const API_BASE = (process.env.AGENTWALLET_API_URL || 'https://hifriendbot.com/wp-json/agentwallet/v1').trim().replace(/\/+$/, '');
@@ -444,7 +460,7 @@ async function api(path, method = 'GET', body, extraHeaders) {
444
460
  extraHeaders = { ...extraHeaders };
445
461
  delete extraHeaders['X-AGW-CTX'];
446
462
  }
447
- let reserved = null;
463
+ const reserved = [];
448
464
  // /sign with a transaction body is the same spend as /send: the caller gets
449
465
  // a broadcast-ready raw transaction (2026-10-14 report, 1.14.2). A message
450
466
  // signature (body.message) moves nothing and is not gated.
@@ -454,18 +470,28 @@ async function api(path, method = 'GET', body, extraHeaders) {
454
470
  const chainId = localChainId(body, new URLSearchParams(path.split('?')[1] || ''));
455
471
  const d = await describeSend(body, chainId);
456
472
  assertSendTarget(d, chainId);
457
- const input = spendInputOf(d, chainId, ctx.tool || (m[2] === 'sign' ? 'sign_transaction' : 'send'), m[1], ctx);
458
- if (input)
459
- reserved = reserveSpend(input);
473
+ const tool = ctx.tool || (m[2] === 'sign' ? 'sign_transaction' : 'send');
474
+ // One line per spend the call makes (a token call that also carries native
475
+ // value makes two); a refusal of a later one releases the earlier ones.
476
+ for (const input of spendInputsOf(d, chainId, tool, m[1], ctx)) {
477
+ try {
478
+ reserved.push(reserveSpend(input));
479
+ }
480
+ catch (e) {
481
+ for (const seq of reserved)
482
+ LEDGER.void(seq, `released, the same call was refused: ${e.message}`);
483
+ throw e;
484
+ }
485
+ }
460
486
  }
461
487
  try {
462
488
  const out = await apiInner(path, method, body, extraHeaders);
463
- if (reserved !== null) {
489
+ for (const seq of reserved) {
464
490
  const r = out;
465
491
  if (m[2] === 'sign')
466
- LEDGER.settle(reserved, 'signed', 'raw transaction returned to the caller; not broadcast by this process, so it stays counted');
492
+ LEDGER.settle(seq, 'signed', 'raw transaction returned to the caller; not broadcast by this process, so it stays counted');
467
493
  else
468
- LEDGER.settle(reserved, 'broadcast', typeof r?.tx_hash === 'string' ? r.tx_hash : typeof r?.signature === 'string' ? r.signature : null);
494
+ LEDGER.settle(seq, 'broadcast', typeof r?.tx_hash === 'string' ? r.tx_hash : typeof r?.signature === 'string' ? r.signature : null);
469
495
  }
470
496
  return out;
471
497
  }
@@ -476,12 +502,12 @@ async function api(path, method = 'GET', body, extraHeaders) {
476
502
  // message and the word "broadcasts" in the local-mode refusal of
477
503
  // transaction signing made every refused sign_transaction consume the
478
504
  // budget for good (reported privately 2026-10-16, 1.14.3).
479
- if (reserved !== null) {
505
+ for (const seq of reserved) {
480
506
  const msg = e.message;
481
507
  if (isMaybeLive(e))
482
- LEDGER.settle(reserved, 'unknown', msg);
508
+ LEDGER.settle(seq, 'unknown', msg);
483
509
  else
484
- LEDGER.void(reserved, msg);
510
+ LEDGER.void(seq, msg);
485
511
  }
486
512
  throw e;
487
513
  }
@@ -1143,7 +1169,7 @@ function resolveChainId(network) {
1143
1169
  const AddressSchema = z.string().regex(/^(0x[a-fA-F0-9]{40}|[1-9A-HJ-NP-Za-km-z]{32,44})$/, 'Must be a 0x-prefixed 40-hex EVM address or a base58 Solana address');
1144
1170
  const server = new McpServer({
1145
1171
  name: 'agentwallet',
1146
- version: '1.14.3',
1172
+ version: '1.14.4',
1147
1173
  }, {
1148
1174
  instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
1149
1175
  ? 'This server runs in LOCAL (self-custody) mode: the operator supplied the key, signing happens in this process, and the only spend guards are the environment caps (AGENTWALLET_MAX_TX_NATIVE / _TOKEN / _SOL per transaction, AGENTWALLET_MAX_AUTOPAY and AGENTWALLET_AUTOPAY_ASSETS for x402, AGENTWALLET_X402_MAX_TIMEOUT for authorization windows). There is no server-side pause, daily limit or approval email; pause_wallet, unpause_wallet and delete_wallet are refused.'
@@ -2815,8 +2841,12 @@ tool('simulate_transaction', 'Dry-run an EVM transaction with nothing signed: th
2815
2841
  const policy = (() => {
2816
2842
  try {
2817
2843
  assertSendTarget(d, chain_id);
2818
- const input = spendInputOf(d, chain_id, 'simulate_transaction', String(wallet_id));
2819
- return input ? spendVerdict(input) : { ok: true };
2844
+ for (const input of spendInputsOf(d, chain_id, 'simulate_transaction', String(wallet_id))) {
2845
+ const v = spendVerdict(input);
2846
+ if (!v.ok)
2847
+ return v;
2848
+ }
2849
+ return { ok: true };
2820
2850
  }
2821
2851
  catch (e) {
2822
2852
  return { ok: false, reason: e.message };
@@ -2826,6 +2856,7 @@ tool('simulate_transaction', 'Dry-run an EVM transaction with nothing signed: th
2826
2856
  kind: d.kind, to: d.to, asset: d.asset || 'native', asset_label: assetLabel(chain_id, d.asset),
2827
2857
  amount: d.unbounded ? 'unlimited' : formatUnits(d.raw, d.decimals),
2828
2858
  ...(d.kind === 'approval' ? { note: 'an approval: `to` is the spender being allowed to pull `amount` of `asset`' } : {}),
2859
+ ...(d.carried ? { carried_native: { amount: formatUnits(d.carried.raw, nativeDecimals(chain_id)), to: d.carried.to, note: 'native value sent along with the calldata: a second spend, against native_budget, to the called contract' } } : {}),
2829
2860
  };
2830
2861
  if (isLocalMode()) {
2831
2862
  const sim = await localSimulate(chain_id, to, BigInt(valueRaw), data && data !== '0x' ? data : undefined);
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/hifriendbot/agentwallet-mcp.git"
7
7
  },
8
- "version": "1.14.3",
8
+ "version": "1.14.4",
9
9
  "description": "MCP server for AgentWallet: self-custody local signing on EVM and Solana, or hosted wallets. Sign, broadcast, pay x402 invoices, and accept x402 payments.",
10
10
  "type": "module",
11
11
  "main": "build/index.js",