helius-mcp 1.2.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 (94) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +42 -30
  3. package/dist/http.d.ts +1 -1
  4. package/dist/index.js +2 -56
  5. package/dist/results/store.d.ts +8 -0
  6. package/dist/results/store.js +72 -0
  7. package/dist/results/types.d.ts +47 -0
  8. package/dist/results/types.js +1 -0
  9. package/dist/router/action-groups.d.ts +6 -0
  10. package/dist/router/action-groups.js +32 -0
  11. package/dist/router/action-handlers.d.ts +20 -0
  12. package/dist/router/action-handlers.js +125 -0
  13. package/dist/router/actions.d.ts +12 -0
  14. package/dist/router/actions.js +123 -0
  15. package/dist/router/catalog.d.ts +6 -0
  16. package/dist/router/catalog.js +388 -0
  17. package/dist/router/context.d.ts +5 -0
  18. package/dist/router/context.js +10 -0
  19. package/dist/router/dispatch.d.ts +4 -0
  20. package/dist/router/dispatch.js +276 -0
  21. package/dist/router/instructions.d.ts +1 -0
  22. package/dist/router/instructions.js +25 -0
  23. package/dist/router/register.d.ts +2 -0
  24. package/dist/router/register.js +15 -0
  25. package/dist/router/required-params.d.ts +9 -0
  26. package/dist/router/required-params.js +66 -0
  27. package/dist/router/responses.d.ts +29 -0
  28. package/dist/router/responses.js +186 -0
  29. package/dist/router/schemas.d.ts +216 -0
  30. package/dist/router/schemas.js +195 -0
  31. package/dist/router/telemetry.d.ts +27 -0
  32. package/dist/router/telemetry.js +52 -0
  33. package/dist/router/types.d.ts +46 -0
  34. package/dist/router/types.js +1 -0
  35. package/dist/scripts/validate-catalog.d.ts +2 -2
  36. package/dist/scripts/validate-catalog.js +10 -10
  37. package/dist/tools/accounts.js +5 -5
  38. package/dist/tools/assets.js +5 -5
  39. package/dist/tools/auth.js +392 -288
  40. package/dist/tools/config.js +3 -3
  41. package/dist/tools/das-extras.js +6 -6
  42. package/dist/tools/docs.js +55 -41
  43. package/dist/tools/enhanced-websockets.js +13 -13
  44. package/dist/tools/fees.js +3 -3
  45. package/dist/tools/index.d.ts +1 -1
  46. package/dist/tools/index.js +2 -80
  47. package/dist/tools/laserstream.js +20 -23
  48. package/dist/tools/network.js +41 -2
  49. package/dist/tools/plans.d.ts +0 -5
  50. package/dist/tools/plans.js +167 -12
  51. package/dist/tools/product-catalog.d.ts +1 -0
  52. package/dist/tools/product-catalog.js +51 -16
  53. package/dist/tools/recommend.d.ts +0 -1
  54. package/dist/tools/recommend.js +9 -28
  55. package/dist/tools/shared.d.ts +1 -0
  56. package/dist/tools/shared.js +10 -2
  57. package/dist/tools/solana-knowledge.js +23 -7
  58. package/dist/tools/staking.d.ts +2 -0
  59. package/dist/tools/staking.js +268 -0
  60. package/dist/tools/transactions.js +167 -3
  61. package/dist/tools/transfers.js +38 -43
  62. package/dist/tools/wallet.js +27 -16
  63. package/dist/tools/webhooks.js +3 -3
  64. package/dist/tools/zk-compression.d.ts +2 -0
  65. package/dist/tools/zk-compression.js +781 -0
  66. package/dist/utils/config.d.ts +2 -2
  67. package/dist/utils/config.js +68 -6
  68. package/dist/utils/errors.d.ts +10 -1
  69. package/dist/utils/errors.js +46 -12
  70. package/dist/utils/feedback.js +1 -4
  71. package/dist/utils/helius.js +2 -1
  72. package/dist/utils/ows.d.ts +74 -0
  73. package/dist/utils/ows.js +155 -0
  74. package/dist/version.d.ts +1 -1
  75. package/dist/version.js +1 -1
  76. package/package.json +2 -2
  77. package/system-prompts/helius/claude.system.md +56 -25
  78. package/system-prompts/helius/full.md +474 -130
  79. package/system-prompts/helius/openai.developer.md +56 -25
  80. package/system-prompts/helius-dflow/claude.system.md +41 -6
  81. package/system-prompts/helius-dflow/full.md +581 -92
  82. package/system-prompts/helius-dflow/openai.developer.md +41 -6
  83. package/system-prompts/helius-jupiter/claude.system.md +333 -0
  84. package/system-prompts/helius-jupiter/full.md +5109 -0
  85. package/system-prompts/helius-jupiter/openai.developer.md +333 -0
  86. package/system-prompts/helius-okx/claude.system.md +182 -0
  87. package/system-prompts/helius-okx/full.md +584 -0
  88. package/system-prompts/helius-okx/openai.developer.md +182 -0
  89. package/system-prompts/helius-phantom/claude.system.md +15 -2
  90. package/system-prompts/helius-phantom/full.md +254 -101
  91. package/system-prompts/helius-phantom/openai.developer.md +15 -2
  92. package/system-prompts/svm/claude.system.md +1 -0
  93. package/system-prompts/svm/full.md +1 -0
  94. package/system-prompts/svm/openai.developer.md +1 -0
@@ -2,53 +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'];
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.`;
19
29
  export function registerAuthTools(server) {
20
- // ── Getting Started Guide ──
30
+ // ── Getting Started ──
21
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 () => {
22
32
  const lines = ['# Getting Started with Helius'];
23
33
  const apiKeyConfigured = hasApiKey();
24
34
  const hasKeypair = keypairExistsOnDisk();
25
35
  const jwt = getJwt();
26
- // ── Already fully set up ──
36
+ // Already fully set up
27
37
  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.');
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.');
29
39
  return mcpText(lines.join('\n'));
30
40
  }
31
- // ── API key set but no JWT (e.g., set via env or setHeliusApiKey) ──
41
+ // API key set but no JWT
32
42
  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`.');
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.');
34
44
  return mcpText(lines.join('\n'));
35
45
  }
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
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.', '');
39
48
  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.', '');
49
+ lines.push(`_(A keypair already exists at \`${KEYPAIR_PATH}\` \`signup\` will reuse it; you can skip step 1.)_`, '');
41
50
  }
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
51
  return mcpText(lines.join('\n'));
47
52
  });
48
53
  // ── 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 () => {
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 () => {
50
55
  try {
51
- // Check disk first — reuse existing keypair if available
52
56
  const existingKey = loadKeypairFromDisk();
53
57
  if (existingKey) {
54
58
  const walletKeypair = loadKeypair(existingKey);
@@ -57,13 +61,8 @@ export function registerAuthTools(server) {
57
61
  setSessionWalletAddress(address);
58
62
  captureWalletAddress(address);
59
63
  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.`);
64
+ `**Wallet Address:** \`${address}\``);
65
65
  }
66
- // Generate new keypair and persist to disk
67
66
  const keypair = await generateKeypair();
68
67
  const walletKeypair = loadKeypair(keypair.secretKey);
69
68
  const address = await getAddress(walletKeypair);
@@ -73,69 +72,33 @@ export function registerAuthTools(server) {
73
72
  captureWalletAddress(address);
74
73
  return mcpText(`**Keypair Generated**\n\n` +
75
74
  `**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.`);
75
+ `**Saved to:** \`${KEYPAIR_PATH}\``);
81
76
  }
82
77
  catch (err) {
83
78
  return handleToolError(err, 'Error generating keypair');
84
79
  }
85
80
  });
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 }) => {
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 }) => {
139
102
  if (discoveryPath || frictionPoints) {
140
103
  sendFeedbackEvent({
141
104
  type: 'discovery',
@@ -144,69 +107,135 @@ export function registerAuthTools(server) {
144
107
  });
145
108
  }
146
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
+ }
147
116
  let signerData;
148
117
  try {
149
118
  signerData = await loadSignerOrFail();
150
119
  }
151
120
  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({
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({
155
137
  secretKey: signerData.secretKey,
156
- userAgent: MCP_USER_AGENT,
157
- plan,
138
+ plan: plan,
158
139
  period,
159
140
  email,
160
141
  firstName,
161
142
  lastName,
162
143
  couponCode,
163
144
  });
164
- // Configure API key for this session and persist to shared config
165
- if (result.apiKey) {
145
+ if (result.kind === 'already_subscribed') {
166
146
  setApiKey(result.apiKey);
167
147
  setSharedApiKey(result.apiKey);
168
- }
169
- // Persist JWT to disk
170
- if (result.jwt) {
171
148
  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
149
  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` +
150
+ `You already have a Helius account on this plan. No payment was needed.\n\n` +
180
151
  `- **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);
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\``);
186
179
  }
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);
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.`);
194
186
  }
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);
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'));
204
233
  }
205
234
  catch (err) {
206
- return handleToolError(err, 'Error during signup');
235
+ return handleToolError(err, 'Error fetching account status');
207
236
  }
208
237
  });
209
- // ── Upgrade, Preview, Renewal Tools ──
238
+ // ── Upgrade ──
210
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.', {
211
240
  plan: z.enum(['developer', 'business', 'professional']).describe('Target plan name'),
212
241
  period: z.enum(['monthly', 'yearly']).default('monthly').describe('Billing period'),
@@ -215,11 +244,21 @@ export function registerAuthTools(server) {
215
244
  try {
216
245
  const jwt = getJwt();
217
246
  if (!jwt) {
218
- 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
+ });
219
253
  }
220
254
  const projects = await listProjects(jwt, MCP_USER_AGENT);
221
255
  if (projects.length === 0) {
222
- 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
+ });
223
262
  }
224
263
  const projectId = projects[0].id;
225
264
  const projectDetails = await getProject(jwt, projectId, MCP_USER_AGENT);
@@ -242,13 +281,7 @@ export function registerAuthTools(server) {
242
281
  if (preview.coupon?.valid) {
243
282
  text += `- Coupon (${preview.coupon.code}): ${preview.coupon.description || 'Applied'}\n`;
244
283
  }
245
- else if (preview.coupon && !preview.coupon.valid) {
246
- text += `- Coupon (${preview.coupon.code}): **Invalid** — ${preview.coupon.invalidReason || 'not applicable'}\n`;
247
- }
248
284
  text += `\n**Due Today: $${(preview.dueToday / 100).toFixed(2)}**\n`;
249
- if (preview.note) {
250
- text += `\n_${preview.note}_\n`;
251
- }
252
285
  text += `\nTo proceed, use the \`upgradePlan\` tool with plan: '${plan}'.`;
253
286
  return mcpText(text);
254
287
  }
@@ -256,204 +289,275 @@ export function registerAuthTools(server) {
256
289
  return handleToolError(err, 'Error previewing upgrade');
257
290
  }
258
291
  });
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.', {
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'),
260
294
  plan: z.enum(['developer', 'business', 'professional']).describe('Target plan name'),
261
295
  period: z.enum(['monthly', 'yearly']).default('monthly').describe('Billing period'),
262
296
  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 }) => {
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 }) => {
267
301
  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
302
  const jwt = getJwt();
286
303
  if (!jwt) {
287
- 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
+ });
288
310
  }
289
311
  const projects = await listProjects(jwt, MCP_USER_AGENT);
290
312
  if (projects.length === 0) {
291
- 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
+ });
292
319
  }
293
320
  const projectId = projects[0].id;
294
- 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,
295
351
  plan,
296
352
  period,
297
- refId: projectId,
298
353
  couponCode,
299
354
  email,
300
355
  firstName,
301
356
  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.`);
357
+ });
358
+ return mcpText(renderPaymentLink(result.paymentLink, 'Upgrade', 'getAccountStatus'));
315
359
  }
316
360
  catch (err) {
317
361
  return handleToolError(err, 'Error upgrading plan');
318
362
  }
319
363
  });
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 () => {
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 }) => {
324
370
  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
371
  const jwt = getJwt();
335
372
  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.`);
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
+ });
341
379
  }
342
- // ── Tier 3: full status via JWT ──
343
380
  const projects = await listProjects(jwt, MCP_USER_AGENT);
344
381
  if (projects.length === 0) {
345
- 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
+ });
346
388
  }
347
389
  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)})`);
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();
408
410
  }
409
- else {
410
- lines.push(`- **Overage:** none`);
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
+ });
411
418
  }
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'));
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'));
423
430
  }
424
431
  catch (err) {
425
- return handleToolError(err, 'Error fetching account status');
432
+ return handleToolError(err, 'Error purchasing credits');
426
433
  }
427
434
  });
428
- 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'),
429
438
  paymentIntentId: z.string().describe('Payment intent ID from renewal notification'),
430
- }, async ({ paymentIntentId }) => {
439
+ }, async ({ mode, paymentIntentId }) => {
431
440
  try {
432
- let signerData;
433
- try {
434
- signerData = await loadSignerOrFail();
435
- }
436
- catch {
437
- return mcpError('No keypair found. Call `generateKeypair` first.');
438
- }
439
441
  const jwt = getJwt();
440
442
  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}\``);
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');
449
465
  }
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.`);
466
+ const result = await payRenewal(jwt, paymentIntentId);
467
+ return mcpText(renderPaymentLink(result.paymentLink, 'Renewal', 'payRenewal again with mode: "autopay" or open the link'));
454
468
  }
455
469
  catch (err) {
456
470
  return handleToolError(err, 'Error processing renewal payment');
457
471
  }
458
472
  });
459
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
+ }