agentwallet-mcp 1.0.0 → 1.3.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/LICENSE +21 -0
- package/README.md +164 -0
- package/build/index.js +502 -2
- package/package.json +11 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HiFriendbot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# AgentWallet MCP Server
|
|
2
|
+
|
|
3
|
+
Permissionless EVM wallet infrastructure for AI agents. Create wallets, sign transactions, and broadcast on-chain — on any EVM chain.
|
|
4
|
+
|
|
5
|
+
**No KYC. No KYT. No transaction monitoring. No one can block your wallet.**
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **21 MCP tools** — create wallets, send transactions, approve tokens, wrap ETH, pay x402 invoices, and more
|
|
10
|
+
- **Any EVM chain** — Ethereum, Base, Polygon, BSC, Arbitrum, Optimism, Avalanche, Zora, PulseChain, and any other EVM-compatible chain
|
|
11
|
+
- **x402 payments** — automatically pay for x402-enabled APIs and resources on any supported chain
|
|
12
|
+
- **Secure** — Private keys encrypted at rest, decrypted only during signing, zeroed from memory immediately after
|
|
13
|
+
- **Permissionless** — No identity verification, no compliance gatekeeping. Create a wallet and transact immediately.
|
|
14
|
+
|
|
15
|
+
## Pricing
|
|
16
|
+
|
|
17
|
+
- **$0.0069 per transaction** (31% less expensive than Coinbase CDP)
|
|
18
|
+
- **6,000 free transactions/month**
|
|
19
|
+
- No monthly fee, no tiers — just pay as you go
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
Get your free API key at [hifriendbot.com/wallet](https://hifriendbot.com/wallet) (no credit card required).
|
|
24
|
+
|
|
25
|
+
### Claude Desktop / OpenClaw
|
|
26
|
+
|
|
27
|
+
Add to your config:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"agentwallet": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "agentwallet-mcp"],
|
|
35
|
+
"env": {
|
|
36
|
+
"AGENTWALLET_USER": "your_username",
|
|
37
|
+
"AGENTWALLET_PASS": "your_api_key"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Claude Code
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
claude mcp add agentwallet \
|
|
48
|
+
-e AGENTWALLET_USER=your_username \
|
|
49
|
+
-e AGENTWALLET_PASS=your_api_key \
|
|
50
|
+
-- npx -y agentwallet-mcp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### VS Code
|
|
54
|
+
|
|
55
|
+
Add to your settings:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcp": {
|
|
60
|
+
"servers": {
|
|
61
|
+
"agentwallet": {
|
|
62
|
+
"command": "npx",
|
|
63
|
+
"args": ["-y", "agentwallet-mcp"],
|
|
64
|
+
"env": {
|
|
65
|
+
"AGENTWALLET_USER": "your_username",
|
|
66
|
+
"AGENTWALLET_PASS": "your_api_key"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Tools
|
|
75
|
+
|
|
76
|
+
| Tool | Description |
|
|
77
|
+
|------|-------------|
|
|
78
|
+
| `create_wallet` | Create a new wallet on any EVM chain |
|
|
79
|
+
| `list_wallets` | List all your wallets |
|
|
80
|
+
| `get_wallet` | Get wallet details by ID |
|
|
81
|
+
| `get_balance` | Check native token balance on any chain |
|
|
82
|
+
| `get_token_balance` | Check ERC-20 token balance |
|
|
83
|
+
| `get_token_info` | Get ERC-20 token name, symbol, and decimals |
|
|
84
|
+
| `transfer` | Send native tokens (ETH, POL, BNB, etc.) |
|
|
85
|
+
| `transfer_token` | Send ERC-20 tokens (USDC, USDT, etc.) |
|
|
86
|
+
| `send_transaction` | Sign and broadcast a raw transaction |
|
|
87
|
+
| `sign_transaction` | Sign a transaction without broadcasting |
|
|
88
|
+
| `call_contract` | Read-only contract call (eth_call) |
|
|
89
|
+
| `approve_token` | Approve ERC-20 token spending for DeFi |
|
|
90
|
+
| `get_allowance` | Check ERC-20 token allowance |
|
|
91
|
+
| `wrap_eth` | Wrap native tokens to WETH/WAVAX/etc. |
|
|
92
|
+
| `unwrap_eth` | Unwrap WETH back to native tokens |
|
|
93
|
+
| `pay_x402` | Pay x402 invoices automatically (fetch, pay, retry) |
|
|
94
|
+
| `get_usage` | Check your monthly usage and billing |
|
|
95
|
+
| `get_chains` | List all supported chains |
|
|
96
|
+
| `pause_wallet` | Emergency pause a wallet |
|
|
97
|
+
| `unpause_wallet` | Resume a paused wallet |
|
|
98
|
+
| `delete_wallet` | Delete a wallet |
|
|
99
|
+
|
|
100
|
+
## Supported Chains
|
|
101
|
+
|
|
102
|
+
| Chain | ID | Native Token | Stablecoin |
|
|
103
|
+
|-------|-----|-------------|------------|
|
|
104
|
+
| Ethereum | 1 | ETH | USDC |
|
|
105
|
+
| Base | 8453 | ETH | USDC |
|
|
106
|
+
| Polygon | 137 | POL | USDC |
|
|
107
|
+
| BSC | 56 | BNB | USDT |
|
|
108
|
+
| Arbitrum | 42161 | ETH | USDC |
|
|
109
|
+
| Optimism | 10 | ETH | USDC |
|
|
110
|
+
| Avalanche | 43114 | AVAX | USDC |
|
|
111
|
+
| Zora | 7777777 | ETH | USDC |
|
|
112
|
+
| PulseChain | 369 | PLS | USDC |
|
|
113
|
+
|
|
114
|
+
## Use Case: GuessMarket
|
|
115
|
+
|
|
116
|
+
Pair with [guessmarket-mcp](https://www.npmjs.com/package/guessmarket-mcp) to let your AI agent trade prediction markets:
|
|
117
|
+
|
|
118
|
+
1. Create a wallet on Base
|
|
119
|
+
2. Approve USDC spending
|
|
120
|
+
3. Buy YES/NO shares on prediction markets
|
|
121
|
+
4. Provide liquidity and earn trading fees
|
|
122
|
+
5. Claim winnings
|
|
123
|
+
|
|
124
|
+
All on-chain. All through MCP. No frontend needed.
|
|
125
|
+
|
|
126
|
+
## x402 Payments
|
|
127
|
+
|
|
128
|
+
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:
|
|
129
|
+
|
|
130
|
+
1. Fetches the URL and detects the 402 response
|
|
131
|
+
2. Parses the payment requirements (amount, token, chain)
|
|
132
|
+
3. Executes the on-chain payment from your wallet
|
|
133
|
+
4. Retries the request with proof of payment
|
|
134
|
+
5. Returns the final response
|
|
135
|
+
|
|
136
|
+
**Always set `max_payment` to control spending:**
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
pay_x402(
|
|
140
|
+
url="https://api.example.com/premium-data",
|
|
141
|
+
wallet_id=1,
|
|
142
|
+
max_payment="1.00"
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Supports ERC-20 tokens (USDC, USDT) and native tokens on any chain. Compatible with x402 V1 and V2 (CAIP-2 chain identifiers).
|
|
147
|
+
|
|
148
|
+
## Security
|
|
149
|
+
|
|
150
|
+
- Private keys are generated server-side and encrypted at rest
|
|
151
|
+
- Keys are decrypted only during transaction signing and zeroed from memory immediately after
|
|
152
|
+
- EIP-1559 transactions only with gas safety caps
|
|
153
|
+
- Emergency pause: freeze any wallet or all wallets instantly
|
|
154
|
+
- Bug bounty program: $50–$500 for responsible disclosure ([details](https://hifriendbot.com/wallet))
|
|
155
|
+
|
|
156
|
+
## Links
|
|
157
|
+
|
|
158
|
+
- **Website:** [hifriendbot.com/wallet](https://hifriendbot.com/wallet)
|
|
159
|
+
- **npm:** [agentwallet-mcp](https://www.npmjs.com/package/agentwallet-mcp)
|
|
160
|
+
- **Security:** [security@hifriendbot.com](mailto:security@hifriendbot.com)
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT
|
package/build/index.js
CHANGED
|
@@ -41,16 +41,83 @@ async function api(path, method = 'GET', body) {
|
|
|
41
41
|
function jsonResponse(data) {
|
|
42
42
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
43
43
|
}
|
|
44
|
+
// ─── EVM Helpers ──────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Convert a human-readable amount (e.g. "0.1") to raw units string given decimals.
|
|
47
|
+
* Uses BigInt for precision — no floating-point errors.
|
|
48
|
+
*/
|
|
49
|
+
function parseUnits(amount, decimals) {
|
|
50
|
+
if (!/^\d+(\.\d+)?$/.test(amount)) {
|
|
51
|
+
throw new Error(`Invalid amount "${amount}". Must be a positive number (e.g. "0.1" or "100").`);
|
|
52
|
+
}
|
|
53
|
+
const [whole, frac = ''] = amount.split('.');
|
|
54
|
+
const fracPadded = frac.slice(0, decimals).padEnd(decimals, '0');
|
|
55
|
+
const raw = BigInt(whole + fracPadded);
|
|
56
|
+
return raw.toString();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Format raw units to human-readable string given decimals.
|
|
60
|
+
*/
|
|
61
|
+
function formatUnits(raw, decimals) {
|
|
62
|
+
const padded = raw.padStart(decimals + 1, '0');
|
|
63
|
+
const whole = padded.slice(0, padded.length - decimals) || '0';
|
|
64
|
+
const frac = padded.slice(padded.length - decimals);
|
|
65
|
+
// Trim trailing zeros but keep at least one decimal
|
|
66
|
+
const trimmed = frac.replace(/0+$/, '') || '0';
|
|
67
|
+
return `${whole}.${trimmed}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Pad an address to 32 bytes (64 hex chars) for ABI encoding.
|
|
71
|
+
*/
|
|
72
|
+
function padAddress(address) {
|
|
73
|
+
return address.slice(2).toLowerCase().padStart(64, '0');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Encode a uint256 as 32 bytes hex (64 chars).
|
|
77
|
+
*/
|
|
78
|
+
function encodeUint256(value) {
|
|
79
|
+
const hex = BigInt(value).toString(16);
|
|
80
|
+
return hex.padStart(64, '0');
|
|
81
|
+
}
|
|
82
|
+
// ─── Server ──────────────────────────────────────────────────────
|
|
83
|
+
// ─── x402 Network Mapping ──────────────────────────────────────
|
|
84
|
+
const X402_NETWORKS = {
|
|
85
|
+
'ethereum': 1,
|
|
86
|
+
'base': 8453,
|
|
87
|
+
'base-sepolia': 84532,
|
|
88
|
+
'polygon': 137,
|
|
89
|
+
'arbitrum': 42161,
|
|
90
|
+
'optimism': 10,
|
|
91
|
+
'bsc': 56,
|
|
92
|
+
'avalanche': 43114,
|
|
93
|
+
'zora': 7777777,
|
|
94
|
+
'pulsechain': 369,
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Resolve an x402 network identifier to a chain ID.
|
|
98
|
+
* Supports plain names ("base"), CAIP-2 format ("eip155:8453"), and raw chain IDs ("8453").
|
|
99
|
+
*/
|
|
100
|
+
function resolveChainId(network) {
|
|
101
|
+
if (X402_NETWORKS[network])
|
|
102
|
+
return X402_NETWORKS[network];
|
|
103
|
+
const caipMatch = network.match(/^eip155:(\d+)$/);
|
|
104
|
+
if (caipMatch)
|
|
105
|
+
return parseInt(caipMatch[1], 10);
|
|
106
|
+
const num = parseInt(network, 10);
|
|
107
|
+
if (!isNaN(num) && num > 0)
|
|
108
|
+
return num;
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
44
111
|
// ─── Server ──────────────────────────────────────────────────────
|
|
45
112
|
const server = new McpServer({
|
|
46
113
|
name: 'agentwallet',
|
|
47
|
-
version: '1.
|
|
114
|
+
version: '1.3.0',
|
|
48
115
|
});
|
|
49
116
|
// ─── Tool: create_wallet ─────────────────────────────────────────
|
|
50
117
|
server.tool('create_wallet', 'Create a new EVM wallet. Returns the wallet ID and address. ' +
|
|
51
118
|
'Private key is encrypted server-side and never exposed.', {
|
|
52
119
|
label: z.string().default('').describe('Friendly name for the wallet'),
|
|
53
|
-
chain_id: z.number().int().default(
|
|
120
|
+
chain_id: z.number().int().default(8453).describe('Default chain ID (1=Ethereum, 8453=Base, 42161=Arbitrum, 10=Optimism, 137=Polygon, 43114=Avalanche, 56=BSC, 7777777=Zora, 369=PulseChain)'),
|
|
54
121
|
}, async ({ label, chain_id }) => {
|
|
55
122
|
const data = await api('/wallets', 'POST', { label, chain_id });
|
|
56
123
|
return jsonResponse(data);
|
|
@@ -129,6 +196,439 @@ server.tool('send_transaction', 'Sign and broadcast an EVM transaction. ' +
|
|
|
129
196
|
const result = await api(`/wallets/${wallet_id}/send`, 'POST', body);
|
|
130
197
|
return jsonResponse(result);
|
|
131
198
|
});
|
|
199
|
+
// ─── Tool: transfer ─────────────────────────────────────────────
|
|
200
|
+
server.tool('transfer', 'Send native tokens (ETH, AVAX, BNB, POL, PLS) to an address. ' +
|
|
201
|
+
'Specify the amount in human-readable format (e.g. "0.1" for 0.1 ETH). ' +
|
|
202
|
+
'The amount is converted to wei automatically. Signs and broadcasts the transaction.', {
|
|
203
|
+
wallet_id: z.number().int().describe('Wallet ID to send from'),
|
|
204
|
+
to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Destination address'),
|
|
205
|
+
amount: z.string().describe('Amount to send in human-readable format (e.g. "0.1" for 0.1 ETH)'),
|
|
206
|
+
chain_id: z.number().int().describe('Chain ID (1=Ethereum, 8453=Base, 42161=Arbitrum, 10=Optimism, 137=Polygon, 43114=Avalanche, 56=BSC, 7777777=Zora, 369=PulseChain)'),
|
|
207
|
+
}, async ({ wallet_id, to, amount, chain_id }) => {
|
|
208
|
+
// Convert human-readable to wei (18 decimals for all native tokens)
|
|
209
|
+
const valueWei = parseUnits(amount, 18);
|
|
210
|
+
const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
|
|
211
|
+
to,
|
|
212
|
+
value: valueWei,
|
|
213
|
+
chain_id,
|
|
214
|
+
data: '',
|
|
215
|
+
});
|
|
216
|
+
return jsonResponse({
|
|
217
|
+
...result,
|
|
218
|
+
amount,
|
|
219
|
+
chain_id,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
// ─── Tool: get_token_balance ────────────────────────────────────
|
|
223
|
+
server.tool('get_token_balance', 'Get the ERC-20 token balance for a wallet on a specific chain. ' +
|
|
224
|
+
'Returns the raw balance and human-readable balance. ' +
|
|
225
|
+
'Use get_chains to find stablecoin addresses for each chain.', {
|
|
226
|
+
wallet_id: z.number().int().describe('Wallet ID to check'),
|
|
227
|
+
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
|
|
228
|
+
chain_id: z.number().int().describe('Chain ID to check on'),
|
|
229
|
+
decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
|
|
230
|
+
}, async ({ wallet_id, token, chain_id, decimals }) => {
|
|
231
|
+
const params = `?chain_id=${chain_id}&token=${token}`;
|
|
232
|
+
const data = await api(`/wallets/${wallet_id}/token-balance${params}`);
|
|
233
|
+
const balanceFormatted = formatUnits(data.balance_raw, decimals);
|
|
234
|
+
return jsonResponse({
|
|
235
|
+
...data,
|
|
236
|
+
balance: balanceFormatted,
|
|
237
|
+
decimals,
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// ─── Tool: transfer_token ───────────────────────────────────────
|
|
241
|
+
server.tool('transfer_token', 'Send ERC-20 tokens (USDC, USDT, etc.) to an address. ' +
|
|
242
|
+
'Specify the amount in human-readable format (e.g. "100" for 100 USDC). ' +
|
|
243
|
+
'Signs and broadcasts the transaction. Use get_chains to find stablecoin addresses.', {
|
|
244
|
+
wallet_id: z.number().int().describe('Wallet ID to send from'),
|
|
245
|
+
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
|
|
246
|
+
to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Recipient address'),
|
|
247
|
+
amount: z.string().describe('Amount in human-readable format (e.g. "100" for 100 USDC)'),
|
|
248
|
+
chain_id: z.number().int().describe('Chain ID'),
|
|
249
|
+
decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
|
|
250
|
+
}, async ({ wallet_id, token, to, amount, chain_id, decimals }) => {
|
|
251
|
+
// Encode ERC-20 transfer(address, uint256) calldata
|
|
252
|
+
const rawAmount = parseUnits(amount, decimals);
|
|
253
|
+
const calldata = '0xa9059cbb' + padAddress(to) + encodeUint256(rawAmount);
|
|
254
|
+
const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
|
|
255
|
+
to: token, // Send TX to the token contract
|
|
256
|
+
value: '0', // No native value for token transfers
|
|
257
|
+
data: calldata,
|
|
258
|
+
chain_id,
|
|
259
|
+
});
|
|
260
|
+
return jsonResponse({
|
|
261
|
+
...result,
|
|
262
|
+
token,
|
|
263
|
+
recipient: to,
|
|
264
|
+
amount,
|
|
265
|
+
decimals,
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
// ─── Tool: call_contract ────────────────────────────────────────
|
|
269
|
+
server.tool('call_contract', 'Execute a read-only call against a smart contract (eth_call). ' +
|
|
270
|
+
'Returns the raw hex result. Does not cost gas or modify state. ' +
|
|
271
|
+
'Useful for reading on-chain data like token balances, prices, positions.', {
|
|
272
|
+
chain_id: z.number().int().describe('Chain ID'),
|
|
273
|
+
to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Contract address'),
|
|
274
|
+
data: z.string().describe('ABI-encoded calldata (0x-prefixed hex)'),
|
|
275
|
+
}, async ({ chain_id, to, data }) => {
|
|
276
|
+
const result = await api('/eth-call', 'POST', { chain_id, to, data });
|
|
277
|
+
return jsonResponse(result);
|
|
278
|
+
});
|
|
279
|
+
// ─── Tool: approve_token ────────────────────────────────────────
|
|
280
|
+
server.tool('approve_token', 'Approve a spender contract to transfer ERC-20 tokens on your behalf. ' +
|
|
281
|
+
'Required before interacting with any DeFi protocol (DEXs, lending, etc.). ' +
|
|
282
|
+
'Use amount "max" for unlimited approval, or specify an exact amount.', {
|
|
283
|
+
wallet_id: z.number().int().describe('Wallet ID'),
|
|
284
|
+
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
|
|
285
|
+
spender: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Contract address to approve as spender'),
|
|
286
|
+
amount: z.string().describe('Amount to approve in human-readable format (e.g. "1000"), or "max" for unlimited'),
|
|
287
|
+
chain_id: z.number().int().describe('Chain ID'),
|
|
288
|
+
decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
|
|
289
|
+
}, async ({ wallet_id, token, spender, amount, chain_id, decimals }) => {
|
|
290
|
+
// approve(address spender, uint256 amount) — selector: 0x095ea7b3
|
|
291
|
+
let rawAmount;
|
|
292
|
+
if (amount.toLowerCase() === 'max') {
|
|
293
|
+
rawAmount = (BigInt(2) ** BigInt(256) - BigInt(1)).toString();
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
rawAmount = parseUnits(amount, decimals);
|
|
297
|
+
}
|
|
298
|
+
const calldata = '0x095ea7b3' + padAddress(spender) + encodeUint256(rawAmount);
|
|
299
|
+
const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
|
|
300
|
+
to: token,
|
|
301
|
+
value: '0',
|
|
302
|
+
data: calldata,
|
|
303
|
+
chain_id,
|
|
304
|
+
});
|
|
305
|
+
return jsonResponse({
|
|
306
|
+
...result,
|
|
307
|
+
token,
|
|
308
|
+
spender,
|
|
309
|
+
amount: amount.toLowerCase() === 'max' ? 'unlimited' : amount,
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
// ─── Tool: get_allowance ────────────────────────────────────────
|
|
313
|
+
server.tool('get_allowance', 'Check how many ERC-20 tokens a spender is approved to transfer. ' +
|
|
314
|
+
'Returns the allowance in both raw and human-readable format. ' +
|
|
315
|
+
'Use this to check if an approval is needed before a DeFi transaction.', {
|
|
316
|
+
wallet_id: z.number().int().describe('Wallet ID (used to determine the owner address)'),
|
|
317
|
+
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
|
|
318
|
+
spender: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Spender contract address to check'),
|
|
319
|
+
chain_id: z.number().int().describe('Chain ID'),
|
|
320
|
+
decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
|
|
321
|
+
}, async ({ wallet_id, token, spender, chain_id, decimals }) => {
|
|
322
|
+
// First get the wallet address
|
|
323
|
+
const wallet = await api(`/wallets/${wallet_id}`);
|
|
324
|
+
// allowance(address owner, address spender) — selector: 0xdd62ed3e
|
|
325
|
+
const calldata = '0xdd62ed3e' + padAddress(wallet.address) + padAddress(spender);
|
|
326
|
+
const result = await api('/eth-call', 'POST', { chain_id, to: token, data: calldata });
|
|
327
|
+
// Parse uint256 result
|
|
328
|
+
const rawHex = result.result.replace('0x', '');
|
|
329
|
+
const raw = BigInt('0x' + (rawHex || '0')).toString();
|
|
330
|
+
const maxUint256 = (BigInt(2) ** BigInt(256) - BigInt(1)).toString();
|
|
331
|
+
return jsonResponse({
|
|
332
|
+
token,
|
|
333
|
+
spender,
|
|
334
|
+
allowance_raw: raw,
|
|
335
|
+
allowance: raw === maxUint256 ? 'unlimited' : formatUnits(raw, decimals),
|
|
336
|
+
is_unlimited: raw === maxUint256,
|
|
337
|
+
decimals,
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
// ─── WETH / Wrapped Native Token addresses ──────────────────────
|
|
341
|
+
const WRAPPED_NATIVE = {
|
|
342
|
+
1: { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'WETH' },
|
|
343
|
+
8453: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
|
|
344
|
+
42161: { address: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1', symbol: 'WETH' },
|
|
345
|
+
10: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
|
|
346
|
+
137: { address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', symbol: 'WPOL' },
|
|
347
|
+
56: { address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', symbol: 'WBNB' },
|
|
348
|
+
43114: { address: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7', symbol: 'WAVAX' },
|
|
349
|
+
7777777: { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH' },
|
|
350
|
+
369: { address: '0xA1077a294dDE1B09bB078844df40758a5D0f9a27', symbol: 'WPLS' },
|
|
351
|
+
};
|
|
352
|
+
// ─── Tool: wrap_eth ─────────────────────────────────────────────
|
|
353
|
+
server.tool('wrap_eth', 'Wrap native tokens (ETH, AVAX, BNB, POL, PLS) into their wrapped ERC-20 version (WETH, WAVAX, etc.). ' +
|
|
354
|
+
'Required for most DeFi protocols that use ERC-20 tokens instead of raw native tokens. ' +
|
|
355
|
+
'Specify amount in human-readable format (e.g. "0.5" for 0.5 ETH).', {
|
|
356
|
+
wallet_id: z.number().int().describe('Wallet ID'),
|
|
357
|
+
amount: z.string().describe('Amount to wrap in human-readable format (e.g. "0.5")'),
|
|
358
|
+
chain_id: z.number().int().describe('Chain ID'),
|
|
359
|
+
}, async ({ wallet_id, amount, chain_id }) => {
|
|
360
|
+
const wrapped = WRAPPED_NATIVE[chain_id];
|
|
361
|
+
if (!wrapped) {
|
|
362
|
+
throw new Error(`No wrapped native token configured for chain ${chain_id}`);
|
|
363
|
+
}
|
|
364
|
+
// WETH deposit() payable — selector: 0xd0e30db0
|
|
365
|
+
const valueWei = parseUnits(amount, 18);
|
|
366
|
+
const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
|
|
367
|
+
to: wrapped.address,
|
|
368
|
+
value: valueWei,
|
|
369
|
+
data: '0xd0e30db0',
|
|
370
|
+
chain_id,
|
|
371
|
+
});
|
|
372
|
+
return jsonResponse({
|
|
373
|
+
...result,
|
|
374
|
+
wrapped_token: wrapped.symbol,
|
|
375
|
+
wrapped_address: wrapped.address,
|
|
376
|
+
amount,
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
// ─── Tool: unwrap_eth ───────────────────────────────────────────
|
|
380
|
+
server.tool('unwrap_eth', 'Unwrap wrapped tokens (WETH, WAVAX, WBNB, etc.) back to native tokens. ' +
|
|
381
|
+
'Specify amount in human-readable format (e.g. "0.5" for 0.5 WETH).', {
|
|
382
|
+
wallet_id: z.number().int().describe('Wallet ID'),
|
|
383
|
+
amount: z.string().describe('Amount to unwrap in human-readable format (e.g. "0.5")'),
|
|
384
|
+
chain_id: z.number().int().describe('Chain ID'),
|
|
385
|
+
}, async ({ wallet_id, amount, chain_id }) => {
|
|
386
|
+
const wrapped = WRAPPED_NATIVE[chain_id];
|
|
387
|
+
if (!wrapped) {
|
|
388
|
+
throw new Error(`No wrapped native token configured for chain ${chain_id}`);
|
|
389
|
+
}
|
|
390
|
+
// WETH withdraw(uint256) — selector: 0x2e1a7d4d
|
|
391
|
+
const rawAmount = parseUnits(amount, 18);
|
|
392
|
+
const calldata = '0x2e1a7d4d' + encodeUint256(rawAmount);
|
|
393
|
+
const result = await api(`/wallets/${wallet_id}/send`, 'POST', {
|
|
394
|
+
to: wrapped.address,
|
|
395
|
+
value: '0',
|
|
396
|
+
data: calldata,
|
|
397
|
+
chain_id,
|
|
398
|
+
});
|
|
399
|
+
return jsonResponse({
|
|
400
|
+
...result,
|
|
401
|
+
unwrapped_token: wrapped.symbol,
|
|
402
|
+
amount,
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
// ─── Tool: get_token_info ───────────────────────────────────────
|
|
406
|
+
/**
|
|
407
|
+
* Decode an ABI-encoded string return value from hex.
|
|
408
|
+
* Handles malformed data gracefully — returns empty string on any parsing failure.
|
|
409
|
+
*/
|
|
410
|
+
function decodeAbiString(hex) {
|
|
411
|
+
try {
|
|
412
|
+
const clean = hex.replace('0x', '');
|
|
413
|
+
if (clean.length < 128)
|
|
414
|
+
return ''; // offset + length minimum
|
|
415
|
+
// First 32 bytes = offset, next 32 bytes at that offset = length
|
|
416
|
+
const offset = parseInt(clean.slice(0, 64), 16) * 2;
|
|
417
|
+
if (isNaN(offset) || offset + 64 > clean.length)
|
|
418
|
+
return '';
|
|
419
|
+
const length = parseInt(clean.slice(offset, offset + 64), 16);
|
|
420
|
+
if (isNaN(length) || length === 0)
|
|
421
|
+
return '';
|
|
422
|
+
const dataHex = clean.slice(offset + 64, offset + 64 + length * 2);
|
|
423
|
+
const pairs = dataHex.match(/.{2}/g);
|
|
424
|
+
if (!pairs)
|
|
425
|
+
return '';
|
|
426
|
+
// Convert hex to UTF-8
|
|
427
|
+
const bytes = new Uint8Array(pairs.map(b => parseInt(b, 16)));
|
|
428
|
+
return new TextDecoder().decode(bytes);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return '';
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
server.tool('get_token_info', 'Get the name, symbol, and decimals of any ERC-20 token by its contract address. ' +
|
|
435
|
+
'Useful for discovering token details before transfers or approvals.', {
|
|
436
|
+
token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
|
|
437
|
+
chain_id: z.number().int().describe('Chain ID'),
|
|
438
|
+
}, async ({ token, chain_id }) => {
|
|
439
|
+
// Make 3 parallel eth_call requests: name(), symbol(), decimals()
|
|
440
|
+
const [nameResult, symbolResult, decimalsResult] = await Promise.all([
|
|
441
|
+
api('/eth-call', 'POST', { chain_id, to: token, data: '0x06fdde03' }).catch(() => ({ result: '0x' })),
|
|
442
|
+
api('/eth-call', 'POST', { chain_id, to: token, data: '0x95d89b41' }).catch(() => ({ result: '0x' })),
|
|
443
|
+
api('/eth-call', 'POST', { chain_id, to: token, data: '0x313ce567' }).catch(() => ({ result: '0x' })),
|
|
444
|
+
]);
|
|
445
|
+
const name = decodeAbiString(nameResult.result);
|
|
446
|
+
const symbol = decodeAbiString(symbolResult.result);
|
|
447
|
+
const decimalsHex = decimalsResult.result.replace('0x', '');
|
|
448
|
+
const decimals = decimalsHex ? parseInt(decimalsHex, 16) : 0;
|
|
449
|
+
return jsonResponse({
|
|
450
|
+
token,
|
|
451
|
+
chain_id,
|
|
452
|
+
name: name || 'Unknown',
|
|
453
|
+
symbol: symbol || 'Unknown',
|
|
454
|
+
decimals,
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
// ─── Tool: pay_x402 ─────────────────────────────────────────────
|
|
458
|
+
server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the server returns HTTP 402 Payment Required, ' +
|
|
459
|
+
'parses the payment requirements, executes the on-chain payment, and retries the request with ' +
|
|
460
|
+
'proof of payment. Returns the final response. Supports the x402 open payment standard ' +
|
|
461
|
+
'(https://x402.org). Set max_payment to prevent overspending.', {
|
|
462
|
+
url: z.string().url().describe('The URL to access (will handle 402 payment if required)'),
|
|
463
|
+
wallet_id: z.number().int().describe('Wallet ID to pay from'),
|
|
464
|
+
method: z.string().default('GET').describe('HTTP method (GET, POST, PUT, DELETE)'),
|
|
465
|
+
headers: z.string().optional().describe('Optional JSON string of additional request headers'),
|
|
466
|
+
body: z.string().optional().describe('Optional request body for POST/PUT requests'),
|
|
467
|
+
max_payment: z.string().optional().describe('Maximum payment in human-readable format (e.g. "1.00" for 1 USDC). ' +
|
|
468
|
+
'Rejects payments above this amount. Strongly recommended to prevent overspending.'),
|
|
469
|
+
prefer_chain: z.number().int().optional().describe('Preferred chain ID if the server accepts payment on multiple chains ' +
|
|
470
|
+
'(e.g. 8453 for Base, 1 for Ethereum)'),
|
|
471
|
+
}, async ({ url, wallet_id, method, headers: headersJson, body: reqBody, max_payment, prefer_chain }) => {
|
|
472
|
+
// Build request headers
|
|
473
|
+
const reqHeaders = { Accept: 'application/json' };
|
|
474
|
+
if (headersJson) {
|
|
475
|
+
try {
|
|
476
|
+
Object.assign(reqHeaders, JSON.parse(headersJson));
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
throw new Error('Invalid headers JSON. Must be a JSON object (e.g. {"Authorization": "Bearer ..."}).');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Validate URL — block private IPs and cloud metadata endpoints
|
|
483
|
+
const urlObj = new URL(url);
|
|
484
|
+
const hostname = urlObj.hostname.toLowerCase();
|
|
485
|
+
const blockedHosts = ['localhost', '127.0.0.1', '0.0.0.0', '[::1]', '169.254.169.254', 'metadata.google.internal'];
|
|
486
|
+
const blockedPrefixes = ['10.', '172.16.', '172.17.', '172.18.', '172.19.', '172.20.', '172.21.',
|
|
487
|
+
'172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.', '172.28.', '172.29.',
|
|
488
|
+
'172.30.', '172.31.', '192.168.'];
|
|
489
|
+
if (blockedHosts.includes(hostname) || blockedPrefixes.some(p => hostname.startsWith(p))) {
|
|
490
|
+
throw new Error('URL points to a private/internal address. Only public URLs are allowed.');
|
|
491
|
+
}
|
|
492
|
+
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
|
493
|
+
throw new Error('Only http: and https: URLs are supported.');
|
|
494
|
+
}
|
|
495
|
+
// Step 1: Make initial request
|
|
496
|
+
const reqOptions = {
|
|
497
|
+
method,
|
|
498
|
+
headers: reqHeaders,
|
|
499
|
+
signal: AbortSignal.timeout(30_000), // 30s timeout
|
|
500
|
+
};
|
|
501
|
+
if (reqBody && method !== 'GET') {
|
|
502
|
+
reqOptions.body = reqBody;
|
|
503
|
+
if (!reqHeaders['Content-Type'])
|
|
504
|
+
reqHeaders['Content-Type'] = 'application/json';
|
|
505
|
+
}
|
|
506
|
+
const initialRes = await fetch(url, reqOptions);
|
|
507
|
+
// If not 402, return the response as-is (no payment needed)
|
|
508
|
+
if (initialRes.status !== 402) {
|
|
509
|
+
const text = await initialRes.text();
|
|
510
|
+
let parsed;
|
|
511
|
+
try {
|
|
512
|
+
parsed = JSON.parse(text);
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
parsed = text;
|
|
516
|
+
}
|
|
517
|
+
return jsonResponse({
|
|
518
|
+
status: initialRes.status,
|
|
519
|
+
payment_required: false,
|
|
520
|
+
response: parsed,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
// Step 2: Parse x402 payment requirements from 402 response
|
|
524
|
+
let paymentInfo;
|
|
525
|
+
try {
|
|
526
|
+
paymentInfo = await initialRes.json();
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
throw new Error('402 response body is not valid JSON. This server may not support x402.');
|
|
530
|
+
}
|
|
531
|
+
if (!paymentInfo.accepts || paymentInfo.accepts.length === 0) {
|
|
532
|
+
throw new Error('402 response has no payment options in "accepts" array.');
|
|
533
|
+
}
|
|
534
|
+
const options = paymentInfo.accepts;
|
|
535
|
+
let option;
|
|
536
|
+
if (prefer_chain) {
|
|
537
|
+
option = options.find(a => resolveChainId(a.network) === prefer_chain) || options[0];
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
option = options[0];
|
|
541
|
+
}
|
|
542
|
+
// Resolve the chain
|
|
543
|
+
const chainId = resolveChainId(option.network);
|
|
544
|
+
if (!chainId) {
|
|
545
|
+
throw new Error(`Unsupported x402 network: "${option.network}". ` +
|
|
546
|
+
`Supported: ${Object.keys(X402_NETWORKS).join(', ')}, or any CAIP-2 / numeric chain ID.`);
|
|
547
|
+
}
|
|
548
|
+
// Calculate human-readable amount
|
|
549
|
+
const amount = formatUnits(option.maxAmountRequired, option.requiredDecimals);
|
|
550
|
+
// Step 4: Check spending limit
|
|
551
|
+
if (max_payment) {
|
|
552
|
+
const maxRaw = parseUnits(max_payment, option.requiredDecimals);
|
|
553
|
+
if (BigInt(option.maxAmountRequired) > BigInt(maxRaw)) {
|
|
554
|
+
return jsonResponse({
|
|
555
|
+
status: 402,
|
|
556
|
+
payment_required: true,
|
|
557
|
+
payment_made: false,
|
|
558
|
+
error: `Payment of ${amount} ${option.extra?.name || 'tokens'} exceeds your max_payment limit of ${max_payment}`,
|
|
559
|
+
required_amount: amount,
|
|
560
|
+
max_allowed: max_payment,
|
|
561
|
+
token: option.extra?.name || 'native',
|
|
562
|
+
network: option.network,
|
|
563
|
+
chain_id: chainId,
|
|
564
|
+
pay_to: option.payTo,
|
|
565
|
+
description: option.description,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Step 5: Execute payment
|
|
570
|
+
let txResult;
|
|
571
|
+
if (option.extra?.token) {
|
|
572
|
+
// ERC-20 token payment (e.g. USDC)
|
|
573
|
+
const calldata = '0xa9059cbb' + padAddress(option.payTo) + encodeUint256(option.maxAmountRequired);
|
|
574
|
+
txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
|
|
575
|
+
to: option.extra.token,
|
|
576
|
+
value: '0',
|
|
577
|
+
data: calldata,
|
|
578
|
+
chain_id: chainId,
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
// Native token payment (ETH, etc.)
|
|
583
|
+
txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
|
|
584
|
+
to: option.payTo,
|
|
585
|
+
value: option.maxAmountRequired,
|
|
586
|
+
data: '',
|
|
587
|
+
chain_id: chainId,
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
const txHash = txResult.tx_hash;
|
|
591
|
+
// Step 6: Build x402 payment proof and retry
|
|
592
|
+
const paymentProof = {
|
|
593
|
+
x402Version: paymentInfo.x402Version || 1,
|
|
594
|
+
scheme: option.scheme,
|
|
595
|
+
network: option.network,
|
|
596
|
+
payload: { txHash },
|
|
597
|
+
};
|
|
598
|
+
const paymentHeader = Buffer.from(JSON.stringify(paymentProof)).toString('base64');
|
|
599
|
+
const retryHeaders = { ...reqHeaders, 'X-PAYMENT': paymentHeader };
|
|
600
|
+
const retryOptions = {
|
|
601
|
+
method,
|
|
602
|
+
headers: retryHeaders,
|
|
603
|
+
signal: AbortSignal.timeout(30_000),
|
|
604
|
+
};
|
|
605
|
+
if (reqBody && method !== 'GET') {
|
|
606
|
+
retryOptions.body = reqBody;
|
|
607
|
+
}
|
|
608
|
+
const retryRes = await fetch(url, retryOptions);
|
|
609
|
+
const retryText = await retryRes.text();
|
|
610
|
+
let retryParsed;
|
|
611
|
+
try {
|
|
612
|
+
retryParsed = JSON.parse(retryText);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
retryParsed = retryText;
|
|
616
|
+
}
|
|
617
|
+
return jsonResponse({
|
|
618
|
+
status: retryRes.status,
|
|
619
|
+
payment_required: true,
|
|
620
|
+
payment_made: true,
|
|
621
|
+
amount,
|
|
622
|
+
token: option.extra?.name || 'native',
|
|
623
|
+
token_address: option.extra?.token || null,
|
|
624
|
+
network: option.network,
|
|
625
|
+
chain_id: chainId,
|
|
626
|
+
pay_to: option.payTo,
|
|
627
|
+
tx_hash: txHash,
|
|
628
|
+
description: option.description,
|
|
629
|
+
response: retryParsed,
|
|
630
|
+
});
|
|
631
|
+
});
|
|
132
632
|
// ─── Tool: get_usage ─────────────────────────────────────────────
|
|
133
633
|
server.tool('get_usage', 'Get the current month\'s usage statistics. ' +
|
|
134
634
|
'Returns operations count, tier info, remaining quota, and fees.', {}, async () => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentwallet-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for AgentWallet — lets AI agents create wallets, sign transactions,
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "MCP server for AgentWallet — lets AI agents create wallets, sign transactions, broadcast on any EVM chain, and pay x402 invoices",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -26,10 +26,19 @@
|
|
|
26
26
|
"evm",
|
|
27
27
|
"ethereum",
|
|
28
28
|
"base",
|
|
29
|
+
"arbitrum",
|
|
30
|
+
"optimism",
|
|
29
31
|
"polygon",
|
|
32
|
+
"avalanche",
|
|
30
33
|
"bsc",
|
|
34
|
+
"zora",
|
|
31
35
|
"pulsechain",
|
|
36
|
+
"erc20",
|
|
37
|
+
"token-transfer",
|
|
32
38
|
"transaction-signing",
|
|
39
|
+
"x402",
|
|
40
|
+
"http-402",
|
|
41
|
+
"agent-payments",
|
|
33
42
|
"agentwallet"
|
|
34
43
|
],
|
|
35
44
|
"author": "HiFriendbot (https://hifriendbot.com)",
|