helius-mcp 1.3.0 → 2.0.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 (101) hide show
  1. package/CHANGELOG.md +79 -79
  2. package/LICENSE +21 -21
  3. package/README.md +144 -132
  4. package/dist/http.d.ts +1 -1
  5. package/dist/index.js +2 -56
  6. package/dist/results/store.d.ts +8 -0
  7. package/dist/results/store.js +72 -0
  8. package/dist/results/types.d.ts +47 -0
  9. package/dist/results/types.js +1 -0
  10. package/dist/router/action-groups.d.ts +6 -0
  11. package/dist/router/action-groups.js +32 -0
  12. package/dist/router/action-handlers.d.ts +20 -0
  13. package/dist/router/action-handlers.js +125 -0
  14. package/dist/router/actions.d.ts +12 -0
  15. package/dist/router/actions.js +123 -0
  16. package/dist/router/catalog.d.ts +6 -0
  17. package/dist/router/catalog.js +388 -0
  18. package/dist/router/context.d.ts +5 -0
  19. package/dist/router/context.js +10 -0
  20. package/dist/router/dispatch.d.ts +4 -0
  21. package/dist/router/dispatch.js +276 -0
  22. package/dist/router/instructions.d.ts +1 -0
  23. package/dist/router/instructions.js +25 -0
  24. package/dist/router/register.d.ts +2 -0
  25. package/dist/router/register.js +15 -0
  26. package/dist/router/required-params.d.ts +9 -0
  27. package/dist/router/required-params.js +66 -0
  28. package/dist/router/responses.d.ts +29 -0
  29. package/dist/router/responses.js +186 -0
  30. package/dist/router/schemas.d.ts +216 -0
  31. package/dist/router/schemas.js +195 -0
  32. package/dist/router/telemetry.d.ts +27 -0
  33. package/dist/router/telemetry.js +52 -0
  34. package/dist/router/types.d.ts +46 -0
  35. package/dist/router/types.js +1 -0
  36. package/dist/scripts/validate-catalog.d.ts +2 -2
  37. package/dist/scripts/validate-catalog.js +10 -10
  38. package/dist/tools/accounts.js +5 -5
  39. package/dist/tools/assets.js +5 -5
  40. package/dist/tools/auth.js +392 -319
  41. package/dist/tools/config.js +3 -3
  42. package/dist/tools/das-extras.js +6 -6
  43. package/dist/tools/docs.js +55 -41
  44. package/dist/tools/enhanced-websockets.js +13 -13
  45. package/dist/tools/fees.js +3 -3
  46. package/dist/tools/index.d.ts +1 -1
  47. package/dist/tools/index.js +2 -80
  48. package/dist/tools/laserstream.js +20 -23
  49. package/dist/tools/network.js +10 -4
  50. package/dist/tools/plans.d.ts +0 -5
  51. package/dist/tools/plans.js +167 -12
  52. package/dist/tools/product-catalog.d.ts +1 -0
  53. package/dist/tools/product-catalog.js +51 -16
  54. package/dist/tools/recommend.d.ts +0 -1
  55. package/dist/tools/recommend.js +9 -28
  56. package/dist/tools/shared.d.ts +1 -0
  57. package/dist/tools/shared.js +21 -13
  58. package/dist/tools/solana-knowledge.js +23 -7
  59. package/dist/tools/staking.d.ts +2 -0
  60. package/dist/tools/staking.js +268 -0
  61. package/dist/tools/transactions.js +167 -3
  62. package/dist/tools/transfers.js +38 -43
  63. package/dist/tools/wallet.js +27 -16
  64. package/dist/tools/webhooks.js +3 -3
  65. package/dist/tools/zk-compression.d.ts +2 -0
  66. package/dist/tools/zk-compression.js +781 -0
  67. package/dist/utils/config.d.ts +2 -2
  68. package/dist/utils/config.js +68 -6
  69. package/dist/utils/errors.d.ts +10 -1
  70. package/dist/utils/errors.js +46 -12
  71. package/dist/utils/feedback.js +1 -4
  72. package/dist/utils/helius.js +2 -1
  73. package/dist/utils/ows.d.ts +74 -0
  74. package/dist/utils/ows.js +155 -0
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/package.json +64 -64
  78. package/system-prompts/helius/claude.system.md +200 -170
  79. package/system-prompts/helius/full.md +3212 -2869
  80. package/system-prompts/helius/openai.developer.md +200 -170
  81. package/system-prompts/helius-dflow/claude.system.md +324 -290
  82. package/system-prompts/helius-dflow/full.md +4136 -3648
  83. package/system-prompts/helius-dflow/openai.developer.md +324 -290
  84. package/system-prompts/helius-jupiter/claude.system.md +333 -0
  85. package/system-prompts/helius-jupiter/full.md +5109 -0
  86. package/system-prompts/helius-jupiter/openai.developer.md +333 -0
  87. package/system-prompts/helius-okx/claude.system.md +182 -0
  88. package/system-prompts/helius-okx/full.md +584 -0
  89. package/system-prompts/helius-okx/openai.developer.md +182 -0
  90. package/system-prompts/helius-phantom/claude.system.md +345 -333
  91. package/system-prompts/helius-phantom/full.md +5625 -5473
  92. package/system-prompts/helius-phantom/openai.developer.md +345 -333
  93. package/system-prompts/svm/claude.system.md +159 -159
  94. package/system-prompts/svm/full.md +631 -631
  95. package/system-prompts/svm/openai.developer.md +159 -159
  96. package/dist/scripts/test-htmltotext.d.ts +0 -5
  97. package/dist/scripts/test-htmltotext.js +0 -67
  98. package/dist/scripts/test-solana-knowledge.d.ts +0 -9
  99. package/dist/scripts/test-solana-knowledge.js +0 -272
  100. package/dist/scripts/validate-templates.d.ts +0 -12
  101. package/dist/scripts/validate-templates.js +0 -94
@@ -2,58 +2,57 @@ import { z } from 'zod';
2
2
  import { generateKeypair } from 'helius-sdk/auth/generateKeypair';
3
3
  import { loadKeypair } from 'helius-sdk/auth/loadKeypair';
4
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
5
  import { listProjects } from 'helius-sdk/auth/listProjects';
9
6
  import { getProject } from 'helius-sdk/auth/getProject';
10
- import { PLAN_CATALOG } from 'helius-sdk/auth/planCatalog';
7
+ import { getCheckoutPreview } from 'helius-sdk/auth/checkout';
8
+ import { signup } from 'helius-sdk/auth/signup';
9
+ import { signupAndPay } from 'helius-sdk/auth/signupAndPay';
10
+ import { upgradePlan, upgradePlanAndPay } from 'helius-sdk/auth/upgradePlan';
11
+ import { purchaseCredits as sdkPurchaseCredits, purchaseCreditsAndPay, } from 'helius-sdk/auth/purchaseCredits';
12
+ import { payRenewal, payRenewalAndPay } from 'helius-sdk/auth/payRenewal';
11
13
  import { MCP_USER_AGENT } from '../http.js';
12
- import { setApiKey, hasApiKey, setSessionSecretKey, setSessionWalletAddress, getSessionWalletAddress, loadSignerOrFail, } from '../utils/helius.js';
14
+ import { setApiKey, hasApiKey, setSessionSecretKey, setSessionWalletAddress, loadSignerOrFail, } from '../utils/helius.js';
13
15
  import { mcpText, mcpError, handleToolError } from '../utils/errors.js';
14
16
  import { fetchDoc, extractSections } from '../utils/docs.js';
15
17
  import { sendFeedbackEvent, captureWalletAddress } from '../utils/feedback.js';
16
- import { setSharedApiKey, setJwt, getJwt, SHARED_CONFIG_PATH, KEYPAIR_PATH, loadKeypairFromDisk, saveKeypairToDisk, keypairExistsOnDisk } from '../utils/config.js';
18
+ import { setSharedApiKey, setJwt, getJwt, SHARED_CONFIG_PATH, KEYPAIR_PATH, loadKeypairFromDisk, saveKeypairToDisk, keypairExistsOnDisk, } from '../utils/config.js';
17
19
  import { HELIUS_PLANS } from './plans.js';
18
- const PAID_PLAN_ORDER = ['developer', 'business', 'professional'];
19
- /** Tracks consecutive insufficient-balance checks to prevent agent polling loops. */
20
- let insufficientBalanceChecks = 0;
21
- const MAX_BALANCE_CHECKS_BEFORE_STOP = 3;
20
+ const renderPaymentLink = (paymentLink, flowName, resumeCmd) => `**${flowName}: payment required**\n\n` +
21
+ `Open this link in a browser to pay:\n\n` +
22
+ `\`${paymentLink.paymentUrl}\`\n\n` +
23
+ `Or send USDC + memo manually:\n` +
24
+ `- **Amount:** ${paymentLink.amountCents / 100} USDC\n` +
25
+ `- **Treasury:** \`${paymentLink.destinationWallet}\`\n` +
26
+ `- **Memo:** \`${paymentLink.memo}\`\n` +
27
+ `- **Plan:** ${paymentLink.planName}\n\n` +
28
+ `After paying, call \`${resumeCmd}\` to confirm activation locally.`;
22
29
  export function registerAuthTools(server) {
23
- // ── Getting Started Guide ──
30
+ // ── Getting Started ──
24
31
  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 () => {
25
32
  const lines = ['# Getting Started with Helius'];
26
33
  const apiKeyConfigured = hasApiKey();
27
34
  const hasKeypair = keypairExistsOnDisk();
28
35
  const jwt = getJwt();
29
- // ── Already fully set up ──
36
+ // Already fully set up
30
37
  if (apiKeyConfigured && jwt) {
31
- 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.');
38
+ 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.** It returns architecture recommendations with Helius products, MCP tools, credit costs, and reference files.');
32
39
  return mcpText(lines.join('\n'));
33
40
  }
34
- // ── API key set but no JWT (e.g., set via env or setHeliusApiKey) ──
41
+ // API key set but no JWT
35
42
  if (apiKeyConfigured) {
36
- 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`.');
43
+ lines.push('', 'Your API key is configured — all Helius tools are ready to use.', '', 'To enable account-status info (plan, credits, rate limits), call `signup`. It will detect your existing account, no payment required.');
37
44
  return mcpText(lines.join('\n'));
38
45
  }
39
- // ── No API key — need to set up ──
40
- 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:', '');
41
- // Adapt steps based on whether a keypair already exists
46
+ // No API key — need to set up
47
+ lines.push('', 'You need a Helius API key to use these tools. Three paths:', '', '## Path A — I already have an API key', '', 'Call `setHeliusApiKey` with your key from https://dashboard.helius.dev. All tools immediately available.', '', '## Path B — I have an existing dashboard.helius.dev account', '', 'If you already signed up at https://dashboard.helius.dev (email, Google, GitHub, or SSO), run `helius login` in your terminal:', '', '```bash', 'npx helius-cli@latest login', '```', '', 'This opens your default browser to authenticate via OAuth/PKCE and writes a session token to `~/.helius/config.json`. This MCP picks it up automatically — no further setup. Then call `setHeliusApiKey` with one of your keys (visible via `helius apikeys`), or just call `getAccountStatus` to confirm the session.', '', '## Path C — Create a new account via crypto checkout', '', '1. Call `generateKeypair` to create (or load) a Solana wallet — required for every signup mode, including link mode (the wallet address is bound to the payment intent).', '2. Call `signup` with your email/name. By default (`mode: "link"`) it returns a hosted-checkout URL the user can open in a browser to pay with any wallet.', '3. After the user pays, call `signup` again with `mode: "resume"` to poll the intent and provision the API key.', '', 'For an autopay flow that pays USDC directly from the local keypair, pass `mode: "autopay"` to `signup`. The wallet must hold ~0.001 SOL for fees and the plan amount in USDC.', '');
42
48
  if (hasKeypair) {
43
- lines.push('### Step 1: Keypair ✓', `You already have a keypair saved at \`${KEYPAIR_PATH}\`.`, 'Call `generateKeypair` to load it and see the wallet address.', '');
49
+ lines.push(`_(A keypair already exists at \`${KEYPAIR_PATH}\` \`signup\` will reuse it; you can skip step 1.)_`, '');
44
50
  }
45
- else {
46
- lines.push('### Step 1: Generate a keypair', 'Call the `generateKeypair` tool. It creates a Solana wallet and returns the address.', '');
47
- }
48
- 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.');
49
51
  return mcpText(lines.join('\n'));
50
52
  });
51
53
  // ── Keypair Generation ──
52
- 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 () => {
54
+ server.tool('generateKeypair', 'Generate a new Solana keypair (or load the existing one from disk). Returns the wallet address. Required before any `signup` call the wallet address is bound to the payment intent in both link and autopay modes.', {}, async () => {
53
55
  try {
54
- // Reset balance-check counter for fresh signup flow
55
- insufficientBalanceChecks = 0;
56
- // Check disk first — reuse existing keypair if available
57
56
  const existingKey = loadKeypairFromDisk();
58
57
  if (existingKey) {
59
58
  const walletKeypair = loadKeypair(existingKey);
@@ -62,13 +61,8 @@ export function registerAuthTools(server) {
62
61
  setSessionWalletAddress(address);
63
62
  captureWalletAddress(address);
64
63
  return mcpText(`**Existing Keypair Loaded** from \`${KEYPAIR_PATH}\`\n\n` +
65
- `**Wallet Address:** \`${address}\`\n\n` +
66
- `To create a Helius account, fund this wallet with:\n` +
67
- `- **~0.001 SOL** for transaction fees\n` +
68
- `- **1 USDC** for basic plan (or more for paid plans)\n\n` +
69
- `Then call \`agenticSignup\` to complete account creation.`);
64
+ `**Wallet Address:** \`${address}\``);
70
65
  }
71
- // Generate new keypair and persist to disk
72
66
  const keypair = await generateKeypair();
73
67
  const walletKeypair = loadKeypair(keypair.secretKey);
74
68
  const address = await getAddress(walletKeypair);
@@ -78,95 +72,33 @@ export function registerAuthTools(server) {
78
72
  captureWalletAddress(address);
79
73
  return mcpText(`**Keypair Generated**\n\n` +
80
74
  `**Wallet Address:** \`${address}\`\n` +
81
- `**Saved to:** \`${KEYPAIR_PATH}\`\n\n` +
82
- `To create a Helius account, fund this wallet with:\n` +
83
- `- **~0.001 SOL** for transaction fees\n` +
84
- `- **1 USDC** for basic plan (or more for paid plans)\n\n` +
85
- `Then call \`agenticSignup\` to complete account creation.`);
75
+ `**Saved to:** \`${KEYPAIR_PATH}\``);
86
76
  }
87
77
  catch (err) {
88
78
  return handleToolError(err, 'Error generating keypair');
89
79
  }
90
80
  });
91
- 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 () => {
92
- try {
93
- let address = getSessionWalletAddress();
94
- // Fall back to disk keypair if no session wallet
95
- if (!address) {
96
- const diskKey = loadKeypairFromDisk();
97
- if (diskKey) {
98
- const walletKeypair = loadKeypair(diskKey);
99
- address = await getAddress(walletKeypair);
100
- setSessionSecretKey(diskKey);
101
- setSessionWalletAddress(address);
102
- }
103
- }
104
- if (!address) {
105
- return mcpError('No signup wallet found. Call `generateKeypair` first to create a wallet.');
106
- }
107
- const solBalance = await checkSolBalance(address);
108
- const usdcBalance = await checkUsdcBalance(address);
109
- const solAmount = Number(solBalance) / 1_000_000_000;
110
- const usdcAmount = Number(usdcBalance) / 1_000_000;
111
- const solOk = solBalance >= 1000000n;
112
- const usdcOk = usdcBalance >= 1000000n; // 1 USDC for basic plan
113
- const funded = solOk && usdcOk;
114
- // Reset counter when balance is sufficient
115
- if (funded) {
116
- insufficientBalanceChecks = 0;
117
- return mcpText(`**Signup Wallet Balance** (\`${address}\`)\n\n` +
118
- `- **SOL:** ${solAmount.toFixed(6)} (sufficient)\n` +
119
- `- **USDC:** ${usdcAmount.toFixed(2)} (sufficient for basic)\n\n` +
120
- `**Status:** Ready for signup (basic plan). For paid plans, ensure sufficient USDC for the plan price.\n\n` +
121
- `Call \`agenticSignup\` to proceed.`);
122
- }
123
- // Insufficient — increment counter and escalate guidance
124
- insufficientBalanceChecks++;
125
- const missing = [];
126
- if (!solOk)
127
- missing.push(`~0.001 SOL (have ${solAmount.toFixed(6)})`);
128
- if (!usdcOk)
129
- missing.push(`1 USDC (have ${usdcAmount.toFixed(2)})`);
130
- let balanceBlock = `**Signup Wallet Balance** (\`${address}\`)\n\n` +
131
- `- **SOL:** ${solAmount.toFixed(6)} ${solOk ? '(sufficient)' : '(insufficient)'}\n` +
132
- `- **USDC:** ${usdcAmount.toFixed(2)} ${usdcOk ? '(sufficient for basic)' : '(insufficient)'}\n\n` +
133
- `**Status:** Need more funds: ${missing.join(', ')}`;
134
- if (insufficientBalanceChecks === 1) {
135
- // First check — normal guidance
136
- balanceBlock +=
137
- `\n\n**Action required:** Ask the user to send the missing funds to \`${address}\`. ` +
138
- `Do **not** call \`checkSignupBalance\` again until the user confirms they have sent the funds.`;
139
- }
140
- else if (insufficientBalanceChecks < MAX_BALANCE_CHECKS_BEFORE_STOP) {
141
- // Second check — firmer nudge
142
- balanceBlock +=
143
- `\n\n**⚠ Balance still insufficient (check ${insufficientBalanceChecks}/${MAX_BALANCE_CHECKS_BEFORE_STOP}).** ` +
144
- `The wallet has not been funded yet. Ask the user to confirm they have sent funds to \`${address}\` before calling this tool again.`;
145
- }
146
- else {
147
- // Third+ check — hard stop
148
- balanceBlock +=
149
- `\n\n**🛑 Balance checked ${insufficientBalanceChecks} times — still insufficient. Stop polling.** ` +
150
- `The wallet \`${address}\` has not received funds. ` +
151
- `Tell the user the exact amounts needed and the wallet address, then **wait for the user to explicitly confirm** they have sent funds before calling \`checkSignupBalance\` again. ` +
152
- `Do not retry automatically.`;
153
- }
154
- return mcpText(balanceBlock);
155
- }
156
- catch (err) {
157
- return handleToolError(err, 'Error checking balances');
158
- }
159
- });
160
- 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.', {
161
- plan: z.string().optional().describe('Plan to sign up for: "basic" ($1, default), "developer", "business", or "professional"'),
162
- period: z.enum(["monthly", "yearly"]).optional().describe('Billing period for paid plans (default: monthly)'),
163
- email: z.string().email().optional().describe('Email address (required for paid plans)'),
164
- firstName: z.string().optional().describe('First name (required for paid plans)'),
165
- lastName: z.string().optional().describe('Last name (required for paid plans)'),
166
- couponCode: z.string().optional().describe('Coupon code for paid plans'),
167
- discoveryPath: z.string().optional().describe('How did you discover Helius? Helps us improve agent onboarding'),
168
- frictionPoints: z.string().optional().describe('What friction did you hit finding or setting up Helius? Free-form feedback'),
169
- }, async ({ plan, period, email, firstName, lastName, couponCode, discoveryPath, frictionPoints }) => {
81
+ // ── Signup ──
82
+ server.tool('signup', 'Create a Helius account via crypto checkout. Requires `generateKeypair` to have been called first (the wallet address is bound to the payment intent in every mode). Default `mode: "link"` returns a hosted-checkout URL the user opens in a browser to pay USDC. `mode: "autopay"` sends USDC + memo directly from the local keypair. `mode: "resume"` polls a pending payment intent and provisions the API key after the user has paid in a browser. NOTE: resume strictly polls the JWT-bound project list and cannot distinguish "pending signup awaiting browser pay" from "stale JWT left over from a long-completed signup" — if the wallet already has an API key, prefer `setHeliusApiKey` over re-resuming.', {
83
+ mode: z
84
+ .enum(['link', 'autopay', 'resume'])
85
+ .default('link')
86
+ .describe('link (default): print payment URL and exit. autopay: send USDC + memo from local keypair. resume: poll an in-flight payment intent.'),
87
+ plan: z
88
+ .enum(['agent', 'developer', 'business', 'professional'])
89
+ .default('agent')
90
+ .describe('Plan: agent ($10 one-time, default), developer, business, professional'),
91
+ period: z
92
+ .enum(['monthly', 'yearly'])
93
+ .optional()
94
+ .describe('Billing period for subscription plans (default: monthly). Ignored for agent.'),
95
+ email: z.string().email().optional().describe('Email (required for fresh signup; not needed for resume)'),
96
+ firstName: z.string().optional().describe('First name (required for fresh signup)'),
97
+ lastName: z.string().optional().describe('Last name (required for fresh signup)'),
98
+ couponCode: z.string().optional().describe('Coupon code'),
99
+ discoveryPath: z.string().optional().describe('How did you discover Helius?'),
100
+ frictionPoints: z.string().optional().describe('What friction did you hit?'),
101
+ }, async ({ mode, plan, period, email, firstName, lastName, couponCode, discoveryPath, frictionPoints }) => {
170
102
  if (discoveryPath || frictionPoints) {
171
103
  sendFeedbackEvent({
172
104
  type: 'discovery',
@@ -175,69 +107,135 @@ export function registerAuthTools(server) {
175
107
  });
176
108
  }
177
109
  try {
110
+ // Resume mode: just poll the existing JWT/intent. Caller must have
111
+ // already gone through link mode in a previous invocation; the JWT
112
+ // from that invocation is on disk.
113
+ if (mode === 'resume') {
114
+ return await handleResumeSignup();
115
+ }
178
116
  let signerData;
179
117
  try {
180
118
  signerData = await loadSignerOrFail();
181
119
  }
182
120
  catch {
183
- return mcpError('No signup keypair found. Call `generateKeypair` first to create a wallet, fund it, then call this tool.');
184
- }
185
- const result = await agenticSignup({
121
+ return mcpError('No signup keypair found. Call `generateKeypair` first to create a wallet, then retry.', { type: 'AUTH', code: 'NO_KEYPAIR', retryable: false, recovery: 'Call `generateKeypair`.' });
122
+ }
123
+ if (mode === 'autopay') {
124
+ const result = await signupAndPay({
125
+ secretKey: signerData.secretKey,
126
+ plan: plan,
127
+ period,
128
+ email,
129
+ firstName,
130
+ lastName,
131
+ couponCode,
132
+ });
133
+ return renderSignupAndPayResult(result);
134
+ }
135
+ // mode === "link" (default)
136
+ const result = await signup({
186
137
  secretKey: signerData.secretKey,
187
- userAgent: MCP_USER_AGENT,
188
- plan,
138
+ plan: plan,
189
139
  period,
190
140
  email,
191
141
  firstName,
192
142
  lastName,
193
143
  couponCode,
194
144
  });
195
- // Configure API key for this session and persist to shared config
196
- if (result.apiKey) {
145
+ if (result.kind === 'already_subscribed') {
197
146
  setApiKey(result.apiKey);
198
147
  setSharedApiKey(result.apiKey);
199
- }
200
- // Persist JWT to disk
201
- if (result.jwt) {
202
148
  setJwt(result.jwt);
203
- }
204
- const saveNote = result.apiKey
205
- ? `\nAPI key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`. All Helius tools are now ready to use.`
206
- : '';
207
- if (result.status === 'existing_project') {
208
149
  return mcpText(`**Helius Account Found**\n\n` +
209
- `You already have a Helius account. No payment was needed.\n\n` +
210
- `- **Wallet:** \`${result.walletAddress}\`\n` +
150
+ `You already have a Helius account on this plan. No payment was needed.\n\n` +
211
151
  `- **Project ID:** \`${result.projectId}\`\n` +
212
- (result.apiKey ? `- **API Key:** \`${result.apiKey}\`\n` : '') +
213
- (result.endpoints ? `- **Mainnet RPC:** \`${result.endpoints.mainnet}\`\n` : '') +
214
- (result.endpoints ? `- **Devnet RPC:** \`${result.endpoints.devnet}\`\n` : '') +
215
- (result.credits !== null ? `- **Credits:** ${result.credits.toLocaleString()}\n` : '') +
216
- saveNote);
152
+ `- **API Key:** \`${result.apiKey}\`\n` +
153
+ `- **Mainnet RPC:** \`${result.endpoints.mainnet}\`\n` +
154
+ `- **Devnet RPC:** \`${result.endpoints.devnet}\`\n\n` +
155
+ `API key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`.`);
156
+ }
157
+ if (result.kind === 'upgrade_required') {
158
+ return mcpText(`**Plan change required**\n\n` +
159
+ `Your wallet is already on plan \`${result.currentPlan}\`. ` +
160
+ `Use the \`upgradePlan\` tool to switch to \`${result.requestedPlan}\`.`);
161
+ }
162
+ // payment_required — store JWT so resume mode can use it.
163
+ setJwt(result.jwt);
164
+ return mcpText(renderPaymentLink(result.paymentLink, 'Sign up', 'signup with mode: "resume"'));
165
+ }
166
+ catch (err) {
167
+ return handleToolError(err, 'Error during signup');
168
+ }
169
+ });
170
+ // ── Account Status ──
171
+ server.tool('getAccountStatus', 'Check your Helius account status: current plan, remaining credits, rate limits, and billing cycle. Requires a JWT session (i.e., you signed up via `signup`). If you only have an API key configured, auth status is confirmed but credit data is unavailable — call `signup` to enable full status.', {}, async () => {
172
+ try {
173
+ if (!hasApiKey()) {
174
+ return mcpText(`## Account Status\n\n` +
175
+ `**Auth:** Not authenticated\n\n` +
176
+ `No API key or session found. To get started:\n` +
177
+ `- If you have a key: use the \`setHeliusApiKey\` tool\n` +
178
+ `- If you need an account: use \`signup\``);
217
179
  }
218
- if (result.status === 'upgraded') {
219
- return mcpText(`**Plan Upgraded to ${plan || 'paid plan'}**\n\n` +
220
- `- **Wallet:** \`${result.walletAddress}\`\n` +
221
- `- **Project ID:** \`${result.projectId}\`\n` +
222
- (result.apiKey ? `- **API Key:** \`${result.apiKey}\`\n` : '') +
223
- (result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
224
- saveNote);
180
+ const jwt = getJwt();
181
+ if (!jwt) {
182
+ return mcpText(`## Account Status\n\n` +
183
+ `**Auth:** Authenticated (API key configured)\n` +
184
+ `**Credit usage:** Not available no JWT session found\n\n` +
185
+ `To see your plan, rate limits, and credit balance, call \`signup\`. Your existing account will be detected automatically — no payment needed.`);
225
186
  }
226
- return mcpText(`**Helius Account Created**\n\n` +
227
- `- **Wallet:** \`${result.walletAddress}\`\n` +
228
- `- **Project ID:** \`${result.projectId}\`\n` +
229
- (result.apiKey ? `- **API Key:** \`${result.apiKey}\`\n` : '') +
230
- (result.endpoints ? `- **Mainnet RPC:** \`${result.endpoints.mainnet}\`\n` : '') +
231
- (result.endpoints ? `- **Devnet RPC:** \`${result.endpoints.devnet}\`\n` : '') +
232
- (result.credits !== null ? `- **Credits:** ${result.credits.toLocaleString()}\n` : '') +
233
- (result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
234
- saveNote);
187
+ const projects = await listProjects(jwt, MCP_USER_AGENT);
188
+ if (projects.length === 0) {
189
+ return mcpError('No projects found. Call `signup` to create an account first.', { type: 'AUTH', code: 'NO_PROJECT', retryable: false, recovery: 'Call `signup`.' });
190
+ }
191
+ const projectId = projects[0].id;
192
+ const details = await getProject(jwt, projectId, MCP_USER_AGENT);
193
+ const planKey = details.subscriptionPlanDetails?.currentPlan ?? 'unknown';
194
+ const upcomingPlan = details.subscriptionPlanDetails?.upcomingPlan;
195
+ const isUpgrading = details.subscriptionPlanDetails?.isUpgrading ?? false;
196
+ const planInfo = HELIUS_PLANS[planKey];
197
+ const usage = details.creditsUsage;
198
+ const cycle = details.billingCycle;
199
+ const lines = [`## Account Status`, ''];
200
+ lines.push(`**Auth:** Authenticated`);
201
+ lines.push(`**Plan:** ${planInfo ? planInfo.name : planKey} | **Project:** \`${projectId}\``);
202
+ if (isUpgrading && upcomingPlan && upcomingPlan !== planKey) {
203
+ lines.push(`**Upcoming plan:** ${upcomingPlan} (takes effect at next billing cycle)`);
204
+ }
205
+ try {
206
+ const billingDoc = await fetchDoc('billing');
207
+ const rateLimits = extractSections(billingDoc, ['standard rate limits', 'rate limits'], {
208
+ includeLooseMatches: false,
209
+ });
210
+ if (rateLimits) {
211
+ lines.push('', '### Rate Limits (live)', '', rateLimits);
212
+ }
213
+ }
214
+ catch {
215
+ // best-effort
216
+ }
217
+ if (usage) {
218
+ const total = usage.remainingCredits + usage.totalCreditsUsed;
219
+ const pctUsed = total > 0 ? ((usage.totalCreditsUsed / total) * 100).toFixed(1) : '0.0';
220
+ const pctRemaining = total > 0 ? (100 - parseFloat(pctUsed)).toFixed(1) : '100.0';
221
+ let cycleStr = '';
222
+ if (cycle) {
223
+ const end = new Date(cycle.end);
224
+ const now = new Date();
225
+ const daysLeft = Math.ceil((end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
226
+ cycleStr = ` (${cycle.start} - ${cycle.end}, ${daysLeft} day${daysLeft !== 1 ? 's' : ''} remaining)`;
227
+ }
228
+ lines.push('', `### Credits — Billing Cycle${cycleStr}`);
229
+ lines.push(`- **Remaining:** ${usage.remainingCredits.toLocaleString()} / ${total.toLocaleString()} (${pctRemaining}%)`);
230
+ lines.push(`- **Used:** ${usage.totalCreditsUsed.toLocaleString()} (${pctUsed}%)`);
231
+ }
232
+ return mcpText(lines.join('\n'));
235
233
  }
236
234
  catch (err) {
237
- return handleToolError(err, 'Error during signup');
235
+ return handleToolError(err, 'Error fetching account status');
238
236
  }
239
237
  });
240
- // ── Upgrade, Preview, Renewal Tools ──
238
+ // ── Upgrade ──
241
239
  server.tool('previewUpgrade', 'Preview pricing for a plan upgrade with proration details. Shows current plan, new plan cost, prorated credits, and amount due today.', {
242
240
  plan: z.enum(['developer', 'business', 'professional']).describe('Target plan name'),
243
241
  period: z.enum(['monthly', 'yearly']).default('monthly').describe('Billing period'),
@@ -246,11 +244,21 @@ export function registerAuthTools(server) {
246
244
  try {
247
245
  const jwt = getJwt();
248
246
  if (!jwt) {
249
- return mcpError('Not authenticated. Call `agenticSignup` or authenticate first.');
247
+ return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
248
+ type: 'AUTH',
249
+ code: 'NOT_AUTHENTICATED',
250
+ retryable: false,
251
+ recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
252
+ });
250
253
  }
251
254
  const projects = await listProjects(jwt, MCP_USER_AGENT);
252
255
  if (projects.length === 0) {
253
- return mcpError('No projects found. Call `agenticSignup` to create an account first.');
256
+ return mcpError('No projects found. Call `signup` to create an account first.', {
257
+ type: 'AUTH',
258
+ code: 'NO_PROJECT',
259
+ retryable: false,
260
+ recovery: 'Call `signup`.',
261
+ });
254
262
  }
255
263
  const projectId = projects[0].id;
256
264
  const projectDetails = await getProject(jwt, projectId, MCP_USER_AGENT);
@@ -273,13 +281,7 @@ export function registerAuthTools(server) {
273
281
  if (preview.coupon?.valid) {
274
282
  text += `- Coupon (${preview.coupon.code}): ${preview.coupon.description || 'Applied'}\n`;
275
283
  }
276
- else if (preview.coupon && !preview.coupon.valid) {
277
- text += `- Coupon (${preview.coupon.code}): **Invalid** — ${preview.coupon.invalidReason || 'not applicable'}\n`;
278
- }
279
284
  text += `\n**Due Today: $${(preview.dueToday / 100).toFixed(2)}**\n`;
280
- if (preview.note) {
281
- text += `\n_${preview.note}_\n`;
282
- }
283
285
  text += `\nTo proceed, use the \`upgradePlan\` tool with plan: '${plan}'.`;
284
286
  return mcpText(text);
285
287
  }
@@ -287,204 +289,275 @@ export function registerAuthTools(server) {
287
289
  return handleToolError(err, 'Error previewing upgrade');
288
290
  }
289
291
  });
290
- 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.', {
292
+ server.tool('upgradePlan', 'Upgrade your Helius plan via crypto checkout. Default `mode: "link"` returns a hosted-checkout URL. `mode: "autopay"` pays USDC from the local keypair and polls. Contact info (email/firstName/lastName) is only needed for first-time upgrades — backend auto-fetches from existing customer otherwise.', {
293
+ mode: z.enum(['link', 'autopay']).default('link').describe('link (default) or autopay'),
291
294
  plan: z.enum(['developer', 'business', 'professional']).describe('Target plan name'),
292
295
  period: z.enum(['monthly', 'yearly']).default('monthly').describe('Billing period'),
293
296
  couponCode: z.string().optional().describe('Optional coupon code'),
294
- email: z.string().email().optional().describe('Email address (required for first-time upgrades)'),
295
- firstName: z.string().optional().describe('First name (required for first-time upgrades)'),
296
- lastName: z.string().optional().describe('Last name (required for first-time upgrades)'),
297
- }, async ({ plan, period, couponCode, email, firstName, lastName }) => {
297
+ email: z.string().email().optional().describe('Email (only for first-time upgrades)'),
298
+ firstName: z.string().optional().describe('First name (only for first-time upgrades)'),
299
+ lastName: z.string().optional().describe('Last name (only for first-time upgrades)'),
300
+ }, async ({ mode, plan, period, couponCode, email, firstName, lastName }) => {
298
301
  try {
299
- // All-or-none customer info validation
300
- const hasAny = email || firstName || lastName;
301
- if (hasAny && (!email || !firstName || !lastName)) {
302
- const missing = [
303
- !email && 'email',
304
- !firstName && 'firstName',
305
- !lastName && 'lastName',
306
- ].filter(Boolean);
307
- return mcpError(`Partial customer info provided. If any of email/firstName/lastName is given, all three are required. Missing: ${missing.join(', ')}`);
308
- }
309
- let signerData;
310
- try {
311
- signerData = await loadSignerOrFail();
312
- }
313
- catch {
314
- return mcpError('No keypair found. Call `generateKeypair` first.');
315
- }
316
302
  const jwt = getJwt();
317
303
  if (!jwt) {
318
- return mcpError('Not authenticated. Call `agenticSignup` or authenticate first.');
304
+ return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
305
+ type: 'AUTH',
306
+ code: 'NOT_AUTHENTICATED',
307
+ retryable: false,
308
+ recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
309
+ });
319
310
  }
320
311
  const projects = await listProjects(jwt, MCP_USER_AGENT);
321
312
  if (projects.length === 0) {
322
- return mcpError('No projects found. Call `agenticSignup` to create an account first.');
313
+ return mcpError('No projects found. Call `signup` first.', {
314
+ type: 'AUTH',
315
+ code: 'NO_PROJECT',
316
+ retryable: false,
317
+ recovery: 'Call `signup`.',
318
+ });
323
319
  }
324
320
  const projectId = projects[0].id;
325
- const result = await executeCheckout(signerData.secretKey, jwt, {
321
+ if (mode === 'autopay') {
322
+ let signerData;
323
+ try {
324
+ signerData = await loadSignerOrFail();
325
+ }
326
+ catch {
327
+ return mcpError('No keypair. Autopay needs one — call `generateKeypair` first.', {
328
+ type: 'AUTH',
329
+ code: 'NO_KEYPAIR',
330
+ retryable: false,
331
+ recovery: 'Call `generateKeypair`.',
332
+ });
333
+ }
334
+ const result = await upgradePlanAndPay({
335
+ secretKey: signerData.secretKey,
336
+ jwt,
337
+ projectId,
338
+ plan,
339
+ period,
340
+ couponCode,
341
+ email,
342
+ firstName,
343
+ lastName,
344
+ });
345
+ return renderUpgradeOrCreditsAndPayResult(result, 'Upgrade');
346
+ }
347
+ // mode === "link"
348
+ const result = await upgradePlan({
349
+ jwt,
350
+ projectId,
326
351
  plan,
327
352
  period,
328
- refId: projectId,
329
353
  couponCode,
330
354
  email,
331
355
  firstName,
332
356
  lastName,
333
- }, MCP_USER_AGENT, { skipProjectPolling: true });
334
- if (result.status !== 'completed') {
335
- return mcpError(`**Upgrade ${result.status}**\n\n` +
336
- (result.error ? `Error: ${result.error}\n` : '') +
337
- (result.txSignature ? `TX: \`${result.txSignature}\`\n` : '') +
338
- `\nIf you need help, contact support with the payment intent ID: \`${result.paymentIntentId}\``);
339
- }
340
- const planInfo = PLAN_CATALOG[plan];
341
- return mcpText(`**Plan Upgraded Successfully**\n\n` +
342
- `- **New Plan:** ${planInfo.name} (${period})\n` +
343
- `- **Project ID:** \`${projectId}\`\n` +
344
- (result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
345
- `\nYour new plan is now active with ${(planInfo.credits / 1_000_000).toFixed(0)}M credits and ${planInfo.requestsPerSecond} RPS.`);
357
+ });
358
+ return mcpText(renderPaymentLink(result.paymentLink, 'Upgrade', 'getAccountStatus'));
346
359
  }
347
360
  catch (err) {
348
361
  return handleToolError(err, 'Error upgrading plan');
349
362
  }
350
363
  });
351
- server.tool('getAccountStatus', 'Check your Helius account status: current plan, remaining credits, rate limits, and billing cycle. ' +
352
- 'Call this before bulk operations to verify you have sufficient credits. ' +
353
- 'Requires a JWT session (i.e., you signed up via agenticSignup). ' +
354
- 'If you only have an API key configured, auth status is confirmed but credit data is unavailable — call agenticSignup to enable full status.', {}, async () => {
364
+ // ── Prepaid Credits ──
365
+ server.tool('purchaseCredits', 'Buy prepaid credits as a one-time USDC top-up. AGENT PLAN ONLY: each unit of `qty` is 1,000,000 credits at $10 USDC, no recurring sub. Subscription plans (Developer / Business / Professional) get monthly allotments and overage is auto-billed at $5/M on the next invoice — they can NOT use this tool; this tool will reject the call with UNSUPPORTED_PLAN. Default `mode: "link"` returns a hosted-checkout URL; `mode: "autopay"` pays USDC from the local keypair and polls.', {
366
+ mode: z.enum(['link', 'autopay']).default('link').describe('link (default) or autopay'),
367
+ qty: z.number().int().min(1).default(1).describe('Quantity multiplier (each unit = 1M credits at $10 USDC)'),
368
+ couponCode: z.string().optional().describe('Optional coupon code'),
369
+ }, async ({ mode, qty, couponCode }) => {
355
370
  try {
356
- // ── Tier 1: not authenticated at all ──
357
- if (!hasApiKey()) {
358
- return mcpText(`## Account Status\n\n` +
359
- `**Auth:** Not authenticated\n\n` +
360
- `No API key or session found. To get started:\n` +
361
- `- If you have a key: use the \`setHeliusApiKey\` tool\n` +
362
- `- If you need an account: use \`generateKeypair\` → fund wallet → \`agenticSignup\``);
363
- }
364
- // ── Tier 2: API key present but no JWT — can't reach dashboard API ──
365
371
  const jwt = getJwt();
366
372
  if (!jwt) {
367
- return mcpText(`## Account Status\n\n` +
368
- `**Auth:** Authenticated (API key configured)\n` +
369
- `**Credit usage:** Not available — no JWT session found\n\n` +
370
- `To see your plan, rate limits, and credit balance, call \`agenticSignup\`.\n` +
371
- `Your existing account will be detected automatically no payment needed.`);
373
+ return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
374
+ type: 'AUTH',
375
+ code: 'NOT_AUTHENTICATED',
376
+ retryable: false,
377
+ recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
378
+ });
372
379
  }
373
- // ── Tier 3: full status via JWT ──
374
380
  const projects = await listProjects(jwt, MCP_USER_AGENT);
375
381
  if (projects.length === 0) {
376
- return mcpError('No projects found. Call `agenticSignup` to create an account first.');
382
+ return mcpError('No projects found. Call `signup` first.', {
383
+ type: 'AUTH',
384
+ code: 'NO_PROJECT',
385
+ retryable: false,
386
+ recovery: 'Call `signup`.',
387
+ });
377
388
  }
378
389
  const projectId = projects[0].id;
379
- const details = await getProject(jwt, projectId, MCP_USER_AGENT);
380
- const planKey = details.subscriptionPlanDetails?.currentPlan ?? 'unknown';
381
- const upcomingPlan = details.subscriptionPlanDetails?.upcomingPlan;
382
- const isUpgrading = details.subscriptionPlanDetails?.isUpgrading ?? false;
383
- const planInfo = HELIUS_PLANS[planKey];
384
- const usage = details.creditsUsage;
385
- const cycle = details.billingCycle;
386
- const lines = [`## Account Status`, ''];
387
- // ── Auth + plan ──
388
- lines.push(`**Auth:** Authenticated`);
389
- lines.push(`**Plan:** ${planInfo ? planInfo.name : planKey} | **Project:** \`${projectId}\``);
390
- if (isUpgrading && upcomingPlan && upcomingPlan !== planKey) {
391
- lines.push(`**Upcoming plan:** ${upcomingPlan} (takes effect at next billing cycle)`);
392
- }
393
- // ── Rate limits (fetched live from billing docs) ──
394
- try {
395
- const billingDoc = await fetchDoc('billing');
396
- const rateLimits = extractSections(billingDoc, ['standard rate limits', 'rate limits'], { includeLooseMatches: false });
397
- if (rateLimits) {
398
- lines.push('', '### Rate Limits (live)', '', rateLimits);
390
+ // Pre-flight plan check. SDK will reject non-Agent plans, but its
391
+ // error path surfaces as a generic SDK_ERROR; intercepting here gives
392
+ // the agent a clean, structured error with the correct semantics.
393
+ const projectPlan = projects[0].subscription?.plan;
394
+ const normalizedPlan = projectPlan?.replace(/_v\d+$/, '');
395
+ if (normalizedPlan && normalizedPlan !== 'agent') {
396
+ return mcpError(`Prepaid credits via \`purchaseCredits\` are only available on the Agent plan. ` +
397
+ `Project ${projectId} is on the ${normalizedPlan} plan, where credit overage ` +
398
+ `is auto-billed at $5 per 1,000,000 credits on the next invoice — there's no ` +
399
+ `manual top-up flow. To preview your usage, call \`getAccountStatus\`.`, {
400
+ type: 'UNSUPPORTED',
401
+ code: 'UNSUPPORTED_PLAN',
402
+ retryable: false,
403
+ recovery: 'Use the dashboard to manage subscription overage, or call `getAccountStatus` to inspect usage.',
404
+ });
405
+ }
406
+ if (mode === 'autopay') {
407
+ let signerData;
408
+ try {
409
+ signerData = await loadSignerOrFail();
399
410
  }
400
- else {
401
- lines.push('', '_Rate limit details: use `getRateLimitInfo` or visit https://www.helius.dev/docs/billing_');
411
+ catch {
412
+ return mcpError('No keypair. Autopay needs one — call `generateKeypair` first.', {
413
+ type: 'AUTH',
414
+ code: 'NO_KEYPAIR',
415
+ retryable: false,
416
+ recovery: 'Call `generateKeypair`.',
417
+ });
402
418
  }
403
- }
404
- catch {
405
- lines.push('', '_Rate limit details: use `getRateLimitInfo` or visit https://www.helius.dev/docs/billing_');
406
- }
407
- // ── Credits ──
408
- if (usage) {
409
- const total = usage.remainingCredits + usage.totalCreditsUsed;
410
- const pctUsed = total > 0 ? ((usage.totalCreditsUsed / total) * 100).toFixed(1) : '0.0';
411
- const pctRemaining = total > 0 ? (100 - parseFloat(pctUsed)).toFixed(1) : '100.0';
412
- // Billing cycle + days remaining
413
- let cycleStr = '';
414
- let daysNote = '';
415
- if (cycle) {
416
- const end = new Date(cycle.end);
417
- const now = new Date();
418
- const daysLeft = Math.ceil((end.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
419
- cycleStr = ` (${cycle.start} - ${cycle.end}, ${daysLeft} day${daysLeft !== 1 ? 's' : ''} remaining)`;
420
- // Burn-rate warning: project usage over elapsed days to end of cycle
421
- const start = new Date(cycle.start);
422
- const totalDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
423
- const elapsedDays = totalDays - daysLeft;
424
- if (elapsedDays > 0 && daysLeft > 0) {
425
- const projectedTotal = Math.round((usage.totalCreditsUsed / elapsedDays) * totalDays);
426
- if (projectedTotal > total) {
427
- const overageM = ((projectedTotal - total) / 1_000_000).toFixed(1);
428
- 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.`;
429
- }
430
- }
431
- }
432
- lines.push('', `### Credits — Billing Cycle${cycleStr}`);
433
- lines.push(`- **Remaining:** ${usage.remainingCredits.toLocaleString()} / ${total.toLocaleString()} (${pctRemaining}%)`);
434
- lines.push(`- **Used:** ${usage.totalCreditsUsed.toLocaleString()} (${pctUsed}%)`);
435
- lines.push(` - API: ${usage.apiUsage.toLocaleString()}`);
436
- lines.push(` - RPC: ${usage.rpcUsage.toLocaleString()} | RPC GPA: ${usage.rpcGPAUsage.toLocaleString()} | Webhooks: ${usage.webhookUsage.toLocaleString()}`);
437
- if (usage.overageCreditsUsed > 0) {
438
- lines.push(`- **Overage:** ${usage.overageCreditsUsed.toLocaleString()} credits ($${usage.overageCost.toFixed(2)})`);
439
- }
440
- else {
441
- lines.push(`- **Overage:** none`);
442
- }
443
- if (usage.remainingPrepaidCredits > 0 || usage.prepaidCreditsUsed > 0) {
444
- lines.push(`- **Prepaid:** ${usage.remainingPrepaidCredits.toLocaleString()} remaining (${usage.prepaidCreditsUsed.toLocaleString()} used)`);
445
- }
446
- if (daysNote)
447
- lines.push(daysNote);
448
- // Low-credit warning
449
- if (parseFloat(pctRemaining) < 20) {
450
- lines.push(`\n> Less than 20% of credits remaining. Use \`previewUpgrade\` to see upgrade pricing, or \`getHeliusPlanInfo\` to compare plans.`);
451
- }
452
- }
453
- return mcpText(lines.join('\n'));
419
+ const result = await purchaseCreditsAndPay({
420
+ secretKey: signerData.secretKey,
421
+ jwt,
422
+ projectId,
423
+ qty,
424
+ couponCode,
425
+ });
426
+ return renderUpgradeOrCreditsAndPayResult(result, 'Credits top-up');
427
+ }
428
+ const result = await sdkPurchaseCredits({ jwt, projectId, qty, couponCode });
429
+ return mcpText(renderPaymentLink(result.paymentLink, 'Credits top-up', 'getAccountStatus'));
454
430
  }
455
431
  catch (err) {
456
- return handleToolError(err, 'Error fetching account status');
432
+ return handleToolError(err, 'Error purchasing credits');
457
433
  }
458
434
  });
459
- server.tool('payRenewal', 'Pay an existing payment intent (e.g., from a renewal notification). Fetches intent details, validates, and processes USDC payment.', {
435
+ // ── Renewal Pay ──
436
+ server.tool('payRenewal', 'Pay an existing renewal payment intent. Default `mode: "link"` returns the hosted-checkout URL for the renewal. `mode: "autopay"` pays USDC from the local keypair.', {
437
+ mode: z.enum(['link', 'autopay']).default('link').describe('link (default) or autopay'),
460
438
  paymentIntentId: z.string().describe('Payment intent ID from renewal notification'),
461
- }, async ({ paymentIntentId }) => {
439
+ }, async ({ mode, paymentIntentId }) => {
462
440
  try {
463
- let signerData;
464
- try {
465
- signerData = await loadSignerOrFail();
466
- }
467
- catch {
468
- return mcpError('No keypair found. Call `generateKeypair` first.');
469
- }
470
441
  const jwt = getJwt();
471
442
  if (!jwt) {
472
- return mcpError('Not authenticated. Call `agenticSignup` or authenticate first.');
473
- }
474
- const result = await executeRenewal(signerData.secretKey, jwt, paymentIntentId, MCP_USER_AGENT);
475
- if (result.status !== 'completed') {
476
- return mcpError(`**Payment ${result.status}**\n\n` +
477
- (result.error ? `Error: ${result.error}\n` : '') +
478
- (result.txSignature ? `TX: \`${result.txSignature}\`\n` : '') +
479
- `\nIf you need help, contact support with the payment intent ID: \`${result.paymentIntentId}\``);
443
+ return mcpError('Not authenticated. Call `signup`, or run `helius login` in your terminal if you have a dashboard.helius.dev account.', {
444
+ type: 'AUTH',
445
+ code: 'NOT_AUTHENTICATED',
446
+ retryable: false,
447
+ recovery: 'Call `signup`, or run `helius login` in your terminal if you already have a dashboard.helius.dev account.',
448
+ });
449
+ }
450
+ if (mode === 'autopay') {
451
+ let signerData;
452
+ try {
453
+ signerData = await loadSignerOrFail();
454
+ }
455
+ catch {
456
+ return mcpError('No keypair. Autopay needs one — call `generateKeypair` first.', {
457
+ type: 'AUTH',
458
+ code: 'NO_KEYPAIR',
459
+ retryable: false,
460
+ recovery: 'Call `generateKeypair`.',
461
+ });
462
+ }
463
+ const result = await payRenewalAndPay(signerData.secretKey, jwt, paymentIntentId);
464
+ return renderUpgradeOrCreditsAndPayResult(result, 'Renewal');
480
465
  }
481
- return mcpText(`**Payment Complete**\n\n` +
482
- `- **Payment Intent:** \`${result.paymentIntentId}\`\n` +
483
- (result.txSignature ? `- **TX:** \`${result.txSignature}\`\n` : '') +
484
- `\nYour subscription has been renewed successfully.`);
466
+ const result = await payRenewal(jwt, paymentIntentId);
467
+ return mcpText(renderPaymentLink(result.paymentLink, 'Renewal', 'payRenewal again with mode: "autopay" or open the link'));
485
468
  }
486
469
  catch (err) {
487
470
  return handleToolError(err, 'Error processing renewal payment');
488
471
  }
489
472
  });
490
473
  }
474
+ // ────────────────────────────────────────────────────────────────────────────
475
+ // Resume / result rendering helpers
476
+ // ────────────────────────────────────────────────────────────────────────────
477
+ async function handleResumeSignup() {
478
+ const jwt = getJwt();
479
+ if (!jwt) {
480
+ return mcpError('No pending signup. Call `signup` (link mode) first.', {
481
+ type: 'AUTH',
482
+ code: 'NOT_AUTHENTICATED',
483
+ retryable: false,
484
+ recovery: 'Call `signup`.',
485
+ });
486
+ }
487
+ const projects = await listProjects(jwt, MCP_USER_AGENT);
488
+ if (projects.length === 0) {
489
+ return mcpText('Payment not yet provisioned on the backend. The webhook may still be processing — try again in a moment.');
490
+ }
491
+ const projectId = projects[0].id;
492
+ const details = await getProject(jwt, projectId, MCP_USER_AGENT);
493
+ const apiKey = details.apiKeys?.[0]?.keyId;
494
+ if (!apiKey) {
495
+ return mcpText('Project is provisioned but no API key has been generated yet. Try again in a moment.');
496
+ }
497
+ setApiKey(apiKey);
498
+ setSharedApiKey(apiKey);
499
+ return mcpText(`**Signup complete!**\n\n` +
500
+ `- **Project ID:** \`${projectId}\`\n` +
501
+ `- **API Key:** \`${apiKey}\`\n\n` +
502
+ `API key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`. All Helius tools are ready to use.`);
503
+ }
504
+ function renderSignupAndPayResult(result) {
505
+ switch (result.kind) {
506
+ case 'completed':
507
+ setApiKey(result.apiKey);
508
+ setSharedApiKey(result.apiKey);
509
+ setJwt(result.jwt);
510
+ return mcpText(`**Helius Account Created**\n\n` +
511
+ `- **Project ID:** \`${result.projectId}\`\n` +
512
+ `- **API Key:** \`${result.apiKey}\`\n` +
513
+ `- **Mainnet RPC:** \`${result.endpoints.mainnet}\`\n` +
514
+ (result.txSignature ? `- **Payment TX:** \`${result.txSignature}\`\n` : '') +
515
+ `\nAPI key configured for this session and saved to \`${SHARED_CONFIG_PATH}\`.`);
516
+ case 'already_subscribed':
517
+ setApiKey(result.apiKey);
518
+ setSharedApiKey(result.apiKey);
519
+ setJwt(result.jwt);
520
+ return mcpText(`**Helius Account Found**\n\n` +
521
+ `You already have a Helius account on this plan. No payment was needed.\n\n` +
522
+ `- **Project ID:** \`${result.projectId}\`\n` +
523
+ `- **API Key:** \`${result.apiKey}\`\n` +
524
+ `\nAPI key configured for this session.`);
525
+ case 'upgrade_required':
526
+ return mcpText(`**Plan change required**\n\n` +
527
+ `Your wallet is already on plan \`${result.currentPlan}\`. ` +
528
+ `Use the \`upgradePlan\` tool to switch to \`${result.requestedPlan}\`.`);
529
+ case 'pending':
530
+ setJwt(result.jwt);
531
+ return mcpText(`**Payment sent — activation pending**\n\n` +
532
+ `USDC was sent (tx: \`${result.txSignature ?? 'unknown'}\`) but the backend hasn't finished provisioning yet.\n\n` +
533
+ `Call \`signup\` with \`mode: "resume"\` to try again.`);
534
+ case 'expired':
535
+ return mcpError(`Payment intent expired (\`${result.paymentIntentId}\`). Run \`signup\` again to start a fresh one.`, { type: 'API', code: 'EXPIRED', retryable: false, recovery: 'Run `signup` again.' });
536
+ case 'failed':
537
+ return mcpError(`Payment failed: ${result.reason ?? 'unknown reason'} (intent \`${result.paymentIntentId}\`).`, { type: 'API', code: 'OPERATION_FAILED', retryable: false, recovery: 'Contact support if this persists.' });
538
+ default: {
539
+ const _exhaustive = result;
540
+ throw new Error(`Unhandled result kind: ${JSON.stringify(_exhaustive)}`);
541
+ }
542
+ }
543
+ }
544
+ function renderUpgradeOrCreditsAndPayResult(result, flowName) {
545
+ switch (result.kind) {
546
+ case 'completed':
547
+ return mcpText(`**${flowName} complete!**\n\n` +
548
+ (result.txSignature ? `- **TX:** \`${result.txSignature}\`\n` : '') +
549
+ `- **Payment Intent:** \`${result.paymentIntentId}\``);
550
+ case 'pending':
551
+ return mcpText(`**Payment sent — activation pending**\n\n` +
552
+ `USDC was sent (tx: \`${result.txSignature ?? 'unknown'}\`). The backend is still processing.\n\n` +
553
+ `Call this tool again or check \`getAccountStatus\` shortly.`);
554
+ case 'expired':
555
+ return mcpError(`Payment intent expired (\`${result.paymentIntentId}\`). Run the tool again to start fresh.`, { type: 'API', code: 'EXPIRED', retryable: false, recovery: 'Retry from scratch.' });
556
+ case 'failed':
557
+ return mcpError(`Payment failed: ${result.reason ?? 'unknown reason'} (intent \`${result.paymentIntentId}\`).`, { type: 'API', code: 'OPERATION_FAILED', retryable: false, recovery: 'Contact support if this persists.' });
558
+ default: {
559
+ const _exhaustive = result;
560
+ throw new Error(`Unhandled result kind: ${JSON.stringify(_exhaustive)}`);
561
+ }
562
+ }
563
+ }