@potswap_io/mcp 0.1.0 → 0.3.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/README.md CHANGED
@@ -1,91 +1,132 @@
1
- # @potswap_io/mcp
2
-
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
1
+ # @potswap_io/mcp
2
+
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 2.5% fee inside the swap (1% on swaps of $1,000 or more). Half of its net fees buy and burn POT every epoch, and the fee you paid is credited to your 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
+ | *(none)* | `~/.potswap/wallet.json` | created by `wallet_create`; loaded when neither variable is set |
56
+ | `SOLANA_RPC_URL` | Potswap's RPC proxy | your own RPC if you trade a lot |
57
+ | `POTSWAP_MAX_TRADE_SOL` | `1` | refuse any swap whose input is worth more than this many SOL |
58
+ | `POTSWAP_MAX_SLIPPAGE_BPS` | `300` | refuse requested slippage above this |
59
+ | `POTSWAP_MAX_DAILY_SOL` | `5` | daily SOL-equivalent cap across all strategies |
60
+ | `POTSWAP_STATE_DIR` | `~/.potswap` | where the agent wallet, strategies and run logs are stored |
61
+ | `POTSWAP_ALLOWED_TOKENS` | — | comma-separated mints; when set, every swap, strategy leg and stake must stay inside the list |
62
+ | `POTSWAP_ALLOWED_RECIPIENTS` | — | comma-separated addresses; when set, `transfer` only sends to these |
63
+ | `POTSWAP_DRY_RUN` | `false` | `true` = `swap`, `transfer`, stakes and strategy legs never sign; they return what they would have done |
64
+ | `POTSWAP_API` | `https://www.potswap.io` | base URL |
65
+
66
+ 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.
67
+
68
+ ## Tools
69
+
70
+ | Tool | Does |
71
+ |---|---|
72
+ | `search_tokens {query}` | Find tokens by name/symbol/mint |
73
+ | `get_token {mint}` | Price, mcap, liquidity, holders, audit data, **risk badges** (`riskLevel` 0 safe → 3 danger) |
74
+ | `get_trending {kind}` | `new`, `trending`, `top`, `graduating` lists |
75
+ | `get_quote {inputMint, outputMint, amount}` | Best route, fee, impact, router. `amount` is in UI units (1 = 1 SOL) |
76
+ | `swap {…, confirm: true}` | Executes at the best route and returns the signature, amounts, fee and POT points |
77
+ | `get_balances {wallet?}` | SOL + SPL balances with USD |
78
+ | `get_positions {wallet?}` | Holdings with USD value, average cost, unrealised and realised PnL (from swap history) |
79
+ | `get_points {wallet?}` | Season 1 points, POT allocated / claimable, next unlock |
80
+ | `get_leaderboard` | Top wallets by fee paid |
81
+ | `get_price_history {mint, timeframe, limit}` | OHLCV candles from the token's top pool (`1m`…`1d`) |
82
+
83
+ ### Wallet and operations (0.3.0)
84
+
85
+ | Tool | Does |
86
+ |---|---|
87
+ | `wallet_create {confirm: true}` | Generates a keypair into `POTSWAP_STATE_DIR/wallet.json` (mode 0600) when no wallet is configured. Returns the public key only. **Back the file up**; it is the only copy. |
88
+ | `get_wallet` | Public key, funding instructions, active limits and allowlists, scheduler status |
89
+ | `transfer {to, mint?, amount, confirm: true}` | Sends SOL or any SPL / Token-2022 token, creating the recipient's token account if needed. No Potswap fee. Honours `POTSWAP_ALLOWED_RECIPIENTS`, `POTSWAP_MAX_TRADE_SOL` and the 0.01 SOL reserve |
90
+ | `get_transaction {signature}` | processed / confirmed / finalized / failed / not found, fee, and the wallet's SOL and token balance changes |
91
+ | `get_history {wallet?, limit}` | Recent parsed transactions: swaps (in/out amounts), transfers (direction, counterparty), other |
92
+ | `stake_sol {amount, provider, confirm: true}` | Liquid-stakes SOL into jitoSOL (default) or mSOL at the best route; normal fee and points. `unstake_sol` is the reverse |
93
+
94
+ Agents that start from nothing: `wallet_create` → fund the address → `get_quote` → `swap`. Set `POTSWAP_ALLOWED_TOKENS` and `POTSWAP_ALLOWED_RECIPIENTS` before giving an agent an unattended wallet.
95
+
96
+ 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.
97
+
98
+ ## Strategies: DCA, rebalance, take-profit / stop-loss, limit orders
99
+
100
+ Standing orders that keep running without you. Every leg is an ordinary Potswap swap (fee + POT points).
101
+
102
+ | Tool | Example |
103
+ |---|---|
104
+ | `dca_create` | `{ inputMint: SOL, outputMint: USDC, amountPerRun: 0.1, intervalMinutes: 60, totalRuns: 24, confirm: true }` |
105
+ | `rebalance_create` | `{ targets: { SOL: 60, USDC: 40 }, driftPct: 5, checkIntervalMinutes: 60, confirm: true }` (mints, not symbols) |
106
+ | `tpsl_create` | `{ mint: BONK, sellPct: 50, takeProfitPct: 50, stopLossPct: 20, trailingPct: 15, confirm: true }` |
107
+ | `limit_order_create` | `{ side: 'buy', mint: BONK, amount: 0.1, triggerPriceUsd: 0.00001, condition: 'below', expiresAt?, confirm: true }` — fills once when the price crosses, then completes |
108
+ | `strategy_list`, `strategy_runs {id}`, `strategy_pause/resume/delete {id}` | manage and audit |
109
+
110
+ **Who runs the clock.** Strategies execute inside the MCP server while your host app is open. For 24/7 operation run the daemon on any always-on machine:
111
+
112
+ ```bash
113
+ POTSWAP_STATE_DIR=~/.potswap SOLANA_KEYPAIR_PATH=~/.config/solana/id.json npx -y @potswap_io/mcp --daemon
114
+ ```
115
+
116
+ State lives in `~/.potswap/strategies.json` (override with `POTSWAP_STATE_DIR`); a PID lock makes sure only one process trades. The first leg runs at creation; a window missed during downtime runs once, never as a burst.
117
+
118
+ **Extra limits.** `POTSWAP_MAX_DAILY_SOL` (default `5`) caps the SOL-equivalent all strategies may spend per UTC day; `POTSWAP_MAX_STRATEGIES` (default `10`). After three consecutive failed legs a strategy pauses itself and tells you why. `POTSWAP_DRY_RUN=true` turns every leg into a logged quote.
119
+
120
+ ## Fees and rewards
121
+
122
+ Each swap carries Potswap's 2.5% integrator fee via Jupiter's referral program; Jupiter keeps 20% of it, and 50% of what remains buys and burns POT each epoch. 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
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ cd mcp && npm i && npm test # unit tests over the real MCP protocol (in-memory transport)
128
+ npm run dev # run from source
129
+ npm run sync-shared && npm run build
130
+ ```
131
+
132
+ MIT © Potswap
@@ -0,0 +1,44 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { Connection } from '@solana/web3.js';
3
+ import { type Config } from './config.js';
4
+ import { type TradeContext } from './trade.js';
5
+ import { type Rpc } from './transfer.js';
6
+ /** Transaction-level RPC reads; injectable for tests. */
7
+ export interface TxRpc {
8
+ getSignatureStatus(sig: string): Promise<{
9
+ confirmationStatus?: string;
10
+ err?: unknown;
11
+ slot?: number;
12
+ } | null>;
13
+ getParsedTransaction(sig: string): Promise<{
14
+ slot: number;
15
+ fee: number;
16
+ err: unknown;
17
+ preBalances: number[];
18
+ postBalances: number[];
19
+ accountKeys: string[];
20
+ preTokenBalances: {
21
+ mint: string;
22
+ owner?: string;
23
+ uiTokenAmount: {
24
+ uiAmount: number | null;
25
+ };
26
+ }[];
27
+ postTokenBalances: {
28
+ mint: string;
29
+ owner?: string;
30
+ uiTokenAmount: {
31
+ uiAmount: number | null;
32
+ };
33
+ }[];
34
+ } | null>;
35
+ }
36
+ export declare function txRpcFromConnection(conn: Connection): TxRpc;
37
+ export interface AgentToolDeps {
38
+ trade: TradeContext;
39
+ config: Config;
40
+ rpc?: Rpc;
41
+ txRpc?: TxRpc;
42
+ schedulerActive?: () => boolean;
43
+ }
44
+ export declare function registerAgentTools(server: McpServer, deps: AgentToolDeps): void;
@@ -0,0 +1,179 @@
1
+ import { Connection } from '@solana/web3.js';
2
+ import { z } from 'zod';
3
+ import { JITOSOL_MINT, MSOL_MINT, NO_WALLET, SOL_MINT } from './config.js';
4
+ import { redact, redactValue } from './redact.js';
5
+ import { createWallet } from './wallet.js';
6
+ import { executeSwap } from './trade.js';
7
+ import { executeTransfer, rpcFromConnection } from './transfer.js';
8
+ import { mapSignatureStatus, normalizeHistory } from './history.js';
9
+ import { computePositions } from './pnl.js';
10
+ const ok = (data, summary) => ({ content: [{ type: 'text', text: redact(summary ?? JSON.stringify(data, null, 2)) }], structuredContent: redactValue(data) });
11
+ const fail = (message, extra = {}) => ({ content: [{ type: 'text', text: redact(message) }], structuredContent: redactValue({ error: message, ...extra }), isError: true });
12
+ const MINT = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/);
13
+ export function txRpcFromConnection(conn) {
14
+ return {
15
+ async getSignatureStatus(sig) { const r = await conn.getSignatureStatuses([sig], { searchTransactionHistory: true }); const s = r.value[0]; return s ? { confirmationStatus: s.confirmationStatus ?? undefined, err: s.err, slot: s.slot } : null; },
16
+ async getParsedTransaction(sig) {
17
+ const t = await conn.getParsedTransaction(sig, { maxSupportedTransactionVersion: 0, commitment: 'confirmed' });
18
+ if (!t?.meta)
19
+ return null;
20
+ return {
21
+ slot: t.slot, fee: t.meta.fee, err: t.meta.err, preBalances: t.meta.preBalances, postBalances: t.meta.postBalances,
22
+ accountKeys: t.transaction.message.accountKeys.map((k) => k.pubkey.toBase58()),
23
+ preTokenBalances: (t.meta.preTokenBalances ?? []).map((b) => ({ mint: b.mint, owner: b.owner, uiTokenAmount: { uiAmount: b.uiTokenAmount.uiAmount } })),
24
+ postTokenBalances: (t.meta.postTokenBalances ?? []).map((b) => ({ mint: b.mint, owner: b.owner, uiTokenAmount: { uiAmount: b.uiTokenAmount.uiAmount } })),
25
+ };
26
+ },
27
+ };
28
+ }
29
+ export function registerAgentTools(server, deps) {
30
+ const { trade, config } = deps;
31
+ const api = trade.api;
32
+ const conn = () => new Connection(config.rpcUrl, 'confirmed');
33
+ const rpc = deps.rpc ?? rpcFromConnection(conn());
34
+ const txRpc = deps.txRpc ?? txRpcFromConnection(conn());
35
+ const walletOr = (w) => w ?? trade.signer?.publicKey ?? null;
36
+ server.registerTool('wallet_create', {
37
+ title: 'Create an agent wallet',
38
+ description: 'Generates a new keypair and stores it in the Potswap state directory (0600). Only when no wallet is configured. Returns the public key; the secret key is never shown. Back up the file.',
39
+ inputSchema: { confirm: z.boolean().default(false) },
40
+ }, async ({ confirm }) => {
41
+ if (trade.signer)
42
+ return fail(`A wallet is already configured (${trade.signer.publicKey}). Nothing was created.`);
43
+ if (!confirm)
44
+ return fail('Set confirm=true to create a wallet. Nothing was created.');
45
+ try {
46
+ const { signer, file } = createWallet(config.stateDir);
47
+ trade.signer = signer;
48
+ return ok({ publicKey: signer.publicKey, file, backup: 'Copy this file somewhere safe; it is the only copy of the secret key.' }, `Wallet created: ${signer.publicKey}\nSecret stored at ${file} (0600). Fund it with SOL to start; keep ≥ 0.01 SOL for fees.`);
49
+ }
50
+ catch (e) {
51
+ return fail(e instanceof Error ? e.message : String(e));
52
+ }
53
+ });
54
+ server.registerTool('get_wallet', {
55
+ title: 'Wallet, limits and status',
56
+ description: 'Public key, funding instructions, configured limits and allowlists, scheduler status.',
57
+ inputSchema: {},
58
+ }, async () => ok({
59
+ publicKey: trade.signer?.publicKey ?? null,
60
+ fund: trade.signer ? `Send SOL to ${trade.signer.publicKey}. Keep at least 0.01 SOL for network fees.` : NO_WALLET,
61
+ limits: { maxTradeSol: config.maxTradeSol, maxDailySol: config.maxDailySol, maxSlippageBps: config.maxSlippageBps, dryRun: config.dryRun },
62
+ allowedTokens: [...config.allowedTokens], allowedRecipients: [...config.allowedRecipients],
63
+ schedulerActive: !!deps.schedulerActive?.(), stateDir: config.stateDir, api: config.apiBase, rpc: config.rpcUrl.replace(/api-key=[^&]+/, 'api-key=***'),
64
+ }));
65
+ server.registerTool('transfer', {
66
+ title: 'Transfer SOL or a token (no Potswap fee)',
67
+ description: 'Sends SOL or an SPL token from the agent wallet. Requires confirm=true. Subject to POTSWAP_ALLOWED_RECIPIENTS, the per-trade cap and the SOL reserve. Every error states whether funds were debited.',
68
+ inputSchema: { to: z.string().min(32).max(44), mint: MINT.optional(), amount: z.number().positive(), confirm: z.boolean().default(false) },
69
+ }, async (a) => {
70
+ const out = await executeTransfer({
71
+ config, signer: trade.signer, rpc, balances: trade.balances, prices: (m) => api.prices(m),
72
+ decimalsOf: async (m) => (await api.token(m)).decimals,
73
+ }, { to: a.to, mint: a.mint, amount: a.amount, confirm: a.confirm });
74
+ if (out.kind === 'ok')
75
+ return ok({ signature: out.signature, explorerUrl: out.explorerUrl }, `${out.summary}. ${out.explorerUrl}`);
76
+ if (out.kind === 'quote-only' || out.kind === 'dry-run')
77
+ return ok({ [out.kind === 'dry-run' ? 'dryRun' : 'confirmRequired']: true }, out.summary);
78
+ return fail(out.message);
79
+ });
80
+ server.registerTool('get_transaction', {
81
+ title: 'Transaction status',
82
+ description: 'Status (processed / confirmed / finalized / failed / not found), slot, fee and the wallet balance changes for a signature.',
83
+ inputSchema: { signature: z.string().min(64).max(90), wallet: MINT.optional() },
84
+ }, async ({ signature, wallet }) => {
85
+ try {
86
+ const st = await txRpc.getSignatureStatus(signature);
87
+ const status = mapSignatureStatus(st);
88
+ const data = { signature, status, slot: st?.slot ?? null, explorerUrl: `https://solscan.io/tx/${signature}` };
89
+ if (status === 'confirmed' || status === 'finalized' || status === 'failed') {
90
+ const t = await txRpc.getParsedTransaction(signature);
91
+ if (t) {
92
+ const w = walletOr(wallet);
93
+ const idx = w ? t.accountKeys.indexOf(w) : -1;
94
+ data.feeSol = t.fee / 1e9;
95
+ data.error = t.err ?? null;
96
+ if (idx >= 0)
97
+ data.solChange = (t.postBalances[idx] - t.preBalances[idx]) / 1e9;
98
+ if (w) {
99
+ const pre = Object.fromEntries(t.preTokenBalances.filter((b) => b.owner === w).map((b) => [b.mint, b.uiTokenAmount.uiAmount ?? 0]));
100
+ const post = Object.fromEntries(t.postTokenBalances.filter((b) => b.owner === w).map((b) => [b.mint, b.uiTokenAmount.uiAmount ?? 0]));
101
+ const mints = new Set([...Object.keys(pre), ...Object.keys(post)]);
102
+ data.tokenChanges = [...mints].map((m) => ({ mint: m, change: (post[m] ?? 0) - (pre[m] ?? 0) })).filter((c) => c.change !== 0);
103
+ }
104
+ }
105
+ }
106
+ return ok(data, `${signature.slice(0, 8)}… ${status}${data.solChange !== undefined ? ` · SOL ${data.solChange >= 0 ? '+' : ''}${data.solChange}` : ''}`);
107
+ }
108
+ catch (e) {
109
+ return fail(e instanceof Error ? e.message : String(e));
110
+ }
111
+ });
112
+ server.registerTool('get_history', {
113
+ title: 'Recent transactions',
114
+ description: 'Parsed recent transactions for a wallet (swaps, transfers, other), newest first.',
115
+ inputSchema: { wallet: MINT.optional(), limit: z.number().int().min(1).max(100).default(25) },
116
+ }, async ({ wallet, limit }) => {
117
+ const w = walletOr(wallet);
118
+ if (!w)
119
+ return fail(NO_WALLET);
120
+ try {
121
+ const raw = await api.history(w, limit);
122
+ const items = normalizeHistory(raw, w);
123
+ return ok({ wallet: w, items }, items.map((i) => `${i.time} ${i.kind}${i.failed ? ' (failed)' : ''}: ${i.kind === 'swap' ? `${i.inAmount} ${short(i.inMint)} → ${i.outAmount} ${short(i.outMint)}` : i.kind === 'transfer' ? `${i.direction} ${i.amount} ${short(i.mint)} ${i.direction === 'out' ? 'to' : 'from'} ${i.counterparty}` : i.source ?? ''} ${i.signature.slice(0, 8)}…`).join('\n') || 'No transactions yet.');
124
+ }
125
+ catch (e) {
126
+ return fail(e instanceof Error ? e.message : String(e));
127
+ }
128
+ });
129
+ server.registerTool('get_price_history', {
130
+ title: 'Price history (OHLCV)',
131
+ description: 'Candles for a token from its top pool. Timeframes: 1m, 5m, 15m, 1h, 4h, 1d.',
132
+ inputSchema: { mint: MINT, timeframe: z.enum(['1m', '5m', '15m', '1h', '4h', '1d']).default('15m'), limit: z.number().int().min(1).max(1000).default(100) },
133
+ }, async ({ mint, timeframe, limit }) => {
134
+ try {
135
+ const d = await api.ohlcv(mint, timeframe, limit);
136
+ const c = d.candles;
137
+ return ok(d, c.length ? `${c.length} ${timeframe} candles · last close ${c[c.length - 1].close} · range ${Math.min(...c.map((x) => x.low))}–${Math.max(...c.map((x) => x.high))}` : d.reason ?? 'No candles');
138
+ }
139
+ catch (e) {
140
+ return fail(e instanceof Error ? e.message : String(e));
141
+ }
142
+ });
143
+ const stakeTool = (name, title, description, from, to) => server.registerTool(name, { title, description, inputSchema: { amount: z.number().positive(), provider: z.enum(['jito', 'marinade']).default('jito'), confirm: z.boolean().default(false), maxSlippageBps: z.number().int().min(1).max(5000).optional() } }, async (a) => {
144
+ const out = await executeSwap(trade, { inputMint: from(a.provider), outputMint: to(a.provider), amount: a.amount, slippageBps: a.maxSlippageBps, confirm: a.confirm });
145
+ switch (out.kind) {
146
+ case 'no-wallet': return fail(out.message);
147
+ case 'refused': return fail(out.message, { quote: out.quote });
148
+ case 'quote-only': return ok({ confirmRequired: true, quote: out.quote }, `Quote only (confirm=true to execute): ${a.amount} ${out.quote.inputSymbol} → ${out.quote.outAmount} ${out.quote.outputSymbol}`);
149
+ case 'dry-run': return ok({ dryRun: true, quote: out.quote }, `DRY RUN: ${a.amount} ${out.quote.inputSymbol} → ${out.quote.outAmount} ${out.quote.outputSymbol}`);
150
+ case 'failed': return fail(out.message, { signature: out.signature });
151
+ case 'ok': return ok({ ...out.result }, `${name === 'stake_sol' ? 'Staked' : 'Unstaked'}: ${a.amount} ${out.result.inputSymbol} → ${out.result.outAmount} ${out.result.outputSymbol}. ${out.result.explorerUrl}`);
152
+ }
153
+ });
154
+ const lst = (p) => (p === 'jito' ? JITOSOL_MINT : MSOL_MINT);
155
+ stakeTool('stake_sol', 'Liquid-stake SOL', 'Swaps SOL into jitoSOL (Jito) or mSOL (Marinade) at the best route. Normal fee and points apply. Requires confirm=true.', () => SOL_MINT, lst);
156
+ stakeTool('unstake_sol', 'Unstake liquid SOL', 'Swaps jitoSOL or mSOL back into SOL at the best route. Normal fee and points apply. Requires confirm=true.', lst, () => SOL_MINT);
157
+ // Upgraded positions with PnL (P13.7): replaces the basic tool registered by the core server.
158
+ server.registerTool('get_positions', {
159
+ title: 'Positions with PnL',
160
+ description: 'Token holdings with USD value, average cost, unrealised and realised PnL from the wallet\'s swap history.',
161
+ inputSchema: { wallet: MINT.optional() },
162
+ }, async ({ wallet }) => {
163
+ const w = walletOr(wallet);
164
+ if (!w)
165
+ return fail(NO_WALLET);
166
+ try {
167
+ const [bal, raw] = await Promise.all([trade.balances(w), api.history(w, 100).catch(() => [])]);
168
+ const history = normalizeHistory(raw, w);
169
+ const mints = [...new Set([...Object.keys(bal), ...history.flatMap((h) => [h.inMint, h.outMint]).filter((m) => !!m)])].slice(0, 50);
170
+ const prices = await api.prices(mints).catch(() => ({}));
171
+ const positions = computePositions(history, bal, prices);
172
+ return ok({ wallet: w, sol: bal[SOL_MINT] ?? 0, solUsd: prices[SOL_MINT] ? (bal[SOL_MINT] ?? 0) * prices[SOL_MINT] : undefined, positions }, positions.map((p) => `${short(p.mint)}: ${p.amount}${p.valueUsd !== undefined ? ` ($${p.valueUsd.toFixed(2)})` : ''}${p.unrealisedUsd !== undefined ? ` unrealised ${p.unrealisedUsd >= 0 ? '+' : ''}$${p.unrealisedUsd.toFixed(2)}` : ''}${p.realisedUsd ? ` realised ${p.realisedUsd >= 0 ? '+' : ''}$${p.realisedUsd.toFixed(2)}` : ''}`).join('\n') || 'No positions.');
173
+ }
174
+ catch (e) {
175
+ return fail(e instanceof Error ? e.message : String(e));
176
+ }
177
+ });
178
+ }
179
+ const short = (m) => (!m ? '?' : m === SOL_MINT ? 'SOL' : m === JITOSOL_MINT ? 'jitoSOL' : m === MSOL_MINT ? 'mSOL' : m.slice(0, 4) + '…');
package/dist/api.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ExecuteResponse, OrderResponse, Token } from './shared/lib-types.js';
2
2
  import type { TerminalToken } from './shared/types.js';
3
+ import { type FeeSchedule } from './shared/fee.js';
3
4
  export type FetchFn = typeof fetch;
4
5
  export declare class ApiError extends Error {
5
6
  status: number;
@@ -8,6 +9,8 @@ export declare class ApiError extends Error {
8
9
  export interface PublicConfig {
9
10
  referralAccount: string | null;
10
11
  referralFeeBps: number;
12
+ fee: FeeSchedule;
13
+ burnShare: number;
11
14
  seasonStart: string | null;
12
15
  pointsEnabled: boolean;
13
16
  }
@@ -25,6 +28,7 @@ export declare class PotswapApi {
25
28
  amount: string;
26
29
  taker?: string;
27
30
  slippageBps?: number;
31
+ feeBps?: number;
28
32
  }): Promise<OrderResponse>;
29
33
  execute(signedTransaction: string, requestId: string): Promise<ExecuteResponse>;
30
34
  searchTokens(query: string): Promise<Token[]>;
@@ -33,5 +37,22 @@ export declare class PotswapApi {
33
37
  list(kind: string): Promise<TerminalToken[]>;
34
38
  terminalToken(mint: string): Promise<TerminalToken>;
35
39
  points(wallet: string): Promise<Record<string, unknown>>;
40
+ history(wallet: string, limit?: number): Promise<import("./history.js").HeliusTx[]>;
41
+ ohlcv(mint: string, tf: string, limit?: number): Promise<{
42
+ mint: string;
43
+ pool: {
44
+ id: string;
45
+ name: string;
46
+ } | null;
47
+ candles: {
48
+ time: number;
49
+ open: number;
50
+ high: number;
51
+ low: number;
52
+ close: number;
53
+ volume: number;
54
+ }[];
55
+ reason?: string;
56
+ }>;
36
57
  leaderboard(): Promise<Record<string, unknown>>;
37
58
  }
package/dist/api.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_FEE_SCHEDULE, parseFeeSchedule } from './shared/fee.js';
1
2
  export class ApiError extends Error {
2
3
  status;
3
4
  constructor(status, message) { super(message); this.status = status; }
@@ -36,10 +37,11 @@ export class PotswapApi {
36
37
  return this.configCache.value;
37
38
  let value;
38
39
  try {
39
- value = await this.req('/api/config');
40
+ const raw = await this.req('/api/config');
41
+ value = { referralAccount: raw.referralAccount ?? null, referralFeeBps: raw.referralFeeBps ?? 0, fee: parseFeeSchedule(raw.fee), burnShare: raw.burnShare ?? 0.5, seasonStart: raw.seasonStart ?? null, pointsEnabled: !!raw.pointsEnabled };
40
42
  }
41
43
  catch {
42
- value = { referralAccount: null, referralFeeBps: 0, seasonStart: null, pointsEnabled: false };
44
+ value = { referralAccount: null, referralFeeBps: 0, fee: DEFAULT_FEE_SCHEDULE, burnShare: 0.5, seasonStart: null, pointsEnabled: false };
43
45
  }
44
46
  this.configCache = { value, at: Date.now() };
45
47
  return value;
@@ -53,7 +55,7 @@ export class PotswapApi {
53
55
  q.set('slippageBps', String(p.slippageBps));
54
56
  if (cfg.referralAccount) {
55
57
  q.set('referralAccount', cfg.referralAccount);
56
- q.set('referralFee', String(cfg.referralFeeBps));
58
+ q.set('referralFee', String(p.feeBps ?? cfg.fee.standardBps));
57
59
  }
58
60
  return this.req(`/api/jup/swap/v2/order?${q}`);
59
61
  }
@@ -85,5 +87,7 @@ export class PotswapApi {
85
87
  list(kind) { return this.req(`/api/terminal/list/${kind}`); }
86
88
  terminalToken(mint) { return this.req(`/api/terminal/token/${mint}`); }
87
89
  points(wallet) { return this.req(`/api/points/${wallet}`); }
90
+ history(wallet, limit = 25) { return this.req(`/api/history/${wallet}?limit=${limit}`); }
91
+ ohlcv(mint, tf, limit = 100) { return this.req(`/api/terminal/ohlcv/${mint}?tf=${tf}&limit=${limit}`); }
88
92
  leaderboard() { return this.req('/api/leaderboard'); }
89
93
  }
package/dist/config.d.ts CHANGED
@@ -2,18 +2,31 @@ export interface Config {
2
2
  apiBase: string;
3
3
  rpcUrl: string;
4
4
  maxTradeSol: number;
5
+ /** SOL-equivalent all strategies together may spend per UTC day (PRD P11.6). */
6
+ maxDailySol: number;
5
7
  maxSlippageBps: number;
6
8
  dryRun: boolean;
7
9
  privateKey?: string;
8
10
  keypairPath?: string;
11
+ /** Where strategies, run logs and a created wallet live. */
12
+ stateDir: string;
13
+ /** When non-empty, swaps/strategies/staking may only touch these mints (PRD P13.4). */
14
+ allowedTokens: Set<string>;
15
+ /** When non-empty, `transfer` may only send to these addresses (PRD P13.3). */
16
+ allowedRecipients: Set<string>;
9
17
  }
10
18
  export declare const DEFAULTS: {
11
19
  apiBase: string;
12
20
  maxTradeSol: number;
21
+ maxDailySol: number;
13
22
  maxSlippageBps: number;
14
23
  solReserve: number;
15
24
  };
16
25
  export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
17
26
  export declare const SOL_MINT = "So11111111111111111111111111111111111111112";
18
27
  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.";
28
+ export declare const JITOSOL_MINT = "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn";
29
+ export declare const MSOL_MINT = "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So";
30
+ export declare const NO_WALLET = "Wallet not configured. Set SOLANA_PRIVATE_KEY or SOLANA_KEYPAIR_PATH, or run wallet_create. No funds were debited.";
31
+ export declare const TOKEN_NOT_ALLOWED = "Token not in POTSWAP_ALLOWED_TOKENS. No funds were debited.";
32
+ export declare const RECIPIENT_NOT_ALLOWED = "Recipient not in POTSWAP_ALLOWED_RECIPIENTS. No funds were debited.";
package/dist/config.js CHANGED
@@ -1,9 +1,14 @@
1
+ // PRD P10 / P13 — configuration from environment variables.
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
1
4
  export const DEFAULTS = {
2
5
  apiBase: 'https://www.potswap.io',
3
6
  maxTradeSol: 1,
7
+ maxDailySol: 5,
4
8
  maxSlippageBps: 300,
5
9
  solReserve: 0.01,
6
10
  };
11
+ const list = (v) => new Set((v ?? '').split(',').map((s) => s.trim()).filter(Boolean));
7
12
  export function loadConfig(env = process.env) {
8
13
  const apiBase = (env.POTSWAP_API || DEFAULTS.apiBase).replace(/\/$/, '');
9
14
  const num = (v, d) => { const n = Number(v); return v !== undefined && v !== '' && Number.isFinite(n) && n > 0 ? n : d; };
@@ -11,12 +16,20 @@ export function loadConfig(env = process.env) {
11
16
  apiBase,
12
17
  rpcUrl: env.SOLANA_RPC_URL || `${apiBase}/api/rpc`,
13
18
  maxTradeSol: num(env.POTSWAP_MAX_TRADE_SOL, DEFAULTS.maxTradeSol),
19
+ maxDailySol: num(env.POTSWAP_MAX_DAILY_SOL, DEFAULTS.maxDailySol),
14
20
  maxSlippageBps: num(env.POTSWAP_MAX_SLIPPAGE_BPS, DEFAULTS.maxSlippageBps),
15
21
  dryRun: /^(1|true|yes)$/i.test(env.POTSWAP_DRY_RUN ?? ''),
16
22
  privateKey: env.SOLANA_PRIVATE_KEY || undefined,
17
23
  keypairPath: env.SOLANA_KEYPAIR_PATH || undefined,
24
+ stateDir: env.POTSWAP_STATE_DIR || join(homedir(), '.potswap'),
25
+ allowedTokens: list(env.POTSWAP_ALLOWED_TOKENS),
26
+ allowedRecipients: list(env.POTSWAP_ALLOWED_RECIPIENTS),
18
27
  };
19
28
  }
20
29
  export const SOL_MINT = 'So11111111111111111111111111111111111111112';
21
30
  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.';
31
+ export const JITOSOL_MINT = 'J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn';
32
+ export const MSOL_MINT = 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So';
33
+ export const NO_WALLET = 'Wallet not configured. Set SOLANA_PRIVATE_KEY or SOLANA_KEYPAIR_PATH, or run wallet_create. No funds were debited.';
34
+ export const TOKEN_NOT_ALLOWED = 'Token not in POTSWAP_ALLOWED_TOKENS. No funds were debited.';
35
+ export const RECIPIENT_NOT_ALLOWED = 'Recipient not in POTSWAP_ALLOWED_RECIPIENTS. No funds were debited.';