agentwallet-mcp 1.10.9 → 1.11.0

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
@@ -219,15 +219,15 @@ All on-chain. All through MCP. No frontend needed.
219
219
 
220
220
  ## x402 Payments
221
221
 
222
- AgentWallet natively supports the [x402 open payment standard](https://x402.org). When your Ai agent encounters an API that returns HTTP 402 Payment Required, the `pay_x402` tool handles the entire flow automatically:
222
+ AgentWallet speaks the [x402 open payment standard](https://x402.org) the way the spec defines it. When your Ai agent hits an API that answers HTTP 402, `pay_x402` does the whole flow:
223
223
 
224
- 1. Fetches the URL and detects the 402 response
225
- 2. Parses the payment requirements (amount, token, chain)
226
- 3. Executes the on-chain payment from your wallet
227
- 4. Retries the request with proof of payment
228
- 5. Returns the final response
224
+ 1. Fetches the URL and reads the payment requirements, from the v2 `PAYMENT-REQUIRED` header or the v1 JSON body.
225
+ 2. Picks an `exact` option (you can steer it with `prefer_chain`).
226
+ 3. Signs an **EIP-3009 `TransferWithAuthorization`** for exactly that amount. Nothing is broadcast and no gas is paid by the payer; the endpoint's facilitator settles it on-chain.
227
+ 4. Retries the request with the payment header (`PAYMENT-SIGNATURE` for v2, `X-PAYMENT` for v1).
228
+ 5. Returns the response, the settlement receipt (`PAYMENT-RESPONSE` / `X-PAYMENT-RESPONSE`) and, if the endpoint declined, its stated reason in `retry_error`.
229
229
 
230
- **Set `max_payment` to control spending:**
230
+ Verified against Coinbase's public facilitator (`isValid: true` for v1 and v2 payloads) and live x402 servers on Base. Works in both custody modes: the local key signs in-process; hosted wallets sign through `POST /wallets/{id}/x402/authorize`, a narrow endpoint that only ever signs this one struct and runs the same pause and token-cap checks as a transfer.
231
231
 
232
232
  ```
233
233
  pay_x402(
@@ -237,9 +237,11 @@ pay_x402(
237
237
  )
238
238
  ```
239
239
 
240
- `max_payment` is enforced as a hard per-payment cap. If you omit it, `pay_x402` falls back to `AGENTWALLET_MAX_AUTOPAY` (default `1`), the same cap the auto-pay path uses, so a malicious or compromised 402 endpoint can never authorize an unbounded payment. Set `max_payment` explicitly (or raise `AGENTWALLET_MAX_AUTOPAY`) to allow a larger single payment.
240
+ `max_payment` is a hard per-payment cap. If you omit it, `pay_x402` falls back to `AGENTWALLET_MAX_AUTOPAY` (default `1`), so a malicious or compromised 402 endpoint can never authorize an unbounded payment. In local mode `AGENTWALLET_MAX_TX_TOKEN` applies to authorizations too, because an authorization is a transfer somebody else executes.
241
241
 
242
- Supports ERC-20 tokens, SPL tokens, and native tokens on EVM and Solana. Compatible with x402 V1 and V2 (CAIP-2 chain identifiers), and reads the token address from the standard x402 `asset` field (falling back to `extra.token`).
242
+ **Schemes.** `exact` is fully supported on every EVM chain AgentWallet knows. `upto` (a Permit2 maximum the seller settles below) is refused with a clear message rather than approximated with an upfront transfer; Permit2 support is next. Native-asset and Solana requests, and AgentWallet's own paywalls, are paid by an on-chain transfer proved by transaction hash, which is what those servers verify.
243
+
244
+ **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.
243
245
 
244
246
  ## x402 Acceptance
245
247
 
package/build/index.js CHANGED
@@ -15,6 +15,8 @@ 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 } from './local-wallet.js';
19
+ import { isLegacyAgentWalletAccept, requiredAmount, buildAuthorization, buildPaymentPayload, paymentHeaderFor, parsePaymentRequired, parseSettlement, pickOption, knownTokenDomain, } from './x402-eip3009.js';
18
20
  import { isSolanaLocalMode, getSolanaAddress, solanaWalletRecord, localSolBalance, localSplBalance, localSolTransfer, localSplTransfer, resolveSolanaRpc, } from './local-solana.js';
19
21
  /** True when either chain family is running non-custodially. */
20
22
  function anyLocalMode() {
@@ -90,6 +92,26 @@ async function routeLocally(path, method, body) {
90
92
  const hasData = data && data !== '0x';
91
93
  return localSend(chainId, to, value, hasData ? data : undefined);
92
94
  }
95
+ // POST /wallets/{id}/x402/authorize (x402 "exact": EIP-3009 authorization, signed here, broadcast by nobody)
96
+ if (method === 'POST' && /^\/wallets\/[^/]*\/x402\/authorize$/.test(rawPath)) {
97
+ if (!isLocalMode())
98
+ refuseMissingLocalKey('evm');
99
+ const chainId = localChainId(body, qs);
100
+ return localSignAuthorization(chainId, String(body?.asset || ''), { name: String(body?.name || ''), version: String(body?.version || '') }, {
101
+ from: getLocalAddress(),
102
+ to: String(body?.to || ''),
103
+ value: String(body?.value ?? '0'),
104
+ validAfter: String(body?.valid_after ?? '0'),
105
+ validBefore: String(body?.valid_before ?? '0'),
106
+ nonce: String(body?.nonce || ''),
107
+ });
108
+ }
109
+ // POST /eth-call (read-only; answered from the local RPC so pure self-custody needs no hosted credentials)
110
+ if (method === 'POST' && rawPath === '/eth-call') {
111
+ if (!isLocalMode())
112
+ return NOT_HANDLED;
113
+ return localEthCall(localChainId(body, qs), String(body?.to || ''), String(body?.data || '0x'));
114
+ }
93
115
  // GET /wallets/{id}/balance
94
116
  if (method === 'GET' && /^\/wallets\/[^/]*\/balance$/.test(rawPath)) {
95
117
  const chainId = localChainId(body, qs);
@@ -423,7 +445,7 @@ function resolveChainId(network) {
423
445
  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');
424
446
  const server = new McpServer({
425
447
  name: 'agentwallet',
426
- version: '1.10.9',
448
+ version: '1.11.0',
427
449
  }, {
428
450
  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.
429
451
 
@@ -935,11 +957,63 @@ server.tool('get_token_info', 'Get the name, symbol, and decimals of any ERC-20
935
957
  decimals,
936
958
  });
937
959
  });
960
+ // ─── x402 "exact" helpers ────────────────────────────────────────
961
+ /** The paying address: the local key in self-custody mode, else the hosted wallet record. */
962
+ async function payerAddress(walletId) {
963
+ const w = (await api(`/wallets/${walletId}`, 'GET', undefined, { 'X-AGW-SKIP-X402': 'true' }));
964
+ const a = String(w?.address || w?.wallet_address || '');
965
+ if (!/^0x[0-9a-fA-F]{40}$/.test(a))
966
+ throw new Error(`x402: could not resolve the EVM address of wallet ${walletId} (got "${a}").`);
967
+ return a;
968
+ }
969
+ async function ethCallHex(chainId, to, data) {
970
+ const r = (await api('/eth-call', 'POST', { chain_id: chainId, to, data }, { 'X-AGW-SKIP-X402': 'true' }));
971
+ return String(r?.result || '');
972
+ }
973
+ /**
974
+ * The token's EIP-712 domain. Order: what the 402 declared (extra.name/version,
975
+ * which is what the reference client requires), then a short list of USDC
976
+ * deployments we know, then the contract's own name()/version(). A wrong
977
+ * domain produces a signature that recovers to a stranger, so we refuse
978
+ * rather than guess.
979
+ */
980
+ async function resolveTokenDomain(chainId, asset, extra) {
981
+ if (extra?.name && extra?.version)
982
+ return { name: String(extra.name), version: String(extra.version) };
983
+ const known = knownTokenDomain(chainId, asset);
984
+ if (known)
985
+ return known;
986
+ let name = '', version = '';
987
+ try {
988
+ name = decodeAbiString(await ethCallHex(chainId, asset, '0x06fdde03')); // name()
989
+ version = decodeAbiString(await ethCallHex(chainId, asset, '0x54fd4d50')); // version()
990
+ }
991
+ catch { /* refused below */ }
992
+ if (!name || !version) {
993
+ throw new Error(`x402: cannot determine the EIP-712 domain for token ${asset} on chain ${chainId}: the endpoint sent no ` +
994
+ `extra.name/extra.version and the contract did not answer name()/version(). Refused rather than signed with a guessed domain.`);
995
+ }
996
+ return { name, version };
997
+ }
998
+ /** Sign the authorization with whichever custody mode is active (local key, or the hosted signer). */
999
+ async function signAuthorization(walletId, chainId, asset, domain, auth) {
1000
+ const r = (await api(`/wallets/${walletId}/x402/authorize`, 'POST', {
1001
+ chain_id: chainId, asset, name: domain.name, version: domain.version,
1002
+ to: auth.to, value: auth.value, valid_after: auth.validAfter, valid_before: auth.validBefore, nonce: auth.nonce,
1003
+ }, { 'X-AGW-SKIP-X402': 'true' }));
1004
+ const sig = String(r?.signature || '');
1005
+ if (!/^0x[0-9a-fA-F]{130}$/.test(sig)) {
1006
+ throw new Error(`x402: the wallet did not return a valid authorization signature${r?.error ? ` (${r.error})` : ''}.`);
1007
+ }
1008
+ return sig;
1009
+ }
938
1010
  // ─── Tool: pay_x402 ─────────────────────────────────────────────
939
1011
  server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server returns HTTP 402 Payment Required, ' +
940
- 'parses the payment requirements, executes the on-chain payment, and retries the request with ' +
941
- 'proof of payment. Returns the final response. Supports the x402 open payment standard ' +
942
- '(https://x402.org). Set max_payment to prevent overspending.', {
1012
+ 'parses the payment requirements (v1 body or v2 PAYMENT-REQUIRED header), signs an EIP-3009 ' +
1013
+ 'TransferWithAuthorization for the "exact" scheme (no gas, nothing broadcast by the payer), and retries ' +
1014
+ 'the request with the payment header. AgentWallet paywalls are paid by on-chain transfer instead. ' +
1015
+ 'The "upto" scheme is refused, never approximated. Returns the final response and the settlement ' +
1016
+ 'receipt. Supports the x402 open payment standard (https://x402.org). Set max_payment to cap spend.', {
943
1017
  url: z.string().url().describe('The URL to access (will handle 402 payment if required)'),
944
1018
  wallet_id: z.number().int().describe('Wallet ID to pay from'),
945
1019
  method: z.string().default('GET').describe('HTTP method (GET, POST, PUT, DELETE)'),
@@ -999,125 +1073,114 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
999
1073
  response: parsed,
1000
1074
  });
1001
1075
  }
1002
- // Step 2: Parse x402 payment requirements from 402 response
1003
- let paymentInfo;
1076
+ // Step 2: Requirements come from the PAYMENT-REQUIRED header (v2) or the JSON body (v1).
1077
+ const initialText = await initialRes.text();
1078
+ let initialBody = null;
1004
1079
  try {
1005
- paymentInfo = await initialRes.json();
1080
+ initialBody = JSON.parse(initialText);
1006
1081
  }
1007
1082
  catch {
1008
- throw new Error('402 response body is not valid JSON. This server may not support x402.');
1009
- }
1010
- if (!paymentInfo.accepts || paymentInfo.accepts.length === 0) {
1011
- throw new Error('402 response has no payment options in "accepts" array.');
1012
- }
1013
- const options = paymentInfo.accepts;
1014
- let option;
1015
- if (prefer_chain) {
1016
- option = options.find(a => resolveChainId(a.network) === prefer_chain) || options[0];
1017
- }
1018
- else {
1019
- option = options[0];
1083
+ initialBody = null;
1084
+ }
1085
+ const paymentInfo = parsePaymentRequired(n => initialRes.headers.get(n), initialBody);
1086
+ if (!paymentInfo || !Array.isArray(paymentInfo.accepts) || paymentInfo.accepts.length === 0) {
1087
+ throw new Error('402 response carried no x402 payment requirements (no PAYMENT-REQUIRED header and no "accepts" array in the body). ' +
1088
+ 'This server may not support x402.');
1089
+ }
1090
+ const x402Version = Number(paymentInfo.x402Version) || 1;
1091
+ const accepts = paymentInfo.accepts;
1092
+ // Step 3: Pick an option this client can sign. "exact" only; "upto" is refused, never approximated (issue #6).
1093
+ const picked = pickOption(accepts, resolveChainId, prefer_chain);
1094
+ if (!picked.option || !picked.chainId) {
1095
+ return jsonResponse({
1096
+ status: 402,
1097
+ payment_required: true,
1098
+ payment_made: false,
1099
+ error: picked.reason,
1100
+ offered: accepts.map(a => ({ scheme: a.scheme, network: a.network, amount: requiredAmount(a), asset: a.asset ?? a.extra?.token ?? null })),
1101
+ });
1020
1102
  }
1021
- // Resolve the chain
1022
- const chainId = resolveChainId(option.network);
1023
- if (!chainId) {
1024
- throw new Error(`Unsupported x402 network: "${option.network}". ` +
1025
- `Supported: ${Object.keys(X402_NETWORKS).join(', ')}, or any CAIP-2 / numeric chain ID.`);
1103
+ const option = picked.option;
1104
+ const chainId = picked.chainId;
1105
+ const rawAmount = requiredAmount(option);
1106
+ if (!/^\d+$/.test(rawAmount)) {
1107
+ throw new Error(`x402: invalid amount "${rawAmount}" in the payment requirements (expected integer base units).`);
1026
1108
  }
1027
1109
  // Decimals come from the registry or the token contract, NEVER from the 402
1028
- // body. The endpoint controls requiredDecimals, so trusting it lets a
1029
- // hostile server inflate the cap AND misreport the amount shown to the
1030
- // agent. The declared value is only used to detect a mismatch and refuse.
1031
- const assetForDecimals = option.asset || option.extra?.token || '';
1032
- const trustedDecimals = assetForDecimals
1033
- ? await resolveTrustedDecimals(chainId, assetForDecimals)
1110
+ // body. The endpoint controls what it declares, so trusting it would let a
1111
+ // hostile server inflate the cap and misreport the amount shown to the agent.
1112
+ const tokenAddress = option.asset || option.extra?.token || '';
1113
+ const trustedDecimals = tokenAddress
1114
+ ? await resolveTrustedDecimals(chainId, tokenAddress)
1034
1115
  : nativeDecimals(chainId); // 18 on EVM (wei), 9 on Solana (lamports)
1035
- assertDeclaredDecimals(option.requiredDecimals, trustedDecimals);
1036
- // Human-readable amount, computed with trusted decimals so what the agent
1037
- // is told matches what will actually leave the wallet.
1038
- const amount = formatUnits(option.maxAmountRequired, trustedDecimals);
1039
- // Step 4: Enforce a hard per-payment cap. ALWAYS applied — if the caller
1040
- // omits max_payment we fall back to AGENTWALLET_MAX_AUTOPAY (default "1"),
1041
- // mirroring the internal auto-pay path (deriveX402Payment). Without this, an
1042
- // untrusted or compromised x402 endpoint could return an arbitrarily large
1043
- // maxAmountRequired and drain the wallet. The cap is never silently skipped.
1116
+ if (typeof option.requiredDecimals === 'number')
1117
+ assertDeclaredDecimals(option.requiredDecimals, trustedDecimals);
1118
+ const amount = formatUnits(rawAmount, trustedDecimals);
1119
+ const tokenLabel = option.extra?.name || (tokenAddress ? 'tokens' : 'native');
1120
+ // Step 4: Hard per-payment cap, ALWAYS applied. If the caller omits max_payment
1121
+ // we fall back to AGENTWALLET_MAX_AUTOPAY (default "1"), so an untrusted 402
1122
+ // endpoint can never authorize an unbounded payment. Never silently skipped.
1044
1123
  const capSource = max_payment ? 'max_payment' : 'AGENTWALLET_MAX_AUTOPAY';
1045
1124
  const effectiveMax = max_payment || process.env.AGENTWALLET_MAX_AUTOPAY || '1';
1046
- if (!isWithinCap(option.maxAmountRequired, trustedDecimals, effectiveMax)) {
1125
+ if (!isWithinCap(rawAmount, trustedDecimals, effectiveMax)) {
1047
1126
  return jsonResponse({
1048
1127
  status: 402,
1049
1128
  payment_required: true,
1050
1129
  payment_made: false,
1051
- error: `Payment of ${amount} ${option.extra?.name || 'tokens'} exceeds the ` +
1052
- `${effectiveMax} cap (from ${capSource}).` +
1130
+ error: `Payment of ${amount} ${tokenLabel} exceeds the ${effectiveMax} cap (from ${capSource}).` +
1053
1131
  (max_payment ? '' : ' No max_payment was provided, so the default cap was applied. ' +
1054
1132
  'Pass max_payment (or raise AGENTWALLET_MAX_AUTOPAY) to allow a larger payment.'),
1055
1133
  required_amount: amount,
1056
1134
  max_allowed: effectiveMax,
1057
1135
  cap_source: capSource,
1058
- token: option.extra?.name || 'native',
1136
+ token: tokenLabel,
1059
1137
  network: option.network,
1060
1138
  chain_id: chainId,
1061
1139
  pay_to: option.payTo,
1062
1140
  description: option.description,
1063
1141
  });
1064
1142
  }
1065
- // Standard x402 puts the token address in `asset`; AgentWallet's own server
1066
- // currently emits it under extra.token. Prefer the standard field, fall back.
1067
- const tokenAddress = option.asset || option.extra?.token || '';
1068
- // Step 5: Execute payment
1069
- let txResult;
1070
- if (isSolanaChain(chainId)) {
1071
- // Solana payment
1072
- if (tokenAddress) {
1073
- // SPL token payment (e.g. USDC on Solana)
1074
- txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
1075
- to: option.payTo,
1076
- value: option.maxAmountRequired,
1077
- token_mint: tokenAddress,
1078
- token_decimals: trustedDecimals,
1079
- chain_id: chainId,
1080
- }));
1143
+ // Step 5: Pay. Standard x402 "exact" on EVM is an EIP-3009 authorization the
1144
+ // facilitator settles; the payer broadcasts nothing and pays no gas. AgentWallet's
1145
+ // own paywalls (verified by receipt), native-asset requests and Solana still use
1146
+ // a broadcast transfer proved by hash.
1147
+ const standardExact = !isSolanaChain(chainId) && Boolean(tokenAddress) && !isLegacyAgentWalletAccept(option);
1148
+ let headerName = 'X-PAYMENT';
1149
+ let paymentHeader = '';
1150
+ let txHash = null;
1151
+ let authorization = null;
1152
+ let payer = null;
1153
+ if (standardExact) {
1154
+ payer = await payerAddress(wallet_id);
1155
+ const domain = await resolveTokenDomain(chainId, tokenAddress, option.extra);
1156
+ authorization = buildAuthorization(payer, option);
1157
+ const signature = await signAuthorization(wallet_id, chainId, tokenAddress, domain, authorization);
1158
+ const payload = buildPaymentPayload(x402Version, option, authorization, signature, paymentInfo.resource, paymentInfo.extensions);
1159
+ const h = paymentHeaderFor(x402Version, payload);
1160
+ headerName = h.name;
1161
+ paymentHeader = h.value;
1162
+ }
1163
+ else {
1164
+ let txResult;
1165
+ if (isSolanaChain(chainId)) {
1166
+ txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', tokenAddress
1167
+ ? { to: option.payTo, value: rawAmount, token_mint: tokenAddress, token_decimals: trustedDecimals, chain_id: chainId }
1168
+ : { to: option.payTo, value: rawAmount, chain_id: chainId }));
1169
+ }
1170
+ else if (tokenAddress) {
1171
+ const calldata = '0xa9059cbb' + padAddress(option.payTo) + encodeUint256(rawAmount);
1172
+ txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: tokenAddress, value: '0', data: calldata, chain_id: chainId }));
1081
1173
  }
1082
1174
  else {
1083
- // Native SOL payment
1084
- txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
1085
- to: option.payTo,
1086
- value: option.maxAmountRequired,
1087
- chain_id: chainId,
1088
- }));
1175
+ txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', { to: option.payTo, value: rawAmount, data: '', chain_id: chainId }));
1089
1176
  }
1177
+ txHash = String(txResult.tx_hash || txResult.signature || '');
1178
+ if (!txHash)
1179
+ throw new Error('x402: the payment transaction returned no hash.');
1180
+ paymentHeader = Buffer.from(JSON.stringify({ x402Version, scheme: option.scheme, network: option.network, payload: { txHash } })).toString('base64');
1090
1181
  }
1091
- else if (tokenAddress) {
1092
- // EVM ERC-20 token payment (e.g. USDC)
1093
- const calldata = '0xa9059cbb' + padAddress(option.payTo) + encodeUint256(option.maxAmountRequired);
1094
- txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
1095
- to: tokenAddress,
1096
- value: '0',
1097
- data: calldata,
1098
- chain_id: chainId,
1099
- }));
1100
- }
1101
- else {
1102
- // EVM native token payment (ETH, etc.)
1103
- txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
1104
- to: option.payTo,
1105
- value: option.maxAmountRequired,
1106
- data: '',
1107
- chain_id: chainId,
1108
- }));
1109
- }
1110
- // Solana returns 'signature', EVM returns 'tx_hash'
1111
- const txHash = (txResult.tx_hash || txResult.signature);
1112
- // Step 6: Build x402 payment proof and retry
1113
- const paymentProof = {
1114
- x402Version: paymentInfo.x402Version || 1,
1115
- scheme: option.scheme,
1116
- network: option.network,
1117
- payload: { txHash },
1118
- };
1119
- const paymentHeader = Buffer.from(JSON.stringify(paymentProof)).toString('base64');
1120
- const retryHeaders = { ...reqHeaders, 'X-PAYMENT': paymentHeader };
1182
+ // Step 6: Retry with the payment header and read the settlement receipt.
1183
+ const retryHeaders = { ...reqHeaders, [headerName]: paymentHeader };
1121
1184
  const retryOptions = {
1122
1185
  method,
1123
1186
  headers: retryHeaders,
@@ -1135,17 +1198,31 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
1135
1198
  catch {
1136
1199
  retryParsed = retryText;
1137
1200
  }
1201
+ const settlement = parseSettlement(n => retryRes.headers.get(n));
1202
+ // A second 402 means the facilitator declined (unfunded payer, expired window, bad domain...). v2 servers
1203
+ // put the reason in a fresh PAYMENT-REQUIRED header, v1 servers in the body; surface it instead of {}.
1204
+ let retryError = null;
1205
+ if (retryRes.status === 402) {
1206
+ const again = parsePaymentRequired(n => retryRes.headers.get(n), retryParsed);
1207
+ retryError = String(again?.error || retryParsed?.error || 'Payment was not accepted (no reason given).');
1208
+ }
1138
1209
  return jsonResponse({
1139
1210
  status: retryRes.status,
1140
1211
  payment_required: true,
1141
- payment_made: true,
1212
+ payment_made: retryRes.status !== 402,
1213
+ retry_error: retryError,
1214
+ payment_method: standardExact ? 'eip3009-authorization' : 'onchain-transfer',
1215
+ x402_version: x402Version,
1142
1216
  amount,
1143
- token: option.extra?.name || 'native',
1144
- token_address: option.asset || option.extra?.token || null,
1217
+ token: tokenLabel,
1218
+ token_address: tokenAddress || null,
1145
1219
  network: option.network,
1146
1220
  chain_id: chainId,
1147
1221
  pay_to: option.payTo,
1148
- tx_hash: txHash,
1222
+ payer,
1223
+ tx_hash: txHash ?? settlement?.transaction ?? null,
1224
+ settlement,
1225
+ authorization: authorization ? { nonce: authorization.nonce, valid_before: authorization.validBefore } : null,
1149
1226
  description: option.description,
1150
1227
  response: retryParsed,
1151
1228
  });
@@ -15,6 +15,7 @@
15
15
  * on the custodial path and say so explicitly rather than silently downgrading.
16
16
  */
17
17
  import { type Address, type Hex } from 'viem';
18
+ import { type Eip3009Authorization } from './x402-eip3009.js';
18
19
  /** True when the server is running non-custodially. */
19
20
  export declare function isLocalMode(): boolean;
20
21
  export declare function getLocalAccount(): {
@@ -88,3 +89,21 @@ export declare function localWalletRecord(): {
88
89
  custody: string;
89
90
  note: string;
90
91
  };
92
+ /**
93
+ * Sign a TransferWithAuthorization for an x402 payment. Nothing is broadcast;
94
+ * the resource server's facilitator settles it on-chain. The amount is put
95
+ * through the same AGENTWALLET_MAX_TX_TOKEN guard as a transfer, because that
96
+ * is exactly what the authorization lets someone else execute.
97
+ */
98
+ export declare function localSignAuthorization(chainId: number, asset: Address, domain: {
99
+ name: string;
100
+ version: string;
101
+ }, auth: Eip3009Authorization): Promise<{
102
+ signature: `0x${string}`;
103
+ from: Address;
104
+ mode: 'local';
105
+ }>;
106
+ /** Read-only eth_call against the local RPC, same shape as the hosted /eth-call route. */
107
+ export declare function localEthCall(chainId: number, to: Address, data: Hex): Promise<{
108
+ result: string;
109
+ }>;
@@ -18,6 +18,7 @@ import { readFileSync } from 'node:fs';
18
18
  import { createPublicClient, createWalletClient, http, encodeFunctionData, parseAbi, formatUnits, } from 'viem';
19
19
  import { privateKeyToAccount } from 'viem/accounts';
20
20
  import { lookupTrustedDecimals } from './x402-payment.js';
21
+ import { typedDataFor } from './x402-eip3009.js';
21
22
  /* ── Key loading ─────────────────────────────────────────────────── */
22
23
  /**
23
24
  * Read the key once at startup. Kept module-private: nothing exports the key
@@ -306,3 +307,31 @@ export function localWalletRecord() {
306
307
  note: 'Signed in-process. The private key is never sent to AgentWallet servers.',
307
308
  };
308
309
  }
310
+ /* ── x402 "exact": EIP-3009 authorization signed in-process ──────── */
311
+ /**
312
+ * Sign a TransferWithAuthorization for an x402 payment. Nothing is broadcast;
313
+ * the resource server's facilitator settles it on-chain. The amount is put
314
+ * through the same AGENTWALLET_MAX_TX_TOKEN guard as a transfer, because that
315
+ * is exactly what the authorization lets someone else execute.
316
+ */
317
+ export async function localSignAuthorization(chainId, asset, domain, auth) {
318
+ if (!/^0x[a-fA-F0-9]{40}$/.test(asset))
319
+ throw new Error(`Local mode needs an EVM token address for x402, got "${asset}".`);
320
+ if (!/^0x[a-fA-F0-9]{40}$/.test(auth.to))
321
+ throw new Error(`Local mode needs an EVM payTo address for x402, got "${auth.to}".`);
322
+ if (!/^\d+$/.test(auth.value))
323
+ throw new Error(`x402 authorization value must be integer base units, got "${auth.value}".`);
324
+ const calldata = ('0xa9059cbb'
325
+ + auth.to.slice(2).toLowerCase().padStart(64, '0')
326
+ + BigInt(auth.value).toString(16).padStart(64, '0'));
327
+ await assertWithinTokenCap(chainId, asset, calldata);
328
+ const from = getLocalAddress();
329
+ const signature = await getLocalAccount().signTypedData(typedDataFor(chainId, asset, domain.name, domain.version, { ...auth, from }));
330
+ return { signature, from, mode: 'local' };
331
+ }
332
+ /** Read-only eth_call against the local RPC, same shape as the hosted /eth-call route. */
333
+ export async function localEthCall(chainId, to, data) {
334
+ const client = createPublicClient({ transport: http(resolveRpcUrl(chainId)) });
335
+ const r = await client.call({ to, data });
336
+ return { result: r.data ?? '0x' };
337
+ }
@@ -0,0 +1,168 @@
1
+ export declare const TRANSFER_WITH_AUTHORIZATION_TYPES: {
2
+ readonly TransferWithAuthorization: readonly [{
3
+ readonly name: "from";
4
+ readonly type: "address";
5
+ }, {
6
+ readonly name: "to";
7
+ readonly type: "address";
8
+ }, {
9
+ readonly name: "value";
10
+ readonly type: "uint256";
11
+ }, {
12
+ readonly name: "validAfter";
13
+ readonly type: "uint256";
14
+ }, {
15
+ readonly name: "validBefore";
16
+ readonly type: "uint256";
17
+ }, {
18
+ readonly name: "nonce";
19
+ readonly type: "bytes32";
20
+ }];
21
+ };
22
+ /** One entry of a 402 "accepts" array, v1 or v2 (and AgentWallet's own legacy shape). */
23
+ export interface X402Requirement {
24
+ scheme: string;
25
+ network: string;
26
+ payTo: string;
27
+ asset?: string;
28
+ amount?: string;
29
+ maxAmountRequired?: string;
30
+ maxTimeoutSeconds?: number;
31
+ resource?: string;
32
+ description?: string;
33
+ mimeType?: string;
34
+ outputSchema?: Record<string, unknown>;
35
+ requiredDecimals?: number;
36
+ extra?: {
37
+ name?: string;
38
+ version?: string;
39
+ token?: string;
40
+ [k: string]: unknown;
41
+ };
42
+ [k: string]: unknown;
43
+ }
44
+ export interface X402PaymentRequired {
45
+ x402Version: number;
46
+ error?: string;
47
+ resource?: {
48
+ url: string;
49
+ description?: string;
50
+ mimeType?: string;
51
+ };
52
+ accepts: X402Requirement[];
53
+ extensions?: Record<string, unknown>;
54
+ }
55
+ export interface Eip3009Authorization {
56
+ from: `0x${string}`;
57
+ to: `0x${string}`;
58
+ value: string;
59
+ validAfter: string;
60
+ validBefore: string;
61
+ nonce: `0x${string}`;
62
+ }
63
+ export interface X402Settlement {
64
+ success?: boolean;
65
+ transaction?: string;
66
+ network?: string;
67
+ payer?: string;
68
+ amount?: string;
69
+ errorReason?: string;
70
+ errorMessage?: string;
71
+ [k: string]: unknown;
72
+ }
73
+ /**
74
+ * AgentWallet's own paywalls (hifriendbot.com /x402/access/{id}) predate the
75
+ * spec's field names: no `asset`, token under `extra.token`, `requiredDecimals`
76
+ * present, and they verify a broadcast transfer by receipt. Anything with an
77
+ * `asset` is treated as standard x402.
78
+ */
79
+ export declare function isLegacyAgentWalletAccept(a: X402Requirement): boolean;
80
+ /** Required amount in base units, whichever spelling the server used. */
81
+ export declare function requiredAmount(a: X402Requirement): string;
82
+ export declare function createNonce(): `0x${string}`;
83
+ /** Authorization window mirrors the reference client: valid from 10 minutes ago until maxTimeoutSeconds from now. */
84
+ export declare function buildAuthorization(from: string, req: X402Requirement, nowSeconds?: number, nonce?: `0x${string}`): Eip3009Authorization;
85
+ /** EIP-712 typed data for the authorization. `name`/`version` are the token's own domain values. */
86
+ export declare function typedDataFor(chainId: number, asset: string, name: string, version: string, auth: Eip3009Authorization): {
87
+ domain: {
88
+ name: string;
89
+ version: string;
90
+ chainId: number;
91
+ verifyingContract: `0x${string}`;
92
+ };
93
+ types: {
94
+ readonly TransferWithAuthorization: readonly [{
95
+ readonly name: "from";
96
+ readonly type: "address";
97
+ }, {
98
+ readonly name: "to";
99
+ readonly type: "address";
100
+ }, {
101
+ readonly name: "value";
102
+ readonly type: "uint256";
103
+ }, {
104
+ readonly name: "validAfter";
105
+ readonly type: "uint256";
106
+ }, {
107
+ readonly name: "validBefore";
108
+ readonly type: "uint256";
109
+ }, {
110
+ readonly name: "nonce";
111
+ readonly type: "bytes32";
112
+ }];
113
+ };
114
+ primaryType: "TransferWithAuthorization";
115
+ message: {
116
+ from: `0x${string}`;
117
+ to: `0x${string}`;
118
+ value: bigint;
119
+ validAfter: bigint;
120
+ validBefore: bigint;
121
+ nonce: `0x${string}`;
122
+ };
123
+ };
124
+ /**
125
+ * Token EIP-712 domains we know without asking the chain. USDC's domain name
126
+ * differs by deployment ("USD Coin" on most mainnets, "USDC" on Base Sepolia),
127
+ * which is exactly the kind of thing that silently produces an unrecoverable
128
+ * signature, so only well-known deployments are listed; anything else is read
129
+ * from the contract's name()/version().
130
+ */
131
+ export declare const KNOWN_TOKEN_DOMAINS: Record<string, {
132
+ name: string;
133
+ version: string;
134
+ }>;
135
+ export declare function knownTokenDomain(chainId: number, asset: string): {
136
+ name: string;
137
+ version: string;
138
+ } | null;
139
+ /** Decode a single ABI-encoded string return value (name()/version()). */
140
+ export declare function decodeAbiString(hex: string): string;
141
+ /** Build the payment payload the resource server expects for this x402 version. */
142
+ export declare function buildPaymentPayload(x402Version: number, req: X402Requirement, auth: Eip3009Authorization, signature: string, resource?: X402PaymentRequired['resource'], extensions?: Record<string, unknown>): Record<string, unknown>;
143
+ /** 16-128 chars of [A-Za-z0-9_-], the shape the payment-identifier extension accepts. */
144
+ export declare function generatePaymentId(): string;
145
+ /**
146
+ * x402 v2 "payment-identifier" extension: the server may ask for a client-chosen
147
+ * id so it can de-duplicate retries. The 402's extensions are echoed back with
148
+ * info.id filled in. Other extensions pass through untouched.
149
+ */
150
+ export declare function withPaymentIdentifier(extensions?: Record<string, unknown>, id?: string): Record<string, unknown> | null;
151
+ export declare function paymentHeaderFor(x402Version: number, payload: Record<string, unknown>): {
152
+ name: string;
153
+ value: string;
154
+ };
155
+ /** v2 servers put the requirements in PAYMENT-REQUIRED; v1 servers put them in the JSON body. */
156
+ export declare function parsePaymentRequired(getHeader: (name: string) => string | null | undefined, body: unknown): X402PaymentRequired | null;
157
+ export declare function parseSettlement(getHeader: (name: string) => string | null | undefined): X402Settlement | null;
158
+ /**
159
+ * Choose which accepts[] entry to pay. `exact` is the only scheme this client
160
+ * can sign today; `upto` needs Permit2 (a max authorization the seller settles
161
+ * below) and is refused rather than approximated with a full upfront transfer,
162
+ * which is what issue #6 caught. Returns the reason when nothing is payable.
163
+ */
164
+ export declare function pickOption(accepts: X402Requirement[], resolveChainId: (network: string) => number | null, preferChain?: number): {
165
+ option: X402Requirement | null;
166
+ chainId: number | null;
167
+ reason?: string;
168
+ };
@@ -0,0 +1,199 @@
1
+ /**
2
+ * x402 "exact" scheme on EVM, done the way the spec says: an EIP-3009
3
+ * TransferWithAuthorization signed off-chain and handed to the resource server
4
+ * in the payment header. The facilitator settles it; the payer broadcasts
5
+ * nothing and pays no gas.
6
+ *
7
+ * Before v1.11.0 pay_x402 broadcast a plain ERC-20 transfer() and put the tx
8
+ * hash in the header. That only ever worked against AgentWallet's own paywalls
9
+ * (which verify by receipt); every standard x402 server rejected it (issue #7).
10
+ * The receipt-style flow is kept for those paywalls and detected by their
11
+ * shape, see isLegacyAgentWalletAccept().
12
+ *
13
+ * Wire formats, both supported (issue #7 endpoint speaks v1, newer servers v2):
14
+ * v1: 402 body {x402Version:1, accepts:[{maxAmountRequired, asset, payTo, network:"base", ...}]}
15
+ * request header X-PAYMENT: base64({x402Version:1, scheme, network, payload:{signature, authorization}})
16
+ * response header X-PAYMENT-RESPONSE
17
+ * v2: 402 header PAYMENT-REQUIRED: base64({x402Version:2, resource, accepts:[{amount, asset, payTo, network:"eip155:8453", ...}]})
18
+ * request header PAYMENT-SIGNATURE: base64({x402Version:2, resource, accepted, payload:{signature, authorization}})
19
+ * response header PAYMENT-RESPONSE
20
+ */
21
+ import { randomBytes } from 'node:crypto';
22
+ export const TRANSFER_WITH_AUTHORIZATION_TYPES = {
23
+ TransferWithAuthorization: [
24
+ { name: 'from', type: 'address' },
25
+ { name: 'to', type: 'address' },
26
+ { name: 'value', type: 'uint256' },
27
+ { name: 'validAfter', type: 'uint256' },
28
+ { name: 'validBefore', type: 'uint256' },
29
+ { name: 'nonce', type: 'bytes32' },
30
+ ],
31
+ };
32
+ /**
33
+ * AgentWallet's own paywalls (hifriendbot.com /x402/access/{id}) predate the
34
+ * spec's field names: no `asset`, token under `extra.token`, `requiredDecimals`
35
+ * present, and they verify a broadcast transfer by receipt. Anything with an
36
+ * `asset` is treated as standard x402.
37
+ */
38
+ export function isLegacyAgentWalletAccept(a) {
39
+ if (a.asset)
40
+ return false;
41
+ return Boolean(a.extra?.token) || typeof a.requiredDecimals === 'number';
42
+ }
43
+ /** Required amount in base units, whichever spelling the server used. */
44
+ export function requiredAmount(a) {
45
+ const v = a.amount ?? a.maxAmountRequired ?? '';
46
+ return String(v);
47
+ }
48
+ export function createNonce() {
49
+ return ('0x' + randomBytes(32).toString('hex'));
50
+ }
51
+ const DEFAULT_TIMEOUT_SECONDS = 300;
52
+ /** Authorization window mirrors the reference client: valid from 10 minutes ago until maxTimeoutSeconds from now. */
53
+ export function buildAuthorization(from, req, nowSeconds = Math.floor(Date.now() / 1000), nonce = createNonce()) {
54
+ const value = requiredAmount(req);
55
+ if (!/^\d+$/.test(value))
56
+ throw new Error(`x402: invalid required amount "${value}" (expected integer base units).`);
57
+ if (!/^0x[0-9a-fA-F]{40}$/.test(req.payTo))
58
+ throw new Error(`x402: payTo is not an EVM address: "${req.payTo}".`);
59
+ const timeout = Number.isFinite(req.maxTimeoutSeconds) && req.maxTimeoutSeconds > 0
60
+ ? Math.floor(req.maxTimeoutSeconds) : DEFAULT_TIMEOUT_SECONDS;
61
+ return {
62
+ from: from,
63
+ to: req.payTo,
64
+ value,
65
+ validAfter: String(nowSeconds - 600),
66
+ validBefore: String(nowSeconds + timeout),
67
+ nonce,
68
+ };
69
+ }
70
+ /** EIP-712 typed data for the authorization. `name`/`version` are the token's own domain values. */
71
+ export function typedDataFor(chainId, asset, name, version, auth) {
72
+ return {
73
+ domain: { name, version, chainId, verifyingContract: asset },
74
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
75
+ primaryType: 'TransferWithAuthorization',
76
+ message: {
77
+ from: auth.from,
78
+ to: auth.to,
79
+ value: BigInt(auth.value),
80
+ validAfter: BigInt(auth.validAfter),
81
+ validBefore: BigInt(auth.validBefore),
82
+ nonce: auth.nonce,
83
+ },
84
+ };
85
+ }
86
+ /**
87
+ * Token EIP-712 domains we know without asking the chain. USDC's domain name
88
+ * differs by deployment ("USD Coin" on most mainnets, "USDC" on Base Sepolia),
89
+ * which is exactly the kind of thing that silently produces an unrecoverable
90
+ * signature, so only well-known deployments are listed; anything else is read
91
+ * from the contract's name()/version().
92
+ */
93
+ export const KNOWN_TOKEN_DOMAINS = {
94
+ '8453:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': { name: 'USD Coin', version: '2' }, // USDC Base
95
+ '84532:0x036cbd53842c5426634e7929541ec2318f3dcf7e': { name: 'USDC', version: '2' }, // USDC Base Sepolia
96
+ '1:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': { name: 'USD Coin', version: '2' }, // USDC Ethereum
97
+ '137:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359': { name: 'USD Coin', version: '2' }, // USDC Polygon (native)
98
+ '42161:0xaf88d065e77c8cc2239327c5edb3a432268e5831': { name: 'USD Coin', version: '2' }, // USDC Arbitrum
99
+ '10:0x0b2c639c533813f4aa9d7837caf62653d097ff85': { name: 'USD Coin', version: '2' }, // USDC Optimism
100
+ '43114:0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e': { name: 'USD Coin', version: '2' }, // USDC Avalanche
101
+ };
102
+ export function knownTokenDomain(chainId, asset) {
103
+ return KNOWN_TOKEN_DOMAINS[`${chainId}:${asset.toLowerCase()}`] ?? null;
104
+ }
105
+ /** Decode a single ABI-encoded string return value (name()/version()). */
106
+ export function decodeAbiString(hex) {
107
+ const h = (hex || '').replace(/^0x/, '');
108
+ if (h.length < 128)
109
+ return '';
110
+ const len = parseInt(h.slice(64, 128), 16);
111
+ if (!Number.isFinite(len) || len <= 0 || len > 256)
112
+ return '';
113
+ const bytes = Buffer.from(h.slice(128, 128 + len * 2), 'hex');
114
+ return bytes.toString('utf8');
115
+ }
116
+ /** Build the payment payload the resource server expects for this x402 version. */
117
+ export function buildPaymentPayload(x402Version, req, auth, signature, resource, extensions) {
118
+ const payload = { signature, authorization: auth };
119
+ if (x402Version >= 2) {
120
+ const out = { x402Version, accepted: req, payload };
121
+ if (resource)
122
+ out.resource = resource;
123
+ const ext = withPaymentIdentifier(extensions);
124
+ if (ext)
125
+ out.extensions = ext;
126
+ return out;
127
+ }
128
+ return { x402Version: 1, scheme: req.scheme, network: req.network, payload };
129
+ }
130
+ /** 16-128 chars of [A-Za-z0-9_-], the shape the payment-identifier extension accepts. */
131
+ export function generatePaymentId() {
132
+ return 'agw_' + randomBytes(16).toString('hex');
133
+ }
134
+ /**
135
+ * x402 v2 "payment-identifier" extension: the server may ask for a client-chosen
136
+ * id so it can de-duplicate retries. The 402's extensions are echoed back with
137
+ * info.id filled in. Other extensions pass through untouched.
138
+ */
139
+ export function withPaymentIdentifier(extensions, id) {
140
+ if (!extensions || typeof extensions !== 'object')
141
+ return null;
142
+ const out = { ...extensions };
143
+ const pid = out['payment-identifier'];
144
+ if (pid && typeof pid === 'object' && pid.info && typeof pid.info === 'object') {
145
+ out['payment-identifier'] = { ...pid, info: { ...pid.info, id: pid.info.id || id || generatePaymentId() } };
146
+ }
147
+ return out;
148
+ }
149
+ export function paymentHeaderFor(x402Version, payload) {
150
+ const value = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
151
+ return { name: x402Version >= 2 ? 'PAYMENT-SIGNATURE' : 'X-PAYMENT', value };
152
+ }
153
+ function decodeBase64Json(v) {
154
+ if (!v)
155
+ return null;
156
+ try {
157
+ const parsed = JSON.parse(Buffer.from(v.trim(), 'base64').toString('utf8'));
158
+ return parsed && typeof parsed === 'object' ? parsed : null;
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ }
164
+ /** v2 servers put the requirements in PAYMENT-REQUIRED; v1 servers put them in the JSON body. */
165
+ export function parsePaymentRequired(getHeader, body) {
166
+ const fromHeader = decodeBase64Json(getHeader('PAYMENT-REQUIRED') ?? getHeader('payment-required'));
167
+ if (fromHeader && Array.isArray(fromHeader.accepts))
168
+ return fromHeader;
169
+ if (body && typeof body === 'object' && Array.isArray(body.accepts)) {
170
+ return body;
171
+ }
172
+ return null;
173
+ }
174
+ export function parseSettlement(getHeader) {
175
+ return (decodeBase64Json(getHeader('PAYMENT-RESPONSE') ?? getHeader('payment-response'))
176
+ ?? decodeBase64Json(getHeader('X-PAYMENT-RESPONSE') ?? getHeader('x-payment-response')));
177
+ }
178
+ /**
179
+ * Choose which accepts[] entry to pay. `exact` is the only scheme this client
180
+ * can sign today; `upto` needs Permit2 (a max authorization the seller settles
181
+ * below) and is refused rather than approximated with a full upfront transfer,
182
+ * which is what issue #6 caught. Returns the reason when nothing is payable.
183
+ */
184
+ export function pickOption(accepts, resolveChainId, preferChain) {
185
+ const payable = accepts.filter(a => a.scheme === 'exact' && resolveChainId(a.network) !== null);
186
+ if (payable.length === 0) {
187
+ const schemes = Array.from(new Set(accepts.map(a => a.scheme))).join(', ') || 'none';
188
+ const reason = accepts.some(a => a.scheme === 'upto')
189
+ ? 'This endpoint only offers the x402 "upto" scheme (Permit2 max-authorization settled by usage). ' +
190
+ 'AgentWallet does not sign upto authorizations yet and will not approximate one with an upfront transfer. ' +
191
+ 'Ask the endpoint for an "exact" option, or wait for Permit2 support.'
192
+ : `No payable option: schemes offered were ${schemes}.`;
193
+ return { option: null, chainId: null, reason };
194
+ }
195
+ let option = payable[0];
196
+ if (preferChain)
197
+ option = payable.find(a => resolveChainId(a.network) === preferChain) ?? option;
198
+ return { option, chainId: resolveChainId(option.network) };
199
+ }
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.10.9",
4
+ "version": "1.11.0",
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/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/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",
20
20
  "prepublishOnly": "npm run build"
21
21
  },
22
22
  "keywords": [