agentwallet-mcp 1.12.3 → 1.12.5

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` and Permit2 `approve`, 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. Calls to other contracts (routers, bridges) pass, since they can only pull what an approval already allowed. 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.
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` and Permit2 `approve`, 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. 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.
47
47
 
48
48
  ### Solana local signing
49
49
 
@@ -259,6 +259,8 @@ pay_x402(
259
259
 
260
260
  **Token domains.** The EIP-712 domain comes from the endpoint's `extra.name` / `extra.version`, then a short registry of USDC deployments, then the token contract's own `name()` / `version()`. If none of those answer, the payment is refused rather than signed with a guessed domain.
261
261
 
262
+ **Delegated payers.** If the paying address is an EIP-7702 delegated account, `pay_x402` reports it in `payer_delegation` (the delegate and whether it answers ERC-1271). A delegate without ERC-1271 is declined by facilitators that check account code before recovering the signer, and the reason they return reads like a signature fault; the warning names the real cause. Pay from a plain EOA for reliable settlement.
263
+
262
264
  ## x402 Acceptance
263
265
 
264
266
  AgentWallet also lets you **accept** x402 payments. Create a paywall, point it at any resource, and get a public URL that charges agents automatically:
package/build/index.js CHANGED
@@ -15,10 +15,10 @@ import { z } from 'zod';
15
15
  import { assertPublicUrl, safeFetch } from './ssrf-guard.js';
16
16
  import { deriveX402Payment, isWithinCap, lookupTrustedDecimals, assertDeclaredDecimals, nativeDecimals, SOLANA_CHAIN_IDS, } from './x402-payment.js';
17
17
  import { isLocalMode, getLocalAddress, localWalletRecord, localSend, localNativeBalance, localTokenBalance, localSignMessage, resolveRpcUrl, } from './local-wallet.js';
18
- import { localSignAuthorization, localEthCall, localSignPermit2Upto } from './local-wallet.js';
18
+ import { localSignAuthorization, localEthCall, localGetCode, localSignPermit2Upto } from './local-wallet.js';
19
19
  import { buildUptoAuthorization, uptoPayload, permit2AllowanceCalldata, permit2ApproveCalldata, decodeUint, PERMIT2_ADDRESS, } from './x402-permit2.js';
20
20
  import { assessTokenRisk } from './token-risk.js';
21
- import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, buildPaymentPayloadRaw, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, } from './x402-eip3009.js';
21
+ import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, buildPaymentPayloadRaw, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, parseEip7702Delegation, erc1271ProbeCalldata, classifyErc1271Probe, } from './x402-eip3009.js';
22
22
  import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, } from './local-solana.js';
23
23
  /** True when either chain family is running non-custodially. */
24
24
  function anyLocalMode() {
@@ -128,6 +128,12 @@ async function routeLocally(path, method, body) {
128
128
  return NOT_HANDLED;
129
129
  return localEthCall(localChainId(body, qs), String(body?.to || ''), String(body?.data || '0x'));
130
130
  }
131
+ // POST /eth-get-code (read-only; the payer's account code, checked for an EIP-7702 delegation before an x402 authorization)
132
+ if (method === 'POST' && rawPath === '/eth-get-code') {
133
+ if (!isLocalMode())
134
+ return NOT_HANDLED;
135
+ return localGetCode(localChainId(body, qs), String(body?.address || ''));
136
+ }
131
137
  // GET /wallets/{id}/balance
132
138
  if (method === 'GET' && /^\/wallets\/[^/]*\/balance$/.test(rawPath)) {
133
139
  const chainId = localChainId(body, qs);
@@ -461,7 +467,7 @@ function resolveChainId(network) {
461
467
  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');
462
468
  const server = new McpServer({
463
469
  name: 'agentwallet',
464
- version: '1.12.3',
470
+ version: '1.12.5',
465
471
  }, {
466
472
  instructions: `AgentWallet gives AI agents their own blockchain wallets. Private keys are encrypted server-side and never exposed — agents sign and broadcast transactions without ever touching raw keys.
467
473
 
@@ -994,6 +1000,39 @@ async function payerAddress(walletId) {
994
1000
  throw new Error(`x402: could not resolve the EVM address of wallet ${walletId} (got "${a}").`);
995
1001
  return a;
996
1002
  }
1003
+ /**
1004
+ * Best-effort look at the payer's account code. An EIP-7702 delegated EOA whose delegate
1005
+ * does not implement ERC-1271 is declined by facilitators that check code before recovering
1006
+ * the signer, with an error that reads like a signature fault (issue #9). Nothing is refused
1007
+ * here: the authorization costs nothing to sign and other facilitators accept it. The result
1008
+ * carries the warning so the agent can explain a signature-shaped rejection. A lookup failure
1009
+ * (a hosted server without the route, RPC trouble) reports nothing rather than blocking the payment.
1010
+ */
1011
+ async function payerDelegation(chainId, payer) {
1012
+ try {
1013
+ const r = (await api('/eth-get-code', 'POST', { chain_id: chainId, address: payer }, { 'X-AGW-SKIP-X402': 'true' }));
1014
+ const delegate = parseEip7702Delegation(r?.code);
1015
+ if (!delegate)
1016
+ return null;
1017
+ let erc1271 = 'unknown';
1018
+ try {
1019
+ const probe = (await api('/eth-call', 'POST', { chain_id: chainId, to: payer, data: erc1271ProbeCalldata() }, { 'X-AGW-SKIP-X402': 'true' }));
1020
+ erc1271 = classifyErc1271Probe(probe?.result);
1021
+ }
1022
+ catch {
1023
+ erc1271 = 'unknown';
1024
+ }
1025
+ const warning = erc1271 === 'no'
1026
+ ? `Payer ${payer} is an EIP-7702 delegated account (delegate ${delegate}) and the delegate does not answer ERC-1271 isValidSignature. ` +
1027
+ `Facilitators that check account code before recovering the signer decline this authorization with a signature error. ` +
1028
+ `Pay from a plain EOA, or clear the delegation, for reliable x402 settlement.`
1029
+ : `Payer ${payer} is an EIP-7702 delegated account (delegate ${delegate}); a facilitator may verify the authorization through ERC-1271 on the delegate.`;
1030
+ return { delegate, erc1271, warning };
1031
+ }
1032
+ catch {
1033
+ return null;
1034
+ }
1035
+ }
997
1036
  async function ethCallHex(chainId, to, data) {
998
1037
  const r = (await api('/eth-call', 'POST', { chain_id: chainId, to, data }, { 'X-AGW-SKIP-X402': 'true' }));
999
1038
  return String(r?.result || '');
@@ -1225,8 +1264,10 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
1225
1264
  let uptoAuth = null;
1226
1265
  let payer = null;
1227
1266
  let permit2ApprovalTx = null;
1267
+ let payerDelegationInfo = null;
1228
1268
  if (isUpto) {
1229
1269
  payer = await payerAddress(wallet_id);
1270
+ payerDelegationInfo = await payerDelegation(chainId, payer);
1230
1271
  const allowance = decodeUint(await ethCallHex(chainId, tokenAddress, permit2AllowanceCalldata(payer)));
1231
1272
  if (allowance < BigInt(rawAmount)) {
1232
1273
  if (process.env.AGENTWALLET_PERMIT2_AUTO_APPROVE === '1') {
@@ -1253,6 +1294,7 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
1253
1294
  }
1254
1295
  else if (standardExact) {
1255
1296
  payer = await payerAddress(wallet_id);
1297
+ payerDelegationInfo = await payerDelegation(chainId, payer);
1256
1298
  const domain = await resolveTokenDomain(chainId, tokenAddress, option.extra);
1257
1299
  authorization = buildAuthorization(payer, option);
1258
1300
  const signature = await signAuthorization(wallet_id, chainId, tokenAddress, domain, authorization, approvalUsed);
@@ -1306,6 +1348,8 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
1306
1348
  if (retryRes.status === 402) {
1307
1349
  const again = parsePaymentRequired(n => retryRes.headers.get(n), retryParsed);
1308
1350
  retryError = String(again?.error || retryParsed?.error || 'Payment was not accepted (no reason given).');
1351
+ if (payerDelegationInfo && /signature/i.test(retryError))
1352
+ retryError += ` Likely cause: ${payerDelegationInfo.warning}`;
1309
1353
  }
1310
1354
  return jsonResponse({
1311
1355
  status: retryRes.status,
@@ -1326,6 +1370,7 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
1326
1370
  chain_id: chainId,
1327
1371
  pay_to: option.payTo,
1328
1372
  payer,
1373
+ payer_delegation: payerDelegationInfo,
1329
1374
  tx_hash: txHash ?? settlement?.transaction ?? null,
1330
1375
  settlement,
1331
1376
  authorization: authorization ? { nonce: authorization.nonce, valid_before: authorization.validBefore }
@@ -108,6 +108,10 @@ export declare function localSignAuthorization(chainId: number, asset: Address,
108
108
  export declare function localEthCall(chainId: number, to: Address, data: Hex): Promise<{
109
109
  result: string;
110
110
  }>;
111
+ /** Read-only eth_getCode against the local RPC, same shape as the hosted /eth-get-code route. */
112
+ export declare function localGetCode(chainId: number, address: Address): Promise<{
113
+ code: string;
114
+ }>;
111
115
  /** x402 "upto": sign a Permit2 max-authorization in-process. The maximum goes through the token cap like a transfer would. */
112
116
  export declare function localSignPermit2Upto(chainId: number, auth: UptoPermit2Authorization): Promise<{
113
117
  signature: `0x${string}`;
@@ -15,7 +15,7 @@
15
15
  * on the custodial path and say so explicitly rather than silently downgrading.
16
16
  */
17
17
  import { readFileSync } from 'node:fs';
18
- import { createPublicClient, createWalletClient, http, encodeFunctionData, parseAbi, formatUnits, } from 'viem';
18
+ import { createPublicClient, createWalletClient, http, encodeFunctionData, parseAbi, formatUnits, BaseError, ContractFunctionZeroDataError, ContractFunctionRevertedError, ExecutionRevertedError, AbiDecodingDataSizeTooSmallError, AbiDecodingDataSizeInvalidError, } from 'viem';
19
19
  import { privateKeyToAccount } from 'viem/accounts';
20
20
  import { lookupTrustedDecimals } from './x402-payment.js';
21
21
  import { typedDataFor } from './x402-eip3009.js';
@@ -195,8 +195,7 @@ export async function localTokenBalance(chainId, token) {
195
195
  const decimalsCache = new Map();
196
196
  /**
197
197
  * Decimals for `token`: the trusted registry, then the contract's own
198
- * decimals() (cached on success). Null when the address does not answer like
199
- * an ERC-20 at all, which is how the guard tells a token from a router.
198
+ * decimals() (cached on success). See DecimalsProbe for the other outcomes.
200
199
  */
201
200
  async function probeTokenDecimals(chainId, token) {
202
201
  const known = lookupTrustedDecimals(chainId, token);
@@ -206,28 +205,47 @@ async function probeTokenDecimals(chainId, token) {
206
205
  const cached = decimalsCache.get(key);
207
206
  if (typeof cached === 'number')
208
207
  return cached;
208
+ let onChain;
209
209
  try {
210
210
  const { pub } = clients(chainId);
211
- const onChain = await pub.readContract({
211
+ onChain = await pub.readContract({
212
212
  address: token,
213
213
  abi: ERC20_ABI,
214
214
  functionName: 'decimals',
215
215
  });
216
- const d = Number(onChain);
217
- // Reject nonsense rather than trusting it; an out-of-range value would
218
- // widen the ceiling exactly like the old assumption did.
219
- if (Number.isInteger(d) && d >= 0 && d <= 36) {
220
- decimalsCache.set(key, d);
221
- return d;
222
- }
223
216
  }
224
- catch {
225
- // Unreachable RPC, non-standard token, not a token at all.
217
+ catch (err) {
218
+ // viem wraps every readContract failure in ContractFunctionExecutionError;
219
+ // the cause says whether the contract itself declined (revert, empty
220
+ // return: not an ERC-20) or the answer never arrived (RPC down, timeout,
221
+ // malformed response). Only the first is evidence of anything.
222
+ const declined = err instanceof BaseError && err.walk((e) => e instanceof ContractFunctionZeroDataError
223
+ || e instanceof ContractFunctionRevertedError
224
+ || e instanceof ExecutionRevertedError // nodes that report a revert as -32000 rather than 3
225
+ ) !== null;
226
+ if (declined)
227
+ return 'not-a-token';
228
+ // A decode failure on non-empty data means the contract answered
229
+ // decimals() with something that is not a uint8: a token, just a broken one.
230
+ const decodeFailed = err instanceof BaseError && err.walk((e) => e instanceof AbiDecodingDataSizeTooSmallError || e instanceof AbiDecodingDataSizeInvalidError) !== null;
231
+ return decodeFailed ? 'unusable' : 'unreachable';
226
232
  }
227
- return null;
233
+ const d = Number(onChain);
234
+ // Reject nonsense rather than trusting it; an out-of-range value would
235
+ // widen the ceiling exactly like the old assumption did.
236
+ if (Number.isInteger(d) && d >= 0 && d <= 36) {
237
+ decimalsCache.set(key, d);
238
+ return d;
239
+ }
240
+ return 'unusable';
228
241
  }
242
+ /**
243
+ * Decimals to price a known transfer or approval at. Anything short of a
244
+ * usable answer is evaluated at 0, the only value that cannot fail open.
245
+ */
229
246
  async function resolveTokenDecimals(chainId, token) {
230
- return (await probeTokenDecimals(chainId, token)) ?? 0;
247
+ const probe = await probeTokenDecimals(chainId, token);
248
+ return typeof probe === 'number' ? probe : 0;
231
249
  }
232
250
  /** Canonical Permit2, the same address on every EVM chain it is deployed to. */
233
251
  const PERMIT2 = '0x000000000022d473030f116ddee9f6b43ac78ba3';
@@ -263,12 +281,18 @@ async function assertWithinTokenCap(chainId, to, data) {
263
281
  if (!priced) {
264
282
  if (process.env.AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS === '1')
265
283
  return;
266
- const looksLikeToken = isPermit2 || (await probeTokenDecimals(chainId, to)) !== null;
267
- if (looksLikeToken) {
268
- throw new Error(`Blocked by local guard: calldata selector 0x${selector} sent to ` +
269
- `${isPermit2 ? 'Permit2' : 'a token contract'} is not one AGENTWALLET_MAX_TX_TOKEN can price, ` +
270
- `so it is refused rather than let through uncapped. Use transfer, transferFrom, approve, ` +
271
- `increaseAllowance or Permit2 approve, or set AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1 to allow it deliberately.`);
284
+ // Only a contract that demonstrably declines decimals() is let through.
285
+ // "Answered nonsense" and "could not ask" both refuse: the first is a
286
+ // token with a broken or hostile decimals(), the second is no evidence at
287
+ // all, and this guard does not allow on no evidence (2026-09-26 report).
288
+ const probe = isPermit2 ? 'unusable' : await probeTokenDecimals(chainId, to);
289
+ if (probe !== 'not-a-token') {
290
+ const why = probe === 'unreachable'
291
+ ? `the RPC for chain ${chainId} could not be reached to classify the target, so the guard cannot tell a token from a router`
292
+ : `the target is ${isPermit2 ? 'Permit2' : 'a token contract'} and this calldata is not one AGENTWALLET_MAX_TX_TOKEN can price`;
293
+ throw new Error(`Blocked by local guard: calldata selector 0x${selector} refused rather than let through uncapped: ${why}. ` +
294
+ `Use transfer, transferFrom, approve, increaseAllowance or Permit2 approve, ` +
295
+ `or set AGENTWALLET_ALLOW_UNKNOWN_TOKEN_CALLS=1 to allow it deliberately.`);
272
296
  }
273
297
  return; // an ordinary contract call; it can only pull what an approval already allowed
274
298
  }
@@ -380,6 +404,12 @@ export async function localEthCall(chainId, to, data) {
380
404
  const r = await client.call({ to, data });
381
405
  return { result: r.data ?? '0x' };
382
406
  }
407
+ /** Read-only eth_getCode against the local RPC, same shape as the hosted /eth-get-code route. */
408
+ export async function localGetCode(chainId, address) {
409
+ const client = createPublicClient({ transport: http(resolveRpcUrl(chainId)) });
410
+ const code = await client.getCode({ address });
411
+ return { code: code ?? '0x' };
412
+ }
383
413
  /** x402 "upto": sign a Permit2 max-authorization in-process. The maximum goes through the token cap like a transfer would. */
384
414
  export async function localSignPermit2Upto(chainId, auth) {
385
415
  const token = auth.permitted.token;
@@ -169,3 +169,22 @@ export declare function pickOption(accepts: X402Requirement[], resolveChainId: (
169
169
  chainId: number | null;
170
170
  reason?: string;
171
171
  };
172
+ /**
173
+ * An EIP-7702 delegated EOA has code 0xef0100 || delegate (23 bytes). A facilitator that
174
+ * checks account code before recovering the signer treats such a payer as a contract
175
+ * wallet and calls ERC-1271 isValidSignature on it; a delegate that does not implement
176
+ * it answers with empty data, and the payment is declined with a signature-shaped error
177
+ * that has nothing to do with the signature (issue #9: a well-known test key that
178
+ * sweeper bots had delegated on Base mainnet).
179
+ */
180
+ export declare function parseEip7702Delegation(code: string | null | undefined): string | null;
181
+ export declare const ERC1271_MAGIC = "0x1626ba7e";
182
+ /** Calldata for isValidSignature(bytes32, bytes) with a placeholder hash and a 65-byte placeholder signature. */
183
+ export declare function erc1271ProbeCalldata(): `0x${string}`;
184
+ export type Erc1271Support = 'yes' | 'no' | 'unknown';
185
+ /**
186
+ * Classify an isValidSignature probe. Empty return data means the account has no such
187
+ * function (the failure shape from issue #9); a 4-byte word means the interface exists,
188
+ * whatever it thought of the placeholder signature; a revert or transport failure is unknown.
189
+ */
190
+ export declare function classifyErc1271Probe(result: string | null | undefined, failed?: boolean): Erc1271Support;
@@ -205,3 +205,40 @@ export function pickOption(accepts, resolveChainId, preferChain) {
205
205
  option = payable.find(a => resolveChainId(a.network) === preferChain) ?? option;
206
206
  return { option, chainId: resolveChainId(option.network) };
207
207
  }
208
+ // ─── Payer account shape (EIP-7702) ──────────────────────────────
209
+ /**
210
+ * An EIP-7702 delegated EOA has code 0xef0100 || delegate (23 bytes). A facilitator that
211
+ * checks account code before recovering the signer treats such a payer as a contract
212
+ * wallet and calls ERC-1271 isValidSignature on it; a delegate that does not implement
213
+ * it answers with empty data, and the payment is declined with a signature-shaped error
214
+ * that has nothing to do with the signature (issue #9: a well-known test key that
215
+ * sweeper bots had delegated on Base mainnet).
216
+ */
217
+ export function parseEip7702Delegation(code) {
218
+ const m = /^0xef0100([0-9a-f]{40})$/.exec(String(code || '').toLowerCase());
219
+ return m ? `0x${m[1]}` : null;
220
+ }
221
+ export const ERC1271_MAGIC = '0x1626ba7e';
222
+ /** Calldata for isValidSignature(bytes32, bytes) with a placeholder hash and a 65-byte placeholder signature. */
223
+ export function erc1271ProbeCalldata() {
224
+ const hash = '11'.repeat(32);
225
+ const offset = (64).toString(16).padStart(64, '0');
226
+ const length = (65).toString(16).padStart(64, '0');
227
+ const sig = 'aa'.repeat(65).padEnd(192, '0');
228
+ return `${ERC1271_MAGIC}${hash}${offset}${length}${sig}`;
229
+ }
230
+ /**
231
+ * Classify an isValidSignature probe. Empty return data means the account has no such
232
+ * function (the failure shape from issue #9); a 4-byte word means the interface exists,
233
+ * whatever it thought of the placeholder signature; a revert or transport failure is unknown.
234
+ */
235
+ export function classifyErc1271Probe(result, failed = false) {
236
+ if (failed)
237
+ return 'unknown';
238
+ const r = String(result || '').toLowerCase();
239
+ if (r === '' || r === '0x')
240
+ return 'no';
241
+ if (/^0x[0-9a-f]{8}0{56}$/.test(r))
242
+ return 'yes';
243
+ return 'unknown';
244
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentwallet-mcp",
3
3
  "mcpName": "io.github.hifriendbot/agentwallet",
4
- "version": "1.12.3",
4
+ "version": "1.12.5",
5
5
  "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.",
6
6
  "type": "module",
7
7
  "main": "build/index.js",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "build": "tsc",
18
18
  "start": "node build/index.js",
19
- "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/local-mode.test.mjs && node test/ssrf-guard.test.mjs",
19
+ "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",
20
20
  "prepublishOnly": "npm run build"
21
21
  },
22
22
  "keywords": [