@potswap_io/mcp 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +114 -3
- package/dist/api.d.ts +41 -0
- package/dist/api.js +91 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +24 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -0
- package/dist/limits.d.ts +13 -0
- package/dist/limits.js +23 -0
- package/dist/redact.d.ts +5 -0
- package/dist/redact.js +23 -0
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +39 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +201 -0
- package/dist/shared/errors.d.ts +5 -0
- package/dist/shared/errors.js +24 -0
- package/dist/shared/fee.d.ts +24 -0
- package/dist/shared/fee.js +42 -0
- package/dist/shared/lib-types.d.ts +42 -0
- package/dist/shared/lib-types.js +1 -0
- package/dist/shared/risk.d.ts +15 -0
- package/dist/shared/risk.js +35 -0
- package/dist/shared/types.d.ts +50 -0
- package/dist/shared/types.js +1 -0
- package/dist/strategies/engine.d.ts +40 -0
- package/dist/strategies/engine.js +100 -0
- package/dist/strategies/lock.d.ts +7 -0
- package/dist/strategies/lock.js +30 -0
- package/dist/strategies/scheduler.d.ts +38 -0
- package/dist/strategies/scheduler.js +219 -0
- package/dist/strategies/store.d.ts +26 -0
- package/dist/strategies/store.js +53 -0
- package/dist/strategies/tools.d.ts +10 -0
- package/dist/strategies/tools.js +143 -0
- package/dist/strategies/types.d.ts +68 -0
- package/dist/strategies/types.js +2 -0
- package/dist/trade.d.ts +86 -0
- package/dist/trade.js +83 -0
- package/dist/wallet.d.ts +10 -0
- package/dist/wallet.js +48 -0
- package/package.json +58 -6
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,114 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
| `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_MAX_DAILY_SOL` | `5` | daily SOL-equivalent cap across all strategies |
|
|
59
|
+
| `POTSWAP_STATE_DIR` | `~/.potswap` | where strategies and run logs are stored |
|
|
60
|
+
| `POTSWAP_DRY_RUN` | `false` | `true` = `swap` never signs; returns what it would have done |
|
|
61
|
+
| `POTSWAP_API` | `https://www.potswap.io` | base URL |
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
## Tools
|
|
66
|
+
|
|
67
|
+
| Tool | Does |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `search_tokens {query}` | Find tokens by name/symbol/mint |
|
|
70
|
+
| `get_token {mint}` | Price, mcap, liquidity, holders, audit data, **risk badges** (`riskLevel` 0 safe → 3 danger) |
|
|
71
|
+
| `get_trending {kind}` | `new`, `trending`, `top`, `graduating` lists |
|
|
72
|
+
| `get_quote {inputMint, outputMint, amount}` | Best route, fee, impact, router. `amount` is in UI units (1 = 1 SOL) |
|
|
73
|
+
| `swap {…, confirm: true}` | Executes at the best route and returns the signature, amounts, fee and POT points |
|
|
74
|
+
| `get_balances {wallet?}` | SOL + SPL balances with USD |
|
|
75
|
+
| `get_positions {wallet?}` | Holdings with USD; PnL coming |
|
|
76
|
+
| `get_points {wallet?}` | Season 1 points, POT allocated / claimable, next unlock |
|
|
77
|
+
| `get_leaderboard` | Top wallets by fee paid |
|
|
78
|
+
|
|
79
|
+
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.
|
|
80
|
+
|
|
81
|
+
## Strategies: DCA, rebalance, take-profit / stop-loss
|
|
82
|
+
|
|
83
|
+
Standing orders that keep running without you. Every leg is an ordinary Potswap swap (fee + POT points).
|
|
84
|
+
|
|
85
|
+
| Tool | Example |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `dca_create` | `{ inputMint: SOL, outputMint: USDC, amountPerRun: 0.1, intervalMinutes: 60, totalRuns: 24, confirm: true }` |
|
|
88
|
+
| `rebalance_create` | `{ targets: { SOL: 60, USDC: 40 }, driftPct: 5, checkIntervalMinutes: 60, confirm: true }` (mints, not symbols) |
|
|
89
|
+
| `tpsl_create` | `{ mint: BONK, sellPct: 50, takeProfitPct: 50, stopLossPct: 20, trailingPct: 15, confirm: true }` |
|
|
90
|
+
| `strategy_list`, `strategy_runs {id}`, `strategy_pause/resume/delete {id}` | manage and audit |
|
|
91
|
+
|
|
92
|
+
**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:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
POTSWAP_STATE_DIR=~/.potswap SOLANA_KEYPAIR_PATH=~/.config/solana/id.json npx -y @potswap_io/mcp --daemon
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
**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.
|
|
101
|
+
|
|
102
|
+
## Fees and rewards
|
|
103
|
+
|
|
104
|
+
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
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
cd mcp && npm i && npm test # unit tests over the real MCP protocol (in-memory transport)
|
|
110
|
+
npm run dev # run from source
|
|
111
|
+
npm run sync-shared && npm run build
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
MIT © Potswap
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExecuteResponse, OrderResponse, Token } from './shared/lib-types.js';
|
|
2
|
+
import type { TerminalToken } from './shared/types.js';
|
|
3
|
+
import { type FeeSchedule } from './shared/fee.js';
|
|
4
|
+
export type FetchFn = typeof fetch;
|
|
5
|
+
export declare class ApiError extends Error {
|
|
6
|
+
status: number;
|
|
7
|
+
constructor(status: number, message: string);
|
|
8
|
+
}
|
|
9
|
+
export interface PublicConfig {
|
|
10
|
+
referralAccount: string | null;
|
|
11
|
+
referralFeeBps: number;
|
|
12
|
+
fee: FeeSchedule;
|
|
13
|
+
burnShare: number;
|
|
14
|
+
seasonStart: string | null;
|
|
15
|
+
pointsEnabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare class PotswapApi {
|
|
18
|
+
private base;
|
|
19
|
+
private fetchFn;
|
|
20
|
+
private configCache;
|
|
21
|
+
private tokenCache;
|
|
22
|
+
constructor(base: string, fetchFn?: FetchFn);
|
|
23
|
+
private req;
|
|
24
|
+
config(): Promise<PublicConfig>;
|
|
25
|
+
order(p: {
|
|
26
|
+
inputMint: string;
|
|
27
|
+
outputMint: string;
|
|
28
|
+
amount: string;
|
|
29
|
+
taker?: string;
|
|
30
|
+
slippageBps?: number;
|
|
31
|
+
feeBps?: number;
|
|
32
|
+
}): Promise<OrderResponse>;
|
|
33
|
+
execute(signedTransaction: string, requestId: string): Promise<ExecuteResponse>;
|
|
34
|
+
searchTokens(query: string): Promise<Token[]>;
|
|
35
|
+
token(mint: string): Promise<Token>;
|
|
36
|
+
prices(mints: string[]): Promise<Record<string, number>>;
|
|
37
|
+
list(kind: string): Promise<TerminalToken[]>;
|
|
38
|
+
terminalToken(mint: string): Promise<TerminalToken>;
|
|
39
|
+
points(wallet: string): Promise<Record<string, unknown>>;
|
|
40
|
+
leaderboard(): Promise<Record<string, unknown>>;
|
|
41
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { DEFAULT_FEE_SCHEDULE, parseFeeSchedule } from './shared/fee.js';
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(status, message) { super(message); this.status = status; }
|
|
5
|
+
}
|
|
6
|
+
export class PotswapApi {
|
|
7
|
+
base;
|
|
8
|
+
fetchFn;
|
|
9
|
+
configCache = null;
|
|
10
|
+
tokenCache = new Map();
|
|
11
|
+
constructor(base, fetchFn = fetch) { this.base = base; this.fetchFn = fetchFn; }
|
|
12
|
+
async req(path, init, retry = true) {
|
|
13
|
+
const res = await this.fetchFn(this.base + path, { ...init, headers: { accept: 'application/json', ...init?.headers } });
|
|
14
|
+
if (res.status === 429 || res.status === 503) {
|
|
15
|
+
if (retry) {
|
|
16
|
+
await new Promise((r) => setTimeout(r, 1100));
|
|
17
|
+
return this.req(path, init, false);
|
|
18
|
+
}
|
|
19
|
+
throw new ApiError(res.status, 'Potswap is busy; retry in a few seconds. No funds were debited.');
|
|
20
|
+
}
|
|
21
|
+
const text = await res.text();
|
|
22
|
+
let data = null;
|
|
23
|
+
try {
|
|
24
|
+
data = text ? JSON.parse(text) : null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
data = { error: text };
|
|
28
|
+
}
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
const msg = data?.error ?? data?.message ?? `HTTP ${res.status}`;
|
|
31
|
+
throw new ApiError(res.status, msg);
|
|
32
|
+
}
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
async config() {
|
|
36
|
+
if (this.configCache && Date.now() - this.configCache.at < 300_000)
|
|
37
|
+
return this.configCache.value;
|
|
38
|
+
let value;
|
|
39
|
+
try {
|
|
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 };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
value = { referralAccount: null, referralFeeBps: 0, fee: DEFAULT_FEE_SCHEDULE, burnShare: 0.5, seasonStart: null, pointsEnabled: false };
|
|
45
|
+
}
|
|
46
|
+
this.configCache = { value, at: Date.now() };
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
async order(p) {
|
|
50
|
+
const cfg = await this.config();
|
|
51
|
+
const q = new URLSearchParams({ inputMint: p.inputMint, outputMint: p.outputMint, amount: p.amount });
|
|
52
|
+
if (p.taker)
|
|
53
|
+
q.set('taker', p.taker);
|
|
54
|
+
if (p.slippageBps !== undefined)
|
|
55
|
+
q.set('slippageBps', String(p.slippageBps));
|
|
56
|
+
if (cfg.referralAccount) {
|
|
57
|
+
q.set('referralAccount', cfg.referralAccount);
|
|
58
|
+
q.set('referralFee', String(p.feeBps ?? cfg.fee.standardBps));
|
|
59
|
+
}
|
|
60
|
+
return this.req(`/api/jup/swap/v2/order?${q}`);
|
|
61
|
+
}
|
|
62
|
+
execute(signedTransaction, requestId) {
|
|
63
|
+
return this.req('/api/jup/swap/v2/execute', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ signedTransaction, requestId }) });
|
|
64
|
+
}
|
|
65
|
+
async searchTokens(query) {
|
|
66
|
+
const list = await this.req(`/api/jup/tokens/v2/search?query=${encodeURIComponent(query)}`);
|
|
67
|
+
for (const t of list)
|
|
68
|
+
this.tokenCache.set(t.id, t);
|
|
69
|
+
return list;
|
|
70
|
+
}
|
|
71
|
+
async token(mint) {
|
|
72
|
+
const hit = this.tokenCache.get(mint);
|
|
73
|
+
if (hit)
|
|
74
|
+
return hit;
|
|
75
|
+
const list = await this.searchTokens(mint);
|
|
76
|
+
const t = list.find((x) => x.id === mint);
|
|
77
|
+
if (!t)
|
|
78
|
+
throw new ApiError(404, `Unknown token ${mint}`);
|
|
79
|
+
return t;
|
|
80
|
+
}
|
|
81
|
+
async prices(mints) {
|
|
82
|
+
if (!mints.length)
|
|
83
|
+
return {};
|
|
84
|
+
const d = await this.req(`/api/price/v3?ids=${mints.join(',')}`.replace('/api/price', '/api/jup/price'));
|
|
85
|
+
return Object.fromEntries(Object.entries(d).map(([k, v]) => [k, v.usdPrice]));
|
|
86
|
+
}
|
|
87
|
+
list(kind) { return this.req(`/api/terminal/list/${kind}`); }
|
|
88
|
+
terminalToken(mint) { return this.req(`/api/terminal/token/${mint}`); }
|
|
89
|
+
points(wallet) { return this.req(`/api/points/${wallet}`); }
|
|
90
|
+
leaderboard() { return this.req('/api/leaderboard'); }
|
|
91
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
apiBase: string;
|
|
3
|
+
rpcUrl: string;
|
|
4
|
+
maxTradeSol: number;
|
|
5
|
+
/** SOL-equivalent all strategies together may spend per UTC day (PRD P11.6). */
|
|
6
|
+
maxDailySol: number;
|
|
7
|
+
maxSlippageBps: number;
|
|
8
|
+
dryRun: boolean;
|
|
9
|
+
privateKey?: string;
|
|
10
|
+
keypairPath?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const DEFAULTS: {
|
|
13
|
+
apiBase: string;
|
|
14
|
+
maxTradeSol: number;
|
|
15
|
+
maxDailySol: number;
|
|
16
|
+
maxSlippageBps: number;
|
|
17
|
+
solReserve: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
|
|
20
|
+
export declare const SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
21
|
+
export declare const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
22
|
+
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,24 @@
|
|
|
1
|
+
export const DEFAULTS = {
|
|
2
|
+
apiBase: 'https://www.potswap.io',
|
|
3
|
+
maxTradeSol: 1,
|
|
4
|
+
maxDailySol: 5,
|
|
5
|
+
maxSlippageBps: 300,
|
|
6
|
+
solReserve: 0.01,
|
|
7
|
+
};
|
|
8
|
+
export function loadConfig(env = process.env) {
|
|
9
|
+
const apiBase = (env.POTSWAP_API || DEFAULTS.apiBase).replace(/\/$/, '');
|
|
10
|
+
const num = (v, d) => { const n = Number(v); return v !== undefined && v !== '' && Number.isFinite(n) && n > 0 ? n : d; };
|
|
11
|
+
return {
|
|
12
|
+
apiBase,
|
|
13
|
+
rpcUrl: env.SOLANA_RPC_URL || `${apiBase}/api/rpc`,
|
|
14
|
+
maxTradeSol: num(env.POTSWAP_MAX_TRADE_SOL, DEFAULTS.maxTradeSol),
|
|
15
|
+
maxDailySol: num(env.POTSWAP_MAX_DAILY_SOL, DEFAULTS.maxDailySol),
|
|
16
|
+
maxSlippageBps: num(env.POTSWAP_MAX_SLIPPAGE_BPS, DEFAULTS.maxSlippageBps),
|
|
17
|
+
dryRun: /^(1|true|yes)$/i.test(env.POTSWAP_DRY_RUN ?? ''),
|
|
18
|
+
privateKey: env.SOLANA_PRIVATE_KEY || undefined,
|
|
19
|
+
keypairPath: env.SOLANA_KEYPAIR_PATH || undefined,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export const SOL_MINT = 'So11111111111111111111111111111111111111112';
|
|
23
|
+
export const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
|
24
|
+
export const NO_WALLET = 'Wallet not configured. Set SOLANA_PRIVATE_KEY or SOLANA_KEYPAIR_PATH. No funds were debited.';
|
package/dist/format.d.ts
ADDED
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}`;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Potswap MCP server over stdio, or `--daemon` to run strategies headlessly (PRD P11.11).
|
|
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
|
+
import { createRuntime } from './runtime.js';
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
const daemon = process.argv.includes('--daemon');
|
|
10
|
+
const rt = createRuntime(config);
|
|
11
|
+
if (daemon) {
|
|
12
|
+
if (!rt.scheduler) {
|
|
13
|
+
log(`potswap-mcp v${VERSION}: another scheduler (pid ${rt.lockOwner}) already runs on ${rt.store.location}; exiting.`);
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
rt.scheduler.start();
|
|
17
|
+
log(`potswap-mcp v${VERSION} daemon · state=${rt.store.location} · maxTradeSol=${config.maxTradeSol} · maxDailySol=${config.maxDailySol}${config.dryRun ? ' · DRY RUN' : ''}${rt.signer ? ` · wallet=${rt.signer.publicKey}` : ' · no wallet (strategies cannot execute)'}`);
|
|
18
|
+
const shutdown = async () => { log('stopping after the in-flight leg…'); await rt.scheduler.stop(); rt.release(); process.exit(0); };
|
|
19
|
+
process.on('SIGINT', shutdown);
|
|
20
|
+
process.on('SIGTERM', shutdown);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const server = createServer({ config, signer: rt.signer, strategyStore: rt.store, scheduler: rt.scheduler ?? undefined });
|
|
24
|
+
rt.scheduler?.start();
|
|
25
|
+
const transport = new StdioServerTransport();
|
|
26
|
+
await server.connect(transport);
|
|
27
|
+
log(`potswap-mcp v${VERSION} ready · api=${config.apiBase} · maxTradeSol=${config.maxTradeSol} · maxSlippageBps=${config.maxSlippageBps}${config.dryRun ? ' · DRY RUN' : ''}${rt.signer ? '' : ' · no wallet (read-only)'}${rt.scheduler ? ' · scheduler on' : ` · scheduler owned by pid ${rt.lockOwner}`}`);
|
|
28
|
+
process.on('exit', () => rt.release());
|
|
29
|
+
}
|
package/dist/limits.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/redact.d.ts
ADDED
|
@@ -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,15 @@
|
|
|
1
|
+
import type { Config } from './config.js';
|
|
2
|
+
import { type Signer } from './wallet.js';
|
|
3
|
+
import { type StrategyStore } from './strategies/store.js';
|
|
4
|
+
import { Scheduler } from './strategies/scheduler.js';
|
|
5
|
+
import type { BalancesFn, TradeContext } from './trade.js';
|
|
6
|
+
export interface Runtime {
|
|
7
|
+
signer: Signer | null;
|
|
8
|
+
store: StrategyStore;
|
|
9
|
+
scheduler: Scheduler | null;
|
|
10
|
+
lockOwner: number | null;
|
|
11
|
+
trade: TradeContext;
|
|
12
|
+
release(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createRuntime(config: Config): Runtime;
|
|
15
|
+
export declare function rpcBalances(rpcUrl: string): BalancesFn;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Wires config → signer, API, balances, strategy store, lock and scheduler. Used by index.ts (both modes).
|
|
2
|
+
import { Connection, PublicKey } from '@solana/web3.js';
|
|
3
|
+
import { PotswapApi } from './api.js';
|
|
4
|
+
import { loadSigner } from './wallet.js';
|
|
5
|
+
import { FileStore, defaultStateDir } from './strategies/store.js';
|
|
6
|
+
import { acquireLock } from './strategies/lock.js';
|
|
7
|
+
import { Scheduler } from './strategies/scheduler.js';
|
|
8
|
+
export function createRuntime(config) {
|
|
9
|
+
const signer = loadSigner(config);
|
|
10
|
+
const api = new PotswapApi(config.apiBase);
|
|
11
|
+
const balances = rpcBalances(config.rpcUrl);
|
|
12
|
+
const trade = { api, config, signer, balances };
|
|
13
|
+
const dir = defaultStateDir();
|
|
14
|
+
const store = new FileStore(dir);
|
|
15
|
+
const lock = acquireLock(dir);
|
|
16
|
+
const scheduler = lock.acquired ? new Scheduler({ store, trade, maxDailySol: config.maxDailySol }) : null;
|
|
17
|
+
return { signer, store, scheduler, lockOwner: lock.ownerPid, trade, release: () => lock.release() };
|
|
18
|
+
}
|
|
19
|
+
export function rpcBalances(rpcUrl) {
|
|
20
|
+
const SOL = 'So11111111111111111111111111111111111111112';
|
|
21
|
+
return async (wallet) => {
|
|
22
|
+
const conn = new Connection(rpcUrl, 'confirmed');
|
|
23
|
+
const owner = new PublicKey(wallet);
|
|
24
|
+
const out = {};
|
|
25
|
+
const [lamports, spl, t22] = await Promise.all([
|
|
26
|
+
conn.getBalance(owner),
|
|
27
|
+
conn.getParsedTokenAccountsByOwner(owner, { programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') }),
|
|
28
|
+
conn.getParsedTokenAccountsByOwner(owner, { programId: new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb') }).catch(() => ({ value: [] })),
|
|
29
|
+
]);
|
|
30
|
+
out[SOL] = lamports / 1e9;
|
|
31
|
+
for (const { account } of [...spl.value, ...t22.value]) {
|
|
32
|
+
const info = account.data.parsed?.info;
|
|
33
|
+
const ui = Number(info?.tokenAmount?.uiAmount ?? 0);
|
|
34
|
+
if (info && ui > 0)
|
|
35
|
+
out[info.mint] = (out[info.mint] ?? 0) + ui;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
};
|
|
39
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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 BalancesFn } from './trade.js';
|
|
5
|
+
import { type Signer } from './wallet.js';
|
|
6
|
+
import type { StrategyStore } from './strategies/store.js';
|
|
7
|
+
import type { Scheduler } from './strategies/scheduler.js';
|
|
8
|
+
export declare const VERSION = "0.2.0";
|
|
9
|
+
export interface ServerDeps {
|
|
10
|
+
config?: Config;
|
|
11
|
+
signer?: Signer | null;
|
|
12
|
+
fetchFn?: FetchFn;
|
|
13
|
+
/** Balance reader; defaults to an RPC connection. Injectable for tests. */
|
|
14
|
+
balances?: BalancesFn;
|
|
15
|
+
/** Strategy persistence and scheduler (P11); defaults are created from config when omitted. */
|
|
16
|
+
strategyStore?: StrategyStore;
|
|
17
|
+
scheduler?: Scheduler;
|
|
18
|
+
}
|
|
19
|
+
export declare function createServer(deps?: ServerDeps): McpServer;
|
|
20
|
+
export { SOL_MINT, USDC_MINT };
|