agentwallet-mcp 1.5.0 → 1.7.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.
Files changed (3) hide show
  1. package/README.md +16 -12
  2. package/build/index.js +217 -69
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AgentWallet MCP Server
2
2
 
3
- Permissionless EVM wallet infrastructure for AI agents. Create wallets, sign transactions, and broadcast on-chain — on any EVM chain.
3
+ Permissionless wallet infrastructure for AI agents. Create wallets, sign transactions, and broadcast on-chain — on any EVM chain and Solana. Up to 50% less expensive than Coinbase.
4
4
 
5
5
  **No KYC. No KYT. No transaction monitoring. No one can block your wallet.**
6
6
 
@@ -11,15 +11,16 @@ Permissionless EVM wallet infrastructure for AI agents. Create wallets, sign tra
11
11
  ## Features
12
12
 
13
13
  - **28 MCP tools** — create wallets, send transactions, approve tokens, wrap ETH, pay and accept x402 payments, and more
14
- - **Any EVM chain** — Ethereum, Base, Polygon, BSC, Arbitrum, Optimism, Avalanche, Zora, PulseChain, and any other EVM-compatible chain
15
- - **x402 payments** — pay for x402-enabled APIs automatically, or accept x402 payments on your own endpoints
14
+ - **EVM + Solana** — Ethereum, Base, Polygon, BSC, Arbitrum, Optimism, Avalanche, Zora, PulseChain, Solana, and any other EVM-compatible chain
15
+ - **SOL + SPL tokens** — native SOL transfers and SPL token transfers (USDC, USDT, etc.) with automatic account creation
16
+ - **x402 payments** — pay for x402-enabled APIs automatically, or accept x402 payments on your own endpoints (EVM and Solana)
16
17
  - **Secure** — Private keys encrypted at rest, decrypted only during signing, zeroed from memory immediately after
17
18
  - **Permissionless** — No identity verification, no compliance gatekeeping. Create a wallet and transact immediately.
18
19
 
19
20
  ## Pricing
20
21
 
21
- - **$0.0069 per transaction** (31% less expensive than Coinbase CDP)
22
- - **6,000 free transactions/month**
22
+ - **$0.00345 per operation** (31% less expensive than Coinbase CDP)
23
+ - **6,000 free operations/month**
23
24
  - **$0.0005 per x402 verification** (50% less expensive than Coinbase)
24
25
  - **1,000 free x402 verifications/month**
25
26
  - No monthly fee, no tiers — just pay as you go
@@ -81,14 +82,14 @@ Add to your settings:
81
82
 
82
83
  | Tool | Description |
83
84
  |------|-------------|
84
- | `create_wallet` | Create a new wallet on any EVM chain |
85
+ | `create_wallet` | Create a new EVM or Solana wallet |
85
86
  | `list_wallets` | List all your wallets |
86
87
  | `get_wallet` | Get wallet details by ID |
87
88
  | `get_balance` | Check native token balance on any chain |
88
- | `get_token_balance` | Check ERC-20 token balance |
89
+ | `get_token_balance` | Check ERC-20 or SPL token balance |
89
90
  | `get_token_info` | Get ERC-20 token name, symbol, and decimals |
90
- | `transfer` | Send native tokens (ETH, POL, BNB, etc.) |
91
- | `transfer_token` | Send ERC-20 tokens (USDC, USDT, etc.) |
91
+ | `transfer` | Send native tokens (ETH, SOL, POL, BNB, etc.) |
92
+ | `transfer_token` | Send ERC-20 or SPL tokens (USDC, USDT, etc.) |
92
93
  | `send_transaction` | Sign and broadcast a raw transaction |
93
94
  | `sign_transaction` | Sign a transaction without broadcasting |
94
95
  | `call_contract` | Read-only contract call (eth_call) |
@@ -123,6 +124,8 @@ Add to your settings:
123
124
  | Avalanche | 43114 | AVAX | USDC |
124
125
  | Zora | 7777777 | ETH | USDC |
125
126
  | PulseChain | 369 | PLS | USDC |
127
+ | Solana | 900 | SOL | USDC |
128
+ | Solana Devnet | 901 | SOL | USDC |
126
129
 
127
130
  ## Use Case: GuessMarket
128
131
 
@@ -156,7 +159,7 @@ pay_x402(
156
159
  )
157
160
  ```
158
161
 
159
- Supports ERC-20 tokens (USDC, USDT) and native tokens on any chain. Compatible with x402 V1 and V2 (CAIP-2 chain identifiers).
162
+ Supports ERC-20 tokens, SPL tokens, and native tokens on EVM and Solana. Compatible with x402 V1 and V2 (CAIP-2 chain identifiers).
160
163
 
161
164
  ## x402 Acceptance
162
165
 
@@ -180,13 +183,14 @@ When an agent hits the paywall URL:
180
183
  3. Retries with proof of payment
181
184
  4. Receives the protected content
182
185
 
183
- On-chain verification ensures every payment is real. Replay protection prevents double-spending. Revenue tracking shows you who paid, how much, and when.
186
+ On-chain verification ensures every payment is real. Replay protection prevents double-spending. Revenue tracking shows you who paid, how much, and when. 1,000 free verifications/month, then $0.0005 each — 50% less expensive than Coinbase.
184
187
 
185
188
  ## Security
186
189
 
187
190
  - Private keys are generated server-side and encrypted at rest
188
191
  - Keys are decrypted only during transaction signing and zeroed from memory immediately after
189
- - EIP-1559 transactions only with gas safety caps
192
+ - EIP-1559 transactions on EVM with gas safety caps
193
+ - Ed25519 signing on Solana with finalized commitment level
190
194
  - Bug bounty program: $50–$500 for responsible disclosure ([details](https://hifriendbot.com/wallet))
191
195
 
192
196
  ## Links
package/build/index.js CHANGED
@@ -41,6 +41,23 @@ 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
+ // ─── Solana Helpers ───────────────────────────────────────────────
45
+ const SOLANA_CHAIN_IDS = new Set([900, 901, 902]);
46
+ function isSolanaChain(chainId) {
47
+ return SOLANA_CHAIN_IDS.has(chainId);
48
+ }
49
+ /**
50
+ * Validate an address — accepts both EVM (0x...) and Solana (Base58).
51
+ */
52
+ function isValidAddress(address) {
53
+ // EVM: 0x + 40 hex chars
54
+ if (/^0x[a-fA-F0-9]{40}$/.test(address))
55
+ return true;
56
+ // Solana: 32-44 chars, Base58 alphabet (no 0, O, I, l)
57
+ if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address))
58
+ return true;
59
+ return false;
60
+ }
44
61
  // ─── EVM Helpers ──────────────────────────────────────────────────
45
62
  /**
46
63
  * Convert a human-readable amount (e.g. "0.1") to raw units string given decimals.
@@ -92,17 +109,33 @@ const X402_NETWORKS = {
92
109
  'avalanche': 43114,
93
110
  'zora': 7777777,
94
111
  'pulsechain': 369,
112
+ 'solana': 900,
113
+ 'solana-devnet': 901,
114
+ };
115
+ // Solana genesis hash prefixes for CAIP-2
116
+ const SOLANA_GENESIS = {
117
+ 900: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // mainnet-beta
118
+ 901: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1', // devnet
95
119
  };
96
120
  /**
97
121
  * Resolve an x402 network identifier to a chain ID.
98
122
  * Supports plain names ("base"), CAIP-2 format ("eip155:8453"), and raw chain IDs ("8453").
99
123
  */
100
124
  function resolveChainId(network) {
101
- if (X402_NETWORKS[network])
102
- return X402_NETWORKS[network];
125
+ const lower = network.toLowerCase();
126
+ if (X402_NETWORKS[lower])
127
+ return X402_NETWORKS[lower];
103
128
  const caipMatch = network.match(/^eip155:(\d+)$/);
104
129
  if (caipMatch)
105
130
  return parseInt(caipMatch[1], 10);
131
+ // Solana CAIP-2: solana:<genesis_prefix>
132
+ const solanaMatch = network.match(/^solana:(.+)$/);
133
+ if (solanaMatch) {
134
+ for (const [chainId, genesis] of Object.entries(SOLANA_GENESIS)) {
135
+ if (solanaMatch[1] === genesis)
136
+ return parseInt(chainId, 10);
137
+ }
138
+ }
106
139
  const num = parseInt(network, 10);
107
140
  if (!isNaN(num) && num > 0)
108
141
  return num;
@@ -111,13 +144,13 @@ function resolveChainId(network) {
111
144
  // ─── Server ──────────────────────────────────────────────────────
112
145
  const server = new McpServer({
113
146
  name: 'agentwallet',
114
- version: '1.5.0',
147
+ version: '1.7.0',
115
148
  });
116
149
  // ─── Tool: create_wallet ─────────────────────────────────────────
117
- server.tool('create_wallet', 'Create a new EVM wallet. Returns the wallet ID and address. ' +
150
+ server.tool('create_wallet', 'Create a new EVM or Solana wallet. Returns the wallet ID and address. ' +
118
151
  'Private key is encrypted server-side and never exposed.', {
119
152
  label: z.string().default('').describe('Friendly name for the wallet'),
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)'),
153
+ 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, 900=Solana, 901=Solana Devnet)'),
121
154
  }, async ({ label, chain_id }) => {
122
155
  const data = await api('/wallets', 'POST', { label, chain_id });
123
156
  return jsonResponse(data);
@@ -138,7 +171,7 @@ server.tool('get_wallet', 'Get details for a specific wallet by ID. ' +
138
171
  });
139
172
  // ─── Tool: get_balance ───────────────────────────────────────────
140
173
  server.tool('get_balance', 'Get the native token balance for a wallet on a specific chain. ' +
141
- 'Returns balance in both wei and human-readable format.', {
174
+ 'Returns balance in both wei (or lamports for Solana) and human-readable format.', {
142
175
  wallet_id: z.number().int().describe('Wallet ID'),
143
176
  chain_id: z.number().int().optional().describe('Chain ID to check (defaults to wallet\'s default chain)'),
144
177
  }, async ({ wallet_id, chain_id }) => {
@@ -147,72 +180,114 @@ server.tool('get_balance', 'Get the native token balance for a wallet on a speci
147
180
  return jsonResponse(data);
148
181
  });
149
182
  // ─── Tool: sign_transaction ──────────────────────────────────────
150
- server.tool('sign_transaction', 'Sign an EVM transaction with a wallet\'s private key. ' +
151
- 'Returns the signed raw transaction hex (ready for manual broadcast). ' +
183
+ server.tool('sign_transaction', 'Sign a transaction with a wallet\'s private key. ' +
184
+ 'For EVM: returns signed raw transaction hex. For Solana: returns base64 signed transaction. ' +
152
185
  'Does NOT broadcast — use send_transaction for sign + broadcast.', {
153
186
  wallet_id: z.number().int().describe('Wallet ID'),
154
- to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Destination address'),
187
+ to: z.string().describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
155
188
  chain_id: z.number().int().optional().describe('Chain ID (defaults to wallet\'s default)'),
156
- value: z.string().default('0').describe('Value in wei (decimal string)'),
157
- data: z.string().default('').describe('Hex-encoded calldata (0x-prefixed) for contract calls'),
158
- gas_limit: z.string().optional().describe('Gas limit (auto-estimated if omitted)'),
159
- max_fee: z.string().optional().describe('Max fee per gas in wei (auto if omitted)'),
160
- priority_fee: z.string().optional().describe('Max priority fee per gas in wei (auto if omitted)'),
161
- }, async ({ wallet_id, to, chain_id, value, data, gas_limit, max_fee, priority_fee }) => {
162
- const body = { to, value, data };
189
+ value: z.string().default('0').describe('Value in wei/lamports (decimal string)'),
190
+ data: z.string().default('').describe('Hex-encoded calldata (0x-prefixed) for EVM contract calls'),
191
+ gas_limit: z.string().optional().describe('Gas limit — EVM only (auto-estimated if omitted)'),
192
+ max_fee: z.string().optional().describe('Max fee per gas in wei — EVM only (auto if omitted)'),
193
+ priority_fee: z.string().optional().describe('Max priority fee per gas in wei — EVM only (auto if omitted)'),
194
+ token_mint: z.string().optional().describe('SPL token mint address — Solana only (for SPL token transfers)'),
195
+ token_decimals: z.number().int().optional().describe('SPL token decimals — Solana only (6 for USDC)'),
196
+ }, async ({ wallet_id, to, chain_id, value, data, gas_limit, max_fee, priority_fee, token_mint, token_decimals }) => {
197
+ // Validate address format
198
+ if (!isValidAddress(to)) {
199
+ throw new Error(`Invalid address "${to}". Use 0x-prefixed hex for EVM or Base58 for Solana.`);
200
+ }
201
+ const body = { to, value };
163
202
  if (chain_id)
164
203
  body.chain_id = chain_id;
165
- if (gas_limit)
166
- body.gas_limit = gas_limit;
167
- if (max_fee)
168
- body.max_fee = max_fee;
169
- if (priority_fee)
170
- body.priority_fee = priority_fee;
204
+ if (isSolanaChain(chain_id ?? 0)) {
205
+ // Solana-specific params
206
+ if (token_mint)
207
+ body.token_mint = token_mint;
208
+ if (token_decimals !== undefined)
209
+ body.token_decimals = token_decimals;
210
+ }
211
+ else {
212
+ // EVM-specific params
213
+ body.data = data;
214
+ if (gas_limit)
215
+ body.gas_limit = gas_limit;
216
+ if (max_fee)
217
+ body.max_fee = max_fee;
218
+ if (priority_fee)
219
+ body.priority_fee = priority_fee;
220
+ }
171
221
  const result = await api(`/wallets/${wallet_id}/sign`, 'POST', body);
172
222
  return jsonResponse(result);
173
223
  });
174
224
  // ─── Tool: send_transaction ──────────────────────────────────────
175
- server.tool('send_transaction', 'Sign and broadcast an EVM transaction. ' +
176
- 'Returns the transaction hash on success. ' +
225
+ server.tool('send_transaction', 'Sign and broadcast a transaction. ' +
226
+ 'Returns the transaction hash (EVM) or signature (Solana) on success. ' +
177
227
  'The transaction is signed server-side and broadcast via RPC.', {
178
228
  wallet_id: z.number().int().describe('Wallet ID'),
179
- to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Destination address'),
229
+ to: z.string().describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
180
230
  chain_id: z.number().int().optional().describe('Chain ID (defaults to wallet\'s default)'),
181
- value: z.string().default('0').describe('Value in wei (decimal string)'),
182
- data: z.string().default('').describe('Hex-encoded calldata (0x-prefixed) for contract calls'),
183
- gas_limit: z.string().optional().describe('Gas limit (auto-estimated if omitted)'),
184
- max_fee: z.string().optional().describe('Max fee per gas in wei (auto if omitted)'),
185
- priority_fee: z.string().optional().describe('Max priority fee per gas in wei (auto if omitted)'),
186
- }, async ({ wallet_id, to, chain_id, value, data, gas_limit, max_fee, priority_fee }) => {
187
- const body = { to, value, data };
231
+ value: z.string().default('0').describe('Value in wei/lamports (decimal string)'),
232
+ data: z.string().default('').describe('Hex-encoded calldata (0x-prefixed) for EVM contract calls'),
233
+ gas_limit: z.string().optional().describe('Gas limit — EVM only (auto-estimated if omitted)'),
234
+ max_fee: z.string().optional().describe('Max fee per gas in wei — EVM only (auto if omitted)'),
235
+ priority_fee: z.string().optional().describe('Max priority fee per gas in wei — EVM only (auto if omitted)'),
236
+ token_mint: z.string().optional().describe('SPL token mint address — Solana only (for SPL token transfers)'),
237
+ token_decimals: z.number().int().optional().describe('SPL token decimals — Solana only (6 for USDC)'),
238
+ }, async ({ wallet_id, to, chain_id, value, data, gas_limit, max_fee, priority_fee, token_mint, token_decimals }) => {
239
+ // Validate address format
240
+ if (!isValidAddress(to)) {
241
+ throw new Error(`Invalid address "${to}". Use 0x-prefixed hex for EVM or Base58 for Solana.`);
242
+ }
243
+ const body = { to, value };
188
244
  if (chain_id)
189
245
  body.chain_id = chain_id;
190
- if (gas_limit)
191
- body.gas_limit = gas_limit;
192
- if (max_fee)
193
- body.max_fee = max_fee;
194
- if (priority_fee)
195
- body.priority_fee = priority_fee;
246
+ if (isSolanaChain(chain_id ?? 0)) {
247
+ // Solana-specific params
248
+ if (token_mint)
249
+ body.token_mint = token_mint;
250
+ if (token_decimals !== undefined)
251
+ body.token_decimals = token_decimals;
252
+ }
253
+ else {
254
+ // EVM-specific params
255
+ body.data = data;
256
+ if (gas_limit)
257
+ body.gas_limit = gas_limit;
258
+ if (max_fee)
259
+ body.max_fee = max_fee;
260
+ if (priority_fee)
261
+ body.priority_fee = priority_fee;
262
+ }
196
263
  const result = await api(`/wallets/${wallet_id}/send`, 'POST', body);
197
264
  return jsonResponse(result);
198
265
  });
199
266
  // ─── Tool: transfer ─────────────────────────────────────────────
200
- server.tool('transfer', 'Send native tokens (ETH, AVAX, BNB, POL, PLS) to an address. ' +
267
+ server.tool('transfer', 'Send native tokens (ETH, AVAX, BNB, POL, PLS, SOL) to an address. ' +
201
268
  '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.', {
269
+ 'The amount is converted to wei/lamports automatically. Signs and broadcasts the transaction.', {
203
270
  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'),
271
+ to: z.string().describe('Destination address (0x-prefixed for EVM, Base58 for Solana)'),
205
272
  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)'),
273
+ 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, 900=Solana, 901=Solana Devnet)'),
207
274
  }, 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', {
275
+ // Validate address format
276
+ if (!isValidAddress(to)) {
277
+ throw new Error(`Invalid address "${to}". Use 0x-prefixed hex for EVM or Base58 for Solana.`);
278
+ }
279
+ // SOL uses 9 decimals (lamports), EVM native tokens use 18 decimals (wei)
280
+ const decimals = isSolanaChain(chain_id) ? 9 : 18;
281
+ const valueRaw = parseUnits(amount, decimals);
282
+ const body = {
211
283
  to,
212
- value: valueWei,
284
+ value: valueRaw,
213
285
  chain_id,
214
- data: '',
215
- });
286
+ };
287
+ if (!isSolanaChain(chain_id)) {
288
+ body.data = '';
289
+ }
290
+ const result = await api(`/wallets/${wallet_id}/send`, 'POST', body);
216
291
  return jsonResponse({
217
292
  ...result,
218
293
  amount,
@@ -220,17 +295,30 @@ server.tool('transfer', 'Send native tokens (ETH, AVAX, BNB, POL, PLS) to an add
220
295
  });
221
296
  });
222
297
  // ─── Tool: get_token_balance ────────────────────────────────────
223
- server.tool('get_token_balance', 'Get the ERC-20 token balance for a wallet on a specific chain. ' +
298
+ server.tool('get_token_balance', 'Get the ERC-20 or SPL token balance for a wallet on a specific chain. ' +
224
299
  'Returns the raw balance and human-readable balance. ' +
225
300
  'Use get_chains to find stablecoin addresses for each chain.', {
226
301
  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'),
302
+ token: z.string().describe('Token address (0x-prefixed ERC-20 contract for EVM, Base58 mint for Solana)'),
228
303
  chain_id: z.number().int().describe('Chain ID to check on'),
229
304
  decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
230
305
  }, async ({ wallet_id, token, chain_id, decimals }) => {
306
+ // Validate token address format
307
+ if (!isValidAddress(token)) {
308
+ throw new Error(`Invalid token address "${token}". Use 0x-prefixed hex for EVM or Base58 for Solana.`);
309
+ }
231
310
  const params = `?chain_id=${chain_id}&token=${token}`;
232
311
  const data = await api(`/wallets/${wallet_id}/token-balance${params}`);
233
- const balanceFormatted = formatUnits(data.balance_raw, decimals);
312
+ // Solana API returns balance_formatted + decimals directly
313
+ if (isSolanaChain(chain_id) && data.balance_formatted !== undefined) {
314
+ return jsonResponse({
315
+ ...data,
316
+ balance: data.balance_formatted,
317
+ decimals: data.decimals ?? decimals,
318
+ });
319
+ }
320
+ // EVM: format from raw
321
+ const balanceFormatted = formatUnits(data.balance_raw || '0', decimals);
234
322
  return jsonResponse({
235
323
  ...data,
236
324
  balance: balanceFormatted,
@@ -238,25 +326,45 @@ server.tool('get_token_balance', 'Get the ERC-20 token balance for a wallet on a
238
326
  });
239
327
  });
240
328
  // ─── Tool: transfer_token ───────────────────────────────────────
241
- server.tool('transfer_token', 'Send ERC-20 tokens (USDC, USDT, etc.) to an address. ' +
329
+ server.tool('transfer_token', 'Send ERC-20 tokens (EVM) or SPL tokens (Solana) to an address. ' +
242
330
  'Specify the amount in human-readable format (e.g. "100" for 100 USDC). ' +
243
331
  'Signs and broadcasts the transaction. Use get_chains to find stablecoin addresses.', {
244
332
  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'),
333
+ token: z.string().describe('Token address (0x-prefixed ERC-20 contract for EVM, Base58 mint for Solana)'),
334
+ to: z.string().describe('Recipient address (0x-prefixed for EVM, Base58 for Solana)'),
247
335
  amount: z.string().describe('Amount in human-readable format (e.g. "100" for 100 USDC)'),
248
336
  chain_id: z.number().int().describe('Chain ID'),
249
337
  decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
250
338
  }, async ({ wallet_id, token, to, amount, chain_id, decimals }) => {
251
- // Encode ERC-20 transfer(address, uint256) calldata
339
+ // Validate addresses
340
+ if (!isValidAddress(token)) {
341
+ throw new Error(`Invalid token address "${token}". Use 0x-prefixed hex for EVM or Base58 for Solana.`);
342
+ }
343
+ if (!isValidAddress(to)) {
344
+ throw new Error(`Invalid recipient address "${to}". Use 0x-prefixed hex for EVM or Base58 for Solana.`);
345
+ }
252
346
  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
- });
347
+ let result;
348
+ if (isSolanaChain(chain_id)) {
349
+ // Solana SPL transfer — the server handles ATA derivation + instruction building
350
+ result = await api(`/wallets/${wallet_id}/send`, 'POST', {
351
+ to,
352
+ value: rawAmount,
353
+ token_mint: token,
354
+ token_decimals: decimals,
355
+ chain_id,
356
+ });
357
+ }
358
+ else {
359
+ // EVM ERC-20 transfer(address, uint256) calldata
360
+ const calldata = '0xa9059cbb' + padAddress(to) + encodeUint256(rawAmount);
361
+ result = await api(`/wallets/${wallet_id}/send`, 'POST', {
362
+ to: token, // Send TX to the token contract
363
+ value: '0', // No native value for token transfers
364
+ data: calldata,
365
+ chain_id,
366
+ });
367
+ }
260
368
  return jsonResponse({
261
369
  ...result,
262
370
  token,
@@ -273,6 +381,9 @@ server.tool('call_contract', 'Execute a read-only call against a smart contract
273
381
  to: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('Contract address'),
274
382
  data: z.string().describe('ABI-encoded calldata (0x-prefixed hex)'),
275
383
  }, async ({ chain_id, to, data }) => {
384
+ if (isSolanaChain(chain_id)) {
385
+ throw new Error('call_contract is not supported on Solana. Use Solana-specific RPC methods instead.');
386
+ }
276
387
  const result = await api('/eth-call', 'POST', { chain_id, to, data });
277
388
  return jsonResponse(result);
278
389
  });
@@ -287,6 +398,9 @@ server.tool('approve_token', 'Approve a spender contract to transfer ERC-20 toke
287
398
  chain_id: z.number().int().describe('Chain ID'),
288
399
  decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
289
400
  }, async ({ wallet_id, token, spender, amount, chain_id, decimals }) => {
401
+ if (isSolanaChain(chain_id)) {
402
+ throw new Error('approve_token is not supported on Solana. Solana SPL tokens do not use ERC-20 style approvals.');
403
+ }
290
404
  // approve(address spender, uint256 amount) — selector: 0x095ea7b3
291
405
  let rawAmount;
292
406
  if (amount.toLowerCase() === 'max') {
@@ -319,6 +433,9 @@ server.tool('get_allowance', 'Check how many ERC-20 tokens a spender is approved
319
433
  chain_id: z.number().int().describe('Chain ID'),
320
434
  decimals: z.number().int().default(18).describe('Token decimals (6 for USDC, 18 for most tokens)'),
321
435
  }, async ({ wallet_id, token, spender, chain_id, decimals }) => {
436
+ if (isSolanaChain(chain_id)) {
437
+ throw new Error('get_allowance is not supported on Solana. Solana SPL tokens do not use ERC-20 style allowances.');
438
+ }
322
439
  // First get the wallet address
323
440
  const wallet = await api(`/wallets/${wallet_id}`);
324
441
  // allowance(address owner, address spender) — selector: 0xdd62ed3e
@@ -357,6 +474,9 @@ server.tool('wrap_eth', 'Wrap native tokens (ETH, AVAX, BNB, POL, PLS) into thei
357
474
  amount: z.string().describe('Amount to wrap in human-readable format (e.g. "0.5")'),
358
475
  chain_id: z.number().int().describe('Chain ID'),
359
476
  }, async ({ wallet_id, amount, chain_id }) => {
477
+ if (isSolanaChain(chain_id)) {
478
+ throw new Error('wrap_eth is not supported on Solana. Solana does not use wrapped native tokens like WETH.');
479
+ }
360
480
  const wrapped = WRAPPED_NATIVE[chain_id];
361
481
  if (!wrapped) {
362
482
  throw new Error(`No wrapped native token configured for chain ${chain_id}`);
@@ -383,6 +503,9 @@ server.tool('unwrap_eth', 'Unwrap wrapped tokens (WETH, WAVAX, WBNB, etc.) back
383
503
  amount: z.string().describe('Amount to unwrap in human-readable format (e.g. "0.5")'),
384
504
  chain_id: z.number().int().describe('Chain ID'),
385
505
  }, async ({ wallet_id, amount, chain_id }) => {
506
+ if (isSolanaChain(chain_id)) {
507
+ throw new Error('unwrap_eth is not supported on Solana. Solana does not use wrapped native tokens like WETH.');
508
+ }
386
509
  const wrapped = WRAPPED_NATIVE[chain_id];
387
510
  if (!wrapped) {
388
511
  throw new Error(`No wrapped native token configured for chain ${chain_id}`);
@@ -436,6 +559,9 @@ server.tool('get_token_info', 'Get the name, symbol, and decimals of any ERC-20
436
559
  token: z.string().regex(/^0x[a-fA-F0-9]{40}$/).describe('ERC-20 token contract address'),
437
560
  chain_id: z.number().int().describe('Chain ID'),
438
561
  }, async ({ token, chain_id }) => {
562
+ if (isSolanaChain(chain_id)) {
563
+ throw new Error('get_token_info is not supported on Solana. Use Solana token metadata programs to query SPL token details.');
564
+ }
439
565
  // Make 3 parallel eth_call requests: name(), symbol(), decimals()
440
566
  const [nameResult, symbolResult, decimalsResult] = await Promise.all([
441
567
  api('/eth-call', 'POST', { chain_id, to: token, data: '0x06fdde03' }).catch(() => ({ result: '0x' })),
@@ -568,8 +694,29 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
568
694
  }
569
695
  // Step 5: Execute payment
570
696
  let txResult;
571
- if (option.extra?.token) {
572
- // ERC-20 token payment (e.g. USDC)
697
+ if (isSolanaChain(chainId)) {
698
+ // Solana payment
699
+ if (option.extra?.token) {
700
+ // SPL token payment (e.g. USDC on Solana)
701
+ txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
702
+ to: option.payTo,
703
+ value: option.maxAmountRequired,
704
+ token_mint: option.extra.token,
705
+ token_decimals: option.requiredDecimals,
706
+ chain_id: chainId,
707
+ }));
708
+ }
709
+ else {
710
+ // Native SOL payment
711
+ txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
712
+ to: option.payTo,
713
+ value: option.maxAmountRequired,
714
+ chain_id: chainId,
715
+ }));
716
+ }
717
+ }
718
+ else if (option.extra?.token) {
719
+ // EVM ERC-20 token payment (e.g. USDC)
573
720
  const calldata = '0xa9059cbb' + padAddress(option.payTo) + encodeUint256(option.maxAmountRequired);
574
721
  txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
575
722
  to: option.extra.token,
@@ -579,7 +726,7 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
579
726
  }));
580
727
  }
581
728
  else {
582
- // Native token payment (ETH, etc.)
729
+ // EVM native token payment (ETH, etc.)
583
730
  txResult = (await api(`/wallets/${wallet_id}/send`, 'POST', {
584
731
  to: option.payTo,
585
732
  value: option.maxAmountRequired,
@@ -587,7 +734,8 @@ server.tool('pay_x402', 'Handle an x402 payment flow. Fetches a URL, and if the
587
734
  chain_id: chainId,
588
735
  }));
589
736
  }
590
- const txHash = txResult.tx_hash;
737
+ // Solana returns 'signature', EVM returns 'tx_hash'
738
+ const txHash = (txResult.tx_hash || txResult.signature);
591
739
  // Step 6: Build x402 payment proof and retry
592
740
  const paymentProof = {
593
741
  x402Version: paymentInfo.x402Version || 1,
@@ -650,7 +798,7 @@ server.tool('unpause_wallet', 'Resume a paused wallet so transactions can be sig
650
798
  return jsonResponse(data);
651
799
  });
652
800
  // ─── Tool: get_chains ────────────────────────────────────────────
653
- server.tool('get_chains', 'List all supported EVM chains with their chain IDs, native tokens, ' +
801
+ server.tool('get_chains', 'List all supported chains (EVM + Solana) with their chain IDs, native tokens, ' +
654
802
  'stablecoins, and RPC configuration status.', {}, async () => {
655
803
  const data = await api('/chains');
656
804
  return jsonResponse(data);
@@ -671,8 +819,8 @@ server.tool('create_paywall', 'Create an x402 paywall that charges agents/client
671
819
  name: z.string().describe('Human-readable paywall name (e.g. "Premium API Access")'),
672
820
  description: z.string().default('').describe('Description shown in the 402 response'),
673
821
  amount: z.string().describe('Price in human-readable format (e.g. "0.01" for 0.01 USDC)'),
674
- token_type: z.enum(['erc20', 'native']).default('erc20').describe('"erc20" for stablecoin payments, "native" for ETH/POL/etc.'),
675
- token_address: z.string().default('').describe('ERC-20 token contract address (required if token_type is "erc20"). Use get_chains to find stablecoin addresses.'),
822
+ token_type: z.enum(['erc20', 'spl', 'native']).default('erc20').describe('"erc20" for EVM stablecoins, "spl" for Solana SPL tokens, "native" for ETH/SOL/POL/etc.'),
823
+ token_address: z.string().default('').describe('Token contract address (ERC-20 for EVM, SPL mint Base58 for Solana). Required if token_type is "erc20" or "spl". Use get_chains to find stablecoin addresses.'),
676
824
  token_decimals: z.number().int().default(6).describe('Token decimals (6 for USDC, 18 for ETH/most tokens)'),
677
825
  token_name: z.string().default('USDC').describe('Token display name (e.g. "USDC", "ETH")'),
678
826
  chain_id: z.number().int().default(8453).describe('Chain ID for payments (8453=Base, 1=Ethereum, etc.)'),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentwallet-mcp",
3
- "version": "1.5.0",
4
- "description": "MCP server for AgentWallet — lets AI agents create wallets, sign transactions, broadcast on any EVM chain, pay x402 invoices, and accept x402 payments",
3
+ "version": "1.7.0",
4
+ "description": "MCP server for AgentWallet — lets AI agents create wallets, sign transactions, broadcast on EVM + Solana, pay x402 invoices, and accept x402 payments",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
7
7
  "bin": {
@@ -33,6 +33,8 @@
33
33
  "bsc",
34
34
  "zora",
35
35
  "pulsechain",
36
+ "solana",
37
+ "spl-token",
36
38
  "erc20",
37
39
  "token-transfer",
38
40
  "transaction-signing",