@potswap_io/mcp 0.1.0 → 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/README.md +114 -91
- package/dist/api.d.ts +4 -0
- package/dist/api.js +5 -3
- package/dist/config.d.ts +3 -0
- package/dist/config.js +2 -0
- package/dist/index.js +23 -5
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +39 -0
- package/dist/server.d.ts +8 -2
- package/dist/server.js +15 -63
- package/dist/shared/fee.d.ts +24 -0
- package/dist/shared/fee.js +42 -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/package.json +58 -55
package/README.md
CHANGED
|
@@ -1,91 +1,114 @@
|
|
|
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
|
|
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
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
|
68
|
-
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
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[]>;
|
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
|
-
|
|
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.
|
|
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
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ 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;
|
|
@@ -10,6 +12,7 @@ export interface Config {
|
|
|
10
12
|
export declare const DEFAULTS: {
|
|
11
13
|
apiBase: string;
|
|
12
14
|
maxTradeSol: number;
|
|
15
|
+
maxDailySol: number;
|
|
13
16
|
maxSlippageBps: number;
|
|
14
17
|
solReserve: number;
|
|
15
18
|
};
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const DEFAULTS = {
|
|
2
2
|
apiBase: 'https://www.potswap.io',
|
|
3
3
|
maxTradeSol: 1,
|
|
4
|
+
maxDailySol: 5,
|
|
4
5
|
maxSlippageBps: 300,
|
|
5
6
|
solReserve: 0.01,
|
|
6
7
|
};
|
|
@@ -11,6 +12,7 @@ export function loadConfig(env = process.env) {
|
|
|
11
12
|
apiBase,
|
|
12
13
|
rpcUrl: env.SOLANA_RPC_URL || `${apiBase}/api/rpc`,
|
|
13
14
|
maxTradeSol: num(env.POTSWAP_MAX_TRADE_SOL, DEFAULTS.maxTradeSol),
|
|
15
|
+
maxDailySol: num(env.POTSWAP_MAX_DAILY_SOL, DEFAULTS.maxDailySol),
|
|
14
16
|
maxSlippageBps: num(env.POTSWAP_MAX_SLIPPAGE_BPS, DEFAULTS.maxSlippageBps),
|
|
15
17
|
dryRun: /^(1|true|yes)$/i.test(env.POTSWAP_DRY_RUN ?? ''),
|
|
16
18
|
privateKey: env.SOLANA_PRIVATE_KEY || undefined,
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Potswap MCP server over stdio
|
|
2
|
+
// Potswap MCP server over stdio, or `--daemon` to run strategies headlessly (PRD P11.11).
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { createServer, VERSION } from './server.js';
|
|
5
5
|
import { loadConfig } from './config.js';
|
|
6
6
|
import { log } from './redact.js';
|
|
7
|
+
import { createRuntime } from './runtime.js';
|
|
7
8
|
const config = loadConfig();
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
}
|
|
@@ -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
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { type FetchFn } from './api.js';
|
|
3
3
|
import { SOL_MINT, USDC_MINT, type Config } from './config.js';
|
|
4
|
+
import { type BalancesFn } from './trade.js';
|
|
4
5
|
import { type Signer } from './wallet.js';
|
|
5
|
-
|
|
6
|
+
import type { StrategyStore } from './strategies/store.js';
|
|
7
|
+
import type { Scheduler } from './strategies/scheduler.js';
|
|
8
|
+
export declare const VERSION = "0.2.0";
|
|
6
9
|
export interface ServerDeps {
|
|
7
10
|
config?: Config;
|
|
8
11
|
signer?: Signer | null;
|
|
9
12
|
fetchFn?: FetchFn;
|
|
10
13
|
/** Balance reader; defaults to an RPC connection. Injectable for tests. */
|
|
11
|
-
balances?:
|
|
14
|
+
balances?: BalancesFn;
|
|
15
|
+
/** Strategy persistence and scheduler (P11); defaults are created from config when omitted. */
|
|
16
|
+
strategyStore?: StrategyStore;
|
|
17
|
+
scheduler?: Scheduler;
|
|
12
18
|
}
|
|
13
19
|
export declare function createServer(deps?: ServerDeps): McpServer;
|
|
14
20
|
export { SOL_MINT, USDC_MINT };
|
package/dist/server.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
// PRD P10 — the Potswap MCP server. Tools are thin, safe wrappers over the Potswap API.
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
-
import { Connection, PublicKey
|
|
3
|
+
import { Connection, PublicKey } from '@solana/web3.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { PotswapApi, ApiError } from './api.js';
|
|
6
6
|
import { loadConfig, NO_WALLET, SOL_MINT, USDC_MINT } from './config.js';
|
|
7
|
-
import {
|
|
8
|
-
import { explorer, fromRaw, toRaw } from './format.js';
|
|
7
|
+
import { buildQuote, executeSwap } from './trade.js';
|
|
9
8
|
import { redact, redactValue } from './redact.js';
|
|
10
9
|
import { loadSigner } from './wallet.js';
|
|
11
|
-
import { describeOrderError, friendlyError } from './shared/errors.js';
|
|
12
10
|
import { riskBadges, riskLevel } from './shared/risk.js';
|
|
13
|
-
|
|
11
|
+
import { registerStrategyTools } from './strategies/tools.js';
|
|
12
|
+
export const VERSION = '0.2.0';
|
|
14
13
|
const ok = (data, summary) => ({
|
|
15
14
|
content: [{ type: 'text', text: redact(summary ?? JSON.stringify(data, null, 2)) }], structuredContent: redactValue(data),
|
|
16
15
|
});
|
|
@@ -22,6 +21,7 @@ export function createServer(deps = {}) {
|
|
|
22
21
|
const signer = deps.signer !== undefined ? deps.signer : loadSigner(config);
|
|
23
22
|
const api = new PotswapApi(config.apiBase, deps.fetchFn);
|
|
24
23
|
const readBalances = deps.balances ?? rpcBalances(config.rpcUrl);
|
|
24
|
+
const trade = { api, config, signer, balances: readBalances };
|
|
25
25
|
const server = new McpServer({ name: 'potswap', version: VERSION });
|
|
26
26
|
const walletOr = (w) => w ?? signer?.publicKey ?? null;
|
|
27
27
|
server.registerTool('search_tokens', {
|
|
@@ -73,7 +73,7 @@ export function createServer(deps = {}) {
|
|
|
73
73
|
}, async (a) => {
|
|
74
74
|
try {
|
|
75
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}%)`);
|
|
76
|
+
return ok({ ...q }, `${a.amount} ${q.inputSymbol} → ${q.outAmount} ${q.outputSymbol} via ${q.router} (fee ${q.feeLabel ?? `${q.feeBps} bps`}, impact ${q.priceImpactPct}%)`);
|
|
77
77
|
}
|
|
78
78
|
catch (e) {
|
|
79
79
|
return fail(msg(e));
|
|
@@ -84,48 +84,14 @@ export function createServer(deps = {}) {
|
|
|
84
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
85
|
inputSchema: { ...quoteShape, confirm: z.boolean().default(false), maxSlippageBps: z.number().int().min(1).max(5000).optional() },
|
|
86
86
|
}, async (a) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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)));
|
|
87
|
+
const out = await executeSwap(trade, { inputMint: a.inputMint, outputMint: a.outputMint, amount: a.amount, slippageBps: a.slippageBps ?? a.maxSlippageBps, confirm: a.confirm });
|
|
88
|
+
switch (out.kind) {
|
|
89
|
+
case 'no-wallet': return fail(out.message);
|
|
90
|
+
case 'refused': return fail(out.message, { quote: out.quote });
|
|
91
|
+
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} via ${out.quote.router}`);
|
|
92
|
+
case 'dry-run': return ok({ dryRun: true, quote: out.quote }, `DRY RUN, nothing signed: ${a.amount} ${out.quote.inputSymbol} → ${out.quote.outAmount} ${out.quote.outputSymbol}`);
|
|
93
|
+
case 'failed': return fail(out.message, { signature: out.signature, explorerUrl: out.explorerUrl, quote: out.quote });
|
|
94
|
+
case 'ok': return ok({ ...out.result }, `Swapped ${a.amount} ${out.result.inputSymbol} → ${out.result.outAmount} ${out.result.outputSymbol} via ${out.result.router}. ${out.result.explorerUrl}`);
|
|
129
95
|
}
|
|
130
96
|
});
|
|
131
97
|
server.registerTool('get_balances', {
|
|
@@ -199,6 +165,7 @@ export function createServer(deps = {}) {
|
|
|
199
165
|
return fail(msg(e));
|
|
200
166
|
}
|
|
201
167
|
});
|
|
168
|
+
registerStrategyTools(server, { trade, store: deps.strategyStore, scheduler: deps.scheduler });
|
|
202
169
|
return server;
|
|
203
170
|
}
|
|
204
171
|
const quoteShape = {
|
|
@@ -207,21 +174,6 @@ const quoteShape = {
|
|
|
207
174
|
amount: z.number().positive().describe('Amount in UI units of the input token (1 = 1 SOL)'),
|
|
208
175
|
slippageBps: z.number().int().min(1).max(5000).optional().describe('Fixed slippage in bps; omit for auto'),
|
|
209
176
|
};
|
|
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
177
|
function slimToken(t) {
|
|
226
178
|
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
179
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface FeeSchedule {
|
|
2
|
+
standardBps: number;
|
|
3
|
+
sizeBps: number;
|
|
4
|
+
sizeThresholdUsd: number;
|
|
5
|
+
stakerDiscountBps: number;
|
|
6
|
+
/** POT that must be staked for the discount; null until TGE (discount disabled). */
|
|
7
|
+
stakerMinPot: number | null;
|
|
8
|
+
}
|
|
9
|
+
export declare const JUP_MIN_BPS = 50;
|
|
10
|
+
export declare const JUP_MAX_BPS = 255;
|
|
11
|
+
export declare const DEFAULT_FEE_SCHEDULE: FeeSchedule;
|
|
12
|
+
export type FeeTier = 'standard' | 'size' | 'staker';
|
|
13
|
+
export interface FeeDecision {
|
|
14
|
+
bps: number;
|
|
15
|
+
tier: FeeTier;
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* @param inputUsd USD value of the input amount at quote time; undefined when no price is known (→ standard tier)
|
|
20
|
+
* @param stakedPot POT staked by the wallet; 0 / undefined when unknown
|
|
21
|
+
*/
|
|
22
|
+
export declare function feeBpsFor(schedule: FeeSchedule, inputUsd?: number, stakedPot?: number): FeeDecision;
|
|
23
|
+
/** Parses the schedule from /api/config, falling back to defaults field by field. */
|
|
24
|
+
export declare function parseFeeSchedule(raw: unknown): FeeSchedule;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-shared.mjs from app/src/lib/fee.ts. Do not edit here.
|
|
2
|
+
// PRD P8.12–P8.15 — the fee schedule. One pure function shared by the web app and the MCP
|
|
3
|
+
// (mcp/src/shared/fee.ts is a generated copy). Jupiter allows 50–255 bps.
|
|
4
|
+
export const JUP_MIN_BPS = 50;
|
|
5
|
+
export const JUP_MAX_BPS = 255;
|
|
6
|
+
export const DEFAULT_FEE_SCHEDULE = {
|
|
7
|
+
standardBps: 250,
|
|
8
|
+
sizeBps: 100,
|
|
9
|
+
sizeThresholdUsd: 1000,
|
|
10
|
+
stakerDiscountBps: 100,
|
|
11
|
+
stakerMinPot: null,
|
|
12
|
+
};
|
|
13
|
+
const clamp = (bps) => Math.min(JUP_MAX_BPS, Math.max(JUP_MIN_BPS, Math.round(bps)));
|
|
14
|
+
/**
|
|
15
|
+
* @param inputUsd USD value of the input amount at quote time; undefined when no price is known (→ standard tier)
|
|
16
|
+
* @param stakedPot POT staked by the wallet; 0 / undefined when unknown
|
|
17
|
+
*/
|
|
18
|
+
export function feeBpsFor(schedule, inputUsd, stakedPot = 0) {
|
|
19
|
+
const size = inputUsd !== undefined && inputUsd >= schedule.sizeThresholdUsd;
|
|
20
|
+
let bps = size ? schedule.sizeBps : schedule.standardBps;
|
|
21
|
+
let tier = size ? 'size' : 'standard';
|
|
22
|
+
if (schedule.stakerMinPot !== null && stakedPot >= schedule.stakerMinPot) {
|
|
23
|
+
bps -= schedule.stakerDiscountBps;
|
|
24
|
+
tier = 'staker';
|
|
25
|
+
}
|
|
26
|
+
bps = clamp(bps);
|
|
27
|
+
const pct = `${bps / 100}%`;
|
|
28
|
+
const label = tier === 'standard' ? pct : tier === 'size' ? `${pct} (size tier)` : `${pct} (staker)`;
|
|
29
|
+
return { bps, tier, label };
|
|
30
|
+
}
|
|
31
|
+
/** Parses the schedule from /api/config, falling back to defaults field by field. */
|
|
32
|
+
export function parseFeeSchedule(raw) {
|
|
33
|
+
const r = (raw ?? {});
|
|
34
|
+
const num = (v, d) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
|
|
35
|
+
return {
|
|
36
|
+
standardBps: clamp(num(r.standardBps, DEFAULT_FEE_SCHEDULE.standardBps)),
|
|
37
|
+
sizeBps: clamp(num(r.sizeBps, DEFAULT_FEE_SCHEDULE.sizeBps)),
|
|
38
|
+
sizeThresholdUsd: num(r.sizeThresholdUsd, DEFAULT_FEE_SCHEDULE.sizeThresholdUsd),
|
|
39
|
+
stakerDiscountBps: num(r.stakerDiscountBps, DEFAULT_FEE_SCHEDULE.stakerDiscountBps),
|
|
40
|
+
stakerMinPot: typeof r.stakerMinPot === 'number' && r.stakerMinPot > 0 ? r.stakerMinPot : null,
|
|
41
|
+
};
|
|
42
|
+
}
|