@potswap_io/mcp 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Potswap
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,91 @@
1
- # Temporary Holding Version
1
+ # @potswap_io/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Solana swap tool for AI agents. One MCP server gives your agent best-route swaps across every Solana DEX (Metis, JupiterZ, DFlow and OKX compete on each order, MEV-protected), token research with risk flags, balances, and POT rewards for every trade.
4
+
5
+ - **Non-custodial.** Transactions are signed on your machine with your own keypair. The key never leaves the process and is redacted from every output.
6
+ - **Safe by default.** `swap` needs `confirm: true`; per-trade cap 1 SOL; slippage cap 3%; dry-run mode; a 0.01 SOL reserve is always kept. Every error says whether funds were debited.
7
+ - **Pays you back.** Potswap charges a 0.5% fee inside the swap and returns it to the trading wallet as POT points (Season 1). Agents earn exactly like humans.
8
+ - **$0 to run.** Talks only to `potswap.io`; no API keys needed.
9
+
10
+ ## Install
11
+
12
+ Node 20+. No install step; run it with `npx`.
13
+
14
+ ### Claude Desktop
15
+
16
+ `claude_desktop_config.json` (Settings → Developer → Edit Config):
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "potswap": {
22
+ "command": "npx",
23
+ "args": ["-y", "@potswap_io/mcp"],
24
+ "env": {
25
+ "SOLANA_PRIVATE_KEY": "<base58 secret or JSON byte array>",
26
+ "POTSWAP_MAX_TRADE_SOL": "0.5"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ### Claude Code
34
+
35
+ ```bash
36
+ claude mcp add potswap -e SOLANA_KEYPAIR_PATH=~/.config/solana/id.json -- npx -y @potswap_io/mcp
37
+ ```
38
+
39
+ ### Cursor
40
+
41
+ `.cursor/mcp.json` in your project:
42
+
43
+ ```json
44
+ { "mcpServers": { "potswap": { "command": "npx", "args": ["-y", "@potswap_io/mcp"], "env": { "SOLANA_KEYPAIR_PATH": "/home/you/.config/solana/id.json" } } } }
45
+ ```
46
+
47
+ Without a key the server still runs in read-only mode (search, quotes, trending, points of any wallet).
48
+
49
+ ## Configuration
50
+
51
+ | Variable | Default | Meaning |
52
+ |---|---|---|
53
+ | `SOLANA_PRIVATE_KEY` | — | base58 secret key (64 bytes) or JSON byte array, as exported by Phantom / `solana-keygen` |
54
+ | `SOLANA_KEYPAIR_PATH` | — | path to a `solana-keygen` JSON file; used when `SOLANA_PRIVATE_KEY` is unset |
55
+ | `SOLANA_RPC_URL` | Potswap's RPC proxy | your own RPC if you trade a lot |
56
+ | `POTSWAP_MAX_TRADE_SOL` | `1` | refuse any swap whose input is worth more than this many SOL |
57
+ | `POTSWAP_MAX_SLIPPAGE_BPS` | `300` | refuse requested slippage above this |
58
+ | `POTSWAP_DRY_RUN` | `false` | `true` = `swap` never signs; returns what it would have done |
59
+ | `POTSWAP_API` | `https://www.potswap.io` | base URL |
60
+
61
+ Use a dedicated agent wallet with only the funds you want the agent to control. Keep the caps low until you trust the agent's behaviour.
62
+
63
+ ## Tools
64
+
65
+ | Tool | Does |
66
+ |---|---|
67
+ | `search_tokens {query}` | Find tokens by name/symbol/mint |
68
+ | `get_token {mint}` | Price, mcap, liquidity, holders, audit data, **risk badges** (`riskLevel` 0 safe → 3 danger) |
69
+ | `get_trending {kind}` | `new`, `trending`, `top`, `graduating` lists |
70
+ | `get_quote {inputMint, outputMint, amount}` | Best route, fee, impact, router. `amount` is in UI units (1 = 1 SOL) |
71
+ | `swap {…, confirm: true}` | Executes at the best route and returns the signature, amounts, fee and POT points |
72
+ | `get_balances {wallet?}` | SOL + SPL balances with USD |
73
+ | `get_positions {wallet?}` | Holdings with USD; PnL coming |
74
+ | `get_points {wallet?}` | Season 1 points, POT allocated / claimable, next unlock |
75
+ | `get_leaderboard` | Top wallets by fee paid |
76
+
77
+ Example prompt: *"Check BONK's risk flags, and if riskLevel is 0 or 1 buy 0.1 SOL worth."* The agent calls `get_token`, then `swap` with `confirm: true`; you can require that it shows you the quote first by leaving `POTSWAP_DRY_RUN=true` until you are comfortable.
78
+
79
+ ## Fees and rewards
80
+
81
+ Each swap carries Potswap's 0.5% integrator fee via Jupiter's referral program; Jupiter keeps 20% of it. The fee basis is credited to the trading wallet as Season 1 points, split daily: 666,666 POT per epoch, by `fee^1.1`, capped at 10% per wallet. Points become POT tokens after the token launch; unlock 7 days after each epoch. Details: https://www.potswap.io
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ cd mcp && npm i && npm test # unit tests over the real MCP protocol (in-memory transport)
87
+ npm run dev # run from source
88
+ npm run sync-shared && npm run build
89
+ ```
90
+
91
+ MIT © Potswap
package/dist/api.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import type { ExecuteResponse, OrderResponse, Token } from './shared/lib-types.js';
2
+ import type { TerminalToken } from './shared/types.js';
3
+ export type FetchFn = typeof fetch;
4
+ export declare class ApiError extends Error {
5
+ status: number;
6
+ constructor(status: number, message: string);
7
+ }
8
+ export interface PublicConfig {
9
+ referralAccount: string | null;
10
+ referralFeeBps: number;
11
+ seasonStart: string | null;
12
+ pointsEnabled: boolean;
13
+ }
14
+ export declare class PotswapApi {
15
+ private base;
16
+ private fetchFn;
17
+ private configCache;
18
+ private tokenCache;
19
+ constructor(base: string, fetchFn?: FetchFn);
20
+ private req;
21
+ config(): Promise<PublicConfig>;
22
+ order(p: {
23
+ inputMint: string;
24
+ outputMint: string;
25
+ amount: string;
26
+ taker?: string;
27
+ slippageBps?: number;
28
+ }): Promise<OrderResponse>;
29
+ execute(signedTransaction: string, requestId: string): Promise<ExecuteResponse>;
30
+ searchTokens(query: string): Promise<Token[]>;
31
+ token(mint: string): Promise<Token>;
32
+ prices(mints: string[]): Promise<Record<string, number>>;
33
+ list(kind: string): Promise<TerminalToken[]>;
34
+ terminalToken(mint: string): Promise<TerminalToken>;
35
+ points(wallet: string): Promise<Record<string, unknown>>;
36
+ leaderboard(): Promise<Record<string, unknown>>;
37
+ }
package/dist/api.js ADDED
@@ -0,0 +1,89 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ constructor(status, message) { super(message); this.status = status; }
4
+ }
5
+ export class PotswapApi {
6
+ base;
7
+ fetchFn;
8
+ configCache = null;
9
+ tokenCache = new Map();
10
+ constructor(base, fetchFn = fetch) { this.base = base; this.fetchFn = fetchFn; }
11
+ async req(path, init, retry = true) {
12
+ const res = await this.fetchFn(this.base + path, { ...init, headers: { accept: 'application/json', ...init?.headers } });
13
+ if (res.status === 429 || res.status === 503) {
14
+ if (retry) {
15
+ await new Promise((r) => setTimeout(r, 1100));
16
+ return this.req(path, init, false);
17
+ }
18
+ throw new ApiError(res.status, 'Potswap is busy; retry in a few seconds. No funds were debited.');
19
+ }
20
+ const text = await res.text();
21
+ let data = null;
22
+ try {
23
+ data = text ? JSON.parse(text) : null;
24
+ }
25
+ catch {
26
+ data = { error: text };
27
+ }
28
+ if (!res.ok) {
29
+ const msg = data?.error ?? data?.message ?? `HTTP ${res.status}`;
30
+ throw new ApiError(res.status, msg);
31
+ }
32
+ return data;
33
+ }
34
+ async config() {
35
+ if (this.configCache && Date.now() - this.configCache.at < 300_000)
36
+ return this.configCache.value;
37
+ let value;
38
+ try {
39
+ value = await this.req('/api/config');
40
+ }
41
+ catch {
42
+ value = { referralAccount: null, referralFeeBps: 0, seasonStart: null, pointsEnabled: false };
43
+ }
44
+ this.configCache = { value, at: Date.now() };
45
+ return value;
46
+ }
47
+ async order(p) {
48
+ const cfg = await this.config();
49
+ const q = new URLSearchParams({ inputMint: p.inputMint, outputMint: p.outputMint, amount: p.amount });
50
+ if (p.taker)
51
+ q.set('taker', p.taker);
52
+ if (p.slippageBps !== undefined)
53
+ q.set('slippageBps', String(p.slippageBps));
54
+ if (cfg.referralAccount) {
55
+ q.set('referralAccount', cfg.referralAccount);
56
+ q.set('referralFee', String(cfg.referralFeeBps));
57
+ }
58
+ return this.req(`/api/jup/swap/v2/order?${q}`);
59
+ }
60
+ execute(signedTransaction, requestId) {
61
+ return this.req('/api/jup/swap/v2/execute', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ signedTransaction, requestId }) });
62
+ }
63
+ async searchTokens(query) {
64
+ const list = await this.req(`/api/jup/tokens/v2/search?query=${encodeURIComponent(query)}`);
65
+ for (const t of list)
66
+ this.tokenCache.set(t.id, t);
67
+ return list;
68
+ }
69
+ async token(mint) {
70
+ const hit = this.tokenCache.get(mint);
71
+ if (hit)
72
+ return hit;
73
+ const list = await this.searchTokens(mint);
74
+ const t = list.find((x) => x.id === mint);
75
+ if (!t)
76
+ throw new ApiError(404, `Unknown token ${mint}`);
77
+ return t;
78
+ }
79
+ async prices(mints) {
80
+ if (!mints.length)
81
+ return {};
82
+ const d = await this.req(`/api/price/v3?ids=${mints.join(',')}`.replace('/api/price', '/api/jup/price'));
83
+ return Object.fromEntries(Object.entries(d).map(([k, v]) => [k, v.usdPrice]));
84
+ }
85
+ list(kind) { return this.req(`/api/terminal/list/${kind}`); }
86
+ terminalToken(mint) { return this.req(`/api/terminal/token/${mint}`); }
87
+ points(wallet) { return this.req(`/api/points/${wallet}`); }
88
+ leaderboard() { return this.req('/api/leaderboard'); }
89
+ }
@@ -0,0 +1,19 @@
1
+ export interface Config {
2
+ apiBase: string;
3
+ rpcUrl: string;
4
+ maxTradeSol: number;
5
+ maxSlippageBps: number;
6
+ dryRun: boolean;
7
+ privateKey?: string;
8
+ keypairPath?: string;
9
+ }
10
+ export declare const DEFAULTS: {
11
+ apiBase: string;
12
+ maxTradeSol: number;
13
+ maxSlippageBps: number;
14
+ solReserve: number;
15
+ };
16
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
17
+ export declare const SOL_MINT = "So11111111111111111111111111111111111111112";
18
+ export declare const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
19
+ export declare const NO_WALLET = "Wallet not configured. Set SOLANA_PRIVATE_KEY or SOLANA_KEYPAIR_PATH. No funds were debited.";
package/dist/config.js ADDED
@@ -0,0 +1,22 @@
1
+ export const DEFAULTS = {
2
+ apiBase: 'https://www.potswap.io',
3
+ maxTradeSol: 1,
4
+ maxSlippageBps: 300,
5
+ solReserve: 0.01,
6
+ };
7
+ export function loadConfig(env = process.env) {
8
+ const apiBase = (env.POTSWAP_API || DEFAULTS.apiBase).replace(/\/$/, '');
9
+ const num = (v, d) => { const n = Number(v); return v !== undefined && v !== '' && Number.isFinite(n) && n > 0 ? n : d; };
10
+ return {
11
+ apiBase,
12
+ rpcUrl: env.SOLANA_RPC_URL || `${apiBase}/api/rpc`,
13
+ maxTradeSol: num(env.POTSWAP_MAX_TRADE_SOL, DEFAULTS.maxTradeSol),
14
+ maxSlippageBps: num(env.POTSWAP_MAX_SLIPPAGE_BPS, DEFAULTS.maxSlippageBps),
15
+ dryRun: /^(1|true|yes)$/i.test(env.POTSWAP_DRY_RUN ?? ''),
16
+ privateKey: env.SOLANA_PRIVATE_KEY || undefined,
17
+ keypairPath: env.SOLANA_KEYPAIR_PATH || undefined,
18
+ };
19
+ }
20
+ export const SOL_MINT = 'So11111111111111111111111111111111111111112';
21
+ export const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
22
+ export const NO_WALLET = 'Wallet not configured. Set SOLANA_PRIVATE_KEY or SOLANA_KEYPAIR_PATH. No funds were debited.';
@@ -0,0 +1,3 @@
1
+ export declare function toRaw(amount: number | string, decimals: number): string;
2
+ export declare const fromRaw: (raw: string | number | bigint, decimals: number) => number;
3
+ export declare const explorer: (sig: string) => string;
package/dist/format.js ADDED
@@ -0,0 +1,7 @@
1
+ export function toRaw(amount, decimals) {
2
+ const [i = '0', f = ''] = String(amount).replace(/,/g, '.').split('.');
3
+ const frac = (f + '0'.repeat(decimals)).slice(0, decimals);
4
+ return (BigInt(i || '0') * 10n ** BigInt(decimals) + BigInt(frac || '0')).toString();
5
+ }
6
+ export const fromRaw = (raw, decimals) => Number(BigInt(raw)) / 10 ** decimals;
7
+ export const explorer = (sig) => `https://solscan.io/tx/${sig}`;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // Potswap MCP server over stdio. Configure via env (see README).
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { createServer, VERSION } from './server.js';
5
+ import { loadConfig } from './config.js';
6
+ import { log } from './redact.js';
7
+ const config = loadConfig();
8
+ const server = createServer({ config });
9
+ const transport = new StdioServerTransport();
10
+ await server.connect(transport);
11
+ log(`potswap-mcp v${VERSION} ready · api=${config.apiBase} · maxTradeSol=${config.maxTradeSol} · maxSlippageBps=${config.maxSlippageBps}${config.dryRun ? ' · DRY RUN' : ''}${config.privateKey || config.keypairPath ? '' : ' · no wallet (read-only)'}`);
@@ -0,0 +1,13 @@
1
+ export interface LimitInput {
2
+ amountUi: number;
3
+ inputMint: string;
4
+ inputSymbol: string;
5
+ inputUsd?: number;
6
+ solUsd?: number;
7
+ isSol: boolean;
8
+ slippageBps?: number;
9
+ maxTradeSol: number;
10
+ maxSlippageBps: number;
11
+ balanceUi?: number;
12
+ }
13
+ export declare function checkLimits(i: LimitInput): string | null;
package/dist/limits.js ADDED
@@ -0,0 +1,23 @@
1
+ // PRD P10.6 — safety limits applied before anything is signed.
2
+ import { DEFAULTS } from './config.js';
3
+ export function checkLimits(i) {
4
+ if (!(i.amountUi > 0))
5
+ return 'Amount must be a positive number in UI units (1 = 1 SOL). No funds were debited.';
6
+ if (i.slippageBps !== undefined && i.slippageBps > i.maxSlippageBps) {
7
+ return `Refused by safety limit: slippage ${i.slippageBps / 100}% is above POTSWAP_MAX_SLIPPAGE_BPS (${i.maxSlippageBps / 100}%). No funds were debited.`;
8
+ }
9
+ const solEquivalent = i.isSol ? i.amountUi : i.inputUsd !== undefined && i.solUsd ? i.inputUsd / i.solUsd : undefined;
10
+ if (solEquivalent !== undefined && solEquivalent > i.maxTradeSol) {
11
+ const hint = i.amountUi >= 1_000_000 ? ' Hint: amount is in UI units (1 = 1 SOL), not lamports.' : '';
12
+ return `Refused by safety limit: ${i.amountUi} ${i.inputSymbol} ≈ ${solEquivalent.toFixed(3)} SOL is above POTSWAP_MAX_TRADE_SOL (${i.maxTradeSol}).${hint} No funds were debited.`;
13
+ }
14
+ if (i.balanceUi !== undefined) {
15
+ const spendable = i.isSol ? Math.max(i.balanceUi - DEFAULTS.solReserve, 0) : i.balanceUi;
16
+ if (i.amountUi > spendable) {
17
+ return i.isSol
18
+ ? `Insufficient SOL: balance ${i.balanceUi.toFixed(4)}, keep at least ${DEFAULTS.solReserve} SOL for fees. No funds were debited.`
19
+ : `Insufficient ${i.inputSymbol} balance (${i.balanceUi}). No funds were debited.`;
20
+ }
21
+ }
22
+ return null;
23
+ }
@@ -0,0 +1,5 @@
1
+ export declare function registerSecret(s: string): void;
2
+ export declare function redact(text: string): string;
3
+ export declare function redactValue<T>(v: T): T;
4
+ /** stderr logger (stdout is the MCP transport); always redacted. */
5
+ export declare function log(...parts: unknown[]): void;
package/dist/redact.js ADDED
@@ -0,0 +1,23 @@
1
+ // PRD P10.9 — anything that might contain a secret passes through redact() before leaving the process.
2
+ const secrets = new Set();
3
+ export function registerSecret(s) { if (s && s.length >= 16)
4
+ secrets.add(s); }
5
+ export function redact(text) {
6
+ let out = text;
7
+ for (const s of secrets) {
8
+ if (out.includes(s))
9
+ out = out.split(s).join('[REDACTED]');
10
+ // JSON byte arrays can be re-serialised with different spacing: compare whitespace-free.
11
+ if (s.startsWith('[') && out.replace(/\s+/g, '').includes(s.replace(/\s+/g, ''))) {
12
+ out = out.replace(/\[\s*(?:\d{1,3}\s*,\s*){63}\d{1,3}\s*\]/g, '[REDACTED]');
13
+ }
14
+ }
15
+ return out;
16
+ }
17
+ export function redactValue(v) {
18
+ return JSON.parse(redact(JSON.stringify(v)));
19
+ }
20
+ /** stderr logger (stdout is the MCP transport); always redacted. */
21
+ export function log(...parts) {
22
+ process.stderr.write(redact(parts.map((p) => (typeof p === 'string' ? p : JSON.stringify(p))).join(' ')) + '\n');
23
+ }
@@ -0,0 +1,14 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type FetchFn } from './api.js';
3
+ import { SOL_MINT, USDC_MINT, type Config } from './config.js';
4
+ import { type Signer } from './wallet.js';
5
+ export declare const VERSION = "0.1.0";
6
+ export interface ServerDeps {
7
+ config?: Config;
8
+ signer?: Signer | null;
9
+ fetchFn?: FetchFn;
10
+ /** Balance reader; defaults to an RPC connection. Injectable for tests. */
11
+ balances?: (wallet: string) => Promise<Record<string, number>>;
12
+ }
13
+ export declare function createServer(deps?: ServerDeps): McpServer;
14
+ export { SOL_MINT, USDC_MINT };
package/dist/server.js ADDED
@@ -0,0 +1,249 @@
1
+ // PRD P10 — the Potswap MCP server. Tools are thin, safe wrappers over the Potswap API.
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { Connection, PublicKey, VersionedTransaction } from '@solana/web3.js';
4
+ import { z } from 'zod';
5
+ import { PotswapApi, ApiError } from './api.js';
6
+ import { loadConfig, NO_WALLET, SOL_MINT, USDC_MINT } from './config.js';
7
+ import { checkLimits } from './limits.js';
8
+ import { explorer, fromRaw, toRaw } from './format.js';
9
+ import { redact, redactValue } from './redact.js';
10
+ import { loadSigner } from './wallet.js';
11
+ import { describeOrderError, friendlyError } from './shared/errors.js';
12
+ import { riskBadges, riskLevel } from './shared/risk.js';
13
+ export const VERSION = '0.1.0';
14
+ const ok = (data, summary) => ({
15
+ content: [{ type: 'text', text: redact(summary ?? JSON.stringify(data, null, 2)) }], structuredContent: redactValue(data),
16
+ });
17
+ const fail = (message, extra = {}) => ({
18
+ content: [{ type: 'text', text: redact(message) }], structuredContent: redactValue({ error: message, ...extra }), isError: true,
19
+ });
20
+ export function createServer(deps = {}) {
21
+ const config = deps.config ?? loadConfig();
22
+ const signer = deps.signer !== undefined ? deps.signer : loadSigner(config);
23
+ const api = new PotswapApi(config.apiBase, deps.fetchFn);
24
+ const readBalances = deps.balances ?? rpcBalances(config.rpcUrl);
25
+ const server = new McpServer({ name: 'potswap', version: VERSION });
26
+ const walletOr = (w) => w ?? signer?.publicKey ?? null;
27
+ server.registerTool('search_tokens', {
28
+ title: 'Search Solana tokens',
29
+ description: 'Find tokens by name, symbol or mint address. Returns up to 20 with price, market cap and liquidity.',
30
+ inputSchema: { query: z.string().min(1).max(64) },
31
+ }, async ({ query }) => {
32
+ try {
33
+ const list = (await api.searchTokens(query)).slice(0, 20);
34
+ const tokens = list.map(slimToken);
35
+ return ok({ tokens }, tokens.map((t) => `${t.symbol} (${t.name}) ${t.mint} price=${t.usdPrice ?? '?'} mcap=${t.mcap ?? '?'}${t.isVerified ? ' ✓' : ''}`).join('\n') || 'No tokens found');
36
+ }
37
+ catch (e) {
38
+ return fail(msg(e));
39
+ }
40
+ });
41
+ server.registerTool('get_token', {
42
+ title: 'Token details and risk flags',
43
+ description: 'Full token record: price, mcap, liquidity, holders, 5m/1h/6h/24h stats, audit data and risk badges (riskLevel 0 safe … 3 danger).',
44
+ inputSchema: { mint: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/) },
45
+ }, async ({ mint }) => {
46
+ try {
47
+ const t = await api.terminalToken(mint);
48
+ const badges = riskBadges(t);
49
+ return ok({ token: t, riskBadges: badges, riskLevel: riskLevel(badges) }, `${t.symbol} (${t.name})\nprice=${t.usdPrice} mcap=${t.mcap} liquidity=${t.liquidity} holders=${t.holderCount}\nrisk ${riskLevel(badges)}/3: ${badges.map((b) => b.label).join(', ')}`);
50
+ }
51
+ catch (e) {
52
+ return fail(msg(e));
53
+ }
54
+ });
55
+ server.registerTool('get_trending', {
56
+ title: 'Trending / new / top / graduating tokens',
57
+ description: 'Discovery lists from Potswap (edge-cached, refreshed every 15 s).',
58
+ inputSchema: { kind: z.enum(['new', 'trending', 'top', 'graduating']).default('trending'), limit: z.number().int().min(1).max(50).default(20) },
59
+ }, async ({ kind, limit }) => {
60
+ try {
61
+ const list = (await api.list(kind)).slice(0, limit);
62
+ const rows = list.map((t) => ({ mint: t.id, symbol: t.symbol, name: t.name, usdPrice: t.usdPrice, mcap: t.mcap, liquidity: t.liquidity, holders: t.holderCount, change5m: t.stats5m?.priceChange, change1h: t.stats1h?.priceChange, bondingProgress: t.bondingProgress, riskLevel: riskLevel(riskBadges(t)) }));
63
+ return ok({ kind, tokens: rows }, rows.map((r) => `${r.symbol} ${r.mint} mcap=${r.mcap} 5m=${r.change5m} risk=${r.riskLevel}`).join('\n'));
64
+ }
65
+ catch (e) {
66
+ return fail(msg(e));
67
+ }
68
+ });
69
+ server.registerTool('get_quote', {
70
+ title: 'Get a swap quote',
71
+ description: 'Best route across Metis, JupiterZ, DFlow and OKX. Amount is in UI units of the input token (1 = 1 SOL). Includes the Potswap fee (returned to the wallet as POT points).',
72
+ inputSchema: quoteShape,
73
+ }, async (a) => {
74
+ try {
75
+ const q = await buildQuote(api, a);
76
+ return ok(q, `${a.amount} ${q.inputSymbol} → ${q.outAmount} ${q.outputSymbol} via ${q.router} (fee ${q.feeBps} bps, impact ${q.priceImpactPct}%)`);
77
+ }
78
+ catch (e) {
79
+ return fail(msg(e));
80
+ }
81
+ });
82
+ server.registerTool('swap', {
83
+ title: 'Swap tokens (signs with the agent wallet)',
84
+ description: 'Executes a swap at the best route. Requires confirm=true; otherwise returns the quote only. Protected by POTSWAP_MAX_TRADE_SOL and POTSWAP_MAX_SLIPPAGE_BPS. Every error states whether funds were debited.',
85
+ inputSchema: { ...quoteShape, confirm: z.boolean().default(false), maxSlippageBps: z.number().int().min(1).max(5000).optional() },
86
+ }, async (a) => {
87
+ if (!signer)
88
+ return fail(NO_WALLET);
89
+ try {
90
+ const q = await buildQuote(api, a);
91
+ const slippageBps = a.slippageBps ?? a.maxSlippageBps;
92
+ const prices = await api.prices([SOL_MINT, a.inputMint]).catch(() => ({}));
93
+ let balanceUi;
94
+ try {
95
+ balanceUi = (await readBalances(signer.publicKey))[a.inputMint] ?? 0;
96
+ }
97
+ catch {
98
+ balanceUi = undefined;
99
+ }
100
+ const refusal = checkLimits({
101
+ amountUi: a.amount, inputMint: a.inputMint, inputSymbol: q.inputSymbol, isSol: a.inputMint === SOL_MINT,
102
+ inputUsd: prices[a.inputMint] ? a.amount * prices[a.inputMint] : undefined, solUsd: prices[SOL_MINT],
103
+ slippageBps, maxTradeSol: config.maxTradeSol, maxSlippageBps: config.maxSlippageBps, balanceUi,
104
+ });
105
+ if (refusal)
106
+ return fail(refusal, { quote: q });
107
+ if (!a.confirm)
108
+ return ok({ confirmRequired: true, quote: q }, `Quote only (confirm=true to execute): ${a.amount} ${q.inputSymbol} → ${q.outAmount} ${q.outputSymbol} via ${q.router}`);
109
+ if (config.dryRun)
110
+ return ok({ dryRun: true, quote: q }, `DRY RUN, nothing signed: ${a.amount} ${q.inputSymbol} → ${q.outAmount} ${q.outputSymbol}`);
111
+ const order = await api.order({ inputMint: a.inputMint, outputMint: a.outputMint, amount: q.inAmountRaw, taker: signer.publicKey, slippageBps });
112
+ if (!order.transaction)
113
+ return fail(`${describeOrderError(order)} No funds were debited.`, { quote: q });
114
+ const tx = VersionedTransaction.deserialize(Buffer.from(order.transaction, 'base64'));
115
+ signer.sign(tx);
116
+ const res = await api.execute(Buffer.from(tx.serialize()).toString('base64'), order.requestId);
117
+ if (res.status !== 'Success')
118
+ return fail(friendlyError(res.error || `Swap failed (code ${res.code})`), { signature: res.signature, explorerUrl: res.signature ? explorer(res.signature) : undefined });
119
+ const outDec = (await api.token(a.outputMint)).decimals;
120
+ const outAmount = fromRaw(res.totalOutputAmount ?? order.outAmount, outDec);
121
+ const feeBps = order.platformFee?.feeBps ?? order.feeBps ?? 0;
122
+ const feeUsd = prices[a.inputMint] ? a.amount * prices[a.inputMint] * feeBps / 10_000 : undefined;
123
+ const cfg = await api.config();
124
+ const result = { signature: res.signature, explorerUrl: explorer(res.signature), inAmount: a.amount, inputSymbol: q.inputSymbol, outAmount, outputSymbol: q.outputSymbol, router: order.router, feeBps, feeUsd, pointsAdded: cfg.referralAccount ? feeUsd : 0 };
125
+ return ok(result, `Swapped ${a.amount} ${q.inputSymbol} → ${outAmount} ${q.outputSymbol} via ${order.router}. ${explorer(res.signature)}`);
126
+ }
127
+ catch (e) {
128
+ return fail(friendlyError(msg(e)));
129
+ }
130
+ });
131
+ server.registerTool('get_balances', {
132
+ title: 'Wallet balances',
133
+ description: 'SOL and SPL token balances with USD values. Defaults to the agent wallet.',
134
+ inputSchema: { wallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional() },
135
+ }, async ({ wallet }) => {
136
+ const w = walletOr(wallet);
137
+ if (!w)
138
+ return fail(NO_WALLET);
139
+ try {
140
+ const bal = await readBalances(w);
141
+ const mints = Object.keys(bal);
142
+ const prices = await api.prices(mints.slice(0, 50)).catch(() => ({}));
143
+ const rows = mints.map((m) => ({ mint: m, amount: bal[m], usd: prices[m] ? bal[m] * prices[m] : undefined })).sort((x, y) => (y.usd ?? 0) - (x.usd ?? 0));
144
+ const total = rows.reduce((s, r) => s + (r.usd ?? 0), 0);
145
+ return ok({ wallet: w, totalUsd: total, balances: rows }, `${w}\ntotal ≈ $${total.toFixed(2)}\n` + rows.map((r) => `${r.mint === SOL_MINT ? 'SOL' : r.mint}: ${r.amount}${r.usd !== undefined ? ` ($${r.usd.toFixed(2)})` : ''}`).join('\n'));
146
+ }
147
+ catch (e) {
148
+ return fail(msg(e));
149
+ }
150
+ });
151
+ server.registerTool('get_positions', {
152
+ title: 'Positions',
153
+ description: 'Token holdings with 24h price change (PnL arrives in a later version).',
154
+ inputSchema: { wallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional() },
155
+ }, async ({ wallet }) => {
156
+ const w = walletOr(wallet);
157
+ if (!w)
158
+ return fail(NO_WALLET);
159
+ try {
160
+ const bal = await readBalances(w);
161
+ const mints = Object.keys(bal).filter((m) => m !== SOL_MINT).slice(0, 20);
162
+ const infos = mints.length ? await api.searchTokens(mints.join(',')).catch(() => []) : [];
163
+ const prices = await api.prices([SOL_MINT, ...mints]).catch(() => ({}));
164
+ const positions = mints.map((m) => { const t = infos.find((x) => x.id === m); return { mint: m, symbol: t?.symbol ?? m.slice(0, 4), amount: bal[m], usd: prices[m] ? bal[m] * prices[m] : undefined }; });
165
+ return ok({ wallet: w, sol: bal[SOL_MINT] ?? 0, solUsd: prices[SOL_MINT] ? (bal[SOL_MINT] ?? 0) * prices[SOL_MINT] : undefined, positions });
166
+ }
167
+ catch (e) {
168
+ return fail(msg(e));
169
+ }
170
+ });
171
+ server.registerTool('get_points', {
172
+ title: 'POT points',
173
+ description: "Season 1 points for a wallet: this epoch, all-time, POT allocated and claimable, next unlock. Defaults to the agent wallet.",
174
+ inputSchema: { wallet: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).optional() },
175
+ }, async ({ wallet }) => {
176
+ const w = walletOr(wallet);
177
+ if (!w)
178
+ return fail(NO_WALLET);
179
+ try {
180
+ return ok({ enabled: true, ...(await api.points(w)) });
181
+ }
182
+ catch (e) {
183
+ if (e instanceof ApiError && e.status === 503)
184
+ return ok({ enabled: false, wallet: w, note: 'The points engine is not enabled yet. Swaps made now still pay the Potswap fee; points start with Season 1.' });
185
+ return fail(msg(e));
186
+ }
187
+ });
188
+ server.registerTool('get_leaderboard', {
189
+ title: 'Leaderboard',
190
+ description: 'Top wallets by Potswap fee paid, with POT allocated.',
191
+ inputSchema: {},
192
+ }, async () => {
193
+ try {
194
+ return ok({ enabled: true, ...(await api.leaderboard()) });
195
+ }
196
+ catch (e) {
197
+ if (e instanceof ApiError && e.status === 503)
198
+ return ok({ enabled: false, note: 'The points engine is not enabled yet.' });
199
+ return fail(msg(e));
200
+ }
201
+ });
202
+ return server;
203
+ }
204
+ const quoteShape = {
205
+ inputMint: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe('Mint of the token to sell (SOL = So11111111111111111111111111111111111111112)'),
206
+ outputMint: z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/).describe('Mint of the token to buy'),
207
+ amount: z.number().positive().describe('Amount in UI units of the input token (1 = 1 SOL)'),
208
+ slippageBps: z.number().int().min(1).max(5000).optional().describe('Fixed slippage in bps; omit for auto'),
209
+ };
210
+ async function buildQuote(api, a) {
211
+ const [inTok, outTok] = await Promise.all([api.token(a.inputMint), api.token(a.outputMint)]);
212
+ const inAmountRaw = toRaw(a.amount, inTok.decimals);
213
+ const o = await api.order({ inputMint: a.inputMint, outputMint: a.outputMint, amount: inAmountRaw, slippageBps: a.slippageBps });
214
+ if (o.errorMessage && !o.outAmount)
215
+ throw new Error(`${describeOrderError(o)} No funds were debited.`);
216
+ const outAmount = fromRaw(o.outAmount, outTok.decimals);
217
+ return {
218
+ inputMint: a.inputMint, inputSymbol: inTok.symbol, outputMint: a.outputMint, outputSymbol: outTok.symbol,
219
+ inAmount: a.amount, inAmountRaw, outAmount, price: outAmount / a.amount,
220
+ priceImpactPct: o.priceImpactPct !== undefined ? Math.abs(Number(o.priceImpactPct)) : undefined,
221
+ slippageBps: o.slippageBps, feeBps: o.platformFee?.feeBps ?? o.feeBps ?? 0, router: o.router, requestId: o.requestId,
222
+ expiresAt: o.expireAt ?? null,
223
+ };
224
+ }
225
+ function slimToken(t) {
226
+ return { mint: t.id, symbol: t.symbol, name: t.name, decimals: t.decimals, usdPrice: t.usdPrice, mcap: t.mcap, liquidity: t.liquidity, isVerified: t.isVerified ?? false };
227
+ }
228
+ function msg(e) { return e instanceof Error ? e.message : String(e); }
229
+ function rpcBalances(rpcUrl) {
230
+ return async (wallet) => {
231
+ const conn = new Connection(rpcUrl, 'confirmed');
232
+ const owner = new PublicKey(wallet);
233
+ const out = {};
234
+ const [lamports, spl, t22] = await Promise.all([
235
+ conn.getBalance(owner),
236
+ conn.getParsedTokenAccountsByOwner(owner, { programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') }),
237
+ conn.getParsedTokenAccountsByOwner(owner, { programId: new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb') }).catch(() => ({ value: [] })),
238
+ ]);
239
+ out[SOL_MINT] = lamports / 1e9;
240
+ for (const { account } of [...spl.value, ...t22.value]) {
241
+ const info = account.data.parsed?.info;
242
+ const ui = Number(info?.tokenAmount?.uiAmount ?? 0);
243
+ if (info && ui > 0)
244
+ out[info.mint] = (out[info.mint] ?? 0) + ui;
245
+ }
246
+ return out;
247
+ };
248
+ }
249
+ export { SOL_MINT, USDC_MINT };
@@ -0,0 +1,5 @@
1
+ import type { OrderResponse } from './lib-types.js';
2
+ /** Maps /order error codes (transaction empty) to user copy. */
3
+ export declare function describeOrderError(o: Pick<OrderResponse, 'errorCode' | 'errorMessage'>): string;
4
+ /** User-facing error copy. Every message states whether funds were debited. */
5
+ export declare function friendlyError(msg: string): string;
@@ -0,0 +1,24 @@
1
+ /** Maps /order error codes (transaction empty) to user copy. */
2
+ export function describeOrderError(o) {
3
+ const map = {
4
+ 1: 'Insufficient balance for this swap.',
5
+ 2: 'Not enough SOL to pay network fees. Keep at least 0.01 SOL.',
6
+ 3: 'Amount too small for this route. Try a larger amount.',
7
+ };
8
+ return (o.errorCode && map[o.errorCode]) || o.errorMessage || 'Could not build the swap. Try again.';
9
+ }
10
+ /** User-facing error copy. Every message states whether funds were debited. */
11
+ export function friendlyError(msg) {
12
+ const m = msg.toLowerCase();
13
+ if (m.includes('user rejected') || m.includes('rejected the request'))
14
+ return 'You rejected the transaction in your wallet. No funds were debited.';
15
+ if (m.includes('slippage') || m.includes('0x1771') || m.includes('6001'))
16
+ return 'Price moved beyond your slippage tolerance. No funds were debited. Try again or raise slippage in settings.';
17
+ if (m.includes('insufficient'))
18
+ return 'Insufficient balance for this swap, including network fees. No funds were debited.';
19
+ if (m.includes('expired') || m.includes('-1005') || m.includes('block height'))
20
+ return 'The quote expired before the transaction landed. No funds were debited. Please try again.';
21
+ if (m.includes('failed to land') || m.includes('-1000'))
22
+ return 'The network was congested and the transaction did not land. No funds were debited. Please try again.';
23
+ return `${msg} If the transaction did not land, no funds were debited.`;
24
+ }
@@ -0,0 +1,42 @@
1
+ export interface Token {
2
+ id: string;
3
+ symbol: string;
4
+ name: string;
5
+ icon?: string;
6
+ decimals: number;
7
+ isVerified?: boolean;
8
+ usdPrice?: number;
9
+ }
10
+ export interface OrderResponse {
11
+ router: 'metis' | 'jupiterz' | 'dflow' | 'okx' | string;
12
+ inAmount: string;
13
+ outAmount: string;
14
+ inUsdValue?: number;
15
+ outUsdValue?: number;
16
+ priceImpactPct?: string | number;
17
+ slippageBps?: number;
18
+ feeBps?: number;
19
+ feeMint?: string;
20
+ platformFee?: {
21
+ amount: string;
22
+ feeBps: number;
23
+ feeMint: string;
24
+ };
25
+ transaction: string | null;
26
+ requestId: string;
27
+ lastValidBlockHeight?: number;
28
+ expireAt?: string;
29
+ errorCode?: number;
30
+ errorMessage?: string;
31
+ mode?: string;
32
+ transactionVersion?: number;
33
+ }
34
+ export interface ExecuteResponse {
35
+ status: 'Success' | 'Failed';
36
+ signature?: string;
37
+ code: number;
38
+ error?: string;
39
+ totalInputAmount?: string;
40
+ totalOutputAmount?: string;
41
+ }
42
+ export type SwapStage = 'idle' | 'signing' | 'sending' | 'confirming' | 'success' | 'failed';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { TerminalToken } from './types.js';
2
+ export type Tone = 'good' | 'warn' | 'bad' | 'muted';
3
+ export interface Badge {
4
+ key: string;
5
+ icon: string;
6
+ label: string;
7
+ tone: Tone;
8
+ tip: string;
9
+ }
10
+ export declare const TOP_HOLDER_WARN = 30;
11
+ export declare const DEV_BALANCE_WARN = 10;
12
+ export declare const DEV_MINTS_WARN = 20;
13
+ export declare function riskBadges(t: Pick<TerminalToken, 'audit' | 'isVerified'>): Badge[];
14
+ /** 0 (safe) … 3 (danger) summary used for row colouring. */
15
+ export declare function riskLevel(badges: Badge[]): number;
@@ -0,0 +1,35 @@
1
+ export const TOP_HOLDER_WARN = 30; // %
2
+ export const DEV_BALANCE_WARN = 10; // %
3
+ export const DEV_MINTS_WARN = 20;
4
+ export function riskBadges(t) {
5
+ const a = t.audit;
6
+ if (!a)
7
+ return [{ key: 'unaudited', icon: '?', label: 'Unaudited', tone: 'muted', tip: 'No audit data for this token yet.' }];
8
+ const out = [];
9
+ out.push(a.mintAuthorityDisabled
10
+ ? { key: 'mint', icon: '🔒', label: 'Mint revoked', tone: 'good', tip: 'Nobody can mint more supply.' }
11
+ : { key: 'mint', icon: '🖨️', label: 'Mintable', tone: 'bad', tip: 'The creator can still mint new supply.' });
12
+ out.push(a.freezeAuthorityDisabled
13
+ ? { key: 'freeze', icon: '🧊', label: 'Freeze revoked', tone: 'good', tip: 'Token accounts cannot be frozen.' }
14
+ : { key: 'freeze', icon: '❄️', label: 'Freezable', tone: 'bad', tip: 'The creator can freeze your tokens.' });
15
+ if (a.topHoldersPercentage !== undefined && a.topHoldersPercentage > TOP_HOLDER_WARN) {
16
+ out.push({ key: 'top', icon: '⚠', label: `Top holders ${a.topHoldersPercentage.toFixed(0)}%`, tone: 'warn', tip: 'A few wallets hold a large share of the supply.' });
17
+ }
18
+ if (a.devBalancePercentage !== undefined && a.devBalancePercentage > DEV_BALANCE_WARN) {
19
+ out.push({ key: 'dev', icon: '🧑‍💻', label: `Dev holds ${a.devBalancePercentage.toFixed(0)}%`, tone: 'warn', tip: 'The deployer wallet still holds a large share.' });
20
+ }
21
+ if (a.devMints !== undefined && a.devMints > DEV_MINTS_WARN) {
22
+ out.push({ key: 'serial', icon: '🔁', label: `Dev launched ${a.devMints.toLocaleString()} tokens`, tone: 'bad', tip: 'This deployer has launched many tokens before.' });
23
+ }
24
+ if (t.isVerified)
25
+ out.push({ key: 'verified', icon: '✓', label: 'Verified', tone: 'good', tip: 'On Jupiter\'s verified list.' });
26
+ return out;
27
+ }
28
+ /** 0 (safe) … 3 (danger) summary used for row colouring. */
29
+ export function riskLevel(badges) {
30
+ const bad = badges.filter((b) => b.tone === 'bad').length;
31
+ const warn = badges.filter((b) => b.tone === 'warn').length;
32
+ if (badges.some((b) => b.key === 'unaudited'))
33
+ return 1;
34
+ return Math.min(3, bad * 2 + warn);
35
+ }
@@ -0,0 +1,50 @@
1
+ export interface TokenStats {
2
+ priceChange?: number;
3
+ buyVolume?: number;
4
+ sellVolume?: number;
5
+ numBuys?: number;
6
+ numSells?: number;
7
+ numTraders?: number;
8
+ numNetBuyers?: number;
9
+ }
10
+ export interface TokenAudit {
11
+ mintAuthorityDisabled?: boolean;
12
+ freezeAuthorityDisabled?: boolean;
13
+ topHoldersPercentage?: number;
14
+ devBalancePercentage?: number;
15
+ devMigrations?: number;
16
+ devMints?: number;
17
+ }
18
+ export interface TerminalToken {
19
+ id: string;
20
+ symbol: string;
21
+ name: string;
22
+ icon?: string;
23
+ decimals: number;
24
+ usdPrice?: number;
25
+ mcap?: number;
26
+ fdv?: number;
27
+ liquidity?: number;
28
+ holderCount?: number;
29
+ organicScore?: number;
30
+ organicScoreLabel?: string;
31
+ launchpad?: string;
32
+ graduatedPool?: string;
33
+ graduatedAt?: string;
34
+ firstPool?: {
35
+ id: string;
36
+ createdAt: string;
37
+ };
38
+ createdAt?: string;
39
+ tags?: string[];
40
+ audit?: TokenAudit;
41
+ stats5m?: TokenStats;
42
+ stats1h?: TokenStats;
43
+ stats6h?: TokenStats;
44
+ stats24h?: TokenStats;
45
+ isVerified?: boolean;
46
+ /** Added by the graduating list: estimated bonding-curve progress 0–1. */
47
+ bondingProgress?: number;
48
+ }
49
+ export type ListKind = 'new' | 'trending' | 'top' | 'graduating';
50
+ export declare const LIST_KINDS: ListKind[];
@@ -0,0 +1 @@
1
+ export const LIST_KINDS = ['new', 'trending', 'top', 'graduating'];
@@ -0,0 +1,10 @@
1
+ import { type VersionedTransaction } from '@solana/web3.js';
2
+ export interface Signer {
3
+ publicKey: string;
4
+ sign(tx: VersionedTransaction): VersionedTransaction;
5
+ }
6
+ export declare function parseSecret(raw: string): Uint8Array;
7
+ export declare function loadSigner(opts: {
8
+ privateKey?: string;
9
+ keypairPath?: string;
10
+ }): Signer | null;
package/dist/wallet.js ADDED
@@ -0,0 +1,48 @@
1
+ // PRD P10.2/P10.9 — load the agent's keypair; the secret never leaves this module.
2
+ import { readFileSync } from 'node:fs';
3
+ import { Keypair } from '@solana/web3.js';
4
+ import bs58 from 'bs58';
5
+ import { registerSecret } from './redact.js';
6
+ export function parseSecret(raw) {
7
+ const s = raw.trim();
8
+ if (s.startsWith('[')) {
9
+ const arr = JSON.parse(s);
10
+ if (!Array.isArray(arr) || arr.length !== 64 || !arr.every((n) => Number.isInteger(n) && n >= 0 && n < 256))
11
+ throw new Error('Keypair JSON must be an array of 64 bytes');
12
+ return Uint8Array.from(arr);
13
+ }
14
+ let bytes;
15
+ try {
16
+ bytes = bs58.decode(s);
17
+ }
18
+ catch {
19
+ throw new Error('Private key is not valid base58 or a JSON byte array');
20
+ }
21
+ if (bytes.length === 64)
22
+ return bytes;
23
+ if (bytes.length === 32)
24
+ return Keypair.fromSeed(bytes).secretKey;
25
+ throw new Error(`Private key has ${bytes.length} bytes; expected 64 (or a 32-byte seed)`);
26
+ }
27
+ export function loadSigner(opts) {
28
+ let raw = opts.privateKey;
29
+ if (!raw && opts.keypairPath) {
30
+ try {
31
+ raw = readFileSync(opts.keypairPath, 'utf8');
32
+ }
33
+ catch {
34
+ throw new Error(`Cannot read keypair file at SOLANA_KEYPAIR_PATH`);
35
+ }
36
+ }
37
+ if (!raw)
38
+ return null;
39
+ const kp = Keypair.fromSecretKey(parseSecret(raw));
40
+ // Register every encoding the secret could leak in, not just the one supplied.
41
+ registerSecret(raw.trim());
42
+ registerSecret(bs58.encode(kp.secretKey));
43
+ registerSecret(JSON.stringify(Array.from(kp.secretKey)));
44
+ return {
45
+ publicKey: kp.publicKey.toBase58(),
46
+ sign(tx) { tx.sign([kp]); return tx; },
47
+ };
48
+ }
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@potswap_io/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Potswap MCP server: best-route Solana swaps for AI agents, non-custodial, earns POT.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "potswap-mcp": "dist/index.js"
9
+ },
10
+ "main": "dist/server.js",
11
+ "types": "dist/server.d.ts",
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "dev": "tsx src/index.ts",
23
+ "test": "vitest run",
24
+ "typecheck": "tsc -p tsconfig.json --noEmit",
25
+ "sync-shared": "node scripts/sync-shared.mjs",
26
+ "prepublishOnly": "npm run sync-shared && npm run build"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/Nahargeva/potswap"
31
+ },
32
+ "homepage": "https://www.potswap.io",
33
+ "keywords": [
34
+ "mcp",
35
+ "solana",
36
+ "swap",
37
+ "jupiter",
38
+ "ai-agent",
39
+ "potswap",
40
+ "model-context-protocol"
41
+ ],
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.32.2",
44
+ "@solana/web3.js": "^1.99.0",
45
+ "bs58": "^6.0.0",
46
+ "zod": "^4.6.7"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^26.6.5",
50
+ "tsx": "^4.23.15",
51
+ "typescript": "^7.0.2",
52
+ "vite": "^8.3.5",
53
+ "vitest": "^5.0.5"
54
+ }
55
+ }