agentwallet-mcp 1.7.4 → 1.8.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 +7 -3
- package/build/index.js +48 -30
- package/build/x402-payment.d.ts +48 -0
- package/build/x402-payment.js +66 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -52,7 +52,9 @@ Add to your config:
|
|
|
52
52
|
}
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
> `AGENTWALLET_WALLET_ID` is optional. Set it to enable x402 auto-pay
|
|
55
|
+
> `AGENTWALLET_WALLET_ID` is optional. Set it to enable x402 auto-pay: when you exceed the free tier without a credit card, the MCP server automatically pays for operations with USDC from this wallet.
|
|
56
|
+
>
|
|
57
|
+
> **Auto-pay safety cap.** `AGENTWALLET_MAX_AUTOPAY` (optional, default `1`) is the maximum amount, in human-readable units of the asset, that x402 auto-pay will authorize for a single payment. Any 402 requirement above this cap is rejected instead of paid, so a malformed or tampered payment requirement cannot drain the wallet. Raise it only if you genuinely need larger automatic payments (for example `"5"` to allow up to 5 USDC per call).
|
|
56
58
|
|
|
57
59
|
### Claude Code
|
|
58
60
|
|
|
@@ -158,7 +160,7 @@ AgentWallet natively supports the [x402 open payment standard](https://x402.org)
|
|
|
158
160
|
4. Retries the request with proof of payment
|
|
159
161
|
5. Returns the final response
|
|
160
162
|
|
|
161
|
-
**
|
|
163
|
+
**Set `max_payment` to control spending:**
|
|
162
164
|
|
|
163
165
|
```
|
|
164
166
|
pay_x402(
|
|
@@ -168,7 +170,9 @@ pay_x402(
|
|
|
168
170
|
)
|
|
169
171
|
```
|
|
170
172
|
|
|
171
|
-
|
|
173
|
+
`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.
|
|
174
|
+
|
|
175
|
+
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`).
|
|
172
176
|
|
|
173
177
|
## x402 Acceptance
|
|
174
178
|
|
package/build/index.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
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
|
+
import { deriveX402Payment, isWithinCap } from './x402-payment.js';
|
|
15
16
|
// ─── Configuration ──────────────────────────────────────────────
|
|
16
17
|
const API_BASE = process.env.AGENTWALLET_API_URL || 'https://hifriendbot.com/wp-json/agentwallet/v1';
|
|
17
18
|
const API_USER = process.env.AGENTWALLET_USER || '';
|
|
@@ -55,15 +56,19 @@ async function handleX402Payment(x402Data, originalPath, originalMethod, origina
|
|
|
55
56
|
throw new Error('402 Payment Required but no payment options available.');
|
|
56
57
|
}
|
|
57
58
|
const accept = accepts[0]; // Use first option
|
|
58
|
-
const tokenAddress = accept.extra?.token || '';
|
|
59
|
-
const amount = accept.maxAmountRequired;
|
|
60
59
|
const payTo = accept.payTo;
|
|
61
|
-
const decimals = accept.requiredDecimals || 6;
|
|
62
60
|
// Determine chain_id from network string (CAIP-2, plain name, or raw ID)
|
|
63
61
|
const network = accept.network || '';
|
|
64
62
|
const chainId = resolveChainId(network) ?? 8453;
|
|
65
|
-
//
|
|
66
|
-
|
|
63
|
+
// x402 maxAmountRequired is ALREADY in base/atomic units (e.g. "10000" =
|
|
64
|
+
// 0.01 USDC at 6 decimals) and must be used directly. The previous code ran
|
|
65
|
+
// it through parseUnits(), which re-scaled it by 10**decimals and overpaid by
|
|
66
|
+
// that factor (1,000,000x for USDC). deriveX402Payment uses it directly (like
|
|
67
|
+
// the pay_x402 tool) and enforces a hard ceiling so a malformed or malicious
|
|
68
|
+
// 402 response cannot drain the wallet. AGENTWALLET_MAX_AUTOPAY is in
|
|
69
|
+
// human-readable units of the asset; default 1.
|
|
70
|
+
const maxAutopay = process.env.AGENTWALLET_MAX_AUTOPAY || '1';
|
|
71
|
+
const { tokenAddress, rawAmount, decimals } = deriveX402Payment(accept, maxAutopay);
|
|
67
72
|
// Send payment using our wallet (with X-AGW-SKIP-X402 to prevent recursion)
|
|
68
73
|
let txHash;
|
|
69
74
|
try {
|
|
@@ -216,7 +221,7 @@ function resolveChainId(network) {
|
|
|
216
221
|
// ─── Server ──────────────────────────────────────────────────────
|
|
217
222
|
const server = new McpServer({
|
|
218
223
|
name: 'agentwallet',
|
|
219
|
-
version: '1.
|
|
224
|
+
version: '1.8.0',
|
|
220
225
|
}, {
|
|
221
226
|
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.
|
|
222
227
|
|
|
@@ -739,7 +744,10 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
739
744
|
headers: z.string().optional().describe('Optional JSON string of additional request headers'),
|
|
740
745
|
body: z.string().optional().describe('Optional request body for POST/PUT requests'),
|
|
741
746
|
max_payment: z.string().optional().describe('Maximum payment in human-readable format (e.g. "1.00" for 1 USDC). ' +
|
|
742
|
-
'Rejects payments above this amount.
|
|
747
|
+
'Rejects payments above this amount. If omitted, a default cap of ' +
|
|
748
|
+
'AGENTWALLET_MAX_AUTOPAY (default "1") is applied, so an untrusted 402 ' +
|
|
749
|
+
'endpoint can never authorize an unbounded payment. Set this explicitly ' +
|
|
750
|
+
'to allow a larger single payment.'),
|
|
743
751
|
prefer_chain: z.number().int().optional().describe('Preferred chain ID if the server accepts payment on multiple chains ' +
|
|
744
752
|
'(e.g. 8453 for Base, 1 for Ethereum)'),
|
|
745
753
|
}, async ({ url, wallet_id, method, headers: headersJson, body: reqBody, max_payment, prefer_chain }) => {
|
|
@@ -821,35 +829,45 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
821
829
|
}
|
|
822
830
|
// Calculate human-readable amount
|
|
823
831
|
const amount = formatUnits(option.maxAmountRequired, option.requiredDecimals);
|
|
824
|
-
// Step 4:
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
832
|
+
// Step 4: Enforce a hard per-payment cap. ALWAYS applied — if the caller
|
|
833
|
+
// omits max_payment we fall back to AGENTWALLET_MAX_AUTOPAY (default "1"),
|
|
834
|
+
// mirroring the internal auto-pay path (deriveX402Payment). Without this, an
|
|
835
|
+
// untrusted or compromised x402 endpoint could return an arbitrarily large
|
|
836
|
+
// maxAmountRequired and drain the wallet. The cap is never silently skipped.
|
|
837
|
+
const capSource = max_payment ? 'max_payment' : 'AGENTWALLET_MAX_AUTOPAY';
|
|
838
|
+
const effectiveMax = max_payment || process.env.AGENTWALLET_MAX_AUTOPAY || '1';
|
|
839
|
+
if (!isWithinCap(option.maxAmountRequired, option.requiredDecimals, effectiveMax)) {
|
|
840
|
+
return jsonResponse({
|
|
841
|
+
status: 402,
|
|
842
|
+
payment_required: true,
|
|
843
|
+
payment_made: false,
|
|
844
|
+
error: `Payment of ${amount} ${option.extra?.name || 'tokens'} exceeds the ` +
|
|
845
|
+
`${effectiveMax} cap (from ${capSource}).` +
|
|
846
|
+
(max_payment ? '' : ' No max_payment was provided, so the default cap was applied. ' +
|
|
847
|
+
'Pass max_payment (or raise AGENTWALLET_MAX_AUTOPAY) to allow a larger payment.'),
|
|
848
|
+
required_amount: amount,
|
|
849
|
+
max_allowed: effectiveMax,
|
|
850
|
+
cap_source: capSource,
|
|
851
|
+
token: option.extra?.name || 'native',
|
|
852
|
+
network: option.network,
|
|
853
|
+
chain_id: chainId,
|
|
854
|
+
pay_to: option.payTo,
|
|
855
|
+
description: option.description,
|
|
856
|
+
});
|
|
842
857
|
}
|
|
858
|
+
// Standard x402 puts the token address in `asset`; AgentWallet's own server
|
|
859
|
+
// currently emits it under extra.token. Prefer the standard field, fall back.
|
|
860
|
+
const tokenAddress = option.asset || option.extra?.token || '';
|
|
843
861
|
// Step 5: Execute payment
|
|
844
862
|
let txResult;
|
|
845
863
|
if (isSolanaChain(chainId)) {
|
|
846
864
|
// Solana payment
|
|
847
|
-
if (
|
|
865
|
+
if (tokenAddress) {
|
|
848
866
|
// SPL token payment (e.g. USDC on Solana)
|
|
849
867
|
txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
|
|
850
868
|
to: option.payTo,
|
|
851
869
|
value: option.maxAmountRequired,
|
|
852
|
-
token_mint:
|
|
870
|
+
token_mint: tokenAddress,
|
|
853
871
|
token_decimals: option.requiredDecimals,
|
|
854
872
|
chain_id: chainId,
|
|
855
873
|
}));
|
|
@@ -863,11 +881,11 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
863
881
|
}));
|
|
864
882
|
}
|
|
865
883
|
}
|
|
866
|
-
else if (
|
|
884
|
+
else if (tokenAddress) {
|
|
867
885
|
// EVM ERC-20 token payment (e.g. USDC)
|
|
868
886
|
const calldata = '0xa9059cbb' + padAddress(option.payTo) + encodeUint256(option.maxAmountRequired);
|
|
869
887
|
txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
|
|
870
|
-
to:
|
|
888
|
+
to: tokenAddress,
|
|
871
889
|
value: '0',
|
|
872
890
|
data: calldata,
|
|
873
891
|
chain_id: chainId,
|
|
@@ -916,7 +934,7 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
|
|
|
916
934
|
payment_made: true,
|
|
917
935
|
amount,
|
|
918
936
|
token: option.extra?.name || 'native',
|
|
919
|
-
token_address: option.extra?.token || null,
|
|
937
|
+
token_address: option.asset || option.extra?.token || null,
|
|
920
938
|
network: option.network,
|
|
921
939
|
chain_id: chainId,
|
|
922
940
|
pay_to: option.payTo,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for x402 auto-pay amount/token derivation.
|
|
3
|
+
*
|
|
4
|
+
* Isolated from the server bootstrap in index.ts so the funds-sensitive amount
|
|
5
|
+
* math can be unit-tested without starting the MCP server.
|
|
6
|
+
*/
|
|
7
|
+
export interface X402AcceptLike {
|
|
8
|
+
maxAmountRequired: string;
|
|
9
|
+
requiredDecimals?: number;
|
|
10
|
+
asset?: string;
|
|
11
|
+
extra?: {
|
|
12
|
+
token?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Derive the on-chain transfer parameters from an x402 payment requirement.
|
|
17
|
+
*
|
|
18
|
+
* CRITICAL: x402 `maxAmountRequired` is ALREADY denominated in base/atomic
|
|
19
|
+
* units (e.g. "10000" = 0.01 USDC at 6 decimals). It MUST be used directly.
|
|
20
|
+
* Running it through parseUnits() re-scales it by 10**decimals and overpays by
|
|
21
|
+
* that factor (1,000,000x for USDC). This mirrors the pay_x402 tool, which has
|
|
22
|
+
* always used maxAmountRequired directly.
|
|
23
|
+
*
|
|
24
|
+
* Also enforces a hard ceiling so a malformed or malicious 402 response can
|
|
25
|
+
* never authorize a transfer larger than `maxAutopayHuman` units of the asset.
|
|
26
|
+
*
|
|
27
|
+
* @param accept the chosen x402 accept option
|
|
28
|
+
* @param maxAutopayHuman max auto-pay, in human-readable units of the asset
|
|
29
|
+
*/
|
|
30
|
+
export declare function deriveX402Payment(accept: X402AcceptLike, maxAutopayHuman: string): {
|
|
31
|
+
tokenAddress: string;
|
|
32
|
+
rawAmount: string;
|
|
33
|
+
decimals: number;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Returns true if an x402 required amount (already in base/atomic units) is
|
|
37
|
+
* within a human-readable per-payment cap.
|
|
38
|
+
*
|
|
39
|
+
* Shared by both x402 payment paths so neither can authorize an unbounded
|
|
40
|
+
* transfer: the internal auto-pay path and the public pay_x402 MCP tool both
|
|
41
|
+
* gate on this. pay_x402 uses it (instead of deriveX402Payment's throw) so it
|
|
42
|
+
* can return a structured rejection to the calling agent.
|
|
43
|
+
*
|
|
44
|
+
* @param rawAmountRequired x402 maxAmountRequired, already in base units
|
|
45
|
+
* @param decimals token decimals
|
|
46
|
+
* @param capHuman cap in human-readable units of the asset (e.g. "1")
|
|
47
|
+
*/
|
|
48
|
+
export declare function isWithinCap(rawAmountRequired: string, decimals: number, capHuman: string): boolean;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for x402 auto-pay amount/token derivation.
|
|
3
|
+
*
|
|
4
|
+
* Isolated from the server bootstrap in index.ts so the funds-sensitive amount
|
|
5
|
+
* math can be unit-tested without starting the MCP server.
|
|
6
|
+
*/
|
|
7
|
+
/** Convert a human-readable decimal amount (e.g. "0.01") to base/atomic units. */
|
|
8
|
+
function toBaseUnits(amount, decimals) {
|
|
9
|
+
if (!/^\d+(\.\d+)?$/.test(amount)) {
|
|
10
|
+
throw new Error(`Invalid amount "${amount}".`);
|
|
11
|
+
}
|
|
12
|
+
const [whole, frac = ''] = amount.split('.');
|
|
13
|
+
const fracPadded = frac.slice(0, decimals).padEnd(decimals, '0');
|
|
14
|
+
return BigInt(whole + fracPadded).toString();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Derive the on-chain transfer parameters from an x402 payment requirement.
|
|
18
|
+
*
|
|
19
|
+
* CRITICAL: x402 `maxAmountRequired` is ALREADY denominated in base/atomic
|
|
20
|
+
* units (e.g. "10000" = 0.01 USDC at 6 decimals). It MUST be used directly.
|
|
21
|
+
* Running it through parseUnits() re-scales it by 10**decimals and overpays by
|
|
22
|
+
* that factor (1,000,000x for USDC). This mirrors the pay_x402 tool, which has
|
|
23
|
+
* always used maxAmountRequired directly.
|
|
24
|
+
*
|
|
25
|
+
* Also enforces a hard ceiling so a malformed or malicious 402 response can
|
|
26
|
+
* never authorize a transfer larger than `maxAutopayHuman` units of the asset.
|
|
27
|
+
*
|
|
28
|
+
* @param accept the chosen x402 accept option
|
|
29
|
+
* @param maxAutopayHuman max auto-pay, in human-readable units of the asset
|
|
30
|
+
*/
|
|
31
|
+
export function deriveX402Payment(accept, maxAutopayHuman) {
|
|
32
|
+
const decimals = accept.requiredDecimals || 6;
|
|
33
|
+
// Standard x402 uses `asset` for the token address; AgentWallet's own server
|
|
34
|
+
// currently emits it under extra.token. Prefer the standard field, fall back.
|
|
35
|
+
const tokenAddress = accept.asset || accept.extra?.token || '';
|
|
36
|
+
const rawAmount = accept.maxAmountRequired;
|
|
37
|
+
if (!/^\d+$/.test(rawAmount)) {
|
|
38
|
+
throw new Error(`x402 auto-pay: invalid maxAmountRequired "${rawAmount}" (expected an integer base-unit amount).`);
|
|
39
|
+
}
|
|
40
|
+
const capRaw = toBaseUnits(maxAutopayHuman, decimals);
|
|
41
|
+
if (BigInt(rawAmount) > BigInt(capRaw)) {
|
|
42
|
+
throw new Error(`x402 auto-pay blocked: required amount (${rawAmount} base units) exceeds the ` +
|
|
43
|
+
`AGENTWALLET_MAX_AUTOPAY cap of ${maxAutopayHuman}. Raise AGENTWALLET_MAX_AUTOPAY to allow it.`);
|
|
44
|
+
}
|
|
45
|
+
return { tokenAddress, rawAmount, decimals };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns true if an x402 required amount (already in base/atomic units) is
|
|
49
|
+
* within a human-readable per-payment cap.
|
|
50
|
+
*
|
|
51
|
+
* Shared by both x402 payment paths so neither can authorize an unbounded
|
|
52
|
+
* transfer: the internal auto-pay path and the public pay_x402 MCP tool both
|
|
53
|
+
* gate on this. pay_x402 uses it (instead of deriveX402Payment's throw) so it
|
|
54
|
+
* can return a structured rejection to the calling agent.
|
|
55
|
+
*
|
|
56
|
+
* @param rawAmountRequired x402 maxAmountRequired, already in base units
|
|
57
|
+
* @param decimals token decimals
|
|
58
|
+
* @param capHuman cap in human-readable units of the asset (e.g. "1")
|
|
59
|
+
*/
|
|
60
|
+
export function isWithinCap(rawAmountRequired, decimals, capHuman) {
|
|
61
|
+
if (!/^\d+$/.test(rawAmountRequired)) {
|
|
62
|
+
throw new Error(`Invalid maxAmountRequired "${rawAmountRequired}" (expected an integer base-unit amount).`);
|
|
63
|
+
}
|
|
64
|
+
const capRaw = toBaseUnits(capHuman, decimals);
|
|
65
|
+
return BigInt(rawAmountRequired) <= BigInt(capRaw);
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentwallet-mcp",
|
|
3
3
|
"mcpName": "io.github.hifriendbot/agentwallet",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "MCP server for AgentWallet
|
|
4
|
+
"version": "1.8.0",
|
|
5
|
+
"description": "MCP server for AgentWallet: lets AI agents create wallets, sign transactions, broadcast on EVM + Solana, pay x402 invoices, and accept x402 payments",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "build/index.js",
|
|
8
8
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc",
|
|
18
18
|
"start": "node build/index.js",
|
|
19
|
+
"test": "node test/x402-payment.test.mjs",
|
|
19
20
|
"prepublishOnly": "npm run build"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
],
|
|
47
48
|
"author": "HiFriendbot (https://hifriendbot.com)",
|
|
48
49
|
"license": "MIT",
|
|
49
|
-
"homepage": "https://hifriendbot.com/
|
|
50
|
+
"homepage": "https://hifriendbot.com/wallet/",
|
|
50
51
|
"engines": {
|
|
51
52
|
"node": ">=18.0.0"
|
|
52
53
|
},
|