agentwallet-mcp 1.14.9 → 1.14.11

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
@@ -119,7 +119,7 @@ Add to your config:
119
119
  }
120
120
  ```
121
121
 
122
- > `AGENTWALLET_WALLET_ID` is optional. Set it to enable x402 auto-pay: when you exceed the free tier without a credit card, the MCP server automatically pays for operations with USDC from this wallet. Only the metered routes (creating a wallet, signing, sending) are paid this way, at most `AGENTWALLET_MAX_AUTOPAY_PER_HOUR` times an hour (default 20), and every automatic payment is reported in the tool result under `autopay_payment`. A 402 on any other route is reported, never paid.
122
+ > `AGENTWALLET_WALLET_ID` is optional. Set it to enable x402 auto-pay: when you exceed the free tier without a credit card, the MCP server automatically pays for operations with USDC from this wallet. Only the metered routes (creating a wallet, signing, sending) are paid this way, at most `AGENTWALLET_MAX_AUTOPAY_PER_HOUR` times an hour (default 20; the slot is taken before the payment is sent, so parallel calls cannot overrun it, and processes sharing a ledger file share the hour, while without one the count is per process), and every automatic payment is reported in the tool result under `autopay_payment`. A 402 on any other route is reported, never paid.
123
123
  >
124
124
  > **Auto-pay safety cap.** `AGENTWALLET_MAX_AUTOPAY` (optional, default `1`) is the most one x402 payment may authorize, in stablecoin units: `1` means one dollar. It prices registry stablecoins only (USDC, USDT, USDbC, DAI on the supported chains, USDC and USDT on Solana). A requirement in the chain's native asset or in any other token is refused, because "1" measured in ETH is a few thousand dollars; list such assets in `AGENTWALLET_AUTOPAY_ASSETS` (comma-separated addresses or mints, or the word `native`) to allow them, and the cap then applies in that asset's own units. Any requirement above the cap is rejected instead of paid, so a malformed or tampered payment requirement cannot drain the wallet. This automatic path settles `exact` requirements only; an `upto` offer from the API is refused before any wallet call, because paying a usage maximum upfront is not what the offer means (use `pay_x402`, which signs a Permit2 authorization for it). The cap is the operator's ceiling: a `max_payment` argument can lower it for one call but never raise it. Raise the variable itself if you genuinely need larger automatic payments (for example `"5"` for up to 5 USDC per call).
125
125
 
@@ -369,23 +369,25 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
369
369
  "per_host": { "*.example.com": { "daily": "2", "per_payment": "0.25" } },
370
370
  "recipients": { "deny": ["0x0000000000000000000000000000000000000000"] },
371
371
  "chains": { "allow": [8453, 900] },
372
+ "contracts": { "allow": ["8453:0x2626664c2603336E57B271c5C0b26F421741e481"] },
372
373
  "approval": { "above": "0.50", "max": "20" },
373
374
  "tools": { "allow": ["pay_x402", "quote_x402", "discover_x402", "get_balance", "list_payments", "spend_policy", "wallet_mode"] }
374
375
  }
375
376
  ```
376
377
 
377
378
  - **`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"`. An SPL send is priced at the mint's own decimals; a `token_decimals` hint that disagrees with the mint is refused before any request is made.
378
- - **`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) and, in local Solana mode, the rent an SPL send pays to create the recipient's token account (the recipient owns that account and can close it for the lamports; the ledger records it as its own line, method `rent`, to the recipient, and `assets.allow` does not need to list `native` for it).
379
- - **Approvals are spends.** An ERC-20 allowance (`approve_token`, `approve_permit2`, `increaseAllowance`, `approveAndCall`, Permit2 `approve`, a signed `permit` in its EIP-2612, DAI or Permit2 shape, 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, since no budget can count it; grant an exact amount. An NFT operator grant (`setApprovalForAll`, ERC-777 `authorizeOperator`) is unbounded by construction and refused the same way, and an ERC-721 or ERC-1155 transfer is a spend of the collection to the recipient that no budget can price, so it is refused under a dollar budget unless `unpriced_spends` is `"allow"`, with the recipient checked either way. A zero-value call to any other contract (a router, a bridge) reserves nothing: it can only pull what an approval already granted. The exception is Permit2 itself: a call to it that the policy cannot price (`permitBatch`, `transferFrom`, `permitTransferFrom`, `lockdown`, nonce invalidation, or an unknown selector) is refused under a policy, since each moves tokens or changes what may be moved.
379
+ - **`native_budget`**: the same per chain, in the chain's own coin, counting native transfers, native x402 payments 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) and, in local Solana mode, the rent an SPL send pays to create the recipient's token account (the recipient owns that account and can close it for the lamports; the ledger records it as its own line, method `rent`, to the recipient, and `assets.allow` does not need to list `native` for it). The dollar budget has no price for the chain's coin, so on a chain with no `native_budget` entry a native spend is an unpriced spend: refused while a `budget` or a `per_host` ceiling is set, unless `"unpriced_spends": "allow"`. Give every chain the wallet spends on an entry.
380
+ - **Approvals are spends.** An ERC-20 allowance (`approve_token`, `approve_permit2`, `increaseAllowance`, `increaseApproval`, either `approveAndCall`, Permit2 `approve`, a signed `permit` in its EIP-2612, DAI or Permit2 shape, 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, since no budget can count it; grant an exact amount. An operator grant (`setApprovalForAll`, ERC-777 `authorizeOperator`, EIP-6909 `setOperator`) is unbounded by construction and refused the same way. Every token mover the standards define is a spend to its recipient: ERC-20 `transfer` and `transferFrom`, the ERC-677 and ERC-1363 `transferAndCall` and `transferFromAndCall` shapes, the ERC-223 `transfer` with data, ERC-777 `send` and `operatorSend`, and the ERC-721, ERC-1155 and EIP-6909 transfers, which no budget can price and so are refused under a dollar budget unless `unpriced_spends` is `"allow"`, with the recipient checked either way. A redemption of the wallet's own position is a spend too: ERC-4626 `redeem` and `withdraw` are spends of the vault to the receiver (unpriced, like an NFT), and Aave `Pool.withdraw` is a transfer of the underlying to the receiver. A call to any other contract (a router, a bridge, a protocol whose payout shape is not listed here) is refused under a policy unless the contract is in `contracts.allow` or `unknown_calls` is `"allow"`, since 1.14.11: the policy cannot tell what such a call moves, so the operator decides which contracts may receive one. A listed contract's unpriced call reserves nothing beyond the native value it carries: it can only pull what an approval already granted, or pay out a position in a shape the policy does not know, so a policy that lists protocol contracts should set `recipients.allow` too. Permit2 is never admitted this way: a call to it that the policy cannot price (`permitBatch`, `transferFrom`, `permitTransferFrom`, `lockdown`, nonce invalidation, or an unknown selector) is refused under a policy whatever the lists say, since each moves tokens or changes what may be moved.
380
381
  - **`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. A host is matched by its canonical DNS name (lowercase, the root label's trailing dot removed), so `api.example.com.` and its percent-encoded spelling are `api.example.com` to every list and to the ledger's per-host sums.
381
- - **`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.
382
+ - **`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 call is aimed at (a token being approved, a router) is checked against the deny list, the chain list and the `contracts` lists.
382
383
  - **`chains`**, **`assets`**: allow lists; an asset entry can be scoped `"8453:0x..."` or be the word `native`.
384
+ - **`contracts`**, **`unknown_calls`**: what the wallet may call with calldata the policy cannot price. The policy prices the token movers, approvals, permits and redemptions listed under "Approvals are spends" below; any other calldata (a swap, a bridge, a protocol call) is refused under a policy unless the contract is in `contracts.allow` (entries scoped `"8453:0x..."` or bare), because a shape the policy does not know could be moving anything. `"unknown_calls": "allow"` restores the pre-1.14.11 behaviour of passing such calls as zero-value contract calls, with their native value still counted. `contracts.deny` refuses every call to a contract, priced or not. Without a policy nothing is refused on this ground.
383
385
  - **`approval`**: above `above`, every x402 payment is put to the human at the MCP client through [elicitation](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation) (the client shows the amount, token, recipient and resource and asks yes or no; a client that declared no elicitation support cannot answer, so the payment is refused). In local mode an accepted answer may admit a payment above the per-payment cap, up to `max`. Budgets are never lifted by anyone's approval.
384
386
  - **`tools`**: the tools the model sees. A payer-only deployment exposes seven; the rest are not registered. `wallet_mode` and `spend_policy` are always present so the operator can see what the server is.
385
387
 
386
388
  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.
387
389
 
388
- **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.
390
+ **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 or answered with a 5xx, any host answer that carries a transaction hash) is kept counted as `unknown` rather than voided; a failure before anything left this process, including a policy or local-mode refusal, or a 4xx refusal from the host, is always voided and never consumes budget. A hosted send is forwarded with the chain the policy priced it on (`chain_id`, else `AGENTWALLET_CHAIN_ID`, else 8453), and a custodian answer naming another chain is written onto the ledger line as a mismatch. 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.
389
391
 
390
392
  ### Encrypted keyfiles
391
393
 
package/build/index.js CHANGED
@@ -28,13 +28,13 @@ import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDe
28
28
  import { createHash } from 'node:crypto';
29
29
  import { isLocalMode, getLocalAddress, localWalletRecord, localSend, localNativeBalance, localTokenBalance, localSignMessage, resolveRpcUrl, } from './local-wallet.js';
30
30
  import { localSignAuthorization, localEthCall, localGetCode, localSignPermit2Upto, probeTokenDecimals, assertRpcServesChain } from './local-wallet.js';
31
- import { WRAPPED_NATIVE } from './wrapped-native.js';
31
+ import { WRAPPED_NATIVE, wrappedNativeAddress } from './wrapped-native.js';
32
32
  import { buildUptoAuthorization, uptoPayload, permit2AllowanceCalldata, permit2ApproveCalldata, decodeUint, permit2Address, } from './x402-permit2.js';
33
33
  import { assessTokenRisk } from './token-risk.js';
34
34
  import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, buildPaymentPayloadRaw, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, decodeAbiString as decodeDomainString, parseEip7702Delegation, erc1271ProbeCalldata, classifyErc1271Probe, settlementVerdict, legacyReceiptAllowed, sanitizeRequired, } from './x402-eip3009.js';
35
35
  import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, mintDecimalsStrict, splAccountRent, } from './local-solana.js';
36
36
  import { localSimulate } from './local-wallet.js';
37
- import { loadPolicy, describePolicy, hostAllowed, recipientAllowed, targetAllowed, chainAllowed, assetAllowed, perHostRule, toolAllowed, hostOf, budgetReport, units as policyUnits, human as policyHuman, rawToUnits, } from './policy.js';
37
+ import { loadPolicy, describePolicy, hostAllowed, recipientAllowed, targetAllowed, unknownCallAllowed, chainAllowed, assetAllowed, perHostRule, toolAllowed, hostOf, budgetReport, units as policyUnits, human as policyHuman, rawToUnits, } from './policy.js';
38
38
  import { Ledger } from './ledger.js';
39
39
  import { markMaybeLive, isMaybeLive, carryMaybeLive, connectionNeverMade } from './maybe-live.js';
40
40
  import { discover } from './discovery.js';
@@ -121,6 +121,8 @@ function spendVerdict(input) {
121
121
  function usdOf(chainId, asset, raw, decimals) {
122
122
  return autopayAssetAllowed(chainId, asset).via === 'stablecoin' ? policyHuman(rawToUnits(raw, decimals)) : null;
123
123
  }
124
+ /** WETH9 deposit() and withdraw(uint256): known shapes when aimed at the chain's own wrapped-native contract (wrap_eth, unwrap_eth). */
125
+ const WRAPPED_NATIVE_SELECTORS = new Set(['d0e30db0', '2e1a7d4d']);
124
126
  /* Calldata the policy prices: which word holds the amount, which the recipient
125
127
  or spender, and for Permit2.approve which the token (there `to` is Permit2).
126
128
  The same functions the local token cap prices, so the two guards agree. */
@@ -162,6 +164,43 @@ const SPEND_LAYOUTS = {
162
164
  d505accf: { kind: 'approval', what: 'EIP-2612 permit', toIndex: 1, amountIndex: 2 }, // permit(address,address,uint256,uint256,uint8,bytes32,bytes32)
163
165
  '8fcbaf0c': { kind: 'approval', what: 'DAI permit', toIndex: 1, amountIndex: 4, operator: true }, // permit(address,address,uint256,uint256,bool,uint8,bytes32,bytes32): allowed=true is unbounded by construction
164
166
  '2b67b570': { kind: 'approval', what: 'Permit2 permit', toIndex: 5, amountIndex: 2, tokenIndex: 1, permit2Only: true }, // Permit2.permit(address,((address,uint160,uint48,uint48),address,uint256),bytes): PermitSingle is static, so inlined
167
+ // Sibling shapes of the movers and grants above (reported privately
168
+ // 2026-11-02, 1.14.10, four reports): the legacy StandardToken
169
+ // increaseApproval, the ERC-1363 two-argument transferAndCall and
170
+ // approveAndCall and both transferFromAndCall shapes, the ERC-223 transfer
171
+ // with data, and the EIP-6909 multi-token movers and grants. Each moves the
172
+ // token or grants a standing allowance exactly as the shape already decoded
173
+ // beside it, and each fell through as a zero-value contract call.
174
+ d73dd623: { kind: 'approval', what: 'increaseApproval', toIndex: 0, amountIndex: 1 }, // increaseApproval(address,uint256)
175
+ '3177029f': { kind: 'approval', what: 'approveAndCall', toIndex: 0, amountIndex: 1 }, // approveAndCall(address,uint256) (ERC-1363)
176
+ '1296ee62': { kind: 'token', what: 'transferAndCall', toIndex: 0, amountIndex: 1 }, // transferAndCall(address,uint256) (ERC-1363)
177
+ d8fbe994: { kind: 'token', what: 'transferFromAndCall', toIndex: 1, amountIndex: 2 }, // transferFromAndCall(address,address,uint256) (ERC-1363)
178
+ c1d34b89: { kind: 'token', what: 'transferFromAndCall', toIndex: 1, amountIndex: 2 }, // transferFromAndCall(address,address,uint256,bytes) (ERC-1363)
179
+ be45fd62: { kind: 'token', what: 'ERC-223 transfer', toIndex: 0, amountIndex: 1 }, // transfer(address,uint256,bytes)
180
+ // EIP-6909: setOperator is the setApprovalForAll of a multi-token contract
181
+ // (unbounded by construction); the per-id approve and the movers carry an
182
+ // id word before the amount, and the contract has no decimals or price, so
183
+ // they are treated as the ERC-1155 shapes are: unpriced, recipient-checked.
184
+ '558a7297': { kind: 'approval', what: 'EIP-6909 setOperator', toIndex: 0, amountIndex: 1, nft: true, operator: true }, // setOperator(address,bool)
185
+ '426a8493': { kind: 'approval', what: 'EIP-6909 approve', toIndex: 0, amountIndex: 2, nft: true }, // approve(address,uint256,uint256)
186
+ '095bcdb6': { kind: 'token', what: 'EIP-6909 transfer', toIndex: 0, amountIndex: 2, nft: true }, // transfer(address,uint256,uint256)
187
+ fe99049a: { kind: 'token', what: 'EIP-6909 transferFrom', toIndex: 1, amountIndex: 3, nft: true }, // transferFrom(address,address,uint256,uint256)
188
+ // Redemptions (reported privately 2026-11-02, 1.14.10): a vault or lending
189
+ // position is the wallet's asset, and its withdraw and redeem calls name a
190
+ // receiver, so a zero-value call paid the whole position to any address
191
+ // with nothing reserved. An ERC-4626 vault is itself the share token, so
192
+ // the spend is recorded under the vault (shares for redeem; for withdraw
193
+ // the amount is in the underlying, printed at the vault's decimals, which
194
+ // the standard keeps equal to the underlying's in every common vault), and
195
+ // priced only if the registry knows it, which a vault never is: refused
196
+ // under a dollar budget unless unpriced_spends allows, recipient-checked
197
+ // either way. Aave's Pool.withdraw names the underlying in the calldata, so
198
+ // that one is priced like a transfer of it. Any other protocol's shape is
199
+ // outside this table: a policy that holds positions elsewhere should list
200
+ // recipients.allow and, where it can, the contracts it may call.
201
+ ba087652: { kind: 'token', what: 'ERC-4626 redeem', toIndex: 1, amountIndex: 0 }, // redeem(uint256,address,address)
202
+ b460af94: { kind: 'token', what: 'ERC-4626 withdraw', toIndex: 1, amountIndex: 0 }, // withdraw(uint256,address,address)
203
+ '69328dec': { kind: 'token', what: 'Aave withdraw', toIndex: 2, amountIndex: 1, tokenIndex: 0 }, // Pool.withdraw(address,uint256,address)
165
204
  };
166
205
  /* The rest of Permit2's surface, by selector: permitBatch, transferFrom (two
167
206
  shapes), lockdown, permitTransferFrom and permitWitnessTransferFrom (two
@@ -281,8 +320,15 @@ async function describeSend(body, chainId) {
281
320
  `refused rather than treated as a plain contract call. Use Permit2 approve or a single permit, each of which is reserved as an approval. Nothing was signed.`);
282
321
  }
283
322
  }
284
- if (data && data !== '0x')
285
- return { kind: 'contract', to, asset: '', raw: value, decimals: nativeDecimals(chainId) };
323
+ if (data && data !== '0x') {
324
+ // A shape the table does not know. Under a policy it is refused unless
325
+ // the operator listed the contract (assertSendTarget, 1.14.11); the
326
+ // chain's wrapped-native deposit and withdraw are known shapes, since
327
+ // wrap_eth and unwrap_eth build them.
328
+ const selector = /^0x[0-9a-fA-F]{8}/.test(data) ? data.slice(0, 10).toLowerCase() : data.slice(0, 10);
329
+ const isWrapped = to.toLowerCase() === wrappedNativeAddress(chainId) && WRAPPED_NATIVE_SELECTORS.has(selector.slice(2));
330
+ return { kind: 'contract', to, asset: '', raw: value, decimals: nativeDecimals(chainId), ...(isWrapped ? {} : { unknown: selector }) };
331
+ }
286
332
  return { kind: 'native', to, asset: '', raw: value, decimals: nativeDecimals(chainId) };
287
333
  }
288
334
  /** The ledger method a decoded send is recorded under, unless the caller named one. */
@@ -300,15 +346,28 @@ function assertSendTarget(d, chainId) {
300
346
  if (!c.ok)
301
347
  throw new Error(c.reason + ' Nothing was signed.');
302
348
  if (d.kind === 'contract' || d.kind === 'approval') {
303
- const t = targetAllowed(POL(), d.kind === 'approval' ? d.asset : d.to);
349
+ const t = targetAllowed(POL(), d.kind === 'approval' ? d.asset : d.to, chainId);
350
+ if (!t.ok)
351
+ throw new Error(t.reason + ' Nothing was signed.');
352
+ }
353
+ if (d.kind === 'token' && d.asset) {
354
+ const t = targetAllowed(POL(), d.asset, chainId); // the token or vault being moved; contracts.deny binds it
304
355
  if (!t.ok)
305
356
  throw new Error(t.reason + ' Nothing was signed.');
306
357
  }
307
358
  if (d.carried) {
308
- const t = targetAllowed(POL(), d.carried.to); // the contract that keeps the value
359
+ const t = targetAllowed(POL(), d.carried.to, chainId); // the contract that keeps the value
309
360
  if (!t.ok)
310
361
  throw new Error(t.reason + ' Nothing was signed.');
311
362
  }
363
+ // Calldata the table cannot price is the operator's call, not the table's
364
+ // (1.14.11). Without a policy nothing changes: the local caps and the
365
+ // hosted signer judge the call on their own terms, as before.
366
+ if (d.unknown && POLICY.path) {
367
+ const u = unknownCallAllowed(POL(), chainId, d.to, d.unknown);
368
+ if (!u.ok)
369
+ throw new Error(u.reason + ' Nothing was signed.');
370
+ }
312
371
  }
313
372
  /**
314
373
  * The spends a decoded send makes against the policy, in the order they are
@@ -401,7 +460,13 @@ function localChainId(body, qs) {
401
460
  const fromEnv = (process.env.AGENTWALLET_CHAIN_ID || '').trim();
402
461
  if (fromEnv)
403
462
  return parseInt(fromEnv, 10);
404
- return 8453; // Base, matching the custodial default
463
+ // Base. This used to say "matching the custodial default", and it did not:
464
+ // the hosted signer falls back to chain 1 when a body names no chain, so a
465
+ // chain-less hosted send was priced and ledgered here on 8453 and executed
466
+ // there on Ethereum (reported privately 2026-11-02, 1.14.10). The hosted
467
+ // path now writes the chain it priced into the body it forwards (api()), so
468
+ // whatever this returns, the custodian is told the same chain.
469
+ return 8453;
405
470
  }
406
471
  /**
407
472
  * Refuse rather than silently hand the operation to someone else's signer.
@@ -584,8 +649,18 @@ async function api(path, method = 'GET', body, extraHeaders) {
584
649
  // signature (body.message) moves nothing and is not gated.
585
650
  const m = /^\/wallets\/([^/]+)\/(send|sign)(\?|$)/.exec(path);
586
651
  const signsTx = m !== null && (m[2] === 'send' || typeof body?.message !== 'string');
652
+ let reservedChain = null;
587
653
  if (method === 'POST' && m && body && signsTx) {
588
654
  const chainId = localChainId(body, new URLSearchParams(path.split('?')[1] || ''));
655
+ // The custodian must execute on the chain the policy priced. A body with
656
+ // no chain_id used to go out as it came and the hosted signer chose its
657
+ // own default (chain 1), while the ledger said 8453 (reported privately
658
+ // 2026-11-02, 1.14.10). The chain is written into the body here, before
659
+ // the decode, so the two cannot differ; the custodian's answer is still
660
+ // compared with it below.
661
+ if (!anyLocalMode() && typeof body.chain_id !== 'number')
662
+ body = { ...body, chain_id: chainId };
663
+ reservedChain = chainId;
589
664
  const d = await describeSend(body, chainId);
590
665
  assertSendTarget(d, chainId);
591
666
  const tool = ctx.tool || (m[2] === 'sign' ? 'sign_transaction' : 'send');
@@ -604,12 +679,21 @@ async function api(path, method = 'GET', body, extraHeaders) {
604
679
  }
605
680
  try {
606
681
  const out = await apiInner(path, method, body, extraHeaders);
682
+ // A custodian that answers with a chain other than the one it was asked
683
+ // for executed somewhere the ledger did not count; the line cannot be
684
+ // re-booked (the spend is live), so the mismatch is written onto it and
685
+ // onto stderr for the operator.
686
+ const reported = out?.chain_id;
687
+ const mismatch = reserved.length && reservedChain !== null && typeof reported === 'number' && reported !== reservedChain
688
+ ? ` CHAIN MISMATCH: reserved on chain ${reservedChain}, the API host reports chain ${reported}; the budget of chain ${reported} did not count this spend.` : '';
689
+ if (mismatch)
690
+ console.error(`[agentwallet] ${path}:${mismatch}`);
607
691
  for (const seq of reserved) {
608
692
  const r = out;
609
693
  if (m[2] === 'sign')
610
- LEDGER.settle(seq, 'signed', 'raw transaction returned to the caller; not broadcast by this process, so it stays counted');
694
+ LEDGER.settle(seq, 'signed', 'raw transaction returned to the caller; not broadcast by this process, so it stays counted' + mismatch);
611
695
  else
612
- LEDGER.settle(seq, 'broadcast', typeof r?.tx_hash === 'string' ? r.tx_hash : typeof r?.signature === 'string' ? r.signature : null);
696
+ LEDGER.settle(seq, 'broadcast', (typeof r?.tx_hash === 'string' ? r.tx_hash : typeof r?.signature === 'string' ? r.signature : '') + mismatch || null);
613
697
  }
614
698
  return out;
615
699
  }
@@ -713,6 +797,21 @@ async function apiInner(path, method = 'GET', body, extraHeaders) {
713
797
  }
714
798
  if (!res.ok) {
715
799
  const error = serverText(data.error) || `HTTP ${res.status}`;
800
+ // The host was reached and answered. A 4xx is its refusal and the
801
+ // reservation is voided; a 5xx on a send or sign came after it took the
802
+ // request, and the custodian may have broadcast before whatever failed
803
+ // (a downstream write, a proxy), so the spend stays counted as unknown.
804
+ // Until 1.14.10 every non-2xx voided, and a 500 that carried the
805
+ // transaction hash in its body restored the whole budget and dropped
806
+ // the hash (reported privately 2026-11-02).
807
+ if (method === 'POST' && /^\/wallets\/[^/]+\/(send|sign)(\?|$)/.test(path)) {
808
+ const d = data;
809
+ const hash = looksLikeTxHash(d?.tx_hash) ? d.tx_hash : typeof d?.signature === 'string' && /^[1-9A-HJ-NP-Za-km-z]{43,120}$/.test(d.signature) ? d.signature : null;
810
+ if (hash)
811
+ throw markMaybeLive(new Error(`${error} The AgentWallet API host answered ${path} with HTTP ${res.status} but reported a transaction (${hash}), so the spend is live and stays counted; check the wallet's history before retrying.`));
812
+ if (res.status >= 500)
813
+ throw markMaybeLive(new Error(`${error} The AgentWallet API host answered ${path} with HTTP ${res.status} after taking the request; whether the custodian broadcast is unknown, so the spend stays counted. Check the wallet's history before retrying.`));
814
+ }
716
815
  throw new Error(error);
717
816
  }
718
817
  // A send that comes back without a hash was not sent, whatever the body says
@@ -893,12 +992,24 @@ async function handleX402Payment(x402Data, originalPath, originalMethod, origina
893
992
  const { tokenAddress, rawAmount, decimals } = deriveX402Payment(accept, maxAutopay, trustedDecimals);
894
993
  // A hostile or broken host that answers 402 on every call must not drain the
895
994
  // wallet one cap at a time: at most AUTOPAY_PER_HOUR payments an hour (round 3).
995
+ // The slot is taken here, synchronously, before anything is awaited: the
996
+ // count used to be read here and written after the payment, so eight calls
997
+ // pipelined at once all read 0 and all paid under a limit of 2 (reported
998
+ // privately 2026-11-02, 1.14.10). A payment that never left this process
999
+ // gives its slot back below; one that may be live keeps it. The ledger's
1000
+ // own auto-pay lines are counted too, so several processes sharing one
1001
+ // ledger share the hour; without a ledger file the count is per process.
896
1002
  const hourAgo = Date.now() - 3_600_000;
897
1003
  while (autopayLog.length && autopayLog[0] < hourAgo)
898
1004
  autopayLog.shift();
899
- if (autopayLog.length >= AUTOPAY_PER_HOUR) {
900
- throw new Error(`x402 auto-pay: ${autopayLog.length} automatic payments were already made in the last hour (AGENTWALLET_MAX_AUTOPAY_PER_HOUR=${AUTOPAY_PER_HOUR}); refusing another. Original error: ${serverText(x402Data.error)}`);
901
- }
1005
+ const made = Math.max(autopayLog.length, LEDGER.countSince('autopay', hourAgo));
1006
+ if (made >= AUTOPAY_PER_HOUR) {
1007
+ throw new Error(`x402 auto-pay: ${made} automatic payments were already made in the last hour (AGENTWALLET_MAX_AUTOPAY_PER_HOUR=${AUTOPAY_PER_HOUR}); refusing another. Original error: ${serverText(x402Data.error)}`);
1008
+ }
1009
+ const slot = Date.now();
1010
+ autopayLog.push(slot);
1011
+ const giveBack = () => { const i = autopayLog.indexOf(slot); if (i >= 0)
1012
+ autopayLog.splice(i, 1); };
902
1013
  // Send payment using our wallet (with X-AGW-SKIP-X402 to prevent recursion).
903
1014
  // The ledger context names the API host and route; the /send hook reserves and settles the spend.
904
1015
  const ctx = { 'X-AGW-SKIP-X402': 'true', 'X-AGW-CTX': JSON.stringify({ tool: 'autopay', host: hostOf(API_BASE), resource: `${originalMethod} ${originalPath.split('?')[0]}`, method: 'autopay' }) };
@@ -922,12 +1033,13 @@ async function handleX402Payment(x402Data, originalPath, originalMethod, origina
922
1033
  }
923
1034
  }
924
1035
  catch (e) {
1036
+ if (!isMaybeLive(e))
1037
+ giveBack(); // refused, or never reached the host: the slot is free again
925
1038
  throw new Error(`x402 auto-pay failed: ${e.message}. Original error: ${serverText(x402Data.error)}`);
926
1039
  }
927
1040
  if (!txHash) {
928
- throw new Error('x402 auto-pay: no transaction hash returned.');
1041
+ throw new Error('x402 auto-pay: no transaction hash returned.'); // the host answered the send; the slot stays taken
929
1042
  }
930
- autopayLog.push(Date.now());
931
1043
  const autopayNotice = {
932
1044
  amount: formatUnits(rawAmount, decimals), token: assetLabel(chainId, tokenAddress), pay_to: payTo, chain_id: chainId, tx_hash: txHash,
933
1045
  route: `${originalMethod} ${originalPath}`,
@@ -1287,7 +1399,7 @@ function resolveChainId(network) {
1287
1399
  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');
1288
1400
  const server = new McpServer({
1289
1401
  name: 'agentwallet',
1290
- version: '1.14.9',
1402
+ version: '1.14.11',
1291
1403
  }, {
1292
1404
  instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
1293
1405
  ? '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.'
package/build/ledger.d.ts CHANGED
@@ -110,6 +110,8 @@ export declare class Ledger {
110
110
  private liveSpends;
111
111
  /** Dollar sums over rolling windows from a fresh read of the file; nowMs for tests. */
112
112
  spentUsd(nowMs?: number, host?: string): Spent;
113
+ /** Live spends (not voided) with this method written since `sinceMs`, from a fresh read: the hourly auto-pay count across every process sharing the file (1.14.10). */
114
+ countSince(method: SpendMethod, sinceMs: number): number;
113
115
  spentNative(chainId: number, nowMs?: number): Spent;
114
116
  private sumUsd;
115
117
  private sumNative;
package/build/ledger.js CHANGED
@@ -185,6 +185,11 @@ export class Ledger {
185
185
  this.sync();
186
186
  return this.sumUsd(nowMs, host);
187
187
  }
188
+ /** Live spends (not voided) with this method written since `sinceMs`, from a fresh read: the hourly auto-pay count across every process sharing the file (1.14.10). */
189
+ countSince(method, sinceMs) {
190
+ this.sync();
191
+ return this.liveSpends().filter(e => e.method === method && Date.parse(e.ts) >= sinceMs).length;
192
+ }
188
193
  spentNative(chainId, nowMs = Date.now()) {
189
194
  this.sync();
190
195
  return this.sumNative(chainId, nowMs);
@@ -223,9 +228,21 @@ export class Ledger {
223
228
  return { ok: false, reason: `The ledger at ${this.path} is not intact (${this.integrity}); every spend is refused until the operator has looked at it.` };
224
229
  const amount18 = rawToUnits(input.raw, input.decimals);
225
230
  if (input.asset === '') {
226
- const r = refusal(limits.native, this.sumNative(input.chainId, nowMs), amount18, `native_budget.${input.chainId}`);
227
- if (r)
228
- return { ok: false, reason: r };
231
+ // The chain's own coin is counted by native_budget alone; the dollar
232
+ // budget has no price for it. Until 1.14.10 a chain with no
233
+ // native_budget entry fell through here with nothing checked, so a
234
+ // policy of "budget daily 0.5, unpriced_spends refuse" bounded every
235
+ // stablecoin payment and no native one (reported privately
236
+ // 2026-11-02). Now such a spend is what it is to the dollar budget, an
237
+ // unpriced spend, and the unpriced rule decides it.
238
+ if (limits.native) {
239
+ const r = refusal(limits.native, this.sumNative(input.chainId, nowMs), amount18, `native_budget.${input.chainId}`);
240
+ if (r)
241
+ return { ok: false, reason: r };
242
+ }
243
+ else if (limits.unpriced === 'refuse' && (limits.usd || limits.host)) {
244
+ return { ok: false, reason: `policy: ${input.assetLabel} on chain ${input.chainId} is the chain's own coin, which the dollar budget cannot price, and the policy has no native_budget.${input.chainId}; refused (add native_budget.${input.chainId} to the policy to count it, or set unpriced_spends to "allow" to let native spends through uncounted). Nothing was signed.` };
245
+ }
229
246
  }
230
247
  else if (input.usd === null) {
231
248
  if (limits.unpriced === 'refuse' && (limits.usd || limits.host)) {
@@ -337,7 +337,8 @@ const PERMIT2_SELECTORS = new Set(['87517c45', '2b67b570', '2a2d80d1', '36c78516
337
337
  and so read as "not a token" to the cap, which let setApprovalForAll to an
338
338
  attacker through uncapped (2026-10-04 round 2, verified on a live
339
339
  collection). Nothing prices an NFT, so these are refused outright. */
340
- const NFT_SELECTORS = new Set(['a22cb465', '42842e0e', 'b88d4fde', 'f242432a', '2eb2c2d6', '959b8c3f']); // plus ERC-777 authorizeOperator: an unbounded delegation
340
+ const NFT_SELECTORS = new Set(['a22cb465', '42842e0e', 'b88d4fde', 'f242432a', '2eb2c2d6', '959b8c3f', // plus ERC-777 authorizeOperator: an unbounded delegation
341
+ '558a7297', '426a8493', '095bcdb6', 'fe99049a']); // EIP-6909 setOperator, approve, transfer, transferFrom: a multi-token contract has no decimals() either (1.14.10)
341
342
  const CAP_LAYOUTS = {
342
343
  a9059cbb: { amountIndex: 1, what: 'token transfer' }, // transfer(address,uint256)
343
344
  '23b872dd': { amountIndex: 2, what: 'token transfer' }, // transferFrom(address,address,uint256)
@@ -358,6 +359,15 @@ const CAP_LAYOUTS = {
358
359
  // and are refused by the rule below, as every unpriced call to a token or to
359
360
  // Permit2 is; the spend policy classifies both.
360
361
  d505accf: { amountIndex: 2, what: 'approval (EIP-2612 permit)' }, // permit(address,address,uint256,uint256,uint8,bytes32,bytes32)
362
+ // Sibling shapes priced by the spend policy since 1.14.10 (reported
363
+ // privately 2026-11-02): the same amount word, the same treatment, so the
364
+ // two guards keep pricing the same calls.
365
+ d73dd623: { amountIndex: 1, what: 'approval (increaseApproval)' }, // increaseApproval(address,uint256)
366
+ '3177029f': { amountIndex: 1, what: 'approval (approveAndCall)' }, // approveAndCall(address,uint256)
367
+ '1296ee62': { amountIndex: 1, what: 'token transfer (transferAndCall)' }, // transferAndCall(address,uint256)
368
+ d8fbe994: { amountIndex: 2, what: 'token transfer (transferFromAndCall)' }, // transferFromAndCall(address,address,uint256)
369
+ c1d34b89: { amountIndex: 2, what: 'token transfer (transferFromAndCall)' }, // transferFromAndCall(address,address,uint256,bytes)
370
+ be45fd62: { amountIndex: 1, what: 'token transfer (ERC-223)' }, // transfer(address,uint256,bytes)
361
371
  // WETH9 calls, priced only when `to` is the chain's own wrapped-native
362
372
  // contract (wrappedNativeOnly). deposit() carries its amount as msg.value,
363
373
  // not in calldata, hence amountIndex -1; withdraw(uint256) has it at word 0.
@@ -416,7 +426,7 @@ async function assertWithinTokenCap(chainId, to, data, valueWei = 0n) {
416
426
  `or ask the operator, who alone can set AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1.`);
417
427
  }
418
428
  if (NFT_SELECTORS.has(selector)) {
419
- throw new Error(`Blocked by local guard: calldata selector 0x${selector} is an NFT approval or transfer (ERC-721/1155) aimed at ${to}; AGENTWALLET_MAX_TX_TOKEN cannot price it, ` +
429
+ throw new Error(`Blocked by local guard: calldata selector 0x${selector} is an NFT approval or transfer (ERC-721/1155, or an EIP-6909 multi-token call) aimed at ${to}; AGENTWALLET_MAX_TX_TOKEN cannot price it, ` +
420
430
  `so it is refused rather than let through uncapped. Only the operator can allow it (AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1).`);
421
431
  }
422
432
  if (PERMIT2_SELECTORS.has(selector)) {
package/build/policy.d.ts CHANGED
@@ -35,6 +35,14 @@ export declare const PolicySchema: z.ZodObject<{
35
35
  assets: z.ZodOptional<z.ZodObject<{
36
36
  allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
37
37
  }, z.core.$strict>>;
38
+ contracts: z.ZodOptional<z.ZodObject<{
39
+ allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
40
+ deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
41
+ }, z.core.$strict>>;
42
+ unknown_calls: z.ZodOptional<z.ZodEnum<{
43
+ allow: "allow";
44
+ refuse: "refuse";
45
+ }>>;
38
46
  approval: z.ZodOptional<z.ZodObject<{
39
47
  above: z.ZodOptional<z.ZodString>;
40
48
  max: z.ZodOptional<z.ZodString>;
@@ -96,7 +104,22 @@ export declare function recipientAllowed(p: Policy, to: string): {
96
104
  reason: string;
97
105
  };
98
106
  /** A contract target (approve, wrap, arbitrary calldata): the deny list only; an allow list is about where funds go. */
99
- export declare function targetAllowed(p: Policy, to: string): {
107
+ export declare function targetAllowed(p: Policy, to: string, chainId?: number): {
108
+ ok: true;
109
+ } | {
110
+ ok: false;
111
+ reason: string;
112
+ };
113
+ /**
114
+ * May the wallet send calldata the policy cannot price to this contract?
115
+ * Since 1.14.11 the answer under a policy is no unless the operator listed
116
+ * the contract or set unknown_calls to "allow": the classifier is a table of
117
+ * known shapes, and every shape it did not know was, until then, a
118
+ * zero-value call that reserved nothing (nineteen disclosure rows between
119
+ * 2026-10-14 and 2026-11-02 were that one gap). The open set is now the
120
+ * operator's decision, not the table's.
121
+ */
122
+ export declare function unknownCallAllowed(p: Policy, chainId: number, to: string, selector: string): {
100
123
  ok: true;
101
124
  } | {
102
125
  ok: false;
package/build/policy.js CHANGED
@@ -35,6 +35,10 @@ export const PolicySchema = z.object({
35
35
  recipients: z.object({ allow: z.array(Address).max(1000).optional(), deny: z.array(Address).max(1000).optional() }).strict().optional(),
36
36
  chains: z.object({ allow: z.array(z.number().int().positive()).max(100).optional() }).strict().optional(),
37
37
  assets: z.object({ allow: z.array(AssetRef).max(500).optional() }).strict().optional(),
38
+ /** Contracts the wallet may call with calldata the policy cannot price (a router, a bridge, a protocol). Entries may be scoped "chainId:0x...". deny refuses any call to the contract, priced or not. */
39
+ contracts: z.object({ allow: z.array(AssetRef).max(1000).optional(), deny: z.array(AssetRef).max(1000).optional() }).strict().optional(),
40
+ /** What to do with calldata the policy cannot price, aimed at a contract not in contracts.allow. Default refuse (1.14.11); "allow" restores the pre-1.14.11 behaviour of passing it as a zero-value call. */
41
+ unknown_calls: z.enum(['refuse', 'allow']).optional(),
38
42
  /** Ask the human (MCP elicitation) above `above`; in local mode an accepted ask may admit up to `max`, above the per-payment cap. */
39
43
  approval: z.object({ above: Dec.optional(), max: Dec.optional(), timeout_seconds: z.number().int().min(30).max(3600).optional() }).strict().optional(),
40
44
  tools: z.object({ allow: z.array(z.string().regex(/^[a-z_0-9]+$/)).max(100).optional(), deny: z.array(z.string().regex(/^[a-z_0-9]+$/)).max(100).optional() }).strict().optional(),
@@ -102,6 +106,12 @@ export function parsePolicy(text) {
102
106
  throw new Error('policy field hosts.allow: an empty allow list would refuse every host; remove it or list hosts.');
103
107
  if (p.recipients?.allow && p.recipients.allow.length === 0)
104
108
  throw new Error('policy field recipients.allow: an empty allow list would refuse every recipient; remove it or list addresses.');
109
+ if (p.contracts?.allow && p.contracts.allow.length === 0)
110
+ throw new Error('policy field contracts.allow: an empty allow list is the default (unpriced calls refused); remove it or list contracts.');
111
+ for (const k of ['allow', 'deny'])
112
+ for (const e of p.contracts?.[k] || [])
113
+ if (e.endsWith('native'))
114
+ throw new Error(`policy field contracts.${k}: "${e}" is not a contract.`);
105
115
  return p;
106
116
  }
107
117
  /** Load the policy named by AGENTWALLET_POLICY (none set: EMPTY). The ledger path comes from AGENTWALLET_LEDGER, then the policy, then a default next to the policy when a budget needs one. */
@@ -181,11 +191,40 @@ export function recipientAllowed(p, to) {
181
191
  return { ok: true };
182
192
  }
183
193
  /** A contract target (approve, wrap, arbitrary calldata): the deny list only; an allow list is about where funds go. */
184
- export function targetAllowed(p, to) {
194
+ export function targetAllowed(p, to, chainId) {
185
195
  if (p.recipients?.deny?.some(x => sameAddress(x, to)))
186
196
  return { ok: false, reason: `policy denies contract target ${to} (recipients.deny).` };
197
+ if (chainId !== undefined && p.contracts?.deny?.some(e => contractRefMatches(e, chainId, to)))
198
+ return { ok: false, reason: `policy denies contract ${to} on chain ${chainId} (contracts.deny).` };
187
199
  return { ok: true };
188
200
  }
201
+ /** "8453:0xabc" matches that contract on that chain; a bare address matches it on every chain. */
202
+ function contractRefMatches(entry, chainId, to) {
203
+ const i = entry.indexOf(':');
204
+ if (i < 0)
205
+ return sameAddress(entry, to);
206
+ return Number(entry.slice(0, i)) === chainId && sameAddress(entry.slice(i + 1), to);
207
+ }
208
+ /**
209
+ * May the wallet send calldata the policy cannot price to this contract?
210
+ * Since 1.14.11 the answer under a policy is no unless the operator listed
211
+ * the contract or set unknown_calls to "allow": the classifier is a table of
212
+ * known shapes, and every shape it did not know was, until then, a
213
+ * zero-value call that reserved nothing (nineteen disclosure rows between
214
+ * 2026-10-14 and 2026-11-02 were that one gap). The open set is now the
215
+ * operator's decision, not the table's.
216
+ */
217
+ export function unknownCallAllowed(p, chainId, to, selector) {
218
+ if (p.contracts?.allow?.some(e => contractRefMatches(e, chainId, to)))
219
+ return { ok: true };
220
+ if ((p.unknown_calls ?? 'refuse') === 'allow')
221
+ return { ok: true };
222
+ return {
223
+ ok: false,
224
+ reason: `policy: calldata selector ${selector} to ${to} on chain ${chainId} is not a shape the policy can price (a transfer, an approval, a permit, a redemption), and the policy does not list the contract in contracts.allow; ` +
225
+ `refused rather than passed as a zero-value call. The operator can add "${chainId}:${to}" to contracts.allow, or set unknown_calls to "allow" to let every unpriced call through as before 1.14.11.`,
226
+ };
227
+ }
189
228
  export function chainAllowed(p, chainId) {
190
229
  if (p.chains?.allow && !p.chains.allow.includes(chainId))
191
230
  return { ok: false, reason: `policy does not allow chain ${chainId}: it is not in chains.allow.` };
@@ -245,6 +284,8 @@ export function describePolicy(lp) {
245
284
  recipients: p.recipients ? { allow: p.recipients.allow?.length ?? null, deny: p.recipients.deny?.length ?? null } : null,
246
285
  chains: p.chains ?? null,
247
286
  assets: p.assets ?? null,
287
+ contracts: p.contracts ? { allow: p.contracts.allow?.length ?? null, deny: p.contracts.deny?.length ?? null } : null,
288
+ unknown_calls: lp.path ? (p.unknown_calls ?? 'refuse') : 'allow (no policy set)',
248
289
  approval: p.approval ?? null,
249
290
  tools: p.tools ?? null,
250
291
  ledger: lp.ledgerPath,
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.9",
8
+ "version": "1.14.11",
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",