agentwallet-mcp 1.14.7 → 1.14.9

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
@@ -43,7 +43,7 @@ Use `AGENTWALLET_KEYFILE=/path/to/key` instead if you would rather keep the key
43
43
 
44
44
  `AGENTWALLET_MAX_TX_NATIVE` is a per-transaction ceiling in native units (ETH, MATIC and so on). In hosted mode the server enforces limits; in local mode there is no server, so these guards are the only ones there are. Set them.
45
45
 
46
- `AGENTWALLET_MAX_TX_TOKEN` is the equivalent ceiling for ERC-20 movement, in human units of the token. **Set this one too if you hold stablecoins.** The native cap cannot see a token transfer: an ERC-20 send carries `value = 0` with the amount in the calldata, so `AGENTWALLET_MAX_TX_NATIVE` alone leaves a USDC balance uncapped. `AGENTWALLET_MAX_TX_TOKEN` covers `transfer`, `transferFrom`, `approve`, `increaseAllowance`, Permit2 `approve`, and `deposit()` / `withdraw(uint256)` on the chain's own wrapped-native contract (so `wrap_eth` and `unwrap_eth` keep working under the cap), the approvals because an unbounded allowance is a drain waiting to happen. Any other calldata sent to a token contract or to Permit2 is refused while the cap is set, because the guard cannot price it; set `AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1` to allow such calls deliberately. `increaseAllowance` is judged on the resulting total: the current allowance is read first and the call refuses if it cannot be, so two compliant increments cannot stack a standing allowance above the cap. Calls to other contracts (routers, bridges) pass, since they can only pull what an approval already allowed; a contract counts as "other" only when it demonstrably declines `decimals()`, so an absurd or malformed answer, or an RPC that cannot be reached, refuses rather than allows. Decimals are resolved locally from the trusted registry, then the token's own `decimals()`; a token that resolves neither way is evaluated at 0 decimals, the only value that cannot fail open. For a token outside the registry the RPC is the only source of decimals, and an RPC you do not control could scale the amount and the cap together; pin such tokens with `AGENTWALLET_TOKEN_DECIMALS` (for example `8453:0x<token>=6`) and the pin is used everywhere the RPC's answer would have been.
46
+ `AGENTWALLET_MAX_TX_TOKEN` is the equivalent ceiling for ERC-20 movement, in human units of the token. **Set this one too if you hold stablecoins.** The native cap cannot see a token transfer: an ERC-20 send carries `value = 0` with the amount in the calldata, so `AGENTWALLET_MAX_TX_NATIVE` alone leaves a USDC balance uncapped. `AGENTWALLET_MAX_TX_TOKEN` covers `transfer`, `transferFrom`, `approve`, `increaseAllowance`, EIP-2612 `permit`, Permit2 `approve`, and `deposit()` / `withdraw(uint256)` on the chain's own wrapped-native contract (so `wrap_eth` and `unwrap_eth` keep working under the cap), the approvals because an unbounded allowance is a drain waiting to happen. ERC-721 and ERC-1155 calldata (`setApprovalForAll`, `safeTransferFrom`) is refused while the cap is set, since nothing prices an NFT; without the cap it passes the way an ERC-20 transfer of any size does, except that an operator grant is an unlimited approval and is refused everywhere by default (see `AGENTWALLET_ALLOW_UNLIMITED_APPROVAL`). Any other calldata sent to a token contract or to Permit2 is refused while the cap is set, because the guard cannot price it; set `AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1` to allow such calls deliberately. `increaseAllowance` is judged on the resulting total: the current allowance is read first and the call refuses if it cannot be, so two compliant increments cannot stack a standing allowance above the cap. Calls to other contracts (routers, bridges) pass, since they can only pull what an approval already allowed; a contract counts as "other" only when it demonstrably declines `decimals()`, so an absurd or malformed answer, or an RPC that cannot be reached, refuses rather than allows. Decimals are resolved locally from the trusted registry, then the token's own `decimals()`; a token that resolves neither way is evaluated at 0 decimals, the only value that cannot fail open. For a token outside the registry the RPC is the only source of decimals, and an RPC you do not control could scale the amount and the cap together; pin such tokens with `AGENTWALLET_TOKEN_DECIMALS` (for example `8453:0x<token>=6`) and the pin is used everywhere the RPC's answer would have been.
47
47
 
48
48
  ### Solana local signing
49
49
 
@@ -340,6 +340,7 @@ Which guards apply depends on who holds the key.
340
340
  - `AGENTWALLET_MAX_FEE_NATIVE`: the most one transaction may burn in fees, gas x max fee per gas, in native units (default `0.01`). The value cap alone did not bound fees: a node answering an absurd tip or gas estimate could take the whole balance as priority fee on a cap-compliant transfer. Caller-supplied `gas_limit` / `max_fee` / `priority_fee` are honoured and then bounded by this cap
341
341
  - `AGENTWALLET_MAX_TX_TOKEN`: ceiling per ERC-20 transfer or approval, in human units of the token
342
342
  - `AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS`: set to `1` to let calldata the token cap cannot price reach a token contract or Permit2 (refused by default while the cap is set)
343
+ - `AGENTWALLET_ALLOW_UNLIMITED_APPROVAL`: set to `1` to let an unlimited ERC-20 allowance (`approve_token` with `"max"`, `approve_permit2` with no amount) or an NFT operator grant (`setApprovalForAll`) through while no spend policy is set; refused by default, and always refused under a policy
343
344
  - `AGENTWALLET_TOKEN_DECIMALS`: pin decimals for tokens outside the registry (`8453:0x<token>=6,<mint>=9`); a pin is used for amounts and caps instead of asking the RPC
344
345
  - `AGENTWALLET_MAX_TX_SOL`: ceiling per native SOL transfer (also charged for the rent of a recipient token account an SPL send has to create). It does not cover SPL token amounts: `AGENTWALLET_MAX_TX_TOKEN` does, on Solana as on EVM
345
346
  - `AGENTWALLET_MAX_AUTOPAY`: ceiling per x402 auto-payment in stablecoin units (default `1`); `AGENTWALLET_AUTOPAY_ASSETS` opts native or non-stable assets in; `AGENTWALLET_X402_MAX_TIMEOUT` bounds how long a signed authorization stays valid (default 3600 seconds)
@@ -375,7 +376,7 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
375
376
 
376
377
  - **`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.
377
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).
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
+ - **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
380
  - **`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.
380
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.
381
382
  - **`chains`**, **`assets`**: allow lists; an asset entry can be scoped `"8453:0x..."` or be the word `native`.
package/build/index.js CHANGED
@@ -13,13 +13,15 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
14
  import { z } from 'zod';
15
15
  // Validation errors used to echo an unknown key or enum value unbounded, a reflector for a second-stage injection (round 6).
16
- z.setErrorMap((issue, ctx) => {
17
- const cut = (v) => { const t = String(v); return t.length > 80 ? t.slice(0, 80) + '…' : t; };
18
- if (issue.code === 'unrecognized_keys')
19
- return { message: `Unrecognized key(s) in object: ${issue.keys.map(k => `'${cut(k)}'`).join(', ')}` };
20
- if (issue.code === 'invalid_enum_value')
21
- return { message: `Invalid enum value. Expected ${issue.options.map(o => `'${o}'`).join(' | ')}, received '${cut(issue.received)}'` };
22
- return { message: ctx.defaultError };
16
+ z.config({
17
+ customError: (issue) => {
18
+ const cut = (v) => { const t = String(v); return t.length > 80 ? t.slice(0, 80) + '…' : t; };
19
+ if (issue.code === 'unrecognized_keys')
20
+ return `Unrecognized key(s) in object: ${issue.keys.map((k) => `'${cut(k)}'`).join(', ')}`;
21
+ if (issue.code === 'invalid_value')
22
+ return `Invalid enum value. Expected ${issue.values.map((o) => `'${String(o)}'`).join(' | ')}, received '${cut(issue.input)}'`;
23
+ return undefined;
24
+ },
23
25
  });
24
26
  import { assertPublicUrl, safeFetch, finalUrlOf } from './ssrf-guard.js';
25
27
  import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDecimals, nativeDecimals, SOLANA_CHAIN_IDS, assetLabel, autopayAssetAllowed, autopayEnvCap, effectiveAutopayCap, maxAuthWindowSeconds, parseDecimalPins, approvalRefusal, authorizationResource, resolveNetworkChainId, normalizeRawAmount, parseAutopayAssets, assetSupportsExact, approvalResourceUrl, } from './x402-payment.js';
@@ -132,8 +134,68 @@ const SPEND_LAYOUTS = {
132
134
  '39509351': { kind: 'approval', what: 'increaseAllowance', toIndex: 0, amountIndex: 1 }, // increaseAllowance(address,uint256)
133
135
  cae9ca51: { kind: 'approval', what: 'approveAndCall', toIndex: 0, amountIndex: 1 }, // approveAndCall(address,uint256,bytes)
134
136
  '87517c45': { kind: 'approval', what: 'Permit2 approve', toIndex: 1, amountIndex: 2, tokenIndex: 0, permit2Only: true }, // Permit2.approve(address,address,uint160,uint48)
137
+ // ERC-721 / ERC-1155 movers and operator grants (reported privately
138
+ // 2026-10-26, 1.14.8): none of these was decoded, so an NFT approval or
139
+ // transfer fell through to 'contract' with value 0 and reached the signer
140
+ // under a policy with no budget line, no ledger line and no recipient check.
141
+ // An operator grant is an unbounded approval by construction (`operator`;
142
+ // amountIndex -1 means there is no amount word, otherwise the bool at that
143
+ // word says grant or revoke). A transfer moves a token no budget can price
144
+ // (`nft`; amountIndex -1 means one token), so under a dollar budget it is an
145
+ // unpriced spend, refused unless unpriced_spends allows it, and its
146
+ // recipient is checked either way.
147
+ a22cb465: { kind: 'approval', what: 'setApprovalForAll', toIndex: 0, amountIndex: 1, nft: true, operator: true }, // setApprovalForAll(address,bool)
148
+ '959b8c3f': { kind: 'approval', what: 'ERC-777 authorizeOperator', toIndex: 0, amountIndex: -1, operator: true }, // authorizeOperator(address)
149
+ '42842e0e': { kind: 'token', what: 'ERC-721 safeTransferFrom', toIndex: 1, amountIndex: -1, nft: true }, // safeTransferFrom(address,address,uint256)
150
+ b88d4fde: { kind: 'token', what: 'ERC-721 safeTransferFrom', toIndex: 1, amountIndex: -1, nft: true }, // safeTransferFrom(address,address,uint256,bytes)
151
+ f242432a: { kind: 'token', what: 'ERC-1155 safeTransferFrom', toIndex: 1, amountIndex: 3, nft: true }, // safeTransferFrom(address,address,uint256,uint256,bytes)
152
+ '2eb2c2d6': { kind: 'token', what: 'ERC-1155 safeBatchTransferFrom', toIndex: 1, amountIndex: 3, nft: true, batch: true }, // safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)
153
+ // Signed allowances (reported privately 2026-11-01, 1.14.9): permit() sets
154
+ // allowance[owner][spender] exactly as approve() does, and the wallet can
155
+ // produce the owner's signature in-process, so one send_transaction both
156
+ // signs and broadcasts a standing allowance. None of these was decoded, so
157
+ // a permit fell through as a zero-value contract call with no budget line,
158
+ // no ledger line, no spender check and no unlimited refusal: the 1.14.1
159
+ // bypass again, through a selector that fix did not cover. The owner word
160
+ // is not compared with the wallet: a permit relayed for another owner is
161
+ // counted as if it were the wallet's own, which over-counts, never under.
162
+ d505accf: { kind: 'approval', what: 'EIP-2612 permit', toIndex: 1, amountIndex: 2 }, // permit(address,address,uint256,uint256,uint8,bytes32,bytes32)
163
+ '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
+ '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
165
+ };
166
+ /* The rest of Permit2's surface, by selector: permitBatch, transferFrom (two
167
+ shapes), lockdown, permitTransferFrom and permitWitnessTransferFrom (two
168
+ each), invalidateNonces and invalidateUnorderedNonces. Each moves tokens or
169
+ changes what may be moved, in shapes this table does not price (a batch is
170
+ several allowances in one call), so under a policy such a call to the
171
+ chain's Permit2 is refused rather than let through as a zero-value call to
172
+ "some contract" (1.14.9). The local token cap has refused the same calls
173
+ since the 2026-10-04 audit. */
174
+ const PERMIT2_OTHER = {
175
+ '2a2d80d1': 'permit (batch)', '36c78516': 'transferFrom', '0d58b1db': 'transferFrom (batch)', cc53287f: 'lockdown',
176
+ '30f28b7a': 'permitTransferFrom', edd9444b: 'permitTransferFrom (batch)', '137c29fe': 'permitWitnessTransferFrom', fe8ec1a7: 'permitWitnessTransferFrom (batch)',
177
+ '3ff9dcb1': 'invalidateNonces', '65d9723c': 'invalidateUnorderedNonces',
135
178
  };
136
179
  const UNBOUNDED_APPROVAL = 1n << 128n;
180
+ const UINT256_MAX = (1n << 256n) - 1n;
181
+ /**
182
+ * The sum of the `amounts` array of an ERC-1155 safeBatchTransferFrom, read
183
+ * through the caller's word reader so a short word refuses the whole call.
184
+ * `offsetWord` holds the array's byte offset from the start of the arguments.
185
+ */
186
+ function batchTotal(wordAt, offsetWord) {
187
+ const offset = BigInt('0x' + wordAt(offsetWord));
188
+ if (offset % 32n !== 0n || offset > 1n << 20n)
189
+ throw new Error('policy: calldata for ERC-1155 safeBatchTransferFrom has a malformed amounts offset; refused rather than treated as a plain contract call. Nothing was signed.');
190
+ const base = Number(offset / 32n);
191
+ const count = BigInt('0x' + wordAt(base));
192
+ if (count > 4096n)
193
+ throw new Error('policy: calldata for ERC-1155 safeBatchTransferFrom names more than 4096 amounts; refused. Nothing was signed.');
194
+ let total = 0n;
195
+ for (let i = 0; i < Number(count); i++)
196
+ total += BigInt('0x' + wordAt(base + 1 + i));
197
+ return total;
198
+ }
137
199
  async function describeSend(body, chainId) {
138
200
  if (body.token_mint) {
139
201
  const mint = String(body.token_mint);
@@ -175,14 +237,27 @@ async function describeSend(body, chainId) {
175
237
  };
176
238
  const token = layout.tokenIndex === undefined ? to : '0x' + wordAt(layout.tokenIndex).slice(24);
177
239
  const counterparty = '0x' + wordAt(layout.toIndex).slice(24);
178
- const amount = BigInt('0x' + wordAt(layout.amountIndex));
179
240
  // The call's own value does not vanish because the calldata was
180
241
  // recognised: the contract keeps whatever rides along. It is a second
181
242
  // spend, native, to the contract (reported privately 2026-10-23, 1.14.4).
182
243
  const carried = BigInt(value) > 0n ? { raw: value, to } : undefined;
244
+ if (layout.operator) {
245
+ // setApprovalForAll(operator, false) takes a grant away and moves
246
+ // nothing: a zero-value call to the collection, reserving nothing.
247
+ const granted = layout.amountIndex < 0 || BigInt('0x' + wordAt(layout.amountIndex)) !== 0n;
248
+ if (!granted)
249
+ return { kind: 'contract', to, asset: '', raw: value, decimals: nativeDecimals(chainId) };
250
+ return { kind: 'approval', to: counterparty, asset: token, raw: UINT256_MAX.toString(), decimals: 0, unbounded: true, ...(carried ? { carried } : {}) };
251
+ }
252
+ const amount = layout.amountIndex < 0 ? 1n : layout.batch ? batchTotal(wordAt, layout.amountIndex) : BigInt('0x' + wordAt(layout.amountIndex));
183
253
  if (layout.kind === 'approval' && amount >= UNBOUNDED_APPROVAL) {
184
254
  return { kind: 'approval', to: counterparty, asset: token, raw: amount.toString(), decimals: 0, unbounded: true, ...(carried ? { carried } : {}) };
185
255
  }
256
+ if (layout.nft) {
257
+ // A collection has no decimals and no price: whole tokens, unpriced,
258
+ // to the recipient, recorded under the collection's address.
259
+ return { kind: layout.kind, to: counterparty, asset: token, raw: amount.toString(), decimals: 0, ...(carried ? { carried } : {}) };
260
+ }
186
261
  // Decimals matter here only to price a registry stablecoin (resolved
187
262
  // offline) and to print the ledger amount. A token the registry and the
188
263
  // node cannot place is unpriced whatever its decimals, so a failed lookup
@@ -198,6 +273,13 @@ async function describeSend(body, chainId) {
198
273
  }
199
274
  return { kind: layout.kind, to: counterparty, asset: token, raw: amount.toString(), decimals, ...(carried ? { carried } : {}) };
200
275
  }
276
+ // Every priced layout has returned by now; a Permit2 target here is a call
277
+ // the table does not know.
278
+ if (isPermit2 && POLICY.path) {
279
+ const other = PERMIT2_OTHER[data.slice(2, 10).toLowerCase()];
280
+ throw new Error(`policy: calldata selector 0x${data.slice(2, 10)} aimed at Permit2 is ${other ? `Permit2 ${other}` : 'not a Permit2 call this policy can price'}, which moves tokens or changes what may be moved in a shape the policy cannot count; ` +
281
+ `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
+ }
201
283
  }
202
284
  if (data && data !== '0x')
203
285
  return { kind: 'contract', to, asset: '', raw: value, decimals: nativeDecimals(chainId) };
@@ -238,12 +320,25 @@ function assertSendTarget(d, chainId) {
238
320
  * carried on a token or approval call is a second spend, native, to the
239
321
  * called contract (1.14.4); the rent a local SPL send pays for the recipient's
240
322
  * token account is a native spend to the recipient, method 'rent' (1.14.7).
241
- * Throws for an unbounded approval under a policy.
323
+ * Throws for an unbounded approval: under a policy always, and otherwise
324
+ * unless the operator allowed it.
242
325
  */
326
+ const unlimitedApprovalsAllowed = () => !POLICY.path && process.env.AGENTWALLET_ALLOW_UNLIMITED_APPROVAL === '1';
327
+ const unlimitedApprovalsReport = () => POLICY.path
328
+ ? 'refused (a spend policy is set; no budget can count an unlimited allowance)'
329
+ : unlimitedApprovalsAllowed() ? 'allowed by the operator (AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1)' : 'refused by default (the operator can set AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1 while no policy is set)';
243
330
  function spendInputsOf(d, chainId, tool, wallet, ctx = {}) {
244
- if (d.kind === 'approval' && d.unbounded && POLICY.path) {
245
- 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. ` +
246
- `Approve an exact amount instead. Nothing was signed.`);
331
+ if (d.kind === 'approval' && d.unbounded) {
332
+ // The README promised an unlimited approval was "refused outright" while
333
+ // the code refused it only under a policy (reported privately 2026-10-26,
334
+ // 1.14.8). Now the default everywhere; only the operator opens it, and
335
+ // only where no policy is set.
336
+ if (POLICY.path)
337
+ 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. ` +
338
+ `Approve an exact amount instead. Nothing was signed.`);
339
+ if (!unlimitedApprovalsAllowed())
340
+ throw new Error(`an unlimited approval of ${assetLabel(chainId, d.asset)} to ${d.to} is refused by default: it is a standing grant over the whole balance. ` +
341
+ `Approve an exact amount instead. Only the operator can allow unlimited approvals (AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1, never under a spend policy). Nothing was signed.`);
247
342
  }
248
343
  const base = { tool, mode: anyLocalMode() ? 'local' : 'hosted', wallet: anyLocalMode() ? 'local' : wallet, chainId, host: ctx.host ?? null, resource: ctx.resource ?? null };
249
344
  const out = [];
@@ -1066,7 +1161,7 @@ function validateGuardEnv() {
1066
1161
  throw new Error(`Unknown environment variable ${k}. AgentWallet refuses to start on an unrecognised AGENTWALLET_* name so that a misspelled cap cannot run uncapped. Known names: ${[...KNOWN_ENV].join(', ')}, AGENTWALLET_RPC_<chainId>, AGENTWALLET_SOLANA_RPC_<chainId>.`);
1067
1162
  }
1068
1163
  }
1069
- const KNOWN_ENV = new Set(["AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS", "AGENTWALLET_API_URL", "AGENTWALLET_APPROVALS", "AGENTWALLET_AUTOPAY_ASSETS", "AGENTWALLET_CHAIN_ID", "AGENTWALLET_DASHBOARD_URL", "AGENTWALLET_KEYFILE", "AGENTWALLET_KEYFILE_PASSPHRASE", "AGENTWALLET_KEYFILE_PASSPHRASE_FILE", "AGENTWALLET_LEDGER", "AGENTWALLET_LEGACY_PAYWALL_ORIGINS", "AGENTWALLET_MAX_AUTOPAY", "AGENTWALLET_MAX_AUTOPAY_PER_HOUR", "AGENTWALLET_MAX_FEE_NATIVE", "AGENTWALLET_MAX_TX_NATIVE", "AGENTWALLET_MAX_TX_SOL", "AGENTWALLET_MAX_TX_TOKEN", "AGENTWALLET_PASS", "AGENTWALLET_PERMIT2_AUTO_APPROVE", "AGENTWALLET_POLICY", "AGENTWALLET_PRIVATE_KEY", "AGENTWALLET_RPC_URL", "AGENTWALLET_SOLANA_KEY", "AGENTWALLET_SOLANA_KEYFILE", "AGENTWALLET_SOLANA_RPC", "AGENTWALLET_TOKEN_DECIMALS", "AGENTWALLET_TOKEN_RISK", "AGENTWALLET_USER", "AGENTWALLET_WALLET_ID", "AGENTWALLET_WEBHOOK_SECRET", "AGENTWALLET_WEBHOOK_URL", "AGENTWALLET_X402_MAX_TIMEOUT"]);
1164
+ const KNOWN_ENV = new Set(["AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS", "AGENTWALLET_ALLOW_UNLIMITED_APPROVAL", "AGENTWALLET_API_URL", "AGENTWALLET_APPROVALS", "AGENTWALLET_AUTOPAY_ASSETS", "AGENTWALLET_CHAIN_ID", "AGENTWALLET_DASHBOARD_URL", "AGENTWALLET_KEYFILE", "AGENTWALLET_KEYFILE_PASSPHRASE", "AGENTWALLET_KEYFILE_PASSPHRASE_FILE", "AGENTWALLET_LEDGER", "AGENTWALLET_LEGACY_PAYWALL_ORIGINS", "AGENTWALLET_MAX_AUTOPAY", "AGENTWALLET_MAX_AUTOPAY_PER_HOUR", "AGENTWALLET_MAX_FEE_NATIVE", "AGENTWALLET_MAX_TX_NATIVE", "AGENTWALLET_MAX_TX_SOL", "AGENTWALLET_MAX_TX_TOKEN", "AGENTWALLET_PASS", "AGENTWALLET_PERMIT2_AUTO_APPROVE", "AGENTWALLET_POLICY", "AGENTWALLET_PRIVATE_KEY", "AGENTWALLET_RPC_URL", "AGENTWALLET_SOLANA_KEY", "AGENTWALLET_SOLANA_KEYFILE", "AGENTWALLET_SOLANA_RPC", "AGENTWALLET_TOKEN_DECIMALS", "AGENTWALLET_TOKEN_RISK", "AGENTWALLET_USER", "AGENTWALLET_WALLET_ID", "AGENTWALLET_WEBHOOK_SECRET", "AGENTWALLET_WEBHOOK_URL", "AGENTWALLET_X402_MAX_TIMEOUT"]);
1070
1165
  /**
1071
1166
  * Register a tool whose arguments are strict: an unknown key (max_payemnt for
1072
1167
  * max_payment) is an error that names it, not a silently dropped guard. The
@@ -1192,7 +1287,7 @@ function resolveChainId(network) {
1192
1287
  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');
1193
1288
  const server = new McpServer({
1194
1289
  name: 'agentwallet',
1195
- version: '1.14.7',
1290
+ version: '1.14.9',
1196
1291
  }, {
1197
1292
  instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
1198
1293
  ? '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.'
@@ -1537,12 +1632,12 @@ tool('call_contract', 'Execute a read-only call against a smart contract (eth_ca
1537
1632
  // ─── Tool: approve_token ────────────────────────────────────────
1538
1633
  tool('approve_token', 'Approve a spender contract to transfer ERC-20 tokens on your behalf. ' +
1539
1634
  'Required before interacting with any DeFi protocol (DEXs, lending, etc.). ' +
1540
- 'Use amount "max" for unlimited approval, or specify an exact amount. ' +
1541
- 'Under a spend policy an approval is a spend: it counts against the budgets, the spender is checked as a recipient, and "max" is refused.', {
1635
+ 'Specify an exact amount. "max" (an unlimited allowance) is refused by default and always under a spend policy; only the operator can allow it, with AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1 while no policy is set. ' +
1636
+ 'Under a spend policy an approval is a spend: it counts against the budgets and the spender is checked as a recipient.', {
1542
1637
  wallet_id: z.number().int().describe('Wallet ID'),
1543
1638
  token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
1544
1639
  spender: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Contract address to approve as spender'),
1545
- amount: z.string().describe('Amount to approve in human-readable format (e.g. "1000"), or "max" for unlimited'),
1640
+ amount: z.string().describe('Amount to approve in human-readable format (e.g. "1000"); "max" means unlimited and is refused unless the operator allowed it'),
1546
1641
  chain_id: z.number().int().describe('Chain ID'),
1547
1642
  decimals: z.number().int().min(0).max(36).optional().describe('Token decimals. Resolved from the registry or the token contract when omitted; if given, it must match'),
1548
1643
  }, async ({ wallet_id, token, spender, amount, chain_id, decimals: givenDecimals }) => {
@@ -2442,8 +2537,8 @@ tool('check_approval', 'Status of a payment approval created when pay_x402 excee
2442
2537
  'Returns pending, approved, denied or expired. Once approved, call pay_x402 again with approval_id.', { approval_id: z.string().describe('Approval id returned by pay_x402') }, async ({ approval_id }) => jsonResponse(await api(`/approvals/${encodeURIComponent(approval_id)}`, 'GET', undefined, { 'X-AGW-SKIP-X402': 'true' })));
2443
2538
  // ─── Tool: approve_permit2 ──────────────────────────────────────
2444
2539
  tool('approve_permit2', 'One-time ERC-20 approval of a token to the Permit2 contract, needed before paying x402 "upto" endpoints with that token. ' +
2445
- 'A normal on-chain transaction (needs gas). By default approves the maximum, the ecosystem norm, so it never has to be repeated; ' +
2446
- 'pass amount to grant a bounded allowance instead (required under AGENTWALLET_MAX_TX_TOKEN and under a spend policy, both of which refuse a max approval; under a policy the allowance counts against the budgets). ' +
2540
+ 'A normal on-chain transaction (needs gas). Pass amount to grant a bounded allowance; with no amount it asks for the maximum, which is refused by default, under AGENTWALLET_MAX_TX_TOKEN, and always under a spend policy (where the allowance counts against the budgets). ' +
2541
+ 'Only the operator can allow a maximum approval, with AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1 while no policy is set. ' +
2447
2542
  'Permit2 itself only moves what each signed authorization allows.', {
2448
2543
  wallet_id: z.number().int().describe('Wallet ID'),
2449
2544
  token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token address (e.g. USDC on Base)'),
@@ -2907,6 +3002,7 @@ tool('wallet_mode', 'Report whether this server is signing locally (self-custody
2907
3002
  api_base: redactUrl(API_BASE),
2908
3003
  spend_policy: POLICY.path ? { path: POLICY.path, sha256: POLICY.hash, name: POLICY.policy.name ?? null, ledger: POLICY.ledgerPath, see: 'spend_policy' } : 'none (AGENTWALLET_POLICY not set)',
2909
3004
  client_guards: {
3005
+ unlimited_approvals: unlimitedApprovalsReport(),
2910
3006
  autopay_cap_usd: autopayEnvCap(),
2911
3007
  internal_autopay_armed: Boolean(X402_WALLET_ID),
2912
3008
  max_autopay_per_hour: AUTOPAY_PER_HOUR,
@@ -2933,6 +3029,7 @@ tool('wallet_mode', 'Report whether this server is signing locally (self-custody
2933
3029
  x402_max_timeout_seconds: maxAuthWindowSeconds(),
2934
3030
  per_tx_cap_token: (process.env.AGENTWALLET_MAX_TX_TOKEN || '').trim() || 'not set (ERC-20 and SPL transfers are uncapped)',
2935
3031
  allow_unknown_token_calls: process.env.AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS === '1',
3032
+ unlimited_approvals: unlimitedApprovalsReport(),
2936
3033
  token_decimals_pins: parseDecimalPins(process.env.AGENTWALLET_TOKEN_DECIMALS).size,
2937
3034
  token_risk_lookup: process.env.AGENTWALLET_TOKEN_RISK !== '0',
2938
3035
  permit2_auto_approve: process.env.AGENTWALLET_PERMIT2_AUTO_APPROVE === '1',
@@ -351,6 +351,13 @@ const CAP_LAYOUTS = {
351
351
  '9bd9bbc6': { amountIndex: 1, what: 'token transfer (ERC-777 send)' }, // send(address,uint256,bytes)
352
352
  '62ad1b83': { amountIndex: 2, what: 'token transfer (ERC-777 operatorSend)' }, // operatorSend(address,address,uint256,bytes,bytes)
353
353
  '87517c45': { amountIndex: 2, tokenIndex: 0, what: 'Permit2 approval' }, // Permit2.approve(address,address,uint160,uint48)
354
+ // EIP-2612 permit sets the allowance exactly as approve does, with the
355
+ // owner's signature in the calldata (the local key can produce it), so it
356
+ // is priced as an approval (1.14.9, reported privately 2026-11-01). The DAI
357
+ // permit (0x8fcbaf0c, a bool grant) and Permit2.permit stay unpriced here
358
+ // and are refused by the rule below, as every unpriced call to a token or to
359
+ // Permit2 is; the spend policy classifies both.
360
+ d505accf: { amountIndex: 2, what: 'approval (EIP-2612 permit)' }, // permit(address,address,uint256,uint256,uint8,bytes32,bytes32)
354
361
  // WETH9 calls, priced only when `to` is the chain's own wrapped-native
355
362
  // contract (wrappedNativeOnly). deposit() carries its amount as msg.value,
356
363
  // not in calldata, hence amountIndex -1; withdraw(uint256) has it at word 0.
@@ -404,7 +411,7 @@ async function assertWithinTokenCap(chainId, to, data, valueWei = 0n) {
404
411
  ? `the RPC for chain ${chainId} could not be reached to classify the target, so the guard cannot tell a token from a router`
405
412
  : `the target is ${isPermit2 ? 'Permit2' : 'a token contract'} and this calldata is not one AGENTWALLET_MAX_TX_TOKEN can price`;
406
413
  throw new Error(`Blocked by local guard: calldata selector 0x${selector} refused rather than let through uncapped: ${why}. ` +
407
- `Use transfer, transferFrom, approve, increaseAllowance, Permit2 approve, ` +
414
+ `Use transfer, transferFrom, approve, increaseAllowance, EIP-2612 permit, Permit2 approve, ` +
408
415
  `or deposit/withdraw on the chain's wrapped-native contract, ` +
409
416
  `or ask the operator, who alone can set AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1.`);
410
417
  }
package/build/policy.d.ts CHANGED
@@ -2,197 +2,50 @@ import { z } from 'zod';
2
2
  export declare const PolicySchema: z.ZodObject<{
3
3
  version: z.ZodLiteral<1>;
4
4
  name: z.ZodOptional<z.ZodString>;
5
- /** Rolling budgets in stablecoin (dollar) units across every chain: x402 payments and stablecoin transfers. */
6
5
  budget: z.ZodOptional<z.ZodObject<{
7
6
  daily: z.ZodOptional<z.ZodString>;
8
7
  monthly: z.ZodOptional<z.ZodString>;
9
8
  total: z.ZodOptional<z.ZodString>;
10
- }, "strict", z.ZodTypeAny, {
11
- total?: string | undefined;
12
- daily?: string | undefined;
13
- monthly?: string | undefined;
14
- }, {
15
- total?: string | undefined;
16
- daily?: string | undefined;
17
- monthly?: string | undefined;
18
- }>>;
19
- /** Rolling budgets for the chain's own coin, keyed by chain id, in that coin's units. */
9
+ }, z.core.$strict>>;
20
10
  native_budget: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
21
11
  daily: z.ZodOptional<z.ZodString>;
22
12
  monthly: z.ZodOptional<z.ZodString>;
23
13
  total: z.ZodOptional<z.ZodString>;
24
- }, "strict", z.ZodTypeAny, {
25
- total?: string | undefined;
26
- daily?: string | undefined;
27
- monthly?: string | undefined;
28
- }, {
29
- total?: string | undefined;
30
- daily?: string | undefined;
31
- monthly?: string | undefined;
32
- }>>>;
33
- /** What to do with a spend the budget cannot price (a token outside the registry). Default refuse when a budget is set. */
34
- unpriced_spends: z.ZodOptional<z.ZodEnum<["refuse", "allow"]>>;
35
- hosts: z.ZodOptional<z.ZodObject<{
36
- allow: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
37
- deny: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
38
- }, "strict", z.ZodTypeAny, {
39
- allow?: string[] | undefined;
40
- deny?: string[] | undefined;
41
- }, {
42
- allow?: string[] | undefined;
43
- deny?: string[] | undefined;
14
+ }, z.core.$strict>>>;
15
+ unpriced_spends: z.ZodOptional<z.ZodEnum<{
16
+ allow: "allow";
17
+ refuse: "refuse";
44
18
  }>>;
45
- /** Per-host ceilings for x402 payments, dollar units. */
19
+ hosts: z.ZodOptional<z.ZodObject<{
20
+ allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
22
+ }, z.core.$strict>>;
46
23
  per_host: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
47
24
  daily: z.ZodOptional<z.ZodString>;
48
25
  monthly: z.ZodOptional<z.ZodString>;
49
26
  per_payment: z.ZodOptional<z.ZodString>;
50
- }, "strict", z.ZodTypeAny, {
51
- daily?: string | undefined;
52
- monthly?: string | undefined;
53
- per_payment?: string | undefined;
54
- }, {
55
- daily?: string | undefined;
56
- monthly?: string | undefined;
57
- per_payment?: string | undefined;
58
- }>>>;
27
+ }, z.core.$strict>>>;
59
28
  recipients: z.ZodOptional<z.ZodObject<{
60
- allow: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
61
- deny: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
62
- }, "strict", z.ZodTypeAny, {
63
- allow?: string[] | undefined;
64
- deny?: string[] | undefined;
65
- }, {
66
- allow?: string[] | undefined;
67
- deny?: string[] | undefined;
68
- }>>;
29
+ allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
30
+ deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
31
+ }, z.core.$strict>>;
69
32
  chains: z.ZodOptional<z.ZodObject<{
70
- allow: z.ZodOptional<z.ZodArray<z.ZodNumber, "many">>;
71
- }, "strict", z.ZodTypeAny, {
72
- allow?: number[] | undefined;
73
- }, {
74
- allow?: number[] | undefined;
75
- }>>;
33
+ allow: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
34
+ }, z.core.$strict>>;
76
35
  assets: z.ZodOptional<z.ZodObject<{
77
- allow: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
78
- }, "strict", z.ZodTypeAny, {
79
- allow?: string[] | undefined;
80
- }, {
81
- allow?: string[] | undefined;
82
- }>>;
83
- /** Ask the human (MCP elicitation) above `above`; in local mode an accepted ask may admit up to `max`, above the per-payment cap. */
36
+ allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
37
+ }, z.core.$strict>>;
84
38
  approval: z.ZodOptional<z.ZodObject<{
85
39
  above: z.ZodOptional<z.ZodString>;
86
40
  max: z.ZodOptional<z.ZodString>;
87
41
  timeout_seconds: z.ZodOptional<z.ZodNumber>;
88
- }, "strict", z.ZodTypeAny, {
89
- max?: string | undefined;
90
- above?: string | undefined;
91
- timeout_seconds?: number | undefined;
92
- }, {
93
- max?: string | undefined;
94
- above?: string | undefined;
95
- timeout_seconds?: number | undefined;
96
- }>>;
42
+ }, z.core.$strict>>;
97
43
  tools: z.ZodOptional<z.ZodObject<{
98
- allow: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
99
- deny: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
100
- }, "strict", z.ZodTypeAny, {
101
- allow?: string[] | undefined;
102
- deny?: string[] | undefined;
103
- }, {
104
- allow?: string[] | undefined;
105
- deny?: string[] | undefined;
106
- }>>;
107
- /** Ledger path; relative paths resolve against the policy file's directory. */
44
+ allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
45
+ deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
46
+ }, z.core.$strict>>;
108
47
  ledger: z.ZodOptional<z.ZodString>;
109
- }, "strict", z.ZodTypeAny, {
110
- version: 1;
111
- name?: string | undefined;
112
- approval?: {
113
- max?: string | undefined;
114
- above?: string | undefined;
115
- timeout_seconds?: number | undefined;
116
- } | undefined;
117
- budget?: {
118
- total?: string | undefined;
119
- daily?: string | undefined;
120
- monthly?: string | undefined;
121
- } | undefined;
122
- native_budget?: Record<string, {
123
- total?: string | undefined;
124
- daily?: string | undefined;
125
- monthly?: string | undefined;
126
- }> | undefined;
127
- unpriced_spends?: "allow" | "refuse" | undefined;
128
- hosts?: {
129
- allow?: string[] | undefined;
130
- deny?: string[] | undefined;
131
- } | undefined;
132
- per_host?: Record<string, {
133
- daily?: string | undefined;
134
- monthly?: string | undefined;
135
- per_payment?: string | undefined;
136
- }> | undefined;
137
- recipients?: {
138
- allow?: string[] | undefined;
139
- deny?: string[] | undefined;
140
- } | undefined;
141
- chains?: {
142
- allow?: number[] | undefined;
143
- } | undefined;
144
- assets?: {
145
- allow?: string[] | undefined;
146
- } | undefined;
147
- tools?: {
148
- allow?: string[] | undefined;
149
- deny?: string[] | undefined;
150
- } | undefined;
151
- ledger?: string | undefined;
152
- }, {
153
- version: 1;
154
- name?: string | undefined;
155
- approval?: {
156
- max?: string | undefined;
157
- above?: string | undefined;
158
- timeout_seconds?: number | undefined;
159
- } | undefined;
160
- budget?: {
161
- total?: string | undefined;
162
- daily?: string | undefined;
163
- monthly?: string | undefined;
164
- } | undefined;
165
- native_budget?: Record<string, {
166
- total?: string | undefined;
167
- daily?: string | undefined;
168
- monthly?: string | undefined;
169
- }> | undefined;
170
- unpriced_spends?: "allow" | "refuse" | undefined;
171
- hosts?: {
172
- allow?: string[] | undefined;
173
- deny?: string[] | undefined;
174
- } | undefined;
175
- per_host?: Record<string, {
176
- daily?: string | undefined;
177
- monthly?: string | undefined;
178
- per_payment?: string | undefined;
179
- }> | undefined;
180
- recipients?: {
181
- allow?: string[] | undefined;
182
- deny?: string[] | undefined;
183
- } | undefined;
184
- chains?: {
185
- allow?: number[] | undefined;
186
- } | undefined;
187
- assets?: {
188
- allow?: string[] | undefined;
189
- } | undefined;
190
- tools?: {
191
- allow?: string[] | undefined;
192
- deny?: string[] | undefined;
193
- } | undefined;
194
- ledger?: string | undefined;
195
- }>;
48
+ }, z.core.$strict>;
196
49
  export type Policy = z.infer<typeof PolicySchema>;
197
50
  export declare const PRECISION = 18;
198
51
  /** Decimal string to an integer at 18 places. */
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.7",
8
+ "version": "1.14.9",
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",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "tsc",
22
22
  "start": "node build/index.js",
23
- "test": "node test/x402-payment.test.mjs && node test/eip3009.test.mjs && node test/permit2-risk.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/unknown-decimals-cap.test.mjs && node test/decimals-range-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs && node test/x402-policy.test.mjs && node test/local-guard-shape.test.mjs && node test/allowance-total-cap.test.mjs && node test/autopay-scheme.test.mjs && node test/spl-recipient-guard.test.mjs && node test/approval-binding.test.mjs && node test/ata-rent-size.test.mjs && node test/final-url-guard.test.mjs && node test/audit-1135.test.mjs && node test/local-no-autopay.test.mjs && node test/audit-1136.test.mjs && node test/delete-confirm.test.mjs && node test/audit-1138.test.mjs && node test/audit-1139.test.mjs && node test/audit-11310.test.mjs && node test/audit-11311.test.mjs && node test/audit-11312.test.mjs && node test/audit-11313.test.mjs && node test/audit-11316.test.mjs && node test/spend-policy.test.mjs && node test/policy-path-resolution.test.mjs",
23
+ "test": "node test/x402-payment.test.mjs && node test/eip3009.test.mjs && node test/permit2-risk.test.mjs && node test/token-cap.test.mjs && node test/spl-cap.test.mjs && node test/unknown-decimals-cap.test.mjs && node test/decimals-range-cap.test.mjs && node test/local-mode.test.mjs && node test/ssrf-guard.test.mjs && node test/x402-policy.test.mjs && node test/local-guard-shape.test.mjs && node test/allowance-total-cap.test.mjs && node test/autopay-scheme.test.mjs && node test/spl-recipient-guard.test.mjs && node test/approval-binding.test.mjs && node test/ata-rent-size.test.mjs && node test/final-url-guard.test.mjs && node test/audit-1135.test.mjs && node test/local-no-autopay.test.mjs && node test/audit-1136.test.mjs && node test/delete-confirm.test.mjs && node test/audit-1138.test.mjs && node test/audit-1139.test.mjs && node test/audit-11310.test.mjs && node test/audit-11311.test.mjs && node test/audit-11312.test.mjs && node test/audit-11313.test.mjs && node test/audit-11316.test.mjs && node test/spend-policy.test.mjs && node test/policy-path-resolution.test.mjs && node test/server-json-env.test.mjs",
24
24
  "prepublishOnly": "npm run build"
25
25
  },
26
26
  "keywords": [
@@ -59,12 +59,12 @@
59
59
  "node": ">=22.19.0"
60
60
  },
61
61
  "dependencies": {
62
- "@modelcontextprotocol/sdk": "^1.30.1",
62
+ "@modelcontextprotocol/sdk": "^1.32.2",
63
63
  "@solana/web3.js": "^1.99.0",
64
64
  "bs58": "^6.0.0",
65
- "undici": "^8.11.2",
65
+ "undici": "^8.12.3",
66
66
  "viem": "^2.56.9",
67
- "zod": "^3.24.0"
67
+ "zod": "^4.6.7"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/node": "^20.0.0",