@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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { NO_WALLET } from '../config.js';
|
|
3
|
+
import { redact, redactValue } from '../redact.js';
|
|
4
|
+
import { newId, SOL_MINT, nextRunAfter } from './engine.js';
|
|
5
|
+
import { findStrategy, runsOf } from './store.js';
|
|
6
|
+
const MINT = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/);
|
|
7
|
+
const MAX_STRATEGIES = Number(process.env.POTSWAP_MAX_STRATEGIES) || 10;
|
|
8
|
+
const ok = (data, summary) => ({ content: [{ type: 'text', text: redact(summary ?? JSON.stringify(data, null, 2)) }], structuredContent: redactValue(data) });
|
|
9
|
+
const fail = (message, extra = {}) => ({ content: [{ type: 'text', text: redact(message) }], structuredContent: redactValue({ error: message, ...extra }), isError: true });
|
|
10
|
+
export function registerStrategyTools(server, deps) {
|
|
11
|
+
const { trade } = deps;
|
|
12
|
+
if (!deps.store)
|
|
13
|
+
return; // strategies disabled (no state dir)
|
|
14
|
+
const store = deps.store;
|
|
15
|
+
const scheduler = deps.scheduler;
|
|
16
|
+
const now = () => Date.now();
|
|
17
|
+
const base = (kind, maxSlippageBps) => ({
|
|
18
|
+
id: newId(kind, now()), createdAt: new Date(now()).toISOString(), status: 'active', nextRunAt: new Date(now()).toISOString(),
|
|
19
|
+
consecutiveFailures: 0, totals: { runs: 0, volumeUsd: 0, feeUsd: 0, pointsAdded: 0 }, maxSlippageBps,
|
|
20
|
+
});
|
|
21
|
+
const guard = (confirm) => {
|
|
22
|
+
if (!trade.signer)
|
|
23
|
+
return fail(NO_WALLET);
|
|
24
|
+
if (!confirm)
|
|
25
|
+
return fail('Set confirm=true to create a strategy. Nothing was created and no funds were debited.');
|
|
26
|
+
if (store.load().strategies.filter((s) => s.status === 'active').length >= MAX_STRATEGIES)
|
|
27
|
+
return fail(`Limit of ${MAX_STRATEGIES} active strategies reached. Pause or delete one first.`);
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
const created = async (s, summary) => {
|
|
31
|
+
store.update((st) => { st.strategies.push(s); });
|
|
32
|
+
// First leg right away (P11.1) when a scheduler is attached.
|
|
33
|
+
if (scheduler)
|
|
34
|
+
await scheduler.tick();
|
|
35
|
+
const fresh = findStrategy(store.load(), s.id) ?? s;
|
|
36
|
+
return ok({ strategy: fresh, schedulerActive: !!scheduler?.active, runs: runsOf(store.load(), s.id, 3) }, `${summary}\nid=${s.id}${scheduler?.active ? '' : '\nNote: no scheduler is running in this process; run `npx @potswap_io/mcp --daemon` to execute strategies 24/7.'}`);
|
|
37
|
+
};
|
|
38
|
+
server.registerTool('dca_create', {
|
|
39
|
+
title: 'Create a DCA strategy',
|
|
40
|
+
description: 'Buy a fixed amount of a token on a schedule (first run immediately). Every leg pays the Potswap fee and earns POT points. Runs while this host is open, or 24/7 with `npx @potswap_io/mcp --daemon`.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
inputMint: MINT, outputMint: MINT,
|
|
43
|
+
amountPerRun: z.number().positive().describe('UI units of the input token per run (1 = 1 SOL)'),
|
|
44
|
+
intervalMinutes: z.number().int().min(5).max(60 * 24 * 30),
|
|
45
|
+
totalRuns: z.number().int().min(1).max(1000).optional(),
|
|
46
|
+
maxSlippageBps: z.number().int().min(1).max(5000).optional(),
|
|
47
|
+
confirm: z.boolean().default(false),
|
|
48
|
+
},
|
|
49
|
+
}, async (a) => {
|
|
50
|
+
const g = guard(a.confirm);
|
|
51
|
+
if (g)
|
|
52
|
+
return g;
|
|
53
|
+
if (a.inputMint === a.outputMint)
|
|
54
|
+
return fail('inputMint and outputMint must differ. Nothing was created.');
|
|
55
|
+
const s = { ...base('dca', a.maxSlippageBps), kind: 'dca', inputMint: a.inputMint, outputMint: a.outputMint, amountPerRun: a.amountPerRun, intervalMinutes: a.intervalMinutes, totalRuns: a.totalRuns, runsDone: 0 };
|
|
56
|
+
return created(s, `DCA created: ${a.amountPerRun} per run every ${a.intervalMinutes} min${a.totalRuns ? ` × ${a.totalRuns}` : ''}.`);
|
|
57
|
+
});
|
|
58
|
+
server.registerTool('rebalance_create', {
|
|
59
|
+
title: 'Create a rebalance strategy',
|
|
60
|
+
description: 'Keep the wallet at target percentages (2–6 assets incl. SOL). Checks on an interval; trades only when an asset drifts more than driftPct, routing through SOL.',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
targets: z.record(MINT, z.number().min(0).max(100)).describe('mint → percent; must sum to 100 and include SOL'),
|
|
63
|
+
driftPct: z.number().min(1).max(50).default(5),
|
|
64
|
+
checkIntervalMinutes: z.number().int().min(15).max(60 * 24 * 7).default(60),
|
|
65
|
+
minTradeUsd: z.number().min(1).default(5),
|
|
66
|
+
maxSlippageBps: z.number().int().min(1).max(5000).optional(),
|
|
67
|
+
confirm: z.boolean().default(false),
|
|
68
|
+
},
|
|
69
|
+
}, async (a) => {
|
|
70
|
+
const g = guard(a.confirm);
|
|
71
|
+
if (g)
|
|
72
|
+
return g;
|
|
73
|
+
const mints = Object.keys(a.targets);
|
|
74
|
+
const sum = Object.values(a.targets).reduce((x, y) => x + y, 0);
|
|
75
|
+
if (mints.length < 2 || mints.length > 6)
|
|
76
|
+
return fail('targets must list 2–6 assets. Nothing was created.');
|
|
77
|
+
if (Math.abs(sum - 100) > 0.5)
|
|
78
|
+
return fail(`targets must sum to 100 (got ${sum}). Nothing was created.`);
|
|
79
|
+
if (!(SOL_MINT in a.targets))
|
|
80
|
+
return fail('targets must include SOL (So11111111111111111111111111111111111111112). Nothing was created.');
|
|
81
|
+
const s = { ...base('rebalance', a.maxSlippageBps), kind: 'rebalance', targets: a.targets, driftPct: a.driftPct, checkIntervalMinutes: a.checkIntervalMinutes, minTradeUsd: a.minTradeUsd };
|
|
82
|
+
return created(s, `Rebalance created: ${mints.map((m) => `${m === SOL_MINT ? 'SOL' : m.slice(0, 4)} ${a.targets[m]}%`).join(' / ')}, drift ${a.driftPct}%, every ${a.checkIntervalMinutes} min.`);
|
|
83
|
+
});
|
|
84
|
+
server.registerTool('tpsl_create', {
|
|
85
|
+
title: 'Create a take-profit / stop-loss',
|
|
86
|
+
description: 'Sell a percentage of a position when its price reaches a take-profit, stop-loss or trailing-stop level (entry = price now). Checks every 30 s while a scheduler runs.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
mint: MINT, sellPct: z.number().min(1).max(100).default(100),
|
|
89
|
+
takeProfitPct: z.number().positive().optional(), stopLossPct: z.number().positive().max(99).optional(), trailingPct: z.number().positive().max(99).optional(),
|
|
90
|
+
exitMint: MINT.default(SOL_MINT), maxSlippageBps: z.number().int().min(1).max(5000).optional(),
|
|
91
|
+
confirm: z.boolean().default(false),
|
|
92
|
+
},
|
|
93
|
+
}, async (a) => {
|
|
94
|
+
const g = guard(a.confirm);
|
|
95
|
+
if (g)
|
|
96
|
+
return g;
|
|
97
|
+
if (a.takeProfitPct === undefined && a.stopLossPct === undefined && a.trailingPct === undefined)
|
|
98
|
+
return fail('Set at least one of takeProfitPct, stopLossPct, trailingPct. Nothing was created.');
|
|
99
|
+
if (a.mint === a.exitMint)
|
|
100
|
+
return fail('mint and exitMint must differ. Nothing was created.');
|
|
101
|
+
let entry;
|
|
102
|
+
try {
|
|
103
|
+
entry = (await trade.api.prices([a.mint]))[a.mint];
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
entry = undefined;
|
|
107
|
+
}
|
|
108
|
+
if (!entry)
|
|
109
|
+
return fail('No price available for this token right now; try again in a moment. Nothing was created.');
|
|
110
|
+
const s = { ...base('tpsl', a.maxSlippageBps), kind: 'tpsl', mint: a.mint, sellPct: a.sellPct, exitMint: a.exitMint, entryPrice: entry, highWater: entry, takeProfitPct: a.takeProfitPct, stopLossPct: a.stopLossPct, trailingPct: a.trailingPct, nextRunAt: null };
|
|
111
|
+
return created(s, `TP/SL created at entry ${entry}: ${[a.takeProfitPct && `TP +${a.takeProfitPct}%`, a.stopLossPct && `SL −${a.stopLossPct}%`, a.trailingPct && `trailing ${a.trailingPct}%`].filter(Boolean).join(', ')}, sells ${a.sellPct}%.`);
|
|
112
|
+
});
|
|
113
|
+
server.registerTool('strategy_list', { title: 'List strategies', description: 'All strategies with status, next run, totals and last result.', inputSchema: {} }, async () => {
|
|
114
|
+
const st = store.load();
|
|
115
|
+
const rows = st.strategies.map((s) => ({ ...s, lastRun: runsOf(st, s.id, 1)[0] ?? null }));
|
|
116
|
+
return ok({ schedulerActive: !!scheduler?.active, stateFile: store.location, strategies: rows }, rows.length ? rows.map((s) => `${s.id} ${s.kind} ${s.status}${s.pausedReason ? ` (${s.pausedReason})` : ''} next=${s.nextRunAt ?? '—'} runs=${s.totals.runs} fees=$${s.totals.feeUsd.toFixed(2)}`).join('\n') : 'No strategies yet.');
|
|
117
|
+
});
|
|
118
|
+
const byId = { id: z.string().min(3) };
|
|
119
|
+
server.registerTool('strategy_pause', { title: 'Pause a strategy', description: 'Stops future legs; keeps history.', inputSchema: byId }, async ({ id }) => mutate(id, (s) => { s.status = 'paused'; s.pausedReason = 'paused by user'; s.nextRunAt = null; }));
|
|
120
|
+
server.registerTool('strategy_resume', { title: 'Resume a strategy', description: 'Re-activates a paused strategy; the next run is scheduled from now.', inputSchema: byId }, async ({ id }) => mutate(id, (s) => { if (s.status === 'completed')
|
|
121
|
+
throw new Error('Completed strategies cannot be resumed; create a new one.'); s.status = 'active'; s.pausedReason = undefined; s.consecutiveFailures = 0; s.nextRunAt = s.kind === 'tpsl' ? null : nextRunAfter(s, now()); }));
|
|
122
|
+
server.registerTool('strategy_delete', { title: 'Delete a strategy', description: 'Removes the strategy; its run history is kept.', inputSchema: byId }, async ({ id }) => {
|
|
123
|
+
const existed = store.update((st) => { const n = st.strategies.length; st.strategies = st.strategies.filter((s) => s.id !== id); return n !== st.strategies.length; });
|
|
124
|
+
return existed ? ok({ deleted: id }, `Deleted ${id}.`) : fail(`No strategy ${id}.`);
|
|
125
|
+
});
|
|
126
|
+
server.registerTool('strategy_runs', { title: 'Strategy run log', description: 'Recent legs with amounts, signatures, fees, points, or the skip/fail reason.', inputSchema: { id: z.string().min(3), limit: z.number().int().min(1).max(200).default(20) } }, async ({ id, limit }) => {
|
|
127
|
+
const st = store.load();
|
|
128
|
+
if (!findStrategy(st, id))
|
|
129
|
+
return fail(`No strategy ${id}.`);
|
|
130
|
+
const runs = runsOf(st, id, limit);
|
|
131
|
+
return ok({ id, runs }, runs.length ? runs.map((r) => `${r.at} ${r.outcome}: ${r.action}${r.signature ? ` sig=${r.signature}` : ''}${r.message ? ` — ${r.message}` : ''}`).join('\n') : 'No runs yet.');
|
|
132
|
+
});
|
|
133
|
+
function mutate(id, fn) {
|
|
134
|
+
try {
|
|
135
|
+
const s = store.update((st) => { const cur = findStrategy(st, id); if (!cur)
|
|
136
|
+
return null; fn(cur); return cur; });
|
|
137
|
+
return s ? ok({ strategy: s }, `${s.id} is now ${s.status}.`) : fail(`No strategy ${id}.`);
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
return fail(e instanceof Error ? e.message : String(e));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type StrategyStatus = 'active' | 'paused' | 'completed';
|
|
2
|
+
interface Base {
|
|
3
|
+
id: string;
|
|
4
|
+
createdAt: string;
|
|
5
|
+
status: StrategyStatus;
|
|
6
|
+
pausedReason?: string;
|
|
7
|
+
nextRunAt: string | null;
|
|
8
|
+
consecutiveFailures: number;
|
|
9
|
+
totals: {
|
|
10
|
+
runs: number;
|
|
11
|
+
volumeUsd: number;
|
|
12
|
+
feeUsd: number;
|
|
13
|
+
pointsAdded: number;
|
|
14
|
+
};
|
|
15
|
+
maxSlippageBps?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface DcaStrategy extends Base {
|
|
18
|
+
kind: 'dca';
|
|
19
|
+
inputMint: string;
|
|
20
|
+
outputMint: string;
|
|
21
|
+
amountPerRun: number;
|
|
22
|
+
intervalMinutes: number;
|
|
23
|
+
totalRuns?: number;
|
|
24
|
+
runsDone: number;
|
|
25
|
+
}
|
|
26
|
+
export interface RebalanceStrategy extends Base {
|
|
27
|
+
kind: 'rebalance';
|
|
28
|
+
targets: Record<string, number>;
|
|
29
|
+
driftPct: number;
|
|
30
|
+
checkIntervalMinutes: number;
|
|
31
|
+
minTradeUsd: number;
|
|
32
|
+
}
|
|
33
|
+
export interface TpSlStrategy extends Base {
|
|
34
|
+
kind: 'tpsl';
|
|
35
|
+
mint: string;
|
|
36
|
+
sellPct: number;
|
|
37
|
+
exitMint: string;
|
|
38
|
+
entryPrice: number;
|
|
39
|
+
highWater: number;
|
|
40
|
+
takeProfitPct?: number;
|
|
41
|
+
stopLossPct?: number;
|
|
42
|
+
trailingPct?: number;
|
|
43
|
+
}
|
|
44
|
+
export type Strategy = DcaStrategy | RebalanceStrategy | TpSlStrategy;
|
|
45
|
+
export type RunOutcome = 'ok' | 'skipped' | 'failed' | 'dry-run' | 'no-op' | 'completed';
|
|
46
|
+
export interface Run {
|
|
47
|
+
id: string;
|
|
48
|
+
strategyId: string;
|
|
49
|
+
at: string;
|
|
50
|
+
action: string;
|
|
51
|
+
outcome: RunOutcome;
|
|
52
|
+
message?: string;
|
|
53
|
+
signature?: string;
|
|
54
|
+
inAmount?: number;
|
|
55
|
+
inputSymbol?: string;
|
|
56
|
+
outAmount?: number;
|
|
57
|
+
outputSymbol?: string;
|
|
58
|
+
feeUsd?: number;
|
|
59
|
+
pointsAdded?: number;
|
|
60
|
+
solEquivalent?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface StrategyState {
|
|
63
|
+
version: 1;
|
|
64
|
+
strategies: Strategy[];
|
|
65
|
+
runs: Run[];
|
|
66
|
+
}
|
|
67
|
+
export declare const EMPTY_STATE: StrategyState;
|
|
68
|
+
export {};
|
package/dist/trade.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { PotswapApi } from './api.js';
|
|
2
|
+
import { type Config } from './config.js';
|
|
3
|
+
import type { Signer } from './wallet.js';
|
|
4
|
+
export type BalancesFn = (wallet: string) => Promise<Record<string, number>>;
|
|
5
|
+
export interface TradeContext {
|
|
6
|
+
api: PotswapApi;
|
|
7
|
+
config: Config;
|
|
8
|
+
signer: Signer | null;
|
|
9
|
+
balances: BalancesFn;
|
|
10
|
+
}
|
|
11
|
+
export interface SwapArgs {
|
|
12
|
+
inputMint: string;
|
|
13
|
+
outputMint: string;
|
|
14
|
+
amount: number;
|
|
15
|
+
slippageBps?: number;
|
|
16
|
+
confirm?: boolean;
|
|
17
|
+
/** Extra SOL-equivalent already spent in the current window (strategies' daily cap). */
|
|
18
|
+
extraSolCap?: {
|
|
19
|
+
remainingSol: number;
|
|
20
|
+
label: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export interface Quote {
|
|
24
|
+
inputMint: string;
|
|
25
|
+
inputSymbol: string;
|
|
26
|
+
outputMint: string;
|
|
27
|
+
outputSymbol: string;
|
|
28
|
+
inAmount: number;
|
|
29
|
+
inAmountRaw: string;
|
|
30
|
+
outAmount: number;
|
|
31
|
+
price: number;
|
|
32
|
+
priceImpactPct?: number;
|
|
33
|
+
slippageBps?: number;
|
|
34
|
+
feeBps: number;
|
|
35
|
+
feeTier: string | null;
|
|
36
|
+
feeLabel: string | null;
|
|
37
|
+
scheduledFeeBps: number;
|
|
38
|
+
inputUsd?: number;
|
|
39
|
+
router: string;
|
|
40
|
+
requestId: string;
|
|
41
|
+
expiresAt: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface SwapOk {
|
|
44
|
+
signature: string;
|
|
45
|
+
explorerUrl: string;
|
|
46
|
+
inAmount: number;
|
|
47
|
+
inputSymbol: string;
|
|
48
|
+
outAmount: number;
|
|
49
|
+
outputSymbol: string;
|
|
50
|
+
router: string;
|
|
51
|
+
feeBps: number;
|
|
52
|
+
feeUsd?: number;
|
|
53
|
+
pointsAdded?: number;
|
|
54
|
+
solEquivalent?: number;
|
|
55
|
+
}
|
|
56
|
+
export type SwapOutcome = {
|
|
57
|
+
kind: 'no-wallet';
|
|
58
|
+
message: string;
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'refused';
|
|
61
|
+
message: string;
|
|
62
|
+
quote: Quote;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'quote-only';
|
|
65
|
+
quote: Quote;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'dry-run';
|
|
68
|
+
quote: Quote;
|
|
69
|
+
} | {
|
|
70
|
+
kind: 'failed';
|
|
71
|
+
message: string;
|
|
72
|
+
quote?: Quote;
|
|
73
|
+
signature?: string;
|
|
74
|
+
explorerUrl?: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'ok';
|
|
77
|
+
result: SwapOk;
|
|
78
|
+
quote: Quote;
|
|
79
|
+
};
|
|
80
|
+
export declare function buildQuote(api: PotswapApi, a: {
|
|
81
|
+
inputMint: string;
|
|
82
|
+
outputMint: string;
|
|
83
|
+
amount: number;
|
|
84
|
+
slippageBps?: number;
|
|
85
|
+
}): Promise<Quote>;
|
|
86
|
+
export declare function executeSwap(ctx: TradeContext, a: SwapArgs): Promise<SwapOutcome>;
|
package/dist/trade.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Swap execution shared by the `swap` tool (P10) and the strategy scheduler (P11).
|
|
2
|
+
// One path: quote → limits → fresh order as taker → local signature → execute.
|
|
3
|
+
import { VersionedTransaction } from '@solana/web3.js';
|
|
4
|
+
import { NO_WALLET, SOL_MINT } from './config.js';
|
|
5
|
+
import { checkLimits } from './limits.js';
|
|
6
|
+
import { explorer, fromRaw, toRaw } from './format.js';
|
|
7
|
+
import { describeOrderError, friendlyError } from './shared/errors.js';
|
|
8
|
+
import { feeBpsFor } from './shared/fee.js';
|
|
9
|
+
export async function buildQuote(api, a) {
|
|
10
|
+
const [inTok, outTok, cfg, px] = await Promise.all([
|
|
11
|
+
api.token(a.inputMint), api.token(a.outputMint), api.config(), api.prices([a.inputMint]).catch(() => ({})),
|
|
12
|
+
]);
|
|
13
|
+
const inAmountRaw = toRaw(a.amount, inTok.decimals);
|
|
14
|
+
// PRD P8.12–P8.15: the fee tier depends on the USD value of the input at quote time.
|
|
15
|
+
const inputUsd = px[a.inputMint] ? a.amount * px[a.inputMint] : undefined;
|
|
16
|
+
const feeDecision = feeBpsFor(cfg.fee, inputUsd);
|
|
17
|
+
const o = await api.order({ inputMint: a.inputMint, outputMint: a.outputMint, amount: inAmountRaw, slippageBps: a.slippageBps, feeBps: feeDecision.bps });
|
|
18
|
+
if (o.errorMessage && !o.outAmount)
|
|
19
|
+
throw new Error(`${describeOrderError(o)} No funds were debited.`);
|
|
20
|
+
const outAmount = fromRaw(o.outAmount, outTok.decimals);
|
|
21
|
+
return {
|
|
22
|
+
inputMint: a.inputMint, inputSymbol: inTok.symbol, outputMint: a.outputMint, outputSymbol: outTok.symbol,
|
|
23
|
+
inAmount: a.amount, inAmountRaw, outAmount, price: outAmount / a.amount,
|
|
24
|
+
priceImpactPct: o.priceImpactPct !== undefined ? Math.abs(Number(o.priceImpactPct)) : undefined,
|
|
25
|
+
slippageBps: o.slippageBps, feeBps: o.platformFee?.feeBps ?? o.feeBps ?? 0,
|
|
26
|
+
feeTier: cfg.referralAccount ? feeDecision.tier : null, feeLabel: cfg.referralAccount ? feeDecision.label : null,
|
|
27
|
+
scheduledFeeBps: feeDecision.bps, inputUsd, router: o.router, requestId: o.requestId, expiresAt: o.expireAt ?? null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function executeSwap(ctx, a) {
|
|
31
|
+
const { api, config, signer } = ctx;
|
|
32
|
+
if (!signer)
|
|
33
|
+
return { kind: 'no-wallet', message: NO_WALLET };
|
|
34
|
+
try {
|
|
35
|
+
const quote = await buildQuote(api, a);
|
|
36
|
+
const prices = await api.prices([SOL_MINT, a.inputMint]).catch(() => ({}));
|
|
37
|
+
let balanceUi;
|
|
38
|
+
try {
|
|
39
|
+
balanceUi = (await ctx.balances(signer.publicKey))[a.inputMint] ?? 0;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
balanceUi = undefined;
|
|
43
|
+
}
|
|
44
|
+
const inputUsd = prices[a.inputMint] ? a.amount * prices[a.inputMint] : undefined;
|
|
45
|
+
const solEquivalent = a.inputMint === SOL_MINT ? a.amount : inputUsd !== undefined && prices[SOL_MINT] ? inputUsd / prices[SOL_MINT] : undefined;
|
|
46
|
+
const refusal = checkLimits({
|
|
47
|
+
amountUi: a.amount, inputMint: a.inputMint, inputSymbol: quote.inputSymbol, isSol: a.inputMint === SOL_MINT,
|
|
48
|
+
inputUsd, solUsd: prices[SOL_MINT], slippageBps: a.slippageBps,
|
|
49
|
+
maxTradeSol: config.maxTradeSol, maxSlippageBps: config.maxSlippageBps, balanceUi,
|
|
50
|
+
});
|
|
51
|
+
if (refusal)
|
|
52
|
+
return { kind: 'refused', message: refusal, quote };
|
|
53
|
+
if (a.extraSolCap && solEquivalent !== undefined && solEquivalent > a.extraSolCap.remainingSol) {
|
|
54
|
+
return { kind: 'refused', message: `${a.extraSolCap.label} No funds were debited.`, quote };
|
|
55
|
+
}
|
|
56
|
+
if (!a.confirm)
|
|
57
|
+
return { kind: 'quote-only', quote };
|
|
58
|
+
if (config.dryRun)
|
|
59
|
+
return { kind: 'dry-run', quote };
|
|
60
|
+
const order = await api.order({ inputMint: a.inputMint, outputMint: a.outputMint, amount: quote.inAmountRaw, taker: signer.publicKey, slippageBps: a.slippageBps, feeBps: quote.scheduledFeeBps });
|
|
61
|
+
if (!order.transaction)
|
|
62
|
+
return { kind: 'failed', message: `${describeOrderError(order)} No funds were debited.`, quote };
|
|
63
|
+
const tx = VersionedTransaction.deserialize(Buffer.from(order.transaction, 'base64'));
|
|
64
|
+
signer.sign(tx);
|
|
65
|
+
const res = await api.execute(Buffer.from(tx.serialize()).toString('base64'), order.requestId);
|
|
66
|
+
if (res.status !== 'Success') {
|
|
67
|
+
return { kind: 'failed', message: friendlyError(res.error || `Swap failed (code ${res.code})`), quote, signature: res.signature, explorerUrl: res.signature ? explorer(res.signature) : undefined };
|
|
68
|
+
}
|
|
69
|
+
const outDec = (await api.token(a.outputMint)).decimals;
|
|
70
|
+
const outAmount = fromRaw(res.totalOutputAmount ?? order.outAmount, outDec);
|
|
71
|
+
const feeBps = order.platformFee?.feeBps ?? order.feeBps ?? 0;
|
|
72
|
+
const feeUsd = inputUsd !== undefined ? inputUsd * feeBps / 10_000 : undefined;
|
|
73
|
+
const cfg = await api.config();
|
|
74
|
+
const result = {
|
|
75
|
+
signature: res.signature, explorerUrl: explorer(res.signature), inAmount: a.amount, inputSymbol: quote.inputSymbol,
|
|
76
|
+
outAmount, outputSymbol: quote.outputSymbol, router: order.router, feeBps, feeUsd, pointsAdded: cfg.referralAccount ? feeUsd : 0, solEquivalent,
|
|
77
|
+
};
|
|
78
|
+
return { kind: 'ok', result, quote };
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
return { kind: 'failed', message: friendlyError(e instanceof Error ? e.message : String(e)) };
|
|
82
|
+
}
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,55 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@potswap_io/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Potswap MCP server: best-route Solana swaps for AI agents, non-custodial, earns POT.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"potswap-mcp": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"main": "dist/server.js",
|
|
11
|
-
"types": "dist/server.d.ts",
|
|
12
|
-
"files": [
|
|
13
|
-
"dist",
|
|
14
|
-
"README.md",
|
|
15
|
-
"LICENSE"
|
|
16
|
-
],
|
|
17
|
-
"engines": {
|
|
18
|
-
"node": ">=20"
|
|
19
|
-
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "tsc -p tsconfig.json",
|
|
22
|
-
"dev": "tsx src/index.ts",
|
|
23
|
-
"test": "vitest run",
|
|
24
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
-
"sync-shared": "node scripts/sync-shared.mjs",
|
|
26
|
-
"prepublishOnly": "npm run sync-shared && npm run build"
|
|
27
|
-
},
|
|
28
|
-
"repository": {
|
|
29
|
-
"type": "git",
|
|
30
|
-
"url": "https://github.com/
|
|
31
|
-
},
|
|
32
|
-
"homepage": "https://www.potswap.io",
|
|
33
|
-
"keywords": [
|
|
34
|
-
"mcp",
|
|
35
|
-
"solana",
|
|
36
|
-
"swap",
|
|
37
|
-
"jupiter",
|
|
38
|
-
"ai-agent",
|
|
39
|
-
"potswap",
|
|
40
|
-
"model-context-protocol"
|
|
41
|
-
],
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
44
|
-
"@solana/web3.js": "^1.99.0",
|
|
45
|
-
"bs58": "^6.0.0",
|
|
46
|
-
"zod": "^4.6.7"
|
|
47
|
-
},
|
|
48
|
-
"devDependencies": {
|
|
49
|
-
"@types/node": "^26.6.5",
|
|
50
|
-
"tsx": "^4.23.15",
|
|
51
|
-
"typescript": "^7.0.2",
|
|
52
|
-
"vite": "^8.3.5",
|
|
53
|
-
"vitest": "^5.0.5"
|
|
54
|
-
}
|
|
55
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@potswap_io/mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Potswap MCP server: best-route Solana swaps, DCA / rebalance / take-profit strategies for AI agents, non-custodial, earns POT.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"potswap-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/server.js",
|
|
11
|
+
"types": "dist/server.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"dev": "tsx src/index.ts",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"sync-shared": "node scripts/sync-shared.mjs",
|
|
26
|
+
"prepublishOnly": "npm run sync-shared && npm run build"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/potswap-io/mcp.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://www.potswap.io",
|
|
33
|
+
"keywords": [
|
|
34
|
+
"mcp",
|
|
35
|
+
"solana",
|
|
36
|
+
"swap",
|
|
37
|
+
"jupiter",
|
|
38
|
+
"ai-agent",
|
|
39
|
+
"potswap",
|
|
40
|
+
"model-context-protocol"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
44
|
+
"@solana/web3.js": "^1.99.0",
|
|
45
|
+
"bs58": "^6.0.0",
|
|
46
|
+
"zod": "^4.6.7"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^26.6.5",
|
|
50
|
+
"tsx": "^4.23.15",
|
|
51
|
+
"typescript": "^7.0.2",
|
|
52
|
+
"vite": "^8.3.5",
|
|
53
|
+
"vitest": "^5.0.5"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/potswap-io/mcp/issues"
|
|
57
|
+
}
|
|
58
|
+
}
|