helius-mcp 0.5.3 → 1.2.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 (67) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +1 -1
  3. package/README.md +97 -21
  4. package/dist/http.d.ts +1 -0
  5. package/dist/http.js +2 -0
  6. package/dist/index.js +93 -2
  7. package/dist/scripts/validate-catalog.d.ts +13 -0
  8. package/dist/scripts/validate-catalog.js +76 -0
  9. package/dist/tools/accounts.js +114 -204
  10. package/dist/tools/assets.js +109 -123
  11. package/dist/tools/auth.d.ts +2 -0
  12. package/dist/tools/auth.js +459 -0
  13. package/dist/tools/balance.js +28 -32
  14. package/dist/tools/blocks.js +68 -87
  15. package/dist/tools/config.js +18 -79
  16. package/dist/tools/das-extras.js +56 -41
  17. package/dist/tools/docs.js +12 -54
  18. package/dist/tools/enhanced-websockets.js +104 -74
  19. package/dist/tools/fees.js +42 -61
  20. package/dist/tools/guides.js +126 -515
  21. package/dist/tools/index.js +50 -2
  22. package/dist/tools/laserstream.js +107 -53
  23. package/dist/tools/network.js +47 -69
  24. package/dist/tools/plans.d.ts +21 -0
  25. package/dist/tools/plans.js +105 -246
  26. package/dist/tools/product-catalog.d.ts +10 -0
  27. package/dist/tools/product-catalog.js +123 -0
  28. package/dist/tools/recommend.d.ts +4 -0
  29. package/dist/tools/recommend.js +233 -0
  30. package/dist/tools/shared.js +8 -3
  31. package/dist/tools/solana-knowledge.d.ts +2 -0
  32. package/dist/tools/solana-knowledge.js +544 -0
  33. package/dist/tools/tokens.js +17 -18
  34. package/dist/tools/transactions.js +232 -302
  35. package/dist/tools/transfers.d.ts +2 -0
  36. package/dist/tools/transfers.js +270 -0
  37. package/dist/tools/wallet.js +175 -177
  38. package/dist/tools/webhooks.js +80 -82
  39. package/dist/types/transaction-types.d.ts +1 -1
  40. package/dist/types/transaction-types.js +2 -1
  41. package/dist/utils/config.d.ts +27 -0
  42. package/dist/utils/config.js +76 -0
  43. package/dist/utils/docs.d.ts +24 -0
  44. package/dist/utils/docs.js +72 -0
  45. package/dist/utils/errors.d.ts +32 -0
  46. package/dist/utils/errors.js +157 -0
  47. package/dist/utils/feedback.d.ts +16 -0
  48. package/dist/utils/feedback.js +87 -0
  49. package/dist/utils/formatters.d.ts +0 -1
  50. package/dist/utils/formatters.js +0 -3
  51. package/dist/utils/helius.d.ts +15 -5
  52. package/dist/utils/helius.js +52 -45
  53. package/dist/version.d.ts +1 -0
  54. package/dist/version.js +1 -0
  55. package/package.json +17 -7
  56. package/system-prompts/helius/claude.system.md +170 -0
  57. package/system-prompts/helius/full.md +2868 -0
  58. package/system-prompts/helius/openai.developer.md +170 -0
  59. package/system-prompts/helius-dflow/claude.system.md +290 -0
  60. package/system-prompts/helius-dflow/full.md +3647 -0
  61. package/system-prompts/helius-dflow/openai.developer.md +290 -0
  62. package/system-prompts/helius-phantom/claude.system.md +348 -0
  63. package/system-prompts/helius-phantom/full.md +5472 -0
  64. package/system-prompts/helius-phantom/openai.developer.md +348 -0
  65. package/system-prompts/svm/claude.system.md +174 -0
  66. package/system-prompts/svm/full.md +699 -0
  67. package/system-prompts/svm/openai.developer.md +174 -0
@@ -0,0 +1,459 @@
1
+ import { z } from 'zod';
2
+ import { generateKeypair } from 'helius-sdk/auth/generateKeypair';
3
+ import { loadKeypair } from 'helius-sdk/auth/loadKeypair';
4
+ import { getAddress } from 'helius-sdk/auth/getAddress';
5
+ import { checkSolBalance, checkUsdcBalance } from 'helius-sdk/auth/checkBalances';
6
+ import { agenticSignup } from 'helius-sdk/auth/agenticSignup';
7
+ import { getCheckoutPreview, executeCheckout, executeRenewal } from 'helius-sdk/auth/checkout';
8
+ import { listProjects } from 'helius-sdk/auth/listProjects';
9
+ import { getProject } from 'helius-sdk/auth/getProject';
10
+ import { PLAN_CATALOG } from 'helius-sdk/auth/planCatalog';
11
+ import { MCP_USER_AGENT } from '../http.js';
12
+ import { setApiKey, hasApiKey, setSessionSecretKey, setSessionWalletAddress, getSessionWalletAddress, loadSignerOrFail, } from '../utils/helius.js';
13
+ import { mcpText, mcpError, handleToolError } from '../utils/errors.js';
14
+ import { fetchDoc, extractSections } from '../utils/docs.js';
15
+ import { sendFeedbackEvent, captureWalletAddress } from '../utils/feedback.js';
16
+ import { setSharedApiKey, setJwt, getJwt, SHARED_CONFIG_PATH, KEYPAIR_PATH, loadKeypairFromDisk, saveKeypairToDisk, keypairExistsOnDisk } from '../utils/config.js';
17
+ import { HELIUS_PLANS } from './plans.js';
18
+ const PAID_PLAN_ORDER = ['developer', 'business', 'professional'];
19
+ export function registerAuthTools(server) {
20
+ // ── Getting Started Guide ──
21
+ server.tool('getStarted', 'Get setup instructions for Helius. Checks whether an API key is configured (not validated), whether a keypair exists on disk, and whether a JWT session is present, then tells you exactly what to do next. Call this when a user asks "how do I get started?" or needs onboarding help.', {}, async () => {
22
+ const lines = ['# Getting Started with Helius'];
23
+ const apiKeyConfigured = hasApiKey();
24
+ const hasKeypair = keypairExistsOnDisk();
25
+ const jwt = getJwt();
26
+ // ── Already fully set up ──
27
+ if (apiKeyConfigured && jwt) {
28
+ lines.push('', 'You\'re all set! Your API key and account session are configured.', '', '**What you can do:**', '- Query NFTs and tokens: `getAssetsByOwner`, `searchAssets`', '- Check balances: `getBalance`, `getTokenAccounts`', '- Parse transactions: `parseTransactions`', '- Manage webhooks: `createWebhook`, `getAllWebhooks`', '- Check your account: `getAccountStatus`', '', 'Just ask a question in plain English and the right tool will be used automatically.', '', '**IMPORTANT — if the user described a project they want to build, call `recommendStack` now** with their project description. It returns architecture recommendations with Helius products, MCP tools, credit costs, and reference files tailored to their plan.');
29
+ return mcpText(lines.join('\n'));
30
+ }
31
+ // ── API key set but no JWT (e.g., set via env or setHeliusApiKey) ──
32
+ if (apiKeyConfigured) {
33
+ lines.push('', 'Your API key is configured — all Helius tools are ready to use.', '', '**What you can do:**', '- Query NFTs and tokens: `getAssetsByOwner`, `searchAssets`', '- Check balances: `getBalance`, `getTokenAccounts`', '- Parse transactions: `parseTransactions`', '- Manage webhooks: `createWebhook`, `getAllWebhooks`', '', 'Just ask a question in plain English and the right tool will be used automatically.', '', '**IMPORTANT — if the user described a project they want to build, call `recommendStack` now** with their project description. It returns architecture recommendations with Helius products, MCP tools, credit costs, and reference files.', '', '**Optional:** To see your plan, credits, and rate limits, call `agenticSignup` — it will detect your existing account (no payment needed) and enable `getAccountStatus`.');
34
+ return mcpText(lines.join('\n'));
35
+ }
36
+ // ── No API key — need to set up ──
37
+ lines.push('', 'You need a Helius API key to use these tools. Choose one of these paths:', '', '---', '', '## Path A — I already have an API key', '', 'If you have a key from https://dashboard.helius.dev:', '1. Call the `setHeliusApiKey` tool with your key', '2. Done — all tools are immediately available', '', '---', '', '## Path B — Create a new account', '', 'The signup is fully autonomous — no browser needed. It takes ~2 minutes:', '');
38
+ // Adapt steps based on whether a keypair already exists
39
+ if (hasKeypair) {
40
+ lines.push('### Step 1: Keypair ✓', `You already have a keypair saved at \`${KEYPAIR_PATH}\`.`, 'Call `generateKeypair` to load it and see the wallet address.', '');
41
+ }
42
+ else {
43
+ lines.push('### Step 1: Generate a keypair', 'Call the `generateKeypair` tool. It creates a Solana wallet and returns the address.', '');
44
+ }
45
+ lines.push('### Step 2: Fund the wallet', 'Send the following to the wallet address from Step 1:', '- **~0.001 SOL** — covers transaction fees', '- **1 USDC** — pays for the basic plan ($1)', '', 'You can send from any Solana wallet, exchange, or on-ramp.', 'The USDC token mint on Solana is: `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`', '', `For paid plans, send more USDC instead: ${PAID_PLAN_ORDER.filter(k => k in PLAN_CATALOG).map(k => `$${PLAN_CATALOG[k].monthlyPrice / 100} (${PLAN_CATALOG[k].name})`).join(', ')}.`, '', '### Step 3: Verify funding', 'Call `checkSignupBalance` to confirm your SOL and USDC balances are sufficient.', '', '### Step 4: Create the account', 'Call `agenticSignup` to process the payment and create your Helius account.', 'Your API key will be configured automatically — no extra steps needed.', '', '> **Paid plans only:** `agenticSignup` requires `email`, `firstName`, and `lastName` for developer/business/professional plans. Basic plan ($1) does not require them.', '', '---', '', '## Path C — Use the Helius CLI', '', 'Same flow from the terminal:', '```', 'npx helius-cli@latest keygen # Generate keypair', '# Fund the wallet address shown above', 'npx helius-cli@latest signup # Verify balance + create account', '```', '', '---', '', '**After setup:** Use `recommendStack` to plan your project — describe what you\'re building and get architecture recommendations at different cost and complexity levels.');
46
+ return mcpText(lines.join('\n'));
47
+ });
48
+ // ── Keypair Generation ──
49
+ server.tool('generateKeypair', 'Generate a new Solana keypair for Helius account signup. Returns the wallet address. The user must fund this wallet with ~0.001 SOL + 1 USDC (basic plan) or more USDC (for paid plans) before calling agenticSignup.', {}, async () => {
50
+ try {
51
+ // Check disk first — reuse existing keypair if available
52
+ const existingKey = loadKeypairFromDisk();
53
+ if (existingKey) {
54
+ const walletKeypair = loadKeypair(existingKey);
55
+ const address = await getAddress(walletKeypair);
56
+ setSessionSecretKey(existingKey);
57
+ setSessionWalletAddress(address);
58
+ captureWalletAddress(address);
59
+ return mcpText(`**Existing Keypair Loaded** from \`${KEYPAIR_PATH}\`\n\n` +
60
+ `**Wallet Address:** \`${address}\`\n\n` +
61
+ `To create a Helius account, fund this wallet with:\n` +
62
+ `- **~0.001 SOL** for transaction fees\n` +
63
+ `- **1 USDC** for basic plan (or more for paid plans)\n\n` +
64
+ `Then call \`agenticSignup\` to complete account creation.`);
65
+ }
66
+ // Generate new keypair and persist to disk
67
+ const keypair = await generateKeypair();
68
+ const walletKeypair = loadKeypair(keypair.secretKey);
69
+ const address = await getAddress(walletKeypair);
70
+ saveKeypairToDisk(keypair.secretKey);
71
+ setSessionSecretKey(keypair.secretKey);
72
+ setSessionWalletAddress(address);
73
+ captureWalletAddress(address);
74
+ return mcpText(`**Keypair Generated**\n\n` +
75
+ `**Wallet Address:** \`${address}\`\n` +
76
+ `**Saved to:** \`${KEYPAIR_PATH}\`\n\n` +
77
+ `To create a Helius account, fund this wallet with:\n` +
78
+ `- **~0.001 SOL** for transaction fees\n` +
79
+ `- **1 USDC** for basic plan (or more for paid plans)\n\n` +
80
+ `Then call \`agenticSignup\` to complete account creation.`);
81
+ }
82
+ catch (err) {
83
+ return handleToolError(err, 'Error generating keypair');
84
+ }
85
+ });
86
+ server.tool('checkSignupBalance', 'Check if the signup wallet has sufficient SOL and USDC balance for Helius basic plan ($1 USDC). Paid plans (developer/business/professional) require more USDC — the exact amount depends on the plan and is checked during checkout.', {}, async () => {
87
+ try {
88
+ let address = getSessionWalletAddress();
89
+ // Fall back to disk keypair if no session wallet
90
+ if (!address) {
91
+ const diskKey = loadKeypairFromDisk();
92
+ if (diskKey) {
93
+ const walletKeypair = loadKeypair(diskKey);
94
+ address = await getAddress(walletKeypair);
95
+ setSessionSecretKey(diskKey);
96
+ setSessionWalletAddress(address);
97
+ }
98
+ }
99
+ if (!address) {
100
+ return mcpError('No signup wallet found. Call `generateKeypair` first to create a wallet.');
101
+ }
102
+ const solBalance = await checkSolBalance(address);
103
+ const usdcBalance = await checkUsdcBalance(address);
104
+ const solAmount = Number(solBalance) / 1_000_000_000;
105
+ const usdcAmount = Number(usdcBalance) / 1_000_000;
106
+ const solOk = solBalance >= 1000000n;
107
+ const usdcOk = usdcBalance >= 1000000n; // 1 USDC for basic plan
108
+ let status;
109
+ if (solOk && usdcOk) {
110
+ status = 'Ready for signup (basic plan). For paid plans, ensure sufficient USDC for the plan price.';
111
+ }
112
+ else {
113
+ const missing = [];
114
+ if (!solOk)
115
+ missing.push(`~0.001 SOL (have ${solAmount.toFixed(6)})`);
116
+ if (!usdcOk)
117
+ missing.push(`1 USDC (have ${usdcAmount.toFixed(2)})`);
118
+ status = `Need more funds: ${missing.join(', ')}`;
119
+ }
120
+ return mcpText(`**Signup Wallet Balance** (\`${address}\`)\n\n` +
121
+ `- **SOL:** ${solAmount.toFixed(6)} ${solOk ? '(sufficient)' : '(insufficient)'}\n` +
122
+ `- **USDC:** ${usdcAmount.toFixed(2)} ${usdcOk ? '(sufficient for basic)' : '(insufficient)'}\n\n` +
123
+ `**Status:** ${status}`);
124
+ }
125
+ catch (err) {
126
+ return handleToolError(err, 'Error checking balances');
127
+ }
128
+ });
129
+ server.tool('agenticSignup', 'Create a Helius account using the generated keypair. Default: basic plan ($1 USDC). For paid plans (developer/business/professional), email, firstName, and lastName are required. On success, automatically configures the API key for this session.', {
130
+ plan: z.string().optional().describe('Plan to sign up for: "basic" ($1, default), "developer", "business", or "professional"'),
131
+ period: z.enum(["monthly", "yearly"]).optional().describe('Billing period for paid plans (default: monthly)'),
132
+ email: z.string().email().optional().describe('Email address (required for paid plans)'),
133
+ firstName: z.string().optional().describe('First name (required for paid plans)'),
134
+ lastName: z.string().optional().describe('Last name (required for paid plans)'),
135
+ couponCode: z.string().optional().describe('Coupon code for paid plans'),
136
+ discoveryPath: z.string().optional().describe('How did you discover Helius? Helps us improve agent onboarding'),
137
+ frictionPoints: z.string().optional().describe('What friction did you hit finding or setting up Helius? Free-form feedback'),
138
+ }, async ({ plan, period, email, firstName, lastName, couponCode, discoveryPath, frictionPoints }) => {
139
+ if (discoveryPath || frictionPoints) {
140
+ sendFeedbackEvent({
141
+ type: 'discovery',
142
+ discoveryPath,
143
+ frictionPoints,
144
+ });
145
+ }
146
+ try {
147
+ let signerData;
148
+ try {
149
+ signerData = await loadSignerOrFail();
150
+ }
151
+ catch {
152
+ return mcpError('No signup keypair found. Call `generateKeypair` first to create a wallet, fund it, then call this tool.');
153
+ }
154
+ const result = await agenticSignup({
155
+ secretKey: signerData.secretKey,
156
+ userAgent: MCP_USER_AGENT,
157
+ plan,
158
+ period,
159
+ email,
160
+ firstName,
161
+ lastName,
162
+ couponCode,
163
+ });
164
+ // Configure API key for this session and persist to shared config
165
+ if (result.apiKey) {
166
+ setApiKey(result.apiKey);
167
+ setSharedApiKey(result.apiKey);
168
+ }
169
+ // Persist JWT to disk
170
+ if (result.jwt) {
171
+ setJwt(result.jwt);
172
+ }
173
+ const saveNote = result.apiKey
174
+ ? `\nAPI key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`. All Helius tools are now ready to use.`
175
+ : '';
176
+ if (result.status === 'existing_project') {
177
+ return mcpText(`**Helius Account Found**\n\n` +
178
+ `You already have a Helius account. No payment was needed.\n\n` +
179
+ `- **Wallet:** \`${result.walletAddress}\`\n` +
180
+ `- **Project ID:** \`${result.projectId}\`\n` +
181
+ (result.apiKey ? `- **API Key:** \`${result.apiKey}\`\n` : '') +
182
+ (result.endpoints ? `- **Mainnet RPC:** \`${result.endpoints.mainnet}\`\n` : '') +
183
+ (result.endpoints ? `- **Devnet RPC:** \`${result.endpoints.devnet}\`\n` : '') +
184
+ (result.credits !== null ? `- **Credits:** ${result.credits.toLocaleString()}\n` : '') +
185
+ saveNote);
186
+ }
187
+ if (result.status === 'upgraded') {
188
+ return mcpText(`**Plan Upgraded to ${plan || 'paid plan'}**\n\n` +
189
+ `- **Wallet:** \`${result.walletAddress}\`\n` +
190
+ `- **Project ID:** \`${result.projectId}\`\n` +
191
+ (result.apiKey ? `- **API Key:** \`${result.apiKey}\`\n` : '') +
192
+ (result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
193
+ saveNote);
194
+ }
195
+ return mcpText(`**Helius Account Created**\n\n` +
196
+ `- **Wallet:** \`${result.walletAddress}\`\n` +
197
+ `- **Project ID:** \`${result.projectId}\`\n` +
198
+ (result.apiKey ? `- **API Key:** \`${result.apiKey}\`\n` : '') +
199
+ (result.endpoints ? `- **Mainnet RPC:** \`${result.endpoints.mainnet}\`\n` : '') +
200
+ (result.endpoints ? `- **Devnet RPC:** \`${result.endpoints.devnet}\`\n` : '') +
201
+ (result.credits !== null ? `- **Credits:** ${result.credits.toLocaleString()}\n` : '') +
202
+ (result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
203
+ saveNote);
204
+ }
205
+ catch (err) {
206
+ return handleToolError(err, 'Error during signup');
207
+ }
208
+ });
209
+ // ── Upgrade, Preview, Renewal Tools ──
210
+ server.tool('previewUpgrade', 'Preview pricing for a plan upgrade with proration details. Shows current plan, new plan cost, prorated credits, and amount due today.', {
211
+ plan: z.enum(['developer', 'business', 'professional']).describe('Target plan name'),
212
+ period: z.enum(['monthly', 'yearly']).default('monthly').describe('Billing period'),
213
+ couponCode: z.string().optional().describe('Optional coupon code'),
214
+ }, async ({ plan, period, couponCode }) => {
215
+ try {
216
+ const jwt = getJwt();
217
+ if (!jwt) {
218
+ return mcpError('Not authenticated. Call `agenticSignup` or authenticate first.');
219
+ }
220
+ const projects = await listProjects(jwt, MCP_USER_AGENT);
221
+ if (projects.length === 0) {
222
+ return mcpError('No projects found. Call `agenticSignup` to create an account first.');
223
+ }
224
+ const projectId = projects[0].id;
225
+ const projectDetails = await getProject(jwt, projectId, MCP_USER_AGENT);
226
+ const currentPlan = projectDetails.subscriptionPlanDetails?.currentPlan || 'unknown';
227
+ const preview = await getCheckoutPreview(jwt, plan, period, projectId, couponCode, MCP_USER_AGENT);
228
+ let text = `**Upgrade Preview**\n\n` +
229
+ `- **Current Plan:** ${currentPlan}\n` +
230
+ `- **Target Plan:** ${preview.planName} (${preview.period})\n\n` +
231
+ `**Pricing Breakdown:**\n` +
232
+ `- Subtotal: $${(preview.subtotal / 100).toFixed(2)}\n`;
233
+ if (preview.proratedCredits > 0) {
234
+ text += `- Prorated credit: -$${(preview.proratedCredits / 100).toFixed(2)}\n`;
235
+ }
236
+ if (preview.appliedCredits > 0) {
237
+ text += `- Applied credits: -$${(preview.appliedCredits / 100).toFixed(2)}\n`;
238
+ }
239
+ if (preview.discounts > 0) {
240
+ text += `- Discounts: -$${(preview.discounts / 100).toFixed(2)}\n`;
241
+ }
242
+ if (preview.coupon?.valid) {
243
+ text += `- Coupon (${preview.coupon.code}): ${preview.coupon.description || 'Applied'}\n`;
244
+ }
245
+ else if (preview.coupon && !preview.coupon.valid) {
246
+ text += `- Coupon (${preview.coupon.code}): **Invalid** — ${preview.coupon.invalidReason || 'not applicable'}\n`;
247
+ }
248
+ text += `\n**Due Today: $${(preview.dueToday / 100).toFixed(2)}**\n`;
249
+ if (preview.note) {
250
+ text += `\n_${preview.note}_\n`;
251
+ }
252
+ text += `\nTo proceed, use the \`upgradePlan\` tool with plan: '${plan}'.`;
253
+ return mcpText(text);
254
+ }
255
+ catch (err) {
256
+ return handleToolError(err, 'Error previewing upgrade');
257
+ }
258
+ });
259
+ server.tool('upgradePlan', 'Upgrade your Helius plan. Processes USDC payment with proration. Call previewUpgrade first to see pricing. Requires email, firstName, and lastName for first-time upgrades — all three must be provided together.', {
260
+ plan: z.enum(['developer', 'business', 'professional']).describe('Target plan name'),
261
+ period: z.enum(['monthly', 'yearly']).default('monthly').describe('Billing period'),
262
+ couponCode: z.string().optional().describe('Optional coupon code'),
263
+ email: z.string().email().optional().describe('Email address (required for first-time upgrades)'),
264
+ firstName: z.string().optional().describe('First name (required for first-time upgrades)'),
265
+ lastName: z.string().optional().describe('Last name (required for first-time upgrades)'),
266
+ }, async ({ plan, period, couponCode, email, firstName, lastName }) => {
267
+ try {
268
+ // All-or-none customer info validation
269
+ const hasAny = email || firstName || lastName;
270
+ if (hasAny && (!email || !firstName || !lastName)) {
271
+ const missing = [
272
+ !email && 'email',
273
+ !firstName && 'firstName',
274
+ !lastName && 'lastName',
275
+ ].filter(Boolean);
276
+ return mcpError(`Partial customer info provided. If any of email/firstName/lastName is given, all three are required. Missing: ${missing.join(', ')}`);
277
+ }
278
+ let signerData;
279
+ try {
280
+ signerData = await loadSignerOrFail();
281
+ }
282
+ catch {
283
+ return mcpError('No keypair found. Call `generateKeypair` first.');
284
+ }
285
+ const jwt = getJwt();
286
+ if (!jwt) {
287
+ return mcpError('Not authenticated. Call `agenticSignup` or authenticate first.');
288
+ }
289
+ const projects = await listProjects(jwt, MCP_USER_AGENT);
290
+ if (projects.length === 0) {
291
+ return mcpError('No projects found. Call `agenticSignup` to create an account first.');
292
+ }
293
+ const projectId = projects[0].id;
294
+ const result = await executeCheckout(signerData.secretKey, jwt, {
295
+ plan,
296
+ period,
297
+ refId: projectId,
298
+ couponCode,
299
+ email,
300
+ firstName,
301
+ lastName,
302
+ }, MCP_USER_AGENT, { skipProjectPolling: true });
303
+ if (result.status !== 'completed') {
304
+ return mcpError(`**Upgrade ${result.status}**\n\n` +
305
+ (result.error ? `Error: ${result.error}\n` : '') +
306
+ (result.txSignature ? `TX: \`${result.txSignature}\`\n` : '') +
307
+ `\nIf you need help, contact support with the payment intent ID: \`${result.paymentIntentId}\``);
308
+ }
309
+ const planInfo = PLAN_CATALOG[plan];
310
+ return mcpText(`**Plan Upgraded Successfully**\n\n` +
311
+ `- **New Plan:** ${planInfo.name} (${period})\n` +
312
+ `- **Project ID:** \`${projectId}\`\n` +
313
+ (result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
314
+ `\nYour new plan is now active with ${(planInfo.credits / 1_000_000).toFixed(0)}M credits and ${planInfo.requestsPerSecond} RPS.`);
315
+ }
316
+ catch (err) {
317
+ return handleToolError(err, 'Error upgrading plan');
318
+ }
319
+ });
320
+ server.tool('getAccountStatus', 'Check your Helius account status: current plan, remaining credits, rate limits, and billing cycle. ' +
321
+ 'Call this before bulk operations to verify you have sufficient credits. ' +
322
+ 'Requires a JWT session (i.e., you signed up via agenticSignup). ' +
323
+ 'If you only have an API key configured, auth status is confirmed but credit data is unavailable — call agenticSignup to enable full status.', {}, async () => {
324
+ try {
325
+ // ── Tier 1: not authenticated at all ──
326
+ if (!hasApiKey()) {
327
+ return mcpText(`## Account Status\n\n` +
328
+ `**Auth:** Not authenticated\n\n` +
329
+ `No API key or session found. To get started:\n` +
330
+ `- If you have a key: use the \`setHeliusApiKey\` tool\n` +
331
+ `- If you need an account: use \`generateKeypair\` → fund wallet → \`agenticSignup\``);
332
+ }
333
+ // ── Tier 2: API key present but no JWT — can't reach dashboard API ──
334
+ const jwt = getJwt();
335
+ if (!jwt) {
336
+ return mcpText(`## Account Status\n\n` +
337
+ `**Auth:** Authenticated (API key configured)\n` +
338
+ `**Credit usage:** Not available — no JWT session found\n\n` +
339
+ `To see your plan, rate limits, and credit balance, call \`agenticSignup\`.\n` +
340
+ `Your existing account will be detected automatically — no payment needed.`);
341
+ }
342
+ // ── Tier 3: full status via JWT ──
343
+ const projects = await listProjects(jwt, MCP_USER_AGENT);
344
+ if (projects.length === 0) {
345
+ return mcpError('No projects found. Call `agenticSignup` to create an account first.');
346
+ }
347
+ const projectId = projects[0].id;
348
+ const details = await getProject(jwt, projectId, MCP_USER_AGENT);
349
+ const planKey = details.subscriptionPlanDetails?.currentPlan ?? 'unknown';
350
+ const upcomingPlan = details.subscriptionPlanDetails?.upcomingPlan;
351
+ const isUpgrading = details.subscriptionPlanDetails?.isUpgrading ?? false;
352
+ const planInfo = HELIUS_PLANS[planKey];
353
+ const usage = details.creditsUsage;
354
+ const cycle = details.billingCycle;
355
+ const lines = [`## Account Status`, ''];
356
+ // ── Auth + plan ──
357
+ lines.push(`**Auth:** Authenticated`);
358
+ lines.push(`**Plan:** ${planInfo ? planInfo.name : planKey} | **Project:** \`${projectId}\``);
359
+ if (isUpgrading && upcomingPlan && upcomingPlan !== planKey) {
360
+ lines.push(`**Upcoming plan:** ${upcomingPlan} (takes effect at next billing cycle)`);
361
+ }
362
+ // ── Rate limits (fetched live from billing docs) ──
363
+ try {
364
+ const billingDoc = await fetchDoc('billing');
365
+ const rateLimits = extractSections(billingDoc, ['standard rate limits', 'rate limits'], { includeLooseMatches: false });
366
+ if (rateLimits) {
367
+ lines.push('', '### Rate Limits (live)', '', rateLimits);
368
+ }
369
+ else {
370
+ lines.push('', '_Rate limit details: use `getRateLimitInfo` or visit https://www.helius.dev/docs/billing_');
371
+ }
372
+ }
373
+ catch {
374
+ lines.push('', '_Rate limit details: use `getRateLimitInfo` or visit https://www.helius.dev/docs/billing_');
375
+ }
376
+ // ── Credits ──
377
+ if (usage) {
378
+ const total = usage.remainingCredits + usage.totalCreditsUsed;
379
+ const pctUsed = total > 0 ? ((usage.totalCreditsUsed / total) * 100).toFixed(1) : '0.0';
380
+ const pctRemaining = total > 0 ? (100 - parseFloat(pctUsed)).toFixed(1) : '100.0';
381
+ // Billing cycle + days remaining
382
+ let cycleStr = '';
383
+ let daysNote = '';
384
+ if (cycle) {
385
+ const end = new Date(cycle.end);
386
+ const now = new Date();
387
+ const daysLeft = Math.ceil((end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
388
+ cycleStr = ` (${cycle.start} - ${cycle.end}, ${daysLeft} day${daysLeft !== 1 ? 's' : ''} remaining)`;
389
+ // Burn-rate warning: project usage over elapsed days to end of cycle
390
+ const start = new Date(cycle.start);
391
+ const totalDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
392
+ const elapsedDays = totalDays - daysLeft;
393
+ if (elapsedDays > 0 && daysLeft > 0) {
394
+ const projectedTotal = Math.round((usage.totalCreditsUsed / elapsedDays) * totalDays);
395
+ if (projectedTotal > total) {
396
+ const overageM = ((projectedTotal - total) / 1_000_000).toFixed(1);
397
+ daysNote = `\n> At current burn rate you're projected to use ~${(projectedTotal / 1_000_000).toFixed(1)}M credits this cycle — ${overageM}M over your ${(total / 1_000_000).toFixed(0)}M limit. Consider upgrading or reducing usage.`;
398
+ }
399
+ }
400
+ }
401
+ lines.push('', `### Credits — Billing Cycle${cycleStr}`);
402
+ lines.push(`- **Remaining:** ${usage.remainingCredits.toLocaleString()} / ${total.toLocaleString()} (${pctRemaining}%)`);
403
+ lines.push(`- **Used:** ${usage.totalCreditsUsed.toLocaleString()} (${pctUsed}%)`);
404
+ lines.push(` - API: ${usage.apiUsage.toLocaleString()}`);
405
+ lines.push(` - RPC: ${usage.rpcUsage.toLocaleString()} | RPC GPA: ${usage.rpcGPAUsage.toLocaleString()} | Webhooks: ${usage.webhookUsage.toLocaleString()}`);
406
+ if (usage.overageCreditsUsed > 0) {
407
+ lines.push(`- **Overage:** ${usage.overageCreditsUsed.toLocaleString()} credits ($${usage.overageCost.toFixed(2)})`);
408
+ }
409
+ else {
410
+ lines.push(`- **Overage:** none`);
411
+ }
412
+ if (usage.remainingPrepaidCredits > 0 || usage.prepaidCreditsUsed > 0) {
413
+ lines.push(`- **Prepaid:** ${usage.remainingPrepaidCredits.toLocaleString()} remaining (${usage.prepaidCreditsUsed.toLocaleString()} used)`);
414
+ }
415
+ if (daysNote)
416
+ lines.push(daysNote);
417
+ // Low-credit warning
418
+ if (parseFloat(pctRemaining) < 20) {
419
+ lines.push(`\n> Less than 20% of credits remaining. Use \`previewUpgrade\` to see upgrade pricing, or \`getHeliusPlanInfo\` to compare plans.`);
420
+ }
421
+ }
422
+ return mcpText(lines.join('\n'));
423
+ }
424
+ catch (err) {
425
+ return handleToolError(err, 'Error fetching account status');
426
+ }
427
+ });
428
+ server.tool('payRenewal', 'Pay an existing payment intent (e.g., from a renewal notification). Fetches intent details, validates, and processes USDC payment.', {
429
+ paymentIntentId: z.string().describe('Payment intent ID from renewal notification'),
430
+ }, async ({ paymentIntentId }) => {
431
+ try {
432
+ let signerData;
433
+ try {
434
+ signerData = await loadSignerOrFail();
435
+ }
436
+ catch {
437
+ return mcpError('No keypair found. Call `generateKeypair` first.');
438
+ }
439
+ const jwt = getJwt();
440
+ if (!jwt) {
441
+ return mcpError('Not authenticated. Call `agenticSignup` or authenticate first.');
442
+ }
443
+ const result = await executeRenewal(signerData.secretKey, jwt, paymentIntentId, MCP_USER_AGENT);
444
+ if (result.status !== 'completed') {
445
+ return mcpError(`**Payment ${result.status}**\n\n` +
446
+ (result.error ? `Error: ${result.error}\n` : '') +
447
+ (result.txSignature ? `TX: \`${result.txSignature}\`\n` : '') +
448
+ `\nIf you need help, contact support with the payment intent ID: \`${result.paymentIntentId}\``);
449
+ }
450
+ return mcpText(`**Payment Complete**\n\n` +
451
+ `- **Payment Intent:** \`${result.paymentIntentId}\`\n` +
452
+ (result.txSignature ? `- **TX:** \`${result.txSignature}\`\n` : '') +
453
+ `\nYour subscription has been renewed successfully.`);
454
+ }
455
+ catch (err) {
456
+ return handleToolError(err, 'Error processing renewal payment');
457
+ }
458
+ });
459
+ }
@@ -2,25 +2,28 @@ import { z } from 'zod';
2
2
  import { getHeliusClient, hasApiKey } from '../utils/helius.js';
3
3
  import { formatSol, formatAddress } from '../utils/formatters.js';
4
4
  import { noApiKeyResponse } from './shared.js';
5
+ import { mcpText, getErrorMessage, handleToolError, addressError } from '../utils/errors.js';
5
6
  export function registerBalanceTools(server) {
6
7
  // Get SOL Balance
7
- server.tool('getBalance', 'Get native SOL balance for a Solana wallet address. Returns balance in both SOL and lamports (1 SOL = 1 billion lamports). Use this for checking how much SOL a wallet has. For token balances, use getTokenBalances instead.', {
8
- address: z.string().describe('Solana wallet address (base58 encoded, e.g. Gh4tdJhLP1s55xGfghHHvPNPPrNtaDjc6dzZJ374DGHJ)')
8
+ server.tool('getBalance', 'BEST FOR: SOL-only balance checks. PREFER getTokenBalances for SPL tokens, getWalletBalances for full portfolio with USD. Get native SOL balance for a wallet. Returns balance in SOL and lamports. Credit cost: 1 credit.', {
9
+ address: z.string().describe('Solana wallet address (base58 encoded)')
9
10
  }, async ({ address }) => {
10
11
  if (!hasApiKey())
11
12
  return noApiKeyResponse();
12
- const helius = getHeliusClient();
13
- const balance = await helius.getBalance(address);
14
- const lamports = Number(balance.value);
15
- return {
16
- content: [{
17
- type: 'text',
18
- text: `**SOL Balance for ${formatAddress(address)}**\n\n${formatSol(lamports)} (${lamports.toLocaleString()} lamports)`
19
- }]
20
- };
13
+ try {
14
+ const helius = getHeliusClient();
15
+ const balance = await helius.getBalance(address);
16
+ const lamports = Number(balance.value);
17
+ return mcpText(`**SOL Balance for ${formatAddress(address)}**\n\n${formatSol(lamports)} (${lamports.toLocaleString()} lamports)`);
18
+ }
19
+ catch (err) {
20
+ return handleToolError(err, 'Error fetching balance', [
21
+ addressError(`SOL Balance for ${formatAddress(address)}`, 'Invalid Solana address. Please provide a valid base58-encoded wallet address.'),
22
+ ]);
23
+ }
21
24
  });
22
25
  // Get Token Balances
23
- server.tool('getTokenBalances', 'Get all SPL token balances for a Solana wallet with full token info: names, symbols, properly formatted amounts with decimals, and USD prices (when available). Includes fungible tokens like USDC, BONK, JUP, etc. Does NOT include NFTs — use getAssetsByOwner for NFTs. For native SOL balance, use getBalance instead. Automatically paginates to fetch all tokens using parallel requests for speed.', {
26
+ server.tool('getTokenBalances', 'BEST FOR: listing SPL token holdings with prices. PREFER getBalance for SOL-only, getWalletBalances for full portfolio with USD and SOL. Get all SPL token balances for a wallet with names, symbols, amounts, and USD prices. Auto-paginates. Credit cost: 10 credits/page (DAS API).', {
24
27
  address: z.string().describe('Solana wallet address (base58 encoded)')
25
28
  }, async ({ address }) => {
26
29
  if (!hasApiKey())
@@ -44,7 +47,7 @@ export function registerBalanceTools(server) {
44
47
  return { items, hasMore: items.length === currentLimit };
45
48
  }
46
49
  catch (err) {
47
- const errorMsg = err instanceof Error ? err.message : String(err);
50
+ const errorMsg = getErrorMessage(err);
48
51
  if (errorMsg.includes('too big') || errorMsg.includes('Response is too big')) {
49
52
  if (currentLimit > 5)
50
53
  currentLimit = 5;
@@ -63,15 +66,18 @@ export function registerBalanceTools(server) {
63
66
  return { items: [], hasMore: false };
64
67
  }
65
68
  // Step 1: Fetch first page to probe
66
- const firstResult = await fetchPage(1);
69
+ let firstResult;
70
+ try {
71
+ firstResult = await fetchPage(1);
72
+ }
73
+ catch (err) {
74
+ return handleToolError(err, 'Error fetching token balances', [
75
+ addressError(`Token Balances for ${formatAddress(address)}`, 'Invalid Solana address. Please provide a valid base58-encoded wallet address.'),
76
+ ]);
77
+ }
67
78
  const allAssets = [...firstResult.items];
68
79
  if (allAssets.length === 0) {
69
- return {
70
- content: [{
71
- type: 'text',
72
- text: `**Token Balances for ${formatAddress(address)}**\n\nNo tokens found.`
73
- }]
74
- };
80
+ return mcpText(`**Token Balances for ${formatAddress(address)}**\n\nNo tokens found.`);
75
81
  }
76
82
  // Step 2: If first page is full, fetch remaining pages in parallel
77
83
  if (firstResult.hasMore && allAssets.length < MAX_ASSETS) {
@@ -97,12 +103,7 @@ export function registerBalanceTools(server) {
97
103
  }
98
104
  const fungibleTokens = allAssets.filter((asset) => asset.interface === 'FungibleToken' || asset.interface === 'FungibleAsset');
99
105
  if (fungibleTokens.length === 0) {
100
- return {
101
- content: [{
102
- type: 'text',
103
- text: `**Token Balances for ${formatAddress(address)}**\n\nNo fungible tokens found.`
104
- }]
105
- };
106
+ return mcpText(`**Token Balances for ${formatAddress(address)}**\n\nNo fungible tokens found.`);
106
107
  }
107
108
  // Enrich tokens missing name/symbol by fetching full asset data
108
109
  const unknownTokens = fungibleTokens.filter((asset) => !asset.token_info?.symbol && !asset.content?.metadata?.symbol && !asset.content?.metadata?.name);
@@ -157,11 +158,6 @@ export function registerBalanceTools(server) {
157
158
  header += ` (${tokensWithPrice}/${fungibleTokens.length} tokens have price data)`;
158
159
  }
159
160
  }
160
- return {
161
- content: [{
162
- type: 'text',
163
- text: `${header}\n\n${lines.join('\n')}`
164
- }]
165
- };
161
+ return mcpText(`${header}\n\n${lines.join('\n')}`);
166
162
  });
167
163
  }