agentwallet-mcp 1.14.6 → 1.14.8

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`, 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
 
@@ -256,7 +256,7 @@ pay_x402(
256
256
 
257
257
  **Asset risk.** `check_token_risk` (and every `approve_token` result) reports honeypot, tax, owner-power, verified-source, holder-concentration and liquidity flags from GoPlus Security, with an on-chain fallback. A warning for the caller to weigh, never a block.
258
258
 
259
- **Proof.** Real settlements are listed in [PAYMENTS.md](https://github.com/hifriendbot/agentwallet-mcp/blob/main/PAYMENTS.md); the first is a 0.02 USDC `exact` payment on Base with the facilitator paying the gas.
259
+ **Proof.** Real settlements are listed in [PAYMENTS.md](https://github.com/hifriendbot/agentwallet-mcp/blob/main/PAYMENTS.md); the first is a 0.02 USDC `exact` payment on Base with the facilitator paying the gas. The same list, rendered with Basescan links, is at [hifriendbot.com/wallet/payments](https://hifriendbot.com/wallet/payments/), and a five-minute walkthrough from install to a real settlement is at [hifriendbot.com/wallet/quickstart](https://hifriendbot.com/wallet/quickstart/). Security policy, fixed disclosures and the release history: [hifriendbot.com/security](https://hifriendbot.com/security/) and [hifriendbot.com/changelog](https://hifriendbot.com/changelog/).
260
260
 
261
261
  ## Pay any x402 API from Claude in three steps
262
262
 
@@ -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)
@@ -374,8 +375,8 @@ A per-transaction cap bounds one signature. It cannot see that the five hundredt
374
375
  ```
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
- - **`native_budget`**: the same per chain, in the chain's own coin, counting native transfers and the native value sent into any contract (`wrap_eth`, `send_transaction` with value and calldata), including value carried on a token transfer or approval call (a payable token contract keeps it, so it is a second spend, to that contract).
378
- - **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.
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`, 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.
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';
@@ -30,7 +32,7 @@ import { WRAPPED_NATIVE } from './wrapped-native.js';
30
32
  import { buildUptoAuthorization, uptoPayload, permit2AllowanceCalldata, permit2ApproveCalldata, decodeUint, permit2Address, } from './x402-permit2.js';
31
33
  import { assessTokenRisk } from './token-risk.js';
32
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';
33
- import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, mintDecimalsStrict, } from './local-solana.js';
35
+ import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, mintDecimalsStrict, splAccountRent, } from './local-solana.js';
34
36
  import { localSimulate } from './local-wallet.js';
35
37
  import { loadPolicy, describePolicy, hostAllowed, recipientAllowed, targetAllowed, chainAllowed, assetAllowed, perHostRule, toolAllowed, hostOf, budgetReport, units as policyUnits, human as policyHuman, rawToUnits, } from './policy.js';
36
38
  import { Ledger } from './ledger.js';
@@ -92,7 +94,10 @@ function reserveSpend(input) {
92
94
  const chain = chainAllowed(p, input.chainId);
93
95
  if (!chain.ok)
94
96
  throw new Error(chain.reason + ' Nothing was signed.');
95
- const asset = assetAllowed(p, input.chainId, input.asset);
97
+ // Rent is SOL by nature, not by choice: the operator allowed the token, and
98
+ // the account that holds it is paid for in the chain's coin, so assets.allow
99
+ // does not have to list native for the rent line. native_budget counts it.
100
+ const asset = input.method === 'rent' ? { ok: true } : assetAllowed(p, input.chainId, input.asset);
96
101
  if (!asset.ok)
97
102
  throw new Error(asset.reason + ' Nothing was signed.');
98
103
  const to = recipientAllowed(p, input.to);
@@ -105,7 +110,7 @@ function reserveSpend(input) {
105
110
  /** The same checks with nothing recorded, for quotes and simulations. */
106
111
  function spendVerdict(input) {
107
112
  const p = POL();
108
- for (const r of [chainAllowed(p, input.chainId), assetAllowed(p, input.chainId, input.asset), recipientAllowed(p, input.to)])
113
+ for (const r of [chainAllowed(p, input.chainId), input.method === 'rent' ? { ok: true } : assetAllowed(p, input.chainId, input.asset), recipientAllowed(p, input.to)])
109
114
  if (!r.ok)
110
115
  return r;
111
116
  const limits = limitsFor(input.host);
@@ -129,8 +134,43 @@ const SPEND_LAYOUTS = {
129
134
  '39509351': { kind: 'approval', what: 'increaseAllowance', toIndex: 0, amountIndex: 1 }, // increaseAllowance(address,uint256)
130
135
  cae9ca51: { kind: 'approval', what: 'approveAndCall', toIndex: 0, amountIndex: 1 }, // approveAndCall(address,uint256,bytes)
131
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)
132
153
  };
133
154
  const UNBOUNDED_APPROVAL = 1n << 128n;
155
+ const UINT256_MAX = (1n << 256n) - 1n;
156
+ /**
157
+ * The sum of the `amounts` array of an ERC-1155 safeBatchTransferFrom, read
158
+ * through the caller's word reader so a short word refuses the whole call.
159
+ * `offsetWord` holds the array's byte offset from the start of the arguments.
160
+ */
161
+ function batchTotal(wordAt, offsetWord) {
162
+ const offset = BigInt('0x' + wordAt(offsetWord));
163
+ if (offset % 32n !== 0n || offset > 1n << 20n)
164
+ 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.');
165
+ const base = Number(offset / 32n);
166
+ const count = BigInt('0x' + wordAt(base));
167
+ if (count > 4096n)
168
+ throw new Error('policy: calldata for ERC-1155 safeBatchTransferFrom names more than 4096 amounts; refused. Nothing was signed.');
169
+ let total = 0n;
170
+ for (let i = 0; i < Number(count); i++)
171
+ total += BigInt('0x' + wordAt(base + 1 + i));
172
+ return total;
173
+ }
134
174
  async function describeSend(body, chainId) {
135
175
  if (body.token_mint) {
136
176
  const mint = String(body.token_mint);
@@ -141,7 +181,19 @@ async function describeSend(body, chainId) {
141
181
  // reached the custodian (reported privately 2026-10-24, 1.14.5). The chain
142
182
  // would have refused the TransferChecked, but the ledger was already wrong.
143
183
  const given = Number.isInteger(d) && d >= 0 && d <= 36 ? d : undefined;
144
- return { kind: 'spl', to: String(body.to || ''), asset: mint, raw: String(body.value ?? '0'), decimals: await verifiedDecimals(chainId, mint, given) };
184
+ const to = String(body.to || '');
185
+ const decimals = await verifiedDecimals(chainId, mint, given);
186
+ // In local mode this process is the payer: when the recipient has no token
187
+ // account yet, the send creates one and the rent is native value leaving
188
+ // the wallet, reserved here from the same reading the signer will make.
189
+ // The hosted custodian pays its own rent and bounds it on its side.
190
+ let carried;
191
+ if (isSolanaLocalMode() && to) {
192
+ const rent = await splAccountRent(mint, to, chainId);
193
+ if (rent > 0n)
194
+ carried = { raw: rent.toString(), to, rent: true };
195
+ }
196
+ return { kind: 'spl', to, asset: mint, raw: String(body.value ?? '0'), decimals, ...(carried ? { carried } : {}) };
145
197
  }
146
198
  const data = String(body.data || '');
147
199
  const to = String(body.to || '');
@@ -160,14 +212,27 @@ async function describeSend(body, chainId) {
160
212
  };
161
213
  const token = layout.tokenIndex === undefined ? to : '0x' + wordAt(layout.tokenIndex).slice(24);
162
214
  const counterparty = '0x' + wordAt(layout.toIndex).slice(24);
163
- const amount = BigInt('0x' + wordAt(layout.amountIndex));
164
215
  // The call's own value does not vanish because the calldata was
165
216
  // recognised: the contract keeps whatever rides along. It is a second
166
217
  // spend, native, to the contract (reported privately 2026-10-23, 1.14.4).
167
218
  const carried = BigInt(value) > 0n ? { raw: value, to } : undefined;
219
+ if (layout.operator) {
220
+ // setApprovalForAll(operator, false) takes a grant away and moves
221
+ // nothing: a zero-value call to the collection, reserving nothing.
222
+ const granted = layout.amountIndex < 0 || BigInt('0x' + wordAt(layout.amountIndex)) !== 0n;
223
+ if (!granted)
224
+ return { kind: 'contract', to, asset: '', raw: value, decimals: nativeDecimals(chainId) };
225
+ return { kind: 'approval', to: counterparty, asset: token, raw: UINT256_MAX.toString(), decimals: 0, unbounded: true, ...(carried ? { carried } : {}) };
226
+ }
227
+ const amount = layout.amountIndex < 0 ? 1n : layout.batch ? batchTotal(wordAt, layout.amountIndex) : BigInt('0x' + wordAt(layout.amountIndex));
168
228
  if (layout.kind === 'approval' && amount >= UNBOUNDED_APPROVAL) {
169
229
  return { kind: 'approval', to: counterparty, asset: token, raw: amount.toString(), decimals: 0, unbounded: true, ...(carried ? { carried } : {}) };
170
230
  }
231
+ if (layout.nft) {
232
+ // A collection has no decimals and no price: whole tokens, unpriced,
233
+ // to the recipient, recorded under the collection's address.
234
+ return { kind: layout.kind, to: counterparty, asset: token, raw: amount.toString(), decimals: 0, ...(carried ? { carried } : {}) };
235
+ }
171
236
  // Decimals matter here only to price a registry stablecoin (resolved
172
237
  // offline) and to print the ledger amount. A token the registry and the
173
238
  // node cannot place is unpriced whatever its decimals, so a failed lookup
@@ -221,12 +286,27 @@ function assertSendTarget(d, chainId) {
221
286
  * recipients.allow and .deny bind who may be allowed to pull; the native
222
287
  * value of a contract call is a spend to that contract; the native value
223
288
  * carried on a token or approval call is a second spend, native, to the
224
- * called contract (1.14.4). Throws for an unbounded approval under a policy.
289
+ * called contract (1.14.4); the rent a local SPL send pays for the recipient's
290
+ * token account is a native spend to the recipient, method 'rent' (1.14.7).
291
+ * Throws for an unbounded approval: under a policy always, and otherwise
292
+ * unless the operator allowed it.
225
293
  */
294
+ const unlimitedApprovalsAllowed = () => !POLICY.path && process.env.AGENTWALLET_ALLOW_UNLIMITED_APPROVAL === '1';
295
+ const unlimitedApprovalsReport = () => POLICY.path
296
+ ? 'refused (a spend policy is set; no budget can count an unlimited allowance)'
297
+ : 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)';
226
298
  function spendInputsOf(d, chainId, tool, wallet, ctx = {}) {
227
- if (d.kind === 'approval' && d.unbounded && POLICY.path) {
228
- 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. ` +
229
- `Approve an exact amount instead. Nothing was signed.`);
299
+ if (d.kind === 'approval' && d.unbounded) {
300
+ // The README promised an unlimited approval was "refused outright" while
301
+ // the code refused it only under a policy (reported privately 2026-10-26,
302
+ // 1.14.8). Now the default everywhere; only the operator opens it, and
303
+ // only where no policy is set.
304
+ if (POLICY.path)
305
+ 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. ` +
306
+ `Approve an exact amount instead. Nothing was signed.`);
307
+ if (!unlimitedApprovalsAllowed())
308
+ 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. ` +
309
+ `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.`);
230
310
  }
231
311
  const base = { tool, mode: anyLocalMode() ? 'local' : 'hosted', wallet: anyLocalMode() ? 'local' : wallet, chainId, host: ctx.host ?? null, resource: ctx.resource ?? null };
232
312
  const out = [];
@@ -238,7 +318,7 @@ function spendInputsOf(d, chainId, tool, wallet, ctx = {}) {
238
318
  });
239
319
  }
240
320
  if (d.carried) {
241
- out.push({ ...base, asset: '', assetLabel: assetLabel(chainId, ''), raw: d.carried.raw, decimals: nativeDecimals(chainId), usd: null, to: d.carried.to, method: 'native' });
321
+ out.push({ ...base, asset: '', assetLabel: assetLabel(chainId, ''), raw: d.carried.raw, decimals: nativeDecimals(chainId), usd: null, to: d.carried.to, method: d.carried.rent ? 'rent' : 'native' });
242
322
  }
243
323
  return out;
244
324
  }
@@ -1049,7 +1129,7 @@ function validateGuardEnv() {
1049
1129
  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>.`);
1050
1130
  }
1051
1131
  }
1052
- 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"]);
1132
+ 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"]);
1053
1133
  /**
1054
1134
  * Register a tool whose arguments are strict: an unknown key (max_payemnt for
1055
1135
  * max_payment) is an error that names it, not a silently dropped guard. The
@@ -1175,7 +1255,7 @@ function resolveChainId(network) {
1175
1255
  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');
1176
1256
  const server = new McpServer({
1177
1257
  name: 'agentwallet',
1178
- version: '1.14.6',
1258
+ version: '1.14.8',
1179
1259
  }, {
1180
1260
  instructions: `AgentWallet gives AI agents their own blockchain wallets. ${anyLocalMode()
1181
1261
  ? '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.'
@@ -1520,12 +1600,12 @@ tool('call_contract', 'Execute a read-only call against a smart contract (eth_ca
1520
1600
  // ─── Tool: approve_token ────────────────────────────────────────
1521
1601
  tool('approve_token', 'Approve a spender contract to transfer ERC-20 tokens on your behalf. ' +
1522
1602
  'Required before interacting with any DeFi protocol (DEXs, lending, etc.). ' +
1523
- 'Use amount "max" for unlimited approval, or specify an exact amount. ' +
1524
- '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.', {
1603
+ '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. ' +
1604
+ 'Under a spend policy an approval is a spend: it counts against the budgets and the spender is checked as a recipient.', {
1525
1605
  wallet_id: z.number().int().describe('Wallet ID'),
1526
1606
  token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
1527
1607
  spender: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Contract address to approve as spender'),
1528
- amount: z.string().describe('Amount to approve in human-readable format (e.g. "1000"), or "max" for unlimited'),
1608
+ 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'),
1529
1609
  chain_id: z.number().int().describe('Chain ID'),
1530
1610
  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'),
1531
1611
  }, async ({ wallet_id, token, spender, amount, chain_id, decimals: givenDecimals }) => {
@@ -2425,8 +2505,8 @@ tool('check_approval', 'Status of a payment approval created when pay_x402 excee
2425
2505
  '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' })));
2426
2506
  // ─── Tool: approve_permit2 ──────────────────────────────────────
2427
2507
  tool('approve_permit2', 'One-time ERC-20 approval of a token to the Permit2 contract, needed before paying x402 "upto" endpoints with that token. ' +
2428
- 'A normal on-chain transaction (needs gas). By default approves the maximum, the ecosystem norm, so it never has to be repeated; ' +
2429
- '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). ' +
2508
+ '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). ' +
2509
+ 'Only the operator can allow a maximum approval, with AGENTWALLET_ALLOW_UNLIMITED_APPROVAL=1 while no policy is set. ' +
2430
2510
  'Permit2 itself only moves what each signed authorization allows.', {
2431
2511
  wallet_id: z.number().int().describe('Wallet ID'),
2432
2512
  token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token address (e.g. USDC on Base)'),
@@ -2890,6 +2970,7 @@ tool('wallet_mode', 'Report whether this server is signing locally (self-custody
2890
2970
  api_base: redactUrl(API_BASE),
2891
2971
  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)',
2892
2972
  client_guards: {
2973
+ unlimited_approvals: unlimitedApprovalsReport(),
2893
2974
  autopay_cap_usd: autopayEnvCap(),
2894
2975
  internal_autopay_armed: Boolean(X402_WALLET_ID),
2895
2976
  max_autopay_per_hour: AUTOPAY_PER_HOUR,
@@ -2916,6 +2997,7 @@ tool('wallet_mode', 'Report whether this server is signing locally (self-custody
2916
2997
  x402_max_timeout_seconds: maxAuthWindowSeconds(),
2917
2998
  per_tx_cap_token: (process.env.AGENTWALLET_MAX_TX_TOKEN || '').trim() || 'not set (ERC-20 and SPL transfers are uncapped)',
2918
2999
  allow_unknown_token_calls: process.env.AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS === '1',
3000
+ unlimited_approvals: unlimitedApprovalsReport(),
2919
3001
  token_decimals_pins: parseDecimalPins(process.env.AGENTWALLET_TOKEN_DECIMALS).size,
2920
3002
  token_risk_lookup: process.env.AGENTWALLET_TOKEN_RISK !== '0',
2921
3003
  permit2_auto_approve: process.env.AGENTWALLET_PERMIT2_AUTO_APPROVE === '1',
package/build/ledger.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { type Spent } from './policy.js';
2
- /** 'approval' is an ERC-20 allowance granted (approve, increaseAllowance, Permit2.approve): a spend the spender executes later, counted when granted. */
3
- export type SpendMethod = 'eip3009' | 'permit2' | 'transfer' | 'native' | 'spl' | 'autopay' | 'approval';
2
+ /**
3
+ * 'approval' is an ERC-20 allowance granted (approve, increaseAllowance, Permit2.approve): a spend the spender executes later, counted when granted.
4
+ * 'rent' is the SOL an SPL send pays to create the recipient's token account (1.14.7): native, to the recipient, who owns the account and can close it for the lamports.
5
+ */
6
+ export type SpendMethod = 'eip3009' | 'permit2' | 'transfer' | 'native' | 'spl' | 'autopay' | 'approval' | 'rent';
4
7
  export interface SpendEntry {
5
8
  seq: number;
6
9
  ts: string;
@@ -76,6 +76,16 @@ export declare function localSolTransfer(to: string, lamports: string, chainId?:
76
76
  * not exist, using the idempotent instruction so a race cannot fail the send.
77
77
  */
78
78
  export declare function localSplTransfer(mintStr: string, to: string, rawAmount: string, decimals: number, chainId?: number): Promise<LocalSolResult>;
79
+ /**
80
+ * The SOL an SPL send to `to` spends on creating the recipient's token
81
+ * account, in lamports; 0n when the account exists. The recipient owns that
82
+ * account and can close it to keep the lamports, so it is native value leaving
83
+ * the wallet and the spend policy reserves it against native_budget before the
84
+ * send is built (reported privately 2026-10-26: it was checked against the
85
+ * per-transaction SOL cap only, and fresh recipients took SOL past the daily
86
+ * budget while the ledger held token lines alone).
87
+ */
88
+ export declare function splAccountRent(mintStr: string, to: string, chainId?: number): Promise<bigint>;
79
89
  export declare function solanaWalletRecord(): {
80
90
  id: string;
81
91
  address: string;
@@ -448,31 +448,15 @@ export async function localSplTransfer(mintStr, to, rawAmount, decimals, chainId
448
448
  const mint = new PublicKey(mintStr);
449
449
  const recipient = new PublicKey(to);
450
450
  await assertRecipientIsWallet(conn, recipient);
451
- const { tokenProgram, ataSize } = await mintLayout(conn, mint);
451
+ const { tokenProgram, destAta, rent } = await destinationAccount(conn, mint, recipient);
452
452
  const sourceAta = associatedTokenAddress(payer.publicKey, mint, tokenProgram);
453
- const destAta = associatedTokenAddress(recipient, mint, tokenProgram);
454
453
  const tx = new Transaction();
455
- const destInfo = await conn.getAccountInfo(destAta);
456
- // The associated-account program creates into an address that already holds
457
- // plain SOL (system-owned, no data): it tops up the rent, allocates and
458
- // assigns. Only an address holding something else cannot become the token
459
- // account (2026-10-04 round 2; the 1.13.5 refusal of any occupant let anyone
460
- // block a recipient for the price of the rent-exempt minimum).
461
- const prefunded = destInfo !== null && destInfo.owner.equals(SystemProgram.programId) && destInfo.data.length === 0;
462
- if (destInfo && !destInfo.owner.equals(tokenProgram) && !prefunded) {
463
- throw new Error(`Refusing to send: the recipient's token account address ${destAta.toBase58()} is occupied by an account owned by ${destInfo.owner.toBase58()}, so no token account can be created there.`);
464
- }
465
- if (!destInfo || prefunded) {
454
+ if (rent > 0n) {
466
455
  // Creating the recipient's token account costs the payer rent in SOL, an
467
456
  // outflow the SOL cap must see (0.002 SOL a time adds up over many sends).
468
- // Rent follows the account's real size, which for Token-2022 is above 165;
469
- // SOL already sitting at the address reduces what the payer tops up.
470
- // The node's rent figure is floored at the cluster constant and the SOL the
471
- // node says already sits at the address is not credited: both are numbers a
472
- // lying RPC used to zero so the create went out under the cap (round 4).
473
- const claimed = Number(await conn.getMinimumBalanceForRentExemption(ataSize));
474
- const rent = Math.max(Number.isFinite(claimed) ? Math.trunc(claimed) : 0, (128 + ataSize) * 6960);
475
- assertWithinSolCap(BigInt(rent));
457
+ // The policy reserved the same figure against native_budget before this
458
+ // call (1.14.7); the cap is the per-transaction floor under it.
459
+ assertWithinSolCap(rent);
476
460
  tx.add(createAtaIdempotentIx(payer.publicKey, destAta, recipient, mint, tokenProgram));
477
461
  }
478
462
  tx.add(transferCheckedIx(tokenProgram, sourceAta, mint, destAta, payer.publicKey, BigInt(rawAmount), decimals));
@@ -487,6 +471,52 @@ export async function localSplTransfer(mintStr, to, rawAmount, decimals, chainId
487
471
  signed_locally: true,
488
472
  };
489
473
  }
474
+ /**
475
+ * The recipient's associated account for a mint, and the rent the payer funds
476
+ * when it has to be created: 0 when it already exists. One reading shared by
477
+ * the send and by the policy's reservation, so the ledger line and the
478
+ * instruction carry the same figure.
479
+ *
480
+ * The associated-account program creates into an address that already holds
481
+ * plain SOL (system-owned, no data): it tops up the rent, allocates and
482
+ * assigns. Only an address holding something else cannot become the token
483
+ * account (2026-10-04 round 2; the 1.13.5 refusal of any occupant let anyone
484
+ * block a recipient for the price of the rent-exempt minimum).
485
+ *
486
+ * Rent follows the account's real size, which for Token-2022 is above 165;
487
+ * SOL already sitting at the address reduces what the payer tops up. The
488
+ * node's rent figure is floored at the cluster constant and the SOL the node
489
+ * says already sits at the address is not credited: both are numbers a lying
490
+ * RPC used to zero so the create went out under the cap (round 4).
491
+ */
492
+ async function destinationAccount(conn, mint, recipient) {
493
+ const { tokenProgram, ataSize } = await mintLayout(conn, mint);
494
+ const destAta = associatedTokenAddress(recipient, mint, tokenProgram);
495
+ const destInfo = await conn.getAccountInfo(destAta);
496
+ const prefunded = destInfo !== null && destInfo.owner.equals(SystemProgram.programId) && destInfo.data.length === 0;
497
+ if (destInfo && !destInfo.owner.equals(tokenProgram) && !prefunded) {
498
+ throw new Error(`Refusing to send: the recipient's token account address ${destAta.toBase58()} is occupied by an account owned by ${destInfo.owner.toBase58()}, so no token account can be created there.`);
499
+ }
500
+ if (destInfo && !prefunded)
501
+ return { tokenProgram, destAta, rent: 0n };
502
+ const claimed = Number(await conn.getMinimumBalanceForRentExemption(ataSize));
503
+ const rent = Math.max(Number.isFinite(claimed) ? Math.trunc(claimed) : 0, (128 + ataSize) * 6960);
504
+ return { tokenProgram, destAta, rent: BigInt(rent) };
505
+ }
506
+ /**
507
+ * The SOL an SPL send to `to` spends on creating the recipient's token
508
+ * account, in lamports; 0n when the account exists. The recipient owns that
509
+ * account and can close it to keep the lamports, so it is native value leaving
510
+ * the wallet and the spend policy reserves it against native_budget before the
511
+ * send is built (reported privately 2026-10-26: it was checked against the
512
+ * per-transaction SOL cap only, and fresh recipients took SOL past the daily
513
+ * budget while the ledger held token lines alone).
514
+ */
515
+ export async function splAccountRent(mintStr, to, chainId = 900) {
516
+ const conn = connection(chainId);
517
+ await assertCluster(conn, chainId);
518
+ return (await destinationAccount(conn, new PublicKey(mintStr), new PublicKey(to))).rent;
519
+ }
490
520
  const TOKEN_ACCOUNT_SIZE = 165;
491
521
  const MINT_SIZE = 82;
492
522
  /** Token-2022 pads a mint to the token-account size and tags the next byte: 1 = mint, 2 = token account. */
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.6",
8
+ "version": "1.14.8",
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",